java - 与继承类不兼容的类型
问题描述
我遇到了以下问题,感觉就像处理继承的方式不一致:
import java.util.*;
interface Greeting {
}
class Hello implements Greeting {
public Hello(){}
}
public class HelloWorld{
// Compiles fine
public static Greeting getGreeting() {
return new Hello();
}
// incompatible types: ArrayList<Hello> cannot be converted to ArrayList<Greeting>
public static ArrayList<Greeting> getGreetings() {
return new ArrayList<Hello>();
}
// Hello cannot be converted to O
// where O is a type-variable:
// O extends Greeting declared in method <O>getGenericGreeting()
public static <O extends Greeting> O getGenericGreeting() {
return new Hello();
}
public static void main(String []args){}
}
谁能解释为什么会这样工作,以及后两种方法是否有替代方法无法编译?
解决方案
现实,基本上。我们从一个公理开始:说的“重点”Hello implements Greeting
是Hello是一种问候。如果需要打招呼,那么可以说,Hello 的实例很适合。
因此,Greeting g = new Hello();
编译和工作正常。
你会想:太好了,所以,List<Greeting> g = new ArrayList<Hello>();
也很好,对吧?
没有。不是它是如何工作的。不是“这不是 Java 的工作方式”,不,“这不是该模型的工作方式——这不是现实的工作方式”。这是列表所固有的。
想象一下它确实有效。然后你可以这样做:
interface Greeting {}
class Hello implements Greeting {}
class Goodbye implements Greeting {}
List<Hello> hellos = new ArrayList<Hello>();
List<Greeting> greets = hellos;
greets.add(new Goodbye());
Hello hello = hellos.get(0);
考虑整个代码:希望您意识到它已损坏。
这就是 java 以它的方式工作的原因:为了避免这种情况。如果您尝试编译上述代码,List<Greeting> greets = hellos;
则无法编译。
这称为方差。
协变类型系统允许子类型“适合”它的任何超类型。基本的java是协变的。这是一个协变赋值:Greeting g = new Hello()
.
还有其他变化:不变意味着只允许类型本身,而不是任何子类型,也不是任何超类型。基本泛型是不变的。它是不变的,因为这就是现实如何使用这些东西 - 请参阅上面的片段。
还有逆变。这将是一个逆变分配:Hello h = new Greeting();
-超类型可以代表子类型。不是基本的 java 是如何工作的,而且看起来很荒谬,但请稍等。
最后是“遗留/原始”差异 - 任何事情都会发生,做任何你想做的事,一切都很好,让我们祈祷如果我们犯了错误会发生运行时异常。
泛型支持所有这 4 个:
List<? extends Greeting> g; // This is a covariant list
List<? super Hello> g; // This is a contravariant list
List<Greeting> g; // This is an invariant list
List g; // This is a raw/legacy variance list.
让我们再次尝试完全相同的代码段,这次使用协方差:
List<Hello> hellos = new ArrayList<Hello>();
List<? extends Greeting> g = hellos;
g.add(new Goodbye());
Hello h = hellos.get(0);
这一次,第 3 行编译失败——你不能对协变列表做add
任何事情。那是因为它应该是这样工作的!
不过这是个好消息!编写的代码根本没有意义,所以它不编译很好。但是如果你只是想让它编译(知道在运行时必须让步),legacy 会这样做:
List<Hello> hellos = new ArrayList<Hello>();
List raw = hellos;
raw.add(new Goodbye());
Hello h = hellos.get(0);
以上编译,但它会发出警告。如果运行它,最后一行将导致抛出 ClassCastException,即使第 4 行不包含强制转换。这就是警告有效地告诉你的(好吧,但是,如果你的断言在这里是错误的,你会在奇怪的地方得到 ClassCastExceptions。如果你继续,你的葬礼)。
List<Greeting>
是问候对象的列表,由于基本的 java 是协变的,它可以是 Hello 或 Goodbye 实例(它永远不可能是 Greeting 本身的实例,因为那是一个接口!)
List<? extends Greeting>
是一些未知类型的列表,但我可以向你保证,它至少是问候语。不太一样。因为我们不知道它实际上是什么,你不能add
,addAll
等等。(好吧,除了list.add(null)
编译和运行良好的荒谬学术案例,但仅仅是因为null
它绝对是任何引用类型的有效值。
现在让我们回到逆变:它很有用!有了逆变,你根本不能调用get()
(好吧,你可以,而且你Object
回来了,只是因为所有的 ref 类型都必须是对象,不管发生什么),但是你可以调用add
! List<? super Hello>
is List<Hello>
or List<Greeting>
or List<Object>
,并且调用.add(new Hello())
其中任何一个都很好。
这是一个方便的表:
代码 | 类型名称 | 兼容类型 | 得到? | 添加? | 安全的? |
---|---|---|---|---|---|
列表 | 不变的 | 只要Greeting |
是的 | 是的 | 是的 |
列表<? 扩展问候> | 协变 | Greeting 或任何亚型 |
是的 | 不! | 是的 |
列表<? 超级问候> | 逆变的 | Greeting 或者Object |
不! | 是的 | 是的 |
列表 | 原始/遗留 | 任何你想要的 | 是的 | 是的 | 不! |
您的片段,但已修复。
public static ArrayList<Greeting> getGreetings() { return new ArrayList<Hello>(); }
这不起作用,因为您可以将 a 添加Goodbye
到 a List<Greeting>
,但不能添加到 a List<Hello>
。修理:
public static ArrayList<? extends Greeting> getGreetings() {
return new ArrayList<Hello>();
}
public static <O extends Greeting> O getGenericGreeting() { return new Hello(); }
这段代码说:有一些未知类型。我们所知道的只是它扩展了Greeting
. 该方法应该返回该类型的表达式。
显然,不可能,除非你 [A] 永远不会返回(只是throws
或内部无限循环),或者 [B] 你得到了正确类型的东西,例如你有一个参数,O thingie
或者List<O> thingie
[C] 你返回null
或 [ D] 你添加了一些丑陋的、编译器警告生成的断言。
更一般地说,泛型是 javac 想象的虚构,所以它们“链接”事物:如果你声明一个类型 var 并使用它零次或一次,它要么没用,要么你在做一些语言黑客。换句话说,作为一般规则,如果你使用类型 var 一次或零次,你就搞砸了。
无论如何,编译器不知道 O 可能是什么。Goodbye
这可能new Hello()
不是一个有效的回报。
要解决这个问题:
public static Hello getGenericGreeting() {
return new Hello();
}
也许:
public static <O extends Greeting> O getGenericGreeting(Class<O> type) {
return type.getConstructor().newInstance();
}
推荐阅读
- node.js - DiscordJS 音乐机器人连接,然后立即与语音频道断开连接
- html - 如何使视频的宽度为 100% 或高度为 100%
- swift - 覆盖扩展中的方法,Swift
- google-apps-script - 使用 Google Script 在 GDrive 中获取 XLSX 文件的 URL
- javascript - 反应菜单输入:选中
- sorting - 从切片中对每两个元素进行分组的最佳方法?
- java - 签名算法 SHA256withRSA 在 Java 中失败
- gradle - 在本地环境中跳过 Gradle 任务
- pscp - pscp - 用户名和密码中的特殊字符
- java - 因缺少数据库连接而挂起