Java 8 Parte 1 – Optional e Interfacce Funzionali

J

Con Java 8 si ha una vera e propria rivoluzione del linguaggio Java, che mantiene la sua natura Object Oriented, ma si avvicina sempre più alla programmazione post funzionale.

La novità più importante, legata al carattere funzionale del linguaggio, è l’introduzione di espressioni lambda, che permettono di semplificare la scrittura del codice e renderlo più sintetico. Unita a questa ci sono la modifica del concetto di interfaccia, con la definizione di interfacce funzionali e la ridefinizione dello stream I/O, come Stream funzionale, che può lavorare su sequenze di oggetti, non solo di bytes.

La nuova classe Optional<T>, invece, permette di scrivere codice più robusto, sostituendo il valore null, che, se non gestito correttamente, è causa di NullPointerException.

Questi nuovi costrutti permettono di scrivere codice più chiaro e leggibile, con un guadagno anche dal punto di vista delle performance.

In questo articolo vengono descritti il costrutto Optional e le interfacce funzionali, che possono essere implementate usando espressioni lambda

Classe optional<t>

In Java 8 è stata introdotta la nuova classe java.util.Optional, che è l’implementazione del tipo Option Maybe, comune nella programmazione funzionale. Si tratta di un container con un singolo valore, che  può essere presente oppure no, e fornisce metodi per operare su di esso. Il valore null corrisponde all’assenza di valore e può essere sostituito con Optional.empty(), che crea un’istanza Optional vuota.

Altri metodi significativi della classe sono:

  • Optional.of(T): crea un’istanza Optional dell’oggetto passato come argomento, T deve essere diverso da null altrimenti si avrà NullPointerException
  • Optional.ofNullable(): crea un’istanza Optional non vuota se il valore è presente, altrimenti crea Optional vuota
  • isPresent() e ifPresent(Consumer<? super T> consumer) : il primo permette di controllare la presenza del valore nell’istanza Optional considerata, restituisce TRUE se l’oggetto Optional non è vuoto altrimenti FALSE. Il secondo invoca il Consumer, passandogli il valore, solo se l’oggetto Optional non è vuoto
  • get(): restituisce il valore, se presente, altrimenti lancia  NoSuchElementException
  • orElse (T other) e orElseGet(Supplier<? extends T> other): usati per definire valori di default, il primo restituisce il valore, se presente, altrimenti restituisce l’istanza other, il secondo restituisce il valore, se presente, altrimenti invoca other, e restituisce il risultato dell’invocazione.

orElse  richiede come argomento l’istanza di un oggetto, che viene sempre creato e usato solo se l’istanza Optional è vuota. orElseGet, invece, richiede l’interfaccia funzionale Supplier, che può essere implementata tramite espressione lambda, creata e passata al metodo, ma eseguita solo se l’istanza Optional è vuota. Quindi nel primo caso si ha un costo fisso non sempre necessario, che aumenta se il recupero dell’istanza viene fatto ad esempio tramite la chiamata ad un servizio.

I controlli relativi a valori null,  possono essere sostituiti da chiamate a metodi di Optional, garantendo l’eliminazione a run time di NullPointerException.

Ad esempio, il seguente codice

String str;

if(str!=null){
//operazioni da eseguire

}

in Java 8 con l’uso di Optional diventa

String str;
Optional<String> optionalString = Optional.of(str);

if(optionalString.isPresent()){
//operazioni da eseguire

}

Nelle versioni Java precedenti, è comunque possibile usare questo costrutto usando librerie esterne, ad esempio la libreria Java di Google, Guava, che fornisce la classe com.google.common.base.Optional<T>

CLASSI ASTRATTE E interfacce

Definizione

Interfaccia e classe astratta forniscono un modo strutturato per mantenere separata l’interfaccia di un oggetto (definizione delle proprietà e dei metodi supportati) dalla sua implementazione. Entrambe definiscono un tipo astratto, che non può essere istanziato; nel caso di una classe astratta si ha un’astrazione generica delle caratteristiche che un certo oggetto deve avere, mentre per un’interfaccia si ha un’astrazione a livello comportamentale.

Questa differenza si traduce poi nella definizione delle due strutture e nel loro uso.

Una classe astratta è una classe che contiene la definizione di almeno un metodo pubblico abstract, cioè privo di implementazione. Può avere sia proprietà che metodi statici o non statici, con qualunque visibilità, a parte i metodi abstract che non possono essere private, altrimenti si genera un errore a compile time. 

