mer 09 ottobre 2024 - Lo Sviluppatore  anno VI

Java 8 Best Practices

Condividi

Java 9 è ancora in fase di sviluppo e al momento dobbiamo utilizzare ancora per un pò Java 8 che già
comunque ha segnato una evoluzione fondamentale nello sviluppo del linguaggio, con l’introduzione di nuove features molto interessanti.
Riflettiamo su di esso e parliamo delle best practice che sono naturalmente cresciute dal momento in cui Java 8 è stato rilasciato.
In questo articolo vedremo i temi caldi del linguaggio Java 8 ed in particolare vedremo: i metodi predefiniti,  le lambda expression e gli Stream e gli Optional per gestire i valori NULL.

Scarica anche la piccola guida riassuntiva di un foglio, una comoda reference stampabile sugli argomenti trattati:

Java 8 Best Practices Cheat Sheet

I metodi predefiniti

La possibilità di dare una implementazione di default nei metodi dichiarati in una interfaccia è stata introdotto con il JDK 8. Prima dell’introduzione di questa feature era impossibile aggiungere un nuovo metodo ad una interfaccia senza richiedere a tutte le classi che la implementavano di dare una propria implementazione del nuovo metodo. Invece a partire dalla versione 1.8 del JDK è possibile fornire una implementazione predefinita per i metodi mediate l’uso della keyword default. In questo modo è possibile estendere un’interfaccia senza “rompere” la compatibilità con eventuali classi preesistenti che la implementano.

La regola principale empirica per l’utilizzo dei metodi predefiniti è quello di non abusare del loro utilizzo per non rendere il codice più incasinato di quanto sarebbe stato senza di esso. L’uso tipico ed opportuno di questa feature è, ad esempio,  quando si desidera aggiungere una sorta di funzionalità comune a delle classi Java senza “inquinare” la loro gerarchia con una superclasse comune, allora in questo caso si può prendere in considerazione la creazione di un’interfaccia separata con il/i metodi che implementano la funzionalità in comune. Ecco un esempio di un’interfaccia chiamata Debuggable che utilizza l’API di reflection per ottenere l’accesso ai campi dell’oggetto e fornisce un metodo debug()  che stampa i valori dei campi:

 

public interface Debuggable {
  default String debug() {
    StringBuilder sb = new StringBuilder(this.getClass().getName());
    sb.append(" [ ");
    Field[] fields = this.getClass().getDeclaredFields();
    for(Field f: fields) {
      f.setAccessible(true);
      try {
        sb.append(f.getName() + " = " + f.get(this));
        sb.append(", ");
      } catch (IllegalArgumentException | IllegalAccessException e) {
        throw new RuntimeException(e);
      }
    }
    sb.append("]");
    return sb.toString();
  }
   
  public String warning(String msg);
  public String severe(String msg);
}

Qui la parte importante è la parola chiave default sulla firma del metodo. Sotto una implementazione specifica dell’interfaccia Debuggable vista sopra:

public class DebuggableClass implements Debuggable {
  int a = 100;
  String b = "Hello";
public String warning(String msg){
  // implementazione specifica  
  ............
}

public String severe(String msg){
  // implementazione specifica  
  ............
}

 public static void main(String[] args) {
   DebuggableClass dc = new DebuggableClass();
   System.out.println(dc.debug());
 }
}

Questa classe eredita l’implementazione di default del metodo debug e il risultato dell’esecuzione del metodo main sarà:

DebuggableClass [ a = 100  b = Hello ]

 Una cosa degna di nota in questo contesto è quello dell’uso delle cosidette interfacce funzionali, cioè interfacce che dichiarano un singolo metodo astratto. In presenza di un’interfaccia del genere l’uso di uno o più metodi di default non “rompe” questo tipo di contratto.  Sotto un esempio di Interfaccia funzionale, come si può vedere abbiamo un unico metodo astratto e un metodo di default:

@FunctionalInterface
public interface SimpleInterface { 
 public void faiQualcosa(); 

 // Un metodo di default 
 default public void faiQulcosAltro(){ 
     System.out.println("Implementazione di faiQulcosAltro nell'Interfaccia"); 
 } 
}

Lambdas e Streams

Per anni Java ha ricevuto l’etichetta di non essere un linguaggio di programmazione appropriato per le tecniche di programmazione funzionale, perché le funzioni non sono state mai considerate come parte importante del linguaggio. In effetti, non c’era un modo pulito e accettato di fare riferimento a un blocco di codice mediante un nome o di pasarlo come argomento ad esempio in un metodo. Con l’ntroduzione delle Lambda nel JDK 8, tutto è però cambiato. Ora possiamo usare i method references per fare riferimento ad un metodo specifico, assegnare funzioni a variabili, comporre funzioni e passarle come argomenti potendo così godere di tutti i vantaggi che il paradigma di programmazione funzionale offre.


