Java est à ce jour, le langage le plus utilisé de la planète. Grâce à sa vaste bibliothèque d’API, Il trouve des applications dans de nombreux domaines, tels que le développement mobile, les applications web, ou encore le développement Cloud. En Big Data, il est à la base de frameworks massivement parallèles comme Hadoop ou Kafka.  Donc, si vous souhaitez travailler dans le Big Data, ou si vous exercez déjà dans le domaine, à un moment ou un autre, vous l’utiliserez. Tenez ! D’ailleurs le MapReduce qui est à la base du traitement massivement parallèle, s’appuie sur les collections de Java…

Dans cette chronique, nous allons voir les structures de données et les collections de Java. En d’autres termes, nous allons vous montrer comment utiliser les structures de données et les collections de Java pour développer des applications de données. Même si en matière de structure de données, le langage Scala reste la référence en Big Data, il n’en demeure pas moins que Java dispose de collections très intéressantes, et leur maîtrise vous donnera un avantage considérable dans le développement de vos applications Big Data.

L’ensemble des collections et structures de données de Java sont regroupées dans le package appelé Java.util.Map. Cela signifie que pour les utiliser, vous commencer par les importer dans votre application en faisant :

import java.util.Map 

En réalité, Java dispose de 3 collections principales (regroupées dans ce package) : le Map, le HashMap, et le TreeMap. Dans ce tutoriel, nous allons vous montrer comment utiliser chacune de ces 3 collections.

Avant d’entrer dans le vif du sujet, il est important de comprendre ce que Java entend par « collection ». Par exemple, en Scala, une collection est une séquence de données immuable (ou pas). Du coup, pour Scala, la Liste, le tableau, sont des collections, alors que pour Java, ces éléments sont juste des types de base. Pour Java, une table de hachage est une collection, alors qu’en Scala, elle n’est qu’un type de base (même si elle est traitée comme une collection). Il faut donc commencer par bien comprendre ce que Java entend par « collection« .

La collection Map en Java

En fait, en java, une collection est une table de hachage (hash table), c’est-à-dire une séquence de données qui fait correspondre à chaque enregistrement, une clé à une valeur. A la différence des tables de hachage classiques qui sont des objets ou des types de base (comme en Scala), en Java ces tables sont des Interfaces, c’est-à-dire des méthodes abstraites. Elles n’ont donc aucune existence réelle, et leur utilisation va demander une implémentation préalable. Pour plus de détails sur les notions d’interface et d’implémentation, je vous recommande notre chronique Apprendre la programmation orientée objet.

Java appelle formellement ses collection « Map« , d’où le nom du package Java.util.Map. De prime à bord, cette différence syntaxique chez Java peut créer de la confusion surtout si vous avez déjà de l’expérience en programmation dans un autre langage. D’autant plus que « Map » (avec la majuscule dans la fin de la signature du package) peut désigner la classe Map, alors qu’en fait c’est l’interface qui renvoie l’ensemble des collections de Java ! Le schéma suivant l’illustre parfaitement.

Map est en réalité une interface de toutes les collections de Java. Les 5 Map dont il hérite représente ses différentes implémentations.

Aussi, traditionnellement, « Map » fait référence à un mapping, une table de correspondance entre une clé et une valeur (ce qu’on appelle une table de hachage). Ne confondez pas sémantiquement le Map de Java, qui conceptuellement renvoie certes à une table de hachage, mais qui programmatiquement, correspond à une interface, au Map qui est un type de base.

L’interface Map comprend toutes les méthodes de l’interface Collection qu’il hérite. Comme c’est une interface, on ne peut pas créer d’objets à partir de celle-ci. Pour utiliser ses fonctionnalités, il faut alors utiliser ses implémentations (les classes) :

  •     HashMap
  •     EnumMap
  •     LinkedHashMap
  •     WeakHashMap
  •     TreeMap

Dans une collection Map (chacune de ses 5 implémentations), chaque entrée est constituée d’une clé avec sa valeur correspondante. Les clés y sont uniques.

En Big Data, et dans le traitement de données de manière général, la structure clé-valeur des maps est très utile pour apporter des modifications sur un gros volume de données. C’est d’ailleurs cette structure qui est à la base de l’algorithme mapreduce où les partitions de données sont traitées en 2 phases dans des collections map. En dehors des algorithmes, la structure clé-valeur est également très importante pour manipuler les fichiers semi-structurés comme le JSON ou le XML.

1	 // implementation de Map avec HashMap
2	 Map<String, String> langues = new HashMap<>(); 

