Una delle tante novità introdotte in Java 8 è la aggiunta del nuovo metodo removeIf
nella interfaccia java.util.Collection
. Questo metodo è una funzionalità molto piccola e modesta rispetto a tutte le grandi novità portate da Java 8. In certe situazioni però questo metodo può rivelarsi molto utile ed è quindi sicuramente interessante conoscerlo. In questo articolo descriverò il concetto e l’utilizzo di removeIf
con l’aiuto di alcuni semplici e utili esempi pratici.
Innanzitutto il metodo removeIf
è definito in Collection
nel seguente modo:
public interface Collection<E> extends Iterable<E> { default boolean removeIf(Predicate<? super E> filter) { //....... implementazione predefinita } }
Lo scopo di questo nuovo metodo removeIf
è abbastanza facile da spiegare e comprendere: permette di eliminare facilmente (e senza dover fare esplicitamente un ciclo apposito) tutti gli elementi della collezione che soddisfano un certo “predicato”. Un predicato, in questo contesto, è una funzione che riceve un valore e in base ad un determinato criterio restituisce un risultato di tipo booleano che è concettualmente una risposta del tipo “va bene”/“non va bene”. Il senso del true
o false
restituito dipende ovviamente da dove/come viene usato il predicato e nel caso di removeIf
il valore true
significa che l’elemento deve essere rimosso.
La prima cosa da notare è che si tratta innanzitutto di un metodo che ha il modificatore default
. I metodi di default sono una nuova funzionalità del linguaggio introdotta proprio in Java 8 per dare la possibilità di definire in una interface
Java un metodo che possiede una implementazione, cioè ha un corpo con del codice e quindi non è “astratto” (prima di Java 8 le interface
potevano solo avere metodi astratti). Tutte le classi che implementano la interfaccia quindi “ereditano” automaticamente la implementazione di default e possono, se necessario, anche ridefinirla effettuando un appropriato override.
La seconda cosa da notare è che il metodo ha come parametro un Predicate<? super E>
. La interfaccia Predicate
è un’altra novità di Java 8 che serve proprio a rappresentare il concetto di predicato descritto prima. Per invocare removeIf
quindi è sufficiente fornire una implementazione di Predicate
che si può realizzare in diversi modi: nel modo tradizionale con una normale classe (sia top-level, sia una inner/nested class) oppure da Java 8 con una lambda expression o un method reference.
La terza cosa da notare è che removeIf
restituisce un valore di tipo boolean
che rappresenta il risultato complessivo di tutta la operazione di iterazione e rimozione. Il valore restituito è true
se almeno un elemento è stato rimosso, altrimenti è false
.
Il fatto che removeIf
sia stato aggiunto nella interfaccia Collection
ha alcuni risvolti positivi e importanti.
- Significa che
removeIf
è automaticamente disponibile per tutte le collezioni che derivano daCollection
, escluse ovviamente le “map” poiché hanno una loro gerarchia separata. - Essendo un metodo “di istanza”, si invoca direttamente su un oggetto di una collezione. La cosa più interessante però è che ciascuna implementazione concreta di una collezione (es.
ArrayList
) ha la facoltà di ridefinire ilremoveIf
in modo da renderlo più appropriato ed anche eventualmente più efficiente per la struttura dati usata dalla collezione.
Riguardo il punto 2, è bene dare una spiegazione un po’ più ampia. In Collection
il metodo removeIf
è stato implementato in maniera sensata ed appropriata ma molto “basilare”, il suo comportamento predefinito è a grandi linee il seguente: ottiene un iteratore (java.util.Iterator
) dalla collezione stessa, scorre gli elementi, per ciascuno di essi invoca la funzione di predicato e se questa restituisce true
allora rimuove l’elemento usando il remove()
dell’iteratore (attenzione: non il remove
della collezione!).
La implementazione che ho appena descritto è validissima, in generale, quando il remove()
dell’iteratore è ragionevolmente “efficiente”. Questo è ad esempio il caso della collezione LinkedList
, che è una lista linkata di nodi in cui la rimozione del nodo “corrente” della iterazione è una operazione facile e veloce.
Non è invece così per l’iteratore di ArrayList
(collezione basata su array) poiché rimuovere un elemento singolarmente significa dover spostare fisicamente indietro di una posizione tutti gli elementi successivi. In ArrayList
infatti il removeIf
è stato appositamente ridefinito per implementarlo in maniera più efficiente.
Esempi di utilizzo
Di seguito vengono riportati alcuni esempi di utilizzo del removeIf
.
Rimuovere i numeri negativi in un List<Integer>
Per rimuovere i valori negativi in una lista List<Integer>
è sufficiente usare la seguente lambda expression: v -> v < 0
. Questa rappresenta effettivamente un “predicato” perché riceve un valore (dedotto per inferenza di tipo Integer
) e restituisce un boolean
dato dalla espressione di confronto v < 0
.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class RemoveIfInteroNegativo {
public static void main(String[] args) {
List<Integer> interi = new ArrayList<>(Arrays.asList(8, -2, 5, 13, -7, 0, 2, 9, -1));
System.out.println(interi); // stampa: [8, -2, 5, 13, -7, 0, 2, 9, -1]
interi.removeIf(v -> v < 0); // predicato: true se valore negativo
System.out.println(interi); // stampa: [8, 5, 13, 0, 2, 9]
}
}
Rimuovere gli elementi null in una lista
Talvolta si ha una collezione (di qualunque tipo, non è importante) che contiene per qualche motivo degli elementi null
che devono essere eliminati. Inizialmente si potrebbe pensare ad una lambda expression del tipo obj -> obj != null
che è tecnicamente corretta ma si può fare di meglio.
La classe di utilità java.util.Objects
(già esistente da Java 7) a partire da Java 8 possiede un nuovo metodo statico di “utilità” chiamato isNull(Object obj)
che a prima vista potrebbe sembrare abbastanza inutile ma in realtà è molto comodo se viene usato come method reference nella forma Objects::isNull
per fare da “predicato”.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public class RemoveIfNull {
public static void main(String[] args) {
List<String> stringhe = new ArrayList<>(Arrays.asList("aa", null, "bb", "cc", null));
System.out.println(stringhe); // stampa: [aa, null, bb, cc, null]
stringhe.removeIf(Objects::isNull); // predicato: true se null
System.out.println(stringhe); // stampa: [aa, bb, cc]
}
}
Rimuovere le stringhe in un List<String> che non soddisfano una regular expression
In una lista List<String>
si potrebbe voler rimuovere tutte le stringhe che hanno (o al contrario, non hanno) una certa forma. Si può quindi sfruttare il meccanismo delle regular expression (introdotto in Java 1.4) per fare questo tipo di riconoscimento.
Nell’esempio pratico sotto viene creata una lista contenente una serie di stringhe. L’obiettivo è di mantenere nella lista solo le stringhe che rappresentano un qualunque numero valido che può avere opzionalmente come primo carattere il segno "+" o "-". Per rappresentare in maniera generalizzata questa forma, si può utilizzare la seguente regular expression: [+-]?\d+
Questa espressione si può idealmente vedere suddivisa in quattro parti:
[+-]
descrive “il carattere+
o-
”?
descrive “zero o una occorrenza del precedente costrutto” (in pratica, rende il+
o-
facoltativo)\d
descrive “una cifra decimale”+
descrive “una o più occorrenze del precedente costrutto” (in pratica, una sequenza di cifre decimali)
Per rendere più semplice ma anche molto “riutilizzabile” la creazione del predicato, una soluzione è la definizione di un metodo apposito come il regexMatchesPredicate
nell’esempio. Si tratta di un metodo che riceve il pattern della regular expression e un valore true
/false
per indicare se il match deve essere “positivo” o “negativo”. Alla fine il metodo fornisce in uscita la funzione di predicato restituendo una lambda expression.
Nell’esempio il metodo regexMatchesPredicate
viene invocato con false
perché ovviamente si vuole rimuovere le stringhe che non corrispondono alla forma descritta prima.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Pattern;
public class RemoveIfNonMatchRegex {
public static void main(String[] args) {
List<String> stringhe = new ArrayList<>();
stringhe.add("234"); // ok
stringhe.add("abc");
stringhe.add("+345"); // ok
stringhe.add("+567x");
stringhe.add("");
stringhe.add("-678"); // ok
stringhe.add("++99");
System.out.println(stringhe); // stampa: [234, abc, +345, +567x, , -678, ++99]
stringhe.removeIf(regexMatchesPredicate("[+-]?\\d+", false));
System.out.println(stringhe); // stampa: [234, +345, -678]
}
public static Predicate<String> regexMatchesPredicate(String regex, boolean match) {
Pattern pattern = Pattern.compile(regex);
return str -> pattern.matcher(str).matches() == match;
}
}