Java – I metodi replace/replaceAll di Map da Java 8

In Java 8 sono stati introdotti nella interfaccia java.util.Map svariati nuovi metodi, tra cui due nuovi metodi chiamati replace e un altro nuovo metodo chiamato replaceAll. In questo articolo descriverò questi metodi usando anche alcuni esempi pratici.

I nuovi metodi di Map innanzitutto hanno la seguente forma:

default V replace(K key, V value)
default boolean replace(K key, V oldValue, V newValue)
default void replaceAll(BiFunction<? super K,? super V,? extends V> function)

Sono tutti metodi di “default”, una nuova funzionalità di Java 8 che permette di definire in una interface Java un metodo che possiede una implementazione, cioè ha un corpo con del codice e quindi non è “astratto”.

Questi tre nuovi metodi, come indica il termine inglese “replace”, servono per sostituire i valori associati alle chiavi presenti nella mappa. Ciascuno di questi metodi però applica la sostituzione con una logica un po’ differente.

V replace(K key, V value)

Questo replace è il più semplice da comprendere dei tre nuovi metodi: effettua la sostituzione del valore ma solo se la chiave è già contenuta all’interno della mappa, altrimenti non ha alcun effetto. La documentazione javadoc ufficiale di questo replace chiarisce bene che la condizione fondamentale è che la chiave sia associata a “qualunque” valore, non che il valore sia solo, banalmente, diverso da null. La implementazione predefinita si basa infatti sul containsKey(Object), quindi è sufficiente che la chiave “esista”.

Il valore restituito da replace è sempre quello associato in “precedenza” (eventualmente null, se la chiave non è presente) cioè è il valore esistente prima della invocazione di replace.

La sequenza di istruzioni riportata di seguito dovrebbe chiarire bene quando replace modifica il valore e quando invece non lo modifica.

Map<String,Integer> mappa = new HashMap<>();

System.out.println(mappa.get("abc"));             // null
System.out.println(mappa.containsKey("abc"));     // false

// "abc" NON è presente, replace NON modifica il valore
System.out.println(mappa.replace("abc", 5678));   // null

System.out.println(mappa.get("abc"));             // null
System.out.println(mappa.containsKey("abc"));     // false

mappa.put("abc", 1234);

System.out.println(mappa.get("abc"));             // 1234
System.out.println(mappa.containsKey("abc"));     // true

// "abc" è presente, replace MODIFICA il valore
System.out.println(mappa.replace("abc", 5678));   // 1234   (nota: è il valore precedente!)

System.out.println(mappa.get("abc"));             // 5678

mappa.put("abc", null);

// "abc" è presente (valore null), replace MODIFICA il valore
System.out.println(mappa.replace("abc", 5678));   // null   (nota: è il valore precedente!)

System.out.println(mappa.get("abc"));             // 5678

boolean replace(K key, V oldValue, V newValue)

Quest’altra versione di replace è solamente un pochino più sofisticata rispetto al precedente replace: effettua la sostituzione del valore ma solo se la chiave è già contenuta all’interno della mappa E il valore corrente associato è uguale a oldValue (nota: “uguale” nel senso del metodo equals() degli oggetti). In qualunque altra condizione questo replace non ha alcun effetto. Il risultato del metodo è true se il valore viene sostituito, altrimenti è false.

La sequenza di istruzioni riportata di seguito dovrebbe chiarire bene quando questo replace modifica il valore e quando invece non lo modifica.

Map<String,Integer> mappa = new HashMap<>();

System.out.println(mappa.get("abc"));                   // null
System.out.println(mappa.containsKey("abc"));           // false

// "abc" NON è presente, replace NON modifica il valore
System.out.println(mappa.replace("abc", 1234, 5678));   // false

System.out.println(mappa.get("abc"));                   // null
System.out.println(mappa.containsKey("abc"));           // false

mappa.put("abc", 100);

// "abc" è presente ma il valore NON è 1234, replace NON modifica il valore
System.out.println(mappa.replace("abc", 1234, 5678));   // false

System.out.println(mappa.get("abc"));                   // 100

mappa.put("abc", 1234);

// "abc" è presente E il valore è 1234, replace MODIFICA il valore
System.out.println(mappa.replace("abc", 1234, 5678));   // true

System.out.println(mappa.get("abc"));                   // 5678

mappa.put("abc", null);

// "abc" è presente E il valore è null, replace MODIFICA il valore
System.out.println(mappa.replace("abc", null, 9876));   // true

System.out.println(mappa.get("abc"));                   // 9876

void replaceAll(BiFunction<? super K,? super V,? extends V> function)

Questo replaceAll è il più interessante dei tre nuovi metodi perché permette di modificare tutti i valori in un colpo solo senza dover fare esplicitamente una iterazione sulla mappa. Il replaceAll deve ricevere una implementazione della functional interface BiFunction, il cui metodo apply viene invocato di volta in volta per ciascuna entry passando come argomenti la chiave e il valore. La implementazione di apply può applicare la logica che preferisce e poi deve restituire il nuovo valore da associare alla chiave.

Con questo replaceAll bisogna tenere presente che valgono i seguenti principi:

  • vengono esaminate e aggiornate tutte le entry, non si può “saltare” una entry (al massimo si può restituire lo stesso valore senza alcuna modifica)
  • non si possono sostituire le chiavi (non c’è modo per farlo)
  • se per una chiave X la implementazione di BiFunction restituisce null, la chiave X resta associata a null (non vuol dire che la associazione chiave-valore viene rimossa!)
  • non è possibile eliminare le associazioni chiave-valore (per il punto sopra)

Il seguente codice mostra un esempio basilare dell’uso di replaceAll per convertire tutti i valori String in uppercase.

import java.util.Map;
import java.util.TreeMap;

public class ProvaReplaceAllMap {
    public static void main(String[] args) {
        Map<Integer,String> mappa = new TreeMap<>();
        mappa.put(10, "dieci");
        mappa.put(20, "venti");
        mappa.put(30, "trenta");
        mappa.put(40, "quaranta");

        mappa.forEach((chiave, valore) -> System.out.format("%d = %s%n", chiave, valore));
        System.out.println();

        mappa.replaceAll((chiave, valore) -> valore.toUpperCase());

        mappa.forEach((chiave, valore) -> System.out.format("%d = %s%n", chiave, valore));
    }
}

L’output è il seguente:

10 = dieci
20 = venti
30 = trenta
40 = quaranta

10 = DIECI
20 = VENTI
30 = TRENTA
40 = QUARANTA

Volendo implementare il BiFunction con una anonymous inner class invece che con una lambda expression, si poteva fare nel seguente modo:

        mappa.replaceAll(new BiFunction<Integer, String, String>() {
            @Override
            public String apply(Integer chiave, String valore) {
                return valore.toUpperCase();
            }
        });

Chiaramente questa forma è decisamente più lunga, per questo motivo le nuove lambda expression introdotte in Java 8 risultano molto più convenienti in casi come questi. 😉

NOTA: nel codice sopra il @Override si potrebbe anche omettere senza alcun impatto negativo. Essendo una implementazione diretta di una interfaccia, se anche la signature (firma) del metodo fosse scritta in maniera scorretta, sarebbe comunque un errore di compilazione.