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

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

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真的发明了这么好的特性吗,但忘记了有人想引用它吗?


答案 1

虽然泛型方法的类型参数可以受边界限制,例如,但它们最终由调用方决定。当您呼叫 时,呼叫站点已经知道要解析为什么。通常,编译器会推断这些类型参数,这就是为什么您通常不需要指定它们的原因,如下所示:extends Foo & BargetFooBar()T

FooBar.<FooAndBar>getFooBar();

但即使被推断为,那也是幕后发生的事情。TFooAndBar

所以,为了回答你的问题,这样的语法是这样的:

Foo&Bar bothFooAndBar = FooBar.getFooBar();

在实践中永远不会有用。原因是调用方必须已经知道是什么。要么是某种具体类型:TT

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

从技术上讲,这就结束了答案。但我也想指出,您的示例方法本质上是不安全的。请记住,调用方决定的是结果,而不是方法。由于 不 采用 任何 与 相关的参数,并且由于类型擦除,它的唯一选择是通过进行未经检查的强制转换来返回或“撒谎”,从而冒着堆污染的风险。典型的解决方法是采取一个论点,或者例如一个。getFooBarTgetFooBarTnullgetFooBarClass<T>FooFactory<T>

更新

事实证明,当我断言的呼叫者必须始终知道是什么时,我错了。正如@MiserableVariable所指出的,在某些情况下,泛型方法的类型参数被推断为通配符捕获,而不是具体的类型或类型变量。查看他的答案,了解getFooBar实现的一个很好的例子,该示例使用代理来说明他的观点,即T是未知的。getFooBarT

正如我们在评论中讨论的那样,使用getFooBar的示例造成了混乱,因为它不需要参数来推断。某些编译器在无上下文调用时会引发错误,而其他编译器则对此感到满意我认为不一致的编译错误 - 以及调用是非法的事实 - 验证了我的观点,但事实证明这些是红鲱鱼。TgetFooBar()FooBar.<?>getFooBar()

基于@MiserableVariable的答案,我整理了一个新示例,该示例使用带有参数的通用方法,以消除混淆。假设我们有接口和一个实现:FooBarFooBarImpl

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

我们还有一个简单的容器类,它包装了某种类型的实例实现和 .它声明了一个愚蠢的静态方法,该方法采用 a 并返回其引用:FooBarunwrapFooBarContainer

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 = ...;

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

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

正如我所指出的,使用通配符进行呼叫是非法的:unwrap

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

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

因此,这是OP所询问的语法的用例。调用 on 将返回类型的引用。我们可以将该引用赋给 或 ,但不能同时分配给两者:unwrapunknownFooBarContainer? 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);

这只是一个相当晦涩难懂的用例。允许这种语法(无论是好的还是坏的)将会产生非常深远的影响。这将在不需要的地方为滥用打开空间,并且完全可以理解为什么语言设计人员没有实现这样的事情。但我仍然认为思考起来很有趣。

注意 - 从 JDK 10 开始,存在保留类型名称,这使得这成为可能:var

var fooBar = FooBarContainer.unwrap(unknownFooBarContainer);

该变量被推断为具有一个同时实现和的类型,并且不能在源代码中显式表示。fooBarFooBar


关于堆污染的说明

(主要用于@MiserableVariable)以下是不安全方法(如)如何导致堆污染及其含义的演练。给定以下接口和实现:getFooBar

interface Foo { }

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

static class Foo2 implements Foo { }

让我们实现一个不安全的方法,类似于但对此示例进行了简化:getFoogetFooBar

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

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

当实例尝试分配给具有 reifiable 类型 的引用时,该方法返回后,将发生故障。Foo2foo1Foo1

你可能会想,“好吧,所以它在呼叫站点而不是方法上爆炸了,这很重要。但是,当涉及更多的泛型时,它很容易变得更加复杂。例如:

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

当调用方处于通用范围本身时,它会变得更加复杂。想象一下,我们得到的不是 a,而是把它放在 a 中,然后返回到另一种方法。你明白我希望的想法。List<Foo1>List<T>Map<K, List<T>>


答案 2

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

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

}

和 实现 和 .在 中,对变量的调用和可以分配给变量,尽管没有强烈的理由让它知道恕我直言。FooBar1FooBar2FooBarmaingetFooBar1getFooBar2

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

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