Comment référencer un type de retour générique avec plusieurs limites

J'ai récemment vu que l'on peut déclarer un type de retour qui est également limité par une interface. Considérez la classe et l'interface suivantes:

public class Foo {
    public String getFoo() { ... }
}

public interface Bar {
    public void setBar(String bar);
}

Je peux déclarer un type de retour comme ceci:

public class FooBar {

    public static <T extends Foo & Bar> T getFooBar() {
        //some implementation that returns a Foo object,
        //which is forced to implement Bar
    }
}

Si j'appelle cette méthode de quelque part, mon IDE me dit que le type de retour a la méthode String getFoo() ainsi que setBar(String), mais seulement si je pointe un point derrière la fonction comme ceci:

FooBar.getFooBar(). // here the IDE is showing the available methods.

Existe-t-il un moyen d'obtenir une référence à un tel objet? Je veux dire, si je ferait quelque chose comme ceci:

//bar only has the method setBar(String)
Bar bar = FooBar.getFooBar();
//foo only has the getFoo():String method
Foo foo = FooBar.getFooBar();

Je voudrais avoir une référence comme celle-ci (pseudo code):

<T extents Foo & Bar> fooBar = FooBar.getFooBar();
//or maybe
$1Bar bar = FooBar.getFooBar();
//or else maybe
Foo&Bar bar = FooBar.getFooBar();

Est-ce possible en Java, ou suis-je seulement capable de déclarer des types de retour comme celui-ci? Je pense que Java doit le taper aussi, en quelque sorte. Je préférerais ne pas recourir à un wrapper comme celui-ci, car cela ressemble à de la tricherie:

public class FooBarWrapper<T extends Foo&Bar> extends Foo implements Bar{

    public T foobar;

    public TestClass(T val){
        foobar = val;
    }


    @Override
    public void setBar(String bar) {
        foobar.setBar(bar);
    }

    @Override
    public String getFoo() {
        return foobar.getFoo();
    }
}

Java a-t-il vraiment inventé une fonctionnalité aussi intéressante, mais oublie qu'on aimerait y faire référence?

32
demandé sur Rafael T 2013-01-22 21:14:43

4 réponses

Alors que les paramètres de type d'une méthode générique peuvent être limités par des limites, telles que extends Foo & Bar, Ils sont finalement décidés par l'appelant. Lorsque vous appelez getFooBar(), le site d'appel sait déjà à quoi T est résolu. Souvent, ces paramètres de type seront inférés par le compilateur, c'est pourquoi vous n'avez généralement pas besoin de Les spécifier, comme ceci:

FooBar.<FooAndBar>getFooBar();

Mais même lorsque {[18] } est inféré comme FooAndBar, c'est vraiment ce qui se passe dans les coulisses.

Donc, pour répondre à votre question, une telle syntaxe comme ceci:

Foo&Bar bothFooAndBar = FooBar.getFooBar();

Ne Serait jamais utile dans la pratique. La raison en est que l'appelant devez déjà savoir ce T est. Soit T est un type concret:

FooAndBar bothFooAndBar = FooBar.<FooAndBar>getFooBar(); // T is FooAndBar

Or, T est un paramètre de type non résolu, et nous sommes dans sa portée:

<U extends Foo & Bar> void someGenericMethod() {
    U bothFooAndBar = FooBar.<U>getFooBar(); // T is U
}

Un autre exemple de cela:

class SomeGenericClass<V extends Foo & Bar> {
    void someMethod() {
        V bothFooAndBar = FooBar.<V>getFooBar(); // T is V
    }
}

Techniquement, ça résume la réponse. Mais je voudrais aussi souligner que votre méthode d'exemple getFooBar est intrinsèquement dangereuse. Rappelez-vous que l'appelant décide ce que T devient, pas la méthode. Puisque getFooBar ne prend aucun paramètre lié à T, et à cause de l'effacement de type , ses seules options seraient de retourner nullou de "mentir" en faisant une distribution non cochée, risquant la pollution de tas. Une solution de contournement typique serait que getFooBar prenne un argument Class<T>, ou bien un FooFactory<T> par exemple.

Mise à jour

, Il s'avère que j'avais tort quand j'ai affirmé que l'appelant de getFooBar doit sachez toujours ce qu'est T. Comme le souligne @ MiserableVariable, il existe certaines situations où l'argument type d'une méthode générique est déduit comme une capture Générique , plutôt qu'un type concret ou une variable de type. voir sa réponse pour un excellent exemple d'une implémentation getFooBar qui utilise un proxy pour renvoyer son point que T est inconnu.

