Questions posées à Java Generics (Réponses)

Questions d'entretiens chez Java Generics (+ réponses)

1. introduction

Dans cet article, nous allons passer en revue quelques exemples de questions et réponses d'interview sur les génériques Java.

Les génériques sont un concept fondamental en Java, introduit pour la première fois dans Java 5. De ce fait, presque toutes les bases de code Java les utilisent, ce qui garantit quasiment qu’un développeur les rencontrera à un moment donné. C’est pourquoi il est essentiel de les comprendre correctement, et c’est pourquoi ils sont plus que susceptibles d’être interrogés lors d’un entretien.

2. Des questions

Q1. Qu'est-ce qu'un paramètre de type générique?

Type est le nom d'unclass ouinterface. Comme l'indique le nom,a generic type parameter is when a type can be used as a parameter in a class, method or interface declaration.

Commençons par un exemple simple, sans générique, pour illustrer ceci:

public interface Consumer {
    public void consume(String parameter)
}

Dans ce cas, le type de paramètre de méthode de la méthodeconsume() estString. Il n'est ni paramétré ni configurable.

Remplaçons maintenant notre typeString par un type générique que nous appelleronsT. Il est nommé comme ceci par convention:

public interface Consumer {
    public void consume(T parameter)
}

Lorsque nous implémentons notre consommateur, nous pouvons fournir lestype que nous voulons qu'il consomme comme argument. Ceci est un paramètre de type générique:

public class IntegerConsumer implements Consumer {
    public void consume(Integer parameter)
}

Dans ce cas, nous pouvons maintenant consommer des entiers. Nous pouvons échanger cestype contre tout ce dont nous avons besoin.

Q2. Quels sont les avantages de l'utilisation des types génériques?

L'un des avantages de l'utilisation de génériques est la réduction des coûts et la sécurité du type. Ceci est particulièrement utile lorsque vous travaillez avec des collections. Voyons ceci:

List list = new ArrayList();
list.add("foo");
Object o = list.get(1);
String foo = (String) o;

Dans notre exemple, le type d'élément de notre liste est inconnu du compilateur. Cela signifie que la seule chose qui peut être garantie est que c'est un objet. Ainsi, lorsque nous récupérons notre élément, un objet est ce que nous récupérons. En tant qu'auteurs du code, nous savons que c'est unString, mais nous devons convertir notre objet en un pour résoudre le problème explicitement. Cela produit beaucoup de bruit et passe-partout.

Ensuite, si nous commençons à penser à la marge d'erreur manuelle possible, le problème de casting s'aggrave. Et si nous avions accidentellement un entier dans notre liste?

list.add(1)
Object o = list.get(1);
String foo = (String) o;

Dans ce cas, nous obtiendrions unClassCastException à l'exécution, car unInteger ne peut pas être converti enString.

Maintenant, essayons de nous répéter, cette fois en utilisant des génériques:

List list = new ArrayList<>();
list.add("foo");
String o = list.get(1);    // No cast
Integer foo = list.get(1); // Compilation error

Comme nous pouvons le voir,by using generics we have a compile type check which prevents ClassCastExceptions and removes the need for casting.

The other advantage is to avoid code duplication. Sans génériques, nous devons copier et coller le même code mais pour différents types. Avec les génériques, nous n’avons pas à le faire. Nous pouvons même implémenter des algorithmes qui s'appliquent aux types génériques.

Q3. Qu'est-ce que l'effacement de type?

Il est important de comprendre que les informations de type générique ne sont disponibles que pour le compilateur, pas pour la JVM. En d'autres termes, type erasure means that generic type information is not available to the JVM at runtime, only compile time.

Le choix de l'implémentation principale est simple: préserver la compatibilité avec les versions antérieures de Java. Lorsqu'un code générique est compilé en bytecode, ce sera comme si le type générique n'avait jamais existé. Cela signifie que la compilation va:

  1. Remplacer les types génériques par des objets

  2. Remplacer les types liés (plus de détails dans une question ultérieure) par la première classe liée

  3. Insérez l'équivalent d'incantations lors de la récupération d'objets génériques.

Il est important de comprendre l’effacement des caractères. Sinon, un développeur pourrait être confus et penser qu'il serait en mesure d'obtenir le type au moment de l'exécution:

public foo(Consumer consumer) {
   Type type = consumer.getGenericTypeParameter()
}

L'exemple ci-dessus est un pseudo-code équivalent à ce à quoi les choses pourraient ressembler sans effacement de type, mais malheureusement, c'est impossible. Encore une fois,the generic type information is not available at runtime.

Q4. Si un type générique est omis lors de l'instanciation d'un objet, le code sera-t-il toujours compilé?

