首页 > 解决方案 > 与继承类不兼容的类型

问题描述

我遇到了以下问题,感觉就像处理继承的方式不一致:

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){}
}

谁能解释为什么会这样工作,以及后两种方法是否有替代方法无法编译?

标签: javainheritance

解决方案


现实,基本上。我们从一个公理开始:说的“重点”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>是一些未知类型的列表,但我可以向你保证,它至少是问候语。不太一样。因为我们不知道它实际上是什么,你不能addaddAll等等。(好吧,除了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();
}

推荐阅读