Tramite il meccanismo di ereditarietà, è possibile definire classi concrete o altre classi astratte dalla classe di partenza. Le classi concrete implementano tutti i metodi ereditati dalla classe padre ed eventualmente i propri metodi dichiarati, mentre le classi astratte hanno a loro volta almeno un metodo senza implementazione.

Grazie a questo meccanismo si crea una dipendenza tra classi generiche e classi più specifiche; in Java non è ammessa ereditarietà multipla, quindi ogni classe ha un unica classe padre, che se non viene specificata nella definizione della classe tramite extends corrisponde alla classe Object.

Un’interfaccia può dichiarare solo proprietà final e metodi public, non può contenere un proprio stato; le classi che vogliono implementarla devono fornire l’implementazione di TUTTI i metodi che questa ha dichiarato, altrimenti si ha un errore a compile time.

Tramite le interfacce è possibile superare il limite delle classi astratte e simulare il concetto di ereditarietà multipla: non è imposto alcun limite sul numero di interfacce che una classe può implementare.

In Java 8, le interfacce sono state ridefinite e avvicinate alle classi astratte, a tal punto che nei contesti in cui non interessa lo stato di un oggetto è possibile sostituire una classe astratta con un’interfaccia. Inoltre entrambe possono essere definite tramite espressioni lambda, come descritto nel seguito.

La ridefinizione comprende:

  • la dichiarazione di metodi di default e metodi statici
  • la definizione di interfacce funzionali
metodi di default

I metodi di default (detti anche Defender Methods o Virtual extension methods) sono stati introdotti in Java 8, principalmente per supportare le espressioni lambda nelle Collections API  (ad esempio per introdurre il costrutto forEach) e garantire la retrocompatibilità con il codice precedente.

Con Java 8, in un’interfaccia è possibile specificare per un metodo la sua implementazione, tramite la keyword default, definendo il comportamento di default. Le classi che implementano l’interfaccia possono scegliere di definire una propria implementazione del metodo o usare quella fornita dall’interfaccia.

I metodi di default risultano utili soprattutto quando si modifica un’interfaccia, come appunto nel caso delle Collections API.

Nel codice Java tradizionale l’aggiunta di un metodo all’interfaccia genera errori di compilazione in tutte le classi che la implementano, perché non contengono l’implementazione del nuovo metodo aggiunto. Spesso, per risolvere l’errore, in molte classi viene utilizzata la stessa implementazione, introducendo copia di codice inutile.

Questo problema viene risolto in Java 8, usando i metodi di default: il nuovo metodo aggiunto verrà dichiarato come default, usando l’implementazione comune. Solo nelle classi che hanno richiesto la modifica dell’interfaccia, verrà fornita l’implementazione specifica. In questo modo si evitano gli errori in fase di compilazione e si garantisce la retrocompatibilità con il codice scritto.

I metodi di default però possono generare un problema di conflitto se una classe implementa due o più interfacce che dichiarano come default un metodo con la stessa firma e non fornisce la propria implementazione, sfruttando quella di default.

Questo problema è noto come Diamond Problem ed è il motivo per cui in Java è vietata l’ereditarietà multipla: se una classe eredita da due classi che definiscono lo stesso metodo, il compilatore non è in grado di decidere quale super classe deve usare. Ma l’implementazione multipla è già presente in Java, quindi occorre specificare come risolvere questi conflitti.

Consideriamo i due esempi riportati di seguito

public interface Interfaccia1 {

  default void printMessage(String str){
    System.out.println("Interfaccia 1: " + str);
  }

}

public class DefaultMethodExample implements Interfaccia1{

}
public interface Interfaccia1 {

  default void printMessage(String str){
    System.out.println("Interfaccia 1: " + str);
  }

}

public interface Interfaccia2 {

  default void printMessage(String str){
    System.out.println("Interfaccia 2: " + str);
  }

}

public class DefaultMethodExample implements Interfaccia1, Interfaccia2{

}

Nel primo caso la classe DefaultMethodExample implementa Interfaccia1, che ha solo un metodo, dichiarato come default. Il codice compila senza errori, perché la classe usa il metodo printMessage fornito dall’interfaccia.

Nel secondo caso la classe DefaultMethodExample implementa sia Interfaccia1 che Interfaccia2 ed entrambe forniscono l’implementazione di default per lo stesso metodo. Si ha il seguente errore di compilazione: DefaultMethodExample inherits unrelated defaults for printMessage() from types Interfaccia1 and Interfaccia2 . Il compilatore non è in grado di associare a DefaultMethodExample l’implementazione di printMessage, perché il metodo è presente in entrambe le interfacce e la classe non specifica quale usare.

