mercoledì 20 ottobre 2010

API - Pattern

Le classi per lavorare su pattern in Java sono:

  • Pattern e Matcher e String.split per lavorare su stringhe;
  • Formatter e Scanner per lavorare su stream.
Quando si cerca un determinato pattern all'interno di una stringa si possono usare le regular expression; per usarle si crea un Pattern con l'espressione da cercare, e poi si cerca su un testo:
    Pattern p = Pattern.compile("o."); // the expression
    Matcher m = p.matcher("ciao mondo"); // the source
    while(m.find()) {
        System.out.println(m.start() + " " + m.end() + " <" + m.group() + ">");
    }
stampa
    3 5 <o >
    6 8 <on>
I metodi Matcher.start() restituisce la posizione attuale nel Matcher (dove è stato trovato il match attuale), Matcher.end() restituisce la posizione finale, e Matcher.group() restituisce l'intero contenuto. Il metodo Matcher.group() permette anche di scegliere il sottogruppo ricercato, nel caso di espressioni regolari più complesse; guardiamo l'esempio seguente:
    Pattern p = Pattern.compile("(.)(o)(.)"); // the expression
    Matcher m = p.matcher("ciao mondo"); // the source
    while(m.find()) {
        System.out.println(m.start() + " " + m.end() + " <" + m.group(1) + " - " + m.group(2) + " - " + m.group(3) + ">"); 
    }
restituisce:
    2 5 <a - o -  >
    5 8 <m - o - n>
poiché l'espressione (.)(o)(.) trova tutte le posizioni dove abbiamo una 'o' circondata da due caratteri, e poi divide il risultato in tre gruppi, da cui 'a', 'o', ' ' in "ciao ".

Il passo successivo consiste nell'utilizzare i metacaratteri; disponiamo dei seguenti:
  • \d - trova i digits, cioè cifre da 0-9
  • \w - trova le word, cioè i caratteri (lettere (tra 'a' e 'z') e (tra 'A' e 'Z') e (tra 0 e 9) o '_')
  • \s - trova gli spazi (' ', '\n', '\r', '\t')
oppure possiamo usare le '[' e ']' per racchiudere range di caratteri, per esempio \d è uguale a [0-9] e \w è uguale a [a-zA-Z0-9_]; possiamo usare i quantificatori per indicare quanti elementi vogliamo cercare: se usiamo '+' indichiamo (almeno una), se usiamo '*' indichiamo (tra zero e infinito) e se usiamo '?' indichiamo (o zero o una); se usiamo [^abc] indichiamo (qualsiasi tranne 'a', 'b' o 'c'); se vogliamo indicare un carattere qualsiasi possiamo usare '.'; infine se vogliamo fare ricerche non golose usiamo '*?' al posto di '*' e '?+' al posto di '+'; la differenza tra golose e non è che le golose cercano il match più ampio, mentre le non golose cercano il minore; un esempio è
Pattern greedy: .*xx
Pattern non greedy: .*?xx
Stringa: yyxxxyxx
il match greedy (goloso) da come risultato "yyxxxyxx", mentre il match non greedy da come risultato yyxx e xyxx

Come Matcher, anche Scanner permette di fare ricerche, lavorando su Stream (o su File o su Stringhe):
    Scanner s = new Scanner("ciao mondo");
    String token;
    do {
        token = s.findInLine("(.)(o)(.)");
        System.out.println("found <" + token + ">");
    } while (token != null);

Scanner normalmente viene utilizzato per fare il "tokenizing"; l'alternativa è usare String.split(); vediamo intanto questo e poi come fare con Scanner:
    String [] str = "ciao mondo sono molto felice".split(".o.");
    for (String s : str)
        System.out.print(s + " - ");
che restituisce
ci -  -  -  - o  -  - felice - 
Mentre otteniamo la stessa funzione dello scanner, abbiamo spesso una funzionalità non richiesta: lui esegue il tokenizing di tutta la stringa prima di darci un output; quando questo non è il comportamento desiderato basta utilizzare lo Scanner (che funziona anche su Stream e File, e restituisce tipo primitivi):
    Scanner s = new Scanner("ciao 123 cmondo true sdada");
    while(s.hasNext()) {
    if (s.hasNextInt())
        System.out.print("Intero(" + s.nextInt() + "); ");
    else if (s.hasNextBoolean())
        System.out.print("Boolean(" + s.nextBoolean() + "); ");
    else
        System.out.print(s.next() + "; ");
    }
restituisce:
ciao; Intero(123); cmondo; Boolean(true); sdada; 

