Collections et généricité en Java - Jean-Claude MARTIN
←
→
Transcription du contenu de la page
Si votre navigateur ne rend pas la page correctement, lisez s'il vous plaît le contenu de la page ci-dessous
Collections et généricité en Java Jean-Claude MARTIN Généricité Exemple La généricité permet de rendre un programme plus stable en détectant certaines erreurs dès la compilation. La généricité est très utilisée avec les collections mais peut être aussi utilisée sans les collections. Exemple : une boite dans laquelle on peut mettre un objet public class Box { private Object object; public void add(Object object) { this.object = object; } public Object get() { return object; } } Que faire si on veut restreindre l’ajout à certains objets (par exemple des instances de Integer) ? Un commentaire n’est pas exploitable par le compilateur : public class BoxDemo1 { public static void main(String[] args) { // ONLY place Integer objects into this box! Box integerBox = new Box(); integerBox.add(new Integer(10)); Integer someInteger = (Integer)integerBox.get(); System.out.println(someInteger); } } Le cast est correct mais risque de provoquer une erreur si on ajoute autre chose que des instances de Integer : public class BoxDemo2 { public static void main(String[] args) { // ONLY place Integer objects into this box! Box integerBox = new Box(); // Imagine this is one part of a large application // modified by one programmer. integerBox.add("10"); // note how the type is now String // ... and this is another, perhaps written // by a different programmer Integer someInteger = (Integer)integerBox.get(); System.out.println(someInteger); } } 1
Exécution : ClassCastException: Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer at BoxDemo2.main(BoxDemo2.java:6) Si la classe Box avait été conçue avec la généricité, cette erreur aurait été détectée à la compilation. LA GENERICITE PERMET D’EVITER DE REMPLACER CERTAINES ERREURS D’EXECUTION PAR DES ERREURS DE COMPILATION PLUS FACILEMENT CORRIGEABLES. Types génériques On peut déclarer une classe générique comme suit (c’est utilisable aussi pour définir des interfaces génériques). /** * Generic version of the Box class. */ public class Box { private T t; // T stands for "Type" public void add(T t) { this.t = t; } public T get() { return t; } } T est une variable qui peut représenter une classe ou une interface. Ce ne peut pas être un type de base. C’est un paramètre formel de type. Chaque occurrence de Object a été remplacée par T. Lors de l’utilisation de cette classe, il faut faire une invocation de type générique dans laquelle on remplace T par une classe ou interface : La variable T déclarée dans l’entete peut etre utilisée n’importe où dans la classe. Box integerBox; Comme toute déclaration, cela ne crée pas d’objet. Cette déclaration indique juste que intergerBox contiendra une référence à une boite contenant des objets instances de Integer. On parle de “type paramétré”. 2
Pour instancier la classe : integerBox = new Box(); On peut ensuite appeler des méthodes sans faire de cast : public class BoxDemo3 { public static void main(String[] args) { Box integerBox = new Box(); integerBox.add(new Integer(10)); Integer someInteger = integerBox.get(); // no cast! System.out.println(someInteger); } } De plus, si on essaye de mettre dans box une instance d’une autre classe : BoxDemo3.java:5: add(java.lang.Integer) in Box cannot be applied to (java.lang.String) integerBox.add("10"); ^ 1 error Attention : “T” ne fait pas partie du nom de la classe elle-même : seul un fichier Box.class est généré. Un type générique peut avoir plusieurs paramètres de type mais chaque paramètre ne peut apparaitre qu’une seule fois dans cette déclaration Box => erreur Box => permis Generic Methods and Constructors Type parameters can also be declared within method and constructor signatures to create generic methods and generic constructors. This is similar to declaring a generic type, but the type parameter's scope is limited to the method or constructor in which it's declared. public class Box { private T t; public void add(T t) { this.t = t; } public T get() { return t; } public void inspect(U u){ System.out.println("T: " + t.getClass().getName()); System.out.println("U: " + u.getClass().getName()); } public static void main(String[] args) { Box integerBox = new Box(); integerBox.add(new Integer(10)); 3
integerBox.inspect("some text"); } } Here we've added one generic method, named inspect, that defines one type parameter, named U. This method accepts an object and prints its type to standard output. For comparison, it also prints out the type of T. The output from this program is: T: java.lang.Integer U: java.lang.String Bounded Type Parameters There may be times when you'll want to restrict the kinds of types that are allowed to be passed to a type parameter. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. To declare a bounded type parameter, list the type parameter's name, followed by the extends keyword, followed by its upper bound, which in this example is Number. Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces). public class Box { private T t; public void add(T t) { this.t = t; } public T get() { return t; } public void inspect(U u){ System.out.println("T: " + t.getClass().getName()); System.out.println("U: " + u.getClass().getName()); } public static void main(String[] args) { Box integerBox = new Box(); integerBox.add(new Integer(10)); integerBox.inspect("some text"); // error: this is still String! } } By modifying our generic method to include this bounded type parameter, compilation will now fail, since our invocation of inspect still includes a String: Box.java:21: inspect(U) in Box cannot be applied to (java.lang.String) integerBox.inspect("10"); ^ 1 error To specify additional interfaces that must be implemented, use the & character, as in: 4
Subtyping As you already know, it's possible to assign an object of one type to an object of another type provided that the types are compatible. For example, you can assign an Integer to an Object, since Object is one of Integer's supertypes: Object someObject = new Object(); Integer someInteger = new Integer(10); someObject = someInteger; // OK In object-oriented terminology, this is called an "is a" relationship. Since an Integer is a kind of Object, the assignment is allowed. But Integer is also a kind of Number, so the following code is valid as well: public void someMethod(Number n){ // method body omitted } someMethod(new Integer(10)); // OK someMethod(new Double(10.1)); // OK The same is also true with generics. You can perform a generic type invocation, passing Number as its type argument, and any subsequent invocation of add will be allowed if the argument is compatible with Number: Box box = new Box(); box.add(new Integer(10)); // OK box.add(new Double(10.1)); // OK Now consider the following method: public void boxTest(Box n){ // method body omitted } What type of argument does it accept? By looking at its signature, we can see that it accepts a single argument whose type is Box. But what exactly does that mean? Are you allowed to pass in Box or Box, as you might expect? Surprisingly, the answer is "no", because Box and Box are not subtypes of Box. Understanding why becomes much easier if you think of tangible objects — things you can actually picture — such as a cage: // A cage is a collection of things, with bars to keep them in. interface Cage extends Collection; A lion is a kind of animal, so Lion would be a subtype of Animal: interface Lion extends Animal {} Lion king = ...; Where we need some animal, we're free to provide a lion: Animal a = king; A lion can of course be put into a lion cage: Cage lionCage = ...; lionCage.add(king); and a butterfly into a butterfly cage: 5
interface Butterfly extends Animal {} Butterfly monarch = ...; Cage butterflyCage = ...; butterflyCage.add(monarch); But what about an "animal cage"? English is ambiguous, so to be precise let's assume we're talking about an "all-animal cage": Cage animalCage = ...; This is a cage designed to hold all kinds of animals, mixed together. It must have bars strong enough to hold in the lions, and spaced closely enough to hold in the butterflies. Such a cage might not even be feasible to build, but if it is, then: animalCage.add(king); animalCage.add(monarch); Since a lion is a kind of animal (Lion is a subtype of Animal), the question then becomes, "Is a lion cage a kind of animal cage? Is Cage a subtype of Cage?". By the above definition of animal cage, the answer must be "no". This is surprising! But it makes perfect sense when you think about it: A lion cage cannot be assumed to keep in butterflies, and a butterfly cage cannot be assumed to hold in lions. Therefore, neither cage can be considered an "all-animal" cage: animalCage = lionCage; // compile-time error animalCage = butterflyCage; // compile-time error Without generics, the animals could be placed into the wrong kinds of cages, where it would be possible for them to escape. Wildcards The phrase "animal cage" can reasonably mean "all-animal cage", but it also suggests an entirely different concept: a cage designed not for any kind of animal, but rather for some kind of animal whose type is unknown. In generics, an unknown type is represented by the wildcard character "?". To specify a cage capable of holding some kind of animal: Cage
someCage = lionCage; // OK someCage = butterflyCage; // OK So now the question becomes, "Can you add butterflies and lions directly to someCage?". As you can probably guess, the answer to this question is "no". someCage.add(king); // compiler-time error someCage.add(monarch); // compiler-time error If someCage is a butterfly cage, it would hold butterflies just fine, but the lions would be able to break free. If it's a lion cage, then all would be well with the lions, but the butterflies would fly away. So if you can't put anything at all into someCage, is it useless? No, because you can still read its contents: void feedAnimals(Cage
Summary This chapter described the following problem: We have a Box class, written to be generally useful so it deals with Objects. We need an instance that takes only Integers. The comments say that only Integers go in, so the programmer knows this (or should know it), but the compiler doesn't know it. This means that the compiler can't catch someone erroneously adding a String. When we read the value and cast it to an Integer we'll get an exception: Debugging may be difficult, as the point in the code where the exception is thrown may be far removed from the point in the code where the error is located. It's always better to catch bugs when compiling than when running. Specifically, you learned that generic type declarations can include one or more type parameters; you supply one type argument for each type parameter when you use the generic type. You also learned that type parameters can be used to define generic methods and constructors. Bounded type parameters limit the kinds of types that can be passed into a type parameter; they can specify an upper bound only. Wildcards represent unknown types, and they can specify an upper or lower bound. During compilation, type erasure removes all generic information from a generic class or interface, leaving behind only its raw type. Questions 1. Consider the following classes: public class AnimalHouse { private E animal; public void setAnimal(E x) { animal = x; } public E getAnimal() { return animal; } } public class Animal{ } public class Cat extends Animal { } public class Dog extends Animal { } For the following code snippets, identify whether the code: 1. fails to compile, 2. compiles with a warning, 3. generates an error at runtime, or 4. none of the above (compiles and runs without problem.) a. AnimalHouse house = new AnimalHouse(); 8
b. AnimalHouse house = new AnimalHouse(); c. AnimalHouse house = new AnimalHouse(); house.setAnimal(new Cat()); d. AnimalHouse house = new AnimalHouse(); house.setAnimal(new Dog()); Exercises Design a class that acts as a library for the following kinds of media: book, video, and newspaper. Provide one version of the class that uses generics and one that does not. Feel free to use any additional APIs for storing and retrieving the media. Collections : définitions Structures de données C'est l'organisation efficace d'un ensemble de données, sous la forme de tableaux, de listes, de piles etc. Cette efficacité réside dans la quantité mémoire utilisée pour stocker les données, et le temps nécessaire pour réaliser des opérations sur ces données. Collection Une collection est un objet qui regroupe plusieurs objets. Une collection permet de stocker, récupérer, manipuler et communiquer un ensemble de données. Exemple Un ensemble d’instance de la classe Contact dont chaque objet est représenté par un nom, une adresse, un numéro de téléphone. Framework “Collection” A collections framework is a unified architecture for representing and manipulating collections. All collections frameworks contain the following: Interfaces: These are abstract data types that represent collections. Interfaces allow collections to be manipulated independently of the details of their representation. Implementations: These are the concrete implementations of the collection interfaces. Algorithms: These are the methods that perform useful computations, such as searching and sorting, on objects that implement collection interfaces. The algorithms are said to be polymorphic: that is, the same method can be used on many different implementations of the appropriate collection interface. Les interfaces du framework Collection 9
Collection: un groupe d'objets où la duplication peut-être autorisée. Set: est ensemble ne contenant que des valeurs et ces valeurs ne sont pas dupliquées. Par exemple l'ensemble A = {1,2,4,8}. Set hérite donc de Collection, mais n'autorise pas la duplication. SortedSet est un Set trié. List: hérite aussi de collection, mais autorise la duplication. Dans cette interface, un système d'indexation a été introduit pour permettre l'accès (rapide) aux éléments de la liste. Map: est un groupe de paires contenant une clé et une valeur associée à cette clé. Cette interface n'hérite ni de Set ni de Collection. La raison est que Collection traite des données simples alors que Map des données composées (clé,valeur). SortedMap est un Map trié. Les implémentations des interfaces Description des interfaces L’interface Collection public interface Collection { // Basic Operations int size(); boolean isEmpty(); boolean contains(Object element); boolean add(Object element); // Optional boolean remove(Object element); // Optional Iterator iterator(); int hashCode(); boolean equals(Object element); // “Bulk Operations” => traite plusieurs objets boolean containsAll(Collection c); boolean addAll(Collection c); // Optional boolean removeAll(Collection c); // Optional boolean retainAll(Collection c); // Optional void clear(); // Optional 10
// Array Operations Object[] toArray(); Object[] toArray(Object a[]); } Les interfaces contiennent des méthodes optionnelles. Cette approche permet de traiter les collections particulières sans que nous soyons dans l'obligation de définir les méthodes optionnelles. Ces méthodes optionnelles sont définies qu'en cas de besoin. Un Set non modifiable n'a pas besoin de redéfinir la méthode add, puisque nous ne pouvons pas le modifier! Il y a des opérations réalisées sur un seul objet ou bien sur une collection (un ensemble d'objets). add (remove) permet d'ajouter (resp. de retirer) un élément. Quand à addAll (removeAll) permet d'ajouter (resp. de retirer même si les éléments sont dupliqués dans la collection originale) une collection. contains (containsAll) permet de vérifier si un objet (resp. les éléments d'une collection) est présent dans la collection. size, isEmpty et clear, permettent respectivement de donner la taille de la collection, de vérifier si la collection est vide et finalement d'effacer le contenu de la collection. retainsAll se comporte comme le résultat de l'intersection de deux ensembles. Si A={1,2,5,8} et B={3,8} alors A = {8}. equals permet de tester si deux objets sont égaux. hashCode retourne le code de hachage calculé pour la collection. toArray retourne les éléments de la collection sous le format d'un tableau. toArray(Object a[]) permet de préciser le type du tableau à retourner. Si le tableau est grand les éléments sont rangés dans ce tableau, sinon un nouveau tableau est crée pour recevoir les éléments de la collection. L’interface Iterator C'est l'outil utilisé pour parcourir une collection. public interface Iterator { boolean hasNext(); Object next(); void remove(); // Optional } hasNext permet de vérifier s'il y a un élément qui suit. next permet de pointer l'élément suivant. remove permet de retirer l'élément courant. Exemple : Collection collection = … ; // new d’une classe qui //implémente Collection Iterator iterator = collection.iterator(); while (iterator.hasNext()) { Object element = iterator.next(); if (aSupprimer(element)) { iterator.remove(); } } 11
Enfin, les collections vues comme des ensembles réalisent les 3 opérations mathématiques sur des ensembles: union : add et addAll intersection : retainAll différence : remove et removeAll L’interface Set C'est une interface identique à celle de Collection. Les éléments ne peuvent pas être duppliqués. Deux implémentations possibles: TreeSet: les éléments sont rangés de manière ascendante. HashSet: les éléments sont rangés suivant une méthode de hachage. import java.util.*; public class SetExample { public static void main(String args[]) { Set set = new HashSet(); // Une table de Hachage set.add("Bernadine"); set.add("Elizabeth"); set.add("Gene"); set.add("Elizabeth"); set.add("Clara"); System.out.println(set); Set sortedSet = new TreeSet(set); // Un Set trié System.out.println(sortedSet); } } EXECUTION : [Gene, Clara, Bernadine, Elizabeth] [Bernadine, Clara, Elizabeth, Gene] L’interface List Liste est une collection ordonnée. Elle permet la duplication des éléments. L'interface est renforcée par des méthodes permettant d'ajouter ou de retirer des éléments se trouvant à une position donnée. Elle permet aussi de travailler sur des sous-listes. On utilise le plus souvent des ArrayList sauf s'il y a insertion d'élément(s) au milieu de la liste. Dans ce cas il est préférable d'utiliser une LinkedList pour éviter ainsi les décalages. public interface List extends Collection { // Positional Access Object get(int index); Object set(int index, Object element); // Optional void add(int index, Object element); // Optional Object remove(int index); // Optional boolean addAll(int index, Collection c); // Optional // Search int indexOf(Object o); int lastIndexOf(Object o); // Iteration ListIterator listIterator(); ListIterator listIterator(int index); 12
// Range-view List subList(int fromIndex, int toIndex); } Les méthodes de l'interface List permettent d'agir sur un élément se trouvant à un index donné ou bien un ensemble d'éléments à partir d'un index donné dans la liste. get (remove) retourne (resp. retirer) l'élément se trouvant à la position index. set (add & addAll) modifie (resp. ajouter) l'élément (resp. un seul ou une collection) se trouvant à la position index. indexOf (lastIndexOf) recherche si un objet se trouve dans la liste et retourner son (resp. son dernier) index. subList permet de créer un sous liste d'une liste. Pour parcourir une liste, il a été défini un itérateur spécialement pour la liste. public interface ListIterator extends Iterator { boolean hasNext(); Object next(); boolean hasPrevious(); Object previous(); int nextIndex(); int previousIndex(); void remove(); // Optional void set(Object o); // Optional void add(Object o); // Optional } permet donc de parcourir la liste dans les deux directions et de modifier un élément (set) ou d'ajouter un nouveau élément. List list = ...; ListIterator iterator = list.listIterator(list.size()); while (iterator.hasPrevious()) { Object element = iterator.previous(); // traitement d'un élément } hasNext permet de vérifier s'il y a un élément qui suit. next permet de pointer l'élément courant. nextIndex retourne l'index de l'élément courant. Pour les sous listes, elles sont extraites des listes de fromIndex (inclus) à toIndex (non inclus). Tout changement sur les sous listes affecte la liste de base, et l'inverse provoque un état indéfini s'il y a accès à la sous liste. import java.util.*; public class ListExample { public static void main(String args[]) { List list = new ArrayList(); list.add("Bernadine"); list.add("Elizabeth"); list.add("Gene"); 13
list.add("Elizabeth"); list.add("Clara"); System.out.println(list); System.out.println("2: " + list.get(2)); System.out.println("0: " + list.get(0)); LinkedList queue = new LinkedList(); queue.addFirst("Bernadine"); queue.addFirst("Elizabeth"); queue.addFirst("Gene"); queue.addFirst("Elizabeth"); queue.addFirst("Clara"); System.out.println(queue); queue.removeLast(); queue.removeLast(); System.out.println(queue); } } EXECUTION Bernadine, Elizabeth, Gene, Elizabeth, Clara] 2: Gene 0: Bernadine [Clara, Elizabeth, Gene, Elizabeth, Bernadine] [Clara, Elizabeth, Gene] L’interface Map C'est un ensemble de paires, contenant une clé et une valeur. Deux clés ne peuvent être égales au sens de equals. L'interface interne Entry permet de manipuler les éléments d'une paire comme suit: public interface Entry { Object getKey(); Object getValue(); Object setValue(Object value); } getKey & getValue retournent respectivement la clé et la valeur associée à cette clé. setValue permet de modifier une valeur d'une paire. Remarque: faire attention de ne pas modifier directement la valeur associée à une clé. Pour le faire, retirer l'ancienne clé (et donc sa valeur aussi) et ajouter une nouvelle clé (avec cette nouvelle valeur). public interface Map { // Basic Operations Object put(Object key, Object value); Object get(Object key); Object remove(Object key); boolean containsKey(Object key); boolean containsValue(Object value); int size(); boolean isEmpty(); // Bulk Operations void putAll(Map t); void clear(); // Collection Views public Set keySet(); public Collection values(); public Set entrySet(); 14
// Interface for entrySet elements public interface Entry { Object getKey(); Object getValue(); Object setValue(Object value); } } values retourne les valeurs sous la forme d’une Collection. keySet et entrySet retournent, respectivement, un ensemble (Set) de clés et un ensemble (set) d'Entry. Ceci permet donc d'itérer sur les Map comme suit: si m est un HashMap alors: // sur les clés for (Iterator i = m.keySet().iterator();i.hasNext();) System.out.println(i.next()); // sur les valeurs for (Iterator i = m.values().iterator();i.hasNext();) System.out.println(i.next()); // sur la paire clé/valeur for (Iterator i = m.keySet().iterator();i.hasNext();){ Map.Entry e = (Map.Entry) i.next(); System.out.println(e.getKey() + " ; " + e.getValue()); } import java.util.*; public class MapExample { public static void main(String args[]) { Map map = new HashMap(); Integer ONE = new Integer(1); for (int i=0, n=args.length; i
Trier: sort(List list) ; trie une liste. sort(List list,Comparator comp) ; trie une liste en utilisant un comparateur. Mélanger: shuffle(List liste) ; mélange les éléments de manière aléatoire. Manipuler: reverse(List liste) ; inverse les éléments de la liste. fill (List liste, Object element) ; initialise les éléments de la liste avec element. copy(List dest, List src) ; copy une liste src dans une liste dest. Rechercher: binarySearch(List list, Object element) ; une recherche binaire d'un élément. binarySearch(List list, Object element, Comparator comp) ; une recherche binaire d'un élément en utilisant un comparateur. Effectuer des recherches extrêmes: min (Collection) max (Collection) L’interface Comparable Un algorithme extrêmement rapide et stable (les éléments équivalents ne sont pas réordonnés) est utilisé pour trier la liste en utilisant l'ordre naturel du type. Le tri ne peut avoir lieu que si les classes implantent la méthode Comparable, ce qui n'est toujours pas le cas2. Cette classe contient une seule méthode compareTo: interface Comparable { int compareTo(Object obj); } Cette méthode retourne: entier positif si l'objet qui fait l'appel est plus grand que obj, zéro s'ils sont identiques, négatif si l'objet qui fait l'appel est plus petit que obj. Dans le cas d'une classe qui n'implante pas la classe Comparable, ou bien vous voulez spécifier un autre ordre, vous devez implanter l'interface Comparator. Cette dernière permet de comparer deux éléments de la collection. Pour trier, nous passons une instance de cette classe à la méthode Sort(). interface Comparator { int compare(Object o1, Object o2); boolean equals(Object object); 16
} class CaseInsensitiveComparator implements Comparator { public int compare(Object element1,Object element2) { String lowerE1 = ( (String)element1).toLowerCase(); String lowerE2 = ( (String)element2).toLowerCase(); return lowerE1.compareTo(lowerE2); } } Généricité et collections Toutes les interfaces collection sont génériques. L’interface Collection est déclarée ainsi : public interface Collection... La syntaxe indique que l’interface est générique. Quand on déclare une instance de Collection, il est fortement conseillé de spécifier le type d’objet qui sera contenu dans la collection. Cela permet au compilateur de vérifier que le type d’objet ajouté est correct et permet ainsi de diminuer les erreurs d’exécution. Bibliographie http://www.iro.umontreal.ca/~lokbani/cours/ift1176/communs/Cours/PDF/Collections. pdf http://java.sun.com/docs/books/tutorial/collections/index.html http://java.sun.com/docs/books/tutorial/java/generics/index.html 17
Vous pouvez aussi lire