43 votes

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

Je suis tombé sur un morceau de code qui me fait me demander 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 con <T extends ArrayList<Integer>> ça ne fonctionne plus.

Mise à jour après les commentaires et les 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>();
    }
}

35voto

Holger Points 13789

Si vous déclarez un paramètre de type dans une méthode, vous autorisez l'appelant à choisir un type réel pour ce paramètre, pour autant que ce type réel remplisse les contraintes. Ce type ne doit pas nécessairement être un type concret réel, il peut s'agir d'un type abstrait, d'un type variable ou d'un type d'intersection, en d'autres termes, plus familiers, d'un type hypothétique. Donc, comme dit par Mureinik il pourrait y avoir un type qui s'étend String et de mettre en œuvre List . Nous ne pouvons pas spécifier manuellement un type d'intersection pour l'invocation, mais nous pouvons utiliser une variable de type pour démontrer 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 obtenir un avertissement "unchecked" lors du casting ArrayList a T . La seule mise en œuvre correcte possible serait de renvoyer null ici, ce qui rend la méthode tout à fait inutile.

Le point, pour répéter la déclaration initiale, est que les 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 méthode générique classe 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 de 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 obéir à 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 pas savoir ce que le T est.

Le point essentiel de la programmation générique est d'écrire un code qui fonctionne indépendamment des types réels choisis pour les paramètres de type.

Mais notez que vous pouvez créer un autre Vous pouvez créer une instance indépendante avec le type de votre choix et invoquer la méthode, par ex.

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 choisit les types réels de cette instance. Comme indiqué, ce type réel ne doit pas nécessairement être un type concret.

20voto

Mureinik Points 61228

Je suppose que c'est parce que List est une interface. Si nous ignorons le fait que String es 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 peut être renvoyé par newList() ). Lorsque vous changez le type de retour d'une interface ( T extends List ) à une classe concrète ( T extends ArrayList ) le compilateur peut déduire qu'ils ne sont pas assignables l'un à l'autre, et produit une erreur.

Ceci, bien sûr, se casse la figure puisque String est, en fait, final et on peut s'attendre à ce que le compilateur en tienne compte. IMHO, c'est un bogue, bien que je doive admettre que je ne suis pas un expert en compilation et qu'il pourrait y avoir une bonne raison d'ignorer l'élément final à ce stade.

6voto

tamas rev Points 177

Je ne sais pas pourquoi ça compile. En revanche, je peux vous expliquer comment tirer pleinement parti des vérifications à la compilation.

Donc, newList() est une méthode générique, elle a un paramètre de type. Si vous spécifiez ce paramètre, le compilateur le vérifiera pour vous :

Ne parvient pas à compiler :

String s =  Main.<String>newList(); // this doesn't compile anymore
System.out.println(s);

Passe l'étape de la compilation :

List<Integer> l =  Main.<ArrayList<Integer>>newList(); // this compiles and works well
System.out.println(l);

Spécifier le paramètretype

Les paramètres de type ne permettent qu'une vérification au moment de la compilation. C'est à dessein, java utilise effacement de type pour les types génériques. Pour que le compilateur travaille pour vous, vous devez spécifier ces types dans le code.

Paramètre de type lors de la création de l'instance

Le cas le plus courant est de spécifier les modèles pour une instance d'objet. Par exemple, pour des listes :

List<String> list = new ArrayList<>();

Ici, nous pouvons voir que List<String> spécifie le type des éléments de la liste. D'autre part, la nouvelle ArrayList<>() ne le fait pas. Il utilise le opérateur de diamant à la place. C'est-à-dire le compilateur java déduit le type basé sur la déclaration.

Paramètre de type implicite à l'invocation de la méthode

Lorsque vous invoquez une méthode statique, vous devez alors spécifier le type d'une autre manière. Parfois, vous pouvez le spécifier en tant que 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'invocation de la méthode :

Par exemple, voici comment vous pouvez créer une liste vide sécurisée par un type :

List<Integer> noIntegers = Collections.<Integer>emptyList();

Le paramètre de type <Integer> est transmis à la méthode emptyList() . La seule contrainte est que vous devez également spécifier la classe. C'est-à-dire que vous ne pouvez pas faire cela :

import static java.util.Collections.emptyList;
...
List<Integer> noIntegers = <Integer>emptyList(); // this won't compile

Jeton de type d'exécution

Si aucune de ces astuces ne peut vous aider, vous pouvez alors spécifier un jeton de type d'exécution . C'est-à-dire que vous fournissez une classe en tant que 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);
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X