web-dev-qa-db-fra.com

Pourquoi ce code générique se compile-t-il en Java 8?

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>();
    }
}
43
Denis Rosca

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.

35
Holger

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.

20
Mureinik

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);
}
6
Tamas Rev