Comment utiliser l’interface List en Java?

Maintenant que vous êtes familier avec certaines méthodes communes de l’interface Collection, passons aux interfaces spécifiques. Vous utilisez une liste lorsque vous voulez une collection ordonnée qui peut contenir des entrées en double. Par exemple, une liste de noms peut contenir des doublons, car deux animaux peuvent avoir le même nom. Les éléments peuvent être récupérés et insérés à des positions spécifiques dans la liste en fonction d’un index int, très similaire à un tableau. Contrairement à un tableau, cependant, de nombreuses implémentations de List peuvent changer de taille après leur déclaration.

Les listes sont couramment utilisées car il existe de nombreuses situations en programmation où vous devez garder une trace d’une liste d’objets. Par exemple, vous pourriez faire une liste de ce que vous voulez voir au zoo : d’abord, voir les lions, car ils s’endorment tôt ; ensuite, voir les pandas, car il y a une longue file d’attente plus tard dans la journée ; et ainsi de suite.

Chaque élément de la List a un index et les index commencent à zéro.

Comparaison des implémentations de List

Un ArrayList est comme un tableau redimensionnable. Lorsque des éléments sont ajoutés, l’ArrayList s’agrandit automatiquement. Lorsque vous n’êtes pas sûr de quelle collection utiliser, utilisez un ArrayList. Le principal avantage d’un ArrayList est que vous pouvez rechercher n’importe quel élément en temps constant. L’ajout ou la suppression d’un élément est plus lent que l’accès à un élément. Cela fait d’ArrayList un bon choix lorsque vous lisez plus souvent que (ou autant que) vous écrivez dans l’ArrayList.

Un LinkedList est spécial car il implémente à la fois List et Deque. Il a toutes les méthodes d’une List. Il a également des méthodes supplémentaires pour faciliter l’ajout ou la suppression depuis le début et/ou la fin de la liste.

Les principaux avantages d’un LinkedList sont que vous pouvez accéder, ajouter et supprimer depuis le début et la fin de la liste en temps constant. En contrepartie, traiter un index arbitraire prend un temps linéaire. Cela fait de LinkedList un bon choix lorsque vous l’utiliserez comme Deque. Un LinkedList implémente à la fois les interfaces List et Deque.

Création d’une List avec une fabrique

Lorsque vous créez une List de type ArrayList ou LinkedList, vous connaissez le type. Il existe quelques méthodes spéciales où vous obtenez une List en retour mais ne connaissez pas le type. Ces méthodes vous permettent de créer une List incluant des données en une ligne en utilisant une méthode de fabrique. C’est pratique, surtout lors des tests. Certaines de ces méthodes renvoient un objet immuable. Un objet immuable ne peut pas être modifié. Le tableau 9.1 résume ces trois listes.

MéthodeDescriptionPeut ajouter des éléments?Peut remplacer des éléments?Peut supprimer des éléments?
Arrays.asList(varargs)Renvoie une liste de taille fixe soutenue par un tableauNonOuiNon
List.of(varargs)Renvoie une liste immuableNonNonNon
List.copyOf(collection)Renvoie une liste immuable avec une copie des valeurs de la collection d’origineNonNonNon

Examinons un exemple de ces trois méthodes :

String[] tableau = new String[] {"a", "b", "c"};
List<String> asList = Arrays.asList(tableau); // [a, b, c]
List<String> of = List.of(tableau); // [a, b, c]
List<String> copy = List.copyOf(asList); // [a, b, c]

tableau[0] = "z";

System.out.println(asList); // [z, b, c]
System.out.println(of); // [a, b, c]
System.out.println(copy); // [a, b, c]

asList.set(0, "x");
System.out.println(Arrays.toString(tableau)); // [x, b, c]

copy.add("y"); // UnsupportedOperationException

