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

Raf*_*l T 38 java generics interface multiple-inheritance

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

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

public interface Bar {
    public void setBar(String bar);
}
Run Code Online (Sandbox Code Playgroud)

我可以声明一个这样的返回类型:

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

如果我调用该方法从什么地方,我的IDE告诉我,在返回类型的方法String getFoo(),以及setBar(String),但只有当我点了后面的点功能如下:

FooBar.getFooBar(). // here the IDE is showing the available methods.
Run Code Online (Sandbox Code Playgroud)

有没有办法获得对这样一个对象的引用?我的意思是,如果我做这样的事情:

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

我想有这样的参考(伪代码):

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

这在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();
    }
}
Run Code Online (Sandbox Code Playgroud)

Java真的发明了这么好的功能,但是忘记了想要引用它吗?

Pau*_*ora 30

虽然泛型方法的类型参数可以受到边界的限制,例如extends Foo & Bar,它们最终由调用者决定.当您致电时getFooBar(),呼叫站点已经知道T要解决的问题.通常,这些类型参数将由编译器推断,这就是您通常不需要指定它们的原因,如下所示:

FooBar.<FooAndBar>getFooBar();
Run Code Online (Sandbox Code Playgroud)

但即使T被推断出来FooAndBar,那真的是幕后发生的事情.

那么,要回答你的问题,这样的语法如下:

Foo&Bar bothFooAndBar = FooBar.getFooBar();
Run Code Online (Sandbox Code Playgroud)

在实践中永远不会有用.原因是调用者必须已经知道是什么T.要么T是一些具体的类型:

FooAndBar bothFooAndBar = FooBar.<FooAndBar>getFooBar(); // T is FooAndBar
Run Code Online (Sandbox Code Playgroud)

或者,T是一个未解析的类型参数,我们在其范围内:

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

另一个例子:

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

从技术上讲,这就包含了答案.但我还想指出你的示例方法getFooBar本质上是不安全的.请记住,调用者决定了什么T,而不是方法.由于getFooBar没有采取任何与之相关的参数T,并且由于类型擦除,它唯一的选择是null通过进行未经检查的演员来返回或"撒谎",从而冒着堆污染的风险.一个典型的解决办法将是getFooBar采取Class<T>说法,要不然FooFactory<T>例如.

更新

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

正如我们在评论中讨论的那样,一个使用getFooBar创建混淆的例子,因为它不需要参数来推断T.某些编译器会在无环境调用时抛出错误,getFooBar()而其他编译器则可以正常运行.我认为不一致的编译错误 - 以及调用FooBar.<?>getFooBar()是非法的事实- 验证了我的观点,但这些结果证明是红色的鲱鱼.

基于@MiserableVariable的答案,我将一个使用泛型方法和参数的新示例放在一起,以消除混淆.假设我们有接口FooBar实现FooBarImpl:

interface Foo { }
interface Bar { }
static class FooBarImpl implements Foo, Bar { }
Run Code Online (Sandbox Code Playgroud)

我们也有一些包装类型实现的一个实例的简单容器类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();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在假设我们有一个通配符参数化类型FooBarContainer:

FooBarContainer<?> unknownFooBarContainer = ...;
Run Code Online (Sandbox Code Playgroud)

我们被允许unknownFooBarContainer进入unwrap.这表明我之前的断言是错误的,因为调用站点不知道是什么T- 只是它在边界内是某种类型extends Foo & Bar.

FooBarContainer.unwrap(unknownFooBarContainer); // T is a wildcard capture, ?
Run Code Online (Sandbox Code Playgroud)

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

FooBarContainer.<?>unwrap(unknownFooBarContainer); // compiler error
Run Code Online (Sandbox Code Playgroud)

我只能猜测这是因为通配符捕获永远不能相互匹配 - ?在调用站点提供的参数是模糊的,没有办法说它应该特别匹配类型中的通配符unknownFooBarContainer.

所以,这是OP询问的语法的用例.调用unwrapunknownFooBarContainer返回类型的引用? extends Foo & Bar.我们可以将该引用分配给Fooor Bar,但不是两者:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = FooBarContainer.unwrap(unknownFooBarContainer);
Run Code Online (Sandbox Code Playgroud)

如果由于某种原因unwrap是昂贵的,我们只想打电话一次,我们将被迫施放:

Foo foo = FooBarContainer.unwrap(unknownFooBarContainer);
Bar bar = (Bar)foo;
Run Code Online (Sandbox Code Playgroud)

所以这就是假设语法会派上用场的地方:

Foo&Bar fooBar = FooBarContainer.unwrap(unknownFooBarContainer);
Run Code Online (Sandbox Code Playgroud)

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


关于堆污染的说明

(主要针对@MiserableVariable)以下是一个不安全的方法如何getFooBar导致堆污染及其含义的演练.给出以下接口和实现:

interface Foo { }

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

static class Foo2 implements Foo { }
Run Code Online (Sandbox Code Playgroud)

让我们实现一个不安全的方法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
}
Run Code Online (Sandbox Code Playgroud)

这里,当new Foo2T强制转换时,它被"取消选中",这意味着由于类型擦除,运行时不知道它应该失败,即使它应该在这种情况下T也是如此Foo1.相反,堆被"污染",这意味着引用指向它们不应该被允许的对象.

失败发生在方法返回后,当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
}
Run Code Online (Sandbox Code Playgroud)

现在它不会在通话现场爆炸.当使用内容时,它会在一段时间后爆炸foo1List.这就是堆污染变得难以调试的原因,因为异常堆栈跟踪并不能指向实际问题.

当调用者处于泛型范围内时,它会变得更加复杂.想象一下,而不是得到一个List<Foo1>我们得到一个List<T>,把它放入一个Map<K, List<T>>并将其返回到另一个方法.你得到了我希望的想法.

  • @MiserableVariable具有多个边界的类型参数的擦除总是(擦除)第一个边界.看到这篇文章:http://stackoverflow.com/a/8055497/697449 (3认同)

Mis*_*ble 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());      
    }

}
Run Code Online (Sandbox Code Playgroud)

两者FooBar1FooBar2实现FooBar.在main,调用getFooBar1getFooBar2可以分配给变量,虽然没有强有力的理由让它知道恕我直言.

但是getFooBar有趣的情况是使用代理.实际上,它可能是实现两个接口的对象的唯一实例.不同的方法(show此处)可以与类型更安全的方式使用临时,但如果没有FooBarWrapper问题中描述的黑客,则无法将其分配给变量.甚至不可能创建通用包装器,class Wrapper<T extends U & V>是不允许的.

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