Comme nous l'avons discuté dans les commentaires, un exemple utilisant getFooBar créé la confusion parce qu'il ne prend aucun argument pour en déduire T. Certains compilateurs lancent une erreur sur un appel sans contexte à getFooBar() tandis que d'autres sont bien avec. Je pensais que les erreurs de compilation incohérentes-ainsi que le fait que l'appel à FooBar.<?>getFooBar() est illégal-ont validé mon point, mais ceux-ci se sont avérés être des harengs rouges.

Basé sur la réponse de @MiserableVariable, j'ai mis en place un nouvel exemple qui utilise une méthode générique avec un argument, pour supprimer le confusion. Supposons que nous ayons des interfaces Foo et Bar et une implémentation FooBarImpl:

interface Foo { }
interface Bar { }
static class FooBarImpl implements Foo, Bar { }

Nous avons également une classe de conteneur simple qui encapsule une instance d'un type implémentant Foo et Bar. Il déclare une méthode statique stupide {[45] } qui prend un FooBarContainer et renvoie son référent:

static class FooBarContainer<T extends Foo & Bar> {

    private final T fooBar;

    public FooBarContainer(T fooBar) {
        this.fooBar = fooBar;
    }

    public T get() {
        return fooBar;
    }

    static <T extends Foo & Bar> T unwrap(FooBarContainer<T> fooBarContainer) {
        return fooBarContainer.get();
    }
}

Disons maintenant que nous avons un type paramétré générique de FooBarContainer:

FooBarContainer<?> unknownFooBarContainer = ...;

, Nous sommes autorisés à passer unknownFooBarContainer dans unwrap. Cela montre que mon affirmation antérieure était fausse, parce que le call site ne sait pas ce qu'est T - seulement que c'est un type dans les limites extends Foo & Bar.

FooBarContainer.unwrap(unknownFooBarContainer); // T is a wildcard capture, ?

Comme je l'ai noté, appeler unwrap avec un caractère générique est illégal:

FooBarContainer.<?>unwrap(unknownFooBarContainer); // compiler error

Je ne peux que deviner que c'est parce que les captures génériques ne peuvent jamais correspondre les unes aux autres - l'argument ? fourni sur le site d'appel est ambigu, sans moyen de dire qu'il devrait correspondre spécifiquement au caractère générique dans le type de unknownFooBarContainer.

Donc, voici le cas d'utilisation de la syntaxe que l'OP demande Au sujet. L'appel de unwrap sur unknownFooBarContainer retourne une référence du type ? extends Foo & Bar. Nous pouvons attribuer cette référence à Foo ou Bar, mais pas les deux:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = FooBarContainer.unwrap(unknownFooBarContainer);

Si unwrap étaient chers et nous ne voulions l'appeler une fois, nous serions contraints de les jeter:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = (Bar)foo;

C'est donc là que la syntaxe hypothétique serait utile:

Foo&Bar fooBar = FooBarContainer.unwrap(unknownFooBarContainer);

Ce n'est qu'un cas d'utilisation assez obscur. Il y aurait des implications assez vastes pour permettre une telle syntaxe, à la fois bonne et mauvaise. Cela ouvrirait de la place pour les abus là où cela n'était pas nécessaire, et il est tout à fait compréhensible que les concepteurs de langage n'aient pas implémenté une telle chose. Mais je pense toujours que c'est intéressant de penser.


Une note sur la pollution des tas

(surtout pour @ MiserableVariable ) Voici une procédure pas à pas de la façon dont une méthode dangereuse comme getFooBar provoque la pollution de tas, et ses implications. Compte tenu de l'interface et des implémentations suivantes:

interface Foo { }

static class Foo1 implements Foo {
    public void foo1Method() { }
}

static class Foo2 implements Foo { }

Mettons en œuvre une méthode non sécurisée getFoo, similaire à getFooBar mais simplifiée pour cet exemple:

@SuppressWarnings("unchecked")
static <T extends Foo> T getFoo() {
    //unchecked cast - ClassCastException is not thrown here if T is wrong
    return (T)new Foo2();
}

public static void main(String[] args) {
    Foo1 foo1 = getFoo(); //ClassCastException is thrown here
}

Ici, lorsque le nouveau Foo2 est converti en T, Il est "décoché", ce qui signifie qu'à cause de l'effacement de type, le runtime ne sait pas qu'il devrait échouer, même s'il devrait dans ce cas puisque T était Foo1. Au lieu de cela, le tas est "pollué", ce qui signifie que les références pointent vers des objets auxquels ils n'auraient pas dû être autorisés.

L'échec se produit après le retour de la méthode, lorsque l'instance Foo2 tente pour être affecté à la référence foo1, qui a le type reifiable Foo1.

Vous pensez probablement, " OK donc ça a explosé sur le site d'appel au lieu de la méthode, grosse affaire."Mais cela peut facilement devenir plus compliqué lorsque plus de génériques sont impliqués. Par exemple:

static <T extends Foo> List<T> getFooList(int size) {
    List<T> fooList = new ArrayList<T>(size);
    for (int i = 0; i < size; i++) {
        T foo = getFoo();
        fooList.add(foo);
    }
    return fooList;
}

public static void main(String[] args) {

    List<Foo1> foo1List = getFooList(5);

    // a bunch of things happen

    //sometime later maybe, depending on state
    foo1List.get(0).foo1Method(); //ClassCastException is thrown here
}

Maintenant, il n'explose pas sur le site d'appel. Il explose quelque temps plus tard lorsque le contenu de foo1List s'habitue. C'est ainsi que la pollution de tas devient plus difficile à déboguer, car l'exception stacktrace ne pointe pas vous le problème réel.

Cela devient encore plus compliqué lorsque l'appelant est lui-même dans la portée Générique. Imaginez au lieu d'obtenir un List<Foo1> nous obtenons un List<T>, le mettre dans un Map<K, List<T>> et le retourner à une autre méthode. Vous avez l'idée, je l'espère.

30
répondu Paul Bellora 2017-05-23 12:10:23

Quant à la question de la prime,

Il y a des cas où une méthode appelée renvoyant une valeur peut être utilisée par l'appelant sans que connaisse le type concret. Il est même probable que ce type n'existe pas du tout, c'est seulement un proxy:

import java.lang.reflect.*;

interface Foo {}
interface Bar {}

class FooBar1 implements Foo, Bar {public String toString() { return "FooBar1"; }}
class FooBar2 implements Foo, Bar {public String toString() { return "FooBar2"; }}   

class FooBar {
    static <T extends Foo & Bar> T getFooBar1() { return (T) new FooBar1(); }
    static <T extends Foo & Bar> T getFooBar2() { return (T) new FooBar2(); }
    static <T extends Foo & Bar> T getFooBar() { 
        return (T) 
        Proxy.newProxyInstance(
            Foo.class.getClassLoader(),
            new Class[] { Foo.class, Bar.class },
            new InvocationHandler() {
                public Object invoke(Object proxy, Method method, Object[] args) {
                    return "PROXY!!!";}});
    }

    static <U extends Foo & Bar> void show(U u) { System.out.println(u); }

    public static void main(String[] args) {
        show(getFooBar1());
        show(getFooBar2());
        show(getFooBar());      
    }

}

Deux FooBar1 et FooBar2 mettre en œuvre Foo et Bar. Dans main, les appels à getFooBar1 et getFooBar2 peuvent être affectés à une variable, bien qu'il n'y ait pas de raison forte de savoir à mon humble avis.

Mais getFooBar est l' cas intéressant, qui utilise un proxy. En pratique, il peut s'agir de l'instance seulement d'un objet qui implémente les deux interfaces. Une méthode différente (show ici) peut être utilisée avec un temporaire d'une manière plus sûre, mais elle ne peut pas être affectée à une variable sans le hack FooBarWrapper décrit dans la question. Il n'est même pas possible de créer un wrapper générique, class Wrapper<T extends U & V> n'est pas autorisé.

Le seul problème semble être de définir une syntaxe, d'autres mécanismes de vérification de type semblent être en place, au moins dans Oracle javac 1.7.0.

7
répondu Miserable Variable 2013-01-31 18:51:14

Comme @ Paul Bellora mentionné dans sa réponse, le type est résolu par l'appelant, car essentiellement il va maintenant ce qu'il appelle. Je voudrais juste ajouter à sa réponse avec un cas d'utilisation où je pense que l'utilisation de la syntaxe pourrait être bénéfique.

Il existe toujours des alternatives qui évitent d'utiliser une telle syntaxe. Je ne peux pas penser à un seul exemple que cela est tout à fait nécessaire. Cependant je peux penser à un cas d'utilisation d'une situation particulière que cette syntaxe pourrait être utilisée commodément, bien que je ne l'ai même pas utilisé moi-même. Je sais que ce n'est pas le meilleur exemple là-bas, mais il peut arriver au point.

Cas

Récemment, j'ai travaillé au développement d'une interface utilisateur. Dans cette application, j'utilise une bibliothèque pour gérer mes éléments GUI. En plus des fonctionnalités de la bibliothèque, j'ai créé une interface personnalisée qui définit une vue dans mon application qui a des entrées pour un type spécifique de données, disons, entrée de coordonnées. Cette interface aurait l'air comme:

 public interface CoordinateView extends View
 {
      Coordinate getCoordinate();

      //Maybe more stuff
 } 


