web-dev-qa-db-fra.com

Pourquoi ne pouvez-vous pas avoir plusieurs interfaces dans un générique générique lié?

Je sais qu'il y a toutes sortes de propriétés contre-intuitives des types génériques de Java. En voici un en particulier que je ne comprends pas et que j'espère que quelqu'un pourra m'expliquer. Lorsque vous spécifiez un paramètre de type pour une classe ou une interface, vous pouvez le lier de sorte qu'il doit implémenter plusieurs interfaces avec public class Foo<T extends InterfaceA & InterfaceB>. Cependant, si vous instanciez un objet réel, cela ne fonctionne plus. List<? extends InterfaceA> va bien, mais List<? extends InterfaceA & InterfaceB> ne parvient pas à compiler. Considérez l'extrait complet suivant:

import Java.util.List;

public class Test {

  static interface A {
    public int getSomething();
  }

  static interface B {
    public int getSomethingElse();
  }

  static class AandB implements A, B {
    public int getSomething() { return 1; }
    public int getSomethingElse() { return 2; }
  }

  // Notice the multiple bounds here. This works.
  static class AandBList<T extends A & B> {
    List<T> list;

    public List<T> getList() { return list; }
  }

  public static void main(String [] args) {
    AandBList<AandB> foo = new AandBList<AandB>(); // This works fine!
    foo.getList().add(new AandB());
    List<? extends A> bar = new LinkedList<AandB>(); // This is fine too
    // This last one fails to compile!
    List<? extends A & B> foobar = new LinkedList<AandB>();
  }
}

Il semble que la sémantique de bar devrait être bien définie - je ne peux penser à aucune perte de sécurité de type en permettant une intersection de deux types plutôt que d'un seul. Je suis sûr qu'il y a une explication cependant. Est-ce que quelqu'un sait ce que c'est?

65
Adrian Petrescu

Il est intéressant de noter que l'interface Java.lang.reflect.WildcardType semble prendre en charge les limites supérieure et inférieure d'un argument générique; et chacun peut contenir plusieurs bornes

Type[] getUpperBounds();
Type[] getLowerBounds();

C'est bien au-delà de ce que la langue permet. Il y a un commentaire caché dans le code source

// one or many? Up to language spec; currently only one, but this API
// allows for generalization.

L'auteur de l'interface semble considérer qu'il s'agit d'une limitation accidentelle.

La réponse à votre question est la suivante: les médicaments génériques sont déjà trop compliqués. l'ajout de plus de complexité pourrait s'avérer la dernière paille.

Pour permettre à un caractère générique d'avoir plusieurs limites supérieures, il faut parcourir les spécifications et s'assurer que tout le système fonctionne toujours. 

Un problème que je connais serait dans l’inférence de type. Les règles d'inférence actuelles ne peuvent tout simplement pas traiter les types d'interconnexion. Il n'y a pas de règle pour réduire une contrainte A&B << C. Si nous le réduisions à 

    A<<C 
  or
    A<<B

tout moteur d'inférence en cours doit faire l'objet d'une révision majeure pour permettre cette bifurcation. Mais le vrai problème, c’est que cela permet de multiples solutions, mais rien ne permet de préférer une solution à une autre.

Cependant, l'inférence n'est pas essentielle au type de sécurité; nous pouvons simplement refuser d'inférer dans ce cas, et demander au programmeur de remplir explicitement les arguments de type. Par conséquent, la difficulté d'inférence n'est pas un argument de poids contre les types d'interconnexion.

33
irreputable

A partir de Spécification du langage Java :

4.9 Types d'intersection Un type d'intersection prend la forme T1 & ... & Tn, n> 0, où Ti, 1in sont des expressions de type. Les types d'intersection apparaissent dans les processus de conversion de capture (§5.1.10) et d'inférence de type (§15.12.2.7). Il n'est pas possible d'écrire un type d'intersection directement dans le cadre d'un programme; aucune syntaxe ne supporte ceci. Les valeurs d'un type d'intersection sont les objets correspondant à tous les types Ti, pour 1in.

Alors, pourquoi n'est-ce pas supporté? À mon avis, que devriez-vous faire avec une telle chose? - supposons que cela soit possible:

List<? extends A & B> list = ...

Alors que devrait

list.get(0);

revenir? Il n'y a pas de syntaxe pour capturer une valeur de retour de A & B. L'ajout de quelque chose dans une telle liste ne serait pas possible non plus, donc c'est fondamentalement inutile.

22
emboss

Pas de problème ... déclarez simplement le type dont vous avez besoin dans la signature de la méthode.

Cela compile:

public static <T extends A & B> void main(String[] args) throws Exception
{
    AandBList<AandB> foo = new AandBList<AandB>(); // This works fine!
    foo.getList().add(new AandB());
    List<? extends A> bar = new LinkedList<AandB>(); // This is fine too
    List<T> foobar = new LinkedList<T>(); // This compiles!
}
9
Bohemian

Bonne question. Il m'a fallu un certain temps pour comprendre.

Simplifions votre cas: vous essayez de faire la même chose que si vous déclariez une classe qui étend 2 interfaces, puis une variable qui a pour type ces 2 interfaces, quelque chose comme ceci:

  class MyClass implements Int1, Int2 { }

  Int1 & Int2 variable = new MyClass()

Bien sûr, illégal. Et cela équivaut à ce que vous essayez de faire avec les génériques. Ce que vous essayez de faire est:

  List<? extends A & B> foobar;

Mais alors, pour utiliser foobar, vous devriez utiliser une variable des deux interfaces de cette façon:

  A & B element = foobar.get(0);

Ce qui est pas légal en Java. Cela signifie que vous déclarez les éléments de la liste comme étant de 2 types simultanément, et même si notre cerveau peut s'en occuper, le langage Java ne le peut pas.

1
edutesoy

Pour ce qui en vaut la peine: si quelqu'un se le demande parce qu'il aimerait vraiment l'utiliser, je l'ai corrigé en définissant une interface contenant l'union de toutes les méthodes de toutes les interfaces et de la classe avec lesquelles je travaille. c'est-à-dire que j'essayais de faire ce qui suit:

class A {}

interface B {}

List<? extends A & B> list;

ce qui est illégal - alors j'ai plutôt fait ceci:

class A {
  <A methods>
}

interface B {
  <B methods>
}

interface C {
  <A methods>
  <B methods>
}

List<C> list;

Cela n’est toujours pas aussi utile que de pouvoir taper quelque chose comme List<? extends A implements B>, par exemple. si quelqu'un ajoute ou supprime des méthodes à A ou à B, la saisie de la liste ne sera pas mise à jour automatiquement, elle nécessite un changement manuel en C. Mais cela a fonctionné pour mes besoins.

0
Mark