Article original : How to Use the Java Collections Framework – A Guide for Developers

Dans vos applications Java, vous travaillerez généralement avec divers types d'objets. Et vous pourriez vouloir effectuer des opérations comme le tri, la recherche et l'itération sur ces objets.

Avant l'introduction du framework Collections dans JDK 1.2, vous auriez utilisé des Arrays et des Vectors pour stocker et gérer un groupe d'objets. Mais ils avaient leurs propres inconvénients.

Le Java Collections Framework vise à surmonter ces problèmes en fournissant des implémentations haute performance de structures de données courantes. Cela vous permet de vous concentrer sur l'écriture de la logique de l'application au lieu de vous concentrer sur des opérations de bas niveau.

Ensuite, l'introduction des Generics dans JDK 1.5 a considérablement amélioré le Java Collections Framework. Les Generics vous permettent d'imposer la sécurité des types pour les objets stockés dans une collection, ce qui améliore la robustesse de vos applications. Vous pouvez en lire plus sur les Java Generics ici.

Dans cet article, je vais vous guider à travers l'utilisation du Java Collections Framework. Nous discuterons des différents types de collections, tels que les Listes, les Ensembles, les Files d'attente et les Maps. Je fournirai également une brève explication de leurs caractéristiques clés telles que :

  • Mécanismes internes

  • Gestion des doublons

  • Prise en charge des valeurs nulles

  • Ordre

  • Synchronisation

  • Performance

  • Méthodes clés

  • Implémentations courantes

Nous passerons également en revue quelques exemples de code pour une meilleure compréhension, et j'aborderai la classe utilitaire Collections et son utilisation.

Table des matières :

  1. Comprendre le Java Collections Framework

  2. Interfaces de Collection

  3. Décodage de la hiérarchie du Java Collections Framework

  4. Un regard plus attentif sur les interfaces de collection

  5. Interfaces de Collection Java

  6. Listes

  7. Ensembles

  8. Files d'attente

  9. Maps

  10. Classe utilitaire Collections

  11. Conclusion

Comprendre le Java Collections Framework

Selon la documentation Java, « Une collection est un objet qui représente un groupe d'objets. Un framework de collections est une architecture unifiée pour représenter et manipuler des collections. »

En termes simples, le Java Collections Framework vous aide à gérer un groupe d'objets et à effectuer des opérations sur eux de manière efficace et organisée. Il facilite le développement d'applications en offrant diverses méthodes pour gérer des groupes d'objets. Vous pouvez ajouter, supprimer, rechercher et trier des objets efficacement en utilisant le Java Collections Framework.

Interfaces de Collection

En Java, une interface spécifie un contrat qui doit être rempli par toute classe qui l'implémente. Cela signifie que la classe implémentante doit fournir des implémentations concrètes pour toutes les méthodes déclarées dans l'interface.

Dans le Java Collections Framework, diverses interfaces de collection comme Set, List et Queue étendent l'interface Collection, et elles doivent adhérer au contrat défini par l'interface Collection.

Décodage de la hiérarchie du Java Collections Framework

Consultez ce diagramme pratique de cet article qui illustre la hiérarchie des collections Java :

Diagramme montrant la hiérarchie du Java Collection Framework. Il inclut des interfaces comme Iterable, Collection, List, Queue, Set, Map, et SortedMap, avec des classes telles que ArrayList, LinkedList, Vector, Stack, PriorityQueue, Deque, HashSet, LinkedHashSet, SortedSet, TreeSet, Hashtable, LinkedHashMap, HashMap, et TreeMap. Les flèches indiquent les relations d'implémentation et d'extension.

