web-dev-qa-db-ja.com

Javaジェネリッククラスのジェネリックメソッド

Java(クラスにはジェネリック型パラメーターがあります)でジェネリッククラスを作成する場合、ジェネリックメソッドを使用できますか(メソッドはジェネリック型パラメーターを取ります)?

次の例を考えてみましょう。

_public class MyClass {
  public <K> K doSomething(K k){
    return k;
  }
}

public class MyGenericClass<T> {
  public <K> K doSomething(K k){
    return k;
  }

  public <K> List<K> makeSingletonList(K k){
    return Collections.singletonList(k);
  }
}
_

ジェネリックメソッドで期待するように、任意のオブジェクトを使用してMyClassのインスタンスでdoSomething(K)を呼び出すことができます。

_MyClass clazz = new MyClass();
String string = clazz.doSomething("String");
Integer integer = clazz.doSomething(1);
_

ただし、ジェネリック型を指定してMyGenericClasswithoutのインスタンスを使用しようとすると、渡されたObjectに関係なく、doSomething(K)を呼び出すとKが返されます。

_MyGenericClass untyped = new MyGenericClass();
// this doesn't compile - "Incompatible types. Required: String, Found: Object"
String string = untyped.doSomething("String");
_

奇妙なことに、戻り値の型がジェネリッククラスの場合はコンパイルされます-例: _List<K>_(実際、これは説明できます-以下の回答を参照してください):

_MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String"); // this compiles
_

また、ワイルドカードのみを使用している場合でも、汎用クラスが入力されているとコンパイルされます。

_MyGenericClass<?> wildcard = new MyGenericClass();
String string = wildcard.doSomething("String"); // this compiles
_
  • 型指定されていないジェネリッククラスでジェネリックメソッドを呼び出すことが機能しない理由はありますか?

  • 私が見逃しているジェネリッククラスとジェネリックメソッドに関連する巧妙なトリックはありますか?

編集:

明確にするために、型なしまたはraw型のジェネリッククラスがジェネリッククラスの型パラメーターを尊重しないことを期待します(それらが提供されていないため)。ただし、型なしまたは生型のジェネリッククラスが、ジェネリックメソッドが尊重されないことを意味する理由は私にはわかりません。

この問題はSO、c.f。ですでに提起されていることが明らかになりました。 この質問 。これに対する答えは、クラスが型指定されていない/生の形式である場合、allジェネリックがクラスから削除されることを説明しています-ジェネリックメソッドの型指定を含みます。

しかし、なぜそうなるのかについての説明は実際にはありません。だから私の質問を明確にさせてください:

  • Javaは、型なしまたはraw型のジェネリッククラスでのジェネリックメソッドの型指定を削除するのはなぜですか?これには正当な理由がありますか、それとも単なる見落としでしたか?

編集-JLSの議論:

(前のSO質問とこの質問への回答)で、これは JLS 4.8 で扱われることが提案されています。

スーパークラスまたはスーパーインターフェイスから継承されないrawタイプCのコンストラクター(§8.8)、インスタンスメソッド(§8.4、§9.4)、または非静的フィールド(§8.3)Mのタイプは、対応するrawタイプです。 Cに対応する一般的な宣言のその型の消去に。

これが型指定されていないクラスとどのように関連しているかは明らかです。クラスのジェネリック型は消去型に置き換えられます。クラスジェネリックがバインドされている場合、消去タイプはそれらのバインドに対応します。それらがバインドされていない場合、消去タイプはオブジェクトです-例:.

_// unbound class types
public class MyGenericClass<T> {
  public T doSomething(T t) { return t; }
}
MyGenericClass untyped = new MyGenericClass();
Object t = untyped.doSomething("String");

// bound class types
public class MyBoundedGenericClass<T extends Number> {
  public T doSomething(T t) { return t; }
}
MyBoundedGenericClass bounded = new MyBoundedGenericClass();
Object t1 = bounded.doSomething("String"); // does not compile
Number t2 = bounded.doSomething(1); // does compile
_

