泛型返回类型上限 - 接口与类 - 令人惊讶的有效代码

Ada*_*lik 170 java generics java-8

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

使用Oracle JDK 8u72编译

考虑这两种方法:

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

<X extends String> X getString() {
    return (X) "hello";
}
Run Code Online (Sandbox Code Playgroud)

两者都报告了"未经检查的演员"警告 - 我明白了.困扰我的是我为什么打电话

Integer x = getCharSequence();
Run Code Online (Sandbox Code Playgroud)

它编译?编译器应该知道Integer没有实现CharSequence.打电话给

Integer y = getString();
Run Code Online (Sandbox Code Playgroud)

给出错误(如预期的那样)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String
Run Code Online (Sandbox Code Playgroud)

有人可以解释为什么这种行为被认为是有效的?它会有用吗?

客户端不知道此调用是不安全的 - 客户端的代码在没有警告的情况下编译.为什么编译器不会警告/发出错误?

另外,它与这个例子有什么不同:

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

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

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error
Run Code Online (Sandbox Code Playgroud)

试图传递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
Run Code Online (Sandbox Code Playgroud)

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

Pau*_*ton 183

CharSequence是一个interface.因此即使SomeClass没有实现CharSequence它也很可能创建一个类

class SubClass extends SomeClass implements CharSequence
Run Code Online (Sandbox Code Playgroud)

所以你可以写

SomeClass c = getCharSequence();
Run Code Online (Sandbox Code Playgroud)

因为推断类型X是交集类型SomeClass & CharSequence.

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

<T extends Integer & CharSequence>
Run Code Online (Sandbox Code Playgroud)

另一方面,String不是一个interface,因此不可能扩展SomeClass以获得子类型String,因为java不支持类的多重继承.

通过这个List例子,你需要记住泛型既不是协变也不是逆变.这意味着如果X是子类型Y,List<X>既不是子类型也不是超类型List<Y>.由于Integer未实现CharSequence,您无法List<Integer>在您的doCharSequence方法中使用.

但是,您可以将其编译

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  
Run Code Online (Sandbox Code Playgroud)

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

static <T extends CharSequence> List<T> foo() 
Run Code Online (Sandbox Code Playgroud)

你可以做

List<? extends Integer> list = foo();
Run Code Online (Sandbox Code Playgroud)

同样,这是因为推断类型是Integer & CharSequence,这是一个子类型Integer.

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

欲了解更多信息,这里是JLS的部分地方解释型的边界是如何工作的.您可以包含多个接口,例如

<T extends String & CharSequence & List & Comparator>
Run Code Online (Sandbox Code Playgroud)

