Java – Il metodo removeIf di Collection da Java 8

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.

  1. Significa che removeIf è automaticamente disponibile per tutte le collezioni che derivano da Collection, escluse ovviamente le “map” poiché hanno una loro gerarchia separata.
  2. 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 il removeIf 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;
    }
}