Je suis tombé sur un morceau de code qui me demande pourquoi il se compile avec succès:
public class Main {
public static void main(String[] args) {
String s = newList(); // why does this line compile?
System.out.println(s);
}
private static <T extends List<Integer>> T newList() {
return (T) new ArrayList<Integer>();
}
}
Ce qui est intéressant, c'est que si je modifie la signature de la méthode newList
avec <T extends ArrayList<Integer>>
ça ne marche plus.
Mettre à jour après les commentaires et réponses: Si je déplace le type générique de la méthode vers la classe, le code ne compile plus:
public class SomeClass<T extends List<Integer>> {
public void main(String[] args) {
String s = newList(); // this doesn't compile anymore
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
Si vous déclarez un paramètre de type à une méthode, vous autorisez l'appelant à choisir un type réel pour lui, tant que ce type réel remplira les contraintes. Ce type ne doit pas nécessairement être un type concret réel, il peut être un type abstrait, une variable de type ou un type d'intersection, en d'autres termes plus familiers, un type hypothétique. Ainsi, comme l'a dit Mureinik , il pourrait y avoir un type étendant String
et implémentant List
. Nous ne pouvons pas spécifier manuellement un type d'intersection pour l'appel, mais nous pouvons utiliser une variable de type pour illustrer la logique:
public class Main {
public static <X extends String&List<Integer>> void main(String[] args) {
String s = Main.<X>newList();
System.out.println(s);
}
private static <T extends List<Integer>> T newList() {
return (T) new ArrayList<Integer>();
}
}
Bien sûr, newList()
ne peut pas répondre à l'attente de retourner un tel type, mais c'est le problème de la définition (ou de l'implémentation) de cette méthode. Vous devriez recevoir un avertissement "non vérifié" lors de la conversion de ArrayList
en T
. La seule implémentation correcte possible serait de renvoyer null
ici, ce qui rend la méthode assez inutile.
Le point, pour répéter l'instruction initiale, est que l'appelant d'une méthode générique choisit les types réels pour les paramètres de type. En revanche, lorsque vous déclarez une classe générique comme avec
public class SomeClass<T extends List<Integer>> {
public void main(String[] args) {
String s = newList(); // this doesn't compile anymore
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
le paramètre type fait partie du contrat de la classe, donc celui qui crée une instance choisira les types réels pour cette instance. La méthode d'instance main
fait partie de cette classe et doit respecter ce contrat. Vous ne pouvez pas choisir le T
que vous voulez; le type réel de T
a été défini et en Java, vous ne pouvez généralement même pas savoir ce qu'est T
.
Le point clé de la programmation générique est d'écrire du code qui fonctionne indépendamment des types réels qui ont été choisis pour les paramètres de type.
Mais notez que vous pouvez créer une autre instance indépendante avec le type que vous souhaitez et invoquer la méthode, par exemple.
public class SomeClass<T extends List<Integer>> {
public <X extends String&List<Integer>> void main(String[] args) {
String s = new SomeClass<X>().newList();
System.out.println(s);
}
private T newList() {
return (T) new ArrayList<Integer>();
}
}
Ici, le créateur de la nouvelle instance sélectionne les types réels pour cette instance. Comme nous l'avons dit, ce type réel n'a pas besoin d'être un type concret.
Je suppose que c'est parce que List
est une interface. Si nous ignorons le fait que String
est final
pendant une seconde, vous pourriez, en théorie, avoir une classe qui extends String
(Ce qui signifie que vous pourriez l'affecter à s
) mais implements List<Integer>
(ce qui signifie qu'il pourrait être renvoyé par newList()
). Une fois que vous avez changé le type de retour d'une interface (T extends List
) En une classe concrète (T extends ArrayList
), Le compilateur peut déduire qu'ils ne sont pas assignables les uns des autres et génère une erreur.
Ceci, bien sûr, tombe en panne puisque String
est, en fait, final
, et nous pouvons nous attendre à ce que le compilateur en tienne compte. À mon humble avis, c'est un bogue, bien que je dois admettre que je ne suis pas un expert en compilation et il pourrait y avoir une bonne raison d'ignorer le modificateur final
à ce stade.
Je ne sais pas pourquoi cette compilation. D'un autre côté, je peux expliquer comment vous pouvez tirer pleinement parti des vérifications au moment de la compilation.
Ainsi, newList()
est une méthode générique, elle a un paramètre de type. Si vous spécifiez ce paramètre, le compilateur vérifiera cela pour vous:
Échec de la compilation:
String s = Main.<String>newList(); // this doesn't compile anymore
System.out.println(s);
Passe l'étape de compilation:
List<Integer> l = Main.<ArrayList<Integer>>newList(); // this compiles and works well
System.out.println(l);
Spécification du paramètre de type
Les paramètres de type fournissent uniquement une vérification au moment de la compilation. Ceci est voulu par la conception, Java utilise effacement de type pour les types génériques. Pour que le compilateur fonctionne pour vous, vous devez spécifier ces types dans le code.
Type de paramètre lors de la création de l'instance
Le cas le plus courant consiste à spécifier les modèles d'une instance d'objet. C'est à dire. pour les listes:
List<String> list = new ArrayList<>();
Ici, nous pouvons voir que List<String>
Spécifie le type des éléments de la liste. En revanche, la nouvelle ArrayList<>()
ne fonctionne pas. Il utilise à la place opérateur diamant . C'est à dire. le Java infère le type basé sur la déclaration.
Paramètre de type implicite à l'invocation de la méthode
Lorsque vous appelez une méthode statique, vous devez spécifier le type d'une autre manière. Parfois, vous pouvez le spécifier comme paramètre:
public static <T extends Number> T max(T n1, T n2) {
if (n1.doubleValue() < n2.doubleValue()) {
return n2;
}
return n1;
}
Vous pouvez l'utiliser comme ceci:
int max = max(3, 4); // implicit param type: Integer
Ou comme ça:
double max2 = max(3.0, 4.0); // implicit param type: Double
Paramètres de type explicites lors de l'appel de méthode:
Dites par exemple, voici comment vous pouvez créer une liste vide de type sécurisé:
List<Integer> noIntegers = Collections.<Integer>emptyList();
Le paramètre de type <Integer>
Est passé à la méthode emptyList()
. La seule contrainte est que vous devez également spécifier la classe. C'est à dire. tu ne peux pas faire ça:
import static Java.util.Collections.emptyList;
...
List<Integer> noIntegers = <Integer>emptyList(); // this won't compile
Jeton de type Runtime
Si aucune de ces astuces ne peut vous aider, vous pouvez spécifier un jeton de type à l'exécution . C'est à dire. vous fournissez une classe comme paramètre. Un exemple courant est le EnumMap :
private static enum Letters {A, B, C}; // dummy enum
...
public static void main(String[] args) {
Map<Letters, Integer> map = new EnumMap<>(Letters.class);
}