tl;博士
一个对象在从泛型集合(具有参数化类型)中检索时不会丢失其类型。您可以使用instanceof 测试子类型,然后进行强制转换。
您询问了如何在从启用泛型的集合中检索到的对象上调用特定于子类的方法:
new EarthMap().getElement(0).getLocation() // getLocation() is defined in class Node, not in BaseElement
使用 Java 中的最新功能,这项工作可以很简单:
BaseElement element = new EarthMap().getElement(0) ;
switch ( element )
{
case Node node -> node.getLocation() ; // Method defined in class `Node`, not `BaseElement`.
case Way way -> way.someWaySpecificMethod();
case Relation relation -> relation.someRelationSpecificMethod();
}
... 编译器会验证您是否涵盖了所有可能的情况。
可以使用这样的方法来代替这个问题的评论中提到的繁琐的Visitor Pattern。模式匹配和密封类特性通常会使代码更加透明和直接。
类型不丢失
当您使用Java Generics 从容器中检索对象时,该对象不会“丢失其类型”。对象的类型被屏蔽,作为集合上参数化的更通用类型返回。但是对象仍然知道自己的类型。
多态性作为证明
我们可以通过多态性来验证这个事实。如果子类有自己独特的行为,我们应该看到这种行为而不是超类的行为(如果它们的类型被保留)。
让我们用一个简单的例子,Animal 的超类有两个子类Dog 和Cat。
public abstract class Animal {
public void eat () {
System.out.println( "Eating." );
}
}
public class Dog extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat and veg. Woof." );
}
}
public class Cat extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat. Meow." );
}
}
一个对样本集合中的每个动物调用eat 方法的应用。
List < Animal > animals =
List.of(
new Cat() ,
new Cat() ,
new Dog()
);
animals.stream().forEach( Animal :: eat );
运行时。
Eating meat. Meow.
Eating meat. Meow.
Eating meat and veg. Woof.
所以,我们可以看到猫仍然知道自己是猫,而狗仍然知道自己是狗。
特定于子类的方法
您的问题询问的是在超类上未定义的方法,特定于特定子类的方法。
为此,让我们添加 Dog#snooze 方法和 Cat#petMe 方法,每个方法仅特定于该类。
public class Dog extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat and veg. Woof." );
}
public void snooze () {
System.out.println( "I’m gonna take a nap at your feet, if you don’t mind." );
}
}
public class Cat extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat. Meow." );
}
public void petMe () {
System.out.println( "Pet me, now, servant!" );
}
}
要调用这些特定方法,我们需要将每个对象从通用 Animal 转换为更具体的具体类。
for ( Animal animal : animals )
{
if ( animal instanceof Dog )
{
Dog dog = ( Dog ) animal;
dog.snooze();
}
if ( animal instanceof Cat )
{
Cat cat = ( Cat ) animal;
cat.petMe();
}
}
运行时。
Pet me, now, servant!
Pet me, now, servant!
I’m gonna take a nap at your feet, if you don’t mind.
instanceof 的模式匹配
在 Java 16 及更高版本中,我们现在可以缩短代码测试 instanceof。在一行中,我们可以测试、强制转换和分配给变量。见JEP 394: Pattern Matching for instanceof。
// Java 16+ ➤ JEP 394: Pattern Matching for instanceof
for ( Animal animal : animals )
{
if ( animal instanceof Dog dog )
{
{
dog.snooze();
}
}
else if ( animal instanceof Cat cat )
{
cat.petMe();
}
else
{
System.out.println( "Oops, encountered unexpected type of animal." );
}
}
switch 的模式匹配
使用 Java 17 中预览的新功能使这项工作变得更加简单。
JEP 406: Pattern Matching for switch (Preview) 将 instanceof 模式匹配带到 switch 语句中。
注意:您需要设置您的项目和运行时以启用此尚未正式发布的功能。默认情况下,禁用。
// Preview feature in Java 17 ➤ JEP 406: Pattern Matching for switch (Preview)
for ( Animal animal : animals )
{
switch ( animal )
{
case Dog dog -> dog.snooze();
case Cat cat -> cat.petMe();
case null -> System.out.println( "Oops, no animal." );
default -> System.out.println( "Oops, encountered unexpected type of animal." );
}
}
密封类
Java 17 带来了另一个新特性:密封类。在密封类中,您为特定的超类声明所有可能的子类。然后超类被认为是封闭的,可以被任何其他人扩展。因此,编译器可以在编译时识别所有子类的列表。
见:JEP 409: Sealed Classes
请注意上面代码中switch 底部的default。如果我们没有考虑到Animal 的所有可能子类型,它可以作为一个包罗万象的方法。编译器可以利用已知子类的密封类列表来判断我们的switch 语句是否涵盖了所有可能的情况。
如果我们密封我们的动物、狗和猫类:
// Mark the class as `sealed`.
public abstract sealed class Animal permits Cat, Dog
{
public void eat () {
System.out.println( "Eating." );
}
}
// Mark as `final` as one way to seal the superclass.
public final class Dog extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat and veg. Woof." );
}
public void snooze () {
System.out.println( "I’m gonna take a nap at your feet, if you don’t mind." );
}
}
// Mark as `final` as one way to seal the superclass.
public final class Cat extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat. Meow." );
}
public void petMe () {
System.out.println( "Pet me, now, servant!" );
}
}
...然后我们可以通过省略default 的情况来进一步简化上面的代码。此外,如果我们稍后添加另一个子类型,编译器将通过发出编译器错误来警告我们。如果我们编辑代码以省略任何子类(Dog case 或 Cat case 这里),编译器同样会发出编译器错误。
// Preview feature in Java 17 ➤ JEP 406: Pattern Matching for switch (Preview)
// Combined with Java 17+ feature ➤ JEP 409: Sealed Classes
for ( Animal animal : animals )
{
switch ( animal )
{
case Dog dog -> dog.snooze();
case Cat cat -> cat.petMe();
case null -> System.out.println( "Oops, no animal." );
// NO LONGER NEEDED… default -> System.out.println( "Oops, encountered unexpected type of animal." );
}
}
结论
如果您想收集BaseElement 对象,然后检索它们并将它们视为更具体的更窄类型(Node、Way 和Relation),只需转换即可。或者让最新的 Java 功能为您进行转换。