4. Implémentation des génériques

4.1. Type erasure

Comment les génériques fonctionnent-ils en interne ? Les amateurs de C++ et de templates sont toujours très déçus quand ils l'apprennent : les génériques ne sont pas présents dans le byte code généré. En fait, l'implémentation des génériques passe par la suppression de la référence à T dans le byte code . Toutes les références à T sont remplacées par des références à Object lorsque ce type n'est pas connu. Lorsqu'il est connu, il est remplacé par son vrai type, et des casts sont insérés dans le code afin de préserver le vrai type. Lorsque l'on se retrouve dans une situation où deux méthodes doivent exister avec la même signature, mais des types de retour différents (ce qui est interdit en Java, mais qui peut arriver du fait du type erasure ), des méthodes pont sont ajoutées.

4.2. Types génériques et casts

Le type erasure entraine un certain nombre de cas pathologiques. Considérons la classe Holder suivante.

Exemple 27. Classe Holder

public  class Holder<T> {

    private T t ;

    public Holder(T t) {
       this.t = t ;
   }

    public T get() {
       return t ;
   }
}

Les deux lignes de code suivantes sont équivalentes. Effectivement, à l'exécution du byte code , la référence à Integer n'existe plus.

Exemple 28.  Type erasure et cast - 1

boolean b1 = holder  instanceof Holder<Integer> ;  // Integer n’est pas pris en compte
 boolean b2 = holder  instanceof Holder ;           // équivalent à b2

Sur cet exemple suivant, un warning est généré à la compilation sur la première ligne. Il signale à l'utilisateur que le type Integer ne sera pas présent dans le byte code , et que le cast ne se fera pas sur le type générique, mais plutôt sur Holder. De fait, c'est la deuxième ligne de code qui sera exécutée.

Exemple 29.  Type erasure et cast - 2

Holder<Integer> h1 = (Holder<Integer>)object ;  // warning à la compilation
Holder<Integer> h2 = (Holder)object ;           // h1 équivalent à h2

Enfin, si l'on compare des classes génériques, des surprises peuvent aussi apparaître, comme sur ce dernier exemple.

Exemple 30.  Type erasure et cast - 3

Holder<Integer> h1 = ... ;                    // un premier holder
Holder<String> h2 = ... ;                     // un second holder
 boolean b = h1.getClass() == h2.getClass() ;  // b est vrai

Là encore, la raison en est que les types String et Integer ont été retirés du byte code . On se retrouve donc bien avec deux fois la même classe.

4.3. Type générique et exception

Signalons enfin qu'on ne peut pas utiliser un type générique dans un catch.

Exemple 31. Utilisation illégale d'un type générique dans un catch

try {
   ...
}  catch (T exception) {  // illégal ici
    throw t ;             // légal là
}

4.4. Construction d'une instance générique

Construire une instance générique est un point délicat. Effectivement, les deux appels suivants sont illégaux, car, encore une fois, le type T ne se trouve pas dans le byte code .

Exemple 32. Constructions génériques illégales

public  class Holder<T> {

    private T t ;

    public Holder(T t) {
       this.t =  new T(t) ;               // appel à ce constructeur illégal
       this.t = T.class.newInstance() ;  // appel à T.class illégal
   }
}

Si l'on veut construire une nouvelle instance d'un type T, alors il faut utiliser le pattern suivant.

Exemple 33. Construction générique correcte

public  static <T> T newInstance(Class<T> clazz) {

    return clazz.newInstance() ;  // gérer les exceptions InstantiationException et
                                 // IllegalAccessException
}

4.5. Génériques et membres statiques

Il est illégal de référencer un type générique déclaré au niveau de la classe dans ses membres statiques. L'erreur provient de ce que le type générique est défini au niveau des instances, et ne peut être référencé par des membres statiques. Voyons un exemple de deux déclarations illégales.

Exemple 34. Membres statiques génériques illégaux

public  class Singleton<T> {

    private  static T t ;  // déclaration illégale

    public  static T newInstance(T t) {  // déclaration illégale
      ...
   }
}

4.6. Collisions de méthodes génériques

Une méthode générique ne doit pas surcharger une méthode existante une fois le type erasure appliqué. Dans l'exemple suivant, la méthode equals(T) devient une méthode equals(Object) définie au niveau de la classe Object. Cette déclaration est donc illégale.

Exemple 35. Méthode générique illégale

public  class Holder<T> {

    public  boolean equals(T t) {  // déclaration illégale car collision après
                                 // application du type erasure
      ... ;
   }
}

4.7. Implémentation de plusieurs types identiques

Enfin, il n'est pas légal d'implémenter plusieurs interfaces génériques qui ne varient que par leur déclaration de type. Voyons ceci sur un exemple.

Exemple 36. Implémentation générique illégale

// classe illégale
 public  class ClassIllegale  implements Comparable<Integer>, Comparable<String> {
   ...
}

