2

真の Java Generics スペシャリストにとってはちょっとしたパズルです... ;)

次の 2 つのインターフェイスがあるとします。

interface Processor {
    void process(Foo foo);
}

interface Foo {
    Processor getProcessor();
}

たとえば、次の 2 つの実装クラスがあります。

static class SomeProcessor implements Processor {
    static final SomeProcessor INSTANCE = new SomeProcessor();

    @Override
    public void process(Foo foo) {
        if (foo instanceof SomeFoo) { // <-- GET RID OF THIS ?
            // process ((SomeFoo) foo)
        }
    }
}

class SomeFoo implements Foo {
    @Override
    public Processor getProcessor() {
        return SomeProcessor.INSTANCE;
    }
}

instanceof関数でマークされたチェックを必要とせずprocess()、コードの他の場所で次の構成が機能するように、2 つのインターフェイスをジェネリックにする方法はありますか?

foo.getProcessor().process(foo);

(もちろん、扱っている Foo のサブクラスはわかりません)

言い換えれば、関数を含むオブジェクトのタイプを処理する別のオブジェクトのみを返すことができるように、オブジェクトで関数を定義する方法を探しています。注: 関数を含むオブジェクトの最小公分母スーパークラス (上記: Foo) の処理について話しているだけではなく、そのオブジェクトの実際のクラス (上記: SomeFoo) の処理について話しているだけではありません。

(これは、私が今本当に愚かでない限り、聞こえるかもしれないほど些細なことではありません...)

4

6 に答える 6

2

ジェネリックでできることは次のとおりです。

interface Processor<T extends Foo> {
  void process(T foo);
}

interface Foo {
  Processor<? extends Foo> getProcessor();
}

それから

class SomeProcessor implements Processor<SomeFoo> {
  public void process(SomeFoo foo) {
    // TODO
  }
}

class SomeFoo implements Foo {
  public Processor<? super SomeFoo> getProcessor() {
    return processor;
  }
}

これは明らかに、ワイルドカードを使用した対応するものが必要であることを意味しますFoo.setProcessor()。これは、どこかでチェックされていないキャストになることを意味します。これは言語の観点から見て安全ではなく、これを回避する方法はありません。

スーパー タイプ トークンを使用してプロセッサのインスタンス化を確認できますが、これは実行時に行われるため、コンパイル時に API の誤用を保証することはできません。できる限り最善を尽くして文書化してください。

このパスティは私の考えを示しています。Fooメソッドが現在のインターフェイス実装でインスタンス化されたジェネリック型を返すことをインターフェイスが宣言する方法が必要になるため、この問題をモデル化してコンパイル時にタイプ セーフを実現することはできません。これは継承を壊し、Java では実行できません。

ただし、スーパー タイプ トークンを使用すると、プロセッサが適切なタイプであるかどうかを実行時に確認できます。これは、プロセッサのみがインスタンスでの呼び出しを許可されていることを API ドキュメントで明確に述べていれば、言語レベルで常に保証されます。クライアント プログラマーが従わず、不適切な型で呼び出した場合でも、クラスは実行時に例外をスローします。setProcessor()FoosetProcessor()

補遺: Foo をパラメータ化してはいけない理由

メリットンの回答(現在受け入れられている回答)が気に入らない理由と、型をパラメーター化しFooFoo<T>.

ソフトウェアの失敗: それは悪いことでも珍しいことでもありません。失敗は早ければ早いほど良いので、修正して損失を回避できます (通常はお金ですが、実際には誰かが気にする可能性のあるものなら何でも)。これは、今日 Java を選択する説得力のある理由の 1 つです。コンパイル時に型がチェックされるため、バグのクラス全体が本番環境に到達することはありません。

これがジェネリックの出番です。バグの別のクラス全体が除外されます。あなたのケースに戻ると、誰かが型パラメーターを追加してFoo、コンパイル時に型の安全性を確保できるようにすることを提案しています。ただし、メリットンの実装を使用すると、コンパイラのチェックをバイパスする非論理的なコードを書くことができます。

class SomeFoo implements Foo<WrongFoo> {}

プログラムが意味論的に正しいかどうかを判断するのはコンパイラーの仕事ではないと主張する人もいるかもしれませんが、私も同意しますが、優れた言語はコンパイラーが問題を発見し、解決策を示唆することを可能にします。ここで型パラメーターを使用することは、賢明なソフトウェアを大声で叫ぶだけです。

脆弱な慣習に頼るのではなく、座って IDE を閉じ、クライアント コードが失敗しないように設計を改善する方法を考えたほうがよいでしょう。最終的に、これがソフトウェアが失敗する主な理由です。言語が厳密にまたは動的に型付けされているからではなく、開発者が API を誤解しているからです。厳密に型指定された言語を使用することは、1 つのタイプの誤用を防ぐ方法です。

あなたのインターフェースは非常に奇妙です。なぜなら、それは を定義していますが、プロセッサの設定getProcessor()を誰が担当しているかについては何も伝えていないからです。独自のプロセッサを提供するのとまったく同じである場合、型の安全性は本当に愚かな開発者によってのみ破られます。ただし、実行時にチェックできるため (スーパー タイプ トークンを使用した私のデモを参照)、優れた開発プロセス (単体テスト) で簡単に保証できます。aまたは equalを定義しても、結論はあまり変わりません。FoosetProcessor()

あなたが探しているAPIをJavaで記述することは不可能です-ところで、親クラスが子を知らないという継承の主要なルールを破るため、すべてのオブジェクト指向言語に同じことが当てはまると思います(これは順番にポリモーフィズムをもたらします)。(デモでほのめかしたように) ワイルドカードを使用することは、Java ジェネリックを使用できる最も近い方法であり、タイプ セーフを提供し、理解しやすいものです。