Per rendere le cose più semplici quando si passano le funzioni come parametri, possiamo usare una interfaccia funzionale, con un solo metodo astratto.
Ci sono un sacco di interfacce nel JDK create a posta per coprire quasi ogni caso: funzioni vuote,  funzioni senza parametri, funzioni normali che hanno sia i parametri che i valori di ritorno. Ecco un assaggio di come il vostro codice potrebbe essere utilizzando la sintassi lambda:

// prende in input un Long e restituisce una Stringa
Function<Long, String> f = (l) -> l.toString(); 

// Non prende nulla in input e restituisce Threads
Supplier<Thread> s =Thread::currentThread; 

// Prende una stringa come parametro 
Consumer<String> c = System.out::println; 

L’avvertenza è che il codice è difficile da gestire se si abusa nell’uso delle funzioni anonime. Pensate ad una lambda expression la cui parte desta è molto complessa… il codice ne perderebbe sicuramente in leggibilità. L’uso più adatto delle Lambda è per la definizione di piccole funzioni che elaborano dei dati.  Il codice specifica il flusso di dati e basta collegare le funzionalità specifiche che si desidera eseguire.  Vediamo qui come l’API Stream entra in gioco. Ecco alcuni esempi:

// per usare gli streams
new ArrayList<String>().stream(). 
// peek: debug streams senza fare alcuna modifica
peek(e -> System.out.println(e)). 
// map: converte ogni elemento in qualcosa'altro
map(e -> e.hashCode()). 
// filter: fa passare solo alcuni elementi
filter(e -> ((e.hashCode() % 2) == 0)).   
// trasforma uno stream in una Collection 
collect(Collectors.toCollection(TreeSet::new))

Il codice sopra mi sembra abbastanza auto esplicativo. no? 🙂
In generale, quando si lavora con gli stream, si trasformano i valori contenuti nello stream mediante le funzioni che prevedono la sintassi lambda. Suggerimenti per l’uso delle lambda sono:

  • Se l’espressione lambda è superiore a 3 linee di codice – dividilo in più invocazioni successive di map() che elaborano i dati per passi successivi o estrai un metodo e utilizza il suo riferimento .
  • Non assegnare lambda e funzioni ai campi degli oggetti. Le lambda rappresentano funzioni e queste servono, diciamo, così come sono.

Per una trattazione più esaustiva sulle lambda expression potete leggere questo articolo

java.util.Optional

Optional è un nuovo tipo in Java 8 che fa il wrapping di o un valore o un null, per rappresentare l’assenza di un valore. Quante volte scrivendo codice java ci si trovava a fare cose del tipo if (x != null) …. invece con gli Optional la gestione dei valori nulli avviene in maniera più pulita, diciamo, e  non c’è più bisogno di controllare in modo esplicito per valori null.

Optional è un tipo monadico a cui è possibile associare funzioni  per trasformare il valore al suo interno . Ecco un semplice esempio: Immaginate di avere una chiamata ad una API che potrebbe restituire un valore o un null,  tale valore deve essere poi elaborato mediante la chiamata al metodo transform(); Vediamo come trattare il caso con e senza l’utilizzo degli Optional:

Senza l’uso degli Optional :

User user = getUser(name); // può ritornare null
Location location = null;
if(user != null) { 
  location = getLocation(user);
}

Con l’uso degli Optional :

Optional<User> user = Optional.ofNullable(getUser(user)); 
Optional<Location> location = user.map(u -> getLocation(user));

La soluzione con l’uso degli Optional risulta più elegante e ci permette di non “inquinare” il codice con i controlli dei valori null. L’utilizzo nel codice è sempre possibile in quanto gli Optional posso essere usati con qualsiasi  funzione.


Ora, prima di riscrivere tutto il codice usando gli Optional è bene tenere presente alcune regole empiriche per il loro utilizzo:

  • I campi di istanza delle classi – usare valori semplici. Gli Optional non sono stati creati per l’utilizzo nei campi. Non sono serializzabili e aggiungono un sovraccarico per  l’involucro che non è necessario. Usali invece nei metodi quando si elaborano i dati dei campi.
  • Parametri dei metodi – usare anche qui valori semplici. Usare gli Optional nei parametri dei metodi “inquina” le firme dei metodi e rende il codice più difficile da leggere e mantenere. Mantieni il codice semplice!
  • Valori di ritorno dei metodi – considera l’utilizzo degli Optional . Invece di restituire valori nulli, il tipo Optional potrebbe essere migliore. Chiunque utilizza il codice sarà costretto a gestire i casi null e con l’uso degli Optional il codice risulta più pulito.

 

Conclusioni

Spero che questo articolo abbia dato un’idea su alcune delle migliori pratiche in materia di progammazione funzionale con Java 8 mantendendo comunque un codice leggibile e manutenibile. Se ti è piaciuto, non dimenticate di scaricare la piccola guida riassuntiva di un foglio che ho preparato  in modo da poterla stampare e metterla sotto la tazza di caffè del vostro collega meno esperto…. 😉

Lascia un commento

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.

Top