web-dev-qa-db-ja.com

Javaジェネリックに<が必要な場合<T>の代わりにT>を拡張し、切り替えのマイナス面はありますか?

次の例を考えてみましょう(JUnitをHamcrest matcherと組み合わせて使う):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<Java.util.Date>> result = null;
assertThat(result, is(expected));  

これは、次のJUnitのassertThatメソッドシグネチャではコンパイルされません。

public static <T> void assertThat(T actual, Matcher<T> matcher)

コンパイラのエラーメッセージは次のとおりです。

Error:Error:line (102)cannot find symbol method
assertThat(Java.util.Map<Java.lang.String,Java.lang.Class<Java.util.Date>>,
org.hamcrest.Matcher<Java.util.Map<Java.lang.String,Java.lang.Class
    <? extends Java.io.Serializable>>>)

ただし、assertThatメソッドのシグネチャを次のように変更したとします。

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

それからコンパイルはうまくいきます。

3つの質問

  1. どうして正確に現在のバージョンがコンパイルされないのですか?私はここで共分散の問題を漠然と理解していますが、私がしなければならない場合、私は確かにそれを説明することができませんでした。
  2. assertThatメソッドをMatcher<? extends T>に変更することにマイナス面がありますか?あなたがそうするならば壊れる他のケースがありますか?
  3. JUnitのassertThatメソッドを一般化する意味はありますか? JUnitはmatchesメソッドを呼び出すので、Matcherクラスはそれを必要としていないようです。これは、Matcherクラスでは不可能なので、何もしない型安全を強制しようとする試みのようです。実際には一致し、テストは関係なく失敗します。危険な操作は含まれていません(またはそう思われます)。

参考のために、assertThatのJUnit実装を示します。

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new Java.lang.AssertionError(description.toString());
    }
}
182
Yishai

最初に - 私はあなたに指示する必要があります http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - 彼女は素晴らしい仕事をします。

基本的な考え方はあなたが使うということです

<T extends SomeClass>

実パラメータがSomeClassname__またはそのサブタイプになることができる場合.

あなたの例では、

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<Java.util.Date>> result = null;
assertThat(result, is(expected));

expectedname__には、Serializablename__を実装するクラスを表すClassオブジェクトを含めることができると言っています。あなたの結果マップはそれがDatename__クラスのオブジェクトしか保持できないと言っています。

結果を渡すときは、Tname__をMapname__からStringname__までのクラスオブジェクトの正確なDatename__に設定します。これは、Mapname__のStringname__とSerializablename__のいずれにも一致しません。

1つ確認してください - Datename__ではなくClass<Date>が必要ですか? Stringname__からClass<Date>へのマップは、一般的にはあまり役に立ちません(保持できるのは、Datename__のインスタンスではなく、値としてのDate.classだけです)。

assertThatname__を一般化することに関しては、結果の型に合ったMatchername__が渡されることをメソッドが保証することができるという考えです。

126

質問に答えてくれたみんなのおかげで、それは私にとって物事を明確にするのに本当に役立ちました。結局、Scott Stanchfieldの答えは私がそれを理解することになった方法に最も近いものになりましたが、彼が最初に書いたときには理解できなかったので、私は問題を言い直すように試みます。

Listには一般的なパラメータが1つしかなく、それによって理解が容易になるので、私はListに関してこの問題をもう一度言います。

パラメータ化されたクラス(例のようにList<Date>やMap<K, V>など)の目的は、強制ダウンキャストとし、これが安全であることをコンパイラに保証させることです(ランタイム例外はありません)。

リストの場合を考えます。私の質問の本質は、なぜT型とListを取るメソッドがTよりも継承の連鎖の下にある何かのListを受け入れないのです。この人為的な例を考えてみましょう:

List<Java.util.Date> dateList = new ArrayList<Java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

Listパラメータは文字列のリストではなく日付のリストであるため、これはコンパイルされません。これがコンパイルされれば、ジェネリックスはあまり役に立ちません。

同じことがMap<String, Class<? extends Serializable>>にも当てはまります。これはMap<String, Class<Java.util.Date>>と同じことではありません。これらは共変ではないので、日付クラスを含むマップから値を取得し、それを直列化可能な要素を含むマップに配置する場合は問題ありませんが、次のようなメソッドシグネチャが必要です。

private <T> void genericAdd(T value, List<T> list)

両方を実行できるようにしたいです。

T x = list.get(0);

そして

list.add(value);

この場合、junitメソッドが実際にこれらのことを気にしなくても、メソッドシグネチャは共分散を必要としますが、それは得られないため、コンパイルされません。