Abbiamo infine l'oggetto Formatter che ci fornisce i metodi printf e format che si comportano così:
...

Il codice è qui

API - File

In Java abbiamo due tipi di approccio ai file: considerarli come entità (quindi File e Directory) o considerarli come contenuto (quindi Writer e Reader).

La gestione dei file come entità avviene esclusivamente tramite l'oggetto File; i suoi costruttori permettono di riferirci a percorsi fisici (esistenti o no) e i suoi metodi permettono di leggere directory, creare file vuoti o directory, cancellare file o directory (vuote), rinominare file o directory:

    File f = new File("folder"); //punto ad un file sul file system
    if (!f.exists())
        f.mkdir(); //creo la directory

    File innerFile = new File(f, "file.txt");
    innerFile.createNewFile();

    f.delete(); //non funziona, ritorna false
    if (f.isDirectory() && f.listFiles().length > 0)  innerFile.renameTo(new File("." + File.separatorChar + "test.txt"));

        f.delete(); //ora funziona

in questo esempio vediamo come si crea una directory ( mkdir() ), un file vuoto ( createNewFile() ), come si cancella un file o una directory vuota ( delete() ), e come si rinominano (o spostano) i file e le directory ( renameTo(File f) ), come si controlla se un file o una directory esistono già ( exists() ), se il File è un file o una directory (isFile() e isDirectory() ) e infine come ottenere tutti i file dentro una directory ( listFiles() o list() ).
Questi indicati sono tutti i metodi che servono per l'esame, e di solito nella vita reale.

Le classi relative alla scrittura e lettura del contenuto dei file si dividono in Writer/OutputStream e Reader/InputStream; la differenza tra Writer e OutputStream è che il primo lavora su caratteri mentre il secondo lavora su byte.
Importante ricordare che System.in è un InputStream, e System.out è un PrintWriter;
Per la certificazione non serve conoscere gli Stream, ma sono necessari se si vuole usare la console e non si vuole usare la classe Console.

Le classi che utilizziamo sono:
FileWriter (accetta un File o una String) e fornisce i metodi write(), flush(), close().
BufferedWriter (accetta un Writer) e fornisce come FileWriter e in più newLine().
PrintWriter (accetta File, String, Writer e OutputStream) e fornisce i metodi di FileWriter e inoltre format(), printf(), print(), println().
FileReader (accetta un File o una String) e fornisce read().
BufferedReader (accetta un Reader) e fornisce read() e readLine();

Una classe che può essere utile conoscere (non per la certificazione, ma per se stessi) è InputStreamReader; questa classe è un Reader, e accetta un InputStream; in pratica fa da ponte tra i due sistemi e permette di usare BufferedReader su oggetti InputStream (che sono quasi tutti quelli restituiti dai flussi: System.in, Socket, Servlet, ...).

La lettura (da file, da flusso, da console) si può ottenere usando:

BufferedReader brF = new BufferedReader(new FileReader("nomefile.txt"));
BufferedReader brS = new BufferedReader(new InputStreamReader(socket.getInputStream())); //socket è qui per esempio, non esiste nel contesto
BufferedReader brC = new BufferedReader(new InputStreamReader(System.in));
quindi si può utilizzare brF.readLine(); poiché readLine restituisce null quando il flusso termina, si può usare il costrutto comune:
String res;
while ((res = brF.readLine()) != null) { /* fai qualcosa con res*/ }
La scrittura (ovunque, o su file in modalità append) si ottiene usando:
PrintWriter pwF = new PrintWriter("nomefile.txt");
PrintWriter pwFA = new PrintWriter(new FileWriter("nomefile.txt", true));
PrintWriter pwS = new PrintWriter(socket.getOutputStream()); //socket è qui per esempio, non esiste nel contesto
quindi si scrive usando pwF.println("ciao mondo"); o pwF.format("che valore ha %d?", 123);
è importante ricordare che se non si chiude il flusso e soprattutto se non si fa il flush() dopo aver scritto si rischia di non mandare realmente i dati.

API - Stringhe

Le API di Java relative alle Stringhe sono:

  • String
  • StringBuffer
  • StringBuilder
String
String a = "abc"; //crea "abc" nella pool
String b = new String("abc"); //crea una variabile String nell'heap e "abc" nella pool
Il secondo metodo crea una variabile in più; meglio il primo.

