Vous utilisez une Map lorsque vous souhaitez identifier des valeurs à l’aide d’une clé. Par exemple, lorsque vous utilisez la liste de contacts dans votre téléphone, vous recherchez “Pascal” plutôt que de parcourir chaque numéro de téléphone à tour de rôle.
Vous pouvez imaginer une Map comme illustré dans la Figure 9.8. Vous n’avez pas besoin de connaître les noms des interfaces spécifiques que les différentes maps implémentent, mais vous devez savoir que TreeMap est triée.
Map.of() et Map.copyOf()
Tout comme List et Set, il existe une méthode de fabrique pour créer une Map. Vous passez n’importe quel nombre de paires de clés et de valeurs.
Map.of("cle1", "valeur1", "cle2", "valeur2");
Contrairement à List et Set, cette méthode est moins qu’idéale. Passer des clés et des valeurs est plus difficile à lire car vous devez garder une trace du paramètre correspondant. Heureusement, il existe une meilleure façon. Map fournit également une méthode qui vous permet de fournir des paires clé/valeur.
Map.ofEntries(
Map.entry("cle1", "valeur1"),
Map.entry("cle2", "valeur2"));
Maintenant, nous ne pouvons pas oublier de passer une valeur. Si nous omettons un paramètre, la méthode entry() ne compilera pas. De manière pratique, Map.copyOf(map) fonctionne comme les méthodes copyOf() des interfaces List et Set.
Comparaison des implémentations de Map
Un HashMap stocke les clés dans une table de hachage. Cela signifie qu’il utilise la méthode hashCode() des clés pour récupérer leurs valeurs plus efficacement.
Le principal avantage est que l’ajout d’éléments et la récupération d’éléments par clé se font en temps constant. L’inconvénient est que vous perdez l’ordre dans lequel vous avez inséré les éléments. La plupart du temps, cela n’est pas un problème dans une map. Si vous le souhaitez, vous pourriez utiliser LinkedHashMap.
Un TreeMap stocke les clés dans une structure d’arbre triée. Le principal avantage est que les clés sont toujours dans l’ordre de tri. Comme un TreeSet, l’inconvénient est que l’ajout et la vérification de la présence d’une clé prennent plus de temps à mesure que l’arbre grandit.
Travailler avec les méthodes de Map
Étant donné que Map n’étend pas Collection, davantage de méthodes sont spécifiées sur l’interface Map. Puisqu’il y a à la fois des clés et des valeurs, nous avons besoin de paramètres de type générique pour les deux. La classe utilise K pour la clé et V pour la valeur. Les méthodes que vous devez connaître sont présentées dans le Tableau 9.6. Certaines signatures de méthodes sont simplifiées pour les rendre plus faciles à comprendre.
Méthode | Description |
---|---|
public void clear() | Supprime toutes les clés et valeurs de la map. |
public boolean containsKey(Object key) | Renvoie si la clé est dans la map. |
public boolean containsValue(Object value) | Renvoie si la valeur est dans la map. |
public Set<Map.Entry<K,V>> entrySet() | Renvoie un Set de paires clé/valeur. |
public void forEach(BiConsumer<K key, V value>) | Parcourt chaque paire clé/valeur. |
public V get(Object key) | Renvoie la valeur mappée par la clé ou null si aucune n’est mappée. |
public V getOrDefault(Object key, V defaultValue) | Renvoie la valeur mappée par la clé ou la valeur par défaut si aucune n’est mappée. |
public boolean isEmpty() | Renvoie si la map est vide. |
public Set<K> keySet() | Renvoie un set de toutes les clés. |
public V merge(K key, V value, Function(<V, V, V> func)) | Définit la valeur si la clé n’est pas définie. Exécute la fonction si la clé est définie pour déterminer la nouvelle valeur. Supprime si la valeur est null. |
public V put(K key, V value) | Ajoute ou remplace une paire clé/valeur. Renvoie la valeur précédente ou null. |
public V putIfAbsent(K key, V value) | Ajoute une valeur si la clé n’est pas présente et renvoie null. Sinon, renvoie la valeur existante. |
public V remove(Object key) | Supprime et renvoie la valeur mappée à la clé. Renvoie null si aucune. |
public V replace(K key, V value) | Remplace la valeur pour une clé donnée si la clé est définie. Renvoie la valeur originale ou null si aucune. |
public void replaceAll(BiFunction<K, V, V> func) | Remplace chaque valeur par les résultats de la fonction. |
public int size() | Renvoie le nombre d’entrées (paires clé/valeur) dans la map. |
public Collection<V> values() | Renvoie une Collection de toutes les valeurs. |
Bien que le Tableau 9.6 soit une liste assez longue de méthodes, ne vous inquiétez pas ; beaucoup de noms sont évidents. De plus, beaucoup existent par commodité. Par exemple, containsKey() peut être remplacé par un appel get() qui vérifie si le résultat est null. Celle que vous utilisez dépend de vous.
Appel des méthodes de base
Commençons par comparer le même code avec deux types de Map. D’abord avec HashMap :
Map<String, String> map = new HashMap<>();
map.put("koala", "bambou");
map.put("lion", "viande");
map.put("girafe", "feuille");
String nourriture = map.get("koala"); // bambou
for (String cle: map.keySet())
System.out.print(cle + ","); // koala,girafe,lion,
Ici, nous utilisons la méthode put() pour ajouter des paires clé/valeur à la map et get() pour obtenir une valeur à partir d’une clé. Nous utilisons également la méthode keySet() pour obtenir toutes les clés.
Java utilise le hashCode() de la clé pour déterminer l’ordre. L’ordre ici n’est ni trié ni l’ordre dans lequel nous avons tapé les valeurs. Maintenant, voyons TreeMap :
Map<String, String> map = new TreeMap<>();
map.put("koala", "bambou");
map.put("lion", "viande");
map.put("girafe", "feuille");
String nourriture = map.get("koala"); // bambou
for (String cle: map.keySet())
System.out.print(cle + ","); // girafe,koala,lion,
TreeMap trie les clés comme nous l’attendons. Si nous avions appelé values() au lieu de keySet(), l’ordre des valeurs correspondrait à l’ordre des clés.
Avec notre même map, nous pouvons essayer quelques vérifications booléennes :
System.out.println(map.contains("lion")); // NE COMPILE PAS
System.out.println(map.containsKey("lion")); // true
System.out.println(map.containsValue("lion")); // false
System.out.println(map.size()); // 3
map.clear();
System.out.println(map.size()); // 0
System.out.println(map.isEmpty()); // true
La première ligne est un peu délicate. La méthode contains() est sur l’interface Collection mais pas sur l’interface Map. Les deux lignes suivantes montrent que les clés et les valeurs sont vérifiées séparément. Nous pouvons voir qu’il y a trois paires clé/valeur dans notre map. Ensuite, nous vidons le contenu de la map et voyons qu’il y a zéro élément et qu’elle est vide.
Dans les sections suivantes, nous présentons des méthodes de Map que vous pourriez moins connaître.
Itérer à travers une Map
Vous avez vu la méthode forEach() plus tôt dans le chapitre. Notez qu’elle fonctionne un peu différemment sur une Map. Cette fois, le lambda utilisé par la méthode forEach() a deux paramètres : la clé et la valeur. Regardons un exemple :
Map<Integer, Character> map = new HashMap<>();
map.put(1, 'a');
map.put(2, 'b');
map.put(3, 'c');
map.forEach((k, v) -> System.out.println(v));
Le lambda a à la fois la clé et la valeur comme paramètres. Il se trouve qu’il imprime la valeur mais pourrait faire n’importe quoi avec la clé et/ou la valeur. Il est intéressant de noter que, puisque nous ne nous soucions pas de la clé, ce code particulier aurait pu être écrit avec la méthode values() et une référence de méthode à la place.
map.values().forEach(System.out::println);
Une autre façon de parcourir toutes les données d’une map est d’obtenir les paires clé/valeur dans un Set. Java a une interface statique à l’intérieur de Map appelée Entry. Elle fournit des méthodes pour obtenir la clé et la valeur de chaque paire.
map.entrySet().forEach(e ->
System.out.println(e.getKey() + " " + e.getValue()));
Obtenir des valeurs en toute sécurité
La méthode get() renvoie null si la clé demandée n’est pas dans la map. Parfois, vous préférez qu’une valeur différente soit renvoyée. Heureusement, la méthode getOrDefault() facilite cela. Comparons les deux méthodes :
Map<Character, String> map = new HashMap<>();
map.put('x', "point");
System.out.println("X marque le " + map.get('x'));
System.out.println("X marque le " + map.getOrDefault('x', ""));
System.out.println("Y marque le " + map.get('y'));
System.out.println("Y marque le " + map.getOrDefault('y', ""));
Ce code imprime ceci :
X marque le point
X marque le point
Y marque le null
Y marque le
Comme vous pouvez le voir, les lignes 5 et 6 ont la même sortie car get() et getOrDefault() se comportent de la même manière lorsque la clé est présente. Ils renvoient la valeur mappée par cette clé. Les lignes 7 et 8 donnent une sortie différente, montrant que get() renvoie null lorsque la clé n’est pas présente. En revanche, getOrDefault() renvoie la chaîne vide que nous avons passée comme paramètre.
Remplacer des valeurs
Ces méthodes sont similaires à la version List, sauf qu’une clé est impliquée :
Map<Integer, Integer> map = new HashMap<>();
map.put(1, 2);
map.put(2, 4);
Integer original = map.replace(2, 10); // 4
System.out.println(map); // {1=2, 2=10}
map.replaceAll((k, v) -> k + v);
System.out.println(map); // {1=3, 2=12}
La ligne 24 remplace la valeur pour la clé 2 et renvoie la valeur originale. La ligne 26 appelle une fonction et définit la valeur de chaque élément de la map avec le résultat de cette fonction. Dans notre cas, nous avons ajouté la clé et la valeur ensemble.
Ajout conditionnel avec putIfAbsent
La méthode putIfAbsent() définit une valeur dans la map mais la saute si la valeur est déjà définie avec une valeur non-null.
Map<String, String> favoris = new HashMap<>();
favoris.put("Marie", "Tour en Bus");
favoris.put("Thomas", null);
favoris.putIfAbsent("Marie", "Tram");
favoris.putIfAbsent("Samuel", "Tram");
favoris.putIfAbsent("Thomas", "Tram");
System.out.println(favoris); // {Thomas=Tram, Marie=Tour en Bus, Samuel=Tram}
Comme vous pouvez le voir, la valeur de Marie n’est pas mise à jour car elle était déjà présente. Samuel n’était pas du tout là, donc il a été ajouté. Thomas était présent en tant que clé mais avait une valeur null. Par conséquent, il a également été ajouté.
Fusion des données
La méthode merge() ajoute une logique de choix. Supposons que nous voulions choisir le trajet avec le nom le plus long. Nous pouvons écrire du code pour exprimer cela en passant une fonction de mapping à la méthode merge() :
BiFunction<String, String, String> mapper = (v1, v2)
-> v1.length() > v2.length() ? v1 : v2;
Map<String, String> favoris = new HashMap<>();
favoris.put("Marie", "Tour en Bus");
favoris.put("Thomas", "Tram");
String marie = favoris.merge("Marie", "Téléphérique", mapper);
String thomas = favoris.merge("Thomas", "Téléphérique", mapper);
System.out.println(favoris); // {Thomas=Téléphérique, Marie=Tour en Bus}
System.out.println(marie); // Tour en Bus
System.out.println(thomas); // Téléphérique
Le code aux lignes 11 et 12 prend deux paramètres et renvoie une valeur. Notre implémentation renvoie celle avec le nom le plus long. La ligne 18 appelle cette fonction de mapping, et elle voit que “Tour en Bus” est plus long que “Téléphérique”, donc elle laisse la valeur à “Tour en Bus”. La ligne 19 appelle cette fonction de mapping à nouveau. Cette fois, “Tram” est plus court que “Téléphérique”, donc la map est mise à jour. La ligne 21 imprime le nouveau contenu de la map. Les lignes 22 et 23 montrent que le résultat est renvoyé par merge().
La méthode merge() a également une logique pour ce qui se passe si des valeurs null ou des clés manquantes sont impliquées. Dans ce cas, elle n’appelle pas du tout la BiFunction, et elle utilise simplement la nouvelle valeur.
BiFunction<String, String, String> mapper =
(v1, v2) -> v1.length() > v2.length() ? v1 : v2;
Map<String, String> favoris = new HashMap<>();
favoris.put("Samuel", null);
favoris.merge("Thomas", "Téléphérique", mapper);
favoris.merge("Samuel", "Téléphérique", mapper);
System.out.println(favoris); // {Thomas=Téléphérique, Samuel=Téléphérique}
Notez que la fonction de mapping n’est pas appelée. Si c’était le cas, nous aurions une NullPointerException. La fonction de mapping n’est utilisée que lorsqu’il y a deux valeurs réelles entre lesquelles choisir.
La dernière chose à savoir sur merge() est ce qui se passe lorsque la fonction de mapping est appelée et renvoie null. La clé est supprimée de la map quand cela se produit :
BiFunction<String, String, String> mapper = (v1, v2) -> null;
Map<String, String> favoris = new HashMap<>();
favoris.put("Marie", "Tour en Bus");
favoris.put("Thomas", "Tour en Bus");
favoris.merge("Marie", "Téléphérique", mapper);
favoris.merge("Samuel", "Téléphérique", mapper);
System.out.println(favoris); // {Thomas=Tour en Bus, Samuel=Téléphérique}
Thomas a été laissé tranquille car il n’y avait pas d’appel merge() pour cette clé. Samuel a été ajouté car cette clé n’était pas dans la liste originale. Marie a été supprimée car la fonction de mapping a renvoyé null.
Le Tableau 9.7 montre tous ces scénarios en référence.
Si la clé demandée | Et la fonction de mapping renvoie | Alors : |
---|---|---|
A une valeur null dans la map | N/A (fonction de mapping non appelée) | Mettre à jour la valeur de la clé dans la map avec le paramètre de valeur |
A une valeur non-null dans la map | null | Supprimer la clé de la map |
A une valeur non-null dans la map | Une valeur non-null | Définir la clé au résultat de la fonction de mapping |
N’est pas dans la map | N/A (fonction de mapping non appelée) | Ajouter la clé avec le paramètre de valeur à la map directement sans appeler la fonction de mapping |