如何引用具有多个边界的泛型返回类型

问题描述 投票:36回答:4

我最近看到可以声明一个也受接口限制的返回类型。考虑以下类和接口:

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真的发明了这么好的功能,但是忘记了想要引用它吗?

java generics interface multiple-inheritance
4个回答
30
投票

虽然泛型方法的类型参数可以通过边界限制,例如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本质上是不安全的。请记住,调用者决定T是什么,而不是方法。由于getFooBar没有采取任何与T相关的参数,并且由于type erasure,它唯一的选择是返回null或通过制作一个未经检查的演员来“谎言”,冒险heap pollution。一个典型的解决方法是getFooBar采取Class<T>论证,或者例如FooFactory<T>

更新

当我断言getFooBar的调用者必须总是知道T是什么时,我发现我错了。正如@MiserableVariable指出的那样,在某些情况下,泛型方法的type参数被推断为通配符捕获,而不是具体的类型或类型变量。有关his answer实现的一个很好的例子,请参阅getFooBar,该实现使用代理来推动他的观点,即T未知。

正如我们在评论中所讨论的那样,an example using getFooBar造成了混乱,因为从中推断T没有任何论据。某些编译器throw an errorgetFooBar()的无环境调用,而其他人are fine with it。我认为不一致的编译错误 - 以及调用FooBar.<?>getFooBar()非法的事实 - 验证了我的观点,但这些结果证明是红色的鲱鱼。

根据@ MiserableVariable的答案,我将使用泛型方法和参数的an new example放在一起,以消除混淆。假设我们有接口FooBar以及一个实现FooBarImpl

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

我们还有一个简单的容器类,它包含了一个实现FooBar的某种类型的实例。它声明了一个愚蠢的静态方法unwrap,它接受一个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询问的语法的用例。在unwrap上调用unknownFooBarContainer会返回? extends Foo & Bar类型的引用。我们可以将该引用分配给FooBar,但不能同时分配两者:

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

这只是一个相当模糊的用例。对于允许这样的语法,包括好的和坏的,都会有非常广泛的影响。它会在不需要的地方打开滥用的空间,并且完全可以理解为什么语言设计者没有实现这样的事情。但我仍然认为考虑这个问题很有意思。


关于堆污染的说明

Mostly for @MiserableVariable)以下是getFooBar等不安全方法如何导致堆污染及其影响的演练。给出以下接口和实现:

interface Foo { }

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

static class Foo2 implements Foo { }

让我们实现一个不安全的方法getFoo,类似于getFooBar,但为此示例简化:

@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
}

在这里,当新的Foo2被强制转换为T时,它是“未经检查的”,这意味着由于类型擦除,运行时不知道它应该失败,即使它应该在这种情况下因为TFoo1。相反,堆被“污染”,这意味着引用指向它们不应该被允许的对象。

失败发生在方法返回之后,当Foo2实例尝试分配给foo1引用时,Foo1引用具有可重新类型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>>,把它放在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()); } } 并将其返回到另一种方法。你得到了我希望的想法。


8
投票

在某些情况下,调用者可以在不知道具体类型的情况下使用返回值的被调用方法。甚至可能根本不存在这样的类型,它只是一个代理:

FooBar1

FooBar2Foo都实施Barmain。在getFooBar1中,对getFooBar2getFooBar的调用可以分配给一个变量,尽管没有强有力的理由让它知道恕我直言。

show是一个有趣的案例,它使用代理。实际上,它可能是实现两个接口的对象的唯一实例。一种不同的方法(这里是FooBarWrapper)可以与类型更安全的方式使用临时,但如果没有问题中描述的class Wrapper<T extends U & V> hack,则无法将其分配给变量。甚至不可能创建通用包装器,不允许使用public interface CoordinateView extends View { Coordinate getCoordinate(); //Maybe more stuff }

唯一的麻烦似乎是定义语法,其他类型检查机制似乎已到位,至少在Oracle javac 1.7.0中。


4
投票

就像@Paul Bellora在他的回答中提到的那样,类型由调用者解决,因为它现在基本上它将调用它。我想在一个用例中添加他的答案,我认为语法的使用可能是有益的。

总有一些替代方法可以避免使用这种语法。我想不出一个完全必要的例子。但是,我可以想到一个特定情况的用例,这种语法可以方便地使用,虽然我自己甚至没有使用它。我知道它不是最好的例子,但它可以达到目的。

案件

最近我一直在开发用户界面。在这个应用程序中,我使用库来管理我的GUI元素。除了库的功能之外,我还创建了一个自定义界面,在我的应用程序中定义了一个View,它具有特定类型数据的输入,比如输入坐标。该界面看起来像:

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

我的应用程序中有几个实现此接口的窗口。现在让我们说,由于某种原因,我想在模型中存储在窗口中提交的最后一个坐标,然后关闭窗口。为此,我可以将一个处理程序附加到提交表单的窗口按钮,当用户关闭Window时,处理程序将被触发。我可以通过在每个窗口中匿名添加处理程序来实现这一点,例如:

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

但是,这种设计对我来说并不理想,它不够模块化。考虑到我有相当数量的窗口具有这种行为,改变它可能会变得相当乏味。所以我宁愿在类中提取匿名方法,以便更容易更改和维护。但问题是destroy()方法没有在任何接口中定义,只是窗口的一部分,而getCoordinate()方法是在我定义的接口中定义的。

用法

在这种情况下,我可以使用多个边界,如下所示:

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

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

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

    }
}

那么windows中的代码现在将是:

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

让窗口实现这个更具体的接口,而不是在提取的控制器中不必要地使用泛型参数:

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

对于某些人来说,这种方法可以被视为比以前更清晰,甚至更可重复使用,因为现在可以将其添加到指定层次结构之外的其他“窗口”。就个人而言,我也更喜欢这种方法。然而,它可能导致更多的编码,因为必须仅为了获得对期望方法的访问而定义新接口。

总而言之,虽然我个人不推荐它,但我认为使用具有多个边界的泛型类型可以帮助耦合定义,同时减少代码量。


-2
投票

不确定Eclipse为你做了什么,但上面的大部分代码都没有接近编译....

我做了相应的更改以尽可能地编译它,这是我得到的:

qazxswpoi
© www.soinside.com 2019 - 2024. All rights reserved.