Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Project Coin: le novità di Java 7

Introduzione alle novità del linguaggio java 7, introdotte dal Project Coin
Introduzione alle novità del linguaggio java 7, introdotte dal Project Coin
Link copiato negli appunti

La versione 7 di Java è nata a quasi cinque anni di distanza dalla versione 6.
Un’enormità se si pensa che dal 1995 (anno di nascita di Java 1.0) al 2006 sono nate ben 6 major version, con una media di pubblicazione inferiore ai due anni. Ma in questo caso si tratta della prima major version targata Oracle che, dopo l'acquisizione di Sun, ha avuto difficoltà a mantenere i tempi promessi. Ecco quindi che la data di rilascio è slittata più volte clamorosamente (Java 7 doveva uscire inizialmente nell’autunno del 2008!), e sono stati apportati diversi tagli sulle “new feature” previste originariamente e che ora vedremo soltanto con Java 8. Non è stato facile per Oracle raccogliere l'eredità di Sun, ma l'attesa è finita, ed è tempo di vedere all'opera la nuova versione.

Nonostante diverse polemiche che hanno accompagnato la sua realizzazione, la versione 7 (anche denominata “Dolphin”) ha portato con sé diverse novità interessanti. Il linguaggio ha subito modifiche dall'impatto minore rispetto alla realizzazione della versione 5 (dove ricordiamo tra l'altro l'introduzione delle enumerazioni, le annotazioni e i tipi generici). La semplicità di questi “piccoli cambiamenti” favorirà probabilmente il loro utilizzo, e in breve tempo il nostro modo di scrivere codice cambierà.

In questo articolo ci occuperemo quindi di descrivere essenzialmente solo questi “piccoli cambiamenti”, che sono nati sotto il nome di Project Coin. Tutte queste novità sono state suggerite infatti direttamente da sviluppatori Java da ogni parte del mondo.
Tralasceremo per ora quindi la descrizione di tante altre novità che riguardano performance e librerie, anche se queste rappresentano ulteriori validissimi motivi per migrare alla nuova versione. In particolare ricordiamo un nuovo framework denominato significativamente “Fork/Join” per la gestione dei Thread, l'evoluzione di JDBC alla versione 4.1 per accessi ai database, un modo tutto nuovo e rivoluzionario per gestire l'input-output con la nuova libreria NIO 2.0, e per quanto riguarda la grafica un nuovo look'n'feel chiamato Nimbus, l'introduzione della classe JLayer e la possibilità di mixare sulla stessa interfaccia component componenti grafici heavyweight con lightweight.

Ma ora concentriamoci sul linguaggio.

Novità per la rappresentazione dei tipi numerici

Sono essenzialmente due le novità per quanto riguarda la rappresentazione dei tipi numerici:

  1. L'introduzione della notazione binaria
  2. La possibilità di utilizzare il carattere “_” (underscore) per migliorare la leggibilità dei numeri.

