JAVA : Les collections

En Java, les collections fournissent une structure de données flexible pour stocker, organiser et manipuler des objets. Les collections sont implémentées par le biais d’interfaces et de classes dans le package java.util.

1- Introduction : Pourquoi Utiliser les Collections en Java ?

Les collections en Java jouent un rôle essentiel dans le développement d’applications, offrant une manière efficace et flexible de gérer et manipuler des ensembles d’objets. Une collection peut être considérée comme une structure de données qui permet de stocker, organiser et manipuler des éléments de manière dynamique. Voici quelques raisons fondamentales pour lesquelles les développeurs utilisent activement les collections en Java :

a. Flexibilité de Stockage :

  • Les collections offrent une variété d’implémentations adaptées à différentes situations. Que vous ayez besoin d’une liste ordonnée, d’un ensemble sans doublons, ou d’une association clé-valeur, il existe des collections spécifiquement conçues pour répondre à ces besoins.

b. Gestion Dynamique de la Taille :

  • Contrairement aux tableaux de taille fixe, les collections peuvent être redimensionnées dynamiquement. Elles permettent d’ajouter ou de supprimer des éléments sans avoir à se soucier de la gestion manuelle de la taille ou de la mémoire.

c. Facilité d’Utilisation :

  • Les collections fournissent des méthodes pratiques pour effectuer des opérations courantes telles que l’ajout, la suppression, la recherche et l’itération sur les éléments. Cela simplifie grandement le processus de manipulation des données.

d. Autoboxing et Unboxing :

  • Les collections peuvent travailler avec des types primitifs grâce à l’autoboxing (conversion automatique des types primitifs en objets) et unboxing (conversion automatique des objets en types primitifs). Cela améliore la compatibilité et la flexibilité du code.

e. Itération Facilitée :

  • Les boucles d’itération simplifiées (comme la boucle for-each) rendent le parcours des éléments d’une collection particulièrement élégant et lisible.

f. Algorithme de Traitement des Données :

  • Les collections fournissent des méthodes performantes pour trier, filtrer et manipuler les données. Elles permettent d’appliquer des algorithmes complexes avec une efficacité optimale.

g. Interopérabilité avec d’Autres Composants :

  • Les collections peuvent être utilisées en conjonction avec d’autres fonctionnalités de Java, telles que les flux (streams), les expressions lambda, et les interfaces fonctionnelles, permettant ainsi d’adopter des approches modernes et concises de programmation.

h. Concurrence et Sécurité :

  • Certaines implémentations de collections offrent des mécanismes de gestion de la concurrence, tandis que d’autres garantissent la sécurité dans des contextes multi-threaded.

2 – Qu’est-ce qu’une collection Java ?

Les collections Java sont simplement des structures de données représentant un groupe d’objets Java. Les développeurs peuvent travailler avec des collections de la même manière qu’ils travaillent avec d’autres types de données, en effectuant des tâches courantes telles que des recherches ou en manipulant le contenu de la collection.

Un exemple de collection en Java est l’interface Set (java.util.Set). Un Set est une collection qui n’autorise pas les éléments en double et ne stocke pas les éléments dans un ordre particulier. L’interface Set hérite ses méthodes de Collection (java.util.Collection) et ne contient que ces méthodes.

En plus des ensembles, il existe des files d’attente (java.util.Queue) et des maps (java.util.Map). Les maps ne sont pas des collections au sens le plus vrai car elles n’étendent pas les interfaces de collection, mais les développeurs peuvent manipuler des Maps comme s’il s’agissait de collections. SetsQueuesLists et Maps ont chacun des descendants, tels que des ensembles triés (java. util.SortedSet) et des maps navigables (java.util.NavigableMap).

Lorsqu’ils travaillent avec des collections, les développeurs doivent connaître et comprendre certains termes spécifiques liés aux collections :

  • Modifiable vs non modifiable : comme ces termes le suggèrent à première vue, différentes collections peuvent ou non prendre en charge les opérations de modification
  • Mutable ou immuable : les collections immuables ne peuvent pas être modifiées après leur création. Bien qu’il existe des situations où les collections non modifiables peuvent encore changer en raison de l’accès par un autre code, les collections immuables empêchent de telles modifications. Les collections qui peuvent garantir qu’aucune modification n’est visible avec les objets Collection sont immuables, tandis que les collections non modifiables sont des collections qui n’autorisent pas les opérations de modification telles que « add » ou « clear »
  • Taille fixe ou taille variable : ces termes se réfèrent uniquement à la taille de la collection et n’indiquent pas si la collection est modifiable ou mutable
  • Accès aléatoire ou accès séquentiel : si une collection permet l’indexation d’éléments individuels, il s’agit d’un accès aléatoire. Dans les collections à accès séquentiel, vous devez parcourir tous les éléments précédents pour atteindre un élément donné. Les collections à accès séquentiel peuvent être plus faciles à étendre mais prennent plus de temps à rechercher

