L'evoluzione di Java: verso Java 8

III parte: Java SE 7 e la 'moneta' delle nuove featuredi

Introduzione

Come visto nell’articolo precedente [1] la versione Java SE 7, nome in codice Dolphin (Delfino), è stata rilasciata il 7 luglio del 2011 (ben cinque anni dopo la precedente major release!) e aperta al pubblico dopo 21 giorni. Il progetto è stato organizzato in 13 milestone: il feature complete è stato raggiunto il 23 dicembre, mentre la preview per gli sviluppatori è iniziata il 17 febbraio 2012, quindi non molto tempo fa [2].

Si tratta di una versione molto importante soprattutto per le implicazioni, diciamo così, "politiche" ed "economiche". Come confermato dallo stesso Gosling, "Java SE 7 is important not for any particular feature but for the fact that Oracle was able to bust the political logjam in the JCP that had delayed it for so very long."

Quindi è Gosling stesso a riconoscere che l’importanza della nuova versione SE 7 non è importante tanto per una particolare caratteristicha, ma per il fatto che Oracle è finalmente riuscita a superare quell’empasse "politico" che aveva ritardato per tanto tempo il lancio della nuova versione.

 

Cosa cambia in Java SE 7

Dal punto di vista "politico", questa release è stata caratterizzata da due aspetti molto importanti,

Open JDK

Nel maggio del 2007, Sun Microsystem rilascia finalmente il kit OpenJDK, soddisfacendo a una continua pressione da parte della Java Community. Si tratta di un’implementazione aperta e gratuita del linguaggio di programmazione Java. La licenza prescelta è GNU General Public License (GPL), con un’eccezione di collegamento (linking exception) che esenta Java Class Library dai termini della licenza GPL [3]

Oracle acquisisce Sun

Nel gennaio del 2010, Sun Microsystem viene acquisita da Oracle Coporation con un accordo da 7.4 miliardi di dollari. Il 2 aprile del 2010 James Gosling, da molti considerato il "padre di Java", dopo aver severamente criticato la gestione dell’acquisizione da parte di Oracle (soprattutto per questioni contrattuale) si dimette. Come lui stesso asserisce nel suo blog [4]: "Sì, infatti, le voci sono vere: mi sono dimesso da Oracle una settimana fa (2 aprile). [...] Quasi tutto quello che potrei dire che sia accurato e veritiero farebbe più male che bene. La parte più difficile è non essere più parte del team formato da tutte le grandi persone con cui ho avuto il privilegio di lavorare nel corso degli anni". L’azienda a cui approda è Google [5] alla quale tuttavia non resta a lungo; e infatti dopo cinque mesi decide di dimettersi per iniziare una nuova avventura: iniza a collaborare con Liquid Robotics, una start-up che si occupa di acquisizione ed elaborazione di dati relativi ai fondali oceanici. Come descritto dallo stesso James Gosling "Liquid Robotics affronta un problema estremamente complesso che fa del bene al mondo ed è incredibilmento ‘fico’: Liquid Robotics può cambiare completamente il modo in cui guardiamo agli oceani. Saremo in grado di ottenere una vasta gamma di dati dettagliati più a buon mercato e più pervasiva rispetto a qualsiasi altro metodo. Si tratta di problemi legati alle grandi dimensioni di dati e di controllo: entrambi sono affascinanti per me e sono state miei passioni per anni."

Figura 1 - In questa immagine pubblicata da James Gosling, si piange "il caro estinto" Sun Microsystems.

 

Java SE 7: Plan B

La release Java SE 7 era partita con un piano molto ambizioso che tra l’altro includeva l’introduzione delle espressioni Lambda e la modularizzazione del JDK (progetto Jigsaw, puzzle). Con queste premesse si trattava di una major release abbastanza rivoluzionaria. Tuttavia, l’implementazione non andò proprio come programmato, come peraltro spesso accade per i progetti software. A complicare le cose intervenne l’acquisizione di Sun Microsystems da parte di Oracle, con l’inevitabile avvicendamento alle sedie di comando, che finì per rendere la situazione ancora più complicata.

Già nel settembre del 2010 Mark Reinhold, Sun e Oracle Chief Architect per la piattaforma Java e OpenJDK, scriveva nel suo blog un post dal titolo "There’s not a moment to lose" [7] ("Non c’è un attimo da perdere"), con un’introduzione piuttosto eloquente: "È divenuto chiaro da tempo che il recente programma di sviluppo JDK 7 è, a dir poco, irrealistico. Abbiamo creato quel piano oltre nove mesi fa, prima dell’acquisizione di Sun da parte di Oracle. Il processo di post-acquisizione e di integrazione, purtroppo, ha richiesto più tempo di quello immaginato da tutti noi, ma ora siamo pronti e in grado di concentrarci su questa versione con un team più grande (e ancora in crescita) che continuerà a lavorare in piena trasparenza a fianco di altri collaboratori".

Purtroppo le cose non stavano neanche esattamente in quei termini e il ritardo era ancora maggiore di quanto analizzato. Tuttavia il ritardo era ormai chiaro a tutti e fonte di non poco imbarazzo da parte di Oracle. Nei vari blog circolò anche la notizia che il team Oracle fosse in grado di generare bug addirittura durante l’esercizio di sostituzione delle informazioni di copyright di Sun Microsystems per sostituirle con i vari logo Oracle.

Tutto ciò, unito alle infinite discussioni soprattutto interne ai progetti Jigsaw e Lambda, spinse a pensare a una soluzione alternativa diventata "famosa" (o meglio "famigerata") con il nome di "piano B" che prevedeva:

  • il rilascio di JDK 7 entro Q2 2011 al netto dei progetti Lambda, Jigsaw, e di una parte minore di Coin;
  • la posposizione dei progetti più controversi (Lambda, Jigsaw, e parte di Coin) alla versione Java SE 8, prevista per la fine del 2012.

Come poi si è avuto modo di vedere, anche queste scadenze erano alquanto ottimistiche, altro tipico difetto del personale informatico.

Vantaggi del piano B

