Interfacce Funzionali di Java 8

Benvenuti al tutorial sull’esempio di interfacce funzionali Java 8. Java è sempre stato un linguaggio di Programmazione Orientata agli Oggetti. Ciò significa che tutto nella programmazione java ruota attorno agli Oggetti (ad eccezione di alcuni tipi primitivi per semplicità). Non abbiamo solo funzioni in java, fanno parte della Classe e abbiamo bisogno di utilizzare la classe/oggetto per invocare qualsiasi funzione.

Interfacce Funzionali Java 8

Se guardiamo ad alcuni altri linguaggi di programmazione come C++, JavaScript; sono chiamati linguaggio di programmazione funzionale perché possiamo scrivere funzioni e utilizzarle quando necessario. Alcuni di questi linguaggi supportano la Programmazione Orientata agli Oggetti così come la Programmazione Funzionale. Essere orientato agli oggetti non è un male, ma porta molta verbosità al programma. Ad esempio, diciamo che dobbiamo creare un’istanza di Runnable. Di solito lo facciamo utilizzando classi anonime come di seguito.

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

Se guardi il codice sopra, la parte effettivamente utile è il codice all’interno del metodo run(). Il resto del codice è dovuto al modo in cui i programmi java sono strutturati. Le Interfacce Funzionali Java 8 e le Espressioni Lambda ci aiutano a scrivere codice più piccolo e pulito rimuovendo molti codici di riempimento.

Interfaccia funzionale Java 8

Un’interfaccia con esattamente un metodo astratto viene chiamata Interfaccia Funzionale. Viene aggiunta l’annotazione @FunctionalInterface in modo da poter contrassegnare un’interfaccia come interfaccia funzionale. Non è obbligatorio utilizzarla, ma è una prassi ottimale usarla con le interfacce funzionali per evitare l’aggiunta accidentale di metodi extra. Se l’interfaccia è annotata con l’annotazione @FunctionalInterface e cerchiamo di avere più di un metodo astratto, viene generato un errore del compilatore. Il principale vantaggio delle interfacce funzionali di Java 8 è che possiamo utilizzare le espressioni lambda per istanziarle ed evitare di utilizzare implementazioni di classi anonime ingombranti. L’API delle collezioni di Java 8 è stata riscritta e è stata introdotta la nuova API Stream che utilizza molte interfacce funzionali. Java 8 ha definito molte interfacce funzionali nel pacchetto java.util.function. Alcune delle utili interfacce funzionali di Java 8 sono Consumer, Supplier, Function e Predicate. Puoi trovare maggiori dettagli su di esse in Esempio di Stream Java 8. java.lang.Runnable è un ottimo esempio di interfaccia funzionale con un singolo metodo astratto run(). Il frammento di codice sottostante fornisce qualche indicazione per le interfacce funzionali:

interface Foo { boolean equals(Object obj); }
// Non funzionale perché equals è già un membro implicito (classe Object)

interface Comparator {
 boolean equals(Object obj);
 int compare(T o1, T o2);
}
// Funzionale perché Comparator ha solo un metodo non-Object astratto

interface Foo {
  int m();
  Object clone();
}
// Non funzionale perché il metodo Object.clone non è pubblico

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Funzionale: due metodi, ma hanno la stessa firma

interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// Funzionale: Y.m è una sottosignatura e può essere sostituito il tipo di ritorno

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Non funzionale: Nessun metodo ha una sottosignatura di tutti i metodi astratti

interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class c); }
interface Z extends X, Y {}
// Non funzionale: Nessun metodo ha una sottosignatura di tutti i metodi astratti

interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// Errore del compilatore: nessun metodo è sostituibile per il tipo di ritorno

interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// Errore del compilatore: firme diverse, stessa cancellazione

Espressione Lambda

