web-dev-qa-db-fra.com

Est-ce que <T> Liste <? étend T> f() une signature utile

<T> List<? extends T> f() est-il une signature utile? Y a-t-il un problème avec/l'utiliser?

C'était une question d'entrevue. Je sais ça:

  1. Il compile bien
  2. Utilisez-le comme List<? extends Number> lst = obj.<Number>f(), puis je ne peux appeler sur lst que les méthodes List qui ne contiennent pas T = dans leurs signatures (disons isEmpty (), size (), mais pas add (T), remove (T)

Est-ce que cela répond pleinement à la question?

35
user10777718

Cette signature de méthode est "utile", dans le sens où vous pouvez implémenter avec elle des méthodes non triviales et non dégénérées (c'est-à-dire, renvoyer null et les erreurs de lancement ne sont pas vos seules options). Comme le montre l'exemple suivant, une telle méthode peut être utile pour mettre en œuvre certaines structures algébriques comme par ex. monoides.

Tout d'abord, observez que List<? extends T> est un type avec les propriétés suivantes:

  • Vous savez que tous les éléments de cette liste sont conformes au type T, donc chaque fois que vous extrayez un élément de cette liste, vous pouvez l'utiliser à la position où un T est attendu. Vous pouvez lire dans cette liste .
  • Le type exact est inconnu, vous ne pouvez donc jamais être certain qu'une instance d'un sous-type particulier de T peut être ajoutée à cette liste. C'est-à-dire que vous ne pouvez pas ajouter de nouveaux éléments à une telle liste (à moins que vous n'utilisiez nulls/type casts/exploitez les défauts du système de types de Java, c'est-à-dire).

En combinaison, cela signifie que List<? extends T> est un peu comme une liste protégée contre les ajouts, avec une protection contre les ajouts au niveau du type.

Vous pouvez réellement faire des calculs significatifs avec de telles listes "protégées contre les ajouts". Voici quelques exemples:

  • Vous pouvez créer des listes protégées contre les ajouts avec un seul élément:

    public static <T> List<? extends T> pure(T t) {
      List<T> result = new LinkedList<T>();
      result.add(t);
      return result;
    }
    
  • Vous pouvez créer des listes protégées contre les ajouts à partir de listes ordinaires:

    public static <T> List<? extends T> toAppendProtected(List<T> original) {
      List<T> result = new LinkedList<T>();
      result.addAll(original);
      return result;
    }
    
  • Vous pouvez combiner des listes protégées par ajout:

    public static <T> List<? extends T> combineAppendProtected(
      List<? extends T> a,
      List<? extends T> b
    ) {
      List<T> result = new LinkedList<T>();
      result.addAll(a);
      result.addAll(b);
      return result;
    }
    
  • Et, plus important encore pour cette question, vous pouvez implémenter une méthode qui renvoie une liste vide protégée contre les ajouts de type donné:

    public static <T> List<? extends T> emptyAppendProtected() {
      return new LinkedList<T>();
    }
    

Ensemble, combine et empty forment une véritable structure algébrique (un monoïde), et des méthodes comme pure s'assurent qu'elle n'est pas dégénérée (c'est-à-dire qu'elle a plus d'éléments qu'un simple vide liste). En effet, si vous aviez une interface similaire à la classe de caractères habituelle Monoid :

  public static interface Monoid<X> {
    X empty();
    X combine(X a, X b);
  }

alors vous pouvez utiliser les méthodes ci-dessus pour l'implémenter comme suit:

  public static <T> Monoid<List<? extends T>> appendProtectedListsMonoid() {
    return new Monoid<List<? extends T>>() {
      public List<? extends T> empty() {
        return ReadOnlyLists.<T>emptyAppendProtected();
      }

      public List<? extends T> combine(
        List<? extends T> a,
        List<? extends T> b
      ) {
        return combineAppendProtected(a, b);
      }
    };
  }

Cela montre que les méthodes avec la signature donnée dans votre question peuvent être utilisées pour implémenter certains modèles de conception/structures algébriques communs (monoïdes). Certes, l'exemple est quelque peu artificiel, vous ne voudriez probablement pas l'utiliser dans la pratique, parce que vous ne voulez pas trop étonner les utilisateurs de votre API .


Exemple compilable complet:

import Java.util.*;

class AppendProtectedLists {

  public static <T> List<? extends T> emptyAppendProtected() {
    return new LinkedList<T>();
  }

  public static <T> List<? extends T> combineAppendProtected(
    List<? extends T> a,
    List<? extends T> b
  ) {
    List<T> result = new LinkedList<T>();
    result.addAll(a);
    result.addAll(b);
    return result;
  }

  public static <T> List<? extends T> toAppendProtected(List<T> original) {
    List<T> result = new LinkedList<T>();
    result.addAll(original);
    return result;
  }

  public static <T> List<? extends T> pure(T t) {
    List<T> result = new LinkedList<T>();
    result.add(t);
    return result;
  }

  public static interface Monoid<X> {
    X empty();
    X combine(X a, X b);
  }

  public static <T> Monoid<List<? extends T>> appendProtectedListsMonoid() {
    return new Monoid<List<? extends T>>() {
      public List<? extends T> empty() {
        return AppendProtectedLists.<T>emptyAppendProtected();
      }

      public List<? extends T> combine(
        List<? extends T> a,
        List<? extends T> b
      ) {
        return combineAppendProtected(a, b);
      }
    };
  }

  public static void main(String[] args) {
    Monoid<List<? extends String>> monoid = appendProtectedListsMonoid();
    List<? extends String> e = monoid.empty();
    // e.add("hi"); // refuses to compile, which is good: write protection!
    List<? extends String> a = pure("a");
    List<? extends String> b = pure("b");
    List<? extends String> c = monoid.combine(e, monoid.combine(a, b));
    System.out.println(c); // output: [a, b]
  }

}
22
Andrey Tyukin

J'interprète "est-ce une signature utile" pour signifier "pouvez-vous penser à un cas d'utilisation pour cela".

T est déterminé sur le site d'appel, pas à l'intérieur de la méthode, il n'y a donc que deux choses que vous pouvez retourner à partir de la méthode: null ou une liste vide.

Étant donné que vous pouvez créer ces deux valeurs dans à peu près autant de code que d'invoquer cette méthode, il n'y a pas vraiment de bonne raison de l'utiliser.


En fait, une autre valeur qui peut être renvoyée en toute sécurité est une liste où tous les éléments sont nuls. Mais cela n'est pas utile non plus, car vous ne pouvez appeler que des méthodes qui ajoutent ou suppriment un littéral null de la valeur de retour, en raison de ? extends dans le type lié. Donc, tout ce que vous avez est une chose qui compte le nombre de null qu'elle contient. Ce qui n'est pas utile non plus.

16
Andy Turner

L'officiel tutoriel sur les génériques suggère de ne pas utiliser de types de retour génériques.

Ces instructions ne s'appliquent pas au type de retour d'une méthode. L'utilisation d'un caractère générique comme type de retour doit être évitée car elle oblige les programmeurs à utiliser le code pour gérer les caractères génériques. "

Le raisonnement donné n'est cependant pas vraiment convaincant.

1
togorks

Ce n'est pas particulièrement utile, pour les raisons données dans d'autres réponses. Cependant, considérez que c'est "l'utilité" dans une classe comme la suivante (bien que ce soit probablement un peu un contre-modèle):

public class Repository {
    private List<Object> lst = new ArrayList<>();

    public <T> void add(T item) {
        lst.add(item);
    }

    @SuppressWarnings("unchecked")
    public <T> List<? extends T> f() {
        return (List<? extends T>) lst;
    }

    public static void main(String[] args) {
        Repository rep = new Repository();
        rep.add(BigInteger.ONE);
        rep.add(Double.valueOf(2.0));
        List<? extends Number> list = rep.f();
        System.out.println(list.get(0).doubleValue() + list.get(1).doubleValue());
    }
}

Notez les fonctionnalités suivantes:

  1. Le type de retour déclaré f() signifie que l'appelant peut définir ce qu'il veut et la méthode renverra le type requis. Si f() weren 't déclaré comme ça, alors l'appelant devrait convertir chaque appel à get(N) au type requis.
  2. Comme il utilise un caractère générique, le type de retour déclaré rend la liste retournée en lecture seule. Cela peut souvent être une fonctionnalité utile. Lorsque vous appelez un getter, vous ne voulez pas qu'il renvoie une liste dans laquelle vous pouvez écrire. Souvent, les getters utilisent Collections.unmodifiableList() qui force une liste à être en lecture seule au moment de l'exécution, mais l'utilisation du paramètre générique générique force la liste à être en lecture seule à au moment de la compilation !
  3. L'inconvénient est qu'il n'est pas particulièrement sûr pour le type. Il appartient à l'appelant de s'assurer que f() renvoie un type où T est une superclasse commune de tous les éléments ajoutés précédemment.
  4. Il serait généralement préférable de rendre la classe Repository générique au lieu de la méthode f ().
0
DodgyCodeException