Aggiunte di Java 8 sulle collection

Oltre al poter utilizzare le istruzioni lambda nel Comparator, nulla di quanto visto è particolare di Java 8, vediamo come questa versione di Java fornisce strumenti per poter scrivere codice più conciso ed efficiente e nuovi metodi quali merge, computIfPresent, computIfAbsent, forEach e removeIf

Usare il method references

Il method references è una modalità più compatta di scrittura del codice omettendo alcuni parti che possono essere dedotte automaticamente dal compilatore.

Per capire questo concetto è bene iniziare con un esempio pratico :

public class AereoHelper {
  public static int compareByPeso(Aereo a1, Aereo a2) {
    return a1.peso - a2.peso;
  }
  publis static int compareByNome(Aereo a1, Aereo a2) {
    return a1.nome.compareTo(a2.nome);
  }
}

Ora immaginiamo di voler scrivere un Comparator utilizzando le lambda per ordinare la nostra lista secondo il peso, dovremmo scrivere :

Comparator<Aereo> comp = (a1, a2) -> AereoHelper.compareByPeso(a1,a2);

Non sembra un brutto codice, ma se guardiamo bene, vediamo che c’è un po di codice ridondante, i parametri, sono dichiarati ma l’istruzione lambda non fa altro che passarli ad un metodo, questo può essere omesso

Comparator<Aereo> comp = DuckHelper::compareByPeso;

l’operatore :: dirà a Java di passare i parametri automaticamente al metodo.

Ci sono 4 formati per il method references

  • Metodi statici
  • Metodi di istanza su un istanza particolare
  • Metodi di istanza su istanze determinate a runtime
  • Costruttori

Utilizzeremo 3 interfacce funzionali in questi esempi, più avanti vedremmo anche le altre, per il momento dobbiamo sapere questo :

  • Predicate – 1 parametro in input, 1 boolean parametro in output
  • Consumer – 1 parametro in input, 0 parametri in output
  • Supplier – 0 parametri in input, 1 parametro in output
Consumer<List<Integer>> methodRef = Collections::sort
Consumer<List<Integer>> lambda = l -> Collections.sort(l);

Vediamo che nella prima istruzione, stiamo chiamando un metodo che vuole 1 parametro in input, Java lo sa perchè Consumer vuole 1 parametro e crea di conseguenza un istruzione lamba uguale alla seconda istruzione

Attenzione però, il metodo sort di Collection è overloadato, come fa Java a sapere quale invocare ? Java lo capisce dal contesto, come detto prima, Consumer vuole 1 parametro e di conseguenza invocherà sort con 1 parametro.

String s = "abc";
Predicate<String> methodRef = str::startWith;
Predicate<String> lambda = s -> str.startWith(s);
Predicate<String> methodRef2 = str::isEmpty;
Predicate<String> lambda2 = s -> s.isEmpty();

Vediamo anche l’uso con i costruttori

Supplier<ArrayList> methodRef = ArrayList::new
Supplier<ArrayList> lambda = () -> new ArrayList();

Rimozione condizionale

Java 8 introduce un metodo nuovo per poter rimuovere elementi in una Collection in base ad una condizione, questa è la sua definizione:

boolean removeIf(Predicate<? super E> filer);

usa un predicato, ovvero una lambda con 1 parametro e ritorna un boolean, vediamo come usarlo con un esempio :

List<String> lista = new ArrayList<>();
lista.add("carlo");
lista.add("andrea");
System.out.println(lista); // [carlo, andrea]
list.removeIf(s -> s.startsWith("c"));
System.out.println(lista); // [andrea]

Abbiamo utilizzato la notazione lunga con le lambda, se volessimo utilizzare la method reference ? Non possiamo ! il compilatore non sarebbe in grado di estrarre s dal contesto !

Aggiornamento di tutti gli elementi

Un altro metodo introdotto da Java 8 sulle Liste è replaceAll, puoi passare un espressione lambda e averla applicata a tutti gli elementi nella lista, utilizza come interfaccia funzionale UnaryOperator<T>

void replaceAll(UnaryOperator<T> o);

UnaryOperator prende in input un parametro e ne restituisce uno di egual tipo :

List<Integer> lista = Arrays.asList(1,2,3);
lista.replaceAll( x -> x*2);
System.out.println(lista); // 2,4,6

L’istruzione lambda moltiplica per 2 tutti gli elementi della lista

Ciclare gli elementi di una lista

Usato tantissimo, ognuno di noi esegue loop continuamente sulle liste, Java 8 ci fornisce un meccanismo più rapido per farlo, solitamente, usando l’enanched for loop avremmo scritto :

List<String> aerei = Arrays.asList("md80","Boeing 747","Cessna");
for (String s : aerei)
  System.out.println(s);

Vediamo come farlo utilizzando il nuovo metodo : forEach(Consume<T> c);

aerei.forEach( s -> System.out.println(s));

Abbiamo usato un Consumer, come intuiamo, qui possiamo tranquillamente utilizzare la method reference :

aerei.forEach(System.out::println);

Nuove API per le Map

Ci sono tre nuovi metodi che devi conoscere per l’esame OCP sulle Mappe, e sono :

  • merge
  • computIfAbsent
  • computIfPresent
  • putIfAbsent

putIfAbsent

Utilizzando la vecchia maniera, se vuoi aggiornare un elemento in una mappa puoi farlo cosi :

Map<String, String> aerei = new HashMap<>();
aerei.put("747","Airbus");
aerei.put("747","Boeing");
System.out.println(aerei); // 747=Boeing