Dans le code ci-dessus nous avons créé un Map langues  dont chaque entrée est constitué d’une clé de type String et d’une Valeur de type String aussi.

Outre les méthodes disponibles dans l’interface Collection que l’interface Map hérite, cell-ci comprend de nombreuses méthodes à travers lesquelles manipuler la collection. On peut citer entre autre, les méthodes suivantes :

  •     put(K, V) :  Insère l’association d’une clé K et d’une valeur V dans le Map. Si la clé est déjà présente, la nouvelle valeur remplace l’ancienne.
  •     putAll(Map) : Insère toutes les entrées (clés/valeurs ) du Map fourni en paramètre.
  •     putIfAbsent(K, V) : Insère l’entrée (K, V) si la clé K n’est pas déjà associée à la valeur V.
  •     get(K):  Retourne la valeur associée à la clé K spécifiée. Si la clé n’est pas trouvée, elle retourne null.
  •     getOrDefault(K, defaultValue) : Retourne la valeur associée à la clé K spécifiée. Si la clé n’est pas trouvée, elle retourne la valeur par défaut.
  •     containsKey(K) : Vérifie si la clé spécifiée K est présente dans le Map ou non.
  •     containsValue(V) : Vérifie si la valeur spécifiée V est présente dans le Map ou non.
  •     replace(K, V) :  Remplace la valeur de la clé K par la nouvelle valeur spécifiée V.
  •     replace(K, oldValue, newValue) : Remplace la valeur de la clé K par la nouvelle valeur newValue seulement si la clé K est associée à la valeur oldValue.
  •     remove(K) :  Supprime l’entrée du Map représentée par la clé K.
  •     remove(K, V) :  Supprime l’entrée du Map représentée par la clé K associée à la valeur V.
  •     keySet() :  Retourne l’ensemble de toutes les clés présentes dans une map.
  •     values() : Retourne un ensemble de toutes les valeurs présentes dans une map.
  •     entrySet() : Retourne un ensemble de toutes les correspondances clé/valeur présentes dans une map.

Exemple d’utilisation d’un Map :

public class Main{

   public static void main(String[] args) {
       Map<String, String> langues = new HashMap<>(); 

       langues.put("FR", "Français");
       langues.put("EN", "Anglais");
       langues.put("ES", "Espagnol");

       // Affiche les clés du map
       System.out.println("Clés: " + langues.keySet());

       // Affiche les valeurs du map
       System.out.println("Values: " + langues.values());

       // Affiche les entrées du map
       System.out.println("Entrées: " + langues.entrySet());
    }
	
}

Le code ci-dessus produit l’affichage suivant :

1	Clés: [EN, FR, ES]
2	Values: [Anglais, Français, Espagnol]
3	Entrées: [EN=Anglais, FR=Français, ES=Espagnol]

Maintenant que nous avons couvert les fondamentaux des collections Java, nous allons entrer dans le détail de chacune de ces 5 implémentations.

2 – La collection HashMap

La classe HashMap est sans doute la collection la plus implémentée de Java et celle qui est utilisée par défaut. Elle fournit la fonctionnalité de la structure de données de la table de hachage.

Elle stocke les éléments dans des paires clé/valeur. Ici, les clés sont des identifiants uniques utilisés pour associer chaque valeur  dans une map. Pour l’utiliser, il faut importer du  package java.util. Une fois le Hashmap importé voicila syntaxe à utiliser pour en créer :

HashMap<Integer, String> jours = new HashMap<>();

La classe HashMap fournit diverses méthodes pour effectuer différentes opérations sur les hashmaps. Voyons quelques méthodes de base :

  • put(K,V) : permet d’ajouter un élément dans une hasmap
  • get(K) : permet d’accéder à une valeur d’une hashmap
  • replace(K,V) : permet de changer la valeur associée à une clé dans un hashmap
  •  remove(K) : permet de supprimer un élément dans une hashmap

Par exemple :

public static void main(String[] args) {

    HashMap<Integer, String> jours = new HashMap<>();

    jours.put(1, "Lundi");
    jours.put(2, "Mardi");
    jours.put(3, "Mercredi");
    jours.put(4, "Jeudi");
    jours.put(5, "Vendredi");
    jours.put(6, "Samedi");
    jours.put(7, "Dimanche");

    System.out.println(jours.get(1)); // affiche Lundi
    jours.replace(5, "Friday");
    System.out.println(jours.get(5)); // affiche Friday
    jours.remove(2);
    System.out.println(jours); //affiche {1=Lundi, 3=Mercredi, 4=Jeudi, 5=Friday, //6=Samedi, 7=Dimanche}
	
}

