为什么Java中的泛型类型参数不能有下限?

54
我了解到您无法将Java泛型类型参数绑定到下限(即使用“super”关键字)。我正在阅读Angelika Langer Generics FAQ对此问题的解释。他们说这基本上归结为下限是无用的(“毫无意义”)。
但我并不认同。我可以想象一个用途,它可以帮助您更加灵活地响应库方法调用者的需求,并生成一个类型化的结果。想象一下一个方法,它创建一个用户指定大小的数组列表,并用空字符串填充它。一个简单的声明可能是:
public static ArrayList<String> createArrayListFullOfEmptyStrings(int i);

但这对你的客户来说是不必要的限制。为什么他们不能像这样调用你的方法:

//should compile
List<Object> l1 = createArrayListFullOfEmptyStrings(5); 
List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
List<String> l3 = createArrayListFullOfEmptyStrings(5);

//shouldn't compile
List<Integer> l4 = createArrayListFullOfEmptyStrings(5);

此时我会尝试以下定义:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {
  List<T> list = new ArrayList<T>(size);
  for(int i = 0; i < size; i++) {
     list.add("");
  }
  return list;
}

但它将无法编译; 在此上下文中,super关键字是非法的。

忽略我下面说的内容,上面的例子是否不好?为什么在这里使用下限没有用?如果有用,那么它不被允许在Java中的真正原因是什么?

附言

我知道更好的组织方式可能是这样的:

public static void populateListWithEmptyStrings(List<? super String> list, int size);

List<CharSequence> list = new ArrayList<CharSequence>();
populateListWithEmptyStrings(list, 5);

为了回答这个问题,我们可以假设由于某种要求,我们需要在一个方法调用中执行两个操作吗?

编辑

@Tom G(有理由地)询问拥有List<CharSequence>相比于List<String>有何优势。首先,没有人说返回的列表是不可变的,因此这里有一个优点:

List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);
l2.add(new StringBuilder("foo").append("bar"));

7
Collection.toArray(T[])方法可以使用下限来实现合法的用法。通过将类型变量从<T>更改为<T super E>,可以防止该方法出现ArrayStoreException异常。 - Jeffrey
3
@Jeffrey:这是一个很好的使用案例。我从没意识到toArray在任何情况下都不与集合的元素类型相关联。 - Mark Peters
7
我遇到的另一个用例是:Guava的 Optional.or(T)。*"关于泛型的说明:签名public T or(T defaultValue)过于限制。然而,理想的签名public <S super T> S or(S)在Java中不合法..."* - Mark Peters
2
这是我想编写的一个方法,它将使用上下限类型参数(尽管我认为即使使用通配符也不合法): super List> C internIfEmpty(C collection) { return collection.isEmpty() ? Collections.emptyList() : collection; } - shmosel
1
@shmosel:这个不错。既然所有内容都需要静态知道,这可能是一个很好的情况,可以重载方法...一个是 <T> Collection<T> internIfEmpty(Collection<T>),另一个是 <T> List<T> internIfEmpty(List<T>)。编译器会为您调用更具体的方法,您只需让第一个返回第二个的结果即可。 - Mark Peters
1
@MarkPeters,这正是我正在做的事情。但如果有一种方法声明某种类型在有限范围内,那就太好了。 - shmosel
6个回答

15

基本上,它不够有用。

我认为你的例子指出了一个下界(lower bound)唯一的优点——FAQ称其为“受限实例化(Restricted Instantiation)”:

底线是:一个“超级”绑定(super bound)给你带来的全部益处就是只能使用Number的超类型作为类型参数的限制。....

但正如其他帖子所指出的,即使这个特性的有用性也可能受到限制。

由于多态和特化的本质,根据FAQ所描述的访问非静态成员和类型擦除,上界比下界更有用。我怀疑下界引入的复杂性不值得其有限的价值。


原始帖子作者:我想补充一点,我认为你确实表明它是有用的,只是不够有用。如果你能提出不容置疑的关键用例,我将支持这个JSR。 :-)


