为什么有界通配符泛型中不能有多个接口?

85

我知道Java的泛型类型有各种反直觉的属性。以下是其中之一,我不太理解,希望有人能够解释一下。在为类或接口指定类型参数时,您可以将其限制为必须实现多个接口,如public class Foo<T extends InterfaceA & InterfaceB>。然而,如果您正在实例化一个实际对象,则此方法不再适用。 List<? extends InterfaceA> 是可以的,但 List<? extends InterfaceA & InterfaceB> 无法编译。请考虑以下完整代码片段:

import java.util.List;

public class Test {

  static interface A {
    public int getSomething();
  }

  static interface B {
    public int getSomethingElse();
  }

  static class AandB implements A, B {
    public int getSomething() { return 1; }
    public int getSomethingElse() { return 2; }
  }

  // Notice the multiple bounds here. This works.
  static class AandBList<T extends A & B> {
    List<T> list;

    public List<T> getList() { return list; }
  }

  public static void main(String [] args) {
    AandBList<AandB> foo = new AandBList<AandB>(); // This works fine!
    foo.getList().add(new AandB());
    List<? extends A> bar = new LinkedList<AandB>(); // This is fine too
    // This last one fails to compile!
    List<? extends A & B> foobar = new LinkedList<AandB>();
  }
}

看起来bar的语义应该是明确定义的——我想不出允许两种类型交集而不仅仅是一种会导致类型安全丢失的情况。但我肯定有解释。有人知道是什么吗?


两个要点:第一,写成 "extends A, B" 更为准确。第二,A 和 B 可以不是接口,也可以是类(当然编译器会检测出来)。 - SJuan76
10
不行。& 字符是表示多个边界的标准方式。 "class AandBList<T extends A & B>"只是语言的工作方式,尽管我同意使用<T extends A, B>会更直观,以匹配public interface A extends C,D。在泛型类型的限制中,无论是接口还是类,都使用extends。虽然很令人困惑,但这就是当前的情况。 - Adrian Petrescu
5个回答

45

有趣的是,接口java.lang.reflect.WildcardType看起来支持通配符参数的上限和下限,每个限制可以包含多个边界

Type[] getUpperBounds();
Type[] getLowerBounds();

这已经远超过语言的限制了。在代码中有一个隐藏的注释。

// one or many? Up to language spec; currently only one, but this API
// allows for generalization.

该接口的作者似乎认为这是一个偶然的限制。

对于你的问题的通用回答是,泛型已经足够复杂了;增加更多的复杂性可能会成为压垮骆驼的最后一根稻草。

要允许通配符具有多个上边界,必须扫描规范并确保整个系统仍然可以运行。

我知道的一个问题在于类型推断。当前的推断规则无法处理交集类型。没有规则可以减少约束 A&B << C。如果我们将其减少到

    A<<C 
  or
    A<<B

任何当前的推理引擎都必须经过重大的改造才能允许这种分叉。但真正严重的问题是,这样可以有多个解决方案,但没有理由优先考虑其中一个。

然而,推理并不是类型安全的必要条件;在这种情况下,我们可以简单地拒绝推断,并要求程序员显式填写类型参数。因此,推理难度不是反对交叉类型的有力论据。


3
非常周到的回答。对于java.lang.reflect.WildcardType的发现相当有趣。非常感谢 :) - Adrian Petrescu
我真的不理解这个答案。 - gstackoverflow
你是指“交叉类型”吗? - Noyo
2
为什么类型参数具有多个边界是可以的呢? - Dean Leitersdorf

28

根据Java语言规范:

4.9 交集类型 交集类型采用T1 & ... & Tn的形式,其中Ti, 1≤i≤n,都是类型表达式。交集类型出现在捕获转换(§5.1.10)和类型推断(§15.12.2.7)的过程中。 不可能直接将交集类型编写为程序的一部分;没有语法支持此操作。交集类型的值是那些同时属于所有类型Ti的对象。

那么为什么不支持这个呢?我猜想,如果这样的事情是可以实现的,那么你会怎么做呢?

List<? extends A & B> list = ...
那么应该怎么做呢?
list.get(0);

返回值?没有语法可以捕获A&B的返回值。向这样的列表中添加内容也是不可能的,因此它基本上是无用的。