Java ha sempre supportato ovviamente la notazione decimale, ma anche quella ottale (mediante l'utilizzo del prefisso “0”) e quella esadecimale (mediante l'utilizzo del prefisso “0x”). Nei rari casi in cui si aveva bisogno della notazione binaria (per esempio in particolari situazioni di input output in cui c'era bisogno di analizzare i dati bit per bit) però, eravamo costretti ad utilizzare il metodo statico parseByte() della classe Byte. Per esempio se volevamo utilizzare il numero 01010110, potevamo scrivere:

int i = Byte.parseByte(“01010110”);

Ma questo ovviamente ci obbligava a chiamare un metodo e passargli una stringa per ottenere un semplice intero. Altra tecnica era per esempio quella di scrivere il numero utilizzando un'altra rappresentazione (per esempio la decimale o la esadecimale), accompagnando l’istruzione con un commento esplicativo. Per esempio:

int i = 86; //01010110

oppure

int i = 0x56; //01010110

Risulta evidente che tecniche possono facilmente portare alla creazione di bug di difficile risoluzione. Java 7 inizia a semplificarci la vita mettendoci a disposizione il prefisso “0b” per usare la rappresentazione binaria:

int i = 0b01010110;

Tutto molto semplice.

L'altra novità per i tipi numerici, riguarda la loro leggibilità. È stata infatti introdotta la possibilità di utilizzare l'underscore come separatore all'interno dei numeri. Quante volte abbiamo dovuto contare le cifre di un numero alto come il seguente, per capire di che numero si tratta:

long aLong = 1000000000000L;

Con Java 7 possiamo riscrivere il numero precedente sfruttando qualche underscore per renderlo più leggibile:

long aLong = 100_000_000_000L;

Così è facile leggere anche cento miliardi! Ovviamente l'underscore può aiutare la leggibilità anche per gli altri formati (per esempio il formato binario):

short s = 0b01101101_11000101;

Switch e stringhe

Da sempre è possibile utilizzare come variabile di test per il costrutto switch interi, byte, short e caratteri. Un bel passo avanti per tutti gli sviluppatori si ebbe con l'introduzione della possibilità di utilizzare le enumerazioni per Java 5. Ora in Java 7 viene fatto un passo ulteriore: sarà possibile utilizzare anche il tipo di dato String. Sarà finalmente legittimo scrivere un metodo come il seguente:

public static boolean getBoolean(String string) {
  switch(string) {
    case "YES":
      return true;
    case "NO":
      return false;
  }
  throw new IllegalArgumentException(string);
}

Ovviamente utilizzare una stringa come variabile di test apre molti più scenari di utilizzo rispetto a prima, ma come sempre, con le novità bisogna stare attenti.

Notare come abbiamo aggiunto il lancio dell'eccezione alla fine del metodo precedente: un metodo del genere può essere facilmente utilizzato senza che il parametro string sia settato a “YES” o “NO”. Questa è esattamente la filosofia inversa a quella delle enumerazioni, dove abbiamo una lista precisa di possibili valori "controllati", cioè conosciuti a priori perché definiti nell'enumerazione stessa. Addirittura se uno dei valori possibili dell'enumerazione non è utilizzato come clausola case, il compilatore lancerà un warning.

Qualcuno quindi storcerà il naso dopo aver visto un codice come quello dell'esempio precedente, e potrebbe voler evitare di utilizzare le stringhe in un costrutto switch. Con l'utilizzo di un'enumerazione lo switch diventa più sicuro e robusto e non c'è bisogno di lanciare eccezioni. Ma è anche vero che c'è più codice da scrivere e si sa che meno codice si scrive e meno probabilità avremo di introdurre dei nuovi bug!

Ecco come potremmo ad esempio trasformare il codice precedente usando un'enumerazione:

public enum Choice {
  YES, NO;
}
public static boolean getBoolean (Choice choice) {
  switch(choice) {
    case YES:
      return true;
    case NO:
      return false;
  }
}

Inoltre per chiamare il metodo getBoolean(), avremo comunque probabilmente bisogno di controlli come questi:

Choice choice = null;
if (string != null && string.equals(“YES”)){
  choice = Choice.YES;
} else if (string != null && string.equals(“NO”)){
  choice = Choice.NO;
} else {
    throw new IllegalArgumentException(string);
}
boolean result = getBoolean(choice);

È evidente se c'è bisogno di testare una stringa, è spesso conveniente testarla direttamente piuttosto che utilizzare un'enumerazione “wrapper

.

Multi catch e rilancio di eccezioni

Quando gestiamo le eccezioni in Java, capita spessissimo di scrivere più volte lo stesso codice in clausole catch differenti. Per esempio:

try {
  . . .
} catch (MyException1 ex) {
   LOGGER.log(Level.SEVERE, ex);
   throw ex;
} catch (MyException2 ex) {
   LOGGER.log(Level.SEVERE, ex);
   throw ex;
}

A volte lo sviluppatore viene tentato dall'evitare i copia incolla, e finisce con il generalizzare il parametro del catch con la superclasse Exception:

try {
  . . .
} catch (MyException1 ex) {
   LOGGER.log(Level.SEVERE, ex);
   throw ex;
}

Ovviamente così si perde l'informazione su quali eccezioni possono essere lanciate nel blocco try, e quindi la precedente non si può dichiarare una soluzione corretta.

Java 7 introduce una nuova sintassi, che consente di dichiarare più parametri all'interno di un unico blocco catch. In pratica, l'esempio precedente in Java 7 si scriverà così:

try {
  . . .
} catch (MyException1 | MyException2 ex) {
   LOGGER.log(Level.SEVERE, ex);
   throw ex;
}

Quindi sintassi più compatta, ma un po' insolita vista la presenza di un operatore “OR” tra i nomi di due classi. Ad ogni modo l'obiettivo di poter evitare duplicazioni è raggiunto, ed in maniera tutto sommato semplice.

E' da notare inoltre come il reference ex, all’interno del blocco catch potrà essere sia di tipo MyException1 che di tipo MyException2 al runtime.

Java 7 ha introdotto anche un'altra nuovo comportamento al runtime che riguarda la gestione delle eccezioni, ed in particolare il lancio (throw) di un tipo di eccezione e la relativa dichiarazione (throws). Si tratta sicuramente di un modifica che sfrutteremo relativamente poco, con un impatto non paragonabile a quello che avrà sul nostro codice il multi-catch.

Supponiamo di avere un metodo che potrebbe lanciare varie tipologie di eccezioni come il seguente:

public void throwExceptionWithJava7(int i) throws Exception {
  try {
    if (i == 0) {
      throw new MyException1();
    } else if (i == 1){
      throw new MyException2();
    }
  } catch(Exception exc) {
    throw exc;
  }
}

Notare come nonostante sia evidente che verranno lanciate eccezioni di tipo MyException1 oppure MyException2, non abbiamo potuto dichiarare nella clausola throws queste classi perché il compilatore non ce lo avrebbe permesso, visto che il rilancio dell'eccezione che avviene all'interno del catch, usa un tipo Exception. Questo porterà il programmatore che chiama questo metodo a scrivere del codice supplementare per distinguere il caso in cui l'eccezione rilanciata sia MyException1 o MyException2 (magari sfruttando l'operatore instanceof...). Con Java 7 è invece stato introdotto un controllo al runtime che capirà al volo quali eccezioni verranno lanciate. Questo permetterà al programmatore di dichiarare nella clausola throws MyException1 e MyException2 in questo modo:

public void throwExceptionWithJava7(int i) throws MyException1, MyException2  { . . . }

e quindi evitare codice supplementare in fase di cattura dell'eccezione.

Insomma, si tratta di una novità abbastanza secondaria, la cui utilità sarà limitata a pochi casi circoscritti, ma comunque utile.

Try with resources

Questa è forse la novità più importante di Java 7. Con l’introduzione di questa nuova caratteristica, il programmatore Java sarà finalmente esonerato dal dover gestire la chiusura di risorse che hanno bisogno di essere chiuse dopo il loro utilizzo. Parliamo di istanze di classi come java.net.Socket, java.sql.Connection oppure java.io.InputStream, insomma oggetti che si utilizzano spessissimo.

Prima di dare uno sguardo alla nuova sintassi, occupiamoci di ciò che abbiamo fatto sino ad ora quando avevamo a che fare con oggetti che necessitavano di essere "chiusi".
Facciamo subito un piccolo esempio pratico:

import java.sql.*;
public class TryWithResources {
    public void selectStarFromDB() {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            conn = DriverManager.getConnection(“url”, “username”,
             “password”);
            stmt = conn.createStatement();
            rs = stmt.executeQuery(“SELECT * FROM DIPENDENTE”);
            while (rs.next()) {
                System.out.println(rs.getString(1));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            rs = null;
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            stmt = null;
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                conn = null;
            }
        }
    }
}

Analizziamo velocemente il codice precedente. Nel metodo selectStarFromDB(), vengono inizialmente le variabili conn (di tipo java.sql.Connection), stmt (di tipo java.sql.Statement) e rs (di tipo java.sql.ResultSet). Dopo essere state usate nella clausola finally vengono anche chiuse.