4
哈哈,我并不是那么在意。我认为这应该是一个成本收益决策,但我对FAQ坚持说它们“毫无意义”感到沮丧。不幸的是,在那个部分中,他们只考虑了类类型参数的情况,完全忽略了方法类型参数。 - Mark Peters
1
想出无可辩驳的杀手级用例,我会支持JSR的。如何创建一个内在协变的接口呢?因为Java没有协变的概念,所以我能做的最好的事情(假设我可以做TSuper super TCovariant)就是公开一个简单的方法来转换对象(例如,在Iterable<T>上的一个方法,比如“castUp<TSuper super T() {return (Iterable<TSuper>) this;})。这样的方法,再加上Java的类型推断,会非常方便,不是吗? - Groostav
2
@Lil 你说得对,但我并没有误读FAQ - 自从我几年前发布我的答案以来,FAQ已经发生了变化(https://web.archive.org/web/20110707143151/http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html)。FAQ已经改变以承认OP的特殊情况偶尔是有用的,这也是我在最后一句中承认的。我可能会在未来更新答案以反映更新后的FAQ和您的评论。 - Bert F
2
一个数据库管理器类,它向下兼容到某个特定点怎么样?例如,您希望能够包括您类型的超类,但创建您新类型的元素。您可能需要像这样的东西:class DBManager<T extends foo, S super T>{...}其中T表示您数据类型的当前版本,S表示您想要包含在管理器中的旧版本。假设您通过扩展旧版本来构建新版本。 - ctst
2
在类型层次结构中,有一个您每天肯定会使用的常见用例:Collection<E>.toArray(T[])。方法toArray的类型参数T应该表示Collection的类型参数E的超类型,但是无法指定关系,因此T可以是任何与E无关的东西,编译器不会告诉您。这影响所有集合。 - Holger
显示剩余4条评论

10
规范确实谈到了类型参数的下限,例如:
4.10.2 类型变量是其下边界的直接超类型。
5.1.10 一个新鲜的类型变量...它的下边界
看起来,只有当类型变量作为通配符捕获的结果时才有(非空)下限。如果语言允许在所有类型参数上设置下限,会怎样呢?可能不会引起太多麻烦,它被排除仅是为了保持泛型更简单(好吧...)。更新:据说还没有对下限类型参数进行彻底的理论研究。
更新:一篇声称下限没问题的论文:"Java Type Infererence Is Broken: Can We Fix It" by Daniel Smith"
撤回:以下论点是错误的。原帖中给出的例子是合法的。
你提供的例子并不是很具有说服力。首先,它不是类型安全的。返回的列表确实是一个List,将其视为另一种类型是不安全的。假设您的代码编译成功:
    List<CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

然后我们可以添加非字符串到它,这是错误的。
    CharSequence chars = new StringBuilder();
    l2.add(chars); 

一个 List<String> 不是,但有点像 CharSequence 的列表。你可以使用通配符解决你的需求:

public static  List<String> createArrayListFullOfEmptyStrings(int size)  

// a list of some specific subtype of CharSequence 
List<? extends CharSequence> l2 = createArrayListFullOfEmptyStrings(5);

// legal. can retrieve elements as CharSequence
CharSequence chars = l2.get(0);

// illegal, won't compile. cannot insert elements as CharSequence
l2.add(new StringBuilder());

3
谢谢提供 JLS 的引用。现在有两个要点。1)createArray... 的结果赋给 List<CharSequence> 不会不安全(如果支持下限的话)。它从一开始就不是List<String>,它将一直是一个List<CharSequence>(尽管通过类型擦除可能会感觉很奇怪)。 它从未被视为 List<String>。因此,向其中添加 StringBuilder 实例绝对不会不安全。 2) 将结果赋给 List<? extends CharSequence> 相对于只使用 List<String> 没有任何好处,实际上更加限制了。 - Mark Peters
2
你是对的,你的例子是有效的。下限可能没有被包含进来,这可能是因为它还没有被很好地研究。这不是一个简单的问题,必须要有理论支持。例如,参见JLS3 15.12.2.7,并注意其中的复杂性。这是如此复杂,Java编译器与之相关的错误清单就很长。现在想象一下我们加入下限。不仅算法会膨胀多倍,而且我们可能甚至不知道它是否是一个可靠的算法。 - irreputable
谢谢,我已经点赞了您提供的JLS参考资料。这确实是复杂的内容;如果有一个地方让我无法理解JLS,那就是在处理泛型的形式时。您说“据说对于下限类型参数的理论研究并没有彻底进行”。出于我的好奇心,您能否提供一个链接或参考文献来证明这一点? - Mark Peters
2
JLS 提到的论文是 Java 泛型基础:http://www.sato.kuis.kyoto-u.ac.jp/~igarashi/papers/variance.html - irreputable

1

除了回答之外,这是另一个(可能是致命的?)用例。我有一个ModelDecorator助手。我希望它具有以下公共API

class ModelDecorator<T>{
    public static <T> ModelDecorator<T> create(Class<T> clazz);
    public <SUPER> T from(SUPER fromInstance);
}

因此,假设有类A、B扩展自A,可以像这样使用:
A a = new A();
B b = ModelDecorator.create(B.class).from(a);

但是我希望对T和SUPER进行限制,以确保只有子类可以使用API进行实例化。目前,我可以做到:

C c = new C();
B b = ModelDecorator.create(B.class).from(c);

请翻译:

当B不继承于C时。

显然,如果我能这样做:

    public <SUPER super T> T from(SUPER fromInstance);

那会解决我的问题。

