43

私は最近、インターフェイスによってもバインドされた戻り値の型を宣言できることを見てきました。次のクラスとインターフェースを検討してください。

public class Foo {
    public String getFoo() { ... }
}

public interface Bar {
    public void setBar(String bar);
}

次のような戻り値の型を宣言できます。

public class FooBar {
    public static <T extends Foo & Bar> T getFooBar() {
        //some implementation that returns a Foo object,
        //which is forced to implement Bar
    }
}

そのメソッドをどこかから呼び出すと、IDE は、戻り値の型にメソッドString getFoo()とがあることを伝えますが、次のように関数setBar(String)の後ろにドットを指定した場合のみです。

FooBar.getFooBar(). // here the IDE is showing the available methods.

そのようなオブジェクトへの参照を取得する方法はありますか? つまり、次のようなことをするとします。

//bar only has the method setBar(String)
Bar bar = FooBar.getFooBar();
//foo only has the getFoo():String method
Foo foo = FooBar.getFooBar();

このような参照が必要です(疑似コード):

<T extents Foo & Bar> fooBar = FooBar.getFooBar();
//or maybe
$1Bar bar = FooBar.getFooBar();
//or else maybe
Foo&Bar bar = FooBar.getFooBar();

これはJavaで何とか可能ですか、またはこのような戻り値の型しか宣言できませんか? どういうわけか、Javaもそれを入力する必要があると思います。不正行為のように感じるので、このようなラッパーに頼らないことをお勧めします:

public class FooBarWrapper<T extends Foo&Bar> extends Foo implements Bar {
    public T foobar;

    public TestClass(T val){
        foobar = val;
    }


    @Override
    public void setBar(String bar) {
        foobar.setBar(bar);
    }

    @Override
    public String getFoo() {
        return foobar.getFoo();
    }
}

Java は本当にこのような優れた機能を発明したのに、それを参照したいということを忘れてしまったのでしょうか?

4

4 に答える 4

34

ジェネリック メソッドの型パラメーターは、 などの境界によって制限できますが、extends Foo & Bar最終的には呼び出し元によって決定されます。を呼び出すgetFooBar()と、呼び出しサイトは既に何Tに解決されているかを認識しています。多くの場合、これらの型パラメーターはコンパイラーによって推論されるため、通常は次のように指定する必要はありません。

FooBar.<FooAndBar>getFooBar();

しかし、Tが であると推測される場合でもFooAndBar、それは実際に舞台裏で起こっていることです。

したがって、あなたの質問に答えるには、次のような構文です。

Foo&Bar bothFooAndBar = FooBar.getFooBar();

実際には決して役に立ちません。その理由は、呼び出し元がすでに何が何でTあるかを知っている必要があるためです。いずれTかの具体的なタイプです:

FooAndBar bothFooAndBar = FooBar.<FooAndBar>getFooBar(); // T is FooAndBar

または、T未解決の型パラメーターであり、そのスコープ内にいます。

<U extends Foo & Bar> void someGenericMethod() {
    U bothFooAndBar = FooBar.<U>getFooBar(); // T is U
}

その別の例:

class SomeGenericClass<V extends Foo & Bar> {
    void someMethod() {
        V bothFooAndBar = FooBar.<V>getFooBar(); // T is V
    }
}

技術的には、これで答えは終わりです。getFooBarしかし、あなたのサンプルメソッドは本質的に安全ではないことも指摘したいと思います. メソッドではなく、呼び出し元が何になるかを決定することを忘れないでくださいTgetFooBarは に関連するパラメータをとらないためTタイプが eraseであるため、ヒープ汚染の危険を冒して、未チェックのキャストを作成することによって戻るnullか、「嘘をつく」しかありません。典型的な回避策は、たとえば、引数を取ることです。getFooBarClass<T>FooFactory<T>

アップデート

getFooBarの呼び出し元は常に何が何であるかを知っている必要があると主張したとき、私は間違っていたことがわかりましたT。@MiserableVariable が指摘するように、ジェネリック メソッドの型引数が具体的な型または型変数ではなく、ワイルドカード キャプチャであると推論される状況がいくつかあります。プロキシを使用して不明な点を強調する実装の優れた例については、彼の回答を参照してください。getFooBarT