L’errore si risolve obbligando la classe DefaultMethodExample a definire il metodo printMessage, risolvendo l’ambiguità a livello di classe, come mostrato nel codice seguente

public interface Interfaccia1 {

  default void printMessage(String str){
    System.out.println("Interfaccia 1: " + str);
  }

}

public interface Interfaccia2 {

  default void printMessage(String str){
    System.out.println("Interfaccia 2: " + str);
  }

}

public class DefaultMethodExample implements Interfaccia1, Interfaccia2{

   @Override
    default void printMessage(String str){
    System.out.println("Classe DefaultMethodExample: " + str);
  }
}

Se in DefaultMethodExample  si vuole invocare il metodo printMessage di una delle due interfacce, per evitare conflitti si deve usare la notazione NomeInterfaccia.super.printMessage()

Come ultima cosa, va ricordato che non è possibile definire metodi di default che sovrascrivono metodi della classe Object. Tutte le classi ereditano da questa e quindi si avrebbero due implementazioni per lo stesso metodo. Più in generale, se una classe nella gerarchia di ereditarietà ha un metodo con la stessa firma del metodo di default, questo diventa irrilevante e non viene considerato

metodi statici

I metodi statici sono dichiarati con la keyword static, come per le classi. Sono simili ai metodi di default, ma sono parte dell’interfaccia e non possono essere sovrascritti nelle classi che la implementano; l’uso di @Override con un metodo statico genera errore a compile time. 

Possono essere usati per definire dei metodi di utility all’interno dell’interfaccia, ad esempio controllo su valori null e operazioni di ordinamento dei dati, eliminando la definizione di classi di utility che dichiarano solo metodi statici.

Come per i metodi di default, non è possibile definire un metodo statico per i metodi della classe Object, si avrà il seguente errore a compile time: This static method cannot hide the instance method from Object.  Tutti gli oggetti ereditano da Object e permettendo la definizione sopra, nelle classi che implementano l’interfaccia si avrebbero due metodi con la stessa firma, uno a livello di classe e l’altro a livello di istanza e quindi incompatibili tra loro.

Come i metodi statici classici, vengono chiamati usando il nome dell’interfaccia, seguito dal nome del metodo.

interfacce funzionali

Le interfacce funzionali sono delle interfacce che definiscono un solo metodo astratto (SAMSingle Abstract Method) e zero o più metodi di default o metodi statici. Grazie a questa particolarità possono essere implementate tramite un’espressione lambda.

Sono definite usando l’annotazione @FunctionalInterface, per indicare il carattere particolare dell’interfaccia, ma soprattutto per permettere al compilatore di generare errori se l’interfaccia non soddisfa i requisiti funzionali, ad esempio se contiene più di un metodo astratto.

In Java esistono già delle interfacce funzionali, ad esempio Comparable, Iterable, Autocloseable, Runnable. Queste possono quindi essere usate sia creando un’Anonymous Inner class, sia definendo un’espressione lambda.

Nel codice seguente vengono riportati due modi equivalenti per dichiarare l’interfaccia Runnable

Runnable r = new Runnable() {
      @Override
      public void run() {
        System.out.println("Runnable con Inner Class");
      }
    }

Runnable rLambda = () -> System.out.println("Runnable con Lambda");

L’uso di un’espressione lambda inline permette di definire l’interfaccia Runnable in modo più chiaro e sintetico.

Accanto a queste, ci sono le nuove interfacce funzionali introdotte in Java 8, nel package java.util.function.

Come detto, queste interfacce possono avere un solo metodo astratto, detto functional method, sul quale vengono mappati i valori in input e l’output.

Le nuove interfacce funzionali sono legate al concetto matematico di funzione: ogni funzione ha un dominio, insieme dei possibili argomenti sui quali può operare, e un codomio, insieme dei possibili output prodotti.

In base a questa definizione si hanno diverse interfacce funzionali

Function<T, R>

Definisce una funzione che accetta un argomento di tipo T e restituisce un risultato di tipo R; il suo metodo astratto è apply, che applica la funzione definita all’oggetto passato come argomento.

public interface Function<T, R> {

R apply(T t);

}

