Questions d'entrevue Java Generics (+ réponses)

Cet article fait partie d'une série: • Questions d'entretien sur les collections Java

• Questions d’entretien sur Java Type System

• Questions d’entretien sur Java Concurrency (+ réponses)

• Questions d'entretien sur la structure des classes Java et l'initialisation

• Questions d'entrevue Java 8 (+ réponses)

• Questions d’entrevue sur la gestion de la mémoire dans Java (+ réponses)

• Questions d'entrevue Java Generics (+ réponses) (article actuel) • Questions d'entrevue Java Flow Control (+ réponses)

• Questions d’entretien sur les exceptions Java (+ réponses)

• Questions d'entrevue sur les annotations Java (+ réponses)

• Principales questions d'entrevue Spring Framework

1. Introduction

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

Les génériques sont un concept de base en Java, introduit pour la première fois dans Java 5. Pour cette raison, presque toutes les bases de code Java les utiliseront, garantissant presque 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. Questions

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

Type est le nom d'une classe ou d'une interface . Comme l'indique le nom, un paramètre de type générique est lorsqu'un type peut être utilisé comme paramètre dans une déclaration de classe, de méthode ou d'interface.

Commençons par un exemple simple, sans générique, pour le démontrer:

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

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

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

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

Lorsque nous implémentons notre consommateur, nous pouvons fournir le type que nous voulons qu'il consomme comme argument. Il s'agit d'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 remplacer ce type par tout ce dont nous avons besoin.

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

Un avantage de l'utilisation de génériques est d'éviter les moulages et d'assurer la sécurité du type. Ceci est particulièrement utile lorsque vous travaillez avec des collections. Démontrons ceci:

List list = new ArrayList(); list.add("foo"); Object o = list.get(0); 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 qu'il s'agit d'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 une chaîne, mais nous devons convertir notre objet en un pour résoudre le problème explicitement. Cela produit beaucoup de bruit et de passe-partout.

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

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

Dans ce cas, nous obtiendrions une ClassCastException au moment de l'exécution, car un Integer ne peut pas être converti en String.

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(0); // No cast Integer foo = list.get(0); // Compilation error

Comme nous pouvons le voir, en utilisant des génériques, nous avons une vérification de type de compilation qui empêche ClassCastExceptions et supprime le besoin de transtypage.

L'autre avantage est d'éviter la duplication de code . Sans génériques, nous devons copier et coller le même code mais pour des types différents. Avec les génériques, nous n'avons pas à faire cela. 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 réaliser que les informations de type générique ne sont disponibles que pour le compilateur, pas pour la JVM. En d'autres termes , l'effacement de type signifie que les informations de type générique ne sont pas disponibles pour la machine virtuelle Java au moment de l'exécution, uniquement au moment de la compilation .