Java API avancées
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
API Collection
1. Introduction
2. Interface Collection
2.1. Notion de Collection
2.2. Détail des méthodes disponibles
2.3. Interface Iterator
2.4. Implémentation, exemples d'utilisation
3. Interface List
3.1. Notion de List
3.2. Détail des méthodes disponibles
3.3. Interface ListIterator
3.4. Implémentations, exemples d'utilisation
4. Interface Set
4.1. Notion de Set
4.2. Implémentations HashSet et LinkedHashSet
4.3. Exemples d'utilisation
5. Interface SortedSet
5.1. Notion de SortedSet
5.2. Détails des méthodes disponibles
5.3. Exemples d'utilisation
6. Interface NavigableSet
6.1. Notion de NavigableSet
6.2. Détails des méthodes disponibles
6.3. Exemple d'utilisation
7. Interfaces Queue et Deque
7.1. Notion de file d'attente
7.2. Détail des méthodes disponibles
7.3. Utilisation des interfaces Queue et Deque
8. Tables de hachage
8.1. Notion de table de hachage
8.2. Interface Map
8.3. Interface Map.Entry
8.4. Interface SortedMap
8.5. Interface NavigableMap
8.6. Implémentations
8.7. Exemples d'utilisation
9. Classes utilitaires Collections et Arrays
9.1. Introduction
9.2. Classe Arrays
9.3. Classe Collections
Génériques
1. Introduction
2. Un premier exemple
2.1. Une première classe générique
2.2. Une première méthode générique
3. Contraindre un type générique
3.1. Problème posé
3.2. Contraindre un type générique
4. Implémentation des génériques
4.1. Type erasure
4.2. Types génériques et casts
4.3. Type générique et exception
4.4. Construction d'une instance générique
4.5. Génériques et membres statiques
4.6. Collisions de méthodes génériques
4.7. Implémentation de plusieurs types identiques
5. Type <?>
5.1. Introduction
5.2. Type ? extension d'un type
5.3. Type ? super-type d'un type
Expressions régulières
1. Introduction
2. Mise en œuvre des expressions régulières
2.1. Fonctionnement d'une regexp
2.2. Fonctionnement de l'API en Java
2.3. Un premier exemple
2.4. Classe Pattern
2.5. Classe Matcher
2.6. Utilisation des méthode find() et group()
2.7. Méthodes de remplacement
2.8. Sélection de régions
3. Syntaxe des expressions régulières
3.1. Notion de classe
3.2. Étude d'un cas réel
3.3. Recherche d'un mot précis
3.4. Recherche de deux mots précis
3.5. Recherche d'un mot commençant par une lettre donnée
3.6. Cas de mots comportant des caractères accentués
3.7. Recherche sur les lignes
Introspection
1. Introduction
2. La classe Class
2.1. Utilisation de Class
2.2. Méthodes disponibles
2.3. Remarque sur la propriété Accessible
2.4. Type d'une classe
2.5. Création d'une instance à partir d'un objet Class
2.6. Cas des énumérations
3. Les classes Method et Constructor
3.1. Utilisation de Method
3.2. Utilisation de Constructor
3.3. Méthodes disponibles
3.4. Invocation d'une méthode par introspection
4. La classe Field
4.1. Utilisation de Field
4.2. Méthodes disponibles
4.3. Accès à un champ par introspection
5. La classe Modifier
Programmation concurrente
1. Introduction
2. Lançons nos premiers threads
2.1. Introduction
2.2. Un premier thread, extension de Thread
2.3. Un deuxième thread, implémentation de Runnable
2.4. Remarque sur la méthode Thread.sleep(long)
2.5. Arrêter un thread
3. Concurrence d'accès
3.1. Notion d'état
3.2. Exemple de concurrence d'accès sur un état
3.3. Analyse de la concurrence d'accès
3.4. Solution au problème
3.5. Champs volatile
4. Synchronisation
4.1. Définition d'un bloc synchronisé
4.2. Fonctionnement d'un bloc synchronisé
4.3. Notion de deadlock
4.4. Bonnes pratiques pour la synchronisation de threads
5. Opérations atomiques
5.1. Atomicité d'une opération
5.2. Solutions disponibles
5.3. Variables atomiques
6. Collections synchronisées et concurrentes
6.1. Introduction
6.2. Position du problème
6.3. Solutions proposées
7. Files d'attente
7.1. Introduction, pattern producteur / consommateur
7.2. Interface BlockingQueue<E>
7.3. Implémentations de BlockingQueue
7.4. Exemple de producteur / consommateur
7.5. Arrêter un producteur / consommateur : pilule empoisonnée
8. Classes utilitaires de l'API Concurrent
8.1. Introduction
8.2. Énumération TimeUnit
8.3. Interface Callable<V>
8.4. Interfaces Future<V> et RunnableFuture<V>
8.5. Interface ScheduledFuture<V> et RunnableScheduledFuture<V>
9. Pattern executor
9.1. Notion de réserve de threads
9.2. Interface Executor
9.3. Interface ExecutorService
9.4. Interface ScheduledExecutorService
9.5. Classe Executors
9.6. Pattern de lancement de tâches
10. Classes de contrôle d'accès
10.1. Introduction
10.2. Interfaces Lock et ReadWriteLock
10.3. Notion de verrou réentrant
10.4. Classe RentrantLock
10.5. Classe ReadWriteRentrantLock
11. Sémaphores, barrières et latches
11.1. Introduction
11.2. Notion de sémaphore, classe Semaphore
11.3. Notion de latch, classe CountDownLatch
11.4. Notion de barrière, classe CyclicBarrier