L’obiettivo del piano B era di evitare una lunga attesa, prevista fino al 2013, per il rilascio di Java SE 7, che avrebbe significato ben sette anni dopo il rilascio della precedente major release, Java SE 6. La presentazione del piano B, come era lecito attendersi, suscitò ulteriori dibattiti tra i fautori di tale piano e coloro che invece volevano proseguire con il piano originale. In particolare, i fautori del piano B, utilizzavano i seguenti argomenti a favore della nuova strategia:

  • avere una nuova versione stabile facilmente gestibile, anche se di contenuti ridotti, era preferibile rispetto ad avere una versione con molte importanti variazioni, ma solo dopo un periodo prolungato;
  • release più frequenti avrebbero fornito un maggiore grado di flessibilità ai clienti permettendo una migliore e più graduale programmazione dell’integrazione delle nuove feature;
  • con una versione "intermedia" sarebbe stata più fluida l’evoluzione del linguaggio, grazie a feedback immediati, mirati e più facilmente gestibili; una release "definitiva" dopo tanti anni avrebbe incorporato una moltitudine di funzioni e quindi di potenziali bug;
  • le aziende sembrerebbero non gradire particolarmente approcci del tipo "let’s wait and ship everything" ("attendiamo e consegnamo tutto");
  • cambiamenti non eccessivamente radicali, oppure anche cambiamenti radicali, ma in numero molto limitato, avrebbero favorito il tasso di adozione;
  • il lavoro svolto fino a quel momento, soprattutto nell’area del progetto Coin e l’aggiornamento di NIO 2 già di per se’ avrebbe giustificato una nuova release;
  • il rilascio anticipato della versione Java SE 7 avrebbe generato una vitale "boccata di ossigeno" agli adetti ai lavori dei progetti Lambda e Jigsaw, allentando la pressione sugli sviluppatori;
  • rilasci più frequenti avrebbero permesso di diminuire e gestire meglio i fattori di rischio.

Svantaggi del piano B

A questi vantaggi, i detrattori del piano B, contrapponevano essenzialmente le seguenti motivazioni:

  • il grado di adozione della nuova versione poteva non essere così elevato come atteso: dal momento che la versione Java SE 8 avrebbe dovuto seguire a breve termine la Java SE 7, probabilmente molte aziende avrebbero finito per optare all’integrazione diretta di Java SE 8, saltando a piè pari la precedente release;
  • Java SE 7 veniva percepita come una versione senza sufficienti feature per essere considerata "major";
  • il piano originario A offriva una singola e solida versione invece di consegnare nuove feature a piccoli frammenti, il che doveva semplificare il processo di pianificazione e implementazione dell’adozione da parte delle aziende.

Dall’analisi delle precedenti argomentazioni è possibile evidenziare come i medesimi argomenti venivano utilizzati contemporaneamente sia in quanto punti a favore e che come obiezioni contrarie al piano B! Verosimilmente, Pirandello docet anche nei progetti informatici. Alla fine, come è ben noto, si decise di dar luogo al piano B: ciò nonostante, le date per le due release hanno comunque subito ulteriori importanti dilazioni.

 

Breve lista delle nuove feature

Le principali feature introdotte con Java SE 7 sono:

  • costrutti switch con stringhe;
  • estensione del costrutto catch per l’accettazione di eccezioni multiple;
  • gestione automatica delle risorse (try-with-resources);
  • eccezioni con controllo di tipo più preciso nella clausola throws;
  • miglioramento dell’inferenza di tipo;
  • miglioramento dei warnings e degli errori in compilazione relativi a formali non "reifiable" con i metodi varargs;
  • introduzione dei tipi letterali binari;
  • possibilità di utilizzo degli underscore nei letterali numerici;
  • integrazione più stretta dei puntatori compressi diventati default;
  • estensione della JVM per il supporto dei linguaggi dinamici;
  • introduzione di nuove feature nella libreria I/O;
  • ulteriore miglioramente del package della concorrenza (java.util.concurrent);
  • introduzione dell’algoritmo Elliptic Curve Cryptography (ECC, Crittografia basata sulle curve ellettiche);
  • introduzione di una pipeline grafica XRender per Java 2D;
  • aggiunta delle librerie per il supporto di nuovi protocolli come Stream Control Transmission Procotol (SCTP, Protocollo di trasmissione su stream controllato);
  • aggiornamento dello standard Unicode alla versione 6.0;
  • aggiornamento dei componenti dello stack XML alle versioni più recenti e stabili.

 

Progetto Coin

Le variazioni al linguaggio di programmazione sono state raggruppate e affidate al progetto Coin che include le prime 8 feature della lista di cui sopra [8]. Inizialmente erano presenti altre due feature: unsigned literals e nuove feature per semplificare la dichiarazione e la manipolazione delle collezioni che poi sono state posticipate alla versione Java SE 8. Le variazioni al linguaggio di Java SE 7 son state definite piccole (small) in quando non si poteva modificare la JVM.

Lo slogan di questo progetto era: "Project Coin is a suite of language and library changes to make things programmers do everyday easier." ("Il progetto Coin è un insieme di cambiamenti del linguaggio e della librerie, fatti al fine di semplificare le azioni che i programmatori svolgono quotidianamente").

Costrutto switch con stringhe

Prima della versione Java SE 7, i costrutti switch potevano valutare esclusivamente tipi di dato primitivi quali: byte, short, char e int. Con l’introduzione degli enumeration (J2SE 5.0, JSR 201, settembre 2004, cfr. [9]) il costrutto switch è stato esteso per poter valutare anche i literal che costituiscono gli enumeration, come mostrato nel listato seguente .

public class EnumerationSwitchSample {
 
       // ------------------- CONSTANTS SECTION -------------------
       /** logger */
       private static final Logger LOGGER = Logger.getLogger(EnumerationSwitchSample.class);
 
       // ------------------- ATTRIBUTES SECTION -------------------
       /** day of the week */
       private DayOfTheWeek dayOfWeek = null;
 