Nous commencerons par le haut et travaillerons vers le bas pour que vous puissiez comprendre ce que ce diagramme montre :

  1. À la racine du Java Collections Framework se trouve l'interface Iterable, qui vous permet d'itérer sur les éléments d'une collection.

  2. L'interface Collection étend l'interface Iterable. Cela signifie qu'elle hérite des propriétés et du comportement de l'interface Iterable et ajoute son propre comportement pour ajouter, supprimer et récupérer des éléments.

  3. Des interfaces spécifiques telles que List, Set et Queue étendent davantage l'interface Collection. Chacune de ces interfaces a d'autres classes implémentant leurs méthodes. Par exemple, ArrayList est une implémentation populaire de l'interface List, HashSet implémente l'interface Set, et ainsi de suite.

  4. L'interface Map fait partie du Java Collections Framework, mais elle n'étend pas l'interface Collection, contrairement aux autres mentionnées ci-dessus.

  5. Toutes les interfaces et classes de ce framework font partie du package java.util.

Note : Une source courante de confusion dans le Java Collections Framework tourne autour de la différence entre Collection et Collections. Collection est une interface dans le framework, tandis que Collections est une classe utilitaire. La classe Collections fournit des méthodes statiques qui effectuent des opérations sur les éléments d'une collection.

Interfaces de Collection Java

À ce stade, vous êtes familiarisé avec les différents types de collections qui forment la base du framework de collections. Maintenant, nous allons examiner de plus près les interfaces List, Set, Queue et Map.

Dans cette section, nous discuterons de chacune de ces interfaces tout en explorant leurs mécanismes internes. Nous examinerons comment elles gèrent les éléments en double et si elles prennent en charge l'insertion de valeurs nulles. Nous comprendrons également l'ordre des éléments lors de l'insertion et leur prise en charge de la synchronisation, qui traite du concept de sécurité des threads. Ensuite, nous passerons en revue quelques méthodes clés de ces interfaces et conclurons par un examen des implémentations courantes et de leurs performances pour diverses opérations.

Avant de commencer, parlons brièvement de la Synchronisation et de la Performance.

  • La Synchronisation contrôle l'accès aux objets partagés par plusieurs threads, assurant leur intégrité et prévenant les conflits. Cela est crucial pour maintenir la sécurité des threads.

  • Lors du choix d'un type de collection, un facteur important est sa performance lors des opérations courantes comme l'insertion, la suppression et la récupération. La performance est généralement exprimée en utilisant la notation Big-O. Vous pouvez en apprendre plus à ce sujet ici.

Listes

Une List est une collection ordonnée ou séquentielle d'éléments. Elle suit un indexage basé sur zéro, permettant aux éléments d'être insérés, supprimés ou accès en utilisant leur position d'index.

  1. Mécanisme interne : Une List est soutenue en interne par un tableau ou une liste chaînée, selon le type d'implémentation. Par exemple, un ArrayList utilise un tableau, tandis qu'un LinkedList utilise une liste chaînée en interne. Vous pouvez en lire plus sur LinkedList ici. Une List redimensionne dynamiquement sa taille lors de l'ajout ou de la suppression d'éléments. La récupération basée sur l'indexation en fait un type de collection très efficace.

  2. Doublons : Les éléments en double sont autorisés dans une List, ce qui signifie qu'il peut y avoir plus d'un élément dans une List avec la même valeur. Toute valeur peut être récupérée en fonction de l'index auquel elle est stockée.

  3. Null : Les valeurs nulles sont également autorisées dans une List. Puisque les doublons sont permis, vous pouvez également avoir plusieurs éléments nuls.

  4. Ordre : Une List maintient l'ordre d'insertion, ce qui signifie que les éléments sont stockés dans le même ordre qu'ils ont été ajoutés. Cela est utile lorsque vous souhaitez récupérer les éléments dans l'ordre exact où ils ont été insérés.

  5. Synchronisation : Une List n'est pas synchronisée par défaut, ce qui signifie qu'elle n'a pas de moyen intégré pour gérer l'accès par plusieurs threads en même temps.

  6. Méthodes clés : Voici quelques méthodes clés d'une interface List : add(E element), get(int index), set(int index, E element), remove(int index), et size(). Regardons comment utiliser ces méthodes avec un exemple de programme.

