34 votes

Différents types de valeurs de retour dans l'implémentation de méthodes génériques

Aujourd'hui, je suis tombé sur un code Java fonctionnel que je n'aurais même pas pensé compiler. Réduit à son strict minimum, il ressemble à ceci :

import java.util.List;

interface A {
     List foo();
}

interface B {
     List foo();
}

class C implements A, B {
    @Override
    public List foo()
    {
        return null;
    }
}

À première vue, le paramètre de type des méthodes foo dans A et B semble inutile car T n'est pas utilisé ailleurs. Quoi qu'il en soit, j'ai découvert que cela joue un rôle crucial en permettant aux types de valeur de retour conflictuels de coexister dans la même implémentation : si un ou les deux sont omis, le code ne compile pas. Voici la version qui ne fonctionne pas :

import java.util.List;

interface A {
    List foo();
}

interface B {
    List foo();
}

class C implements A, B {
    @Override
    public List foo()
    {
        return null;
    }
}

Je n'ai pas besoin de corriger les extraits de code ci-dessus car ce ne sont que des exemples que j'ai inventés pour expliquer mon point de vue. Je suis juste curieux de comprendre pourquoi le compilateur se comporte différemment avec eux. Quelqu'un peut-il expliquer quelles règles précises font la différence ici?

21voto

Paul Bellora Points 26524

Alors que le premier exemple compile, il va générer un avertissement de conversion non vérifiée :

// Sécurité du type : le type de retour List pour foo() à partir du type C nécessite
// une conversion non vérifiée pour se conformer à List
public List foo()
{
    return null;
}

Ce qui se passe ici est que en déclarant des paramètres de type, A.foo() et B.foo() sont des méthodes génériques. Ensuite, l'annulation de C.foo() omet ce paramètre de type. C'est similaire à l'utilisation d'un type brut, essentiellement "optant" pour ne pas vérifier le type générique pour cette signature de méthode. Cela fait que le compilateur utilise les effacements des méthodes héritées à la place : List foo() et List foo() deviennent tous les deux List foo(), ce qui peut donc être implémenté par C.foo().

Vous pouvez voir que en gardant le paramètre de type dans la déclaration de C.foo(), il y aura l'erreur du compilateur attendue à la place :

// Le type de retour est incompatible avec A.foo()
public  List foo()
{
    return null;
}

De même, si l'une des méthodes d'interface ne déclare pas de paramètre de type, alors l'omission d'un paramètre de type dans l'annulation échoue à se «désigner» pour la vérification du type générique pour cette méthode, et le type de retour List reste incompatible.

Ce comportement est couvert dans JLS §8.4.2 :

 

La notion de subsignature est conçue pour exprimer une relation entre deux méthodes dont les signatures ne sont pas identiques, mais dans lesquelles l'une peut remplacer l'autre. Plus précisément, cela permet à une méthode dont la signature n'utilise pas de types génériques de remplacer n'importe quelle version générifiée de cette méthode. Ceci est important pour que les concepteurs de bibliothèques puissent librement générer des méthodes indépendamment des clients qui définissent des sous-classes ou des sous-interfaces de la bibliothèque.

Le FAQ sur les génériques d'Angelika Langer élargit ce comportement dans sa section Un méthode non générique peut-elle remplacer une méthode générique ? :

 

Explorons maintenant un exemple où les méthodes de sous-type non génériques remplacent les méthodes de supertype génériques. Les méthodes de sous-type non génériques sont considérées comme des versions de remplacement des méthodes de supertype génériques si les effacements des signatures sont identiques.

    

Exemple (de méthodes de sous-type non génériques remplaçant les méthodes de supertype génériques) :

class Super { 
  public  void set( T arg) { ... } 
  public  T get() { ... } 
} 
class Sub extends Super { 
  public void set( Object arg) { ... } // remplace 
  public Object get() { ... }    // remplace avec un avertissement non vérifié 
} 

    

    

attention : get() dans Sub remplace get() dans Super ; 
le type de retour nécessite une conversion non vérifiée 
trouvé : Object 
requis : T 
      public Object get() { 

    

Ici, les méthodes de sous-type ont des signatures, à savoir set(Object) et get()   , qui sont identiques aux effacements des méthodes de supertype. Ces   signatures érodées sont considérées comme des équivalents de remplacement. 

    

Il y a une imperfection dans le cas de la méthode get : nous recevons une   avertissement non vérifié parce que les types de retour ne sont pas vraiment compatibles.    Le type de retour de la méthode de sous-type get est Object, le type de retour   de la méthode de supertype get est un paramètre de type non borné. Le   type de retour de la méthode de sous-type n'est ni identique à celui du supertype   et n'est pas un sous-type de celui-ci ; dans les deux situations   le compilateur accepterait volontiers les types de retour comme compatibles.    Au lieu de cela, le type de retour de la méthode de sous-type Object est convertible en le   le type de retour de la méthode de supertype par le biais d'une conversion non vérifiée.    Un avertissement non vérifié indique qu'une vérification de type est nécessaire que   ni le compilateur ni la machine virtuelle ne peuvent effectuer. En d'autres termes, l'opération non vérifiée n'est pas sûre en termes de type.    En cas de types de retour convertibles, quelqu'un devrait s'assurer que le   la valeur de retour de la méthode de sous-type est compatible avec le supertype   le type de retour de la méthode, mais personne hormis le programmeur ne peut garantir   cela.

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