Algorithmique avancée
les collections Java
Plan
● Les 4 types principaux.
● Implémentations fournies par l'API.
● Parcours d’une collection.
● « Magouilles » Java
Les 4 types principaux
● A partir de la v5 java : refonte complète des classes proposant des tableaux/listes.
● résultat : ensemble « cohérent » de classe appelé Collections.
● cohérence :
– méthodes communes, par ex. size(),
– règles de nommage, par ex, get()
● MAIS ... pas si cohérent que ça à cause des différences fonctionnelles entre les types proposés.
Les 4 types principaux
● 4 types :
– Set : ensemble d'objets non indicé, non ordonné, sans doublon.
– List : ensemble d'objets indicé (donc ordonné au sens des indices), potentiellement avec doublon.
– Map : ensemble associatif, un objet clé référence un objet valeur. Pas de clé en doublon mais plusieurs clés peuvent référencer le même objet valeur.
– Queue : FIFO (file) / LIFO (pile).
Les 4 types principaux
● Dans l'API, Set, List, Map, Queue = classes interface.
● ne définissent que les prototypes des méthodes manipulant ces types de collection.
=> pas instanciable.
● l'API propose plusieurs implémentations de chaque interface.
● En pratique : une seule implémentation vraiment utile par interface =
HashSet, ArrayList, HashMap, ArrayDeque.
Les implémentations
● Ces 4 classes proposent les traitements classiques :
– accès lecture/modification.
– insertion/suppression.
– taille, vidage, …
● Si l'application a un schéma d'accès particulier (beaucoup insertion/suppression, insertion ordonnée, …) : d'autres implémentations peuvent être plus performantes.
Les implémentations : généricité
● Avant la v5 de Java : pas de généricité => on crée des listes d'Object.
● La classe Object étant à la base de toutes les autres, on peut créer une liste où l'on met n'importe quel type
d'instance dedans.
● pour récupérer & manipuler l'objet : transtypage et possibilité d'erreur à l'exécution.
Date da = new Date();
Integer i = new Integer(56);
Vector v = new Vector();
v.add(d);
v.add(i);
Object o = v.elementAt(0);
Date d2 = (Date)o; // OK
Integer j = (Integer)o; // Erreur transtypage A L'EXECUTION
Les implémentations : généricité
● A partir de la v5 de Java : possibilité d'écrire des classes générique => les types de certains attributs sont
déterminés par le programmeur dans son code.
● Les classes de collections utilisent la généricité :
– un seul type d'objet dans une collection.
– pas d'erreur de transtypage à l'exécution
ArrayList<String> list = null; // déclaration ...
list = new ArrayList<>(); // instanciation list.add("Salut");
list.add(new Date()); // erreur COMPILATION String s = list.get(0);
Integer i = list.get(0); // erreur COMPILATION
Les implémentations : généricité
Remarques :
● Souvent déclaration/définition en utilisant le polymorphisme.
● Type générique = forcément nom de classe.
● Pas nécessaire de remettre le type générique lors de l’instanciation.
List<String> list = null; // déclaration avec la classe interface
list = new ArrayList<String>(); // instanciation de l’implémentation de l'interface
Set<int> set = new HashSet<int>(); // erreur compilation car type = int Set<Integer> set = new HashSet<Integer>(); // OK
Set<Integer> set = new HashSet<>(); // OK, compilo connait déjà le type
Les implémentations : méthodes communes
● Quelle que soit la classe, il y a les méthodes :
– int size() : pour obtenir le nombre d'éléments stockés dans la collection.
– boolean isEmpty() : pour savoir si la collection est vide.
– void clear() : pour vider la collection.
Les implémentations : HashSet
● HashSet = ensemble non indicé sans doublon.
– boolean add(E e) : ajoute e dans la collection, renvoie false si e existe déjà, true sinon.
Rq : si E n'est pas le nom de classe utilisé lors de la déclaration => erreur COMPILATION
– boolean remove(Object o): supprime o s'il existe, sinon renvoie false.
Rq : si o n'est pas de la classe utilisée lors de la déclaration, pas d'erreur mais renvoie false.
– boolean contains(Object o) : renvoie true si o existe dans la collection, false sinon.
Les implémentations : HashSet
● Exemple d’utilisation HashSet :
public class A { ... }
public class B extends A { ... } public class Prog {
...
Set<A> set = new HashSet<>(); // instanciation A a = new A(...);
B b = new B(...);
Date d = new Date();
set.add(a); // OK
set.add(b); // OK car B sous-classe de A : polymorphisme set.add(d); // erreur compilation
boolean rep;
rep = set.remove(a); // OK, renvoie true rep = set.contains(d); // OK, renvoie false rep = set.remove(d); // OK, renvoie false ...
Les implémentations : ArrayList
● ArrayList = ensemble indicé
– boolean add(E e) : insère e en fin de liste
– void add(int idx, E e) : insère e en idx. Si idx non valide, lève une exception (idem pour les autres méthodes avec idx)
– E remove(int idx): supprime et renvoie l'élément en idx
– E get(int idx) : renvoie l'élément en idx.
– int indexOf(Object o) : renvoie l'indice de o s'il existe (-1 sinon).
Les implémentations : ArrayList
● Exemple d’utilisation ArrayList :
List<A> list = new ArrayList<>();
A a1 = new A(...);
A a2 = new A(...);
Date d = new Date();
list.add(a1); // OK list.add(a2); // OK
list.add(d); // erreur compilation -> mauvais type
list.add(15,a1); // erreur exécution : index en dehors de la liste A aa = null;
aa = list.get(0); // OK et aa référence le même objet que a1 aa = list.remove(1); // OK et aa référence le même objet que a2 int pos = list.indexOf(a1); // OK et renvoie 0
pos = list.indexOf(d); // OK et renvoie -1
Les implémentations : HashMap
● HashMap = tableau associatif clé/valeur
– V put(K k, V v) : associe la clé k à la valeur v. Si k était déjà associée à une valeur, renvoie celle-ci, sinon null.
– V get(Object k) : renvoie la valeur associé à l'objet k, s'il existe en tant que clé, sinon renvoie null.
– V remove(Object k): supprime k et la valeur
associé, si k existe en tant que clé, sinon renvoie null.
– boolean containsKey(Object k)
– boolean containsValue(Object v)
Les implémentations : HashMap
● Exemple d’utilisation HashMap :
Map<String,A> map1 = new HashMap<>();
Map<A,Integer> map2 = new HashMap<>();
A a1 = new A(...);
A a2 = new A(...);
Date d = new Date();
map1.put("toto",a1); // OK, renvoie null
map1.put("toto",a2); // OK, renvoie valeur précédente (i.e. a1)
map1.put(a1,"tutu"); // erreur compilation : mauvais types cle/valeur map2.put(a1, new Integer(10); // OK, renvoie val. prec. (i.e. tutu) map2.put(d, new Integer(5)); // erreur compilation : mauvais type clé map2.containsKey(d); // OK, renvoie false : clé inexistante
maps.containsKey(a1); // OK, renvoie true
map2.remove(d); // OK mais ne fait rien : clé inexistante.
map1.remove("toto"); // OK, renvoie a2
map1.remove(a1); // OK, renvoie l'objet Integer.
Les implémentations : ArrayDeque
● ArrayDeque = FIFO ou LIFO
Rq : pas de différence d'instanciation, seulement les méthodes utilisées.
● mode file (FIFO) :
– boolean offer(E e) : ajoute e en fin de file.
– E poll() : renvoie l'objet en tête de file.
● mode pile (LIFO) :
– void push(E e): ajoute au sommet de la pile.
– E pop(): renvoie l'objet au sommet de la pile.
Rq : offer() et push() ont le même effet.
Les implémentations : ArrayDeque
● Exemple d’utilisation ArrayDeque :
Queue<Double> q = new ArrayDeque<>();
// FIFO access q.offer(1);
q.offer(2);
int val = q.poll(); // renvoie 1 // LIFO access
q.push(3);
val = q.pop(); // renvoie 3 val = q.pop(); // renvoie 2
Parcours d’une collection
● deux procédés :
– boucle for :
● syntaxe spéciale.
● uniquement si aucune insertion/suppression dans la collection.
– objet Iterator :
● création d’un objet servant d’itérateur,
● possibilité d’insertion/suppression,
● selon type, retour en arrière possible.
Parcours d’une collection : for
● Exemple avec for :
Set<String> set = new HashSet<>();
List<Date> list = new ArrayList<>();
Map<Integer,String> map = new HashMap<>();
for(String s : set) {
... // faire qqchose avec s }
for(Date d : list) {
... // faire qqchose avec d }
for(Map.Entry<Integer, String> e : map.entrySet() ) { Integer key = e.getKey();
String value = e.getValue();
... // faire qqchose avec key et value }
Parcours d’une collection : itérateur
● Exemple avec itérateur :
Set<String> set = new HashSet<>();
List<Date> list = new ArrayList<>();
Map<Integer,String> map = new HashMap<>();
Iterator<String> is = set.iterator();
while (is.hasNext()) { String s = is.next();
...
if (...) is.remove();
...
}
ListIterator il = list.listIterator();
while (il.hasNext()) { Date d = il.next();
...
// possibilités : il.previous(), il.add(E e), il.remove(), ...
}
Parcours d’une collection : itérateur
● Exemple avec itérateur (suite):
Iterator<Integer> im = map.keySet().iterator();
while(im.hasNext()) {
Integer key = im.next();
String val = map.get(key);
...
}
« Magouilles » Java
● généricité limitée aux objets = gênant.
● contournement : (dés)encapsulation automatique des
types primaires (char, int, double, …) dans des instances des classes équivalentes (Char, Integer, Double, …).
● magouille du compilateur, car brise les principes
fondamentaux de vérification du typage des variables.
● Danger : bizarreries avec les tests du type == et !=.
Map<Integer,Double> map = HashMap<>();
map.put(5, 1.234); // OK : encapsulation automatique de int et double double val = map.get(5); // val contient 1.234 : désencapsulation
// RQ : principe applicable partout dans le code, e.g Double d = 6.666; // OK : encapsulation automatique
if (d == 6.666) { ... } ; // OK aussi grâce à désencapsulation
« Magouilles » Java
● Danger : bizarreries avec les tests == et !=
Integer i1 = 10; // encapsulation Integer i2 = 10; // encapsulation
Integer i3 = new Integer(10); // instanciation classique if (i1 == i2) S.o.p("egal 1"); // affiche egal 1
if (i1 == i3) S.o.p("egal 2"); // n’affiche rien ! Integer i4 = 1000; // encapsulation
Integer i5 = 1000; // encapsulation
if (i4 == i5) S.o.p("egal 3"); // n’affiche rien, argh !!!!!!!
● Conclusion : ne pas faire de tests entre objets qui ont pour origine une donnée encapsulée.