ジェネリックメソッドはインスタンスメソッドですが、JLS4.8がジェネリックメソッドに適用されるかどうかは私にはわかりません。ジェネリックメソッドのタイプ(前の例では_<K>_)は、タイプがメソッドパラメーターによって決定されるため、型なしではありません。クラスのみが型なし/ raw型です。

29
amaidment

「下位互換性のために」は、クラスジェネリック型の型消去の十分な理由のようです-それは必要です。型指定されていないリストを返し、それをレガシーコードに渡すことができるようにします。これをジェネリックメソッドに拡張することは、トリッキーなサブケースのようです。

4.8のJLSスニペット(引用)は、コンストラクター、インスタンスメソッド、およびメンバーフィールドをカバーしています。ジェネリックメソッドは、一般的なインスタンスメソッドの特定のケースにすぎません。したがって、あなたのケースはこのスニペットでカバーされているようです。

JLS 4.8をこの特定のケースに適合させる:

ジェネリックメソッドの型は、Cに対応するジェネリック宣言の型の消去に対応する生の型です。

(ここでは、メソッドの「型」にはすべてのパラメーターと戻り値の型が含まれます)。 「消去」を「すべてのジェネリックを消去する」と解釈すると、これは観察された動作と一致しているように見えますが、あまり直感的ではなく、有用でもありません。ジェネリッククラスのパラメーターだけでなく、すべてのジェネリックを消去することは、熱心すぎる一貫性のように思えます(ただし、デザイナーを2番目に推測するのは誰ですか)。

おそらく、クラスのジェネリックパラメーターがメソッドのジェネリックパラメーターと相互作用する問題がある可能性があります-コードではそれらは完全に独立していますが、それらが割り当てられたり混合されたりする他のケースを想像することができます。 JLSのように、rawタイプの使用は推奨されないことを指摘する価値があると思います。

Raw型の使用は、レガシーコードの互換性への譲歩としてのみ許可されます。 Javaプログラミング言語にジェネリックスを導入した後に記述されたコードでraw型を使用することは、強くお勧めしません。Javaプログラミングの将来のバージョンである可能性があります。言語は生の型の使用を禁止します

Java開発者の考え方のいくつかはここで明らかです:

http://bugs.Sun.com/view_bug.do?bug_id=6400189

(この型消去の目的で、メソッドの戻り値の型がメソッドの型の一部として扱われることを示すバグと修正)

このリクエスト もあります。ここでは、誰かがあなたが説明した動作をリクエストしているように見えます-クラスのジェネリックパラメータのみを消去し、他のジェネリックは消去しません-しかし、次の理由で拒否されました:

型消去を変更して、型宣言Foo<T>で、消去によってパラメーター化された型からTのみが削除されるようにする必要があります。次に、Map<K,V>の宣言内で、Set<Map.Entry<K,V>>Set<Map.Entry>に消去されることがあります。

ただし、Map<K,V>にタイプMap<String,V>をとるメソッドがある場合、その消去はMap<String>になります。型消去で型パラメーターの数を変更することは、特にコンパイル時のメソッド解決にとっては恐ろしいことです。このリクエストは絶対に受け付けません。

ジェネリックス(Set<Map.Entry>)の型安全性の一部を取得しながら、生の型(Map)を使用できると期待するのは多すぎます。

8

Java Generics、ジェネリッククラスの生の形式を使用する場合、クラスのallジェネリック、makeSingletonListメソッドやdoSomethingメソッドなどの無関係なジェネリックメソッドでさえ、私が理解している理由は、プレジェネリックで記述されたJavaコードとの後方互換性を提供するためです。

ジェネリック型パラメーターTを使用しない場合は、それをMyGenericClassから削除し、メソッドをKでジェネリックのままにします。それ以外の場合は、クラス内の他のすべてでジェネリックスを使用するには、クラスタイプパラメーターTをクラスに指定する必要があるという事実に従わなければなりません。

