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


75

我知道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应该明确定义的语义-我想通过允许两种类型而不是一种类型的交集,不会丢失类型安全性。我敢肯定有一个解释。有谁知道它是什么?


有两点:首先,它看起来更像“扩展A,B”。其次,A和B不能是接口,而是一个类(当然,编译器可以检测到它)。
SJuan76

8
不。该&字符来表示多个范围的标准方法。“ class AandBList<T extends A & B>”只是语言的工作方式,尽管我同意使用<T extends A, B>match会更直观public interface A extends C, D。而且,在泛型类型范围内,extends无论接口是类还是类,都可以使用。我知道令人困惑,但这就是目前的样子。
Adrian Petrescu

Answers:


41

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

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

当前的任何推理引擎都必须进行大修,以允许这种分歧。但是真正的严重问题是,这允许有多种解决方案,但是没有理由偏爱一个。

但是,推断对于类型安全性不是必不可少的。在这种情况下,我们可以简单地拒绝推断,并要求程序员显式填充类型参数。因此,推理的难度并不是反对干涉类型的有力依据。


2
非常周到的答案。关于的有趣的发现java.lang.reflect.WildcardType。非常感谢您:)
Adrian Petrescu

我真的不明白这个答案
gstackoverflow 2014年

您是指“路口类型”吗?
Noyo 2014年

为什么类型参数可以有多个界限呢?
迪恩·莱特斯多夫

25

根据Java语言规范

4.9交集类型交集类型采用T1&...&Tn,n> 0的形式,其中Ti,1in是类型表达式。交叉点类型出现在捕获转换(第5.1.10节)和类型推断(第15.12.2.7节)的过程中。不能将交集类型直接编写为程序的一部分;没有语法支持此功能。相交类型的值是那些对象,即所有类型Ti的值,为1in。

那为什么不支持呢?我的猜测是,您应该怎么做?-假设有可能:

List<? extends A & B> list = ...

那应该怎么办

list.get(0);

返回?没有语法可捕获的返回值A & B。也不可能在此列表中添加一些内容,因此基本上是没有用的。


3
感谢您的链接!这真的很有帮助。我想我会这样做,A a = list.get(0);并且B b = list.get(0);两者都是安全的。我想我总是可以重构以获得通用的超级接口。
Adrian Petrescu

16
>>>没有语法来捕获A和B的返回值。也无法在此列表中添加内容,因此基本上是没有用的。如果您extend仍然无法放入该列表。要获取,您只需说A a = list.get(0)B b = list.get(0)。我不认为这有类型理论上的问题,只是Java的限制。另一方面<?超级A&B>几乎毫无意义。
Op De Cirkel

同意@OpDeCirkel。如果您有一个SomeClass<T extends A & B>声明要返回的方法的类型List<T>,然后在type变量上调用该方法,则已经发生了这种情况SomeClass<?>。因此,JLS参考为+1,猜测为-1。
Paul Bellora

1
假设A和B是接口,并且所有J,K,L都实现A和B(以及它们实现的其他接口)。然后list可以包含J,K和L的元素,并list.get(0)可以返回它们。实际上,Java Spec正确地指出,没有办法直接表达这个概念,但是J,K和L通过接口的多重继承间接地利用了它。
2013年

-1正如Bohemian指出的那样,语法可能略有不同。
Aleksandr Dubinsky 2014年

11

没问题...只需在方法签名中声明所需的类型。

这样编译:

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

这是真实的,但它并不能解释为什么的类型参数不能高于其他参考类型中定义JSL,引用类型被定义在这里
运德Cirkel

对不起,我的意思是:WildcardBounds不能等那么extends ReferenceType | super ReferenceType
运德Cirkel

1

好问题。我花了一段时间才弄清楚。

让我们简化一下情况:您试图做的事情就好像声明了一个扩展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中这是不合法的。这意味着,您将列表元素同时声明为两种类型的蜜蜂,即使我们的大脑可以处理它,Java语言也不能。


11
没错,但是通过同时使用这两种类型,我们可以在不同时间将其安全地视为一种或另一种。两者A a = list.get(0);B b = list.get(0);都是有效的。
Adrian Petrescu

“将需要使用”是错误的,您没有被迫这样做,我没有看到任何限制,这样做也是合法的:class C<T extends A & B> { public T foo() {};}您在其中返回具有多个边界的变量。
WorldSEnder

问题是直接使用结果而不是将其存储在变量中。例如,哪些方法可用list.get(0).*
ndm13 '16

-1

这样做的价值:如果有人因为真正想在实践中使用它而想知道它,我已经通过定义一个接口来解决这个问题,该接口包含了我正在使用的所有接口和类中所有方法的并集。即我正在尝试执行以下操作:

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.一类
马克
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.