3
谢谢提供链接!这真的很有帮助。我想我可以这样使用它:A a = list.get(0);B b = list.get(0);都是安全的。不过,我想我可以随时重构代码来获取一个公共的超级接口。 - Adrian Petrescu
19
没有语法可以捕获A和B的返回值。向这样的列表添加内容也是不可能的,因此它基本上是无用的。如果你使用“extend”,你也无法将其放入该列表中。要获取,你可以简单地说“A a = list.get(0)”或“B b = list.get(0)”。我认为这并没有类型理论上的问题,只是Java的限制。另一方面,“<? super A&B>”几乎没有意义。 - Op De Cirkel
同意@OpDeCirkel的观点。如果您有一个声明返回List<T>的类型为SomeClass<T extends A&B>的方法,然后在类型为SomeClass<?>的变量上调用该方法,则已经发生了这种情况。因此,JLS参考值得一加,但猜测则不值得一减。 - Paul Bellora
1
假设A和B是接口,而J、K、L都实现了A和B(除了它们实现的其他接口)。那么list可以包含J、K和L的元素,而list.get(0)可以返回它们。事实上,Java规范正确地指出了没有直接表达这个概念的方法,但是J、K和L通过多重继承接口间接地利用了它。 - achow
正如Bohemian所指出的那样,使用稍微不同的语法是可能的。 - Aleksandr Dubinsky

12
没问题...只需在方法签名中声明所需的类型即可。
这个可以编译通过:
public static <T extends A & B> void main(String[] args) throws Exception
{
    AandBList<AandB> foo = new AandBList<AandB>(); // This works fine!
    foo.getList().add(new AandB());
    List<? extends A> bar = new LinkedList<AandB>(); // This is fine too
    List<T> foobar = new LinkedList<T>(); // This compiles!
}

1
这是正确的,但它并不能解释为什么类型参数的_value_不能是除_JSL_(http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.5.1)中定义的_Reference Type_以外的其他值,参考类型定义在此处 - Op De Cirkel
1
抱歉,我的意思是:WildcardBounds 只能为 extends ReferenceType | super ReferenceType - Op De Cirkel

2

好问题。我花了一些时间才理解明白。

让我们简化你的情况:你正在尝试做的就像声明一个类,该类继承了2个接口,然后一个变量作为这2个接口的类型,就像这样:

  class MyClass implements Int1, Int2 { }

  Int1 & Int2 variable = new MyClass()

当然,这是非法的。 这相当于你在尝试使用泛型。你所尝试做的是:
  List<? extends A & B> foobar;

但是,要使用foobar,您需要同时使用两个接口的变量,如下所示:

  A & B element = foobar.get(0);

在Java中这是不合法的。这意味着,您同时将列表的元素声明为2种类型,并且即使我们的大脑可以处理它,Java语言也无法处理。


11
是的,但同时具备两种类型,我们可以在不同的时间独立地将其视为其中之一。A a = list.get(0);B b = list.get(0); 都是有效的。 - Adrian Petrescu
“would need to use”是错误的,您并不被强制这样做,我没有看到任何限制,也可以这样做:class C<T extends A & B> { public T foo() {};}其中您返回一个具有多个边界的变量。 - WorldSEnder
问题出现在直接使用结果而不将其存储在变量中。例如,对于 list.get(0).*,有哪些可用的方法? - ndm13

-2
值得一提的是:如果有人想在实践中使用这个,我已经通过定义一个包含我正在使用的所有接口和类中所有方法联合的接口来解决了这个问题。也就是说,我试图做以下事情:
class A {}

interface B {}

List<? extends A & B> list;

这是非法的 - 所以我做了这个:

class A {
  <A methods>
}

interface B {
  <B methods>
}

interface C {
  <A methods>
  <B methods>
}

List<C> list;

这仍然不如能够键入List<? extends A implements B>这样的内容有用,例如,如果有人向A或B添加或删除方法,则列表的类型将不会自动更新,需要手动更改C。但它对我的需求有效。


如果您这样做,为什么不使用类C扩展A并实现B呢? 当A或B中有新方法时,您需要手动更新C,但至少您可以确保没有人会忘记更新C,而在您的情况下,这是完全可能的。 (您不会忘记,因为会出现NoClassDefFound编译时错误)。 - Keey
@Keey 我记不清我的用例细节了,但我可能需要将C作为接口,因为我需要将它附加到一个已经被强制扩展除A之外的类。 - Mark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接