Voilà en ce qui concerne la collection hashmap. Nous allons maintenant passer à la collection LinkedHashMap.

La collection LinkedHashMap

La collection LinkedHashMap est une implémentation de type Map et étend de la classe HashMap qui utilise une liste doublement chaînée entre toutes ses entrées pour les ordonner.

Pour l’utiliser, il faut importer LinkedHashMap du  package java.util. Une fois le LinkedHashmap importé voici comment en créer :

LinkedHashMap<Integer, String> jours = new LinkedHashMap<>();

LinkedHashMap vs HashMap

Le LinkedHashMap et le HashMap implémentent tous deux l’interface Map. Cependant, il existe quelques différences entre elles.

  • LinkedHashMap utilise une liste doublement chaînée en interne. De ce fait, elle conserve l’ordre d’insertion de ses éléments.
  • La classe LinkedHashMap nécessite plus de stockage que HashMap. Cela est dû au fait que LinkedHashMap maintient des listes chaînées en interne.
  • Les performances de LinkedHashMap sont plus faibles que celles du HashMap.

La collection WeakHashMap

WeakHashMap est une implémentation de l’interface Map qui ne stocke que des références faibles à ses clés. Le fait de ne stocker que des références faibles permet de mettre à la poubelle une paire clé-valeur lorsque sa clé n’est plus référencée en dehors du WeakHashMap.

Pour l’utiliser, c’est comme les autres collections. Il faut importer WeakHashMap du  package java.util. Une fois la classe importée, voici comment l’utiliser :

WeakHashMap<String, Integer> nbr = new WeakHashMap<>();

Les différences entre HashMap et WeakHashMap

Voyons l’implémentation d’un WeakHashMap en Java

import java.util.WeakHashMap;

public class Programme {
   
    public static void main(String[] args) {
   
       WeakHashMap<String, String> infos = new WeakHashMap<>();
   
       String  nom = new String("Nom");
       String  nomValue = "Toto";
       String  prenom = new String("Prenom");
       String  prenomValue = "Tata";
   
       // insertion des éléments
       infos.put(nom, nomValue);
       infos.put(prenom, prenomValue);
       System.out.println("WeakHashMap: " + infos); //affiche WeakHashMap: {Prenom=Tata, Nom=Toto}
   
       // mettre la réfence à null
       nom = null;
   
       // appel du garbage collector
       System.gc();
   
       System.out.println("WeakHashMap après le garbage collector: " +  infos); // affiche WeakHashMap après le //garbage collector: {Prenom=Tata}
	}
	
}

Comme vous pouvez le voir dans le code précédent, lorsque la clé du weakhashmap est définie comme nulle et que l’on effectue un garbage collection, la clé est supprimée. C’est parce que, contrairement aux hashmaps, les clés des weakhashmaps sont de type référence faible. Cela signifie que les entrées d’une weakhashmap sont supprimées par le ramasse-miettes (garbage collector) si la clé de cette entrée n’est plus utilisée. Ceci est utile pour économiser des ressources.

Voyons maintenant la même implémentation de l’exemple dans un hashmap.

import java.util.HashMap;
import java.util.WeakHashMap;

public class Programme {


 public static void main(String[] args) {

    HashMap<String, String> infos = new HashMap<>();

    String  nom = new String("Nom");
    String  nomValue = "Toto";
    String  prenom = new String("Prenom");
    String  prenomValue = "Tata";

    // insertion des éléments
    infos.put(nom, nomValue);
    infos.put(prenom, prenomValue);
    System.out.println("WeakHashMap: " + infos); //affiche WeakHashMap: {Prenom=Tata, Nom=Toto}

    // mettre la réfence à null
    nom = null;

    // appel du garbage collector
    System.gc();

    System.out.println("WeakHashMap après le garbage collector: " +  infos); // affiche WeakHashMap après le //garbage collector: {Prenom=Tata, Nom=Toto}

    }
}

Ici, lorsque la clé du hashmap est définie comme nulle et que l’on effectue le garbage collection, la clé n’est pas supprimée. Cela est dû au fait que, contrairement aux weakhashmaps , les clés des hashmaps sont de type référence forte. Cela signifie que l’entrée d’une hashmap n’est pas supprimée par le ramasseur miettes même si la clé de cette entrée n’est plus utilisée.

La collection EnumMap

La classe EnumMap  de Java fournit une implémentation de Map  pour les éléments d’une énumération. Dans EnumMap, les éléments de l’énumération sont utilisés comme clés. Elle implémente l’interface Map.

Pour créer une EnumMap, il faut d’abord l’importer du package java.util. Une fois importé, voici comment en créer en Java.