La ligne 17 crée une List qui est soutenue par un tableau. La ligne 21 change le tableau, et la ligne 23 reflète ce changement. Les lignes 27 et 28 montrent l’autre direction où la modification de la List met à jour le tableau sous-jacent. Les lignes 18 et 19 créent une List immuable. La ligne 30 montre qu’elle est immuable en lançant une exception lors de la tentative d’ajout d’une valeur. Les trois listes lanceraient une exception lors de l’ajout ou de la suppression d’une valeur. Les listes of et copy lanceraient également une exception en essayant de mettre à jour un élément.

Création d’une List avec un constructeur

La plupart des Collections ont deux constructeurs que vous devez connaître. Voici comment ils se présentent pour LinkedList :

var liee1 = new LinkedList<String>();
var liee2 = new LinkedList<String>(liee1);

Le premier dit de créer un LinkedList vide contenant toutes les valeurs par défaut. Le second indique à Java que nous voulons faire une copie d’un autre LinkedList. Certes, liee1 est vide dans cet exemple, donc ce n’est pas particulièrement intéressant.

ArrayList a un constructeur supplémentaire que vous devez connaître. Nous montrons maintenant les trois constructeurs :

var liste1 = new ArrayList<String>();
var liste2 = new ArrayList<String>(liste1);
var liste3 = new ArrayList<String>(10);

Les deux premiers sont les constructeurs communs que vous devez connaître pour toutes les Collections. L’exemple final dit de créer un ArrayList contenant un nombre spécifique d’emplacements, mais à nouveau de ne pas en attribuer. Vous pouvez considérer cela comme la taille du tableau sous-jacent.

Utilisation de var avec ArrayList

Considérez ce code, qui mélange var et generics :

var chaines = new ArrayList<String>();
chaines.add("a");
for (String s: chaines) { }

Le type de var est ArrayList<String>. Cela signifie que vous pouvez ajouter une String ou parcourir les objets String. Que se passe-t-il si nous utilisons l’opérateur diamant avec var ?

var liste = new ArrayList<>();

Croyez-le ou non, cela compile. Le type de var est ArrayList<Object>. Puisqu’il n’y a pas de type spécifié pour le générique, Java doit supposer la superclasse ultime. C’est un peu idiot et inattendu, alors s’il vous plaît ne l’écrivez pas. Mais si vous le voyez, vous saurez à quoi vous attendre. Maintenant, pouvez-vous comprendre pourquoi cela ne compile pas ?

var liste = new ArrayList<>();
liste.add("a");
for (String s: liste) { } // NE COMPILE PAS

Le type de var est ArrayList<Object>. Puisqu’il n’y a pas de type dans l’opérateur diamant, Java doit supposer l’option la plus générique possible. Par conséquent, il choisit Object, la superclasse ultime. Ajouter une String à la liste est acceptable. Vous pouvez ajouter n’importe quelle sous-classe de Object. Cependant, dans la boucle, nous devons utiliser le type Object plutôt que String.

Travailler avec les méthodes de List

Les méthodes de l’interface List sont destinées à travailler avec des index. En plus des méthodes Collection héritées, les signatures de méthodes que vous devez connaître sont dans le tableau 9.2.

MéthodeDescription
public boolean add(E element)Ajoute un élément à la fin (disponible sur toutes les API Collection).
public void add(int index, E element)Ajoute un élément à l’index et déplace le reste vers la fin.
public E get(int index)Renvoie l’élément à l’index.
public E remove(int index)Supprime l’élément à l’index et déplace le reste vers l’avant.
public default void replaceAll(UnaryOperator<E> op)Remplace chaque élément de la liste par le résultat de l’opérateur.
public E set(int index, E e)Remplace l’élément à l’index et renvoie l’original. Lance IndexOutOfBoundsException si l’index est invalide.
public default void sort(Comparator<? super E> c)Trie la liste.

Les déclarations suivantes démontrent la plupart de ces méthodes pour travailler avec une List :

List<String> liste = new ArrayList<>();
liste.add("SD"); // [SD]
liste.add(0, "NY"); // [NY,SD]
liste.set(1, "FL"); // [NY,FL]
System.out.println(liste.get(0)); // NY
liste.remove("NY"); // [FL]
liste.remove(0); // []
liste.set(0, "?"); // IndexOutOfBoundsException