Ha inoltre i seguenti metodi:

  • default <V> Function<V,R> compose(Function<? super V,? extends T> before): restituisce una funzione composta che prima applica la funzione before al suo input poi applica questa funzione al risultato ottenuto, utile per modificare l’input prima che venga applicata la funzione
  • default <V> Function<T,V> andThen(Function<? super R,? extends V> after): restituisce una funzione composta che prima applica questa funzione al suo input, poi applica la funzione after al risultato ottenuto, utile per modificare il tipo restituito da una funzione
  • static <T> Function<T,T> identity(): restituisce una funzione che restituisce sempre il suo argomento

Ha come sotto interfaccia UnaryOperator<T>,  che definisce un’operazione su un operando di tipo T e produce un risultato dello stesso tipo. Non ha altri metodi otre a quelli ereditati da Function.

BiFunction<T,U,R>

Specializzazione di Function, definisce una funzione che accetta 2 argomenti, di tipo T e U, e restituisce un risultato di tipo R, il suo metodo astratto è apply. 

public interface BiFunction<T, U, R> {

R apply(T t, U u);

}

Ha un solo metodo di default, <V> BiFunction<T,U,V> andThen(Function<? super R,? extends V> after) .

BinaryOperator<T> è una sua sotto interfaccia, che definisce un’operazione su due operandi dello stesso tipo T e produce un risultato sempre dello stesso tipo.

Oltre ai metodi ereditati da BiFunction, definisce

  • static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator): restituisce un BynaryOperator, che restituisce l’elemento maggiore tra i due argomenti, in base al termine di confronto specificato in comparator
  • static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator): restituisce un BynaryOperator, che restituisce l’elemento minore tra i due argomenti, in base al termine di confronto specificato in comparator
Supplier<T>

Definisce un’operazione che non riceve input e restituisce un risultato di tipo T, il suo metodo astratto è get, che restituisce il risultato calcolato dall’operazione

public interface Supplier<T> {

T get();
}

Non ha altri metodi nè sotto interfacce

Consumer<T>

Definisce un’operazione che accetta un solo input di tipo T e non restituisce risultati, il suo metodo astratto è accept, che esegue l’operazione sull’input

public interface Consumer<T> {

void accept(T t);
}

Ha un solo metodo di default, Consumer<T> andThen(Consumer<? super T> after) e una sotto interfaccia Stream.Builder<T>, usata per costruire uno stream su oggetti di tipo T.

BiConsumer<T,U>

Specializzazione di Consumer, definisce un’operazione che accetta due argomenti di tipo T e U e non restituisce risultati, il suo metodo astratto è accept

public interface BiConsumer<T,U> {

void accept(T t, U u);
}

Come Consumer, ha un solo metodo di default, default BiConsumer<T,U> andThen(BiConsumer<? super T,? super U> after)

Predicate<T>

Rappresenta un predicato, cioè una funzione booleana, che riceve un solo argomento; il suo metodo astratto è test, che valuta il predicato definito sull’argomento passato

public interface Predicate<T> {

boolean test(T t);
}

Ha inoltre i seguenti metodi:

  • default Predicate<T> and(Predicate<? super T> other): restituisce un predicato composto, che rappresenta il risultato dell’operatore logico AND  su due predicati, sfruttando il short-circuiting, se il predicato è falso, other non viene valutato
  • static <T> Predicate<T> isEqual(Object targetRef): restituisce un predicato che verifica se due argomenti sono uguali, usando Objects.equals(Object, Object)
  • default Predicate<T> negate(): restituisce un predicato che rappresenta la negazione logica del suo predicato
  • default Predicate<T> or(Predicate<? super T> other): restituisce un predicato composto, che rappresenta il risultato dell’operatore logico OR su due predicati, sfruttando il short-circuiting, se il predicato è vero, other non viene valutato
BiPredicate<T,U>

Specializzazione di Predicate, rappresenta un predicato, cioè una funzione booleana, che riceve due argomenti; il suo metodo astratto è test, che valuta il predicato definito sui due argomenti passati

public interface BiPredicate<T, U> {

boolean test(T t, U u);
}

Come Predicate , ha i metodi default BiPredicate<T,U> and(BiPredicate<? super T,? super U> other), default BiPredicate<T,U> negate()  e default BiPredicate<T,U> or(BiPredicate<? super T,? super U> other)

Interfacce funzionali con tipi primitivi

Java 8 definisce interfacce funzionali solo per tre tipi primitivi, intlong double.