public class Programme {

    enum Couleur{
       VERT, JAUNE, ORANGE
    }
	
     public static void main(String[] args) {
        EnumMap<Couleur, Integer> sizes = new EnumMap<>(Couleur.class);
    }
}

Dans le tutoriel switch java, nous avons illustré de manière détaillée comment exploiter les énumérations, et dans quel contexte.

La collection TreeMap

La classe TreeMap est une Map qui stocke des éléments de manière triée dans un une structure arborescente. Elle implémente l’interface SortedMap sur lequel nous reviendrons plus tard dans le tutoriel.

Les éléments de la collection TreeMap sont triés selon l’ordre naturel de leur clé (s’ils implémentent l’interface Comparable) ou en utilisant une instance de type Comparator fournie au constructeur de la collection.

La classe TreeMap implémente également l’interface NavigableMap sur laquelle nous reviendrons plus tard dans cet article.

Pour créer une TreeMap, importez la classe java.util.TreeMap. Ensuite, vous pouvez l’implémenter en utilisant cette syntaxe :

TreeMap<String, Interger> nbVoyelles = new TreeMap<>();

Par exemple :


public class Programme {

    public static void main(String[] args) {
        TreeMap<String, Integer> nbvoyelles = new TreeMap<>();

        nbvoyelles.put("u", 1);
        nbvoyelles.put("Bonjour", 3);
        nbvoyelles.put("Toto", 2);

        System.out.println(nbvoyelles); //affiche //{Bonjour=3, Toto=2, u=1}
    }
}

Dans ce code, vous pouvez remarquer par l’affichage {Bonjour=3, Toto=2, u=1} que l’ordre des éléments dans la collection est fait en fonction de manière alphabétique.

Un mot sur le TreeMap Comparator

Dans l’exemple ci-dessus, les éléments du Treemap sont triés naturellement (par ordre croissant). Cependant, il est possible de personnaliser l’ordre des clés. Pour cela, il faut créer votre propre classe de comparateur basée sur le tri des clés dans un treemap. Par exemple :

import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.TreeMap;
import java.util.WeakHashMap;

public class Programme {

    //Création de notre classe Comparator
    public static class WordComparator implements Comparator<String> {

        @Override
        public int compare(String word1, String word2) {
            int res = word1.compareTo(word2);

            // Tri des élément dans l'ordre décroissant
            if (res > 0) {
                return -1;
            }
            else if (res < 0) {
                return 1;
            }
            else {
                return 0;
            }
        }
    }
    public static void main(String[] args) {
        TreeMap<String, Integer> nbvoyelles = new TreeMap<>(new WordComparator());

        nbvoyelles.put("u", 1);
        nbvoyelles.put("Bonjour", 3);
        nbvoyelles.put("Toto", 2);

        System.out.println(nbvoyelles); //affiche {u=1, //Toto=2, Bonjour=3}
    }
}

Vous pouvez remarquer que le programme ci-dessus affiche les éléments du TreeMap nbvoyelles par ordre décroissant de clés.

Les interfaces qui héritent du Map Java

A ce stade, nous avons vu les 5 implémentations du Map. Vous savez désormais comment les utiliser ainsi que le contexte approprié pour chacune d’entre elles. Maintenant, nous allons voir d’autres collections de Java, cette fois-ci les interfaces qui héritent de l’interface Map. Il y’en a 3 principales :

  1. le SortedMap
  2. le NavigableMap
  3. le ConcurrentMap

Nous allons voir chacune d’elle en profondeur.

L’interface SortedMap

L’interface SortedMap hérite de Map mais sa particularité est que les clés d’une SortedMap sont triées de façon naturelle ou en utilisant un Comparator à la création de l’instance de la collection.

Comme SortedMap est une interface, nous ne pouvons pas créer d’objets à partir de celle-ci. Afin d’utiliser ses fonctionnalités, il faut utiliser la classe TreeMap qui l’implémente.

import java.util.SortedMap ;
SortedMap<String, Integer> infos = new TreeMap<>();

L’interface SortedMap comprend toutes les méthodes de l’interface Map. C’est parce que Map est une super interface de SortedMap. Outre toutes ses méthodes de base, voici les méthodes spécifiques à l’interface SortedMap.

  •     comparator() : renvoie un comparateur qui peut être utilisé pour ordonner les clés dans une map.
  •     firstKey() : retourne la première clé de la map triée.
  •     lastKey() :  retourne la dernière clé de la carte triée.
  •     headMap(key) : retourne toutes les entrées d’une map  dont les clés sont inférieures à la clé spécifiée.
  •     tailMap(key) :  renvoie toutes les entrées d’une map dont les clés sont supérieures ou égales à la clé spécifiée.
  •     subMap(key1, key2) : renvoie toutes les entrées d’une map dont les clés sont comprises entre key1 et key2, y compris key1.