import java.util.ArrayList;
import java.util.List;

public class ListExample {
    public static void main(String[] args) {
        // Créer une liste
        List<String> list = new ArrayList<>();

        // add(E element)
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // get(int index)
        String secondElement = list.get(1); // "Banana"

        // set(int index, E element)
        list.set(1, "Blueberry");

        // remove(int index)
        list.remove(0); // Supprime "Apple"

        // size()
        int size = list.size(); // 2

        // Afficher la liste
        System.out.println(list); // Sortie : [Blueberry, Cherry]

        // Afficher la taille de la liste
        System.out.println(size); // Sortie : 2
    }
}
  1. Implémentations courantes : ArrayList, LinkedList, Vector, Stack

  2. Performance : Typiquement, les opérations d'insertion et de suppression sont rapides dans ArrayList et LinkedList. Mais la récupération des éléments peut être lente car vous devez parcourir les nœuds.

OpérationArrayListLinkedList
InsertionRapide à la fin - O(1) amorti, lent au début ou au milieu - O(n)Rapide au début ou au milieu - O(1), lent à la fin - O(n)
SuppressionRapide à la fin - O(1) amorti, lent au début ou au milieu - O(n)Rapide - O(1) si la position est connue
RécupérationRapide - O(1) pour un accès aléatoireLent - O(n) pour un accès aléatoire, car cela implique un parcours

Ensembles

Un Set est un type de collection qui n'autorise pas les éléments en double et représente le concept d'un ensemble mathématique.

  1. Mécanisme interne : Un Set est soutenu en interne par un HashMap. Selon le type d'implémentation, il est soutenu par un HashMap, un LinkedHashMap ou un TreeMap. J'ai écrit un article détaillé sur le fonctionnement interne de HashMap ici. Assurez-vous de le consulter.

  2. Doublons : Puisqu'un Set représente le concept d'un ensemble mathématique, les éléments en double ne sont pas autorisés. Cela garantit que tous les éléments sont uniques, maintenant l'intégrité de la collection.

  3. Null : Un maximum d'une valeur nulle est autorisé dans un Set car les doublons ne sont pas permis. Mais cela ne s'applique pas à l'implémentation TreeSet, où les valeurs nulles ne sont pas autorisées du tout.

  4. Ordre : L'ordre des éléments dans un Set dépend du type d'implémentation.

  5. HashSet : L'ordre n'est pas garanti, et les éléments peuvent être placés à n'importe quelle position.

  6. LinkedHashSet : Cette implémentation maintient l'ordre d'insertion, donc vous pouvez récupérer les éléments dans le même ordre qu'ils ont été insérés.

  7. TreeSet : Les éléments sont insérés en fonction de leur ordre naturel. Alternativement, vous pouvez contrôler l'ordre d'insertion en spécifiant un comparateur personnalisé.

  8. Synchronisation : Un Set n'est pas synchronisé, ce qui signifie que vous pourriez rencontrer des problèmes de concurrence, comme des conditions de course, qui peuvent affecter l'intégrité des données si deux threads ou plus tentent d'accéder à un objet Set simultanément.

  9. Méthodes clés : Voici quelques méthodes clés d'une interface Set : add(E element), remove(Object o), contains(Object o), et size(). Regardons comment utiliser ces méthodes avec un exemple de programme.

import java.util.HashSet;
import java.util.Set;

public class SetExample {
    public static void main(String[] args) {
        // Créer un ensemble
        Set<String> set = new HashSet<>();

        // Ajouter des éléments à l'ensemble
        set.add("Apple");
        set.add("Banana");
        set.add("Cherry");

        // Supprimer un élément de l'ensemble
        set.remove("Banana");

        // Vérifier si l'ensemble contient un élément
        boolean containsApple = set.contains("Apple");
        System.out.println("Contient Apple : " + containsApple);

        // Obtenir la taille de l'ensemble
        int size = set.size();
        System.out.println("Taille de l'ensemble : " + size);
    }
}
  1. Implémentations courantes : HashSet, LinkedHashSet, TreeSet

  2. Performance : Les implémentations de Set offrent des performances rapides pour les opérations de base, sauf pour un TreeSet, où les performances peuvent être relativement plus lentes car la structure de données interne implique le tri des éléments lors de ces opérations.