       // ------------------- METHODS SECTION -------------------
       /**
        * Constructor method
        * @param aDay  a day of the week 
        */
       public EnumerationSwitchSample(DayOfTheWeek aDay) {
             this.dayOfWeek = aDay;
       }
       /**
        * @return a description of the specific day
        */
       public String tellItLikeItIs() {
             String result = null;
             switch (dayOfWeek) {
                    case MONDAY:
                           result ="Mondays are horrible!";
                           break;
                                 
                    case FRIDAY:
                           result = "Fridays are good.";
                           break;
                                         
                    case SATURDAY:
                    case SUNDAY:
                           result = "Weekends are best.";
                           break;
                                       
                    default:
                           result = "Midweek days are so-so.";
                           break;
             }
             return result;
       }
 
       // ------------------------- INNER CLASS --------------------------------
       /**
        * This defines an enumeration that represents the days of the week
        */
       public static enum DayOfTheWeek {
             MONDAY,
             TUESDAY,
             WEDNESDAY,
             THURSDAY,
             FRIDAY,
             SATURDAY,
             SUNDAY
       }
       /**
        * Main method used to test the class: bad practice
        * @param args  list of arguments
        */
       public static void main(String[] args) {
 
             for (DayOfTheWeek aDayOfTheWeek : 
                              EnumerationSwitchSample.DayOfTheWeek.values()){
 
                    EnumerationSwitchSample aDay 
                                     = new EnumerationSwitchSample(aDayOfTheWeek);
                    LOGGER.info("Day of the week:"
                                     +aDayOfTheWeek+"..."+aDay.tellItLikeItIs());
             }
       }

L’esecuzione del listato appena mostrato, al netto delle informazioni del log, genera il seguente output:

 Day of the week:MONDAY...Mondays are horrible!
 Day of the week:TUESDAY...Midweek days are so-so.
 Day of the week:WEDNESDAY...Midweek days are so-so.
 Day of the week:THURSDAY...Midweek days are so-so.
 Day of the week:FRIDAY...Fridays are good.
 Day of the week:SATURDAY...Weekends are best.
 Day of the week:SUNDAY...Weekends are best.

L’ulteriore estensione Java SE 7 include la possibilità di eseguire la valutazione di stringhe come mostrato nel listato seguente:

       /**
        * return a description of the given day of the week
        * @param dayOfWeek a day of the week
        * @return a description of the specific day
        */
       public static String tellItLikeItIs(String dayOfWeek) {
 
             if (dayOfWeek == null) {
                    return null;
             }
 
             String result = null;
             String day = dayOfWeek.toUpperCase().trim();
 
             switch (day) {
                    case "MONDAY":
                           result ="Mondays are horrible!";
                    break;
 
                    case "TUESDAY":
                    case "WEDNESDAY":
                    case "THURSDAY":
                           result = "Midweek days are so-so.";
                    break;
 
                    case "FRIDAY":
                           result = "Fridays are good.";
                    break;
                                         
                    case "SATURDAY":
                    case "SUNDAY":
                           result = "Weekends are best.";
                    break;
                                       
                    default:
                           result = null;
                    break;
             }
 
             return result;
       }
 
      
       /**
        * Main method used to test the class: bad practice!
        * @param args  list of arguments
        */
       public static void main(String[] args) {
             String[] daysOfWeek = {
                    "MONDAY", "TUESDAY", "WEDNESDAY", 
                              "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"
             };
 
             for (String aDayOfTheWeek : daysOfWeek ) {
                    LOGGER.info("Day of the week:"+aDayOfTheWeek+
                    "..."+StringSwitchSample.tellItLikeItIs(aDayOfTheWeek));
             }
       }

L’output è ovviamente lo stesso, tuttavia, come si può notare, è difficile indicare la seconda versione di questo programma come una buona pratica da seguire... Pertanto, sebbene esistano degli scenari in cui il costrutto Switch con stringhe può fornire dei vantaggi, il suo utilizzo in molti casi dovrebbe essere scoraggiato rispetto alla versione più appropriata basata sugli enumerator.

Multi-catch

Il try-catch è uno dei costrutti fondamentali Java per la corretta gestione delle risorse e più in generale delle eccezioni. La struttura classica, prima della versione Java SE 7, prevede la seguente forma canonica:

       try {
             // istruzioni che possono generare eccezioni
             }
       } catch (ExceptionType1 e1) {
             // tentativo di gestione eccezione 1
       } catch (ExceptionType2 e2) {
             // gestione eccezione 2
       } finally {
             // possibilità di eseguire operazioni finali,
             // come la corretta chiusura degli stream
       }

In questa forma canonica i tentativi di gestione delle eccezioni, nella maggior parte dei casi, si risolvono nell’eseguirne il log e quindi demandare ai livelli superiori la gestione. Accade raramente che la parte di codice dove viene generata un’eccezione sia la più adatta a gestirla. Normalmente le informazioni necessarie per prendere le opportune contromisure si trovano a qualche livello superiore (questo argomento è trattato accuratamente nel libro [10], capitolo 5). La logica conseguenza è che, nella maggior parte dei casi, le istruzioni utilizzate per gestire l’eccezione di tipo 1 e di tipo 2 sono assolutamente identiche.

In considerazione di tale limitazione, con Java SE 7 si è definita una nuova sintassi che permette di raggruppare diverse eccezioni nella stessa clausola catch. La logica dietro questa introduzione è eviare che alcuni sviluppatori, per ridurre la quantità di codice da scrivere, ricorrano alla pericolosa tecnica di eseguire direttamente il catch di Exception.

Con Java SE 7 la struttura try-catch-finally può essere definita come segue:

       try {
             // istruzioni che possono generare eccezioni
       } catch (ExceptionType1 | ExceptionType2 e) {
             // gestione eccezioni
       } finally {
             // possibilità di eseguire operazioni finale,
             // come la corretta chiusura degli stream
       }