2番目の質問では、

Matcher<? extends T>

TがObjectの場合、実際には何でも受け入れるという欠点があります。これはAPIの意図ではありません。その意図は、マッチャーが実際のオブジェクトと一致することを静的に保証することであり、その計算からオブジェクトを除外する方法はありません。

3番目の質問に対する答えは、未チェックの機能性に関しては何も失われないということです(このメソッドが汎用化されていなければJUnit API内に安全でない型キャストはありません)。 2つのパラメーターが一致する可能性があります。

編集(さらなる熟考と経験の後):

AssertThatメソッドのシグネチャに関する大きな問題の1つは、変数TをTのジェネリックパラメータと同一視しようとすることです。これらは共変ではないため、機能しません。そのため、例えばList<String>であるTがあっても、コンパイラがMatcher<ArrayList<T>>に解決するという一致を渡すことができます。 ListとArrayListは共変なので、今はそれが型パラメータでなければ、問題はありませんが、Genericsは、コンパイラに関してはArrayListを必要とするので、リストを許容することはできません。上記から。

24
Yishai

それは沸騰する:

Class<? extends Serializable> c1 = null;
Class<Java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

クラス参照c1にLongインスタンスが含まれている可能性があります(ある時点での基礎となるオブジェクトはList<Long>であった可能性があるため)。ただし、 "unknown"クラスがDateであるという保証はないため、明らかにDateにキャストできません。安全ではないので、コンパイラはそれを許可しません。

ただし、他のオブジェクト、たとえばListを紹介すると(このオブジェクトはMatcherです)、次のようになります。

List<Class<? extends Serializable>> l1 = null;
List<Class<Java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

...しかし、リストの型がになったら? Tの代わりにTを拡張します。

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<Java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

Matcher<T> to Matcher<? extends T>を変更することで、基本的にl1 = l2を代入するのと同じようなシナリオを紹介していると思います。

ワイルドカードを入れ子にすることは依然として非常に混乱しますが、一般的な参照を相互に割り当てる方法を検討することで総称を理解するのに役立つ理由については理にかなっています。関数呼び出しをするときにコンパイラがTの型を推論しているので、それはさらに混乱します(あなたはそれがTであると明示的に言っているのではありません)。

14
GreenieMeanie

元のコードがコンパイルされないのは、<? extends Serializable>notを意味するからです。

たとえば、記述されたとおりのコードであれば、new TreeMap<String, Long.class>()>expectedに代入することは完全に有効です。コンパイラがコードのコンパイルを許可した場合、マップ内で見つかったDateオブジェクトではなくLongオブジェクトが想定されるため、assertThat()はおそらく破損します。

8
erickson

私がワイルドカードを理解する一つの方法は、ワイルドカードは与えられた総称参照が「持つ」ことができる可能なオブジェクトのタイプを指定していないと考えることですが、互換性のある他の総称参照のタイプ...)そのように、最初の答えはその言い回しにおいて非常に誤解を招くものです。

言い換えれば、List<? extends Serializable>はあなたがその参照を他のリストに割り当てることができることを意味します。 A SINGLE LISTがSerializableのサブクラスを保持できるという点でそれを考えてはいけません(それは誤った意味論であり、Genericsの誤解につながります)。

7
GreenieMeanie

私はこれが昔からの質問であることを知っていますが、私は境界ワイルドカードをかなりよく説明していると思う例を共有したいと思います。 Java.util.Collectionsはこのメソッドを提供します:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

TのListがある場合、当然、ListはTを拡張する型のインスタンスを含むことができます。リストに動物が含まれる場合、リストには犬と猫の両方(両方の動物)を含めることができます。犬には「woofVolume」というプロパティがあり、猫には「meowVolume」というプロパティがあります。 Tのサブクラスに特有のこれらのプロパティに基づいてソートしたいと思うかもしれませんが、どのようにしてこのメ​​ソッドがそれを行うことができるのでしょうか。 Comparatorの制限は、1つの型(T)のうち2つしか比較できないことです。そのため、単にComparator<T>を要求するだけでこのメソッドは使用可能になります。しかし、このメソッドの作成者は、何かがTであれば、それもTのスーパークラスのインスタンスであることを認識していました。したがって、彼はTのコンパレーターまたはTのスーパークラス、つまり? super Tの使用を許可します。

3
Lucas Ross

あなたが使用する場合はどうなります

Map<String, ? extends Class<? extends Serializable>> expected = null;
1
newacct