Ricordiamo che la clausola finally verrà eseguita appena prima che il metodo termini, sia che termini per il lancio di un'eccezione, sia se l'esecuzione prevista va a buon fine. Proprio per questa ragione abbiamo dovuto aggiungere un'altra struttura di blocchi try-catch che circondano l'istruzione di chiusura della risorsa.
Infatti nel momento in cui viene chiamato il metodo close(), non possiamo essere sicuri che la chiusura vada a buon fine (l'oggetto in questione potrebbe non essere mai stato aperto nel caso fosse scattata un'eccezione prematuramente). Il codice scritto quindi è a dir poco noioso, ma necessario!

Se non si chiude un oggetto da chiudere, quello semplicemente non verrà mai eliminato dalla memoria (in questi casi si parla di “memory leaks”), e questo può portare alla saturazione della memoria stessa. Un oggetto non chiuso semplicemente non lancerà eccezioni, né warning. Quindi ce ne accorgeremo solo quando leggeremo il famigerato “out of memory error” da qualche parte. La risoluzione dei memory leaks può essere complessa e spesso richiede l'utilizzo di strumenti per il monitoring della memoria (come quello offerto dal JDK: JVisualVM).

Con Java 7 sarà possibile riscrivere la classe dell'esempio precedente nel modo seguente:

import java.sql.*;
public class TryWithResources {
    public void selectStarFromDB() {
        try(
            Connection conn = DriverManager.getConnection("url", "username", "password");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("SELECT * FROM DIPENDENTE ")
        ){
            while (rs.next()) {
                System.out.println(rs.getString(1));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

La sintassi prevede la dichiarazione degli oggetti da chiudere automaticamente come parametri del blocco try. Il codice è notevolmente più compatto, ma soprattutto è più facile programmare!

Deduzione automatica del tipo

La “Type inference for generic instance creation” è una nuova caratteristica di Java 7 che potremmo tradurre come “deduzione automatica del tipo per la creazione di un istanza generica”. Si tratta in realtà di un argomento molto semplice.
Ricordiamo che il progetto “Project Coin” si è cercato in base alla segnalazione degli sviluppatori di semplificare la vita al programmatore: scrivere meno codice e scrivere codice semplice, implica meno bug. La deduzione automatica permetterà allo sviluppatore di evitare di scrivere codice superfluo quando si dichiara ed istanzia un tipo generico. Passiamo subito ad un esempio, consideriamo la seguente dichiarazione:

ArrayList<String> arrayList;

Per istanziare questo arraylist, sino ad ora siamo stati costretti a scrivere:

ArrayList<String> arrayList = new ArrayList<String>();

ora con Dolphin è possibile utilizzare l'operatore "diamond" (si potrebbe tradurre "rombo", infatti la parola "diamonds" in inglese indica anche il segno dei "quadri" delle carte da gioco, che hanno appunto la forma di un rombo). In pratica è possibile omettere i parametri per l'oggetto istanziato come nel seguente esempio, ma specificando le parentesi acute vuote (che formano un rombo):

ArrayList<String> arrayList = new ArrayList<>();

Il compilatore in realtà genererà il codice giusto nel bytecode, perché potrà dedurre dalla dichiarazione dell'oggetto arrayList il tipo del parametro.

Segue un altro esempio più interessante:

Map<String, Set<String>> hashMap = new HashMap<String, Set<String>>();

Può ora essere riscritto come:

Map<String, Set<String>> hashMap = new HashMap<>();

E così possiamo ora scrivere qualche altra riga in meno.

Conclusioni

Java 7 ha portato con sé diverse novità per quanto riguarda il linguaggio.

Qualcuna come la deduzione automatica del tipo, il multi-catch e il try-with-resources sarà usata massicciamente, altre come il riconoscimento del tipo di eccezione da rilanciare, oppure la rappresentazione binaria dei numeri indubbiamente di meno.

Ciò che è evidente è che si è cercato di migliorare il linguaggio in maniera moderata, senza sconvolgere ciò che già esisteva e favorendo così la migrazione alla nuova versione. Il linguaggio risulta ora un po' più dinamico, e se questa è la direzione che si è deciso di prendere, ben venga: chiunque vorrebbe scrivere meno codice e sbagliare di meno... iniziare ad utilizzare da subito le nuove feature quindi, sarà semplice questa volta, e soprattutto molto conveniente!


Ti consigliamo anche