4
rgettman

ジェネリックを完全に破棄する理由が1つ見つかりました(ただし、それはあまり良くありません)。理由は次のとおりです。ジェネリックは制限される可能性があります。このクラスを検討してください。

public static class MyGenericClass<T> {
    public <K extends T> K doSomething(K k){
        return k;
    }

    public <K> List<K> makeSingletonList(K k){
        return Collections.singletonList(k);
    }
}

ジェネリックなしでクラスを使用する場合、コンパイラはdoSomething内のジェネリックを破棄する必要があります。そして、私はすべてのジェネリックがこの振る舞いと一致するために捨てられると思います。

makeSingletonListは、JavaがListからList<K>へのチェックされていないキャストを行うためにコンパイルされます(ただし、コンパイラーは警告を表示します)。

2
MAnyKey

これは基本的な質問には答えませんが、makeSingletonList(...)がコンパイルされないのにdoSomething(...)がコンパイルされる理由の問題に対処します。

MyGenericClassの型指定されていない実装では:

MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String");

...と同等です:

MyGenericClass untyped = new MyGenericClass();
List list = untyped.makeSingletonList("String");
List<String> typedList = list;

これはコンパイルされますが(いくつかの警告があります)、実行時エラーが発生する可能性があります。

実際、これは次のようなものにも似ています。

MyGenericClass untyped = new MyGenericClass();
List<Integer> list = untyped.makeSingletonList("String"); // this compiles!!!
Integer a = list.get(0);

...コンパイルされますが、ClassCastException値を取得してStringにキャストしようとすると、ランタイムIntegerがスローされます。

1
amaidment

MAnyKeysの回答に関する私のコメントから:

完全型消去は、前述の後方互換性と組み合わせると理にかなっていると思います。オブジェクトがジェネリック型の引数なしで作成された場合、オブジェクトの使用は非ジェネリックコードで行われると想定できます(またはそうすべきですか?)。

このレガシーコードを検討してください。

public class MyGenericClass {
    public Object doSomething(Object k){
        return k;
    }
}

このように呼ばれます:

MyGenericClass foo = new MyGenricClass();
NotKType notKType = foo.doSomething(new NotKType());

これで、クラスとそのメソッドが汎用になりました。

public class MyGenericClass<T> {
    public <K extends KType> K doSomething(K k){
        return k;
    }
}

NotKTypeKTypeのsuptypeではないため、上記の呼び出し元コードはコンパイルされなくなります。これを回避するために、汎用タイプはObjectに置き換えられます。 (例のように)違いがない場合もありますが、コンパイラがいつ分析するかは少なくとも非常に複雑です。不可能かもしれません。

私はこのシナリオが少し構築されているように見えることを知っていますが、それは時々起こると確信しています。

0
André Stannek

この理由は、プレジェネリックコードとの下位互換性です。ジェネリック以前のコードはジェネリック引数を使用せず、代わりに今日生の型と思われるものを使用していました。ジェネリック以前のコードは、ジェネリック型を使用する参照の代わりにObject参照を使用し、生の型はすべてのジェネリック引数にタイプObjectを使用するため、コードは実際に下位互換性がありました。

例として、次のコードについて考えてみます。

List list = new ArrayList();

これはプレジェネリックコードであり、ジェネリックが導入されると、これは次と同等の生のジェネリック型として解釈されました。

List<?> list = new ArrayList<>();

なぜなら ?その後にextendsまたはsuperキーワードがない場合、次のように変換されます。

List<Object> list = new ArrayList<>();

ジェネリックスの前に使用されていたListのバージョンは、リスト要素を参照するためにObjectを使用し、このバージョンもリスト要素を参照するためにObjectを使用するため、下位互換性があります。互換性は保持されます。

0
tbodt