Les programmeurs débutants peuvent avoir du mal à saisir la différence entre les collections non modifiables et immuables. Les collections non modifiables ne sont pas nécessairement immuables. En effet, les collections non modifiables sont souvent des enveloppes autour d’une collection modifiable à laquelle un autre code peut toujours accéder et modifier. Un autre code peut en fait être en mesure de modifier la collection sous-jacente. Il faudra un certain temps de travail avec les collections pour acquérir un degré de confort avec des collections non modifiables et immuables.

Par exemple, envisagez de créer une liste modifiable des cinq principales crypto-monnaies par capitalisation boursière. Vous pouvez créer une version non modifiable de la liste modifiable sous-jacente à l’aide de la méthode java.util.Collections.unmodifiableList(). Vous pouvez toujours modifier la liste sous-jacente, qui apparaîtra dans la liste non modifiable. Mais vous ne pouvez pas modifier directement la version non modifiable.

import java.util.*;
public class UnmodifiableCryptoListExample {  
    public static void main(String[] args) {  

        List<String> cryptoList = new ArrayList<>();  
        Collections.addAll(cryptoList, "BTC", "ETH", "USDT", "USDC", "BNB");  
        List<String> unmodifiableCryptoList = Collections.unmodifiableList(cryptoList);  
        System.out.println("Unmodifiable crypto List: " + unmodifiableCryptoList);  

        // try to add one more cryptocurrency to modifiable list and show in unmodifiable list
        cryptoList.add("BUSD");
        System.out.println("New unmodifiable crypto List with new element:" + unmodifiableCryptoList);

        // try to add one more cryptocurrency to unmodifiable list and show in unmodifiable list - unmodifiableCryptoList.add would throw an uncaught exception and the println would not run.
        unmodifiableCryptoList.add("XRP");
        System.out.println("New unmodifiable crypto List with new element:" + unmodifiableCryptoList);

        }  
}

Le code fourni démontre l’utilisation de Collections.unmodifiableList pour créer une vue non modifiable (immuable) d’une liste existante en Java.

Explication du code :

  1. Création de la liste modifiable :
    • Une liste cryptoList est créée en utilisant ArrayList et y sont ajoutées plusieurs cryptomonnaies.
  2. Création de la vue non modifiable :
    • En utilisant Collections.unmodifiableList(cryptoList), une vue non modifiable (unmodifiableCryptoList) de la liste existante est créée. Cela signifie que les tentatives de modification directe de la vue généreront une exception.
  3. Affichage de la liste non modifiable :
    • La liste non modifiable est affichée.
  4. Tentative d’ajout à la liste modifiable :
    • Une nouvelle cryptomonnaie (« BUSD ») est ajoutée à la liste modifiable, et la liste non modifiable est affichée pour montrer que la modification a également affecté la vue non modifiable.
  5. Tentative d’ajout à la liste non modifiable :
    • Une tentative d’ajout d’une nouvelle cryptomonnaie (« XRP ») directement à la liste non modifiable génère une exception UnsupportedOperationException.
  6. Affichage de la liste non modifiable après la tentative d’ajout infructueuse :
    • La liste non modifiable est affichée à nouveau après la tentative d’ajout infructueuse pour montrer qu’elle n’a pas été modifiée.

Notez la différence, cependant, si vous créez une liste immuable, puis essayez de modifier la liste sous-jacente. Il existe de nombreuses façons de créer des listes immuables à partir de listes modifiables existantes, et ci-dessous, nous utilisons la méthode List.copyOf().

import java.util.*;
public class UnmodifiableCryptoListExample {  
    public static void main(String[] args) {  

        List<String> cryptoList = new ArrayList<>();  
        Collections.addAll(cryptoList, "BTC", "ETH", "USDT", "USDC", "BNB");
        List immutableCryptoList = List.copyOf(cryptoList);
        System.out.println("Underlying crypto list:" + cryptoList)
        System.out.println("Immutable crypto ist: " + immutableCryptoList);  

        // try to add one more cryptocurrency to modifiable list and show immutable does not display change
        cryptoList.add("BUSD");
        System.out.println("New underlying list:" + cryptoList);
        System.out.println("New immutable crypto List:" + immutableCryptoList);

        // try to add one more cryptocurrency to unmodifiable list and show in unmodifiable list -
        immutableCryptoList.add("XRP");
        System.out.println("New unmodifiable crypto List with new element:" + immutableCryptoList);

        }  
}

Après avoir modifié la liste sous-jacente, la liste immuable n’affiche pas la modification. Et essayer de modifier la liste immuable entraîne directement une UnsupportedOperationException :

Explication du code :

  1. Création de la liste modifiable :
    • Une liste cryptoList est créée en utilisant ArrayList et y sont ajoutées plusieurs cryptomonnaies.
  2. Création de la liste immuable :
    • En utilisant List.copyOf(cryptoList), une liste immuable (immutableCryptoList) est créée à partir de la liste modifiable existante. Cela garantit que la nouvelle liste est immuable.
  3. Affichage des deux listes :
    • Les deux listes (modifiable et immuable) sont affichées pour montrer les éléments initiaux.
  4. Tentative d’ajout à la liste modifiable :
    • Une nouvelle cryptomonnaie (« BUSD ») est ajoutée à la liste modifiable. Les deux listes sont affichées à nouveau pour montrer que la modification a également affecté la liste immuable.
  5. Tentative d’ajout à la liste immuable :
    • Une tentative d’ajout d’une nouvelle cryptomonnaie (« XRP ») directement à la liste immuable génère une exception UnsupportedOperationException.
  6. Affichage de la liste immuable après la tentative d’ajout infructueuse :
    • La liste immuable est affichée à nouveau après la tentative d’ajout infructueuse pour montrer qu’elle n’a pas été modifiée.

Comment les collections sont-elles liées au Java Collections Framework ?

Avant l’introduction du JCF, les développeurs pouvaient regrouper des objets à l’aide de plusieurs classes spéciales, à savoir les classes Array, Vector et HashTable. Malheureusement, ces classes présentaient des limites importantes. En plus de manquer d’interface commune, ils étaient difficiles à étendre.

Le JCF a fourni une architecture commune globale pour travailler avec les collections. L’interface des collections contient plusieurs composants différents, notamment :

  • Interfaces communes : des représentations des principaux types de collections, y compris les ensembles, les listes et les maps
  • Implémentations : des implémentations spécifiques des interfaces de collection, allant de l’usage général à l’usage spécial en passant par l’abstrait ; en outre, il existe des implémentations héritées liées aux anciennes classes Array, Vector et HashTable
  • Algorithmes : des méthodes statiques pour manipuler les collections
  • Infrastructure : la prise en charge sous-jacente des différentes interfaces de collections

Le JCF offrait aux développeurs de nombreux avantages par rapport aux méthodes de regroupement d’objets précédentes. Notamment, le JCF a rendu la programmation Java plus efficace en réduisant la nécessité pour les développeurs d’écrire leurs propres structures de données.

Mais le JCF a également fondamentalement modifié la façon dont les développeurs travaillaient avec les API. Avec un nouveau langage commun pour traiter les différentes API, le JCF a simplifié l’apprentissage et la conception des API et leur mise en œuvre pour les développeurs. De plus, les API sont devenues beaucoup plus interopérables. Un exemple est Eclipse Collections, une bibliothèque de collections Java open source entièrement compatible avec différents types de collections Java.

Des gains d’efficacité de développement supplémentaires sont apparus parce que le JCF a fourni des structures qui ont facilité la réutilisation du code. En conséquence, le temps de développement a diminué et la qualité du programme a augmenté.

Le JCF a une hiérarchie définie d’interfaces. java.util.collection étend la superinterface Iterable. Dans Collection, il existe de nombreuses interfaces et classes filles, comme indiqué ci-dessous :

Comme indiqué précédemment, les ensembles sont des groupes non ordonnés d’objets uniques. Les listes, en revanche, sont des collections ordonnées qui peuvent contenir des doublons. Bien que vous puissiez ajouter des éléments à n’importe quel endroit d’une liste, l’ordre global est préservé.

Pour illustrer ces concepts en Java, voici un exemple utilisant une List et un Set :

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class ExempleListeEtEnsemble {
    public static void main(String[] args) {
        // Exemple de liste
        List<String> liste = new ArrayList<>();
        liste.add("Apple");
        liste.add("Banana");
        liste.add("Orange");
        liste.add("Banana");  // Doublon autorisé

        System.out.println("Liste : " + liste);

        // Exemple d'ensemble
        Set<String> ensemble = new HashSet<>();
        ensemble.add("Apple");
        ensemble.add("Banana");
        ensemble.add("Orange");
        ensemble.add("Banana");  // Ignorera le doublon

        System.out.println("Ensemble : " + ensemble);
    }
}

Explication :

  1. Dans la liste, l’ordre d’insertion est préservé, et les doublons sont autorisés.
  2. Dans l’ensemble, l’ordre n’est pas garanti, et les doublons sont automatiquement ignorés.

Vous pouvez voir la différence entre les deux structures de données en examinant les sorties. La liste conserve l’ordre d’insertion et autorise les doublons, tandis que l’ensemble n’a pas d’ordre spécifique et ne permet pas de doublons. Vous pouvez également observer comment les éléments sont ajoutés et affichés dans ces structures.

Le programme que j’ai fourni affiche les résultats suivants :

Liste : [Apple, Banana, Orange, Banana]
Ensemble : [Orange, Banana, Apple]

Explication du Résultat :

  1. Dans la liste, les éléments sont affichés dans l’ordre dans lequel ils ont été insérés. Le doublon « Banana » est autorisé.
  2. Dans l’ensemble, l’ordre n’est pas préservé, et le doublon « Banana » est ignoré. L’ensemble affiche uniquement des éléments uniques.

Cela démontre les différences fondamentales entre une liste et un ensemble en termes d’ordre et de gestion des doublons.

Les files d’attente (queues) sont des collections où des éléments sont ajoutés à une extrémité et supprimés à l’autre extrémité, c’est-à-dire qu’il s’agit d’une interface premier entré, premier sorti (FIFO). Les Deques (files d’attente à double extrémité) permettent l’ajout ou la suppression d’éléments à chaque extrémité.

Voici un exemple simple en Java qui utilise une file d’attente (Queue) et une double file d’attente (Deque) pour illustrer les principes FIFO et les opérations à double extrémité :

import java.util.LinkedList;
import java.util.Queue;
import java.util.ArrayDeque;
import java.util.Deque;

public class FileAttenteExemple {
    public static void main(String[] args) {
        // Utilisation d'une Queue (file d'attente)
        Queue<String> fileAttente = new LinkedList<>();
        fileAttente.offer("Client1");
        fileAttente.offer("Client2");
        fileAttente.offer("Client3");

        System.out.println("File d'attente (FIFO) : " + fileAttente);

        // Retrait des éléments selon le principe FIFO
        while (!fileAttente.isEmpty()) {
            System.out.println("Client traité : " + fileAttente.poll());
        }

        // Utilisation d'une Deque (double file d'attente)
        Deque<String> doubleFileAttente = new ArrayDeque<>();
        doubleFileAttente.offerFirst("ClientA");
        doubleFileAttente.offerLast("ClientB");
        doubleFileAttente.offerLast("ClientC");

        System.out.println("Double file d'attente : " + doubleFileAttente);

        // Retrait des éléments des deux extrémités
        System.out.println("Client traité à la première extrémité : " + doubleFileAttente.pollFirst());
        System.out.println("Client traité à la dernière extrémité : " + doubleFileAttente.pollLast());
        System.out.println("Double file d'attente après retrait : " + doubleFileAttente);
    }
}

Explication du Programme :

  1. La première partie utilise une Queue (file d’attente) implémentée avec LinkedList pour illustrer le principe FIFO.
  2. La deuxième partie utilise une Deque (double file d’attente) implémentée avec ArrayDeque pour montrer les opérations à double extrémité (ajout/retait au début et à la fin).

Résultat Attendu :

File d'attente (FIFO) : [Client1, Client2, Client3]
Client traité : Client1
Client traité : Client2
Client traité : Client3
Double file d'attente : [ClientA, ClientB, ClientC]
Client traité à la première extrémité : ClientA
Client traité à la dernière extrémité : ClientC
Double file d'attente après retrait : [ClientB]

Les méthodes pour utiliser des collections Java

Chaque interface du JCF, y compris java.util.Collection, possède des méthodes spécifiques disponibles pour accéder et manipuler des éléments individuels de la collection. Parmi les méthodes les plus couramment utilisées dans les collections, citons :

  • size() : renvoie le nombre d’éléments dans une collection
  • add(Collection element) / remove(Collection object) : comme suggéré, ces méthodes modifient le contenu d’une collection ; notez que dans le cas où une collection a des doublons, la suppression n’affecte qu’une seule instance de l’élément
  • equals(Collection object) : compare un objet pour l’équivalence avec une collection
  • clear() : supprime tous les éléments d’une collection

Chaque sous-interface peut également avoir des méthodes supplémentaires. Par exemple, bien que l’interface Set n’inclue que les méthodes de l’interface Collection, l’interface List possède de nombreuses méthodes supplémentaires basées sur l’accès à des éléments de liste spécifiques, y compris :

  • get(int index) : renvoie l’élément de la liste à partir de l’emplacement d’index spécifié
  • set(int index, element) : définit le contenu de l’élément de liste à l’emplacement d’index spécifié
  • remove(int,index) : supprime l’élément à l’emplacement d’index spécifié

En plus, Les interfaces du framework de collections Java (java.util) fournissent plusieurs méthodes communes pour manipuler et accéder aux éléments de collections. Voici certaines des méthodes couramment utilisées :

Méthodes dans l’interface Collection :

  1. boolean add(E element) :
  • Ajoute un élément à la collection.
  1. boolean addAll(Collection<? extends E> collection) :
  • Ajoute tous les éléments d’une autre collection à la collection actuelle.
  1. void clear() :
  • Supprime tous les éléments de la collection.
  1. boolean contains(Object element) :
  • Vérifie si la collection contient un certain élément.
  1. boolean isEmpty() :
  • Vérifie si la collection est vide.
  1. Iterator<E> iterator() :
  • Renvoie un itérateur pour parcourir les éléments de la collection.
  1. boolean remove(Object element) :
  • Supprime la première occurrence d’un élément spécifié de la collection.
  1. boolean removeAll(Collection<?> collection) :
  • Supprime tous les éléments de la collection qui sont également présents dans une autre collection.
  1. boolean retainAll(Collection<?> collection) :
  • Supprime de la collection tous les éléments qui ne sont pas présents dans une autre collection.
  1. int size() :
  • Renvoie le nombre d’éléments dans la collection.

Méthodes dans l’interface List (qui étend Collection) :

  1. void add(int index, E element) :
    • Insère un élément à un emplacement spécifique dans la liste.
  2. boolean containsAll(Collection<?> collection) :
    • Vérifie si tous les éléments d’une autre collection sont présents dans la liste.
  3. E get(int index) :
    • Renvoie l’élément à l’index spécifié dans la liste.
  4. int indexOf(Object element) :
    • Renvoie l’index de la première occurrence d’un élément spécifié dans la liste.
  5. int lastIndexOf(Object element) :
    • Renvoie l’index de la dernière occurrence d’un élément spécifié dans la liste.
  6. ListIterator<E> listIterator() :
    • Renvoie un itérateur de liste pour parcourir les éléments de la liste.
  7. ListIterator<E> listIterator(int index) :
    • Renvoie un itérateur de liste à partir d’une position spécifiée.
  8. E remove(int index) :
    • Supprime l’élément à un emplacement spécifique dans la liste.
  9. E set(int index, E element) :
    • Remplace l’élément à un emplacement spécifique dans la liste.
  10. List<E> subList(int fromIndex, int toIndex) :
    • Renvoie une vue partielle de la liste entre les index spécifiés.

Méthodes dans l’interface Set (qui étend Collection) :

  1. Set<E> intersection(Set<?> set) :
    • Renvoie l’intersection entre deux ensembles.
  2. Set<E> union(Set<? extends E> set) :
    • Renvoie l’union de deux ensembles.
  3. Set<E> difference(Set<?> set) :
    • Renvoie la différence entre deux ensembles.

Méthodes dans l’interface Map :

  1. V get(Object key) :
    • Renvoie la valeur associée à une clé spécifiée dans la carte.
  2. V put(K key, V value) :
    • Associe une valeur à une clé dans la carte.
  3. void putAll(Map<? extends K, ? extends V> map) :
    • Ajoute toutes les entrées d’une autre carte à la carte actuelle.
  4. V remove(Object key) :
    • Supprime l’entrée associée à une clé spécifiée dans la carte.
  5. boolean containsKey(Object key) :
    • Vérifie si la carte contient une clé spécifiée.
  6. boolean containsValue(Object value) :
    • Vérifie si la carte contient une valeur spécifiée.
  7. Set<K> keySet() :
    • Renvoie un ensemble de toutes les clés dans la carte.
  8. Collection<V> values() :
    • Renvoie une collection de toutes les valeurs dans la carte.
  9. Set<Map.Entry<K, V>> entrySet() :
    • Renvoie un ensemble de toutes les entrées (clé-valeur) dans la carte.

Ces méthodes constituent une partie des fonctionnalités offertes par les différentes interfaces de la bibliothèque de collections Java. Il en existe bien d’autres pour répondre à divers besoins de manipulation de données.