我对java泛型如何处理继承/多态性有点困惑。
假设以下层次结构-
动物(父母)
狗-猫(儿童)
所以假设我有一个方法 doSomething(List<Animal> animals)
. 根据继承和多态性的所有规则,我假设 List<Dog>
是一个 List<Animal>
和一个 List<Cat>
是一个 List<Animal>
-所以任何一个都可以传递给这个方法。不是这样的。如果我想实现这个行为,我必须明确地告诉方法接受动物的任何子类的列表 doSomething(List<? extends Animal> animals)
.
我知道这是java的行为。我的问题是为什么?为什么多态性通常是隐式的,但当涉及泛型时必须指定它?
13条答案
按热度按时间xlpyo6sf1#
对于参数化类型,子类型是不变的。甚至在班上都很难
Dog
是的子类型Animal
,参数化类型List<Dog>
不是的子类型List<Animal>
. 相反,协变子类型由数组使用,因此数组类型Dog[]
是的子类型Animal[]
.不变子类型确保不违反java强制的类型约束。考虑@jon skeet给出的以下代码:
正如@jon skeet所说的,这个代码是非法的,因为否则它将违反类型约束,在狗需要时返回cat。
将上述代码与数组的类似代码进行比较是有指导意义的。
代码是合法的。但是,引发数组存储异常。数组在运行时携带其类型,这样jvm就可以强制执行协变子类型的类型安全性。
为了进一步理解这一点,让我们看看
javap
以下类别:使用命令
javap -c Demonstration
,这显示以下java字节码:请注意,方法体的翻译代码是相同的。编译器将每个参数化类型替换为其擦除。这个属性至关重要,这意味着它没有破坏向后兼容性。
总之,参数化类型的运行时安全性是不可能的,因为编译器用其擦除来替换每个参数化类型。这使得参数化类型只不过是语法糖。
g6baxovj2#
如果确定列表项是给定超类型的子类,则可以使用以下方法强制转换列表:
当您希望在构造函数内部传递列表或对其进行迭代时,这是非常有用的。
jdg4fx2g3#
答案和其他答案都是正确的。我将用一个我认为有用的解决方案来补充这些答案。我认为这在编程中经常出现。需要注意的一点是,对于集合(列表、集合等),主要问题是添加到集合中。这就是事情发生的地方。即使移除也可以。
在大多数情况下,我们可以使用
Collection<? extends T>
而不是Collection<T>
这应该是第一选择。然而,我发现这样做并不容易。至于这是否总是最好的办法,还有待讨论。我在这里展示一个类downcastcollection,它可以转换Collection<? extends T>
到Collection<T>
(我们可以为list、set、navigableset等定义类似的类)在使用标准方法时使用非常不方便。下面是一个如何使用它的示例(我们也可以使用Collection<? extends Object>
在本例中,我只是简单地说明如何使用downcastcollection。现在上课:
}
oyjwcjzk4#
让我们以javase教程中的示例为例
因此,为什么狗(圆圈)的列表不应被视为隐含的动物(形状)列表,是因为这种情况:
所以java“架构师”有两个选项来解决这个问题:
不要认为子类型隐式地是它的父类型,并给出一个编译错误,就像现在发生的那样
将子类型视为它的超类型a
pjngdqdw5#
不,是的
List<Dog>
不是一个List<Animal>
. 考虑一下你能用一个List<Animal>
-你可以添加任何动物。。。包括一只猫。现在,你能给一窝小狗加一只猫吗?绝对不是。突然你有一只非常困惑的猫。
现在,你不能添加
Cat
到List<? extends Animal>
因为你不知道这是一个List<Cat>
. 您可以检索一个值并知道它将是一个Animal
,但不能添加任意的动物。反之亦然List<? super Animal>
-在这种情况下,您可以添加Animal
但你不知道从中可以得到什么,因为它可能是一个List<Object>
.7gs2gvoe6#
你要找的是所谓的协变类型参数。这意味着如果一种类型的对象可以在方法中替换为另一种类型的对象(例如,
Animal
可以替换为Dog
),这同样适用于使用这些对象的表达式(因此List<Animal>
可以替换为List<Dog>
). 问题是协方差通常对于可变列表是不安全的。假设你有一个List<Dog>
,它被用作List<Animal>
. 如果你想把猫加到这里面怎么办List<Animal>
这真是一个List<Dog>
? 自动允许类型参数协变会破坏类型系统。添加语法以允许将类型参数指定为协变参数是很有用的,这样可以避免
? extends Foo
但这确实增加了额外的复杂性。xtfmy6hx7#
原因是
List<Dog>
不是一个List<Animal>
,例如,可以插入Cat
变成一个List<Animal>
,但不会变成List<Dog>
... 您可以使用通配符使泛型在可能的情况下更具可扩展性;例如,从一个List<Dog>
类似于从一个List<Animal>
--但不是写作。java语言中的泛型和java教程中关于泛型的部分都有一个非常好的、深入的解释,解释了为什么有些东西是多态的或不多态的,或者泛型允许的。
yws3nbqq8#
我认为应该补充一点,其他答案提到的是
List<Dog>
不是吗List<Animal>
在java中这也是事实
狗的名单是英文的动物名单(根据合理的解释)
op直觉的运作方式——这当然是完全有效的——是后一句话。然而,如果我们应用这种直觉,我们会得到一种在类型系统中不是java风格的语言:假设我们的语言允许在狗的列表中添加一只猫。那是什么意思?这意味着名单不再是狗的名单,而仅仅是动物的名单。一份哺乳动物的名单,一份四足动物的名单。
换一种说法:a
List<Dog>
在java中,在英语中并不是“狗的列表”的意思,它的意思是“可以有狗的列表,没有别的”。更一般地说,op的直觉倾向于一种语言,在这种语言中,对对象的操作可以改变它们的类型,或者更确切地说,对象的类型是其值的(动态)函数。
1yjd4xko9#
我想说,泛型的全部要点是它不允许这样做。考虑数组的情况,数组允许这种协方差:
这段代码编译得很好,但会引发运行时错误(
java.lang.ArrayStoreException: java.lang.Boolean
在第二行)。这是不安全的。泛型的要点是添加编译时类型安全性,否则您只能使用没有泛型的普通类。现在有些时候你需要更加灵活,这就是
? super Class
以及? extends Class
是给你的。前者是需要插入到类型中的时候Collection
(例如),后者用于需要以类型安全的方式从中读取的情况。但同时做这两件事的唯一方法是有一个特定的类型。egdjgwm810#
为了理解这个问题,比较数组是很有用的。
List<Dog>
不是的子类List<Animal>
.但是
Dog[]
是的子类Animal[]
.数组是可重定和协变的。
refiable意味着它们的类型信息在运行时完全可用。
因此,数组提供运行时类型安全,但不提供编译时类型安全。
对于泛型,则相反:
泛型被删除并保持不变。
因此,泛型不能提供运行时类型安全,但它们提供编译时类型安全。
在下面的代码中,如果泛型是协变的,就有可能在第3行造成堆污染。
o0lyfsai11#
这里给出的答案并不能完全说服我。因此,我举另一个例子。
听起来不错,不是吗?但你只能通过
Consumer
s和Supplier
s代表Animal
s。如果你有Mammal
消费者,但是Duck
供应商,他们不应该适合虽然都是动物。为了不允许这样做,增加了额外的限制。我们必须定义所使用类型之间的关系,而不是上述内容。
e。g、 哦。,
确保我们只能使用为消费者提供正确对象类型的供应商。
奥托,我们也可以
我们走另一条路:我们定义
Supplier
并限制它可以放入Consumer
.我们甚至可以
其中,直觉关系
Life
->Animal
->Mammal
->Dog
,Cat
等等,我们甚至可以Mammal
变成一个Life
消费者,但不是String
变成一个Life
消费者。bfnvny8b12#
这种行为的基本逻辑是
Generics
遵循类型擦除机制。因此,在运行时您无法识别collection
不像arrays
在没有这种擦除过程的情况下。回到你的问题上来。。。所以假设有一种方法如下:
现在,如果java允许调用者将类型animal的列表添加到这个方法中,那么您可能会将错误的东西添加到集合中,并且在运行时它也会由于类型擦除而运行。而在数组的情况下,对于这样的场景,您将得到一个运行时异常。。。
因此在本质上,这种行为的实现是为了不向集合中添加错误的东西。现在我相信类型擦除的存在是为了在没有泛型的情况下提供与遗留java的兼容性。。。。
wa7juj8i13#
实际上,你可以使用一个接口来实现你想要的。
}
然后可以使用