3
有趣的用例。我想即使使用“super”,这也可能不会起作用,因为您始终可以使用“Object”来满足SUPER,这意味着您仍然可以将任何内容传递到“from()”中,因为所有内容都可以分配给“Object”。它实际上并未强制要求c的具体类型是B的超类型,只是c的某个超类型也是B的超类型(我们已经知道使用“Object”时成立)。 - Mark Peters

0
在那个时候,使用类型化的列表有什么优势呢?当您迭代返回的集合时,仍然应该能够执行以下操作:
for(String s : returnedList) {
CharSequence cs = s;
//do something with your CharSequence
}

谢谢Tom,我会添加一个编辑作为回复。简而言之,它将允许您向列表中添加除“String”以外的其他内容。 - Mark Peters
听起来不错。我明白,我并不认为这应该是非法的——我只是觉得它不能满足我的需求。 - Thorn G

0
编辑:我带来了一个好消息。有一种方法可以得到你想要的大部分内容。
public static <R extends List<? super String>> R createListFullOfEmptyString(IntFunction<R> creator, int size)
{
  R list = creator.apply(size);
  for (int i = 0; i < size; i++)
  {
    list.add("");
  }
  return list;
}

// compiles
List<Object> l1 = createListFullOfEmptyString(ArrayList::new, 5);
List<CharSequence> l2 = createListFullOfEmptyString(ArrayList::new, 5);
List<String> l3 = createListFullOfEmptyString(ArrayList::new, 5);
// doesn't compile
List<Integer> l4 = createListFullOfEmptyString(ArrayList::new, 5);

缺点是客户端需要提供要改变的 R 实例或构造 R 的某种方法。没有其他安全构造它的方式。

出于信息目的,我将保留下面的原始答案。


总之:

没有一个好的理由,只是还没有做到。

在这样的情况下,为了能够对执行以下所有操作的方法编写正确的变量类型:

A)接受或创建参数化数据结构

B)将计算出的(未传递)值写入该数据结构

C)返回该数据结构

编写/接受值正是逆变性适用的情况,这意味着数据结构上的类型参数必须由写入数据结构的值的类型下限。目前在Java中表达这一点的唯一方法是在数据结构上使用下限通配符,例如List<? super T>。


如果我们正在设计一个类似于OP的API,它可能自然地(但不是法律上)表达为:

// T is the type of the value(s) being computed and written to the data structure

// Method creates the data structure
<S super T> Container<S> create()

// Method writes to the data structure
<S super T> Container<S> write(Container<S> container)

那么我们可用的选项有:

A)使用下限通配符,并强制调用者转换输出:

// This one is actually useless - there is no type the caller can cast to that is both read- and write-safe.
Container<? super T> create()

// Caller must cast result to the same type they passed in.
Container<? super T> write(Container<? super T> container)

B) 过度限制数据结构的类型参数以匹配写入值的类型,并强制调用者转换输入和输出:

// Caller must accept as-is; cannot write values of type S (S super T) into the result.
Container<T> create()

// Caller must cast Container<S> (S super T) to Container<T> before calling, then cast the result back to Container<S>.
Container<T> write(Container<T> container)

C) 使用新的类型参数并在内部进行自己的不安全转换:

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> create()

// Caller must ensure S is a supertype of T - we cast T to S internally!
<S> Container<S> write(Container<S> container)

选择你的毒药。


-3

好的,让我们来处理这个问题。你定义了一个方法:

public static <T super String> List<T> createArrayListFullOfEmptyStrings(int size) {

这是什么意思?这意味着如果我调用你的方法,那么我会得到一个某个 String 超类的列表。也许它返回一个字符串列表。也许它返回一个对象列表。我不知道。

很酷。

List<Object> l1 = createArrayListFullOfEmptyStrings(5);

根据你的说法,这应该编译通过。但这是不对的!我可以把一个整数放进一个对象列表中 - l1.add(3)。但如果你返回一个字符串列表,那么这样做应该是非法的。

List<String> l3 = createArrayListFullOfEmptyStrings(5);

根据你的说法,这应该编译通过。但这是不对的!l3.get(1) 应该总是返回一个字符串...但是该方法可能已经返回了一个对象列表,这意味着 l3.get(1) 可能是一个整数。

唯一有效的方法是

List<? super String> l5 = createArrayListFullOfEmptyStrings(5);

我知道的是,我可以安全地调用l4.put("foo"),并且我可以安全地获取Object o = l4.get(2)


3
当然可以将一个整数放入其中(如果使用List<Object>)。我返回的不是字符串列表,而是只包含字符串的对象列表。这并不会存在什么不安全的问题。 - Mark Peters
2
至于 List<String> 能否包含一个整数,这不是下限工作的方式。你不能将除了 String 以外的任何东西添加到 List<? super String> 中。 - Mark Peters

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