J'ai plusieurs fenêtres à travers mon application qui implémentent cette interface. Maintenant, disons que pour une raison quelconque, je veux stocker dans un modèle la dernière coordonnée soumise dans une fenêtre et fermer la fenêtre juste après. Pour cela, je peux attacher un gestionnaire au bouton de fenêtre qui soumet le formulaire, le gestionnaire sera déclenché lorsque l'utilisateur ferme la fenêtre. Je pourrais y parvenir en ajoutant simplement le gestionnaire anonymement dans chaque fenêtre, comme:

  public MyWindow extends Window implements CoordinateView, OtherInterface
  {
       private Button submitButton;

       public MyWindow()
       {
          super();
          //Create all the elements

          submitButton.addClickHandler(
              new ClickHandler()
              {
                 @Override
                 onCLick(ClickEvent e)
                 {
                   getModel().add(getCoordinate());
                   destroy();
                 }
              });  
       }
  }

Toutefois, cette conception n'est pas souhaitable pour moi, il n'est pas assez modulaire. Considérant que j'ai une quantité décente de fenêtres avec ce comportement, le changer pourrait devenir plutôt fastidieux. Donc, je préfère extraire la méthode anonyme dans une classe afin qu'il soit plus facile de changer et de maintenir. Mais le problème est que la méthode destroy () n'est définie dans aucune interface, fait juste partie de window et la méthode getCoordinate () est définie dans l'interface I définir.

Utilisation

Dans ce cas, je pourrais utiliser plusieurs limites comme suit:

  public class MyController <T extends Window & CoordinateView> implements ClickHandler
  {
      private T windowWithCoordinates;

      public MyController (T window)
      {
         windowWithCoordinates = window;
      }

      @Override
      onClick(ClickEvent e)
      {
          getModel().add(windowWithCoordinates.getCoordinate());
          windowWithCoordinate.destroy();
      }
  }

Alors le code dans windows sera maintenant:

 public MyWindow extends Window implements CoordinateView, OtherInterface
  {
       private Button submitButton;

       public MyWindow()
       {
          super();
          //Create all the elements

          submitButton.addClickHandler(new MyController<MyWindow>(this));

       }
  }

Notez que le comportement restera le même, le code est juste un cohésif comme il était. C'est seulement plus modulaire, mais il n'a pas nécessité la création d'une interface supplémentaire pour pouvoir l'extraire correctement.

Alternative

Alternativement, j'aurais pu définir une interface supplémentaire étendre CoordinateView et définir une méthode pour fermer la fenêtre.

public interface CoordinateWindow extends CoordinateView
{
    void destroy();
}

Faire en sorte que la fenêtre implémente cette interface plus spécifique au lieu d'utiliser inutilement des paramètres génériques dans le contrôleur extrait:

  public class MyController implements ClickHandler
  {
      private CoordinateWindow windowWithCoordinates;

      public MyController (CoordinateWindow window)
      {
         windowWithCoordinates = window;
      }

      @Override
      onClick(ClickEvent e)
      {
          getModel().add(windowWithCoordinates.getCoordinate());
          windowWithCoordinate.destroy();
      }
  }



    public MyWindow extends Window implements CoordinateWindow
   {
       private Button submitButton;

       public MyWindow()
       {
          super();
          //Create all the elements  
          submitButton.addClickHandler(new MyController(this));                  
       }

       @Override
       void destroy(){
          this.destroy();
       }
  }

Cette approche pour certains peut être considérée comme beaucoup plus propre que la précédente et encore plus réutilisable puisque maintenant elle pourrait être ajoutée à d'autres "fenêtres" en dehors de la hiérarchie spécifiée. Personnellement, je préfère cette approche. Cependant, il peut y avoir un peu de plus de codage puisqu'une nouvelle interface doit être définie juste pour obtenir un accès à une méthode désirée.

En conclusion, bien que personnellement je ne le recommande pas, je pense que l'utilisation de types génériques avec plusieurs limites pourrait aider à coupler des définitions tout en réduisant la quantité de code.

4
répondu Sednus 2017-03-07 05:06:33

Je ne sais pas ce que Eclipse fait pour vous, mais la plupart du code ci-dessus ne se rapproche pas de la compilation....

J'ai fait les changements appropriés pour le faire compiler autant que possible et voici ce que je reçois:

public class Foo
{
  public String getFoo() { return ""; }  // must have a body
}
public interface Bar // no ()
{
  public void setBar(String bar);
}
public class FooBar<T>
{
  public static <T extends Foo & Bar> T getFooBar()
  {
    return null;
  }
}
public class FB
{
  private FooBar<Object> fb = new FooBar<Object>();

  public static void main(String args[])
  {
    new FB();
  }

  public FB()
  {
    System.out.println(fb.getFooBar());
  }
}

FB.java:12: type parameters of <T>T cannot be determined; no unique maximal instance exists for type variable T with upper bounds java.lang.Object,Foo,Bar
    System.out.println(fb.getFooBar());
                                   ^
1 error
-2
répondu jco.owens 2013-01-22 17:30:53