Javaジェネリックにはいつ<が必要ですか?<T>の代わりにT>を拡張し、切り替えの欠点はありますか?


205

次の例があるとします(JUnitとHamcrestマッチャーを使用)。

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. assertThatJUnitでメソッドを一般化する意味はありますか?MatcherJUnitのは、何もしないタイプの安全性を強制しようとする試みのような任意の一般的な、そしてただルックスに型付けされていないmatchesメソッドを呼び出しますので、としてクラスは、それを必要としていないようだMatcher、実際にだけではないだろう一致し、テストは失敗します。安全でない操作は含まれていません(またはそのように思われます)。

参考までに、以下にJUnit実装を示しassertThatます。

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

これは、リンクが非常に便利(ジェネリック、継承とサブタイプ)である:docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

回答:


145

まず、http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.htmlに案内する必要があります -彼女は素晴らしい仕事をしています。

基本的な考え方は、

<T extends SomeClass>

実パラメータが、SomeClassまたはそのサブタイプである場合。

あなたの例では、

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

にはexpected、を実装するクラスを表すClassオブジェクトを含めることができますSerializable。結果マップは、Dateクラスオブジェクトのみを保持できると述べています。

あなたは結果を渡すときに、あなたしている設定Tを正確にMapするStringDate一致していないクラスのオブジェクト、MapStringだ何でもしますSerializable

一つは、チェックする-あなたはあなたがしたいですClass<Date>し、ではありませんかDateStringto のマップはClass<Date>、一般的にはあまり役に立たないようです(それが保持できるのDate.classは、のインスタンスではなく値としてのみDate

のジェネリック化assertThatについては、メソッドはMatcher、結果のタイプに適合するaが確実に渡されるようにするという考え方です。


この場合、はい、クラスのマップが必要です。私が提供した例は、カスタムクラスではなく標準のJDKクラスを使用するように工夫されていますが、この場合、クラスは実際にはリフレクションによってインスタンス化され、キーに基づいて使用されます。(クライアントがサーバークラスを利用できない分散アプリ。サーバー側の作業を行うために使用するクラスのキーのみ)。
Yishai、

6
Date型のクラスを含むMapがSerializable型のクラスを含むMapsの型にうまく適合しないのはなぜかというと、頭がおかしいのではないでしょうか。もちろん、Serializable型のクラスは他のクラスでもかまいませんが、Date型は確実に含まれます。
Yishai、

キャストが実行されることを確認するassertThatでは、matcher.matches()メソッドは関係ありません。Tは決して使用されないため、なぜそれを使用するのでしょうか。(メソッドの戻り型は無効です)
Yishai

ああ-それは、assertThatの定義を十分に読まないことで得られるものです フィッティングマッチャーが渡されることを確認するためだけにあるように見えます...
スコットスタンフィールド

28

質問に答えてくれた皆さんに感謝します。結局、スコットスタンチフィールドの答えは私がそれを理解する方法に最も近づきましたが、彼が最初に書いたときに彼を理解していなかったので、うまくいけば、他の誰かが利益を得るように問題を言い直そうとしています。

リストの観点から質問を再説明します。これは、ジェネリックパラメーターが1つだけあり、それにより理解が容易になるためです。

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

リストの場合を考えてみましょう。私の質問の本質は、型Tとリストを受け取るメソッドがTよりも継承のチェーンのさらに下にあるもののリストを受け入れない理由です。この不自然な例を考えてみましょう。

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がオブジェクトの場合、APIの意図ではない場合、実際には何でも受け入れるという欠点があります。その目的は、マッチャーが実際のオブジェクトと一致することを静的に確認することであり、その計算からオブジェクトを除外する方法はありません。

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

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

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


それでも、どうして私が予測できないのかわかりません。日付のリストをシリアル化可能なリストに変換できないのはなぜですか?
Thomas Ahle、2014

@ThomasAhle。これは、それが日付のリストであると考える参照は、文字列またはその他のシリアライズ可能なものを見つけたときにキャストエラーが発生するためです。
Yishai 2014

わかりましたが、もし私が何らかのList<Date>方法で古い参照を取り除いたとしたら、まるでタイプのメソッドからを返したList<Object>かのようですか?Javaで許可されていない場合でも、それは安全なはずです。
Thomas Ahle、2014

14

要約すると:

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>)が、「不明な」クラスが日付であるという保証がないため、明らかに日付にキャストできません。typsesafeではないので、コンパイラはそれを許可しません。

ただし、他のオブジェクト、たとえばリスト(この例ではこのオブジェクトはマッチャー)を導入すると、次のようになります。

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であると明示的には伝えていません)。


9

元のコードがコンパイルされない理由は、「Serializableを拡張する任意のクラス」<? extends Serializable>ではなく、「Serializableを拡張する不明だが特定のクラス」のことです。

例えば、記述されたようなコードが与えられると、割り当てる完全に有効であるnew TreeMap<String, Long.class>()>expected。コンパイラーがコードのコンパイルを許可した場合、マップで検出されたオブジェクトではなくオブジェクトassertThat()を予期するため、が壊れる可能性があります。DateLong


1
私は完全にフォローしていません-「ではないが...ではない」と言うとき:違いは何ですか?(たとえば、前者の定義には当てはまるが後者には当てはまらない「既知だが非特定の」クラスの例は何でしょうか?)
poundifdef

はい、それは少し厄介です。それをよりよく表現する方法がわからない...「」と言う方が理にかなっていますか?不明なタイプであり、何かと一致するタイプではありませんか?」
エリクソン2009年

1
何それを説明するのに役立つかもしれないことは、「直列化を拡張するすべてのクラス」のためにあなたは、単に使用することができることである<Serializable>
c0der

8

ワイルドカードを理解する1つの方法は、ワイルドカードが、特定の総称参照が「持つ」ことができる可能なオブジェクトのタイプを指定していないと考えることです。 ...)そのため、最初の答えは、その表現が非常に誤解を招くものです。

言い換えるとList<? extends Serializable>、タイプがSerializableのサブクラスまたはそのサブクラスである他のリストにその参照を割り当てることができることを意味します。SINGLE LISTがSerializableのサブクラスを保持できるとは考えないでください(これはセマンティクスが正しくなく、ジェネリックスの誤解を招くためです)。


それは確かに役立ちますが、「混乱するように聞こえるかもしれません」は、「混乱するような音」に置き換えられます。補足として、なぜこの説明に従って、Matcherを使用したメソッドが<? extends T>コンパイルされるのでしょうか。
Yishai、

List <Serializable>として定義した場合、同じことをしますか?私はあなたの2番目のパラについて話しています。つまり、ポリモーフィズムはそれを処理しますか?
Supun Wijerathne 2017年

3

これは古い質問であることはわかっていますが、制限付きワイルドカードをかなりよく説明していると思う例を共有したいと思います。java.util.Collectionsこのメソッドを提供します:

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

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


1

もしあなたが使うなら

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

はい、これは私の上の答えが運転していたもののようなものです。
GreenieMeanie 2009年

いいえ、それは状況、少なくとも私が試した方法を助けません。
Yishai、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.