Le espressioni Lambda sono il modo attraverso il quale possiamo visualizzare la programmazione funzionale nel mondo orientato agli oggetti di Java. Gli oggetti sono la base del linguaggio di programmazione Java e non possiamo mai avere una funzione senza un oggetto, ecco perché il linguaggio Java fornisce il supporto per l’uso delle espressioni lambda solo con interfacce funzionali. Poiché c’è solo una funzione astratta nelle interfacce funzionali, non c’è confusione nell’applicare l’espressione lambda al metodo. La sintassi delle espressioni lambda è (argomento) -> (corpo). Ora vediamo come possiamo scrivere il Runnable anonimo sopra usando un’espressione lambda.

Runnable r1 = () -> System.out.println("My Runnable");

Cerchiamo di capire cosa sta succedendo nell’espressione lambda sopra.

  • La Runnable è un’interfaccia funzionale, ecco perché possiamo utilizzare un’espressione lambda per crearne un’istanza.
  • Dato che il metodo run() non accetta argomenti, la nostra espressione lambda non ne ha neanche.
  • Come nei blocchi if-else, possiamo evitare le parentesi graffe ({}) poiché abbiamo una singola istruzione nel corpo del metodo. Per più istruzioni, dovremmo utilizzare le parentesi graffe come per qualsiasi altro metodo.

Perché abbiamo bisogno di un’espressione lambda

  1. Riduzione delle righe di codice Uno dei chiari benefici nell’utilizzare un’espressione lambda è la riduzione del codice, abbiamo già visto quanto sia facile creare un’istanza di un’interfaccia funzionale utilizzando un’espressione lambda anziché utilizzare una classe anonima.

  2. Supporto all’esecuzione sequenziale e parallela Un altro vantaggio nell’utilizzare le espressioni lambda è che possiamo beneficiare del supporto alle operazioni sequenziali e parallele dell’API Stream. Per spiegare questo, prendiamo un esempio semplice in cui dobbiamo scrivere un metodo per testare se un numero passato è un numero primo o no. Tradizionalmente scriveremmo il suo codice come segue. Il codice non è completamente ottimizzato ma va bene per scopi di esempio, quindi sopportatemi su questo.

    //Approccio tradizionale
    private static boolean isPrime(int number) {		
    	if(number < 2) return false;
    	for(int i=2; i

    Il problema con il codice sopra è che è sequenziale per natura, se il numero è molto grande allora ci vorrà un tempo significativo. Un altro problema con il codice è che ci sono così tanti punti di uscita e non è leggibile. Vediamo come possiamo scrivere lo stesso metodo usando le espressioni lambda e l’API Stream.

    //Approccio dichiarativo
    private static boolean isPrime(int number) {		
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					index -> number % index == 0);
    }
    

    IntStream è una sequenza di elementi primitivi int che supporta operazioni aggregate sequenziali e parallele. Questa è la specializzazione primitiva int di Stream. Per una maggiore leggibilità, possiamo anche scrivere il metodo come segue.

    private static boolean isPrime(int number) {
    	IntPredicate isDivisible = index -> number % index == 0;
    	
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					isDivisible);
    }
    

    Se non sei familiare con IntStream, il suo metodo range() restituisce un IntStream ordinato sequenziale da startInclusive (inclusivo) a endExclusive (esclusivo) con un passo incrementale di 1. Il metodo noneMatch() restituisce se nessun elemento di questo stream corrisponde al predicato fornito. Potrebbe non valutare il predicato su tutti gli elementi se non è necessario per determinare il risultato.

  3. Passaggio di comportamenti nei metodi Vediamo come possiamo utilizzare le espressioni lambda per passare il comportamento di un metodo con un semplice esempio. Supponiamo di dover scrivere un metodo per sommare i numeri in una lista se corrispondono a un determinato criterio. Possiamo utilizzare Predicate e scrivere un metodo come segue.

    public static int sommaConCondizione(List<Integer> numeri, Predicate<Integer> predicato) {
    	    return numeri.parallelStream()
    	    		.filter(predicato)
    	    		.mapToInt(i -> i)
    	    		.sum();
    	}
    

    Utilizzo di esempio:

    // somma di tutti i numeri
    sommaConCondizione(numeri, n -> true)
    // somma di tutti i numeri pari
    sommaConCondizione(numeri, i -> i%2==0)
    // somma di tutti i numeri maggiori di 5
    sommaConCondizione(numeri, i -> i>5)
    
  4. Maggiore Efficienza con la Pigrizia Un altro vantaggio dell’uso delle espressioni lambda è la valutazione pigra, ad esempio supponiamo di dover scrivere un metodo per trovare il numero dispari massimo nell’intervallo da 3 a 11 e restituire il quadrato di esso. Di solito scriveremmo il codice per questo metodo in questo modo:

    private static int trovaQuadratoDiMassimoDispari(List<Integer> numeri) {
    		int max = 0;
    		for (int i : numeri) {
    			if (i % 2 != 0 && i > 3 && i < 11 && i > max) {
    				max = i;
    			}
    		}
    		return max * max;
    	}
    

    Il programma sopra verrà sempre eseguito in ordine sequenziale, ma possiamo utilizzare Stream API per ottenere questo e beneficiare della ricerca della Pigrizia. Vediamo come possiamo riscrivere questo codice in modo funzionale utilizzando Stream API ed espressioni lambda.

    public static int trovaQuadratoDiMassimoDispari(List<Integer> numeri) {
    		return numeri.stream()
    				.filter(NumberTest::isDispari) 		//Predicate è un'interfaccia funzionale e
    				.filter(NumberTest::isMaggioreDi3)	// stiamo usando le lambda per inizializzarla
    				.filter(NumberTest::isMinoreDi11)	// piuttosto che classi interne anonime
    				.max(Comparator.naturalOrder())
    				.map(i -> i * i)
    				.get();
    	}
    
    	public static boolean isDispari(int i) {
    		return i % 2 != 0;
    	}
    	
    	public static boolean isMaggioreDi3(int i){
    		return i > 3;
    	}
    	
    	public static boolean isMinoreDi11(int i){
    		return i < 11;
    	}
    

    Se sei sorpreso dall’operatore doppio due punti (::), è stato introdotto in Java 8 ed è utilizzato per i riferimenti ai metodi. Il compilatore Java si occupa di mappare gli argomenti al metodo chiamato. È la forma abbreviata delle espressioni lambda i -> isMaggioreDi3(i) o i -> NumberTest.isMaggioreDi3(i).