À la ligne 3, liste commence vide. La ligne 4 ajoute un élément à la fin de la liste. La ligne 5 ajoute un élément à l’index 0 qui fait passer l’index 0 original à l’index 1. Remarquez comment l’ArrayList est maintenant automatiquement plus grand d’un élément. La ligne 6 remplace l’élément à l’index 1 par une nouvelle valeur.

La ligne 7 utilise la méthode get() pour imprimer l’élément à un index spécifique. La ligne 8 supprime l’élément correspondant à NY. Enfin, la ligne 9 supprime l’élément à l’index 0, et liste est à nouveau vide.

La ligne 10 lance une IndexOutOfBoundsException car il n’y a pas d’éléments dans la List. Puisqu’il n’y a pas d’éléments à remplacer, même l’index 0 n’est pas autorisé. Si la ligne 10 était déplacée entre les lignes 4 et 5, l’appel réussirait.

La sortie serait la même si vous essayiez ces exemples avec LinkedList. Bien que le code serait moins efficace, ce ne serait pas notable jusqu’à ce que vous ayez de très grandes listes.

Maintenant, jetons un coup d’œil à la méthode replaceAll(). Elle utilise un UnaryOperator qui prend un paramètre et renvoie une valeur du même type :

var nombres = Arrays.asList(1, 2, 3);
nombres.replaceAll(x -> x*2);
System.out.println(nombres); // [2, 4, 6]

Cette lambda double la valeur de chaque élément de la liste. La méthode replaceAll() appelle la lambda sur chaque élément de la liste et remplace la valeur à cet index.

Méthodes remove() surchargées

Nous avons maintenant vu deux méthodes remove() surchargées. Celle de Collection supprime un objet qui correspond au paramètre. En revanche, celle de List supprime un élément à un index spécifié.

Cela devient délicat lorsque vous avez un type Integer. Que pensez-vous que le code suivant imprime ?

var liste = new LinkedList<Integer>();
liste.add(3);
liste.add(2);
liste.add(1);
liste.remove(2);
liste.remove(Integer.valueOf(2));
System.out.println(liste);

La réponse correcte est [3]. Voyons comment nous y sommes arrivés. À la fin de la ligne 34, nous avons [3, 2, 1]. La ligne 35 passe un primitif, ce qui signifie que nous demandons la suppression de l’élément à l’index 2. Cela nous laisse avec [3, 2]. Ensuite, la ligne 36 passe un objet Integer, ce qui signifie que nous supprimons la valeur 2. Cela nous amène à [3].

Puisque l’appel de remove() avec un int utilise l’index, un index qui n’existe pas lancera une exception. Par exemple, liste.remove(100) lance une IndexOutOfBoundsException.

Conversion de List en tableau

Puisqu’un tableau peut être passé comme un vararg, le tableau 9.1 a couvert comment convertir un tableau en List. Vous devriez également savoir comment faire l’inverse. Commençons par transformer une List en tableau :

List<String> liste = new ArrayList<>();
liste.add("faucon");
liste.add("rouge-gorge");
Object[] tableauObjet = liste.toArray();
String[] tableauString = liste.toArray(new String[0]);
liste.clear();
System.out.println(tableauObjet.length); // 2
System.out.println(tableauString.length); // 2

La ligne 16 montre qu’une List sait comment se convertir en tableau. Le seul problème est qu’elle se convertit par défaut en un tableau de classe Object. Ce n’est généralement pas ce que vous voulez. La ligne 17 spécifie le type du tableau et fait ce que nous voulons. L’avantage de spécifier une taille de 0 pour le paramètre est que Java créera un nouveau tableau de la taille appropriée pour la valeur de retour. Si vous le souhaitez, vous pouvez suggérer un tableau plus grand à utiliser à la place. Si la List tient dans ce tableau, il sera retourné. Sinon, un nouveau tableau sera créé.

Notez également que la ligne 18 efface la List d’origine. Cela n’affecte aucun des deux tableaux. Le tableau est un objet nouvellement créé sans relation avec la List d’origine. C’est simplement une copie.