Le raisonnement derrière le choix d'implémentation majeur est simple: préserver la compatibilité descendante avec les anciennes versions 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:

  1. Remplacer les types génériques par des objets
  2. Remplacez les types bornés (plus d'informations à ce sujet dans une question ultérieure) par la première classe liée
  3. Insérez l'équivalent de casts 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. Une fois de plus, les informations de type générique ne sont pas disponibles au moment de l'exécution.

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 à partir de la première question, 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. C'est parce que nous perdons le contrôle supplémentaire à la compilation que nous obtenons en utilisant des génériques.

Le point à retenir est que si la rétrocompatibilité et l'effacement de type permettent d'omettre des types génériques, c'est une mauvaise pratique.

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

Une méthode générique est l'endroit où un paramètre de type est introduit dans une méthode, vivant dans le cadre de cette méthode. 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 le souhaitions. En tirant parti de 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 lorsque nous le faisons.

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 retourne T, alors 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'y a pas besoin d'un cast, ni de passer un argument de type générique. Le type d'argument déduit uniquement le type de retour.

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

Jusqu'à présent, toutes nos questions ont couvert des arguments de types génériques qui sont illimités. Cela signifie que nos arguments de type générique peuvent être de n'importe quel type que nous voulons.

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çons T à être une sous-classe d'animal . On pourrait alors avoir une cage de chats:

Cage catCage;

Mais nous ne pourrions pas avoir une cage d'objets, car 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 de l'animal sont disponibles pour le compilateur. Nous savons que notre type l'étend, nous pourrions donc écrire un algorithme générique qui opère 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 plus si nous le souhaitons:

public abstract class Cage

Dans notre exemple, l'animal est une classe et comparable est 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 de comparable, alors le code ne serait pas compilé. Il convient également de se rappeler que si l'une des bornes supérieures est une classe, elle doit être le premier argument.

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

Un type générique représente un type inconnu . Il a explosé avec un point d'interrogation comme suit:

public static void consumeListOfWildcardType(List list)

Ici, nous spécifions une liste qui pourrait être de n'importe quel type . 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?

Un caractère générique à limite supérieure se produit lorsqu'un type générique hérite d'un type concret . Ceci est particulièrement utile lorsque vous travaillez avec des collections et l'héritage.

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

public class Farm { private List animals; public void addAnimals(Collection newAnimals) { animals.addAll(newAnimals); } }

Si nous avions plusieurs sous-classes d'animaux , comme le chat et le chien , nous pourrions supposer à tort que nous pouvons tous les ajouter à notre ferme:

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

C'est parce que le compilateur attend une collection du type concret animal , pas une sous-classe.

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

public void addAnimals(Collection newAnimals)

Maintenant, si nous réessayons, notre code se compilera. C'est parce que nous disons maintenant au compilateur d'accepter une collection de n'importe quel sous-type d'animal.

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'objet. En effet, un caractère générique peut être de 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). Démontrons cela avec un exemple:

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

Encore une fois, la raison pour laquelle la deuxième ligne ne se compile pas est qu'une liste d'objets est requise, pas une liste de chaînes. La première ligne se compile 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, un caractère générique à limite inférieure signifie que nous forçons le type à être une superclasse de notre type borné . Essayons ceci avec un exemple:

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

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

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

Cela a du sens, car un objet est une superclasse d'animaux. Si nous n'utilisions pas le caractère générique de limite inférieure, le code ne se compilerait pas, car une liste d'objets n'est pas une liste d'animaux.

Si nous y réfléchissons, nous ne pourrions pas ajouter un chien à une liste d'une sous-classe d'animaux, comme les chats ou même les chiens. Seulement une superclasse d'animaux. Par exemple, cela ne compilerait pas:

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

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

Lorsqu'il s'agit de collections, une règle courante pour sélectionner entre les caractères génériques de limite supérieure ou inférieure est PECS. PECS signifie producteur étend, consommateur super.

Cela peut être facilement démontré grâce à l'utilisation de certaines interfaces et classes Java standard.

Producer extend signifie simplement que si vous créez un producteur de type générique, utilisez le mot - clé extend. 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 appeler makeNoise () 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 des étendues , nous ne pourrions pas passer des listes de chats , de chiens ou de toute autre sous-classe d'animaux. En appliquant le principe du producteur étend, nous avons le plus de flexibilité possible.

Le super consommateur signifie le contraire du producteur. Tout ce que cela signifie, c'est que si nous avons affaire à quelque chose qui consomme des éléments, 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 ne faisons qu'ajouter à 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 passer dans une liste de n'importe quelle superclasse d'animaux, mais pas une sous-classe. Par exemple, si nous essayions de transmettre une liste de chiens ou de chats, le code ne se compilerait pas.

La dernière chose à considérer est ce qu'il faut faire si une collection est à la fois un consommateur et un producteur. Un exemple de ceci pourrait être une collection dans laquelle des éléments sont à la fois ajoutés et supprimés. Dans ce cas, un caractère générique illimité 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 une situation où un type générique est disponible au moment de l'exécution. C'est quand 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 quelque peu fragile. Par exemple, cela dépend du paramètre de type défini sur la superclasse immédiate. Mais cela démontre que la machine virtuelle Java dispose de ces informations de type.

Suivant » Questions d'entretien sur le contrôle de flux Java (+ réponses) « Précédent Questions d'entretien sur la gestion de la mémoire dans Java (+ réponses)