java - 如何引用具有多个边界的通用返回类型

标签 java generics interface multiple-inheritance

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

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

最佳答案

而泛型方法的类型参数可以通过边界限制,例如 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 指出的那样,在某些情况下,泛型方法的类型参数被推断为通配符捕获,而不是具体的类型或类型变量。 his answer一个很好的例子 getFooBar使用代理来实现他的观点 T未知。

正如我们在评论中所讨论的,an example using getFooBar 造成困惑,因为它不需要参数来推断 T从。某些编译器 throw an error在对 getFooBar() 的无上下文调用中而其他人are fine with it .我认为不一致的编译错误 - 以及调用 FooBar.<?>getFooBar() 的事实。是非法的 - 验证了我的观点,但这些结果是红鲱鱼。

根据@MiserableVariable 的回答,我整理了 an new example使用带有参数的泛型方法来消除混淆。假设我们有接口(interface) FooBar和一个实现 FooBarImpl :
interface Foo { }
interface Bar { }
static class FooBarImpl implements Foo, Bar { }

我们还有一个简单的容器类,它包装了实现 Foo 的某种类型的实例。和 Bar .它声明了一个愚蠢的静态方法 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 询问的语法的用例。调用 unwrapunknownFooBarContainer返回 ? 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)和实现:
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>>并将其返回到另一种方法。你明白我希望的想法。

关于java - 如何引用具有多个边界的通用返回类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14464226/

相关文章:

java - 判断一个字符串是否为回文的最快方法

java - 从 URL 计算文件大小

java - 将图像跨度设置在文本的下一行和中心

c# - 动态对象 - 整洁的属性名称?

javascript - 强制函数的参数必须属于泛型类型 T

c# - 我可以强制一个实现类返回一个它自己类型的对象吗?

asp.net-mvc - 在MVC中,我如何返回IQueryable结果并使用操作在 View 中显示它

java - UML 类图 - 多重关联

objective-c - 带有通用参数的Objective-C类方法无法在Swift中调用

c# - 修复继承困惑的接口(interface)?