web-dev-qa-db-ja.com

<T>リストですか? T> f()便利な署名を拡張します

<T> List<? extends T> f()は便利な署名ですか?何か問題がありますか?

これはインタビューの質問でした。私はこれを知っている:

  1. それはうまくコンパイルします
  2. List<? extends Number> lst = obj.<Number>f()のように使用して、次に、これらを含まないListメソッドのみを呼び出すことができます[〜#〜] t [〜#〜] =シグネチャ内(たとえば、isEmpty()、size()、ではないadd(T)、remove(T)

これで問題は完全に解決しましたか?

35
user10777718

このメソッドシグネチャは "便利"です。これは、重要な、縮退していないメソッドを実装できるという意味で(つまり、nullを返し、スローすることで)エラーだけが選択肢ではありません)。次の例に示すように、このような方法は、たとえば、次のような代数的構造を実装するのに役立ちます。モノイド。

まず、List<? extends T>は、次のプロパティを持つタイプです。

  • このリストのすべての要素がT型に準拠していることがわかっているため、このリストから要素を抽出するときはいつでも、Tが期待される位置で使用できます。あなたはこのリストから読み取ることができます。
  • 正確なタイプは不明であるため、Tの特定のサブタイプのインスタンスをこのリストに追加できるかどうかは確実ではありません。つまり、効果的にそのようなリストに新しい要素を追加することはできませんnulls /型キャスト/ Javaの型の不健全性を利用しない限り)システム、つまり)。

組み合わせて、それはList<? extends T>は、タイプレベルの追加保護を備えた追加保護リストのようなものです。

このような「追加保護」リストを使用すると、実際に意味のある計算を行うことができます。以下にいくつかの例を示します。

  • 単一要素で追加保護リストを作成できます:

    public static <T> List<? extends T> pure(T t) {
      List<T> result = new LinkedList<T>();
      result.add(t);
      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> 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> emptyAppendProtected() {
      return new LinkedList<T>();
    }
    

一緒にcombineemptyは実際の代数構造(モノイド)を形成し、pureのようなメソッドは非縮退であることを保証します(つまり、空のリスト)。実際、通常の Monoid typeclassに似たインターフェースがある場合:

  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 ReadOnlyLists.<T>emptyAppendProtected();
      }

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

これは、質問で指定された署名を持つメソッドを使用して、いくつかの一般的な設計パターン/代数的構造(モノイド)を実装できることを示しています。確かに、この例はやや工夫が凝らされています。 APIのユーザーをあまり驚かせたくない であるため、実際には使用したくないでしょう。


完全にコンパイル可能な例:

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

私は「それは有用なシグネチャーであるか」を「それのユースケースを考えることができるか」を意味すると解釈します。

Tはメソッド内ではなく、呼び出しサイトで決定されるため、メソッドから返すことができるのは、nullまたは空のリストの2つだけです。

このメソッドを呼び出すのとほぼ同じくらい多くのコードでこれらの値の両方を作成できることを考えると、実際にそれを使用する十分な理由はありません。


実際、安全に返すことができる別の値は、すべての要素がnullであるリストです。ただし、型バインドの? extendsがあるため、戻り値からリテラルnullを追加または削除するメソッドのみを呼び出すことができるため、これも有用ではありません。つまり、含まれているnullの数を数えるだけです。どちらも役に立ちません。

16
Andy Turner

公式の Generics tutorial は、ワイルドカードの戻り値の型を使用しないことを推奨しています。

これらのガイドラインは、メソッドの戻り値の型には適用されません。戻り値の型としてワイルドカードを使用すると、プログラマーがコードを使用してワイルドカードを処理する必要が生じるため、使用を避けてください。」

しかし、与えられた推論は正確には説得力がありません。

1
togorks

他の回答で与えられた理由のため、それは特に有用ではありません。ただし、次のようなクラスではそれが「有用」であることを考慮してください(おそらく少しアンチパターンですが)。

_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());
    }
}
_

次の機能に注意してください。

  1. f()の宣言された戻り値の型は、呼び出し元が好きなTを設定でき、メソッドが必要な型を返すことを意味します。f() wasnそのように宣言されていない場合、呼び出し元はget(N)へのすべての呼び出しを必要な型にキャストする必要があります。
  2. ワイルドカードを使用しているため、宣言された戻り値の型は、返されたリストを読み取り専用にします。多くの場合、これは便利な機能です。ゲッターを呼び出したときに、書き込み可能なリストを返したくない場合。多くの場合、ゲッターはCollections.unmodifiableList()を使用して、実行時にリストを強制的に読み取り専用にしますが、ワイルドカードジェネリックパラメーターを使用すると、リストはcompile-timeで強制的に読み取り専用になります。 !
  3. 欠点は、タイプセーフではないことです。 f()が型を返すことを確認するのは呼び出し元の責任です。Tは以前に追加されたすべての項目の共通のスーパークラスです。
  4. 通常、メソッドf()ではなくクラスRepositoryをジェネリックにする方がはるかに優れています。
0