I metodi che bisogna conoscere per la certificazione sono:
  1. charAt() - restituisce il carattere all'indice indicato: "ciao mondo".charAt(2) torna 'a';
  2. concat() - equivalente all'operatore "+"
  3. equalsIgnoreCase() - equivalente a toUpperCase().equals( obj.toUpperCase() ) ;
  4. length() - restituisce la lunghezza della stringa
  5. replace() - sostituisce un carattere nella stringa con un altro
  6. substring() - restituisce una sottostringa a partire dall'indice (start) fino a (end-1)
  7. toLowerCase() - restituisce una stringa con tutti i caratteri minuscoli
  8. toString() - restituisce la stringa 
  9. toUpperCase() - restituisce una stringa con tutti i caratteri maiuscoli
  10. trim() - restituisce una stringa senza spazi ne prima ne dopo (quindi cancella tutti i '\n', ' ', '\r')  

API - Date e numeri

La parte relativa alle API di Date e Numeri in Java (per la certificazione Programmer) riguarda le seguenti classi:

  • Date
  • Calendar
  • Locale
  • DateFormat
  • NumberFormat
a queste possiamo aggiungere l'uso di TimeZone, per conoscenza personale.

Vediamo subito le potenzialità e funzionalità di ogni classe:


Date
La classe Date è quasi totalmente deprecata; viene utilizzata per fare da tramite tra Calendar e DateFormat; non usatela mai direttamente (se i metodi sono deprecati un motivo ci sarà).

martedì 19 ottobre 2010

Design Pattern: Factory Method

Factory Method
Il Factory Method è un Pattern creazionale, utilizzato per l'instanziazione di risorse a partire da un punto unico di accesso; la risorsa migliore per iniziare a utilizzarlo e guardare wikipedia.

La logica base del Factory Method è di fornire un'interfaccia per la creazione di un oggetto.
Creiamo un semplice Factory; iniziamo con il nostro prodotto:

class Prodotto {
    public String nome;
}
quindi con la nostra interfaccia che restituisce un prodotto:
public interface Factory {
    public abstract Prodotto getProdotto();
}
e infine la classe reale che produce il Prodotto effettivamente:
public class FactoryImpl implements Factory {
    public Prodotto getProdotto() {
        return new Prodotto();
    }
}

Un Client che vorrà utilizzare il nostro oggetto otterrà da un Server un Factory e chiamerà il metodo getProdotto() per ottenere una istanza reale.

Il codice reale si trova qui

Design Pattern - Command

Design Pattern: Command
Un primo sguardo a wikipedia può dirci tanto di questo pattern.

Gli oggetti portanti per il pattern sono:

  • Oggetto Command - caratterizzato da avere un tipo associato e dei parametri da trasportare
  • Oggetto CommandType - un tipo associato, normalmente implementato tramite enumeration.
L'oggetto viene creato (per esempio associandolo al Listener di un bottone) e inoltrato ad un sistema tramite Stream; il sistema esterno lo utilizza direttamente, senza dover interpretare stringhe o mantenere costanti numeriche.

Un buon uso del pattern si trova nella gestione dei comandi nei sistemi client-server.


Ed ecco anche del codice di esempio relativo al pattern

RMI

La documentazione principale per conoscere le RMI è: Tutorial Oracle Java