L’interface NavigableMap

L’Interface NavigableMap hérite de l’interface SortedMap. Elle possède des méthodes qui permettent le parcours de la collection dans l’ordre ascendant ou descendant.

Pour créer une NavigableMap, il faut importer l’interface java.util.NavigableMap. Une fois cela fait, voici la syntaxe à utiliser pour la créer :

NavigableMap<Key, Value> infos = new TreeMap<>();

L’interface NavigableMap hérite de toutes les méthodes de l’inteface SortedMap mais elle redéfinie certaines méthodes comme :

  • headMap(key, booleanValue) : la méthode headMap() renvoie toutes les entrées d’une NavigableMap associées à toutes les clés précédant la clé spécifiée (qui est passée en argument). La valeur booléenne (booleanValue) est un paramètre facultatif. Sa valeur par défaut est false.
  • tailMap(clé, booleanValue) : la méthode tailMap() renvoie toutes les entrées d’une NavigableMap associées à toutes les clés après la clé spécifiée (passée en argument), y compris l’entrée associée à la clé spécifiée. La valeur booléenne (booleanValue) est un paramètre facultatif. Sa valeur par défaut est true. Si false est passé comme booleanValue, la méthode renvoie toutes les entrées associées à ces clés après la clé spécifiée, sans inclure l’entrée associée à la clé spécifiée.
  • subMap(k1, bv1, k2, bv2) : la méthode subMap() renvoie toutes les entrées associées aux clés comprises entre k1 et k2, y compris l’entrée associée à k1. Les paramètres bv1 et bv2 sont facultatifs. La valeur par défaut de bv1 est true et la valeur par défaut de bv2 est false. Si false est passé comme bv1, la méthode retourne toutes les entrées associées aux clés entre k1 et k2, sans inclure l’entrée associée à k1. Si true est passé comme bv2, la méthode retourne toutes les entrées associées aux clés entre k1 et k2, y compris l’entrée associée à k1.

L’interface ConcurrentMap de Java

L’interface ConcurrentMap est une collection qui est capable de gérer les accès concurrents lors des opérations de modifications de ses éléments. ConcurrentMap est connue comme une map synchronisée.

Pour utiliser ConcurrentMap, importez la classe java.util.concurrent.ConcurrentMap. Une fois importée, voici comment créer une map concurrente :

CocurrentMap<Key, Value> numbers = new ConcurrentHashMap<>() ;

Key et Value étant les types respectifs de la clé et de la valeur d’une entrée dans la collection.

Voilà ! Nous sommes arrivés à la fin de ce tutoriel sur les collections de Java. Comme note de fin, nous vous rappellerons simplement qu’en Big Data, lorsqu’on souhaite développer des applications qui manipulent des gros volumes de données, la maîtrise de l’utilisation des collections est incontournable. Même si en la matière, scala détient les structures de données et abstractions les plus abouties, Java possède des collections qui sont regroupées dans une interface appelée Map. Cette interface est constituée de 4 implémentations : le TreeMap, l’EnumMap, le LinkedHashMap, et le HashMap. Dans ce tutoriel, nous vous avons montré les différences entre ces différentes collections, et comment les exploiter efficacement. Si vous souhaitez aller plus loin dans l’utilisation des collections, abstractions, et structures qui permettent de manipuler aisément les données à large échelle, vous pouvez lire la documentation officielle des collections Java. Vous pouvez également suivre cette mini-formation d’une heure que nous vous offrons gratuitement sur Scala, car comme nous l’avons précisé tout au long du tutoriel, en la matière, scala est le langage qui fournit les meilleures collections pour le Big Data. Pour télécharger votre formation, cliquez sur le bouton dans le formulaire qui suit et entrez-y votre email personnel. Vous le recevrez directement dans votre boîte de réception email.


Juvénal JVC

Juvénal est spécialisé depuis 2011 dans la valorisation à large échelle des données. Son but est d'aider les professionnels de la data à développer les compétences indispensables pour réussir dans le Big Data. Il travaille actuellement comme Lead Data Engineer auprès des grands comptes. Lorsqu'il n'est pas en voyage, Juvénal rédige des livres ou est en train de préparer la sortie d'un de  ses livres. Vous pouvez télécharger un extrait de son dernier livre en date ici : https://www.data-transitionnumerique.com/extrait-ecosystme-hadoop/

>