但只有第一个边界可能是非界面.

  • 我不知道你可以在通用定义中加上`&`.+1 (62认同)
  • @flkes你可以放多个,但只有第一个参数可以是非接口.`<T extends String&List&Comparator>`没关系,但`<T extends String&Integer>`不是,因为`Integer`不是接口. (13认同)
  • @PaulBoddington这些方法有一些实际用途.例如,如果该类型实际上不用于存储的数据.这方面的例子是`Collections.emptyList()`以及`Optional.empty()`.这些返回通用接口的实现,但不存储任何内容. (7认同)
  • @Federico Peralta Schaffner:这里的重点是,方法`getCharSequence()`承诺返回调用者需要的任何`X`,包括返回一个扩展`Integer`的类型并实现`CharSequence`如果调用者需要它并在此下保证,允许将结果分配给"整数"是正确的.这是方法`getCharSequence()`,因为它不遵守它的承诺而被破坏,但这不是编译器的错. (7认同)
  • 并且没有人说在编译时"final"的类在运行时将是"final". (6认同)
  • @Paul Boddington:不,这是一个奇怪的事情,因为演员应该像赋值一样提供目标类型.我刚刚检查过,如果将调用传递给另一个调用,它将工作,让参数提供目标类型. (4认同)
  • 对于像`<X extends CharSequence> X getCharSequence()`这样的方法,只有两种可能的正确实现,首先,`return null;`,second,`throw ...;`.很少使用它不是排除实际语法有效结构的好标准 - 规则已经足够复杂了.注意这个方法返回一个调用者选择的类型和有用的方法`Collections.emptyList()`和`Optional.empty()`之间的区别,返回一个类型,由调用者*参数化*.还有另一个有效用途系列,请参阅(`private`方法)`Collectors.throwingMerger()`... (3认同)
  • @Federico Peralta Schaffner:在这种情况下,没有堆污染.你不愿意接受的情况是代码显然会因`ClassCastException`而失败,所以不会有堆污染.如果返回的值保存在具有不可重新类型的变量中,则会发生堆污染,但这正是编译器无法确定(in)正确性的情况(我们有一个类型变量而不是`Integer) `).这里的场景,人类读者说"显然是错的",是一个更普遍问题的角落案例. (3认同)
  • @Federico Peralta Schaffner:请记住,正确实现`getCharSequence()`(例如返回`null`,抛出异常,永远不会通过魔法完成或返回正确的东西),编译后的代码,包括赋值给`整数`是正确的. (3认同)
  • @AdamMichalik第一个:它没有_seem_承诺这个,它_ades_承诺这个.它只是没有办法实现这个承诺(除了已经提到的那些:返回`null`,抛出或循环).这与说它只承诺"返回的对象实现CharSequence"非常不同:有很多方法可以实现_that_ promise! (3认同)
  • 非常感谢,保罗,`foo(List <T>列表)`特别有创意!尽管如此,我还是不知道客户端和API提供商如何使用这种通用API实际上有用.使用`<X extends CharSequence> X getCharSequence()`,提供者唯一承诺的是返回的对象实现`CharSequence`.客户端无法知道要转换为有效的其他类型.它可能会转换为`SomeClass`或`OtherClass` - 获得运行时异常.你有一个例子,这可能有用吗?对我来说,这似乎很危险,削弱了编译时检查. (2认同)
  • 实际上,编译时检查仅由未经检查的强制转换取消(并且,关于`SomeClass`示例:实现在任何情况下都不能合理地返回`X`的实例).也许在[JLS第4节](https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-TypeBound)中添加提示可能是值得的,它在哪里制作明确一个`TypeBound`最多只能包含一个`ClassType`(但多个`InterfaceType`). (2认同)
  • @StefanDollase是的,这是一个好点.我的评论具有误导性.我喜欢这些例子,因为它们表明类型擦除有好处.您无法在C#中共享"Collections.emptyList()"的单个实例,因为可以在运行时查询该类型. (2认同)
  • 或者`Comparator.naturalOrder()`或`Function.identity()`... (2认同)
  • @Paul Boddington:考虑像`<T> T getFromId(long id)`这样的方法的不良做法并不罕见,因为在SO上有几个这样的例子,特别是在Java 8环境中,那里适得其反(甚至更多) )... (2认同)
  • @Federico Peralta Schaffner:**可以*是一个扩展两者的类型,`Integer`和`CharSequence`,正如这个答案所解释的那样,`Integer`是`final`的事实不被考虑.它是Java 8类型推断中的新功能,您无需知道该假设类型的实际类型. (2认同)

Luk*_*der 59

在分配之前由编译器推断的类型XInteger & CharSequence.这种类型感觉很奇怪,因为它Integer是最终的,但它是Java中完全有效的类型.然后施展Integer,这是完全可以的.

该类型只有一个可能的值Integer & CharSequence:null.通过以下实现:

<X extends CharSequence> X getCharSequence() {
    return null;
}
Run Code Online (Sandbox Code Playgroud)

以下作业将起作用:

Integer x = getCharSequence();
Run Code Online (Sandbox Code Playgroud)

由于这个可能的价值,没有理由为什么分配应该是错误的,即使它显然是无用的.警告会很有用.

真正的问题是API,而不是呼叫站点

事实上,我最近在博客上谈到了这种API设计反模式.你应该(几乎)从不设计一个泛型方法来返回任意类型,因为你(几乎)永远不能保证推断类型将被传递.一个例外是像这样的方法Collections.emptyList(),如果列表的空白(和泛型类型擦除)是任何推断<T>将工作的原因:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
Run Code Online (Sandbox Code Playgroud)

  • 简短而甜蜜的回答.做得好 (3认同)