OpérationHashSetLinkedHashSetTreeSet
InsertionRapide - O(1)Rapide - O(1)Plus lent - O(log n)
SuppressionRapide - O(1)Rapide - O(1)Plus lent - O(log n)
RécupérationRapide - O(1)Rapide - O(1)Plus lent - O(log n)

Files d'attente

Une Queue est une collection linéaire d'éléments utilisée pour contenir plusieurs éléments avant le traitement, généralement suivant l'ordre FIFO (premier entré, premier sorti). Cela signifie que les éléments sont ajoutés à une extrémité et retirés de l'autre, donc le premier élément ajouté à la file d'attente est le premier à être retiré.

  1. Mécanisme interne : Le fonctionnement interne d'une Queue peut différer en fonction de son implémentation spécifique.

  2. LinkedList – utilise une liste doublement chaînée pour stocker les éléments, ce qui signifie que vous pouvez parcourir à la fois vers l'avant et vers l'arrière, permettant des opérations flexibles.

  3. PriorityQueue – est soutenu en interne par un tas binaire, qui est très efficace pour les opérations de récupération.

  4. ArrayDeque – est implémenté en utilisant un tableau qui s'étend ou se réduit à mesure que des éléments sont ajoutés ou retirés. Ici, les éléments peuvent être ajoutés ou retirés des deux extrémités de la file d'attente.

  5. Doublons : Dans une Queue, les éléments en double sont autorisés, permettant plusieurs instances de la même valeur à être insérées.

  6. Null : Vous ne pouvez pas insérer une valeur nulle dans une Queue car, par conception, certaines méthodes d'une Queue retournent null pour indiquer qu'elle est vide. Pour éviter la confusion, les valeurs nulles ne sont pas autorisées.

  7. Ordre : Les éléments sont insérés en fonction de leur ordre naturel. Alternativement, vous pouvez contrôler l'ordre d'insertion en spécifiant un comparateur personnalisé.

  8. Synchronisation : Une Queue n'est pas synchronisée par défaut. Mais, vous pouvez utiliser une implémentation ConcurrentLinkedQueue ou BlockingQueue pour obtenir la sécurité des threads.

  9. Méthodes clés : Voici quelques méthodes clés d'une interface Queue : add(E element), offer(E element), poll(), et peek(). Regardons comment utiliser ces méthodes avec un exemple de programme.

import java.util.LinkedList;
import java.util.Queue;

public class QueueExample {
    public static void main(String[] args) {
        // Créer une file d'attente en utilisant LinkedList
        Queue<String> queue = new LinkedList<>();

        // Utiliser la méthode add pour insérer des éléments, lance une exception si l'insertion échoue
        queue.add("Element1");
        queue.add("Element2");
        queue.add("Element3");

        // Utiliser la méthode offer pour insérer des éléments, retourne false si l'insertion échoue
        queue.offer("Element4");

        // Afficher la file d'attente
        System.out.println("File d'attente : " + queue);

        // Regarder le premier élément (ne le supprime pas)
        String firstElement = queue.peek();
        System.out.println("Peek : " + firstElement); // affiche "Element1"

        // Récupérer le premier élément (le récupère et le supprime)
        String polledElement = queue.poll();
        System.out.println("Poll : " + polledElement); // affiche "Element1"

        // Afficher la file d'attente après poll
        System.out.println("File d'attente après poll : " + queue);
    }
}
  1. Implémentations courantes : LinkedList, PriorityQueue, ArrayDeque

  2. Performance : Les implémentations comme LinkedList et ArrayDeque sont généralement rapides pour ajouter et supprimer des éléments. Le PriorityQueue est un peu plus lent car il insère des éléments en fonction de l'ordre de priorité défini.

