Les collections en Java permettent de gérer des ensembles d’objets.
Java propose par défaut une bibliothèque de classes permettant de couvrir l’essentiel des besoins d’un programmeur pour gérer des ensembles.
Pour commencer, nous allons nous limiter à la gestion de liste d'objets (List
/ArrayList
), mais nous verrons plus tard qu'il est possible d'utiliser d'autres types d'ensemble.
Nous allons également découvrir l’utilité des Generics dans l’usage des collections, ainsi que l'Autoboxing/Unboxing.
Un premier type de collection très utilisé est List
. Il permet de traiter un ensemble d'objets sous la forme de liste.
Par définition, une liste est ordonnée. Elle est définie par sa longueur, c'est à dire le nombre d'enregistrements qu'elle contient.
Le nombre d'objets distinct peut être différent du nombre d'enregistrement. Autrement dit, elle peut contenir des doublons.
java.util.List
est une interface qui propose les fonctions suivantes (non exhaustif):
public int size()
: détermine le nombre d'éléments dans la liste (et donc l'index maximal auquel on peut accéder)public E get(int index)
: récupère l'élément positionné à l'index donné en paramètrepublic boolean contains(Object o)
: indique si oui ou non l'objet passé en paramètre est contenu dans la liste.public int indexOf(Object o)
: indique l'index où se positionne l'objet passé en paramètre dans la liste.public void add(E e)
: ajoute un élément de typeE
dans la listepublic E remove(int index)
: supprime un élément de typeE
dans la liste et renvoie cet élémentpublic boolean remove(Object o)
: supprime la première occurrence trouvé de l'élément passé en paramètre de la liste
L'interface List
ne peut être instanciée.
java.util.ArrayList
est une classe (concrète) qui implémente cette interface et qui peut être instanciée.
Vous aurez remarqué le type de retour de get(int)
. E
est le type d'objet contenu par la liste. Nous allons voir à quoi cela correspond dans le paragraphe suivant.
Une List
, par essence, permet de stocker n'importe quel objet. Ce sont donc des instances d'Object
qui sont stockés dans une liste.
Il serait peu pratique de devoir spécifier à chaque utilisation des méthodes get(int)
, remove(int)
le type de l'objet retourné.
Dans les version antérieures à la version 1.5 de Java, il était nécessaire d'écrire ceci:
public class AvantLesGenerics{
List list = new ArrayList();
public void init(){
Date date = new Date(11, 12, 2013);
list.add(date);
}
public void testSiPremiereDateValide(){
Date d = (Date)list.get(0); //Transtypage indispensable
System.out.println(d.estValide());
}
}
En l'absence de transtypage, le compilateur indiquait une erreur, car la classe par défaut de List
est Object
et non Date
.
Heureusement, il est possible désormais de se passer de cette étape sans intérêt : grâce aux Generics
Juste après la déclaration du type (List
), il est possible d'indiquer le type d'objet qu'elle contient.
Pour cela, il suffit de noter le type entre chevrons <
et >
: List <Date>
. Cette déclaration indique que la liste contiendra des objets de type Date
.
Une fois le type déclaré pour l'interface List
, il n'est pas utile de le rappeler lors de l'instanciation.
List<Date> list = new ArrayList<>();
Vous noterez que ArrayList
est suivi d'un diamant (diamond operator): deux chevrons accolés <>
.
Ceci permet au compilateur de savoir que le type générique correspondra à celui de la variable telle que déclarée précédemment.
Exemple d'utilisation
public class AvecLesGenerics{
List<Date> list = new ArrayList<>();
public void init(){
Date date = new Date(11,12,2013);
list.add(date);
}
public void testSiPremiereDateValide(){
System.out.println(list.get(0).estValide());
}
}
On se rend bien compte qu'avec ceci, on dispose d'un outil puissant et versatile pour stocker des listes d'objets. La classe ListeForme
que nous avions développée pour gérer une liste de Forme
est, en comparaison, bien plus limitée (en fonctionnalités et surtout en réutilisabilité).
Pour aller plus loin avec les Generics
L'intérêt des collections est qu'elles permettent également de gérer des types primitifs.
En effet, même si ces types ne sont pas des instances, ils vont pouvoir être encapsulés dans des objets.
Ainsi, on aura la possibilité de stocker un int
dans un objet de type Integer
.
Observez bien le code suivant:
public class IntBoxing {
List<Integer> listeInt = new ArrayList<>();
public void init(){
int monNombrePrimitif = 24;
listeInt.add(monNombrePrimitif);
}
public void faire(){
System.out.println(listeInt.get(0) + 12);
}
public static void main(String[] args) {
IntBoxing ib = new IntBoxing();
ib.init();
ib.faire();
}
}
Il est parfaitement fonctionnel.
Il vous est possible d'utiliser les collections pour stocker les types primitifs suivants en utilisant les réceptacles associés :
Type primitif | Classe réceptacle |
---|---|
boolean | Boolean |
byte | Byte |
char | Character |
float | Float |
int | Integer |
long | Long |
short | Short |
double | Double |
Nous avons utilisé l'interface List
avec la classe ArrayList
. Mais il est possible de l'interchanger avec une autre classe sans effet sur le reste du code.
Par exemple avec la classe LinkedList
:
public class IntBoxing {
List<Integer> listeInt = new LinkedList<>();
public void init(){
int monNombrePrimitif = 24;
listeInt.add(monNombrePrimitif);
}
//... (le reste du code est identique à l'exemple précédent
La classe LinkedList
est une liste chaînée. Selon le cas, son mode de fonctionnement sera plus performant que si on utilise une ArrayList
.
Vous pouvez tester cela avec le code suivant:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class PerformancesList {
public static final int MAX_VALUE = 1000000;
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
public void init(List<Integer> liste){
for (int i = 0; i < MAX_VALUE; i++) {
liste.add(0, i);
}
}
public static void main(String[] args) {
PerformancesList pl = new PerformancesList();
long debutOp = System.nanoTime();
pl.init(pl.linkedList);
long finOp = System.nanoTime();
System.out.println("Linked :" + (finOp - debutOp)/1000000 + "ms");
debutOp = System.nanoTime();
pl.init(pl.linkedList);
finOp = System.nanoTime();
System.out.println("Array :" + (finOp - debutOp)/1000000 + "ms");
}
}
Il est possible de parcourir une liste de deux manières avec une boucle for
:
- la construction classique :
for(int i = 0 ; i < l.size() ; i++)
- la construction for-each :
for (Date date : listeDate)
Remplissez une liste avec des entiers numérotés de 1 à 10.
Utilisez les deux types de boucle for
pour parcourir une List
d'entiers (de 1 à 20).
Reprenez le code de l'observateur du chapitre précédent (Travailler avec les objets) et remplacez le tableau par une List
(en utilisant au choix ArrayList
ou LinkedList
).
Utilisez une boucle for-each
pour la notification des observateurs.
Reprenez le code des classes Date
et ValiditeDate
. Adaptez y un observateur (ObservateurDate
) en utilisant cette fois le type List
Nous allons créer un programme qui permet de gérer un carnet d'adresses.
Avant d'écrire ce programme, établissez la représentation UML sous forme de diagramme de classes.
Pour cela, nous allons définir une classe Personne
qui permette de stocker:
- un nom
- un prénom
- une liste d'adresses postales
- une liste de numéros de téléphone
- une liste d'adresses électroniques
Dans la liste d'adresses postale, chaque adresse pourra comporter les éléments suivants:
- numéro de rue
- nom de la voie
- complément d'adresse
- type de la voie
- code postal
- localité
Le carnet d'adresse pourra gérer plusieurs personnes. Pour cela, une liste.
Enfin, vous devrez pouvoir faire afficher dans la console (en implémentant un menu):
- soit la liste de toutes les personnes du carnet d'adresse
- soit les informations concernant une seule personne
- soit les informations concernant toutes les entrées du carnet
Une Map
permet de stocker des éléments en les référençant via une clef d'accès.
C'est l'équivalent d'un dictionnaire. Pour trouver la définition d'un mot, on cherche le mot en question. Une fois trouvé, il est possible d'en lire la définition.
L'utilisation des Map
permet de stocker et récupérer facilement une information, un pointeur ou un objet associé à un autre objet.
Une Map
va associer une clef - une instance d'un objet ayant un certain type (par exemple Date
) - avec une instance d'un objet d'un autre type : par exemple Personne
si l'on souhaite retrouver une personne par sa date d'anniversaire.
import java.util.Map;
import java.util.HashMap;
public class Anniversaire {
private Map<String, Personne> anniv = new HashMap<>();
public void init(){
anniv.put("19/05/1955", new Personne("Gosling", "James"));
anniv.put("09/09/1941", new Personne("Ritchie", "Dennis"));
anniv.put("04/02/1943", new Personne("Thompson", "Ken"));
}
public void affichePrenom(String date){
System.out.println(anniv.get(date).prenom);
}
public static void main(String[] args) {
Anniversaire a = new Anniversaire();
a.init();
a.affichePrenom("04/02/1943");
}
}
class Personne{
String nom;
String prenom;
Personne(String nom, String prenom){
this.nom = nom;
this.prenom = prenom;
}
}
On peut utiliser à la place de HashMap
un objet TreeMap
ainsi que d'autres classes (voir la Javadoc correspondante)
Les différences d'implémentation entre ces différentes classes ne seront pas discutées ici.
Reprendre l'exercice sur le carnet d'adresse.
- Ajouter une fonctionnalité permettant de récupérer un utilisateur à partir d'une adresse email.
- Créez une liste d'utilisateur pour chaque nom présent. Chaque liste contiendra tous les utilisateurs ayant le nom en question. Stockez chaque liste dans une Map, en prenant comme clef le nom.
Le diagramme suivant donne une vision globale des Collections en Java
Nous ne ferons que décrire ici l'interface Set
. Cette interface permet de contenir un ensemble d'objets. Cet ensemble ne contient pas de doublon (un objet y est stocké une et une seule fois).
Parmi les fonctionnalités communes aux collections, il y a la possibilité de parcourir la liste des éléments d'une collection via l'interface Iterator
.
Map
est ici un peu à part. Elle est liée aux collections dans le fait qu'elle possède deux méthodes qui renvoient des instances de l'interface Set
:
Set<K> keySet()
: ensemble des clefsSet<Map.Entry<K,V>> entrySet()
: ensemble des entrées
À partir de ces instances de Set
il est possible de récupérer un Iterator
pour en parcourir les éléments.
Voici un exemple d'utilisation d'un Iterator
. Ceci est applicable à toute sous-interface de Collection
:
public void parcourir(Collection collection) {
Iterator it = collection.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
Utilisez un Iterator pour afficher l'ensemble des éléments du carnet d'adresse plutôt qu'en utilisant une boucle sur une liste