Ciò permette di rendere il codice più sintentico e in generale più elegante. Tuttavia è necessario stare attenti a diverse insidie. Un primo esempio è che spesso è necessario poter gestire diverse eccezioni di cui una è specializzazione dell’altra (per esempio FileNotFoundException e IOException). Chiaramente non è possibile inserire un’eccezione e la sua specializzazione nella stessa riga. Inoltre bisogna porre ben attenzione prima di includere diversi tipi di eccezione in un medesimo catch al fatto che diversi tipi di eccezioni potrebbero richiedere gestioni diverse. Come menzionato prima, la speranza è che un costrutto di questo tipo rimuova la cattiva practica di evitare lunghe sequence catch con un catch unico del tipo } catch (Exception e) {. In altre parole, si modifica lo stumento per evitarne un cattivo uso. Ecco un semplice esempio di struttura catch con multiple exception.

public class MultiCatchSample<T> {
 
       // ------------------- CONSTANTS SECTION -------------------
       /** logger */
       private static final Logger LOGGER   = Logger.getLogger(MultiCatchSample.class);
 
       // ------------------- ATTRIBUTES SECTION -------------------
 
       // ------------------- METHODS SECTION -------------------
 
       /**
        * This method returns the instance of a class given by its name
        * @param className  name of the class to use to generate new instances
        * @return newly created instance
        *            null in case of problems
        */
       public T getGenericInstance(String className) {
             T newInstance = null;
 
             try {
                    newInstance = (T) Class.forName(className).newInstance();
 
             } catch (ClassNotFoundException e) {
                    LOGGER.warn(e);
 
             } catch (InstantiationException e) {
                    LOGGER.warn(e);
 
             } catch (IllegalAccessException e) {
                    LOGGER.warn(e);
 
             } catch (ClassCastException e) {
                    LOGGER.warn(e);    
             }
 
             return newInstance;
       }
 
       /**
        * This method returns the instance of a class given by its name
        * @param className  name of the class to use to generate new instances
        * @return newly created instance
        *            null in case of problems
        */
       public T getGenericInstance2(String className) {
             T newInstance = null;
 
             try {
                    newInstance = (T) Class.forName(className).newInstance();
 
             } catch (ClassNotFoundException |
                            InstantiationException |
                            IllegalAccessException |
                            ClassCastException e) {
 
                    LOGGER.warn(e);    
             }
 
             return newInstance;
       }
 
 
       /**
        * Main method used to test the class: bad practice
        * @param args  list of arguments
        */
       public static void main(String[] args) {
 
             MultiCatchSample<String> aMultiCatchSample = new MultiCatchSample<>();
             String aString = aMultiCatchSample.getGenericInstance("java.lang.String");
             LOGGER.info("Result:"+aString+" class:"+aString.getClass().getCanonicalName()  );
 
             MultiCatchSample<Object> anotherMultiCatchSample = new MultiCatchSample<>();
             Object aObject = anotherMultiCatchSample.getGenericInstance2(
                                Object.class.getCanonicalName());
             LOGGER.info("Result:"+aObject+" class:"+aObject.getClass().getCanonicalName()  );
       }
}

Il listatino precedente mostra un esempio di un metodo che genera istanze vuote di classe fornitegli per mezzo del percorso della classe. Questo funziona correttamente solo nei casi in cui la classe preveda un costruttore vuoto. Da notare che qualora la gestione delle diverse eccezioni sia effettivamente lo stesso, la nuova versione del costrutto catch permette effettivamente di scrivere codice più conciso ed elegante.

Try-with-resource

Il nuovo costrutto try-with-resource, come suggerisce il nome, è un blocco try che dichiara una o più risorse, ossia oggetti che devono essere opportunamente chiuse dopo che il programma ne ha terminato l’utilizzo. L’istruzione try-with-resource assicura che ogni risorsa sia chiusa correttamente automaticamente alla fine della dichiarazione. L’intenzione di questa feature è di assicurarsi la scrittura di codice più sicuro e meno a rischio dal punto di vista di possibili memory leak. Qualsiasi classe che implementa java.lang.AutoCloseable, che comprende tutti gli oggetti che implementano java.io.Closeable, può essere utilizzata come risorsa del costrutto try-with-resource.

L’interfaccia AutoCloseable, introdotta con la versione Java SE 7, dichiara il solo metodo: void close() throws Exception. Si tratta ovviamente del metodo che viene invocato automaticamente sugli oggetti dichiarati nel costrutto try-with-resources. Come lecito attendersi, si occupa di chiudere correttamente l’oggetto liberando ogni risorsa gestita. Questo metodo può lanciare un’eccezione qualora non riesca a chiudere la risorsa. Sebbene la firma del metodo includa un’eccezione di tipo Exception (a questo livello di generalizzazione non si poteva fare altro) gli specifici oggetti scatenano eccezioni più specifiche.

Tutte le classi che gestiscono risorse implementano questa interfaccia. Alcuni esempi, giusto per menzionarne alcuni, sono: BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, ByteArrayInputStream, ByteArrayOutputStream, CharArrayReader, CharArrayWriter, CipherInputStream, CipherOutputStream, DatagramChannel, DatagramSocket, DataInputStream, DataOutputStream, DeflaterInputStream, DeflaterOutputStream, DigestInputStream, DigestOutputStream, FileCacheImageInputStream, FileCacheImageOutputStream, FileChannel, FileInputStream, FileLock, FileOutputStream, FileReader, FileSystem, FileWriter, FilterInputStream, FilterOutputStream, FilterReader, FilterWriter, Formatter, ForwardingJavaFileManager, GZIPInputStream, GZIPOutputStream, InputStream, InputStream, InputStream, InputStreamReader, JarFile, JarInputStream, JarOutputStream, LogStream, MemoryCacheImageInputStream, MemoryCacheImageOutputStream, MulticastSocket, ObjectInputStream, ObjectOutputStream, OutputStream, OutputStream, Pipe.SinkChannel, Pipe.SourceChannel, PipedInputStream, PipedOutputStream, PipedReader, PipedWriter, ProgressMonitorInputStream, PushbackInputStream, PushbackReader, RandomAccessFile, Reader, RMIConnectionImpl, RMIConnectionImpl_Stub, RMIConnector, RMIIIOPServerImpl, RMIJRMPServerImpl, RMIServerImpl, Scanner, SelectableChannel, Selector, SequenceInputStream, ServerSocket, ServerSocketChannel, Socket, SocketChannel, SSLServerSocket, SSLSocket, StringBufferInputStream, StringReader, StringWriter, URLClassLoader, Writer, XMLDecoder, XMLEncoder, ZipFile, ZipInputStream, ZipOutputStream.

Si consideri java.io.BufferedReader, una delle classi presenti fin dalle prime versioni di Java (JDK 1.1), che implementa le interfacce Closeable e AutoCloseable (anche Readable ma questo non è interessente per il contesto corrente). Come tale può essere usata nel nuovo costrutto try nel seguente modo:

try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {

Questo garantisce che l’oggetto BufferedReader sia chiuso (ne verrà invocato il metodo close) indipendentemente dal fatto che il il costrutto try sia eseguito normalmente o terminato bruscamente a seguito di un errore. In sostanza, questo statement equivale ad aggiungere la coppia di istruzioni: if (br != null), br.close() nella parte del costrutto finally. Sebbene questo costrutto dovesse sembrare abbastanza chiaro, ci sono i seguenti aspetti da tenere presente:

  • tipicamente sia i metodi all’interno del try sia l’invocazione del close (try-with-resource) possono scatenare delle eccezioni: nel caso in cui ciò avvenga, allora la prima eccezione generata (quella all’interno del try) viene lasciata sopravvivere mentre quella dell’invocazione close viene soppressa; da notare che un’altro aggiornamento effettuato con Java SE 7 consente di aggiungere e reperire le eccezioni soppresse grazie ai metodi Throwable.addSuppressed e Throwable.getSuppressed.
  • il costrutto try-with-resource può ospitare diverse risorse nella clausola try. In questo caso, l’invocazione dei metodi di chiusura segue un ordine inverso alla chiamata seguendo una logica a scatole cinesi. La comunicazione delle eccezioni segue la regola di cui al punto 1.

Il seguente listatino riporta un esempio di try-with-resource.

public class TryWithCatchSample {
 
       // ------------------- CONSTANTS SECTION -------------------
       /** logger */
       private static final Logger LOGGER   = Logger.getLogger(TryWithCatchSample.class);
 
       // ------------------- ATTRIBUTES SECTION -------------------
 
       // ------------------- METHODS SECTION -------------------
 
       /**
        * Copy the file content into a string
        * @param filePath   file path to copy
        * @return the string with the file content.
        *            null in case o problems
        * @throws IOException in case a problem occurs dealing with the given file
        */
       public static String getFileContent(String filePath) throws IOException {
 
             if ( (filePath == null) || (filePath.isEmpty()) ) {
                    return null;
             }
 
             StringBuilder strBuilder = new StringBuilder();
             BufferedReader br2 = null;
 
             try (BufferedReader br = new BufferedReader(new FileReader(filePath)); ) {
 
                    br2 = br;
 
                    String nextLine = null;
                    while ( (nextLine = br.readLine()) != null) {
                           strBuilder.append(nextLine);
                    }
             }   // finally not needed
 
             // ----- the following part is used only to demonstrate that
             // ----- the underlying BufferedReader is really closed
             try {
                    br2.ready();
             } catch (IOException ioe) {
                    LOGGER.info(ioe.getMessage());
             }
 
             return strBuilder.toString();
       }
 
       /**
        * Main method used to test the class: bad practice
        * @param args  list of arguments
        */
       public static void main(String[] args) {
 
             try {
                    String content = getFileContent(
                                    "C:\\prj\\java_se_7\\src\\resource\\log4J.properties");
                    LOGGER.info("content:"+content);
 
             } catch (IOException ioe) {
                    LOGGER.warn(ioe);
             }
       }
}

Il listato precedente non fa altro che copiare in una stringa, che successivamente viene mostrata nel log, il contenuto del file. Da notare che BufferedReader br2 è stata introdotta solo al fine di mostrare che dopo il ciclo try-with-resource l’oggetto BufferedReader viene effettivamente chiuso. Infatti, l’output mostra sia il contenuto del file (log4J.properties), sia il messaggio: "- Stream closed".

Ecco un try-with-resource multiplo.

       /**
        * Copy the entries present in the given zip file into the given output file
        * @param zipFileName       zip file to read
        * @param outputFileName  output file where to write the entries
        * @throws IOException  a serious problem is occurred
        */
       public static void   writeToFileZipFileContents(String zipFileName, 
                                                           String outputFileName)
                             throws IOException {
 
             if ( (zipFileName == null) || (zipFileName.isEmpty()) ) {
                    return;
             }
 
             if ( (outputFileName == null) || (outputFileName.isEmpty()) ) {
                    return;
             }
 
             Charset charset = StandardCharsets.US_ASCII;
             Path outputFilePath = Paths.get(outputFileName);
 
             // Open zip file and create output file with
             // try-with-resources statement
 
             try ( ZipFile zf = new ZipFile(zipFileName);
                      BufferedWriter writer = Files.newBufferedWriter(outputFilePath, 
                                                                      charset)) {
 
                    // Enumerate each entry
                    for (Enumeration<? extends ZipEntry> entries = zf.entries();
                                      entries.hasMoreElements();) {
 
                           // Get the entry name and write it to the output file
                           String newLine = System.getProperty("line.separator");
                           String zipEntryName = entries.nextElement().getName() + newLine;
                           writer.write(zipEntryName, 0, zipEntryName.length());
                    }
             }
       }

Aumento di precisione nel rilancio delle eccezioni

Con Java SE 7, il compilatore esegue un’analisi più precisa delle eccezioni rilanciate rispetto a quanto accadeva con le versioni precedenti. Ciò consente di specificare gli specifici tipi di eccezione nella clausola throws di una dichiarazione di metodo.

Si consideri il frammento di codice riportato di seguito. Come si può notare, vengono definite due nuove eccezioni che entrambe estendono la classe base Exception. Ora, il costrutto try è in grado di lanciare eccezioni dei due tipi; tuttavia, seguendo una pratica riprovevole, il blocco catch cerca di intercettare tutte le gestioni di tipo checked attraverso il costrutto catch (Exception e). All’interno dello stesso blocco c’è il rilancio dell’eccezione intercettata che quindi forza il metodo a dichiarare il throws Exception, ulteriore pratica riprovevole.

       public class MorePreciseReThrowSample {
 
             static class FirstException extends Exception {
             }
 
             static class SecondException extends Exception {
             }
 
             public void rethrowException(String exceptionName) throws Exception {
                    try {
                           if (exceptionName.equals("First")) {
                                  throw new FirstException();
                           } else {
                                  throw new SecondException();
                           }
                    } catch (Exception e) {
                           throw e;
             }
       }
}

Con Java SE 7, è possibile ancora dar luogo alla pratica riprovevole del catch (Exception e) e rilanciare l’eccezione intercettata throw e, ma in questo caso è possibile evitare che la firma del metodo venga inquinata da questa brutta pratica. Infatti, poiche’ le uniche eccezioni che possono essere generate sono FirstException e SecondException, è possibile dichiarare nella firma del metodo queste due eccezioni e non quella più generale Exception, con un aumento della precisione nel rilancio delle eccezioni, come mostrato nel listato seguente:

             public void rethrowException(String exceptionName)
                    throws FirstException, SecondException {
                    try {
                           // ...istruzioni che lanciano le eccezioni
                    } catch (Exception e) {
                           throw e;
             }
       }

Diamond syntax (la sintassi del diamante)

Il nome di questa feature si deve ai parametri di tipo senza parametri (<>) che servono per specificare il tipo delle classi generic: per la forma che descrive, la coppia di parentesi angolari viene informalmente chiamata diamante.

L’idea alla base di questa innovazione è relativa al fatto che in molti casi il compilatore è in grado di dedurre il corretto tipo degli argomenti per invocare il costruttore di una classe generica attraverso l’esame dell’utilizzo dei relativi oggetti. Questa operazione di deduzione eseguita dal compilatore si chiama inferenza di tipo (type inference). Si consideri per esempio, una dichiarazione del tipo:

       Map<String, List<String>> myMap = new HashMap<String, List<String>>();

In Java SE 7 essa può essere dichiarata semplicemente sostituendo ai tipi parametrizzati il famoso diamante:

       Map<String, List<String>> myMap = new HashMap<>();

Chiaramente bisogna porre attenzione a includere le parentesi angolari, in quando una dichiarazione del genere = new HashMap(); avrebbe un significato molto diverso. In particolare, si finirebbe per fare una dichiarazione di una collezione come si soleva fare prima dell’avvento dei generics. Come specificato sopra, il compilatore è in grado di eseguire l’inferenza di tipo per la creazione di istanze generiche solo nei casi in cui il tipo di parametri del costruttore sia evidente dal contesto.

Si consideri per esempio il seguente listato in cui il compilatore non è in grado di effettuare l’inferenza di tipo:

       List<String> list = new ArrayList<>();
       list.add("A");
 
       list.addAll(new ArrayList<>());

In questo caso il metodo addAll genera un errore di compilazione perche’ il metodo richiede un parametro di tipo Collection<? extends String>.

Sebbene l’inferenza di tipo possa essere utilizzata anche in casi diversi dalla sola creazione di oggetti, è consigliabile limitarne l’utilizzo a questi casi, onde evitare confusione.

Inferenza di tipo per metodi e costruttori generici

In Java è possibile dichiarare parametri di tipo nella firma dei metodi e dei costruttori per creare metodi generici e costruttori generici. Si tratta di una strategia simile alla dichiarazione di un tipo generico, con la differenza che la portata del parametro di tipo è limitata al solo metodo/costruttore in cui viene dichiarata. Si consideri il seguente listato:

public class TypeInferenceSample<T> {
 
       // ------------------- CONSTANTS SECTION -------------------
       /** logger */
       private static final Logger LOGGER   = Logger.getLogger(TypeInferenceSample.class);
 
       // ------------------- ATTRIBUTES SECTION -------------------
       private T attributeOfGenericType = null;
      
       // ------------------- METHODS SECTION -------------------
 
       /**
        * Set the specific object
        * @param attributeOfGenericType an instance of the specific type T
        */
       public void set(T attributeOfGenericType) {
             this.attributeOfGenericType = attributeOfGenericType;
       }
 
       /**
        * Get the instance of the specific object
        * @return
        */
       public T get() {
             return attributeOfGenericType;
       }
 
       /**
        * Inspect and log the two generic types: the class one and the method one
        * @param u  an instance of the method generic type
        */
       public <U> void inspect(U u) {
             LOGGER.info("T: " + attributeOfGenericType.getClass().getName());
             LOGGER.info("U: " + u.getClass().getName());
       }
 
       /**
        * Used to test...
        * @param args
        */
       public static void main(String[] args) {
             TypeInferenceSample<Integer> integerBox = new TypeInferenceSample<>();
             integerBox.set(new Integer(10));
             integerBox.inspect("Life is beatuful!");
       }
}

Come si può notare, si tratta di una classe che incapsula una variabile di tipo generico. Fin qui tutto normale.

La peculiarità di questa classe è rappresentata dalla presenza del metodo inspect il quale include nella propria firma un ulteriore tipo variabile <U>. Pertanto, come mostrato dal corpo del main, è possibile invocare questo metodo specificando una varaibile di tipo diverso, che in questo caso è una stringa. Per la precisione, l’invocazione del metodo non specifica il tipo stringa, il quale viene dedotto dal compilatore grazie al processo di inferenza di tipo. Come è lecito attendersi, l’output prodotto dall’invocazione del metodo main è:

       T: java.lang.Integer
       U: java.lang.String

Unchecked warning

La maggior parte dei tipi parametrizzati, come ArrayList<Number> e List<String>, sono definiti non-reifiable. Cosa significa? Significa che il compilatore rimuove le informazioni del tipo, applicando il processo chiamato erasure (cancellazione), e quindi queste non sono più disponibili in fase di esecuzione. Questa strategia, ampliamente utilizzata a partire da J2SE 5.0 è stata necessaria per assicurare la compatibilità binaria con le librerie Java e le applicazioi create prima dell’introduzione dei generics. Poiche’, in fase di compilazione, il processo di erasure rimuove le informazioni da tipi di parametri, questi tipi non sono reifiable.

L’inquinamento dell’heap (heap pollution), si verifica quando una variabile di un tipo parametrizzato si riferisce a un oggetto che non è di quel tipo parametrizzato. Questa situazione può verificarsi solo se il programma ha eseguito qualche operazione che darebbe luogo a un unchecked warning a tempo di compilazione. Questo tipo di warning è generato se, al momento della compilazione o in fase di runtime, la correttezza di un’operazione che coinvolge tipi parametrizzati non può essere verificata. Si consideri il seguente frammento di codice:

       List l = new ArrayList<Number>();
       List<String> ls = l;              // unchecked warning
       l.add(0, new Integer(42));  // unchecked warning
       String s = ls.get(0);         // ClassCastException!

In fase di compilazione, l’ArrayList<Number> e la List<String> perdono le informazion di tipo, quindi diventano, rispettivametne, ArrayList e List. Quindi, quando si cerca di assegnare la lista l a ls, il compilatore genera un unchecked warning giacche’ non è in grado di verificare, e tanto meno lo sarà la JVM, se la variabile l punta o meno a una lista di stringhe. In questo caso, poiche’ l punta a una lista di Number si genera l’inquinamento dello heap. Una situazione analoga si verifica con l’istruzione successiva. Nel caso del get il compilatore non genera alcun errore in quanto l’istruzione è legittima anche se poi a runtime si riceve una cast exception (un qualsiasi programmatore anche junior è in grado di vederne il problema a prima vista).

La domanda che potrebbe sorgere è perche’ il compilatore non generi un errore in uno scenario di inquinamento del tipo… La risposta ancora una volta è backwards compatibility: compabilità con le versioni precedenti.

Avvertimenti ed errori del compiler con parametri "non reifiable"

Ci sono degli errori e dei warning del compiler che si possono verificare quando si usino parametri formali "non reifiable" con metodi varargs. Si consideri la seguente classe:

public class NewWarningSample {
 
       public static <T> void addToList(List<T> listArg, T... elements) {
             for (T x : elements) {
                    listArg.add(x);
             }
       }
 
       public static void faultyMethod(List<String>... l) {
             Object[] objectArray = l; // ok
             objectArray[0] = Arrays.asList(new Integer(42));
             String s = l[0].get(0); // ClassCastException
       }
 
       public static void main(String[] args) {
 
             List<String> stringListA = new ArrayList<String>();
             List<String> stringListB = new ArrayList<String>();
 
             NewWarningSample.addToList(stringListA, "Seven", "Eight", "Nine");
             NewWarningSample.addToList(stringListA, "Ten", "Eleven", "Twelve");
             List<List<String>> listOfStringLists = new ArrayList<List<String>>();
             NewWarningSample.addToList(listOfStringLists, stringListA, stringListB);
 
             NewWarningSample.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
       }
}

Dalla versione Java SE 7 il compilatore è in grado di generare il seguente warning relativo all’istruzione ArrayBuilder.addToList:

         warning: [varargs] Possible heap pollution 
                                    from parameterized vararg type T

Quando il compilatore esegue il parsing di un metodo varargs, traduce il parametro formale varargs in un corrispondente array. Il problema è che Java non consente la costruzione diretta di array di tipi parametrizzati (a tal fine è necessario eseguire un work-around, come esempio dichiarare un array di oggetti e quindi farne il casting al tipo parametrizzato ). Nel metodo ArrayBuilder.addToList il compilatore traduce il parametro formale varargs T... elements nel corrispondente array parametrizzato T [] elements. Tuttavia, a causa del processo di erasure, il compilatore è costretto a convertire il parametro formale varargs nell’array di oggetti Object [] elements. Di conseguenza, si creano le basi per possibili inquinamenti dell’heap.

Da notare che, come da consuetudine, i warning possono essere soppressi grazie al ricorso alle correspondenti annotazioni, come per esempio @SuppressWarnings({"unchecked", "varargs"}).

Tipi letterali binari

Un’altra novità introdotta con Java SE 7 è la possibilità di specificare per i tipi interi: byte, short, int e long, valori espressi utilizzando il sistema numerico binario. A tal fine è necessario aggiungere il prefisso 0b o 0B al numero binario, come mostrato di seguito:

byte aByte = (byte)0b00100001;
short aShort = (short)0b1010000101000101;
int anInt1 = 0b10100001010001011010000101000101;
int anInt2 = 0b101;
int anInt3 = 0B101;
long aLong = 0b1010000101000101101000010100010110100001010001011010000101000101L;

Un’importante avvertenza consiste nel ricordare che in Java il type byte è signed, quindi il primo bit ("the most significant bit") è utilizzato per specificare il segno. Quindi bisogna porre attenzione a dichiarazioni del tipo (byte)0b10100001 che generano un valore pari a -95.

Questa feature può risultare molto utile qualora sia necessario eseguire degli interfacciamenti con driver/componenti a basso livello, sia quando si vuole gestire dei byte attraverso opportune maschere binarie.

Introduzione degli underscore nei letterali numerici

Java SE 7 introduce anche la possibilità di includere un numero qualsiasi di caratteri di sottolineatura (_) tra le cifre di un valore letterale numerico. Ciò permette di raggruppare cifre in valori numerici, che possono migliorare la leggibilità del codice. Per esempio, se il codice contiene numeri con molte cifre, è possibile utilizzare un carattere di sottolineatura per separare le cifre in gruppi di tre, in modo simile a come si userebbe un punto delle migliaia.

Di seguito sono riportati alcuni modi possibile utilizzare il carattere di sottolineatura in letterali numerici:

long creditCardNumber      = 1234_5678_9012_3456L;
long socialSecurityNumber  = 999_99_9999L;
float pi                   = 3.14_15F;
long hexBytes              = 0xFF_EC_DE_5E;
long hexWords              = 0xCAFE_BABE;
long maxLong               = 0x7fff_ffff_ffff_ffffL;
byte nybbles               = 0b0010_0101;
long bytes                 = 0b11010010_01101001_10010100_10010010;

Da notare che è possibile inserire caratteri underscore solo tra le cifre, mentre non è possibile inserirli nelle seguenti parti:

  • all’inizio o alla fine di un numero
  • accanto a un punto decimale in una costante in virgola mobile
  • prima di un suffisso F o L
  • nelle posizioni in cui ci si aspetta una stringa di digits

Qui di seguito sono riportati esempi che generano errori di compilazione:

float pi1 = 3_.1415F;             // Errore: underscore vicino al punto decimale
float pi2 = 3._1415F;             // Errore: come sopra
long socialSecurityNumber1 = 999_99_9999_L; // Errore: underscore prima del suffisso
int x1 = _52;                     // Errore: _52 è utilizzato per indicare una variabile
int x3 = 52_;                     // Errore: non si può inserire un underscore alla fine
int x5 = 0_x52;                   // Errore: underscore nel prefisso 0x
int x6 = 0x_52;                   // Errore: underscore all’inizio di un numero
int x8 = 0x52_;                   // Errore: underscore alla fine di un numero
int x11 = 052_;                   // Errore: underscore alla fine di un numero

 

Conclusione

Java SE 7.0 è un’importante major release per diversi punti di vista, non sempre di carattere tecnico. In primo luogo perche’ segue il rilascio open source dell’OpenJDK. Tale apertura tuttavia ha escluso le Java Class Library. In secondo luogo perche’ si è trattato della prima versione gestita da Oracle dopo l’acquisizione di Sun Microsystems. Anche se Java è da tempo (Febbraio 2002, J2SE 1.4) gestita da un’apposita community indipendente, attraverso processi aperti, molti sanno bene che Sun Micorsystems prima, e Oracle poi hanno sempre esercitato un certo controllo sullo sviluppo di questa tecnologia sempre più strategica per molte aziende. Non a caso nel dicembre 2010 Apache Software Foundation, fucina di idee innovative e framework per la tecnologia Java, decise di dimettersi dal JCP Executive Commette in aperta polemica rispetto l’egemonia esercitata da Sun.

Java SE 7.0 è stata rilasciata ben dopo 5 anni dalla precedente versione (tempi biblici per l’informatica), con una serie di cambiamenti che riguardano anche il linguaggio stesso e non sempre completamente migliorativi. Il progetto che si è occupato dell’aggiornamente del linguaggio è stato battezzato Coin, analizzato in dettaglio in questo articolo, e alcune feature sono state rimandate alla versione Java SE 8 come l’introduzione delle Lambda Expression e la modularizzazione del JDK. Questo doveva rappresentare la grande "rivoluzione" del linguaggio Java, ma per via di un acceso dibattito all’interno della comunità si è saggiamente deciso di posticiparle alla versione 8 (attuazione del famoso piano B) per non ritardardare ulteriormente il rilascio della release Java SE 7.

I problemi sorti all’interno dei progetti Lambda e Jigsaw evidenziano quelli che possono essere considerati gli effetti collaterali della gestione del disegno di soluzioni per "consenso allargato" (e non sempre guidato da un’agenda prettamente tecnica). Se da un lato valutare soluzioni tecniche attraverso diversi punti di vista ha la potenzialità di migliorarne il disegno, permettendo di valutare anche aspetti non valutati inizialmente, quando il consenso coinvolge un insieme allargato di tecnici, allora si corre il rischio di diladare eccessivamente la tempistica e quindi di generare molta frurstrazione (ogni singolo punto viene discusso all’infinito) e, in casi estremi, di approdare a soluzioni tecnicamente povere, realizzate al solo scopo di raggiungere il compromesso necessario per uscire da situazioni di ristagno.

Le variazioni del linguaggi analizzate in questo articolo sono costrutti switch con stringhe, estensione del costrutto catch per l’accettazione di eccezioni multiple, gestione automatica delle risorse (try-with-resources), eccezioni con controllo di tipo più preciso nella clausola throws, miglioramento dell’inferenza di tipo, miglioramento dei warnings ed errori in compilazione relativi a formali non "reifiable" con i metodi varargs, introduzione dei tipi letterali binari, possibilità di utilizzo degli underscore nei letterali numerici.

Alcune delle modifiche apportate al linguaggio, non sono state realizzate perche’ il linguaggio stesso avesse dei veri problemi da dover risolvere, bensì per evitare che programmatori non molto preparati possano commettere errori seri come memory leak, cattura e rilancio di eccezioni di tipo Exception, e così via.

 

Riferimenti

[1] Luca Vetti Tagliati, "L’evoluzione di Java: verso Java 8 - I parte: il caffè… da una quercia",  MokaByte 171

http://www2.mokabyte.it/cms/article.run?articleId=UYD-5F3-9MO-VGH_7f000001_5290188_f2bc5f1d

 

[2] JDK 7 Milestones

http://openjdk.java.net/projects/jdk7/milestones/

 

[3] OpenJDK

http://openjdk.java.net/

 

[4] James Gosling, "Time to move on…"

http://nighthacks.com/roller/jag/entry/time_to_move_on

 

[5] Il nuovo Blog di James Gosling

http://nighthacks.com

 

[6] Paul Krill for InfoWorld, "Java founder Gosling leaves Google for startup"

http://ow.ly/aO77l

 

[7] Mark Reinhold, "There’s not a moment to lose!"

http://mreinhold.org/blog/rethinking-jdk7

 

[8] Progetto Coin

http://openjdk.java.net/projects/coin/

 

[9] JSR 201: Extending the JavaTM Programming Language with Enumerations, Autoboxing, Enhanced for loops and Static Import

http://jcp.org/en/jsr/detail?id=201

 

[10] Luca Vetti Tagliati, "Java Best Practice. I migliori consigli per scrivere codice di qualità"

http://www.mokabyte.it/2015/09/03_java_best_practice/

 

 

Condividi

Pubblicato nel numero
173 maggio 2012
Luca Vetti Tagliati ha conseguito il PhD presso il Birkbeck College (University of London) e, negli anni, ha applicato la progettazione/programmazione OO, Component Based e SOA in diversi settori che variano dal mondo delle smart card a quello del trading bancario, prevalentemente in ambienti Java EE. Dopo aver lavorato a…
Articoli nella stessa serie
Ti potrebbe interessare anche