コメントで説明したように、を使用した例getFooBarでは、推論に引数が必要ないため、混乱が生じましたT。特定のコンパイラはコンテキストレス呼び出しでエラーをスローしますgetFooBar()が、他のコンパイラはそれで問題ありません。一貫性のないコンパイル エラー (および呼び出しが違法であるという事実) が私の主張を裏付けていると思いましFooBar.<?>getFooBar()たが、これらは赤ニシンであることが判明しました。

@MiserableVariable の回答に基づいて、混乱を取り除くために、引数付きのジェネリック メソッドを使用する新しい例をまとめました。インターフェイスFooBar実装があるとしますFooBarImpl

interface Foo { }
interface Bar { }
static class FooBarImpl implements Foo, Bar { }

Fooまた、 and を実装する何らかの型のインスタンスをラップする単純なコンテナ クラスもありますBarunwrapを受け取り、FooBarContainerその指示対象を返すばかげた静的メソッドを宣言します。

static class FooBarContainer<T extends Foo & Bar> {

    private final T fooBar;

    public FooBarContainer(T fooBar) {
        this.fooBar = fooBar;
    }

    public T get() {
        return fooBar;
    }

    static <T extends Foo & Bar> T unwrap(FooBarContainer<T> fooBarContainer) {
        return fooBarContainer.get();
    }
}

ここで、次のワイルドカード パラメータ化タイプがあるとしFooBarContainerます。

FooBarContainer<?> unknownFooBarContainer = ...;

に渡すことができunknownFooBarContainerますunwrap。これは、呼び出しサイトが何であるかを知らないため、以前の主張が間違っていたことを示していますT- それが境界内の何らかのタイプであることだけですextends Foo & Bar

FooBarContainer.unwrap(unknownFooBarContainer); // T is a wildcard capture, ?

前述したようunwrapに、ワイルドカードを使用した呼び出しは違法です。

FooBarContainer.<?>unwrap(unknownFooBarContainer); // compiler error

これは、ワイルドカード キャプチャが互いに一致しないためであると推測することしかできません?。呼び出しサイトで提供される引数はあいまいであり、型のワイルドカードと具体的に一致する必要があるとは言えませんunknownFooBarContainer

したがって、OPが求めている構文のユースケースは次のとおりです。unwrapon を呼び出すとunknownFooBarContainer、 type の参照が返されます? extends Foo & Bar。その参照をFooorBarに代入できますが、両方には代入できません:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = FooBarContainer.unwrap(unknownFooBarContainer);

何らかの理由unwrapでコストが高く、一度だけ呼び出したい場合は、次のようにキャストする必要があります。

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = (Bar)foo;

したがって、これが仮想構文が役立つ場所です。

Foo&Bar fooBar = FooBarContainer.unwrap(unknownFooBarContainer);

これは、かなりあいまいな使用例の 1 つにすぎません。このような構文を許可することには、良い面と悪い面の両方で、かなり広範囲にわたる影響があります。必要のないところに悪用の余地が生まれることになり、言語設計者がそのようなものを実装しなかった理由は完全に理解できます。でも、考えてみると面白いと思います。


ヒープ汚染に関する注意

(主に @MiserableVariable 用) のような安全でないメソッドgetFooBarがヒープ汚染を引き起こす方法とその影響についてのウォークスルーです。次のインターフェースと実装があるとします。

interface Foo { }

static class Foo1 implements Foo {
    public void foo1Method() { }
}

static class Foo2 implements Foo { }

getFooに似てgetFooBarいますが、この例では簡略化された unsafe method を実装しましょう。

@SuppressWarnings("unchecked")
static <T extends Foo> T getFoo() {
    //unchecked cast - ClassCastException is not thrown here if T is wrong
    return (T)new Foo2();
}

public static void main(String[] args) {
    Foo1 foo1 = getFoo(); //ClassCastException is thrown here
}

ここで、 newFoo2が にキャストされるとT、それは「未チェック」になります。つまり、型消去のために、この場合はTwasであるため失敗する必要があるにもかかわらず、ランタイムは失敗する必要があることを認識しませんFoo1。代わりに、ヒープは「汚染」されています。つまり、参照が許可されるべきではないオブジェクトを指しています。

インスタンスが reifiable type を持つ参照にFoo2割り当てられようとすると、メソッドが返された後に失敗が発生します。foo1Foo1

おそらく、「よし、メソッドではなく呼び出しサイトで爆発した、大したことだ」と考えているでしょう。しかし、より多くのジェネリックが関与すると、簡単に複雑になる可能性があります。例えば:

static <T extends Foo> List<T> getFooList(int size) {
    List<T> fooList = new ArrayList<T>(size);
    for (int i = 0; i < size; i++) {
        T foo = getFoo();
        fooList.add(foo);
    }
    return fooList;
}

public static void main(String[] args) {

    List<Foo1> foo1List = getFooList(5);

    // a bunch of things happen

    //sometime later maybe, depending on state
    foo1List.get(0).foo1Method(); //ClassCastException is thrown here
}

これで、コール サイトで爆発しなくなりました。中身が慣れてくると爆破しfoo1Listます。これは、例外スタック トレースが実際の問題を示していないため、ヒープ汚染のデバッグが難しくなっている理由です。

呼び出し元がジェネリック スコープ自体にある場合は、さらに複雑になります。を取得する代わりに を取得し、それを に入れ、さらに別のメソッドに返すことを想像してみてくださいList<Foo1>。あなたは私が望む考えを得る。List<T>Map<K, List<T>>

于 2013-01-22T22:58:31.033 に答える
8

値を返す呼び出されたメソッドを呼び出し元が具体的な型を知らなくても使用できる場合があります。そのようなタイプはまったく存在しない可能性があります。これは単なるプロキシです。

import java.lang.reflect.*;

interface Foo {}
interface Bar {}

class FooBar1 implements Foo, Bar {public String toString() { return "FooBar1"; }}
class FooBar2 implements Foo, Bar {public String toString() { return "FooBar2"; }}   

class FooBar {
    static <T extends Foo & Bar> T getFooBar1() { return (T) new FooBar1(); }
    static <T extends Foo & Bar> T getFooBar2() { return (T) new FooBar2(); }
    static <T extends Foo & Bar> T getFooBar() { 
        return (T) 
        Proxy.newProxyInstance(
            Foo.class.getClassLoader(),
            new Class[] { Foo.class, Bar.class },
            new InvocationHandler() {
                public Object invoke(Object proxy, Method method, Object[] args) {
                    return "PROXY!!!";}});
    }

    static <U extends Foo & Bar> void show(U u) { System.out.println(u); }

    public static void main(String[] args) {
        show(getFooBar1());
        show(getFooBar2());
        show(getFooBar());      
    }

}

との両方FooBar1FooBar2実装FooBarます。では、およびmainの呼び出しを変数に割り当てることができますが、私見を知る強い理由はありません。 getFooBar1getFooBar2

しかしgetFooBar、プロキシを使用する興味深いケースです。実際には、2 つのインターフェイスを実装するオブジェクトの唯一のインスタンスである可能性があります。タイプセーフな方法で一時的に別のメソッド( here )を使用できますが、質問で説明されているハックshowなしでは変数に割り当てることはできません。FooBarWrapper一般的なラッパーを作成することさえできず、class Wrapper<T extends U & V>許可されていません。

唯一の問題は構文の定義のようです。少なくとも Oracle javac 1.7.0 では、他の型チェック メカニズムが導入されているようです。

于 2013-01-31T18:51:14.183 に答える
4

@Paul Bellora が彼の回答で述べたように、本質的にそれが呼び出しているものになるため、型は呼び出し元によって解決されます。構文の使用が役立つと思われるユースケースを彼の回答に追加したいと思います。

そのような構文の使用を避ける代替手段は常にあります。これが絶対に必要であるという例は、私には 1 つも思いつきません。しかし、私自身は使用したことはありませんが、この構文を便利に使用できる特定の状況の使用例を考えることができます。私はそれが最善の例ではないことを知っていますが、要点をつかむことができます.

場合

最近はユーザーインターフェースの開発に携わっています。このアプリケーションでは、ライブラリを使用して GUI 要素を管理します。ライブラリの機能に加えて、特定の種類のデータ (座標の入力など) の入力を持つアプリケーションのビューを定義するカスタム インターフェイスを作成しました。そのインターフェースは次のようになります。

public interface CoordinateView extends View
{
    Coordinate getCoordinate();
    //Maybe more stuff
} 


このインターフェイスを実装するアプリケーション全体にいくつかのウィンドウがあります。何らかの理由で、ウィンドウに送信された最後の座標をモデルに保存し、その直後にウィンドウを閉じたいとしましょう。このために、フォームを送信するウィンドウ ボタンにハンドラーをアタッチできます。ユーザーがウィンドウを閉じると、ハンドラーがトリガーされます。次のように、すべてのウィンドウにハンドラーを匿名で追加するだけでそれを実現できます。

