web-dev-qa-db-ja.com

一般的なパラメータ:ダイヤモンド演算子のみが機能するようです

背景:質問は この回答 (正確には、回答の最初の改訂版)で出てきました。この質問で提示されたコードは、問題を説明するために最小限に抑えられています。

次のコードがあるとします。

_public class Sample<T extends Sample<T>> {

    public static Sample<? extends Sample<?>> get() {
        return new Sample<>();
    }

    public static void main(String... args) {
        Sample<? extends Sample<?>> sample = Sample.get();
    }
}
_

警告なしにコンパイルされ、正常に実行されます。ただし、return new Sample<>();で推定タイプのget()を何らかの方法で定義しようとすると、コンパイラは文句を言います。

これまで、ダイヤモンド演算子は明示的な型を記述しないための単なる構文糖衣であり、したがって常に何らかの明示的な型に置き換えることができるという印象を受けていました。与えられた例では、コードをコンパイルするための戻り値の明示的な型を定義できませんでした。戻り値のジェネリック型を明示的に定義することは可能ですか、それともこの場合はダイヤモンド演算子が必要ですか?

以下は、対応するコンパイラエラーで戻り値のジェネリック型を明示的に定義するために行ったいくつかの試みです。


_return new Sample<Sample>_の結果:

_Sample.Java:6: error: type argument Sample is not within bounds of type-variable T
            return new Sample<Sample>();
                              ^
  where T is a type-variable:
    T extends Sample<T> declared in class Sample
Sample.Java:6: error: incompatible types: Sample<Sample> cannot be converted to Sample<? extends Sample<?>>
            return new Sample<Sample>();
                   ^
_

_return new Sample<Sample<?>>_の結果:

_Sample.Java:6: error: type argument Sample<?> is not within bounds of type-variable T
            return new Sample<Sample<?>>();
                                    ^
  where T is a type-variable:
    T extends Sample<T> declared in class Sample
_

return new Sample<Sample<>>();の結果:

_Sample.Java:6: error: illegal start of type
           return new Sample<Sample<>>();
                                    ^
_
16
Turing85

[〜#〜] jls [〜#〜] は単に次のように述べています。

クラスの型引数リストが空の場合—ひし形の形式<> —クラスの型引数は推論されます

それで、解を満足させるいくつかの推測されたXはありますか? はい

もちろん、そのようなX明示的に定義するには、次のように宣言する必要があります。

public static <X extends Sample<X>> Sample<? extends Sample<?>> get() {
    return new Sample<X>();
}

明示的なSample<X>は戻り値の型Sample<? extends Sample<?>>と互換性があるため、コンパイラーは満足しています。

戻り値の型がめちゃくちゃになっているという事実Sample<? extends Sample<?>>はまったく別の話です。

11
Andreas

ワイルドカードを使用したジェネリックのインスタンス化

ここにはいくつかの問題がありますが、それらを掘り下げる前に、実際の質問に答えさせてください。

戻り値のジェネリック型を明示的に定義することは可能ですか、それともこの場合はダイヤモンド演算子が必要ですか?

_Sample<? extends Sample<?>>_(またはそのことについては_Sample<?>_)を明示的にインスタンス化することはできません。ワイルドカード禁止ジェネリック型をインスタンス化するときに型引数として使用されますが、型引数内にネストされる場合があります。たとえば、_ArrayList<Sample<?>>_をインスタンス化することは合法ですが、cannot _ArrayList<?>_をインスタンス化することはできません。

最も明白な回避策は、 _Sample<?>_に割り当て可能な他の具象型を返すことです。例えば:

_class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}

    public static Sample<? extends Sample<?>> get() {
        return new X();
    }
}
_

ただし、ワイルドカードを含む_Sample<>_クラスのジェネリックインスタンス化を具体的に返したい場合は、ジェネリック推論に依存して型引数を計算する必要があります。これを行うにはいくつかの方法がありますが、通常には次のいずれかが含まれます。

  1. あなたが今しているように、ダイヤモンド演算子を使用します。
  2. 型変数を使用してワイルドカードをキャプチャするジェネリックメソッドに委任します。

ジェネリックインスタンス化にワイルドカードを直接含めることはできませんが、型変数を含めることは完全に合法であり、それがオプション(2)を可能にします。デリゲートメソッドの型変数が呼び出しサイトでワイルドカードにバインドされていることを確認するだけです。型変数についての言及はすべて、メソッドのシグネチャと本体がそのワイルドカードへの参照に置き換えられます。例えば:

_public class Sample<T extends Sample<T>> {
    public static Sample<? extends Sample<?>> get() {
        final Sample<?> s = get0();
        return s;
    }

    private static <T extends Sample<T>> Sample<T> get0() {
        return new Sample<T>();
    }
}
_

ここで、Sample<T> get0()の戻り値の型は_Sample<WC#1 extends Sample<WC#1>>_に展開されます。ここで、_WC#1_は、Sample<?> s = get0()の割り当てターゲットから推測されたワイルドカードのキャプチャされたコピーを表します。

タイプ署名の複数のワイルドカード

それでは、あなたのメソッドシグネチャについて説明しましょう。提供したコードに基づいて確実に判断するのは難しいですが、_Sample<? extends Sample<?>>_の戻り値の型は* not *であると推測します。 =本当に欲しいもの。ワイルドカードがタイプに表示される場合、各ワイルドカードは他のすべてとは異なりますです。最初のワイルドカードと2番目のワイルドカードが同じタイプを参照するという強制はありません。

get()がタイプXの値を返すとしましょう。 Xが_Sample<X>_を拡張することを確認することが意図されていた場合は、失敗しています。考えてみましょう:

_class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}
    static class Y extends Sample<X> {}

    public static Sample<? extends Sample<?>> get() {
        return new Y();
    }

    public static void main(String... args) {
        Sample<?> s = Sample.get(); // legal (!)
    }
}
_

mainでは、変数sは_Sample<X>_およびYである値を保持しますが、not a _Sample<Y>_です。それはあなたが意図したことですか?そうでない場合は、メソッドシグネチャのワイルドカードを型変数に置き換えてから、呼び出し元に型引数を決定させることをお勧めします。

_class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}
    static class Y extends Sample<X> {}

    public static <T extends Sample<T>> Sample<T> get() { /* ... */ }

    public static void main(String... args) {
        Sample<X> x = Sample.get();     // legal
        Sample<Y> y = Sample.get();     // NOT legal

        Sample<?> ww = Sample.get();    // legal
        Sample<?> wx = Sample.<X>get(); // legal
        Sample<?> wy = Sample.<Y>get(); // NOT legal
    }
}
_

上記のバージョンは、タイプAの一部の戻り値について、戻り値が_Sample<A>_を拡張することを効果的に保証します。理論的には、Tがワイルドカードにバインドされている場合でも機能します。どうして? ワイルドカードキャプチャに戻ります:

元のgetメソッドでは、2つのワイルドカードが異なるタイプを参照することになります。実際、戻りの種類は_Sample<WC#1 extends Sample<WC#2>_でした。ここで、_WC#1_と_WC#2_は、まったく関係のない別個のワイルドカードです。ただし、上記の例では、Tをワイルドカード captures にバインドして、同じワイルドカードを複数の場所に表示できるようにします。したがって、Tがワイルドカード_WC#1_にバインドされている場合、戻り値の型は_Sample<WC#1 extends Sample<WC#1>_に展開されます。その型をJavaで直接表現する方法はないことを忘れないでください。それは、型推論に依存することによってのみ行うことができます。

さて、これはワイルドカード理論上で機能すると言いました。実際には、一般的な制約が実行時に強制可能になるような方法でgetを実装することはおそらくできないでしょう。これは、型消去が原因です。コンパイラはclasscast命令を発行して、戻り値がたとえばXSampleの両方であることを確認できますが、できませんSampleのすべての汎用形式は同じランタイムタイプを持っているため、実際には_Sample<X>_であることを確認してください。具体的な型引数の場合、コンパイラーは通常、疑わしいコードのコンパイルを防ぐことができますが、ワイルドカードを組み合わせて使用​​すると、複雑なジェネリック制約を適用することが困難または不可能になります。バイヤーは注意してください:)。


さておき

これらすべてが混乱している場合でも、心配しないでください。ワイルドカードとワイルドカードキャプチャは、Javaジェネリックを理解するのが最も難しい側面の1つです。これらを理解することが実際に役立つかどうかも、明確ではありません。あなたの当面の目標。APIを念頭に置いている場合は、それをコードレビュースタックエクスチェンジに送信して、どのようなフィードバックが得られるかを確認するのが最善の場合があります。

8
Mike Strobel