タイプ セーフティの名の下に Java の規則を強制するのではなく、プロセッサに型パラメーターを追加するだけで、適切なドキュメントと単体テストを作成することをお勧めします。

于 2012-11-29T23:12:28.590 に答える
2

型の安全性の観点からは、再帰的な境界はすべて必要ありません。次のようなものだけで十分です。

interface Processor<F> {
    void process(F foo);
}

interface Foo<F> {
    Processor<F> getProcessor();
}

class SomeFoo implements Foo<SomeFoo> {
    @Override
    SomeProcessor getProcessor() { ... }
}

class SomeProcessor implements Processor<SomeFoo> {
    @Override
    void process(SomeFoo foo) { ... }
}

// in some other class:
<F extends Foo<? super F>> void process(F foo) {
    foo.getProcessor().process(foo);
}

言い換えれば、関数を含むオブジェクトのタイプを処理する別のオブジェクトのみを返すことができるように、オブジェクトで関数を定義する方法を探しています。注: 関数を含むオブジェクトの最小公分母スーパークラス (上記: Foo) の処理について話しているだけではなく、そのオブジェクトの実際のクラス (上記: SomeFoo) の処理について話しているだけではありません。

Javaで宣言することはできません。代わりに、上記のように、オブジェクトとプロセッサの両方を取り、両方に型を適用するクラス外のジェネリック メソッド (またはそのクラスの静的メソッド) を持つことができます。


process更新: anyで呼び出すことができるようにしたい場合はFoo、メリットンのアイデアgetThis()をこのコードに統合することもできます (繰り返しになりますが、再帰的な境界はありません)。

interface Foo<F> {
    Processor<F> getProcessor();
    F getThis();
}

class SomeFoo implements Foo<SomeFoo> {
    SomeProcessor getProcessor() { ... }
    SomeFoo getThis() { return this; }
}


// in some other class:
<F> void process(Foo<F> foo) {
    foo.getProcessor().process(foo.getThis());
}
于 2012-11-30T10:27:55.217 に答える
1

これは思ったより醜いです。私の見解:

interface Processor<F extends Foo<F>> {
    void process(F foo);
}

interface Foo<F extends Foo<F>> {
    Processor<F> getProcessor();
}

interface SomeFoo extends Foo<SomeFoo> {
    @Override
    SomeProcessor getProcessor();
}

interface SomeProcessor extends Processor<SomeFoo> {
    @Override
    void process(SomeFoo foo);
}

これで、以下がコンパイルされます。

<F extends Foo<F>> void process(F foo) {
    foo.getProcessor().process(foo);
}

しかし

void process(Foo<?> foo) {
    foo.getProcessor().process(foo);
}

誰かが書くことができるように、コンパイラは渡されたfooの実際の型がその型パラメータのサブタイプであることを知ることができないので、そうではありません。

    class Bar implements Foo<SomeFoo> { ... }

これを回避するには、fooのサブタイプにタイプパラメーターへの変換を実装するように要求します。

abstract class Foo<F extends Foo<F>> {
    abstract Processor<F> getProcessor();

    abstract F getThis();
}

class SomeFoo extends Foo<SomeFoo> {
    @Override
    SomeFoo getThis() {
        return this;
    }

    @Override
    Processor<SomeFoo> getProcessor() {
        return new SomeProcessor();
    }
}

今、私たちは書くことができます:

<F extends Foo<F>> void process(Foo<F> foo) {
    foo.getProcessor().process(foo.getThis());
}

でこれを呼び出します

Foo<?> foo = ...;
process(foo);

使いやすくするために、ヘルパーメソッドをクラスFooに移動することをお勧めします。

abstract class Foo<F extends Foo<F>> {
    abstract Processor<F> getProcessor();

    abstract F getThis();

    void processWith(Processor<F> p) {
        p.process(getThis());
    }
}

更新:newacctsの更新された回答は、再帰型の境界を必要としないため、より洗練されたソリューションを示していると思います。

于 2012-11-30T00:05:47.777 に答える
1
interface Processor<F extends Foo<F>> {
    void process(F fooSubclass);
}

interface Foo<F extends Foo<F>> {
    Processor<F> getProcessor();
}

これが正確に正しいかどうかはテストしていませんが、ジェネリック型がそれ自体を参照するこのパターンは、おそらくコンパイル時の Java ジェネリックとほぼ同じです。

于 2012-11-29T23:10:29.730 に答える
1

これは完全に「ジェネリック化された」コードですが、わずかな変更が 1 つINSTANCEあります。変数は静的ではありません。

interface Processor<T extends Foo<T>> {
    void process(T foo);
}

interface Foo<T extends Foo<T>> {
    Processor<T> getProcessor();
}

static class SomeProcessor<T extends Foo<T>> implements Processor<T> {

    final SomeProcessor<T> INSTANCE = new SomeProcessor<T>();

    @Override
    public void process(T foo) {
        // it will only ever be a SomeFoo if T is SomeFoo
    }
}

class SomeFoo implements Foo<SomeFoo> {
    @Override
    public Processor<SomeFoo> getProcessor() {
        return new SomeProcessor<SomeFoo>().INSTANCE;
    }
}

コンパイラ エラーや警告はありません。

INSTANCEクラス型は静的なものにはならないため、インスタンス変数になりました。本当に 1 つだけが必要な場合はINSTANCE、クラスでシングルトン パターンを使用します。

于 2012-11-30T00:09:36.517 に答える
1

いいえ、それは Java Generics が行うように設計されたものではありません。

また、私の意見では、型チェックの必要性は設計上の問題を示しています。これが不要になるように再設計することをお勧めします。

于 2012-11-29T23:04:17.497 に答える