OpérationLinkedListPriorityQueueArrayDeque
InsertionRapide au début ou au milieu - O(1), lent à la fin - O(n)Plus lent - O(log n)Rapide - O(1), Lent - O(n), si cela implique un redimensionnement du tableau interne
SuppressionRapide - O(1) si la position est connuePlus lent - O(log n)Rapide - O(1), Lent - O(n), si cela implique un redimensionnement du tableau interne
RécupérationLent - O(n) pour un accès aléatoire, car cela implique un parcoursRapide - O(1)Rapide - O(1)

Maps

Une Map représente une collection de paires clé-valeur, chaque clé étant associée à une seule valeur. Bien que Map fasse partie du framework de collection Java, elle n'étend pas l'interface java.util.Collection.

  1. Mécanisme interne : Une Map fonctionne en interne en utilisant une HashTable basée sur le concept de hachage. J'ai écrit un article détaillé sur ce sujet ici, alors lisez-le pour une compréhension plus approfondie.

  2. Doublons : Une Map stocke les données sous forme de paires clé-valeur. Ici, chaque clé est unique, donc les clés en double ne sont pas autorisées. Mais les valeurs en double sont permises.

  3. Null : Puisque les clés en double ne sont pas autorisées, une Map ne peut avoir qu'une seule clé nulle. Comme les valeurs en double sont permises, elle peut avoir plusieurs valeurs nulles. Dans l'implémentation TreeMap, les clés ne peuvent pas être nulles car elle trie les éléments en fonction des clés. Cependant, les valeurs nulles sont autorisées.

  4. Ordre : L'ordre d'insertion d'une Map varie selon l'implémentation :

  5. HashMap – l'ordre d'insertion n'est pas garanti car ils sont déterminés en fonction du concept de hachage.

  6. LinkedHashMap – l'ordre d'insertion est préservé et vous pouvez récupérer les éléments dans le même ordre qu'ils ont été ajoutés à la collection.

  7. TreeMap – Les éléments sont insérés en fonction de leur ordre naturel. Alternativement, vous pouvez contrôler l'ordre d'insertion en spécifiant un comparateur personnalisé.

  8. Synchronisation : Une Map n'est pas synchronisée par défaut. Mais vous pouvez utiliser Collections.synchronizedMap() ou les implémentations ConcurrentHashMap pour obtenir la sécurité des threads.

  9. Méthodes clés : Voici quelques méthodes clés d'une interface Map : put(K key, V value), get(Object key), remove(Object key), containsKey(Object key), et keySet(). Regardons comment utiliser ces méthodes avec un exemple de programme.

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapMethodsExample {
    public static void main(String[] args) {
        // Créer une nouvelle HashMap
        Map<String, Integer> map = new HashMap<>();

        // put(K key, V value) - Insère des paires clé-valeur dans la map
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Orange", 3);

        // get(Object key) - Retourne la valeur associée à la clé
        Integer value = map.get("Banana");
        System.out.println("Valeur pour 'Banana' : " + value);

        // remove(Object key) - Supprime la paire clé-valeur pour la clé spécifiée
        map.remove("Orange");

        // containsKey(Object key) - Vérifie si la map contient la clé spécifiée
        boolean hasApple = map.containsKey("Apple");
        System.out.println("Contient 'Apple' : " + hasApple);

        // keySet() - Retourne une vue de type Set des clés contenues dans la map
        Set<String> keys = map.keySet();
        System.out.println("Clés dans la map : " + keys);
    }
}
  1. Implémentations courantes : HashMap, LinkedHashMap, TreeMap, Hashtable, ConcurrentHashMap

  2. Performance : L'implémentation HashMap est largement utilisée principalement en raison de ses caractéristiques de performance efficaces illustrées dans le tableau ci-dessous.