Vengono definiti tre gruppi:

  • interfacce che ricevono in input un tipo primitivo e restituiscono il tipo referenziato: tipoPrimitivoFunction<R>, tipoPrimitivoConsumer tipoPrimitivoPredicate, con tipoPrimitivo uguale a uno dei 3 tipi supportati. Un caso particolare è tipoPrimitivoSupplier, in questa notazione il tipo primitivo è riferito al risultato restituito, in quando Supplier non riceve input
  • interfacce che ricevono in input il tipo referenziato e restituiscono in output un tipo primitivo: ToDoubleFunction<T>ToIntFunction<T> ToLongFunction<T>. Per Consumer Predicate non sono definite, nel primo caso perchè non si ha un valore restituito, nel secondo caso perchè viene sempre restituito un valore booleano.  Per Supplier invece si fa riferimento a quanto detto al punto precedente
  • interfacce che ricevono in input un tipo primitivo e restituiscono un altro tipo primitivo. Per quanto detto al punto precedente, sono applicabili solo a Function, si hanno DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, intToLongFunction, LongToDoubleFunction, LongToIntFunction
INTERFACCE FUNZIONALI COMPLESSE

Java SDK non supporta la definizione di funzioni con più di due argomenti, in quanto rappresentano dei casi complessi da gestire e sono poco utilizzati.

Per superare questo limite, si usa il concetto di currying . Si tratta di una tecnica che scompone una funzione con più argomenti, in una sequenza di funzioni ciascuna con un solo argomento.

Consideriamo ad esempio una funzione con tre argomenti, F(x,y,z) , che restituisce un valore K. Applicando la tecnica del currying a F si ottiene x->(y->(z->K)), cioè x viene mappata su una nuova funzione y->(z->K) e y è a sua volta mappata sulla funzione z->K

Considerando una BiFunction (x->y) -> z, tramite il currying si ha x->w, con w nuova funzione che corrisponde a y->z. A questo punto, tramite la tecnica di applicazione parziale di una funzione, si mantiene fisso il valore di x, e si applica la nuova funzione ad y. Si ottiene una nuova funzione, alla quale poi si applica x e si ricava il risultato finale.

Per capire meglio il concetto, consideriamo il seguente codice, in cui un numero fisso, uguale a 5, viene sommato ad una lista di numeri e il risultato di ogni somma viene stampato a video.

public class FunctionExample{

public static void main(String[] args) {  

 BiFunction<Integer, Integer, Integer> sum = (a,b) -> a + b;  

 sumNumbers(applyPartial(sum,5)); 

}  

public static <T,U,V> Function<U,V> applyPartial(BiFunction<T,U,V> bif, T firstArgument ) {   
    return u -> bif.apply(firstArgument, u); 
}  

public static void sumNumbers(Function<Integer, Integer> adder) {   

    for(Integer n : Arrays.asList(3, 8, 10)) {    
        System.out.println(adder.apply(n));   
     }
  }

}

sum è una BiFunction che definisce la tradizionale somma tra due numeri.

applyPartial è un metodo che riceve una BiFunction e una variabile, firstArgument, di tipo T e restituisce una Function che corrisponde all’applicazione della BiFunction firstArgument e al valore in input alla Function restituita. Questa viene poi applicata nel metodo sumNumbers, ad ogni elemento della lista, andando a sommare il valore fisso all’elemento corrente.

Ad esempio per il primo numero, 3, si avrà

  • adder.apply(3)
  • sum.apply(5,3)
  • (a,b) -> a+b => 5+3 = 8

Eseguendo il programma vengono stampati i risultati della somma, 8, 13 e 15

conclusioni

Con Java 8 sono state introdotte nel linguaggio alcune caratteristiche della programmazione funzionale, come ad esempio le interfacce funzionali.

L’avvicinamento alla programmazione funzionale prosegue con le espressioni lambda, usate per implementare le interfacce funzionali al posto delle anonymous inner class.

 

 

 

A proposito di me

Dario Frongillo

Uno degli admin di Italiancoders e dell iniziativa devtalks.
Dario Frongillo è un software engineer e architect, specializzato in Web API, Middleware e Backend in ambito cloud native. Attualmente lavora presso NTT DATA, realtà di consulenza internazionale.
E' membro e contributor in diverse community italiane per developers; Nel 2017 fonda italiancoders.it, una community di blogger italiani che divulga articoli, video e contenuti per developers.

Di Dario Frongillo

Gli articoli più letti

Articoli recenti

Commenti recenti