Esempi di Espressioni Lambda

Di seguito fornisco alcuni frammenti di codice per le espressioni lambda con brevi commenti che li spiegano.

() -> {}                     // No parameters; void result

() -> 42                     // No parameters, expression body
() -> null                   // No parameters, expression body
() -> { return 42; }         // No parameters, block body with return
() -> { System.gc(); }       // No parameters, void block body

// Blocco di corpo complesso con restituzioni multiple
() -> {
  if (true) return 10;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1             // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1                 // Single inferred-type argument, same as below
x -> x+1                   // Parenthesis optional for single inferred-type case

(String s) -> s.length()   // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length()              // Single inferred-type argument
t -> { t.start(); }          // Single inferred-type argument

(int x, int y) -> x+y      // Multiple declared-type parameters
(x,y) -> x+y               // Multiple inferred-type parameters
(x, final y) -> x+y        // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y          // Illegal: can't mix inferred and declared types

Referenze a Metodo e Costruttore

A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:

System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new

Questo è tutto per il Tutorial sulle Interfacce Funzionali di Java 8 e le Espressioni Lambda. Suggerirei vivamente di studiarlo perché questa sintassi è nuova per Java e ci vorrà del tempo per comprenderla. Dovresti anche dare un’occhiata alle Caratteristiche di Java 8 per conoscere tutti i miglioramenti e i cambiamenti nella versione di Java 8.

Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces