Comment utiliser les API de Collections Java efficacement?

Une collection est un groupe d’objets contenus dans un objet unique. Le Framework de Collections Java est un ensemble de classes dans java.util pour stocker des collections. Il existe quatre interfaces principales dans le Framework de Collections Java.

  • List: Une List est une collection ordonnée d’éléments qui permet les entrées en double. Les éléments d’une liste peuvent être accessibles par un index int.
  • Set: Un Set est une collection qui ne permet pas les entrées en double.
  • Queue: Une Queue est une collection qui ordonne ses éléments dans un ordre spécifique pour le traitement. Une Deque est une sous-interface de Queue qui permet l’accès aux deux extrémités.
  • Map: Une Map est une collection qui associe des clés à des valeurs, sans autoriser de clés en double. Les éléments d’une Map sont des paires clé/valeur.

La Figure 9.1 montre l’interface Collection, ses sous-interfaces et certaines classes qui implémentent les interfaces. Les interfaces sont représentées dans des rectangles, et les classes dans des boîtes arrondies.

Notez que Map n’implémente pas l’interface Collection. Elle est considérée comme faisant partie du Framework de Collections Java même si elle n’est pas techniquement une Collection. C’est une collection (notez la minuscule), cependant, dans le sens où elle contient un groupe d’objets. La raison pour laquelle les cartes sont traitées différemment est qu’elles ont besoin de méthodes différentes en raison de leur structure en paires clé/valeur.

Utilisation de l’Opérateur Diamant

Lors de la construction d’un cadre de Collections Java, vous devez spécifier le type qui sera à l’intérieur. Nous pourrions écrire du code utilisant les génériques comme suit :

Liste<Integer> liste = new ArrayList<Integer>();

Vous pourriez même avoir des génériques qui contiennent d’autres génériques, comme ceci :

Map<Long,Liste<Integer>> carteDesListes = new HashMap<Long,Liste<Integer>>();

C’est beaucoup de code en double à écrire ! Heureusement, l’opérateur diamant (<>) est une notation abrégée qui vous permet d’omettre le type générique du côté droit d’une déclaration lorsque le type peut être déduit. Il est appelé opérateur diamant parce que <> ressemble à un diamant. Comparez les déclarations précédentes avec ces nouvelles versions beaucoup plus courtes :

Liste<Integer> liste = new ArrayList<>();
Map<Long,Liste<Integer>> carteDesListes = new HashMap<>();

Pour le compilateur, ces deux déclarations et nos précédentes sont équivalentes. Pour nous, cependant, la dernière est beaucoup plus courte et plus facile à lire.

L’opérateur diamant ne peut pas être utilisé comme type dans une déclaration de variable. Il ne peut être utilisé que du côté droit d’une opération d’affectation. Par exemple, aucun des éléments suivants ne compile :

Liste<> liste = new ArrayList<Integer>(); // NE COMPILE PAS

class UtilisationInvalide {
    void utiliser(Liste<> données) {} // NE COMPILE PAS
}

Ajout de Données

La méthode ajouter() insère un nouvel élément dans la Collection et renvoie si l’opération a réussi. La signature de la méthode est la suivante :

public boolean ajouter(E element)

Rappelez-vous que le Framework de Collections utilise des génériques. Vous verrez souvent apparaître E. Cela signifie le type générique qui a été utilisé pour créer la collection. Pour certains types de Collection, ajouter() renvoie toujours true. Pour d’autres types, il y a une logique pour déterminer si l’appel ajouter() a réussi. L’exemple suivant montre comment utiliser cette méthode :

Collection<String> liste = new ArrayList<>();
System.out.println(liste.ajouter("Moineau")); // true
System.out.println(liste.ajouter("Moineau")); // true

Collection<String> ensemble = new HashSet<>();
System.out.println(ensemble.ajouter("Moineau")); // true
System.out.println(ensemble.ajouter("Moineau")); // false

Une Liste permet les doublons, ce qui fait que la valeur de retour est true à chaque fois. Un Ensemble ne permet pas les doublons. Sur la ligne 9, nous avons essayé d’ajouter un doublon, donc Java renvoie false de la méthode ajouter().

Suppression de Données

La méthode supprimer() supprime une seule valeur correspondante dans la Collection et renvoie si l’opération a réussi. La signature de la méthode est la suivante :

public boolean supprimer(Object objet)

Cette fois, la valeur de retour booléenne nous indique si une correspondance a été supprimée. L’exemple suivant montre comment utiliser cette méthode :

Collection<String> oiseaux = new ArrayList<>();
oiseaux.ajouter("faucon");         // [faucon]
oiseaux.ajouter("faucon");         // [faucon, faucon]
System.out.println(oiseaux.supprimer("cardinal")); // false
System.out.println(oiseaux.supprimer("faucon"));   // true
System.out.println(oiseaux);                      // [faucon]

La ligne 6 tente de supprimer un élément qui n’est pas dans oiseaux. Elle renvoie false car aucun élément de ce type n’est trouvé. La ligne 7 tente de supprimer un élément qui est dans oiseaux, elle renvoie donc true. Notez qu’elle ne supprime qu’une seule correspondance.

Comptage des Éléments

Les méthodes estVide() et taille() examinent combien d’éléments se trouvent dans la Collection. Les signatures des méthodes sont les suivantes :

public boolean estVide()
public int taille()

L’exemple suivant montre comment utiliser ces méthodes :

Collection<String> oiseaux = new ArrayList<>();
System.out.println(oiseaux.estVide()); // true
System.out.println(oiseaux.taille());  // 0
oiseaux.ajouter("faucon");             // [faucon]
oiseaux.ajouter("faucon");             // [faucon, faucon]
System.out.println(oiseaux.estVide()); // false
System.out.println(oiseaux.taille());  // 2

Au début, oiseaux a une taille de 0 et est vide. Il a une capacité supérieure à 0. Après avoir ajouté des éléments, la taille devient positive, et il n’est plus vide.

Effacement de la Collection

La méthode effacer() fournit un moyen facile de jeter tous les éléments de la Collection. La signature de la méthode est la suivante :

public void effacer()

L’exemple suivant montre comment utiliser cette méthode :

Collection<String> oiseaux = new ArrayList<>();
oiseaux.ajouter("faucon");             // [faucon]
oiseaux.ajouter("faucon");             // [faucon, faucon]
System.out.println(oiseaux.estVide()); // false
System.out.println(oiseaux.taille());  // 2
oiseaux.effacer();                     // []
System.out.println(oiseaux.estVide()); // true
System.out.println(oiseaux.taille());  // 0

Après avoir appelé effacer(), oiseaux redevient un ArrayList vide de taille 0.

Vérification du Contenu

La méthode contient() vérifie si une certaine valeur est dans la Collection. La signature de la méthode est la suivante :

public boolean contient(Object objet)

L’exemple suivant montre comment utiliser cette méthode :

Collection<String> oiseaux = new ArrayList<>();
oiseaux.ajouter("faucon"); // [faucon]
System.out.println(oiseaux.contient("faucon"));  // true
System.out.println(oiseaux.contient("rouge-gorge")); // false

La méthode contient() appelle equals() sur les éléments de l’ArrayList pour voir s’il y a des correspondances.

Suppression avec Conditions

La méthode supprimerSi() supprime tous les éléments qui correspondent à une condition. Nous pouvons spécifier ce qui doit être supprimé en utilisant un bloc de code ou même une référence de méthode.

La signature de la méthode ressemble à ce qui suit. (Nous expliquerons ce que signifie le ? super dans la section « Travailler avec les Génériques » plus loin dans ce chapitre.)

public boolean supprimerSi(Predicate<? super E> filtre)

Elle utilise un Predicate, qui prend un paramètre et renvoie un booléen. Voyons un exemple :

Collection<String> liste = new ArrayList<>();
liste.ajouter("Magicien");
liste.ajouter("Assistant");
System.out.println(liste); // [Magicien, Assistant]
liste.supprimerSi(s -> s.startsWith("A"));
System.out.println(liste); // [Magicien]

La ligne 8 montre comment supprimer toutes les valeurs String qui commencent par la lettre A. Cela nous permet de faire disparaître l’Assistant. Essayons un exemple avec une référence de méthode :

Collection<String> ensemble = new HashSet<>();
ensemble.ajouter("Baguette");
ensemble.ajouter("");
ensemble.supprimerSi(String::isEmpty); // s -> s.isEmpty()
System.out.println(ensemble); // [Baguette]

À la ligne 14, nous supprimons tous les objets String vides de l’ensemble. Le commentaire sur cette ligne montre la lambda équivalente de la référence de méthode. La ligne 15 montre que la méthode supprimerSi() a réussi à supprimer un élément de la liste.

Itération

Il existe une méthode pourChaque() que vous pouvez appeler sur une Collection au lieu d’écrire une boucle. Elle utilise un Consumer qui prend un seul paramètre et ne renvoie rien. La signature de la méthode est la suivante :

public void pourChaque(Consumer<? super T> action)

Les chats aiment explorer, alors imprimons-en deux en utilisant à la fois des références de méthode et des lambdas :

Collection<String> chats = List.of("Annie", "Ripley");
chats.pourChaque(System.out::println);
chats.pourChaque(c -> System.out.println(c));

Les chats ont découvert comment imprimer leurs noms. Maintenant, ils ont plus de temps pour jouer (comme nous) !

Autres Approches d’Itération

Il existe d’autres façons d’itérer à travers une Collection. Par exemple, vous pouvez voir comment parcourir une liste en utilisant une boucle for améliorée.

for (String element: coll)
    System.out.println(element);

Vous pouvez voir une autre approche plus ancienne utilisée.

Iterator<String> iter = coll.iterator();
while(iter.hasNext()) {
    String chaine = iter.next();
    System.out.println(chaine);
}

Faites attention à la différence entre ces techniques. La méthode hasNext() vérifie s’il y a une valeur suivante. En d’autres termes, elle vous indique si next() s’exécutera sans lancer d’exception. La méthode next() déplace réellement l’Iterator vers l’élément suivant.

Détermination de l’Égalité

Il existe une implémentation personnalisée de equals() afin que vous puissiez comparer deux Collections pour comparer le type et le contenu. L’implémentation variera. Par exemple, ArrayList vérifie l’ordre, tandis que HashSet ne le fait pas.

boolean equals(Object objet)

L’exemple suivant montre un exemple :

var liste1 = List.of(1, 2);
var liste2 = List.of(2, 1);
var ensemble1 = Set.of(1, 2);
var ensemble2 = Set.of(2, 1);

System.out.println(liste1.equals(liste2));     // false
System.out.println(ensemble1.equals(ensemble2)); // true
System.out.println(liste1.equals(ensemble1));   // false

La ligne 28 affiche false car les éléments sont dans un ordre différent, et une Liste se soucie de l’ordre. En revanche, la ligne 29 affiche true car un Ensemble n’est pas sensible à l’ordre. Enfin, la ligne 30 affiche false car les types sont différents.

Déballage des nulls

Java nous protège de nombreux problèmes avec les Collections. Cependant, il est toujours possible d’écrire une NullPointerException :

var hauteurs = new ArrayList<Integer>();
hauteurs.ajouter(null);
int h = hauteurs.get(0); // NullPointerException

À la ligne 4, nous ajoutons un null à la liste. C’est légal car une référence null peut être assignée à n’importe quelle variable de référence. À la ligne 5, nous essayons de déballer ce null en un primitif int. C’est un problème. Java essaie d’obtenir la valeur int de null. Puisque l’appel de n’importe quelle méthode sur null donne une NullPointerException, c’est exactement ce que nous obtenons. Soyez prudent lorsque vous voyez null en relation avec l’autoboxing.