Guida Java 6: Annotations, StringBuilder, JAXP e Jconsole
In questa nuova guida di Java tratteremo diversi argomenti in modo esauriente e approfondito, suddividendoli in capitoli per facilitarne la comprensione.
Nel primo capitolo tratteremo i seguenti argomenti: Introduzione a Java Annotations; Tipi di Annotations; Creare e utilizzare nuove annotazioni; Estensione del Collection Framework; Import statici; Override del tipo di ritorno; StringBuilder; Formatter, formattazione del testo.
Nel secondo capitolo parleremo di: Array, Queue e typed collections; JAXP processare un documento XML; Reflection; Collection thread safe; Gestione delle eccezioni a Runtime e Gestione delle code.
Nel terzo e ultimo capitolo di questa guida parleremo di: Programmazione concorrente; Callable; Future e Future Task; Semaphore; Locking; Performance; Jconsole; Generazione dinamica proxy RMI.
Ricordiamo che questa guida è il seguito della precedente —> Guida Java: Interfacce funzionali, costruttori, espressioni Lambda e Streams dove abbiamo trattato: le interfacce funzionali in Java; i metodi e i costruttori in Java; la keyword :: in Java; introduzione alle espressioni Lambda; Stream e Lambda Expression in Java; Parallel programming: Fork/Join framework, in particolare spiegheremo: Sequential vs Parallel programming; Stream in Java: aspetti avanzati; Fork/Join Framework; implementare un algoritmo con il framework Fork-Join.
INTRODUZIONE A JAVA ANNOTATIONS
Possiamo definire un’annotation come un appunto che mettiamo per specificare qualcosa relativo al codice che stiamo scrivendo, un attributo particolare, un metodo o una classe che hanno delle peculiarità. Attraverso questo meccanismo siamo capaci di dare espressività al codice, di renderlo più leggibile agli occhi di altri sviluppatori ma soprattutto agli occhi del compilatore.
Le annotations, infatti, sono delle annotazioni per il compilatore (o per chi si occupa del deploy dell’applicazione) che, attraverso di esse avrà la possibilità di effettuare determinate operazioni.
Questo paradigma, oltre a rendere più espressivo il codice sorgente, permette una migliore manutenzione dello stesso. Utilizzando le opportune annotations, infatti, riusciremo ad evitare possibili errori di compilazione, oppure, meglio ancora, potremo delegare a strumenti esterni la configurazione dell’applicazione che stiamo scrivendo (nell’ultima parte dell’articolo vedremo le annotations in ambiente enterprise).
La stessa Sun definisce gli usi delle annotazioni come di seguito:
- Per informare il compilatore,
- Per processare a tempo di compilazione (o di deploy),
- Per processare a run time.
Le ultime due caratteristiche sono tipiche degli ambienti enterprise, che le possono utilizzare mediante reflection.
Annotazioni previste da JDK 1.5
Un’annotazione si presenta nella seguente forma:
@Autore(
name = “Pasquale Congiustì”,
company = “HTML.it”
)
class ClasseAnnotata() {
…
}
Ogni annotazione si presenta con il simbolo @
seguito dal nome dell’annotazione. Eventualmente può essere valorizzata con dei valori, tra parentesi tonde come coppia nome-valore. Essa precede la classe, il metodo o l’attributo che vogliamo annotare.
In questo esempio abbiamo annotato la classe con l’annotation Autore ed i due attributi name e company.
Nel prossimo capitolo vediamo le Annotations di default, fornite a partire dalla distribuzione J2SE 1.5. Si tratta di tre semplici annotazioni utili al compilatore.
Attenzione: utilizzare le annotazioni significherà che il codice non può essere compilato (e spesso anche eseguito) con versioni Java precedenti.
TIPI DI ANNOTATIONS
@Deprecated
L’annotazione @Deprecated viene utilizzata per specificare che l’elemento indicato è un elemento deprecato, cioè, attivo (per mantenere retrocompatibilità) ma non consigliato perché rimpiazzato da uno nuovo e supportato.
Esempio con un annotazione @Deprecated:
public class TestDeprecated {
@Deprecated
public void metodoA() {
System.out.println(“Questo metodo è DEPRECATO, usa metodoB().”);
}
public void metodoB() {
System.out.println(“Questo metodo è SUPPORTATO.”);
}
}
La compilazione di questa classe non darà alcun segnale, procederà tutto normalmente. Sarà la compilazione della classe che userà TestDeprecated a ricevere segnalazioni di warning dal compilatore quando viene utilizzato il metodo metodoA()
.
TestDeprecated td=new TestDeprecated();
Td.metodoA();
@Override
L’annotation @Override è probabilmente la più utile in quanto consente di evitare degli errori, che in fase di codifica spesso accadono. L’annotazione dice che l’elemento indicato è un elemento che fa l’override (sovrascrive) del relativo elemento, del genitore da cui eredita.
L’esempio ci permetterà di capire meglio.
Esempio di Override:
class A{
void metodo1(){
System.out.println(“Metodo 1”);
}
}
class B extends A{
@Override
void metodoo1(){
System.out.println(“Override A.metodo1()”);
}
}
Abbiamo la classe genitore A, che presenta un metodo, metodo1()
. Creiamo una classe B, erede di A. Vogliamo fare l’override di metodo1()
, quindi annotiamo il metodo presente nella classe B con l’annotazione @Override, indicando che il metodo annotato è un metodo che sovrascrive un metodo del genitore.
Se provate a compilare il codice, il compilatore vi restituirà un errore. Se notate, infatti, ho inserito un errore di battitura nel nome del metodo. Senza l’annotazione @Override la compilazione sarebbe andata a buon fine e non ci saremmo accorti dell’errore.
@SuppressWarning
L’annotazione @SuppressWarning è utile quando vogliamo sopprimere le indicazioni di warning da parte del compilatore, ad esempio, perché stiamo usando dei metodi deprecati.
Esempio di SuppressWarning:
@SuppressWarnings({“deprecation”})
public void usaMetodoDeprecato() {
TestDeprecated t = new TestDeprecated();
t.metodoA();
}
Pur usando dei metodi deprecated, al compilatore abbiamo segnalato di sopprimere i warning.
CREARE E UTILIZZARE NUOVE ANNOTAZIONI
Le annotazioni di default sono di sicuro interesse, in quanto permettono di migliorare alcuni aspetti nelle fasi di compilazione, facendo sì che lo sviluppatore possa fare a meno di preoccuparsi di alcuni potenziali errori di codifica.
JDK 5 permette allo sviluppatore di definire delle proprie annotazioni totalmente customizzabili. Si tratta semplicemente di definire il nome dell’annotazione e di alcune proprietà che la contraddistinguono.
La cosa che rende le annotations uno strumento davvero importante, tanto da alterare il tipico paradigma di programmazione, è la possibilità di effettuare introspezione del codice.
Con la reflection (vedi guida precedente) è possibile valutare a runtime quali annotations sono presenti (e quali valori hanno in esse) e quindi effettuare determinate operazioni. Possiamo pensare ad un framework che gestica la persistenza con un database, dove all’interno del codice sono presenti delle annotations che indicano come mappare attributi di classe su colonne di database. Oppure utilizzare uno strumento personalizzato per team di sviluppo per commentare opportunamente il codice, chi ne modifica i metodi, chi li crea e quando e così via, per mantenere traccia del lavoro svolto.
Per un esempio pratico sulla creazione delle Java Annotations, rimandiamo all’articolo dedicato, che sicuramente tratta meglio della guida l’argomento.
Dove usare le annotazioni
È chiaro come l’utilizzo opportuno di annotation sia capace di dare tanta espressività in più al codice prodotto. Il reale vantaggio che si ha è quando tale strumento viene affiancato da altri strumenti che fanno introspezione del codice e, in base ad esso, creano delle configurazioni per framework.
È proprio in queste situazioni, infatti, che le annotations hanno un notevole vantaggio. Prima citavamo il framework per la persistenza automatizzata, è il caso di Hibernate, che permette di mappare classi e tabelle attraverso descrittori XML. Ora, attraverso l’uso di annotation, non sarà più necessario creare dei descrittori di configurazione, bensì, adottare delle annotazioni che suggeriscano l’associazione direttamente all’interno del codice.
Mantenere descrittori di configurazione opportunamente allineati può essere un problema, in quanto si tratta di file esterni (generalmente XML), difficilmente leggibili dall’occhio umano.
È il caso anche degli EJB 3.0 che, dalle annotazioni, traggono un notevole vantaggio, rendendo più veloce e meno incline ad errori la produzioni di logica applicativa enterprise.
ESTENSIONE DEL COLLECTION FRAMEWORK
Nel precedente capitolo abbiamo visto come poter utilizzare il nuovo Collection Framework con i Generics associati. Vedremo in questo capitolo come creare una nostra classe che accetti Generics Type. Prima di fare ciò, vediamo però come si presenta l’interfaccia Collection a partire dalla versione jdk 1.5:
Esempio di Interfaccia Collection:
package java.util;
public interface Collection<E> extends Iterable<E> {
//..
Iterator<E> iterator();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
}
Abbiamo riportato solo i metodi che sono stati modificati dopo l’introduzione dei Generics. Per prima cosa vediamo come l’interfaccia faccia riferimento al parametro <E>
quando viene definita; lo stesso dicasi dell’interfaccia Iterable<E>
che estende (vedremo il suo uso nel ciclo for-each).
In questo modo si sta informando il compilatore che la classe utilizzerà un parametro di un tipo generico che, d’ora in poi (nei metodi e nel body della classe o interfaccia), verrà riferito come E. Ovviamente si sarebbe potuto utilizzare qualsiasi altro marker per definire il parametro, qui hanno scelto E (ed anche T più avanti nel codice) per definire tipi di cui necessitiamo astrazione (che verranno scelti dallo sviluppatore che utilizzerà questa classe).
Altra cosa che ci rimane da capire è <? extends E>
. Il significato è intuitivo comprenderlo: accetteremo parametri che estendono dalla classe E.
L’esempio ci chiarisce:
Esempio di utilizzo delle Collection:
Collection<Number> list = null;
Collection<Double> ld = null;
//…
list.addAll(ld);
Accettiamo come parametro solo una collezione, il cui tipo estende quello definito per la collezione stessa in fase di scrittura del codice (E = Number
, quindi accettiamo solo tipi che estendono Number).
A questo punto siamo pronti per definire la nostra struttura dati Generics. Non ci inventiamo nulla per non complicare troppo la comprensione dell’argomento. Creiamo una pila che accetti solo tipi numerici (classe Number) e derivati (tutte le classi wrapper).
Esempio di una struttura Generics:
package it.html.generics;
import java.util.Iterator;
import java.util.Stack;
public class Pila<N extends Number> implements Iterable<N>{
private Stack<N> list;
public Pila(){
list=new Stack<N>();
}
public void add(N element){
list.push(element);
}
public N remove(){
return list.pop();
}
boolean isEmpty(){
return list.isEmpty();
}
@Override
public Iterator<N> iterator() {
return list.iterator();
}
}
Definiamo la classe preoccupandoci di implementare l’interfaccia Iterable. Per non complicarci la vita con l’implementazione utilizziamo la classe Stack già fornita da java (definendo il parametro da utilizzare N
, che comunque non conosciamo). Siccome vogliamo che N
sia un numero definiamo la sua estensione dalla classe Number.
Codifichiamo quindi i metodi add
e remove
utilizzando come parametro N, che dinamicamente sarà scelto da chi utilizzerà questa classe. Infine implementiamo iterator()
aggiungendo l’annotazione @Override
per essere sicuri di sovrascrivere il metodo corretto.
Utilizza la pila vista in precedenza:
public static void main(String[] args) {
Pila<Integer> p2=new Pila<Integer>();
for(int i=0;i<50;i++){
p2.add(i);
}
//..
while(!p2.isEmpty()){
System.out.println(p2.remove());
}
//..
}
Lo spezzone di codice sopra effettua un semplice uso della pila, utilizzando i metodi add
e remove
. All’inizio definiamo il tipo, Integer (che estende Number) e dopo dovremo utilizzare solo tipi Integer. Nel primo for, per autoboxing, gli int vengono trasformati in Integer, nel while li stampiamo a video.
Come abbiamo visto, la definizione di Generics è piuttosto semplice e risulta davvero importante, soprattutto una volta che diventerà naturale utilizzarle. Quello che dobbiamo sottolineare è come a livello di bytecode non cambi assolutamente nulla. Quello che cambia è dare un indicazione al compilatore per effettuare dei controlli sui tipi e quindi evitare tanti possibili eccezioni di casting durante l’esecuzione, errori che altrimenti avremmo incontrato solo durante l’esecuzione del programma.
IMPORT STATICI
Uno dei principali obiettivi nella sviluppo delle ultime due versioni di Java è stato quello di portare delle migliorie e facilitazioni nella scrittura del codice e nella sua manutenzione ordinaria. L’introduzione degli import statici aggiunge delle piccole semplificazioni.
Iniziamo con il più classico degli esempi: l’utilizzo degli stream di output e di errore (System.out
e System.err
). Noi utilizziamo gli oggetti out e err sulla classe System in quanto oggetti dichiarati statici (e pubblici). Facciamo un ragionamento più astratto e parliamo di tutte le istanze statiche che utilizziamo. Durante la stesura di un programma, capita di invocare decine e decince di volte questi oggetti, facendo riferimento al package (java.lang è importato di default), alla classe e ai metodi o alle variabili da utilizzare.
L’import statico ci viene in aiuto, consentendoci di importare gli elementi statici ci permette quindi di citarne il riferimento, omettendo il namespace (appena importato). Nel nostro caso potremo importare staticamente gli elementi out
e err
della classe java.lang.System ed utilizzarli riferendoli direttamente:
Esempio di Import statico:
package it.html.tiger;
import static java.lang.System.out;
import static java.lang.System.err;
public class ImportTest {
public static void main(String[] args) {
//Chiamiamo direttamente la variabile out
out.println(“Salve!”);
//e la variabile err
err.println(“Esecuzione eccezionale”);
}
}
Ovviamente non si tratta di una grande rivoluzione, ma aiuta, e di molto, a ridurre il tempo di scrittura, in particolare quando abbiamo di fronte classi e metodi statici di utilità. Quanto detto, infatti, vale sia con variabili che con metodi statici. Pensiamo alle classi di utilità di java.lang.Math che ha decine di metodi statici, utilizzati per effettuare funzioni matematiche di base.
Import statico del metodo max presente nella libreria Math:
import static java.lang.Math.max;
//..
int y = max (10,33);
//..
Nel caso in cui il nome del metodo presenti più casi (per overload) l’import verrà esteso a tutti i metodi. Quando invece ci sono stessi nomi di metodi (statici), associati a diversi oggetti, varrà il principio di associazione che si basa sui parametri con cui chiamiamo la funzione.
C’è da dire che spesso vedremo la funzione di import statico insieme alla definizione e all’uso di enum (altra nuova caratteristica di cui parleremo in seguito).
L’import statico ha lo stesso significato (e sintassi) dell’import normale, con la differenza che questo è associato a tipi definiti statici. La presenza del carattere jolly (*), quindi, ha lo stesso significato dell’import normale. Nell’esempio che segue vediamo infatti come importare tutta la libreria Math e come richiamarne in maniera diretta i metodi:
Esempio di importazione della libreria Math:
import static java.lang.Math.*;
//..
int y = max (10,33);
int z = abs(-3);
double s = sqrt(99);
//..
OVERRIDE DEL TIPO DI RITORNO
Se in una versione di Java 1.4 provavate a modificare il tipo di ritorno di un metodo, in sovrascrittura dalla classe padre, ovviamente vi veniva notificato un errore di tipizzazione. Questo accade tuttora, a meno che il tipo di ritorno non sia un erede del tipo di ritorno atteso.
Creiamo una semplice gerarchia di oggetti, quelli tipici degli esercizi Java per spiegare l’ereditarietà, la classe Point e Point3D; la prima rappresenta le coordinate spaziali bidimensionali, la seconda ne estende il concetto aggiungendo la coordinata di profondità:
Esempio con la gerarchia di oggetti:
package it.html.covariantreturn;
public class Point {
int x;
int y;
public Point(int x,int y){
this.x=x;
this.y=y;
}
}
class Point3D extends Point{
int z;
public Point3D(int x,int y,int z){
super(x,y);
this.z=z;
}
}
Ora immaginiamo che una classe, Figure, utilizzi la classe Point, per rappresentare un generico punto, ed abbia un metodo onePoint()
che lo restituisca. Immaginiamo una classe Figure3D, erede di Figure che abbia lo stesso metodo ma che restituisca un Point3D. In Java 1.5 questo è lecito, in versioni precedenti no, eravamo forzati al tipo definito nella classe genitore.
Effettua l’override del metodo restituendo un tipo derivato da Point:
package it.html.covariantreturn;
public class Figure {
Point p = new Point(0,0);
//…
public Point onePoint(){
return p;
}
}
class Figure3D extends Figure{
Point3D p = new Point3D(0,0,0);
//…
@Override
public Point3D onePoint(){
return p;
}
}
La cosa si ripercuote positivamente nel client che non dovrà più effettuare operazioni di casting per ovviare a quanto succedeva in precedenza. C’è da dire che l’override del metodo di ritorno può esistere a patto che la classe utilizzata sia un erede della classe attesa, altrimenti il compilatore non ci penserà due volte a darvi una segnalazione di errore.
Per completezza vediamo una classe di prova:
Classe di prova dell’override:
package it.html.covariantreturn;
public class Main {
public static void main(String[] args) {
Figure f = new Figure();
Figure3D f2 = new Figure3D();
Point p = f.onePoint();
System.out.println(“[“+p.x+”, “+p.y+”]”);
Point3D p2 = f2.onePoint();
System.out.println(“[“+p2.x+”, “+p2.y+”, “+p2.z+”]”);
//In Java 1.4 avremmo dovuto fare:
Point p3 = (Point3D) f2.onePoint();
//con una possibile eccezione di casting
}
}
In fondo, come ultimo statement, abbiamo riportato il codice che si sarebbe dovuto utilizzare in ambiente pre Java 1.5. Anche in questo caso vediamo bene come le attenzioni delle ultime versioni di Java siano rivolte a evitare le noiose eccezioni di Runtime dovute a un eccessivo uso di casting e conversioni di tipi (vedi autoboxing e generics).
STRING-BUILDER
Nello sviluppo in Java si ha sempre a che fare con tipi di base matematici, logici e catene di testo. Sappiamo bene che in java la classe che si occupa di rappresentare un testo è la classe String (package java.lang) e probabilmente l’abbiamo utilizzata una miriade di volte senza porci troppi problemi alle performance derivanti dal suo uso.
La prima cosa da sottolineare è che l’oggetto String è un oggetto immutabile, cioè, una volta creato, non può essere modificato. Di fatto, nel momento in cui modifichiamo un oggetto String, in Java, ne stiamo creando un’altra istanza, rimpiazzando quella precedente. Gli stessi operatori di append (x+="aaa"
) non fanno altro che fare l’override dei metodi presenti nella classe StringBuffer con la quale la classe String viene gestita per questioni di performance.
Proprio per questo motivo in ambiente pre java 1.5 il modo più ovvio per gestire catene di testo mutabili era quello di usare direttamente la classe StringBuffer ed i relativi metodi per la gestione del testo (in particolare del metodo append e dei metodi replace). La classe è una threadsafe, il che consente di poter effettuare le operazioni sull’oggetto StringBuffer condividendolo tra diversi thread.
StringBuilder è identica a StringBuffer, stessi metodi stessa logica, unica differenza: non è threadsafe. Le performance migliorano in maniera netta, in particolare considerando che molti programmi fanno un notevole uso delle stringhe e della loro modifica durante il ciclo di vita del software. L’utilizzo, quindi, d’ora in poi dovrebbe essere scontato a favore della nuova classe introdotta, nel momento in cui non abbiamo da gestire aspetti legati all’accesso concorrente alla risorsa.
In ambiente enterprise ciò risulta sempre, quindi è evidente che in questo caso utilizzeremo sempre oggetti StringBuilder per la gestione di stringhe. Abbiamo misurato le prestazioni delle tre classi con un semplice esempio che potete effettuare voi stessi:
Test sull’uso delle Stringhe:
package it.html.tiger;
public class StringComparison {
/**
* @param args
*/
public static void main(String[] args) {
long start=System.currentTimeMillis();
testString();
long end=System.currentTimeMillis();
System.out.println(“Tempo di esecuzione testString() “+(end-start)+” millis.”);
start=System.currentTimeMillis();
testStringBuffer();
end=System.currentTimeMillis();
System.out.println(“Tempo di esecuzione testStringBuffer() “+(end-start)+” millis.”);
start=System.currentTimeMillis();
testStringBuilder();
end=System.currentTimeMillis();
System.out.println(“Tempo di esecuzione testStringBuilder() “+(end-start)+” millis.”);
}
private static void testString() {
String x = “”;
for(int i=0;i<15000;i++){
//operazione di append
x+=i;
}
}
private static void testStringBuffer() {
StringBuffer x = new StringBuffer(“”);
for(int i=0;i<15000;i++){
//operazione di append
x.append(i);
}
}
private static void testStringBuilder() {
StringBuilder x = new StringBuilder(“”);
for(int i=0;i<15000;i++){
//operazione di append
x.append(i);
}
}
}
Ecco il risultato ottenuto:
- Tempo di esecuzione
testString()
1656 millis. - Tempo di esecuzione
testStringBuffer()
16 millis. - Tempo di esecuzione
testStringBuilder()
0 millis.
FORMATTER: FORMATTAZIONE DEL TESTO
Uno dei problemi che spesso gli sviluppatori hanno è quello relativo alla gestione della formattazione del testo. Infatti, come sappiamo, la rappresentazione può differire in base alle diverse esigenze che possiamo avere nello sviluppo di un programma.
Seppure banali e ripetitive, queste situazioni, quando si manifestano, mettono a dura prova il miglior programmatore. Altro problema è legato al fatto che, non utilizzando una modalità standard, siamo costretti ogni volta a reinventare la stessa soluzione da zero.
java.util.Format è l’implementazione base che si incarica di gestire gli aspetti di conversione e rappresentazione, anche se spesso vedremo che non la utilizzeremo direttamente.
Primi esempi di formattazione del testo:
import java.util.Date;
import java.util.Formatter;
public class FormatterTest {
public static void main(String[] args) {
String name = “Pasquale”;
Date today = new Date();
double cifra=12.332334;
//Creiamo un oggetto formatter
Formatter formatter = new Formatter ();
formatter.format(“Buongiorno %s, sono le %tT”, name, today);
System.out.println(formatter);
Possiamo creare una nuova istanza della classe e, attraverso il metodo format, effettuare la formattazione desiderata. Il metodo format()
si aspetta una stringa e zero o n Objects (in modo da poter formattare un numero indefinito di parametri).
La notazione da usare è un segno di percentuale seguito da una serie di caratteri con cui diciamo al metodo come formattare. In questo caso stiamo formattando una stringa (%s
) e una data (%T
). La sintassi è piuttosto vasta ed è perfettamente spiegata nella documentazione delle API della classe. Proseguiamo con l’esempio:
//Usiamo in metodo format
System.out.format(“Buongiorno %s, sono le %tT”, name, today);
//Usiamo il metodo printf sullo stream out
System.out.printf(“nBuongiorno %s, sono le %tT”, name, today);
//Formattiamo una cifra
System.out.printf(“nCiao %s, stampo la cifra %.2f con 2 cifre decimali”, name, cifra );
//Possibilità di definire l’ordine dei parametri e di stamparli n volte
System.out.printf(“nCiao %2$2s, stampo la cifra %1$2.2f con 2 cifre decimali.” +
” Adios %2$2s”, cifra, name );
}
}
Come dicevamo precedentemente possiamo utilizzare diversi modi per scrivere la rappresentazione su un flusso di output. Infatti utilizziamo gli stessi parametri sul metodo format()
di System.out. Questo perché all’interno verrà usata la classe Format.
Nell’esempio vediamo come sia possibile utilizzare il metodo printf()
sulla variabile System.out. Gli esempi ci mostrano poi come poter formattare un decimale, troncando le cifre dopo la seconda e infine come sia possibile ordinare e utilizzare i parametri nella rappresentazione testuale.
Fino alla versione 1.4 di Java, a partire dall’oggetto System.in (InputStream), dovevamo costruirci la nostra rappresentazione utilizzando dei filtri (InputStreamReader, BufferedReader, …) ed effettuando gli opportuni controlli.
Java 1.5 presenta un’utilissima classe che già porta con sé tutti i metodi per la lettura e la conversione dei tipi (numerici o stringhe): java.util.Scanner.
Questa classe presenta una lunga serie di metodi, attraverso i quali possiamo gestire un input testuale in maniera ottimale senza preoccuparci delle tipiche operazioni di conversione (ad esempio facendo controlli sui tipi). Inoltre, utilizzando le Regular Expression, è possibile già definire il comportamento, quindi ad esempio stabilire delimitatori di testo in modo da recuperare solo quello di cui abbiamo bisogno.
L’esempio sotto è un programmino che chiede all’utente di digitare il nome del file da visualizzare e dopo legge il file mostrandolo all’utente.
Esempio di lettura di un parametro e di un file:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class TestScanner {
public static void main(String[] args) throws FileNotFoundException {
System.out.println(“Insert file name>>”);
Scanner scan=new Scanner(System.in);
String fileName=scan.next();
System.out.println(“Reading “+fileName);
printToScreen(fileName);
}
Nel main effettuiamo la prima richiesta creando un oggetto Scanner costruito con l’InputStream di default (la console). Ovviamente qui sarà possabile montare tutti i filtri immaginabili (per bufferizzare, cifrare, …). Segue la richiesta con il metodo next()
che legge un insieme di caratteri (inseriti dall’utente) e ne restituisce una stringa, dopodiché chiamiamo la routine printToScreen(...)
passando proprio tale valore.
Prima di proseguire diciamo che è molto interessante l’insieme di metodi esposto dalla classe Scanner. esistono i metodi nextInt()
, nextDouble()
che si occupano di convertire il valore passato dall’utente in tipo numerico. Questi metodi vanno di pari passo con il rispettivo metodo di controllo hasNext()
che restituisce un boolean dicendoci se esiste o meno il prossimo token (quindi avremo anche hasNextInt()
, hasNextDouble()
, ecc).
Bisogna utilizzare sempre i metodi nextXXX()
in coppia con hasNextXXX()
perché la conversione può facilmente causare eccezioni a runtime in base al valore che la classe cerca di convertire.
Seguiamo e vediamo come si presenta il metodo utilizzato per leggere il file (e stamparne il contenuto):
private static void printToScreen(String fileName) throws FileNotFoundException {
Scanner scan=new Scanner(new File(fileName));
while(scan.hasNext()){
String tmp=scan.next();
System.out.println(tmp);
}
}
}
All’interno del metodo creiamo un nuovo Scanner passando al costruttore un file. Effettuiamo poi un’iterazione e stampiamo il contenuto di ogni token. Abbiamo visto che uno Scanner presenta una facile interfaccia per interrogare uno stream di input. Vediamo ora un interessante metodo per effettuare la lettura di una pagina Web in maniera identica alla lettura di un file:
Lettura di una pagina Web:
private static void printURL() throws Exception {
URL url = new URL (“http://www.html.it/”);
Scanner scan=new Scanner(url.openConnection().getInputStream());
scan.useDelimiter(“\Z”);
while(scan.hasNext()){
String tmp = scan.next();
System.out.println(tmp);
}
}
In questo caso dobbiamo definire il delimitatore di fine documento (Z), oppure avremmo dovuto bufferizzare, poiché, altrimenti, l’implementazione recupera solo i primi 1024 byte nella lettura dello stream di rete. Per il resto tutto rimane identico (e abbastanza semplice).
Con la versione 1.6 di Java arriva un oggetto che finalmente virtualizza in tutto e per tutto una console testuale, la classe java.io.Console, appunto. Di fatto è una classe che può essere di utilità solo in applicazioni standalone e solo in virtual machine che hanno una console testuale. Il tipico modo di recuperare quest’oggetto è il metodo System.console()
ed è implementato solo in versioni della virtual machine che presentano una console di testo (e solo dalla shell di comando).
La classe è davvero interessante in quanto non dobbiamo preoccuparci di complicati gestori, di eccezioni, stream e quant’altro… molto utile per chi inizia a programmare. Inoltre c’è un metodo per chiedere la password che evita di scrivere l’echo di quello che stiamo digitando. Vediamo l’API:
public final class Console implements Flushable {
public PrintWriter writer() ;
public Reader reader();
public Console format(String fmt, Object…args);
public Console printf(String format, Object…args);
public String readLine(String fmt, Object…args);
public String readLine();
public char[] readPassword(String fmt, Object…args);
public char[] readPassword();
public void flush();
}
Il metodo più importante è senza dubbio readLine()
la cui chiamata è bloccante in attesa dell’input dell’utente. Il risultato sarà una stringa. I metodi printf()
e format()
effettuano una stampa sull’output della console, mentre readPassword()
restituisce un array che rappresenta i caratteri di input dell’utente. Questo metodo, come dicevamo, è stato codificato per evitare di mostrare il testo digitato (come in qualsiasi applicazione che chiede una password).
Vediamo un esempio concreto dove utilizziamo solo l’interfaccia Console per tutte le operazioni di input/output attraverso i metodi appena visti.
Esempio di utilizzo dell’interfaccia Console:
package it.html.j16;
import java.io.Console;
public class ConsoleTest {
public static void main(String[] args) {
//Recuperiamo l’istanza di Console
//associata a questa macchina virtuale
Console console = System.console();
//Utilizziamola per scrivere in output
console.printf(“Benvenuto alla console Java 1.6”);
if (args.length>0){
//Ora chiediamo un valore in input (notare il formato %s)
String line = console.readLine(“nPrego, %s, inserisci un testo >>”, args[0]);
//facciamo l’eco uppercase
console.printf(“Echo: “+line.toUpperCase());
char pwd[] = console.readPassword(“nPrego, %s, inserisci il PIN >>”, args[0]);
if (pinEsatto(pwd))
console.printf(“Bene! PIN CORRETTO!”);
else
//lo mostriamo giusto per vedere la rappresentazione
//altrimenti non avrebbe senso
console.printf(“PIN ERRATO!”);
}else{
//Mostro la sintassi
console.printf(“nPrego inserire un argomento: java it.html.j16.ConsoleTest yourName”);
}
}
private static boolean pinEsatto(char[] pwd) {
char pinEsatto[] = {‘H’,’T’,’M’,’L’,’.’,’I’,’T’};
//Usiamo l’utility della classe Arrays
return java.util.Arrays.equals(pwd, pinEsatto);
}
}
All’utente chiediamo due operazioni di scrittura, una richiesta di una stringa normale, giusto per effettuare una banale operazione di echo, ed una password. Sulla password effettuiamo un controllo e nel caso di pin positivo lo notifichiamo positivamente. Fate attenzione perché il codice qui non funzionerà se non eseguito da una shell di comando. Eseguire questa classe dal pannello di Eclipse, per esempio, darà un valore null alla chiamata System.console()
.
ARRAY, QUEUE E TYPED COLLECTIONS
A partire dalla versione Java 5 c’è l’introduzione di una serie nuova di realizzazioni concrete di Collection e la modifica alla gerarchia delle collezioni con l’aggiunta del tipo Queue. Il tipo Queue (da non confondersi con le Queue di JMS) era una di quelle strutture dati mancanti nella gerarchia delle collezioni, spesso utilizzata per la gestione delle politiche FIFO (First In First Out). Ora, grazie alla sua introduzione, la gerarchia si presenta così:
Queue è un nuovo tipo che eredita direttamente da Collection e presenta diverse implementazioni pratiche. Ai fini della logica della struttura dati i metodi offer()
e poll()
sono quelli fondamentali per l’inserimento e la rimozione di elementi secondo la logica FIFO.
Una classe di prova per effettuare test:
package it.html.java5;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Queue;
public class QueueTest {
public static void main(String[] args) {
System.out.println(“Ordinamento classico, secondo l’ordine d’arrivo”);
Queue<String> queue= new LinkedList<String>();
for (int i=0; i<50; i++){
queue.offer(“String #”+i);
}
while(!queue.isEmpty()){
System.out.println(queue.poll());
}
//..
Nel primo caso ci troviamo di fronte ad una situazione classica, una coda tipica, ordinata per ordine di arrivo (quindi FIFO). La coppia di metodi offer()
–poll()
ci garantisce la consistenza della coda e del suo ordine. Eseguendo il codice ci aspettiamo di vedere le stringhe ordinate per identificativo crescente. Si noti che il tipo concreto è LinkedList
che è stato reimplementato per garantire l’estensione dell’interfaccia Queue.
Esempio di utilizzo di PriorityQueue:
//..
System.out.println(“Ordinamento alfabetico, in base al compareTo()”);
Queue<String> queuePrio= new PriorityQueue<String>();
for (int i=0; i<50; i++){
queuePrio.offer(“String #”+i);
}
while(!queuePrio.isEmpty()){
System.out.println(queuePrio.poll());
}
}
}
La coda a priorità esegue l’estrazione degli elementi secondo la priorità assegnata. Qui lasciamo il comparatore di default (ma PriorityQueue può accettare un Comparator<E,T>
) in quanto lasciamo al metodo compareTo()
dell’interfaccia Comparable il compito di effettuare la comparazione. L’esecuzione in questo caso darà la lista in ordine alfabetico:
String #0
String #1
String #10
String #11
String #12
String #13
String #14
String #15
String #16
String #17
String #18
String #19
String #2
String #20
String #21
String #22
La novità più importante è l’introduzione nella gerarchia di questo nuovo tipo. Tra le altre piccole novità introdotte sicuramente dobbiamo citare l’estensione della classe di utilità java.util.Arrays che presenta una serie di utilissimi metodi statici per la gestione di array mono e multidimensionali.
Nuove interfacce (package java.util):
- Deque;
- BlockingDeque;
- NavigationSet;
- NavigationMap;
- ConcurrentNavigableMap.
Di fatto queste rappresentano definizioni di nuove strutture dati. La prima è importante ed è direttamente collegata alla coda discussa nel capitolo precedente. Una Deque, è una coda a cui è possibile accedere da entrambi gli estremi (sia in inserimento che in rimozione). In alcune situazioni può essere il modello di struttura dati che necessitiamo, quindi Sun ha provveduto con l’implementazione all’interno del framework.
Per la gestione di sistemi multithread sarà utile l’utilizzo dell’interfaccia BlockingDeque, che estende dalla prima e che effettua l’attesa di spazio disponibile, nel caso di inserimento in coda piena, e l’attesa di un elemento inserito nel caso di coda vuota.
I Set e Map navigabili (NavigationSet e NavigationMap) definiscono il comportamento di un set e una mappa rispettivamente, permettendo delle funzioni di navigazione e attraversamento dei nodi in maniera ordinata. La versione concorrente (ConcurrentNavigableMap) verrà utilizzata nel caso di possibile accesso concorrente.
Ovviamente, per poter realizzare queste interfacce sono state realizzate le rispettive classi che elenchiamo:
- ArrayDeque;
- LinkedBlockingDeque;
- ConcurrentSkipListSet;
- ConcurrentSkipListMap;
- AbstractMap.SimpleEntry;
- AbstractMap.SimpleImmutableEntry;
- LinkedList;
- TreeSet;
- TreeMap.
Partiamo dalle ultime tre, che non sono dei nuovi tipi ma sono delle reimplementazioni, fatte per essere compatibili con le nuove interfacce. LinkedList ora implementa la nuova interfaccia Deque.
ArrayDeque invece è una nuova classe che si occupa di gestire il comportamento di una Deque in concreto. Stessa cosa dicasi per LinkedBlockingDeque, che si occupa di implementare l’interfaccia BlockingDeque. In generale il naming utilizzato già indica la tipologia di implementazione che quella classe vuole realizzare, come per tutte la gerarchia del Collection framework.
A partire dalla versione di Java 1.6 c’è l’introduzione dei metodi Array.copyOf()
e Array.copyOfRange()
che consentono di superare l’utilizzo della vecchia classe di utilità System.arraycopy()
.
JAXP: PROCESSARE UN DOCUMENTO XML
Una delle principali mancanze, delle vecchie versioni di Java, era quella relativa a un paradigma per l’elaborazione di una sorgente XML. Nel momento in cui intendevate effettuare una lettura o qualsiasi operazione su un file XML dovevate utilizzare uno strumento proprietario (API di terze parti) con il rischio di legarvi a vita ad una specifica implementazione.
Una delle modifiche della versione 1.4 di Java è stata quella di introdurre, tra le sue API di base, la JAXP (Java API for XML Processing). Il principale obiettivo di questa modifica è quello di astrarre l’implementazione concreta, lasciando quindi allo sviluppatore un’interfaccia generica da utilizzare, e poi un motore di parsing da poter cambiare in maniera dinamica.
Prima di vedere l’implementazione concreta, citiamo i due modelli di parsing che vengono proposti in JAXP: SAX (Simple API for XML) e DOM (Document Object Model).
Non entriamo nel dettaglio delle due tipologie, richiederebbe un capitolo a parte, ma per comprendere quello che segue è bene sapere che il primo è un modello di accesso semplice, basato sugli eventi incontrati nella lettura di un file XML (apertura e chiusura dei tag, attributi, ecc), il secondo è basato su una scansione del documento e di una rappresentazione dell’albero degli oggetti.
Il primo non mantiene in memoria una rappresentazione del documento, quindi può essere utilizzato per grossi file XML o per accessi veloci. Il secondo, invece, mantiene una struttura dei nodi, quindi può essere utilizzato per documenti di dimensione ridotta o per modificare la struttura dell’albero, aggiungendo e rimuovendo dinamicamente i nodi.
Visto che sono due possibili modelli di utilizzo, JAXP lascia la possibilità di scegliere. La libreria infatti è così composta:
- javax.xml.parsers
- org.w3c.dom
- org.xml.sax
- javax.xml.transform
Il primo è un package generico che definisce le classi factory per poter accedere alle implementazioni concrete di SAX e DOM, e quindi poter processare i file XML.
Il secondo definisce le classi e l’interfaccia programmatica da utilizzare nel caso di un processing DOM delle risorse XML. Qui è contenuta la definizione di Node, Attribute e in generale del modello che rappresenta l’albero dei nodi e come poterlo accedere e modificare.
Il terzo è più semplice in quanto espone il modello programmatico SAX per processare le risorse XML, in questo caso guidato dagli eventi (quindi dalla classe Handler e relativi metodi startTag()
, endTag()
, ecc).
L’ultimo package introduce la possibilità di trasformare il codice XML attraverso il meccanismo della trasformazione XSL, dando accesso a una serie di interfacce di utilità per raggiungere lo scopo.
Come si vede dal codide, non c’è la necessità di importare librerie di terze parti ed è immediato manipolare il documento modificandone la struttura.
Esempio di accesso DOM a un documento:
package it.html.xml;
public class MainXML {
public static void main(String[] args) throws Exception {
//Recupero concretamente l’engine di parsing DOM (di default)
DocumentBuilderFactory dbfactory = DocumentBuilderFactory.newInstance();
DocumentBuilder domparser = dbfactory.newDocumentBuilder();
//Effettuiamo il parsing di un file
Document doc = domparser.parse(new File(“data.xml”));
//Recuperiamo il primo nodo
Node x = doc.getFirstChild();
//Recupero il primo nodo TEAM del documento
Node y = doc.getElementsByTagName(“TEAM”).item(0);
//Aggiungo il nodo y a x
x.appendChild(y);
//Creiamo un nuovo documento
Document doc2 = domparser.newDocument();
//e appendiamo la rappresentazione del primo doc
doc2.appendChild(x);
//infine salviamo il file
XMLSerializer serializer = new XMLSerializer();
serializer.setOutputCharStream(new java.io.FileWriter(“data2.xml”));
serializer.serialize(doc2);
}
}
L’uniformità garantita dall’interfaccia JAXP ci permette di vedere un documento DOM sempre allo stesso modo. Ciò significa che questo codice potrà essere riutilizzato anche quando cambia l’implementazione del motore di parsing sottostante. Infatti, le prime due righe di codice sono quelle che concretamente recuperano l’implementazione che poi ci garantisce un corretto utilizzo della risorsa.
Abbiamo mostrato come funziona il DOM, ma altrettanta semplicità avrete utilizzando l’interfaccia SAX o la trasformazione XSL.
REFLECTION
L’introduzione dei tipi Generics ha portato all’inevitabile cambiamento di diverse classi oltre al framework Collection. In questo capitolo ci occupiamo di descrivere le modifiche apportate al package java.lang.reflection. Come sappiamo, la reflection è uno strumento molto potente utilizzato per analizzare e modificare a runtime il comportamento del linguaggio tramite introspezione. L’introduzione dei tipi generici porta quindi al cambiamento di molte interfacce e classi del package attraverso il loro utilizzo.
Anche l’introduzione delle annotazioni porta all’evoluzione del package con l’introduzione di metodi per accedere a runtime alla definizione delle annotazioni. Ad esempio, la classe java.lang.reflection.Class introduce i due metodi:
<A extends Annotation> A getAnnotation(Class<A> annotationClass)
Annotation[] getAnnotations()
Attraverso questi metodi riusciamo a recuperare a runtime il tipo Annotation che dinamicamente possiamo interrogare per gestire il comportamento dinamico definito dall’annotazione.
Non vediamo tutte le modifiche del package perché sono tante ed applicate a tutte le classi, ci limitiamo a fare un riassunto dei principali tipi di modifica in modo da avere una panoramica generale (per consultare le modifiche basta accedere a Javadocs API).
Supporto ai tipi generics: tutte le principali classi vengono riviste aggiungendo i tipi generics, laddove possibile, per poter sfruttare questo nuovo paradigma.
Ad esempio, adesso dovremo utilizzare la seguente dichiarazione:
Class<UnaClasse> x,y,z;
//..
Constructor<MiaClasse> constr = getDeclaredConstructor(x,y,z)
Supporto per le annotazioni: anche qui abbiamo anticipato l’introduzione dei metodi per poter accedere dinamicamente alle annotazioni.
Supporto per i tipi enum: anche l’introduzione del tipo enum, apporta modifiche al package per far si di effettuare introspezione dinamica.
Supporto per var args: la possibilità di effettuare di gestire un numero di argomenti variabili necessita una struttura opportuna per la sua gestione.
Metodi di utilità: sono presenti una serie di nuovi metodi di utilità per poter utilizzare al meglio la reflection.
La classe java.lang.reflect.Class è stata resa generica, quindi ora dovremo utilizzarla come segue:
Class<MiaClasse> c;
COLLECTION THREAD SAFE
Poniamoci nel caso di programmazione concorrente e diciamo di utilizzare una struttura dati condivisa. Sarebbe interessante vedere le percentuali di utilizzo delle strutture dati che ognuno di noi sceglierebbe. Dico questo perché a parte Vector ed Hashtable le altre strutture dati non sono sincronizzate. Che succede se vogliamo utilizzare un’altra struttura dati che performi in maniera differente rispetto alle precedenti (una lista concatenata, un albero, ecc)?
Ovviamente questo è stato un problema piuttosto sentito in Java, e infatti a partire da Java 1.5 tante sono le novità riguardo alla programmazione concorrente, la prima che vediamo in questo capitolo è appunto l’ingresso di decine di nuove strutture dati, in particolare strutture che consentono l’accesso Thread-safe.
In generale vedremo che il modello di programmazione concorrente è cambiato abbastanza, soprattutto con la versione 1.5 del linguaggio. Non si tratta di semplici modifiche cosmetiche ma di veri e propri cambi nel paradigma, soprattutto nel tema della sincronizzazione e di come questa venga gestita dalla macchina virtuale Java. Le modifiche vanno in due direzioni: migliorare il processo di produzione del codice e migliorare le performance, che in tema di concorrenza è un concetto piuttosto complesso vista l’architettura non nativa di Java.
La prima cosa da conoscere è il package java.util.concurrent, che di fatto racchiude tutte le migliorie apportate alla programmazione concorrente Java. Discuteremo di buona parte delle sue classi durante i prossimi capitoli, in questa ci occupiamo delle implementazioni Thread-safe delle collection.
Riportiamo di seguito la lista di collezioni Thread-safe che a partire da Java 1.5 potrete utilizzare per i vostri programmi concorrenti:
- ConcurrentHashMap<K,V>: si tratta dell’implementazione thread safe da utilizzare per una HashMap;
- ConcurrentLinkedQueue<E>: una lista concatenata (sempre thread-safe) semplice;
- ConcurrentSkipListMap<K,V>: implementazione thread-safe di una mappa navigabile (classe NavigableMap);
- ConcurrentSkipListSet<E>: implementazione thread-safe di una set navigabile (classe NavigableSet);
- CopyOnWriteArrayList<E>: quest’implementazione è utile per liste accedute maggiormente con operazioni di lettura. In pratica la classe si trasforma in thread-safe al momento dell’accesso in scrittura (implementazione di un ArrayList sincronizzato);
- CopyOnWriteArraySet<E>: stessa cosa della classe precedente, solo che qui si tratta dell’implementazione di un ArraySet.
Generalmente le classi implementano solo i metodi di scrittura come concorrenti per una questione ovvia di performance, mantenendo peraltro la consistenza inalterata.
Quindi, a partire da Java 1.5, potete utilizzare direttamente un’implementazione concreta delle precedenti senza stare lì a preoccuparvi degli accessi concorrenti agli elementi di una struttura dati.
GESTIONE DELLE ECCEZIONI A RUNTIME
Quando abbiamo programmato in ambiente multithread, uno degli aspetti più complessi da gestire è sempre stato quello di come comportarsi in eventuali casi eccezionali, senza risultare fatali al thread in esecuzione. Cosa avviene se un thread, nel metodo run, solleva una RuntimeException? Semplicemente il thread si interrompe passando la gestione dell’eccezione al ThreadGroup a cui appartiene. Per poter gestire quest’eccezione dobbiamo creare una estensione di ThreadGroup e gestirne tutto il codice relativo.
Tra le diverse modifiche apportate al modello di programmazione concorrente di Java c’è l’introduzione di un gestore di eccezioni da utilizzare nel caso in cui non volessimo ricorrere alla programmazione di un ThreadGroup a cui delegarne il compito, quindi definendo un comportamento di default per le eccezioni di una classe di Thread.
Java 1.5 introduce il metodo statico Thread.setDefaultUncaughtExceptionHandler() che si aspetta come parametro un Thread.UncaughtExceptionHandler, un’interfaccia inner che dobbiamo implementare. Quando l’eccezione viene sollevata verrà richiamato il metodo Thread.UncaughtExceptionHandler.uncaughtException(), il metodo che dovremo implementare con il relativo codice di gestione.
Attraverso questo nuovo concetto riusciamo a gestire, a livello di singola classe, quello che prima avremmo dovuto gestire a livello di gruppo, con un evidente semplificazione anche a livello concettuale. Implementiamo un semplice Thread che effettua un compito banale e che nel caso di una particolare situazione sollevi un’eccezione a runtime nel metodo run()
.
Effettua un semplice compito per poter sollevare un’eccezione e vedere come viene gestita:
package it.html.threads;
public class ExceptionalThread extends Thread {
private int iterations;
public ExceptionalThread(String threadName,int iterations){
setName(threadName);
this.iterations = iterations;
setDefaultUncaughtExceptionHandler(new ThreadException());
}
public void run(){
System.out.println(“Running #”+getName());
if(iterations<0)
throw new IllegalArgumentException(“Numero di iterazioni negative!”);
//altrimenti effettuiamo l’iterazione
for (int i=0;i<iterations;i++){
System.out.println(“Iterazione “+i+”) “+getName());
}
}
}
La prima parte risulta piuttosto normale, unica cosa nuova è l’inizializzazione di un nuovo exception handler (ThreadException
, di cui vedremo l’implementazione di seguito) ed il setting del medesimo attraverso il metodo statico Thread.setDefaultUncaughtExceptionHandler().
Vediamo in concreto l’implementazione della classe (qui nella stessa unità di compilazione):
Definisce il comportamento da eseguire nel caso venga sollevata nel metodo run() di un Thread:
class ThreadException implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread arg0, Throwable arg1) {
System.out.println(arg0+” ha sollevato la seguente eccezione: “+arg1.getMessage());
//qui potremmo gestire un flusso eccezionale, cosa che prima non poteva essere fatta
}
}
La classe deve implementare il metodo uncaughtException()
e definire la logica eccezionale: sia essa la semplice stampa di un messaggio di log o una qualsiasi logica più o meno complessa. Ponete attenzione in quanto non è sufficiente creare la classe handler, ma bisogna anche settare (con il metodo setter statico) quale handler utilizzare. Provate a fare delle prove commentando il metodo setDefaultHandler UncaughtException(...)
per controllare la differenza nell’esecuzione.
Classe di test per sollevare l’eccezione nel thread:
package it.html.threads;
public class MainExceptionalThread {
public static void main(String[] args) throws InterruptedException {
//Definiamo un pò di thread, alcuni con iterazioni negative
ExceptionalThread et1 = new ExceptionalThread(“Thread 1”,10);
ExceptionalThread et2 = new ExceptionalThread(“Thread 2”,0);
ExceptionalThread et3 = new ExceptionalThread(“Thread 3”,-10);
ExceptionalThread et4 = new ExceptionalThread(“Thread 4”,3);
ExceptionalThread et5 = new ExceptionalThread(“Thread 5”,-1);
//Lanciamo i thread, et3 e et5 devono sollevare eccezione
et1.start();
et2.start();
et3.start();
et4.start();
et5.start();
//Aspettiamo che terminino
et1.join();
et2.join();
et3.join();
et4.join();
et5.join();
System.out.println(“Sessione terminata!”);
}
}
Lanciamo una serie di thread, alcuni dei quali con parametri tali per sollevare l’eccezione. Il risultato, nel caso eccezionale, sarà l’esecuzione dell’handler e la relativa logica.
GESTIONE DELLE CODE
Una limitazione evidente nell’uso delle code è quello del loro essere limitate ma soprattutto quello di non avere il concetto dell’attesa che, spesso, in situazioni di sviluppo reale è addirittura un requisito. Attraverso l’uso di monitor e semafori è senza dubbio possibile realizzare questa attesa ma un comportamento del genere non è direttamente gestito dalla struttura dati ma “esternalizzato” in maniera programmatica.
Sun ha pensato a questa situazione tipica, che si manifesta in molti sistemi a coda reale, come l’acquisto di un biglietto in un sistema elettronico, la gestione delle code alle banche o qualsiasi sistema dove l’attesa è una situazione contemplata. Il package java.util.concurrent vede l’introduzione dell’interfaccia BlockingQueue e delle relative implementazioni concrete:
- ArrayBlockingQueue;
- DelayQueue;
- LinkedBlockingQueue;
- PriorityBlockingQueue;
- SynchronousQueue.
Il comportamento dell’interfaccia (che estende anche Collection e Queue) è quello di estendere le funzionalità della coda tradizionali avendo però i metodi take()
e put()
(rimozione ed inserimento) bloccanti per il thread che li invoca, avendo appunto il comportamento di coda con attesa (illimitata, diciamo per ora).
Per meglio capire, creeremo una serie di thread Consumer che consumano i messaggi testuali di una coda e un solo thread Producer che li produce, condividendo la stesssa coda.
Simula il problema delle attese in caso di coda piena:
package it.html.threads;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class MainTest {
public static void main(String[] args) {
//Creiamo un’istanza di coda “blocking”
BlockingQueue queue = new LinkedBlockingQueue<String>();
//Processi consumer
Consumer c1=new Consumer(queue);
Consumer c2=new Consumer(queue);
Consumer c3=new Consumer(queue);
Consumer c4=new Consumer(queue);
Consumer c5=new Consumer(queue);
System.out.println(“Starting Consumers….”);
c1.start();c2.start();c3.start();c4.start();c5.start();
//Processo producer
Producer p = new Producer(queue);
System.out.println(“Starting Consumers….”);
p.start();
}
}
Il main si spiega da solo, è piuttosto semplice intuirne il comportamento.
Classe che simula il consumo di messaggi di testo:
class Consumer extends Thread{
//teniamo un counter a livello di classe
static int id;
int code;
BlockingQueue<String> queue;
public Consumer(BlockingQueue<String> queue) {
this.queue = queue;
code=++id;
}
public void run(){
//ciclo infinito
while(true){
try {
System.out.println(“Thread#”+code+” waiting for the message…”);
String message = queue.take();
System.out.println(“Thread#”+code+” –> “+message+” taken!”);
//riposa 2 secondi
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Il thread Consumer è altrettanto intuitivo: in un ciclo infinito si mette in attesa di ricezione un messaggio con la chiamata queue.take()
. Questa restituisce un messaggio se presente in coda (ed è il turno del thread), altrimenti aspetta. In una implementazione pre Java 1.5 avremmo dovuto codificare un semaforo e trovare anche una soluzione per risolvere l’ordine di arrivo.
Classe che simula la produzione di messaggi di testo:
class Producer extends Thread{
BlockingQueue<String> queue;
public Producer(BlockingQueue<String> queue) {
this.queue = queue;
}
public void run(){
int messagecode = 0;
//ciclo infinito
while(true){
try {
System.out.println(“Producing “+(++messagecode));
queue.put(“MESSAGE@”+messagecode);
System.out.println(messagecode+” in queue”);
//riposa un secondo
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Il Producer ha ovviamente il comportamento opposto, cioè pone dei messaggi in maniera ciclica e quando la coda è piena attende (metodo queue.put()
). Ovviamente in questo caso eseguendo l’esempio vedrete l’attesa dei processi consumatori che sono in numero maggiore dei produttori, ma invertendo il numero di processi avremmo la situazione inversa (attesa dei produttori).
Come potete vedere nel codice, al momento della simulazione dell’attesa (metodo sleep()
) abbiamo usato il metodo tradizionale. Una nuova classe, anzi, un’enumerazione permette di definire l’unità di tempo da utilizzare nella gestione del ciclo di vita del Thread: si tratta di TimeUnit
e le sue unità definite (SECONDS, MILLIS, MICROSECONDS, NANOSECONDS).
Nel nostro esempio avremmo dovuto scrivere (notate l’import statico):
import static java.util.concurrent.TimeUnit.*;
//..
SECONDS.sleep(2);
PROGRAMMAZIONE CONCORRENTE
La programmazione concorrente è sicuramente una delle migliori caratteristiche di questo linguaggio in quanto consente di avere una forma di programmazione multiprocesso “leggera”, nel senso che i Thread di Java condividono lo stesso spazio di memoria e il tempo per fare il content switching (passaggio di contesto dall’esecuzione di un Thread ad un altro) è minore rispetto ai processi tradizionali. Unico neo è quello di avere la logica di business accoppiata con il ciclo di vita e quindi la logica di esecuzione dello stesso Thread.
Ora vediamo come da Java 1.5 sia possibile disaccoppiare le due cose. Le nuove interfacce e classi di Java permettono di ottenere dei servizi migliori nello sviluppo di programmi concorrenti. Una di queste è l’interfaccia Executor, il cui compito è quello di definire un flusso di esecuzione di task (thread java) e di poterne controllare la stessa esecuzione.
Attraverso l’estensione di java.util.concurrent.Executor ed ExecutorService (che ha un’interfaccia molto più fornita) possiamo definire il ciclo di esecuzione di un generico Thread ma, per gli obiettivi più comuni, utilizzeremo i metodi statici di java.util.concurrent.Executors che ci consentono l’accesso a degli Executor standard, che sono:
- Single Thread Executor
- Fixed Thread Executor
- Cached Thread Executor
- Scheduled Thread Executor
Attraverso l’uso di quest’interfaccia potremo eseguire un set fisso di thread (ad esempio un server che accetta delle connessioni) o altri interessanti combinazioni di logica con l’uso delle classi Callable e Future.
Esempio di un server che effettua un servizio tramite socket:
package it.html.threads;
public class EchoServer {
public static void main(String[] args) {
ServerSocket server = null;
//Avviamo il server
try {
server = new ServerSocket(6666);
} catch (IOException e) {
System.out.println(“Eccezione all’avvio del server: “+e.getMessage());
System.exit(-1);
}
//Creo un pool di thread concorrenti
ExecutorService executor = Executors.newFixedThreadPool(5);
//Aspettiamo una nuova connessione cui deleghiamo l’esecuzione
//all’executor service definito in precedenza
while(true){
try{
System.out.println(“Waiting incoming connection…”);
Socket incoming = server.accept();
executor.execute(new EchoThread(incoming));
}catch(Exception e){
executor.shutdown();
}
}
}
}
Il main definisce un ServerSocket che attende connessioni all’infinito. Su ogni connessione creiamo un thread (che definiamo in seguito) per gestire il servizio. In precedenza avevamo definito un Executor di cinque elementi, quindi stiamo definendo una coda di esecuzione di al massimo cinque elementi da eseguire concorrentemente. Anziché effettuare l’esecuzione (con il classico metodo start()
) qui deleghiamo l’esecuzione all’executor service, ignorando come questo gestisca la logica di avvio.
Nel caso avvenga un’eccezione utilizziamo il metodo shutdown()
che effettua la chiusura di tutti i thread che si stanno eseguendo. Segue il codice del thread:
class EchoThread implements Runnable{
Socket socket;
public EchoThread(Socket socket){
this.socket = socket;
}
public void run(){
System.out.println(“Incoming connection: “+socket.getInetAddress());
//Continua con la lettura/scrittura sulla socket aperta…
}
}
Abbiamo lasciato il servizio blank, visto che non rientra ai fini della discussione. A questo punto vediamo anche un semplice client per effettuare l’esecuzione di un thread (voi potete modificarlo aggiungendo più chiamate concorrenti).
package it.html.threads;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
public class EchoClient {
/**
* @param args
* @throws IOException
* @throws UnknownHostException
*/
public static void main(String[] args) throws UnknownHostException, IOException {
Socket socket = new Socket(“127.0.0.1”,6666);
System.out.println(“Connected!”);
//… fa qualcosa con lo stream
}
}
CALLABLE
I due limiti principali della programmazione concorrente risiedono nella signature del metodo run()
di un Thread. Esso non può essere modificato per poter essere gestito dallo scheduler della JVM e quindi non avremo mai la possibilità di avere un thread che ci restituisce un risultato con un’interrogazione diretta o sollevare checked exceptions. Ovviamente, prima delle novità che andremo a vedere in questo capitolo, c’era il modo per ottenere il risultato di un esecuzione di un thread interrogando una variabile di risultato, ad esempio, attendendo il termine del thread con un metodo join()
.
Queste limitazioni però facevano sì che il programmatore dovesse arrovellarsi non poco, con il risultato di un codice farraginoso e complicato da comprendere o manutenere. Il package java.util.concurrent presenta allora una serie di nuove interfacce per permettere a un thread di comportarsi come una classe sincrona, dove la gestione della concorrenza (quindi l’attesa se il risultato non è disponibile) è effettuata all’interno delle classi che andremo a spiegare.
La prima interfaccia che vediamo è l’interfaccia Callable<T>
che ha un semplice metodo, <T> call()
, la cui esecuzione effettua un’attività asincrona e restituisce il risultato definito come parametro dell’interfaccia:
Esempio di utilizzo di Callable:
package it.html.threads;
import java.util.Collection;
import java.util.NoSuchElementException;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
public class CallableTask implements Callable<Collection<String>> {
String keyword;
public CallableTask(String key){
this.keyword = key;
}
@Override
public Collection<String> call() throws EmptyListException{
Collection<String> toRet = new Vector<String>();
//… simuliamo la presenza di oggetti in un db
toRet.add(“Product #CADASD432”);
toRet.add(“Product #CADAS322”);
//… se non ci sono oggetti solleviamo un’eccezione
if (toRet.isEmpty())
throw new EmptyListException(“Nessun elemento trovato!”);
//… simuliamo il tempo di ricerca
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
// NOOP
}
return toRet;
}
In questo esempio immaginiamo di avere un task che restituisca il risultato di una ricerca su un catalogo prodotti, in base ad una chiave di ricerca. Per ottimizzare il processo di ricerca facciamo in modo che la ricerca sia asincrona, quindi creiamo un Callable concreto di tipo Collection<String>
.
In questo caso restituiamo un risultato concreto oppure solleviamo l’eccezione EmptyListException, per segnalare che la ricerca non ha dato esito (è una checked exception). Vediamo il resto dell’esempio (un main di test e l’eccezione):
…
public static void main(String args[]) throws InterruptedException{
CallableTask task = new CallableTask(“games”);
System.out.println(“Search result (dovrebbero passare 5 secondi prima di vedere il risultato…):”);
try {
showResult(task.call());
} catch (EmptyListException e) {
System.out.println(“Nessun oggetto trovato”);
}
System.out.println(“… fine della computazione”);
}
class EmptyListException extends Exception{
public EmptyListException(String string) {
super(string);
}
}
Creiamo un task passando la keyword di ricerca e stampiamo la lista di risultati attesi oppure una segnalazione di errore se la lista è vuota.
Come vediamo dall’esecuzione dell’esempio, l’uso dell’oggetto Callable è di sicuro interesse in quanto ci dà la possibilità di avere un’esecuzione asincrona con un tipo di ritorno e una lista di eccezioni che desideriamo. In realtà, come potete vedere, di asincrono c’è ben poco, in quanto l’esecuzione del metodo call()
arresta l’esecuzione del flusso del main, in attesa del risultato.
FUTURE E FUTURE TASK
Così com’è la classe Callable non ha molta potenza, in quanto da sola non fa nessun tipo di esecuzione concorrente. Attraverso l’uso delle classi Future e FutureTask, però, possiamo pensare di inserire l’esecuzione in un oggetto che viene eseguito (parallelamente) e restituisce il risultato quando ci serve (attendendo la fine del task se questo non è completato).
Già in uno dei capitoli precedenti abbiamo parlato della classe ExecutorService, da utilizzare per il controllo del flusso dei thread. Utilizzando il metodo submit()
della classe, passando un oggetto di tipo Callable<T>
, avremo un oggetto Future<T>
che potremo utilizzare per l’esecuzione concorrente di un’attività e recuperarne il risultato interrogando il metodo get
(che restituisce un istanza di T) sul Future.
Detto così potrebbe suonare complicato. In realtà, modificando il metodo main visto nella precedente lezione, vedremo come poter effettuare la stessa operazione di ricerca ma in maniera parallela:
public static void main(String args[]) throws InterruptedException{
//Definiamo un executor
ExecutorService executor = Executors.newSingleThreadExecutor();
//Creiamo un task asincrono
CallableTask task = new CallableTask(“games”);
//e lo wrappiamo in un oggetto future
Future<Collection<String>> result = executor.submit(task);
System.out.println(“Search result (dovrebbero passare 5 secondi prima di vedere il risultato…):”);
//…nel frattempo possiamo fare altri compiti, come preparare un layout di presentazione
System.out.println(“#————- games found —- ++”);
//prendiamo il risultato, e se non è stato eseguito attendiamo
try {
showResult(result.get());
} catch (ExecutionException e) {
System.out.println(“Eccezione nell’esecuzione della ricerca”);
}
System.out.println(“… fine della computazione”);
}
Per prima cosa abbiamo creato un ExecutorService sul quale richiamare il Callable creato. Il callable è parametrizzato come <Collection<String>>
, quindi il tipo restituito dall’executor sarà un future con il medesimo parametro, il cui metodo get()
restituirà quel parametro.
Come vediamo, l’uso di generics qui consente uno strettissimo controllo dei tipi già a tempo di compilazione, il che rende il processo di sviluppo molto meno incline a bug dovuti a casting.
Nel momento in cui eseguiamo il task, questo inizia la sua esecuzione parallela (in questo caso abbiamo simulato cinque secondi di esecuzione). Il main, però, procede senza bloccarsi su di esso, quindi prepariamo un ipotetico layout grafico per contenere i risultati (e magari effettuiamo altre operazioni, come un aggiornamento ad una tabella di statistiche per sapere le ricerche effettuate). Nel momento in cui chiamiamo il metodo get()
su FutureTask, se il risultato è disponibile allora verrà mostrato, altrimenti ci sarà un attesa fino alla sua completa esecuzione.
Interessante è la possibilità di utilizzare il metodo get(long,TimeUnit)
che ci consente di settare un timeout di attesa, oltre il quale l’attività è cancellata (utile ad esempio in contesti realtime o per attività con deadline). In generale vi consigliamo di dare un’occhiata alla API delle classi per meglio capire i dettagli di questi nuovi concetti di concorrenza in Java.
SEMAPHORE
Osservando il package java.util.concurrent potete notare le tante nuove interfacce e classi che vi sono contenute. Finora abbiamo discusso di alcune di loro, come gli Executor e i Future, cioè di come sviluppare gli aspetti di logica di ogni singolo thread. Accanto a queste novità gli ingegneri della Sun hanno curato anche l’aspetto della gestione della concorrenza, attraverso delle novità per i meccanismi di accesso in mutua esclusione, laddove necessario. Qui, ci occuperemo di una nuova classe che introduce uno strumento molto importante nella programmazione concorrente, il semaforo.
Un semaforo è una delle strutture di controllo di concorrenza più semplici ed importanti, in quanto permette di dare, o meno, l’accesso a una risorsa condivisa, garantendo mutua esclusione o accesso condiviso a un numero limitato di clienti. Di fatto, un semaforo (classe java.util.concurrent.Semaphore) si comporta come un semaforo stradale, dando l’accesso ad alcuni, e negando l’accesso ad altri.
Finora l’implementazione doveva essere fatta da ognuno che ne avesse l’esigenza e pur trattandosi di una classe banale (tipico esercizio di programmazione concorrente) non ha senso avere decine di diverse implementazioni. La classe si presenta con i metodi acquire()
e release()
, dove il richiedente si pone in attesa sul metodo acquire()
fino a che una risorsa non viene rilasciata da un altro cliente (con il metodo release()
). Ci sono altri interessanti metodi, come il tryAcquire()
dove il richiedente si pone in attesa per un limite di tempo e poi esce (senza acquisire la risorsa).
Il tipico uso che si fa di questa classe è quello del pool, dove vi sono una serie di oggetti condivisi (connessioni, transazioni, ecc) e un gestore che si occupa di assegnarli in base alle richieste e alla disponibilità del pool.
Preseguiamo con un esempio concreto per mostrare l’uso della classe. Come contesto scegliamo una simpatica simulazione dove la risorsa condivisa è una festa (con un numero di posti liberi limitato) e un numero di invitati maggiore. Per rendere la cosa più veritiera abbiamo alcuni invitati che per pigrizia attendono l’ingresso ma non oltre un certo tempo, dopodiché lasciano perdere.
Definisce la virtualizzazione di una risorsa condivisa, in questo caso un party:
package it.html.threads.semaphore;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class Party {
//Dimensione massima della sala: quante persone al massimo riusciamo a servire
int dimensione;
//Il semaforo che controllerà gli accessi
Semaphore semaphore;
public Party(int dim){
this.dimensione = dim;
//creiamo il semaforo con il numero massimo di accessi
this.semaphore = new Semaphore(dimensione);
}
public boolean goIn(){
try{
semaphore.acquire();
if (semaphore.availablePermits()==0)
System.out.println(“Please wait… nessun posto disponibile!”);
return true;
}catch(InterruptedException e){
e.printStackTrace();
}
return false;
}
public boolean goIn(long time){
try{
boolean in = semaphore.tryAcquire(time, TimeUnit.SECONDS);
if (semaphore.availablePermits()==0)
System.out.println(“Please wait… nessun posto disponibile!”);
return in;
}catch(InterruptedException e){
e.printStackTrace();
}
return false;
}
public void goOut(){
semaphore.release();
System.out.println(“Uno meno! Posti liberi… “+semaphore.availablePermits());
}
}
La classe Party simula la festa, quindi ha un numero di posti disponibile e un semaforo con cui cureremo gli aspetti di accesso concorrente. Attraverso i metodi goIn()
i richiedenti avranno accesso all’ingresso, con il metodo goOut()
usciranno, liberando il posto.
Le classi che accedono alla festa sono le classi Invited e LazyInvited che eredita dalla prima:
Simulazione del “traffico” alla festa, con ingressi e uscite:
package it.html.threads.semaphore;
import java.util.concurrent.TimeUnit;
public class Invited extends Thread {
String name;
Party party;
public Invited(String name, Party party){
this.name = name;
this.party = party;
}
//Facciamo un ciclo infinito di ingressi, attese, uscite, attese, …
public void run(){
while(true){
System.out.println(“[“+name+”: ] Sono in coda. Aspetto.”);
party.goIn();
System.out.println(“[“+name+”: ] Yuppy! Sono dentro.”);
try{
//Si diverte un po’ alla festa
int timeToSleep = (int) (Math.random()*10);
System.out.println(“[“+name+”: ] rimarrò “+timeToSleep+” secs.”);
TimeUnit.SECONDS.sleep(timeToSleep);
}catch(InterruptedException e){
e.printStackTrace();
}
//decide di uscire…
party.goOut();
System.out.println(“[“+name+”: ] Esco a prendere aria.”);
try{
//sta un po’ fuori…
int timeToSleep = (int) (Math.random()*10);
TimeUnit.SECONDS.sleep(timeToSleep);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
La vita di un invitato è quella di provare ad entrare nella festa (acquisendo il diritto ad accedere), divertirsi un po’ (simulato da uno sleep random), uscire (rilasciando il diritto di accesso ad altri), riposarsi un po’ (sleep random) e provare a ritornare dentro.
La coppia di metodi goIn()
e goOut()
ci assicura la consistenza dell’azione globale.
Gestione della vita dell’invitato:
package it.html.threads.semaphore;
import java.util.concurrent.TimeUnit;
public class LazyInvited extends Invited {
public LazyInvited(String name, Party party) {
super(name, party);
}
//Facciamo un ciclo infinito di ingressi (con attesa limitata), attese, uscite, attese
public void run(){
while(true){
System.out.println(“[“+name+”: ] Sono in coda. Aspetterò un po’ (5 sec).”);
boolean in = party.goIn(5);
if (in){
System.out.println(“[“+name+”: ] Yuppy! Sono dentro.”);
try{
//Si diverte un po’ alla festa
int timeToSleep = (int) (Math.random()*10);
System.out.println(“[“+name+”: ] rimarrò “+timeToSleep+”
secs.”); TimeUnit.SECONDS.sleep(timeToSleep);
}catch(InterruptedException e){
e.printStackTrace();
}
//decide di uscire…
party.goOut();
System.out.println(“[“+name+”: ] Esco a prendere aria.”);
}else{
//timeout di attesa scaduto!
System.out.println(“[“+name+”: ] Basta! mi sono rotto, esco.”);
}
try{
//sta un po’ fuori…
int timeToSleep = (int) (Math.random()*10);
TimeUnit.SECONDS.sleep(timeToSleep);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
Un invitato “lazy” (pigro) ha lo stesso ciclo di vita di un invitato normale, con l’eccezione che dopo 5 secondi di fila si annoia ed esce dalla coda. In questo caso il metodo goIn(millis)
effettua un tryAcquire()
che, passato il timeout, si risolve senza l’accesso lasciando libero però l’invitato di fare altro.
Per vedere il tutto all’opera creiamo un main e seguiamo l’evoluzione della vita mondana attraverso il testo di output:
Simulazioni inviti e ingressi:
package it.html.threads.semaphore;
public class MainPR {
//Simulazione del PR che invita la gente al party
public static void main(String[] args) {
//Definiamo il party di 20 persone concorrenti
Party party = new Party(20);
//creiamo 40 invitati
Invited inLista[] = new Invited[40];
//metà saranno lazy, gli altri no
for(int i=0;i<inLista.length;i++){
Invited tmp;
if (i%2==0)
tmp = new Invited(“NotLazy#”+i, party);
else
tmp = new LazyInvited(“Lazy#”+i, party);
inLista[i]=tmp;
}
for(int i=0;i<inLista.length;i++){
inLista[i].start();
}
}
}
LOCKING
Nel capitolo precedente abbiamo introdotto alcuni concetti di sincronizzazione avanzata, mostrando l’uso di una struttura dati molto utile, il semaforo. Il concetto che vedremo qui è quello più a basso livello introdotto con la versione Java 5, il locking.
L’introduzione del locking, attraverso la classe java.util.concurrent.locks.Lock ha lo scopo di mettere a disposizione uno strumento più avanzato e potente rispetto alla classica sincronizzazione, tant’è che ci riferiamo ad esso per definire la sincronizzazione avanzata.
Chi sviluppa programmazione concorrente sa benissimo che l’uso del modificatore di accesso synchronized consente l’accesso in mutua esclusione ad una porzione di codice condivisa per evitare il problema della race condition e dell’interleaving di processi concorrenti. Definendo una sezione o un metodo “synchronized” forziamo l’accesso atomico a quel blocco evitando perciò i problemi di race condition. Questo meccanismo funziona bene ed è tuttora valido ma ha alcune limitazioni che l’uso dei Lock permettono di superare:
- Multi locking;
- Timeout;
- Interruptibility.
Il primo caso è noto in letteratura come “hand over hand” e si riferisce ad esempio quando abbiamo più di una risorsa su cui effettuare il locking, quindi ad esempio prima effettuiamo il locking sulla risorsa A, poi accediamo al lock della risorsa B e liberiamo quindi la A e così via (tipico problema di alcune strutture dati).
Quando una classe accede a un blocco synchronized non ha modo di uscirne finché quella sezione non è liberata. In pratica c’è un’attesa indefinita per la risorsa. Il Lock permette di definire un timeout oltre il quale possiamo uscire dall’attesa (come già abbiamo visto nel capitolo precedente).
L’interruptibility è simile a quanto detto in precedenza, un Lock può essere interrotto e quindi saltare il blocco sincronizzato.
Queste nuove caratteristiche rendono possibile sviluppare evitando le tante insidie che la programmazione concorrente nasconde, grazie a queste nuove strutture dati che ci danno dei potenti strumenti di controllo del flusso concorrente. Vediamo una classe in cui abbiamo codificato dei potenziali blocchi atomici:
Classe che codifica blocchi atomici:
package it.html.threads;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockableAction {
private Lock l;
public LockableAction(){
//Creiamo un Reentrant lock, in modo
//da non creare dei deadlock per cicli di lock interni
l = new ReentrantLock();
}
public void doSomethingConcurrent(){
//…svolge qualcosa non concorrente
//acquisici il lock
l.lock();
//…azione 1
//…azione 2
//…
l.unlock();
}
public void doSomethingConcurrentLimitedWait(){
//…svolge qualcosa non concorrente
//Prova ad acquisire il lock
try {
//superati i 5 secondi salta
boolean acquired = l.tryLock(5,TimeUnit.SECONDS);
if (acquired){
//…azione 3
//…azione 4
//…
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
l.unlock();
}
}
}
La classe ha un Lock, definito concretamente come ReentrantLock, che è quello più sicuro in quanto evita problemi nel caso il Lock venga utilizzato internamente, come già accade con i metodi synchronized. I due metodi vengono acceduti concorrentemente da processi esterni che ne condividono un’istanza concreta.
Nel primo metodo simuliamo un blocco synchronized. Se però pensiamo che all’interno di esso possiamo definire altri Lock, allora riusciamo a capire come sia possibile sviluppare logiche più complesse (esiste anche la classe Condition). Il secondo metodo sperimenta il lock con timeout (metodo tryLock()
) dove definiamo un timeout di 5 secondi, oltre il quale svolgiamo altre azioni.
L’esempio è servito per mostrare l’uso della classe che concretamente potrà aiutarvi in compiti di concorrenza anche abbastanza complessi. Esistono altre classi e migliorie a partire dalla versione Java 5 che abbiamo omesso (come ad esempio il package java.util.concurrent.atomic), ma i maggiori cambiamenti sono quelli che abbiamo illustrato. In generale, per avere una maggiore comprensione delle nuove caratteristiche, potete riferirvi alla documentazione del package java.util.concurrent dove risiedono tutte le nuove implementazioni.
PERFORMANCE
Java, essendo un linguaggio di programmazione “virtuale”, è sempre stato etichettato come un linguaggio a scarse performance. Anche da questo punto di vista, negli anni, il linguaggio si è trasformato portando migliorie da una versione all’altra, addirittura tra il 50 e il 75 percento. Tutto ciò è garantito proprio dall’ambiente di compilazione e di esecuzione del bytecode, con delle attività che elevano a uno stato di ottimo le performance di un software sviluppato con Java.
Riportiamo qui una slide presentata al Java Conference di Zurigo del Giugno 2007:
Come vediamo, la crescita nelle performance è lineare e sempre ben marcata (questa la slide della JRE server, ma quella desktop si discosta di poco). Bloccata a 100 la versione 1.3, la 1.6 arriva quasi a 350 di rendimento.Ovviamente mai potrà attestarsi a livelli di un linguaggio a bassissimo livello ma, secondo alcune ricerche, ci sono delle similitudini a livello di performance che permettono di fare ottime esecuzioni (ovviamente non sceglieremo mai Java per sviluppare un software per applicazioni critiche real-time). Vediamo alcune delle attività nascoste del linguaggio Java che gli consentono di avere prestazioni eccellenti:
- Compilazione Just In Time (JIT);
- Hot Spot;
- Garbage Collection.
Discutiamo di come si siano evolute queste attività nelle ultime versioni. Ovviamente si tratta di attività nascoste che il comune programmatore non si deve preoccupare di conoscere, ma che ci sollevano dall’effettuare alcuni compiti (come la gestione della memoria da parte del garbage collector) che necessiterebbero un livello di conoscenza più profondo della tecnologia.
Attraverso la compilazione Just In Time si effettua un lavoro di ottimizzazione per eseguire un pezzo di codice Java compilandolo in codice nativo solo al momento della reale necessità. Java effettua questo con un linguaggio intermedio (il famoso bytecode), il cui compito è quello di rendere più facile il passaggio verso il codice finale “nativo” sul sistema operativo di destinazione.
Nelle ultime versioni di Java sono introdotte delle migliorie che decisamente migliorano in termini percentuali elevati grazie a tecniche “adattative” che danno vita al seguente strumento di Java: l’hot spot.
Java Hot Spot (che è un trademark di Java) è uno strumento, introdotto nella versione di Java 2 (o 1.2), il cui compito è quello di individuare gli “hot spots” (i punti caldi) di esecuzione per poterli ottimizzare e rendere il processo di traduzione ed esecuzione il più rapido possibile. Per intenderci, se l’hot spot individua un metodo di una classe utilizzato spesso, lo compila e lo lascia già compilato in maniera che il prossimo utilizzo non necessita compilazione. Come faccia la JVM ad individuare un hot spot è compito degli algoritmi adattativi di intelligenza artificiale, che addirittura sono in grado di prevedere con un basso margine di errore le attività più frequenti nel ciclo di vita di una applicazione (giusto per fare un esempio, se dopo un metodo open()
, chiamiamo sempre un metodo write()
è facile presupporre che alla prossima chiamata di open()
la JVM si aspetti un write()
). Anche qui, le ultime versioni portano delle migliorie in questi algoritmi adattativi, prendendo a vantaggio anche l’utilizzo del Class Data Sharing.
Il Class Data Sharing viene introdotto in Java 5 e consente di migliorare la velocità all’avvio di una applicazione riutilizzando eventuali package di librerie già caricati in memoria da altre JVM.
Il garbage collector è un altro utilissimo strumento, forse il più importante per il successo di Java. Chiunque abbia sviluppato applicazioni informatiche, prima dell’utilizzo di Java, sa bene quanto complessa e “costosa” sia la gestione della memoria in un linguaggio di programmazione dove lo spazio deve essere allocato (prima del suo utilizzo) e liberato (quando non ci serve più). Con Java tutto questo non è mai servito proprio grazie al Garbage Collector che è un’altra attività automatica effettuata dalla JVM, il cui compito è verificare se un’istanza di un oggetto (e la relativa parte di memoria nell’heap) sia ancora necessario (referenziato da uno degli oggetti che il programma sta utilizzando). Possiamo pensare al garbage collector come a un processo asincrono che tiene una mappa di oggetti e referenze alle istanze utilizzate e che cancella lo spazio di memoria fisico quando uno di questi oggetti non ha referenze da parte di nessuno.
Dal punto di vista delle sostanziali modifiche, anche qui, le principali vennero fatte nella versione 1.5, con l’introduzione del concetto di “young generation” e “old generation”. In realtà il concetto è un po’ più complesso di quello che vado a spiegare ma il principio rimane valido. Quello che si è visto è che la maggior parte delle istanze di una classe sono temporanee, create, utilizzate e scartate, con un ciclo di vita abbastanza giovane. D’altra parte una piccola minoranza di istanze (circa il 10% secondo studi) ha un ciclo di vita maggiore, quasi quello dell’applicazione.
Applicare gli stessi concetti di garbage collection è evidentemente una cosa inutile, in quanto quello che vale per oggetti dal ciclo di vita giovane, non vale per il ciclo di vita vecchio.
La Sun ha quindi introdotto una suddivisione tra “young generation” e “old generation” con garbage collector che appartengono alle due classi e si comportano di conseguenza. Non entriamo nel dettaglio di nessuna delle tecniche utilizzate in quanto richiederebbero approfondimenti piuttosto lunghi, ma penso che già il concetto ci faccia capire le principali modifiche per avere un maggiore rendimento.
JCONSOLE
In questo capitolo affrontiamo una nuova libreria introdotta con la versione 5 di Java: il package java.lang.management. Rispetto ai diversi set di API introdotti finora, questo non ha lo scopo di migliorare gli aspetti di sviluppo, inteso come produzione del codice, ma quelli di gestione dell’applicazione, in particolare della gestione della memoria e delle criticità che l’esecuzione di un programma può presentare (ad esempio accessi concorrenti o altri aspetti peculiari del multithreading).
Effettivamente, una delle carenze del linguaggio era quella di non poter valutare le risorse su cui esso viene eseguito (anche perché non esegue codice nativo) colmata comunque con l’introduzione di questo package e di uno strumento che approfondiremo: JConsole.
Prima di vedere cos’è JConsole, vediamo le principali classi che vengono utilizzate dal tool e che possono essere utilizzate da interfaccia programmatica (API) per le esigenze dei programmi.
- ClassLoadingMXBean Class loading system;
- CompilationMXBean Compilation system;
- MemoryMXBean Memory system;
- ThreadMXBean Threads system;
- RuntimeMXBean Runtime system;
- OperatingSystemMXBean Operating system;
- GarbageCollectorMXBean Garbage collector;
- MemoryManagerMXBean Memory manager;
- MemoryPoolMXBean Memory pool.
Ognuno di essi (come potete vedere dalla convenzione) è un MXBean che è creato secondo lo standard JMX (Java Managment eXtensions). Non vedremo nel dettaglio il funzionamento e l’uso delle suddette classi. Diciamo solo che attraverso esse è possibile interrogare le diverse risorse utilizzate da una JVM per poter capire come le sta utilizzando, quindi torna utile per valutare a runtime la bontà di un nostro algoritmo o di come le nostre classi si stanno relazionando.
In realtà non utilizzeremo quasi mai direttamente queste classi. Se vogliamo monitorare l’andamento di un nostro programma Java lo faremo utilizzando il già citato JConsole, uno strumento diagnostico introdotto da Java 5 che si basa sulla tecnologia JMX; si tratta di uno strumento grafico all’apparenza abbastanza complesso, ma che in realtà ci può dare delle semplici indicazioni su alcuni potenziali problemi che stanno avvenendo sul programma Java in esecuzione.
Il compito di JConsole, secondo la definizione Sun è quello di:
- Scoprire cali di memoria;
- Abilitare o disabilitare il Garbage Collector;
- Scoprire deadlocks;
- Controllare e manipolare dinamicamente il log delle applicazioni;
- Accedere alle risorse del sistema operativo;
- Gestire i Managed Beans (MXBeans) visti sopra.
Per accedere al tool dovrete eseguire il programma bin/jconsole.exe che si trova nella directory degli eseguibili della JDK (1.5 o superiore). Una volta acceduti, un pannello di controllo vi chiederà le azioni da eseguire. Potete scegliere tra “amministrazione locale”, “amministrazione remota” e “amministrazione avanzata”.
La prima gestirà la (o le) Java Virtual Machine in esecuzione in locale, la seconda vi permetterà di accedere ad una macchina remota (un server su cui è presente un servizio, ad esempio), con la terza, sempre remotamente, vi potrete connettere anche a una JVM su cui gira JDK 1.4.
Una volta selezionata la modalità avrete un pannello con le diverse risorse disponibili. Le aree principali sono:
- Sommario
- Memoria
- Threads
- Classi
- Virtual Machine
- Managed Beans
Sta di fatto che in alcune di esse avrete la possibilità di studiare l’andamento grafico (memoria), la concorrenza di risorse (threads) o il numero di classi che, in una determinata istanza della JVM, si stanno eseguendo al momento.
Vi sono alcune caratteristiche basiche che consentono un immediato uso del tool, ma sicuramente, per una amministrazione più professionale, dovrete imparare bene ognuna delle possibili situazioni critiche e come poterle risolvere con JConsole: a tal proposito vi consigliamo la lettura della documentazione Sun.
GENERAZIONE DINAMICA PROXY RMI
Di RMI (Remote Method Invocation) abbiamo discusso nella Guida Java di secondo livello e ne abbiamo parlato a livello teorico, essendo la base su cui la tecnologia distribuita di Java (e quindi anche gli EJB) si basa. Il modello teorico non cambia di una virgola con le ultime release di Java, quello che cambia è l’implementazione che si avvale di uno strumento, il Dynamic Proxy, che evita allo sviluppatore la noia di dover creare a tempo di compilazione gli stub e gli skeleton.
Questa possibilità ci è data dall’introduzione della classe java.lang.reflect.Proxy a partire dalla versione Java 1.3. L’implementazione di RMI è però cambiata solo nella release 1.5 dove, appunto, utilizzando la classe Proxy viene creata dinamicamente l’infrastruttura di stub e skeleton che prima dovevamo definire “a mano” utilizzando il compilatore rmic. Lo sviluppatore di applicazioni RMI, sicuramente, sa quanto vale questa modifica, in quanto lo solleva dal compito di mantenere queste interfacce e modificarle ad ogni cambio, ricompilandole e rieseguendo il tutto. Se poi pensate ad un ambiente distribuito dove una macchina server serve centinaia di client, capite quanto realmente sia una miglioria.
La classe Proxy su cui la generazione dinamica RMI si basa è una classe piuttosto semplice e il concetto alla base è quello di associare un oggetto di tipo java.lang.reflect.InvocationHandler (un’interfaccia) che si occuperà di effettuare la logica che vogliamo controllare in maniera dinamica. In questo caso tutto è gestito a runtime, grazie alla reflection che si occuperà di creare uno stub lato client da connettersi allo skeleton lato server, in maniera del tutto trasparente (effettuando sempre lo scambio di messaggi su protocollo TCP/IP).
Noi alla fine non ci accorgeremo di nulla, il nostro compito sarà sempre quello di definire la classe “server” e di installarla su una macchina e poi utilizzare quell’istanza richiamandola attraverso un registro RMI. Nell’esempio lo vediamo benissimo:
package it.html.java5.rmi.generic;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteEchoExecutor extends Remote {
//definiamo il comportamento dell’interfaccia attraverso i metodi
public String echo(String textToEcho)throws RemoteException;
}
Innanzittutto definiamo l’interfaccia del servizio da realizzare, preoccupandoci solo di definirla Remote. Con l’interfaccia definiamo i metodi da realizzare.
Segue l’implementazione del server, che non deve fare null’altro che definire l’implementazione del servizio.
import it.html.java5.rmi.generic.RemoteEchoExecutor;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class EchoExecutorServer extends UnicastRemoteObject implements RemoteEchoExecutor {
public EchoExecutorServer() throws RemoteException {
super();
}
//Implementiamo il metodo dell’interfaccia: l’esecuzione avviene sul server
public String echo(String textToEcho) throws RemoteException {
System.out.println(“Someone ask to echo: “+textToEcho);
return textToEcho.toUpperCase();
}
}
Sarà la classe UnicastRemoteObject a preoccuparsi di definire il proxy per effettuare lo scambio di informazioni tra client e server, secondo la logica di proxy dinamico detta in precedenza.
L’istanza della classe dovrà essere lanciata ed eseguita su un server, per fare ciò abbiamo bisogno di un registro per pubblicare il servizio che il client interrogherà (in questo modo superiamo il problema dell’accoppiamento tra client e server).
package it.html.java5.rmi.registry;
import it.html.java5.rmi.server.EchoExecutorServer;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RegistryStart {
public static void main(String[] args) throws Exception {
int registryPortNumber = 1099;
// Start RMI registry
LocateRegistry.createRegistry(registryPortNumber);
// Effettuiamo il bind
Naming.rebind(“ECHOSERVER”, new EchoExecutorServer());
System.out.println(“Server running…”);
}
}
Una volta avviato il main, qualsiasi client potrà utilizzare la classe remotamente come nell’esempio che segue:
package it.html.java5.rmi.client;
import it.html.java5.rmi.generic.RemoteEchoExecutor;
import java.rmi.Naming;
public class EchoClientit {
public static void main(String[] args) throws Exception {
String host = “localhost”;
int portNumber = 1099;
String lookupName = “//” + host + “:” + portNumber + “/” + “ECHOSERVER”;
RemoteEchoExecutor executor = (RemoteEchoExecutor)Naming.lookup(lookupName);
String result = executor.echo(“Hello html.it user!”);
System.out.println(“Task executed for ” + result);
}
}
Unica cosa a cui dobbiamo prestare attenzione è il fatto che, sia il client che il server, condividono l’interfaccia RemoteEchoExecutor, quindi entrambe le JVM devono averla sotto il proprio classpath.
Questa che abbiamo descritto è la principale miglioria che potete sfruttare a partire da Java 1.5. Una piccola modifica è stata introdotta in Java 1.6, rendendo generica la classe MarshalledObject<T>
: questa classe è quella che si occupa della serializzazione/deserializzazione dei parametri.
“Greetings! Very useful advice in this particular article! It’s the little changes that produce the most significant changes. Thanks for sharing!”
ragb43235