通用返回类型上限-接口与类-出乎意料的有效代码


171

这是来自第三方库API的真实示例,但已简化。

与Oracle JDK 8u72一起编译

考虑以下两种方法:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

两者都报告“未经检查的演员表”警告-我明白原因。让我感到困惑的是为什么我可以打电话

Integer x = getCharSequence();

它可以编译吗?编译器应该知道Integer没有实现CharSequence。致电

Integer y = getString();

给出错误(按预期)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

有人可以解释为什么将这种行为视为有效吗?有什么用?

客户端不知道此调用是不安全的-客户端的代码在编译时不会发出警告。为何编译器不会对此发出警告/发出错误?

此外,它与此示例有何不同:

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

尝试通过List<Integer>给出了错误,如预期的那样:

method doCharSequence in class generic.GenericTest cannot be applied to given types;
  required: java.util.List<X>
  found: java.util.List<java.lang.Integer>
  reason: inference variable X has incompatible bounds
    equality constraints: java.lang.Integer
    upper bounds: java.lang.CharSequence

如果报告为错误,为什么Integer x = getCharSequence();不这样做?


15
有趣!LHS上的投射Integer x = getCharSequence();会编译,但RHS上的投射会编译Integer x = (Integer) getCharSequence();失败
片状

您正在使用哪个版本的Java编译器?请在问题中指定此信息。
Federico Peralta Schaffner

@FedericoPeraltaSchaffner无法理解为什么这么重要-这是直接有关JLS的问题。
蜘蛛鲍里斯(Boris the Spider)

@BoristheSpider因为Java8的类型推断机制已更改
Federico Peralta Schaffner

1
@FedericoPeraltaSchaffner-我已经用[java-8]标记了问题,但现在在帖子中添加了编译器版本。
Adam Michalik

Answers:


184

CharSequence是一个interface。因此,即使SomeClass不实现CharSequence,也完全有可能创建一个类

class SubClass extends SomeClass implements CharSequence

因此你可以写

SomeClass c = getCharSequence();

因为推断的类型X是相交类型SomeClass & CharSequence

在某些情况下,这是有点奇怪的,Integer因为它Integer是最终的,但final在这些规则中没有任何作用。例如你可以写

<T extends Integer & CharSequence>

另一方面,String由于不是interface,因此无法扩展SomeClass以获得的子类型String,因为Java不支持类的多重继承。

在该List示例中,您需要记住,泛型既不是协变也不是协变。这意味着if X是的子类型YList<X>既不是的子类型也不是的超类型List<Y>。由于Integer未实现CharSequence,因此无法List<Integer>在您的doCharSequence方法中使用。

您可以,但是可以将其编译

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

如果你有一个方法返回一个List<T>像这样的:

static <T extends CharSequence> List<T> foo() 

你可以做

List<? extends Integer> list = foo();

同样,这是因为推断的类型是,Integer & CharSequence并且这是的子类型Integer

当您指定多个边界(例如<T extends SomeClass & CharSequence>)时,交叉点类型会隐式出现。

有关更多信息,是JLS的一部分,其中解释了类型边界的工作方式。您可以包括多个接口,例如

<T extends String & CharSequence & List & Comparator>

但只有第一个边界可以是非接口。


62
我不知道您可以&在泛型定义中添加一个。+1
片片

13
@flkes可以放置多个,但只有第一个参数可以是非接口。<T extends String & List & Comparator>可以,但<T extends String & Integer>不能,因为Integer不是接口。
Paul Boddington

7
@PaulBoddington这些方法有一些实际用途。例如,如果类型实际上未用于存储的数据。的示例Collections.emptyList()还有Optional.empty()。这些返回通用接口的实现,但不存储任何内容。
Stefan Dollase '16

6
没有人说正在final编译时的类会final在运行时。
Holger

7
@Federico Peralta Schaffner:重点是,该方法getCharSequence()承诺将返回X调用者所需的任何内容,包括返回一个扩展类型IntegerCharSequence在调用者需要时实现的类型,并且在此承诺下,允许将结果分配给是正确的Integer。这种方法getCharSequence()被破坏了,因为它没有遵守承诺,但这不是编译器的错。
Holger

59

您的编译器在分配之前推断出的类型XInteger & CharSequence。这种类型感觉很奇怪,因为它Integer是最终类型,但在Java中是完全有效的类型。然后将Integer其强制转换为,这完全可以。

该类型恰好有一个可能的值Integer & CharSequencenull。通过以下实现:

<X extends CharSequence> X getCharSequence() {
    return null;
}

以下任务将起作用:

Integer x = getCharSequence();

由于有这种可能的值,因此即使显然是无用的,也没有理由为什么分配错误。警告会很有用。

真正的问题是API,而不是调用网站

实际上,我最近在博客中发布了有关此API设计反模式的信息。您应该(几乎)永远不要设计一个泛型方法来返回任意类型,因为您(几乎)永远不能保证推断出的类型将被传递。像之类的方法是一个例外Collections.emptyList(),在这种情况下,列表的空白(以及通用类型擦除)是进行任何推断的原因<T>

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
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.