关于Java泛型如何处理继承/多态,我有点困惑 .
假设以下层次结构 -
Animal (家长)
Dog - Cat (儿童)
所以假设我有一个方法 doSomething(List<Animal> animals)
. 通过所有继承和多态的规则,我会假设 List<Dog>
是 List<Animal>
而 List<Cat>
是 List<Animal>
- 因此任何一个都可以传递给这个方法 . 不是这样 . 如果我想实现这种行为,我必须通过说 doSomething(List<? extends Animal> animals)
明确告诉方法接受Animal的任何子类的列表 .
我知道这是Java的行为 . 我的问题是为什么?为什么多态通常是隐含的,但是当涉及泛型时必须指定它?
16 回答
您正在寻找的是covariant type参数 . 这意味着如果一种类型的对象可以替换方法中的另一种类型(例如,
Animal
可以替换为Dog
),则同样适用于使用这些对象的表达式(因此List<Animal>
可以替换为List<Dog>
) . 问题是协方差一般对于可变列表是不安全的 . 假设你有List<Dog>
,它被用作List<Animal>
. 当您尝试将Cat添加到List<Animal>
时会发生什么?这真的是List<Dog>
?自动允许类型参数协变会破坏类型系统 .添加语法以允许将类型参数指定为协变是有用的,这避免了方法声明中的
? extends Foo
,但这确实增加了额外的复杂性 .我们还应该考虑编译器如何威胁泛型类:每当我们填充泛型参数时,“实例化”一个不同的类型 .
因此,我们有
ListOfAnimal
,ListOfDog
,ListOfCat
等,这些是我们指定泛型参数时编译器最终成为"created"的不同类 . 这是一个扁平的层次结构(实际上关于List
根本不是一个层次结构) .在泛型类的情况下,为什么协方差没有意义的另一个论点是,在所有类都相同的情况下是
List
实例 . 通过填充泛型参数来专门化List
不会扩展类,它只是使它适用于该特定的泛型参数 .如果您确定列表项是给定超类型的子类,则可以使用以下方法强制转换列表:
当您想要在构造函数中传递列表或迭代它时,这非常有用
不,
List<Dog>
不是List<Animal>
. 考虑一下你可以用_268829做什么 - 你可以添加任何动物......包括一只猫 . 现在,你可以在逻辑上将一只猫添加到一窝幼犬吗?绝对不 .突然间你有一只非常困惑的猫 .
现在,您无法将
Cat
添加到List<? extends Animal>
,因为您没有't know it'是List<Cat>
. 您可以检索一个值并知道它将是Animal
,但您无法添加任意动物 .List<? super Animal>
反之亦然 - 在这种情况下,您可以安全地添加Animal
,但您不知道从中检索到什么,因为它可能是List<Object>
.这个问题已得到很好的证实 . 但是有一个解决方案; make doSomething generic:
现在,您可以使用List <Dog>或List <Cat>或List <Animal>调用doSomething .
answer以及其他答案都是正确的 . 我将使用我认为有用的解决方案来添加这些答案 . 我认为这经常出现在编程中 . 需要注意的一点是,对于集合(列表,集等),主要问题是添加到集合 . 事情就是崩溃的地方 . 即使删除也行 .
在大多数情况下,我们可以使用
Collection<? extends T>
而不是Collection<T>
,这应该是第一选择 . 但是,我发现这样做并不容易 . 关于这是否总是最好的事情,我们有争议 . 我在这里提出一个类DownCastCollection,它可以将Collection<? extends T>
转换为Collection<T>
(我们可以为List,Set,NavigableSet,...定义类似的类),当使用标准方法时非常不方便使用 . 下面是一个如何使用它的示例(在这种情况下我们也可以使用Collection<? extends Object>
,但我保持简单,使用DownCastCollection来说明 .现在上课:
}
另一个解决方案是 Build 一个新的列表
List<Dog>
不是List<Animal>
的原因是,例如,你可以将Cat
插入List<Animal>
,但不能插入List<Dog>
...你可以使用通配符使generics在可能的情况下更具可扩展性;例如,从List<Dog>
读取类似于从List<Animal>
读取 - 但不是写作 .Generics in the Java Language和Section on Generics from the Java Tutorials有一个非常好的,深入的解释,为什么有些东西是多态的或者不是泛型的 .
我认为应该添加一点otheranswers提到就是这样
这也是事实
OP的直觉起作用的方式 - 当然是完全有效的 - 是后一句 . 但是,如果我们应用这种直觉,我们会在其类型系统中获得一种非Java风格的语言:假设我们的语言允许将猫添加到我们的狗列表中 . 那是什么意思?这将意味着该名单不再是一个狗的名单,而只是一个动物名单 . 以及一系列哺乳动物和一系列四足动物 .
换句话说:Java中的
List<Dog>
并不意味着"a list of dogs"的英文,它意味着"a list which can have dogs, and nothing else" .更一般地说, OP's intuition lends itself towards a language in which operations on objects can change their type ,或者更确切地说,对象的类型是其值的(动态)函数 .
让我们从JavaSE中获取示例tutorial
那么为什么不应该将狗(圆圈)列表隐含地视为动物(形状)列表,因为这种情况:
所以Java“架构师”有2个选项来解决这个问题:
不要认为子类型是隐式的超类型,并给出编译错误,就像它现在发生的那样
认为子类型是它的超类型并且在编译时限制“添加”方法(因此在drawAll方法中,如果将传递圆形列表,形状的子类型,编译器应该检测到并限制编译错误到这样做) .
出于显而易见的原因,这选择了第一种方式 .
实际上,您可以使用界面来实现您想要的效果 .
}
然后,您可以使用集合
要理解这个问题,与数组进行比较是有用的 .
List<Dog>
是 notList<Animal>
的子类 .But
Dog[]
isAnimal[]
的子类 .Arrays are reifiable and covariant .
Reifiable表示其类型信息在运行时完全可用 .
因此,数组提供运行时类型安全性但不提供编译时类型安全性 .
对于泛型,反之亦然:
Generics are erased and invariant .
因此泛型不能提供运行时类型安全性,但它们提供编译时类型安全性 .
在下面的代码中,如果泛型是协变的,则可以在第3行进行heap pollution .
这里给出的答案并没有完全说服我 . 相反,我做了另一个例子 .
听起来不错,不是吗?但是你只能通过
Consumer
和Supplier
来获取Animal
. 如果你有一个Mammal
消费者,但是他们不应该适合,尽管两者都是动物 . 为了禁止这种情况,增加了额外的限制 .而不是上述,我们必须定义我们使用的类型之间的关系 .
E. g . ,
确保我们只能使用为消费者提供正确类型对象的供应商 .
OTOH,我们也可以这样做
我们走另一条路:我们定义
Supplier
的类型并限制它可以放入Consumer
.我们甚至可以做到
在哪里,有一个直观的关系
Life
- >Animal
- >Mammal
- >Dog
,Cat
等,我们甚至可以将Mammal
放入Life
消费者,而不是String
进入Life
消费者 .对于参数化类型,子类型是invariant . 即使类
Dog
是Animal
的子类型,参数化类型List<Dog>
也不是List<Animal>
的子类型 . 相反,数组使用covariant子类型,因此数组类型Dog[]
是Animal[]
的子类型 .不变子类型确保不违反Java强制执行的类型约束 . 考虑@Jon Skeet给出的以下代码:
正如@Jon Skeet所说,这段代码是非法的,因为否则它会在狗预期时通过返回猫来违反类型限制 .
将上述内容与数组的类似代码进行比较是有益的 .
代码是合法的 . 但是,抛出一个array store exception . 数组在运行时携带其类型,这样JVM可以强制执行协变子类型的类型安全性 .
为了进一步理解这一点,让我们看一下下面类的
javap
生成的字节码:使用命令
javap -c Demonstration
,它显示以下Java字节码:注意方法体的翻译代码是相同的 . 编译器用erasure替换了每个参数化类型 . 此属性至关重要,意味着它不会破坏向后兼容性 .
总之,参数化类型无法实现运行时安全性,因为编译器会通过擦除替换每个参数化类型 . 这使得参数化类型只不过是语法糖 .
这种行为的基本逻辑是
Generics
遵循类型擦除机制 . 所以在运行时你无法确定collection
的类型,而不像arrays
那样没有这样的擦除过程 . 所以回到你的问题......所以假设有一种方法如下:
现在,如果java允许调用者向此方法添加类型为Animal的List,然后您可能会将错误的东西添加到集合中,并且在运行时它也会因类型擦除而运行 . 在阵列的情况下,您将获得此类场景的运行时异常...
因此,本质上实现了这种行为,以便人们不能将错误的东西添加到集合中 . 现在我相信类型擦除存在,以便与没有泛型的传统java兼容....
我会说泛型的全部意义在于它不允许这样做 . 考虑数组的情况,它允许这种类型的协方差:
该代码编译良好,但抛出运行时错误(第二行中的
java.lang.ArrayStoreException: java.lang.Boolean
) . 它不是类型安全的 . 泛型的要点是添加编译时类型安全性,否则你可以坚持使用没有泛型的普通类 .现在有时你需要更灵活,这就是
? super Class
和? extends Class
的用途 . 前者是需要插入类型Collection
(例如),后者是需要以类型安全的方式从中读取时 . 但同时做两者的唯一方法就是拥有一种特定的类型 .