public MyWindow extends Window implements CoordinateView, OtherInterface
{
    private Button submitButton;

    public MyWindow()
    {
        super();
        //Create all the elements

        submitButton.addClickHandler(
            new ClickHandler()
            {
                @Override
                onCLick(ClickEvent e)
                {
                    getModel().add(getCoordinate());
                    destroy();
                }
            });  
   }
}

ただし、この設計は私には望ましくありません。十分にモジュール化されていません。この動作をするかなりの量のウィンドウがあることを考えると、それを変更するのはかなり面倒になる可能性があります。そのため、クラス内の匿名メソッドを抽出して、変更と保守が容易になるようにしています。しかし問題は、destroy() メソッドがどのインターフェースにも定義されておらず、ウィンドウの一部にすぎず、定義したインターフェースで getCoordinate() メソッドが定義されていることです。

使用法

この場合、次のように複数の境界を使用できます。

public class MyController <T extends Window & CoordinateView> implements ClickHandler
{
    private T windowWithCoordinates;

    public MyController (T window)
    {
        windowWithCoordinates = window;
    }

    @Override
    onClick(ClickEvent e)
    {
        getModel().add(windowWithCoordinates.getCoordinate());
        windowWithCoordinate.destroy();
    }
}

次に、ウィンドウ内のコードは次のようになります。

public MyWindow extends Window implements CoordinateView, OtherInterface
{
    private Button submitButton;

    public MyWindow()
    {
        super();
        //Create all the elements

        submitButton.addClickHandler(new MyController<MyWindow>(this));

    }
}

動作は同じままであることに注意してください。コードは以前と同じようにまとまりがあります。よりモジュール化されているだけですが、適切に抽出できるようにするために追加のインターフェイスを作成する必要はありませんでした。

または、拡張する追加のインターフェイスCoordinateViewを定義し、ウィンドウを閉じるメソッドを定義することもできます。

public interface CoordinateWindow extends CoordinateView
{
    void destroy();
}

抽出されたコントローラーで一般的なパラメーターを不必要に使用する代わりに、ウィンドウにこのより具体的なインターフェイスを実装させます。

public class MyController implements ClickHandler
{
    private CoordinateWindow windowWithCoordinates;

    public MyController (CoordinateWindow window)
    {
        windowWithCoordinates = window;
    }

    @Override
    onClick(ClickEvent e)
    {
        getModel().add(windowWithCoordinates.getCoordinate());
        windowWithCoordinate.destroy();
    }
}


public MyWindow extends Window implements CoordinateWindow
{
    private Button submitButton;

    public MyWindow()
    {
        super();
        //Create all the elements  
        submitButton.addClickHandler(new MyController(this));                  
    }

    @Override
    void destroy()
    {
        this.destroy();
    }
}

このアプローチは、指定された階層外の他の「ウィンドウ」に追加できるようになったため、以前のアプローチよりもはるかにクリーンで、さらに再利用可能であると見なすことができます。個人的には、私もこのアプローチを好みます。ただし、目的のメソッドにアクセスするためだけに新しいインターフェイスを定義する必要があるため、コーディングが少し増える可能性があります。

結論として、個人的にはお勧めしませんが、複数の境界を持つジェネリック型を使用すると、コードの量を減らしながら定義を結合するのに役立つと思います。

于 2013-01-30T18:28:12.187 に答える
-2

Eclipse が何をしているのかはわかりませんが、上記のコードのほとんどはコンパイルに近づきません....

可能な限りコンパイルできるように適切な変更を加えたところ、次のようになりました。

public class Foo
{
  public String getFoo() { return ""; }  // must have a body
}
public interface Bar // no ()
{
  public void setBar(String bar);
}
public class FooBar<T>
{
  public static <T extends Foo & Bar> T getFooBar()
  {
    return null;
  }
}
public class FB
{
  private FooBar<Object> fb = new FooBar<Object>();

  public static void main(String args[])
  {
    new FB();
  }

  public FB()
  {
    System.out.println(fb.getFooBar());
  }
}

FB.java:12: type parameters of <T>T cannot be determined; no unique maximal instance exists for type variable T with upper bounds java.lang.Object,Foo,Bar
    System.out.println(fb.getFooBar());
                                   ^
1 error
于 2013-01-22T17:30:53.283 に答える