Comme les génériques n'existaient pas avant Java 5, il est possible de ne pas les utiliser du tout. Par exemple, les génériques ont été adaptés à la plupart des classes Java standard telles que les collections. Si nous regardons notre liste de la question un, alors nous verrons que nous avons déjà un exemple d'omission du type générique:

List list = new ArrayList();

Malgré la capacité de compiler, il est toujours probable qu'il y aura un avertissement du compilateur. En effet, nous perdons la vérification supplémentaire liée à l'utilisation des génériques au moment de la compilation.

Le point à retenir est quewhile backward compatibility and type erasure make it possible to omit generic types, it is bad practice.

Q5. Quelle est la différence entre une méthode générique et un type générique?

A generic method is where a type parameter is introduced to a method,living within the scope of that method. Essayons ceci avec un exemple:

public static  T returnType(T argument) {
    return argument;
}

Nous avons utilisé une méthode statique, mais nous aurions pu également utiliser une méthode non statique si nous l’avions souhaité. En exploitant l'inférence de type (traitée dans la question suivante), nous pouvons l'invoquer comme n'importe quelle méthode ordinaire, sans avoir à spécifier d'arguments de type.

Q6. Qu'est-ce que l'inférence de type?

L'inférence de type est le moment où le compilateur peut examiner le type d'un argument de méthode pour déduire un type générique. Par exemple, si nous avons passéT à une méthode qui retourneT,, le compilateur peut déterminer le type de retour. Essayons ceci en invoquant notre méthode générique à partir de la question précédente:

Integer inferredInteger = returnType(1);
String inferredString = returnType("String");

Comme nous pouvons le voir, il n’ya pas besoin de cast, ni de passer un argument de type générique. Le type d'argument n'infère que le type de retour.

Q7. Qu'est-ce qu'un paramètre de type borné?

Jusqu'à présent, toutes nos questions ont porté sur des arguments de types génériques non bornés. Cela signifie que nos arguments de type génériques peuvent être n'importe quel type souhaité.

Lorsque nous utilisons des paramètres bornés, nous limitons les types qui peuvent être utilisés comme arguments de type générique.

À titre d'exemple, disons que nous voulons forcer notre type générique à toujours être une sous-classe d'animal:

public abstract class Cage {
    abstract void addAnimal(T animal)
}

En utilisant extend, nous forçonsT à être une sous-classe de l'animal. Nous pourrions alors avoir une cage de chats:

Cage catCage;

Mais nous ne pourrions pas avoir une cage d'objets, puisqu'un objet n'est pas une sous-classe d'un animal:

Cage objectCage; // Compilation error


Un avantage de ceci est que toutes les méthodes d'animal sont disponibles pour le compilateur. Nous savons que notre type l'étend, nous pourrions donc écrire un algorithme générique qui fonctionne sur n'importe quel animal. Cela signifie que nous n'avons pas à reproduire notre méthode pour différentes sous-classes d'animaux:

public void firstAnimalJump() {
    T animal = animals.get(0);
    animal.jump();
}

Q8. Est-il possible de déclarer un paramètre de type multiple borné?

Déclarer plusieurs limites pour nos types génériques est possible. Dans notre exemple précédent, nous avons spécifié une seule borne, mais nous pourrions également en spécifier davantage si nous le souhaitons:

public abstract class Cage

Dans notre exemple, l'animal est une classe et comparable, une interface. Maintenant, notre type doit respecter ces deux limites supérieures. Si notre type était une sous-classe d’animal mais n’implémentait pas la comparaison, le code ne serait pas compilé. It’s also worth remembering that if one of the upper bounds is a class, it must be the first argument.

Q9. Qu'est-ce qu'un type générique?

A wildcard type represents an unknown type. Il a explosé avec un point d'interrogation comme suit:

public static consumeListOfWildcardType(List list)

Ici, nous spécifions une liste qui pourrait être de n'importe queltype. Nous pourrions passer une liste de n'importe quoi dans cette méthode.

Q10. Qu'est-ce qu'un caractère générique à limite supérieure?

An upper bounded wildcard is when a wildcard type inherits from a concrete type. Ceci est particulièrement utile lorsque vous travaillez avec des collections et des héritages.

Essayons de le démontrer avec une classe de ferme qui stockera les animaux, d'abord sans le type générique:

public class Farm {
  private List animals;

  public void addAnimals(Collection newAnimals) {
    animals.addAll(newAnimals);
  }
}

Si nous avions plusieurs sous-classes d'animaux, comme chat et chien,, nous pourrions faire l'hypothèse incorrecte que nous pouvons tous les ajouter à notre ferme:

farm.addAnimals(cats); // Compilation error
farm.addAnimals(dogs); // Compilation error

Cela est dû au fait que le compilateur attend une collection du type concret animal, et non une sous-classe.

Maintenant, introduisons un caractère générique avec limite supérieure dans notre méthode d'ajout d'animaux:

public void addAnimals(Collection newAnimals)

Maintenant, si nous réessayons, notre code sera compilé. En effet, nous demandons maintenant au compilateur d’accepter une collection de tous les sous-types d’animaux.

Q11. Qu'est-ce qu'un caractère générique illimité?

Un caractère générique illimité est un caractère générique sans limite supérieure ou inférieure, qui peut représenter n'importe quel type.

Il est également important de savoir que le type générique n’est pas synonyme d’object. En effet, un caractère générique peut être n'importe quel type, alors qu'un type d'objet est spécifiquement un objet (et ne peut pas être une sous-classe d'un objet). Voyons cela avec un exemple:

List wildcardList = new ArrayList();
List objectList = new ArrayList(); // Compilation error


Encore une fois, la deuxième ligne ne compile pas parce qu'une liste d'objets est requise, pas une liste de chaînes. La première ligne est compilée car une liste de tout type inconnu est acceptable.

Q12. Qu'est-ce qu'un caractère générique à limite inférieure?

Un caractère générique de limite inférieure est lorsque, au lieu de fournir une limite supérieure, nous fournissons une limite inférieure en utilisant le mot clésuper. En d'autres termes,a lower bounded wildcard means we are forcing the type to be a superclass of our bounded type. Essayons ceci avec un exemple:

public static void addDogs(List list) {
   list.add(new Dog("tom"))
}

En utilisantsuper,, nous pourrions appeler addDogs sur une liste d'objets:

ArrayList objects = new ArrayList<>();
addDogs(objects);


Cela a du sens, car un objet est une super-classe d’animaux. Si nous n'utilisions pas le caractère générique de limite inférieure, le code ne serait pas compilé, car une liste d'objets n'est pas une liste d'animaux.

Si nous y réfléchissons bien, nous ne pourrions pas ajouter de chien à une liste de sous-classes d’animaux, tels que les chats ou même les chiens. Seulement une superclasse d'animal. Par exemple, cela ne compilerait pas:

ArrayList objects = new ArrayList<>();
addDogs(objects);

Q13. Quand choisiriez-vous d'utiliser un type de limite inférieure, par rapport à un type borné supérieur?

Lorsqu’il s’agit de collections, PECS est une règle courante pour choisir entre des caractères génériques de limite supérieure ou inférieure. PECS signifieproducer extends, consumer super.

Cela peut être facilement démontré par l’utilisation de certaines interfaces et classes Java standard.

Producer extends signifie simplement que si vous créez un producteur de type générique, utilisez le mot-cléextends. Essayons d'appliquer ce principe à une collection, pour voir pourquoi cela a du sens:

public static void makeLotsOfNoise(List animals) {
    animals.forEach(Animal::makeNoise);
}

Ici, nous voulons appelermakeNoise() sur chaque animal de notre collection. Cela signifie que notre collection est un producteur, car tout ce que nous faisons avec elle est de la ramener des animaux pour que nous puissions effectuer notre opération. Si nous nous débarrassions deextends, nous ne pourrions pas passer dans les listes de chats, dogs ou d’autres sous-classes d’animaux. En appliquant le principe des extensions de producteur, nous avons le plus de flexibilité possible.

Consumer super signifie le contraire deproducer extends. Tout ce que cela signifie, c'est que si nous avons affaire à quelque chose qui consomme des éléments, alors nous devrions utiliser le mot clésuper. Nous pouvons le démontrer en répétant notre exemple précédent:

public static void addCats(List animals) {
    animals.add(new Cat());
}

Nous ajoutons seulement à notre liste d'animaux, donc notre liste d'animaux est un consommateur. C'est pourquoi nous utilisons le mot clésuper. Cela signifie que nous pourrions transmettre une liste de toutes les super-classes d’animaux, mais pas une sous-classe. Par exemple, si nous essayions de passer une liste de chiens ou de chats, le code ne serait pas compilé.

La dernière chose à considérer est de savoir quoi faire si une collection est à la fois un consommateur et un producteur. Un exemple de ceci pourrait être une collection où des éléments sont à la fois ajoutés et supprimés. Dans ce cas, un caractère générique non limité doit être utilisé.

Q14. Existe-t-il des situations dans lesquelles des informations de type générique sont disponibles au moment de l'exécution?

Il existe un cas où un type générique est disponible au moment de l'exécution. C'est à ce moment qu'un type générique fait partie de la signature de classe, comme ceci:

public class CatCage implements Cage

En utilisant la réflexion, nous obtenons ce paramètre de type:

(Class) ((ParameterizedType) getClass()
  .getGenericSuperclass()).getActualTypeArguments()[0];

Ce code est un peu fragile. Par exemple, cela dépend du paramètre de type défini sur la superclasse immédiate. Mais, cela montre que la machine virtuelle Java a ce type d’information.