I principali problemi nell'uso dei Socket per la comunicazione tra Client e Server sono legati a:
  1. Si deve stabilire una porta su cui il Server ascolta (e il Client in qualche modo deve conoscerla).
  2. Si deve stabilire un protocollo tra i due sistemi (e l'implementazione è lasciata totalmente al programmatore).
  3. Non esistono logiche di programmazione ad oggetti: si ragiona su invio di dati (primitivi o Stringhe); se si vuole implementare una programmazione ad oggetti (vedi uso di Design Pattern Command si deve codificare manualmente.
  4. Non esistono controlli a compile time; Client e Server sono lasciati a loro stessi nella gestione dei tipi.
RMI (Remote Method Invocation) nasce come strato superiore, basato su Socket, per fornire ai programmatori la possibilità di lavorare in modalità "ad oggetti" su sistemi remoti.
L'aspetto negativo principale di RMI è legato all'overhead implicito nel suo uso:
  • Per rendere trasparente la comunicazione bisogna passare attraverso classi generate da Java (i cosiddetti stubs, che rallentano le prestazioni.
Le classi e le interfacce utilizzate per le RMI sono:

Vediamo come si utilizzano realmente:

Sistema RMI dal punto di vista Server
Primo passo da effettuare è definire una Interfaccia che definisca cosa possiamo eseguire sul nostro oggetto Server:
public interface OurServerObject extends Remote {
    public boolean isInsideServer() throws RemoteException;
    public Date getDate() throws RemoteException;
}
Punto importante da ricordare è che le nostre interfacce devono essere Remote e che ogni metodo definito deve poter lanciare RemoteException; questa eccezione (di tipo checked) può avvenire se:

  • Server non attivo, non raggiungibile, o la comunicazione con il Client è terminata
  • Sono avvenuti errori durante il marshaling di parametri o valori di ritorno
Si crea quindi una classe (reale, non astratta) che verrà utilizzata nel Server, ed implementerà la nostra interfaccia:
public class OurServerObjectImpl extends UnicastRemoteObject implements OurServerObject {
    public OurServerObjectImpl() throws RemoteException {
    }

    public boolean isInsideServer() throws RemoteException {
        return true;
    }
    public Date getDate() throws RemoteException {
        return Calendar.getInstance().getTime();
    }
}
Importante il costruttore vuoto perché verrà utilizzato nella creazione dell'oggetto (e deve poter lanciare l'eccezione RemoteException); la nostra classe verrà utilizzata per creare l'oggetto remoto che i Client utilizzeranno;
Creiamo infine una istanza di Server che si occuperà di avviare il registro RMI e di registrare il nostro oggetto:
public class Server {
    public static void main(String [] args) throws RemoteException, MalformedURLException {
        java.rmi.registry.LocateRegistry.createRegistry(1099); //porta di default

        OurServerObjectImpl obj = new OurServerObjectImpl();
        
        //registra l'oggetto nel registro
        Naming.rebind("OurFirstObject", obj);
    }
}
La classe Server verrà lanciata da linea di comando, creerà il nostro oggetto, lo inserirà nel registro RMI e infine...rimarrà in attesa che il sistema liberi le risorse (quindi non terminerà correttamente!!!)
Sistema RMI dal punto di vista Client
Un client può essere un qualsiasi computer (remoto e non) che viene considerato in una JVM separata; i requisiti per poter utilizzare il nostro sistema remoto sono:

  1. Conoscere l'indirizzo della macchina (come IP o come nome host)
  2. STOP!
Come vedete, usare un oggetto remoto non richiede praticamente niente; vediamo con una classe reale come accedere agli oggetti del Server:
public class Client {
public static void main(String[] args) throws MalformedURLException, RemoteException, NotBoundException {
System.out.println("Ottengo un riferimento al server");

//ottengo il riferimento
OurServerObject obj = (OurServerObject) Naming.lookup("OurFirstObject");

System.out.println("Siamo in un server ? -> " + obj.isInsideServer());
System.out.println("Ecco la data del server: " + obj.getDate());
}
}
Altre informazioni importanti su RMI:
Il sistema RMI è basato su un Server Multi-Thread; ogni chiamata al Server restituirà l'istanza dell'oggetto posizionato in memoria; questo significa che tocca al programmatore occuparsi di gestire ogni aspetto per rendere il sistema thread-safe.
Il nostro oggetto estendeva UnicastRemoteObject; se abbiamo un oggetto remoto che non può essere modificato in tal senso, possiamo inserire nel costruttore del nostro oggetto una chiamata al metodo UnicastRemoteObject.exportObject(this) e ricordarci di implementare correttamente i metodi Object.hashcode(), Object.equals(Object o) e Object.toString().
Dalla Java 1.5 in poi non c'è bisogno di creare, usando rmic gli stub e gli skeleton; il sistema li gestisce in maniera automatica; purtroppo, per la certificazione, è necessario crearli con rmic.
Abbiamo utilizzato i metodi remoti senza renderci conto di un particolare importantissimo: i parametri per i metodi remoti e i valori di ritorno devono essere di tipo Serializable; questo porta a varie conseguenze che verranno trattate nel paragrafo relativo a quella interfaccia.
Gli oggetti che inviamo (e riceviamo) possono essere:
  1. tipi primitivi - in questo caso viene fatta una copia (pass-by-value)
  2. oggetti - se non sono Serializable verrà lanciato NotSerializableException; viene passato per valore (come i tipi primitivi)
  3. oggetti remoti - viene mandato lo stub (quindi per riferimento)
In generale gli oggetti che creiamo sul Server (e dentro gli oggetti remoti) possono essere:
  • Remote
  • UnicastRemoteObject
  • Serializable
Gli oggetti Remote sono oggetti che vengono considerati remoti; gli unici metodi che possono essere chiamati da un client sono i metodi definiti in un interfaccia Remote.
Gli oggetti UnicastRemoteObject sono tutti oggetti che vivono sul server; sono gli unici oggetti che rimangono sul server, e che restituiscono un riferimento quando ci viene restituita una istanza dell'oggetto.

Ecco anche il codice relativo all'esempio di RMI.
Ecco anche il codice relativo all'esempio di RMI usando un Factory Method.