OpérationHashMapLinkedHashMapTreeMap
InsertionRapide - O(1)Rapide - O(1)Plus lent - O(log n)
SuppressionRapide - O(1)Rapide - O(1)Plus lent - O(log n)
RécupérationRapide - O(1)Rapide - O(1)Plus lent - O(log n)

Classe utilitaire Collections

Comme souligné au début de cet article, la classe utilitaire Collections dispose de plusieurs méthodes statiques utiles qui vous permettent d'effectuer des opérations couramment utilisées sur les éléments d'une collection. Ces méthodes vous aident à réduire le code redondant dans votre application et vous permettent de vous concentrer sur la logique métier.

Voici quelques fonctionnalités et méthodes clés, ainsi que ce qu'elles font, brièvement énumérées :

  1. Tri : Collections.sort(List<T>) – cette méthode est utilisée pour trier les éléments d'une liste par ordre croissant.

  2. Recherche : Collections.binarySearch(List<T>, key) – cette méthode est utilisée pour rechercher un élément spécifique dans une liste triée et retourner son index.

  3. Ordre inverse : Collections.reverse(List<T>) – cette méthode est utilisée pour inverser l'ordre des éléments dans une liste.

  4. Opérations Min/Max : Collections.min(Collection<T>) et Collections.max(Collection<T>) – ces méthodes sont utilisées pour trouver les éléments minimum et maximum dans une collection, respectivement.

  5. Synchronisation : Collections.synchronizedList(List<T>) – cette méthode est utilisée pour rendre une liste thread-safe en la synchronisant.

  6. Collections non modifiables : Collections.unmodifiableList(List<T>) – cette méthode est utilisée pour créer une vue en lecture seule d'une liste, empêchant les modifications.

Voici un exemple de programme Java qui démontre diverses fonctionnalités de la classe utilitaire Collections :

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>();
        numbers.add(5);
        numbers.add(3);
        numbers.add(8);
        numbers.add(1);

        // Tri
        Collections.sort(numbers);
        System.out.println("Liste triée : " + numbers);

        // Recherche
        int index = Collections.binarySearch(numbers, 3);
        System.out.println("Index de 3 : " + index);

        // Ordre inverse
        Collections.reverse(numbers);
        System.out.println("Liste inversée : " + numbers);

        // Opérations Min/Max
        int min = Collections.min(numbers);
        int max = Collections.max(numbers);
        System.out.println("Min : " + min + ", Max : " + max);

        // Synchronisation
        List<Integer> synchronizedList = Collections.synchronizedList(numbers);
        System.out.println("Liste synchronisée : " + synchronizedList);

        // Collections non modifiables
        List<Integer> unmodifiableList = Collections.unmodifiableList(numbers);
        System.out.println("Liste non modifiable : " + unmodifiableList);
    }
}

Ce programme démontre le tri, la recherche, l'inversion, la recherche des valeurs minimale et maximale, la synchronisation et la création d'une liste non modifiable en utilisant la classe utilitaire Collections.

Conclusion

Dans cet article, vous avez appris le Java Collections Framework et comment il aide à gérer des groupes d'objets dans les applications Java. Nous avons exploré divers types de collections comme les Listes, les Ensembles, les Files d'attente et les Maps, et nous avons acquis des connaissances sur certaines des caractéristiques clés et comment chacun de ces types les prend en charge.

Vous avez appris la performance, la synchronisation et les méthodes clés, obtenant des informations précieuses pour choisir les bonnes structures de données pour vos besoins.

En comprenant ces concepts, vous pouvez pleinement utiliser le Java Collections Framework, ce qui vous permet d'écrire un code plus efficace et de construire des applications robustes.

Si vous avez trouvé cet article intéressant, n'hésitez pas à consulter mes autres articles sur freeCodeCamp et à me connecter sur LinkedIn.