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本质上是不安全的。请记住,调用者决定要做什么T,而不是方法。由于getFooBar不采用与 相关的任何参数T,并且由于类型擦除,它唯一的选择是返回null或通过进行未经检查的强制转换来“撒谎”,冒着堆污染的风险。一个典型的解决方法是getFooBar使用一个Class<T>参数,或者一个FooFactory<T>例子。

更新

事实证明,当我断言调用者getFooBar必须始终知道是什么时,我错了T。正如@MiserableVariable 指出的那样,在某些情况下,泛型方法的类型参数被推断为通配符 capture,而不是具体类型或类型变量。请参阅他的答案,了解一个很好的getFooBar实现示例,该示例使用代理来驱动他T的未知点。

正如我们在评论中所讨论的,一个使用的例子getFooBar造成了混乱,因为它不需要任何参数来推断T。某些编译器在无上下文调用时会抛出错误getFooBar(),而其他编译器则可以。我认为不一致的编译错误 - 以及调用是非法的事实FooBar.<?>getFooBar()- 验证了我的观点,但事实证明这些都是红鲱鱼。

根据@MiserableVariable 的回答,我整理了一个新示例,该示例使用带有参数的泛型方法,以消除混淆。假设我们有接口FooBar一个实现FooBarImpl

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

我们还有一个简单的容器类,它包装了某种类型的实例来实现FooBar。它声明了一个愚蠢的静态方法unwrap,该方法接受 aFooBarContainer并返回其所指对象:

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——只知道它是 bounds 内的某种类型extends Foo & Bar

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

正如我所指出的,unwrap使用通配符调用是非法的:

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

我只能猜测这是因为通配符捕获永远不能相互匹配 -?调用站点提供的参数是模棱两可的,没有办法说它应该专门匹配unknownFooBarContainer.

因此,这是 OP 询问的语法的用例。调用返回类型的unwrap引用。我们可以将该引用分配给or ,但不能同时分配给两者:unknownFooBarContainer? extends Foo & BarFooBar

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

这只是一个相当模糊的用例。允许这样的语法会产生相当广泛的影响,无论好坏。它会在不需要的地方为滥用打开空间,并且完全可以理解为什么语言设计者没有实现这样的东西。但我仍然认为这很有趣。


关于堆污染的说明

主要针对@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
}

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

失败发生在方法返回之后,当Foo2实例尝试分配给foo1具有 reifiable 类型的引用时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>我们没有得到 a ,而是得到 a List<T>,将其放入 aMap<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实施Fooand Bar。在中,对和main的调用可以分配给一个变量,尽管没有充分的理由让它知道恕我直言。 getFooBar1getFooBar2

But getFooBar is the interesting case, which uses a proxy. In practice, it may be the only instance of a an object that implements the two interfaces. A different method (show here) can be used with a temporary in a type-safer manner, but it cannot be assigned to a variable without the FooBarWrapper hack described in the question. It is not even possible to create a generic wrapper, class Wrapper<T extends U & V> is not allowed.

The only trouble seems be defining a syntax, other type checking mechanisms seem to be in place, at least in Oracle javac 1.7.0.

于 2013-01-31T18:51:14.183 回答
4

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

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

案子

最近我一直在开发用户界面。在这个应用程序中,我使用一个库来管理我的 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 回答