Java 8 ci fornisce un nuovo metodo putIfAbsent per permettere l’inserimento di un elemento solo se questo è realmente assente:

aerei.putIfAbsent("md80","McDonnel Douglas");
aerei.putIfAbsent("321","Airbus");
aerei.putIfAbsent("747","Cessna");
Syste.out.println(aerei); //747=Boeing, md80=MdDonnel Douglas, 321=Airbus

Come vediamo, 747-Cessna non è stato inserito poichè la chiave 747 era già presente

merge

Questo metodo è il più complesso, di fatto permette di specificare una logica di aggiornamento sugli elementi di una mappa, in particolare :

  • se la chiave specificata non è associata a nessun valore è e associata con null, lo associa con il valore non null fornito
  • se la chiave è presente, la sostituisce con il risultato della Function passata come parametro, se il risultato della funzione è null, l’elemento verrà rimosso
 Map<String, Integer> aerei = new HashMap<>();
 aerei.put("747",500);
 aerei.put("md80", 300);
 
 BiFunction<Integer, Integer, Integer> mapper = (v1, v2)-> v1 > v2 ? v1 : v2;
 
 Integer vel1 = aerei.merge("747", 300, mapper);
 Integer vel2 = aerei.merge("md80", 400, mapper);
 
 System.out.println(aerei); //747=500, md80=400
 System.out.println(vel1); // 500
 System.out.println(vel2); // 400

In questo esempio abbiamo definito mapper secondo la seguente logica, vogliamo modificare la velocità massima del nostro aereo se e solo se quella fornita è maggiore di quella attuale, vediamo che la definizione della BiFunction mapper fa proprio questo.

NB: BiFunction definisce i primi due parametri come input e il terzo come output

vediamo che nel caso del 747 abbiamo passato 300, ma il 747 aveva già 500, di fatto la BiFunction non altera, nel caso dell’md80, abbiamo passato 400, superiore a 300 definito prima ed il valore viene aggiornato a 400

Abbiamo visto che merge ha anche una logica nel caso in cui il valore sia null, per farla semplice, in questo caso, la Function non verrà invocata e verrà settato direttamente il valore passato nel merge :

public static void main(String... args)
 {
 Map<String, Integer> aerei = new HashMap<>();
 aerei.put("747",500);
 aerei.put("md80", null);
 
 BiFunction<Integer, Integer, Integer> mapper = (v1, v2)-> v1 > v2 ? v1 : v2;
 
 Integer vel1 = aerei.merge("747", 300, mapper);
 Integer vel2 = aerei.merge("md80", 100, mapper);
 
 System.out.println(aerei); //747=500, md80=100
}

Nel caso dell’md80, il suo valore era null, merge gli ha assegnato 100

L’ultimo caso è vedere cosa succede se la Function restituisce un null, di fatto verrà rimosso dalla mappa :

  Map<String, Integer> aerei = new HashMap<>();
 aerei.put("747",500);
 aerei.put("md80", 300);
 
 BiFunction<Integer, Integer, Integer> mapper = (v1, v2)-> v1 > v2 ? null : v2;
 
 Integer vel1 = aerei.merge("747", 700, mapper);
 Integer vel2 = aerei.merge("md80", 100, mapper);
 
 System.out.println(aerei);  //747=700
}

In questo caso abbiamo modificato la nostra function per restituire null se il valore passato è inferiore a quanto già presente, vediamo che md80 è stato rimosso

computeIfPresent e computeIfAbsent

Semplicemente computeIfPresent invoca la BiFunction se la chiave è presente, computeIfAbsent invoca la BiFunction se la chiave non è presente :

 Map<String, Integer> aerei = new HashMap<>();
 aerei.put("747",500);
 
 BiFunction<String, Integer, Integer> mapper = (k, v) -> v*2 ;
 Integer vel1 = aerei.computeIfPresent("747", mapper);
 Integer vel2 = aerei.computeIfPresent("md80", mapper);
 
 System.out.println(aerei);  //747=1000

Vediamo che nel caso di md80 la BiFunction non verrà invocata poichè la chiave non è presente

Vediamo un esempio pratico di computeIfAbsent

 Map<String, Integer> aerei = new HashMap<>();
 aerei.put("747",500);
 
 Function<String, Integer> mapper = (k) -> 200 ;
 Integer vel1 = aerei.computeIfAbsent("747", mapper);
 Integer vel2 = aerei.computeIfAbsent("md80", mapper);
 
 System.out.println(aerei); //747=500, md80=200

Notiamo che è stata utilizzato Function al posto di BiFunction, che verrà invocata solo se la chiave non è presente, in questo caso vogliamo inserire il valore di default 200 se l’aereo non è nella mappa

Naturalmente non è necessario definire la Function come oggetto, è possibile passarla direttamente al metodo computeIfAbsent

 Map<String, Integer> aerei = new HashMap<>();
 aerei.put("747",500);
 
 Integer vel1 = aerei.computeIfPresent("747", (k,v) -> v*2);
 Integer vel2 = aerei.computeIfAbsent("md80", (k) -> 200);
 
 System.out.println(aerei); //747=1000, md80=200

computeIfPresent rimuove la chiave se la function restituisce null

 Map<String, Integer> aerei = new HashMap<>();
 aerei.put("747",500);
 
 Integer vel1 = aerei.computeIfPresent("747", (k,v) -> null);
 Integer vel2 = aerei.computeIfAbsent("md80", (k) -> null);
 
 System.out.println(aerei); // {}