【问题标题】:How do I create a list with multiple types that have the same superclass, without losing the type如何创建具有相同超类的多个类型的列表,而不会丢失类型
【发布时间】:2021-10-18 01:04:46
【问题描述】:

所以我有一个抽象类 MapArrayListBaseElement

BaseElement也是一个抽象类,它扩展了三个类:NodeWayRelation。它只有一个名为id 的属性。

Map 类中,我想要一个addElement 方法和一个getElement 方法。 addElement 方法应该将元素添加到列表中并返回元素,而不会丢失它的类型。 getElement 应该通过它的 id 找到列表中的元素并返回它(也不会丢失类型)。

现在我尝试使用类型泛型来做到这一点,但我对类型泛型还是很陌生。

目前这是我的Map 课程:

public abstract class Map {
    private final String name;
    private final ArrayList<BaseElement> elements;

    protected Map(String name) {
        this.name = name;
        this.elements = new ArrayList<>();
        this.defineElements();
    }

    protected abstract void defineElements();

    protected <T extends BaseElement> T addElement(T element) {
        elements.add(element);
        return element;
    }

    public BaseElement getElement(int elementID) {
        return elements.stream()
                .filter(element -> element.getID() == elementID)
                .findFirst()
                .orElse(null);
    }

    public String getName() {
        return name;
    }
}

例如,我想添加一个带有addElement 的元素,就像这样...:

public class EarthMap extends Map {
    public EarthMap() {
        super("Earth");
    }

    @Override
    protected void defineElements() {
        Node exampleNode = this.addElement(BaseElementFactory.createNode(new Location(1.2345, 2.3456)));
    }
}

然后使用getElement 再次获取元素,如下所示:

public class Main {
    public static void main(String[] args) {
        new EarthMap().getElement(0).getLocation() // getLocation() is defined in class Node, not in BaseElement
    }
}

如果不破坏这里的逻辑,我真的不确定该怎么做。我必须将多个从BaseElement 扩展的对象存储在一个列表中,而不会丢失它们的类型。

我可以使用类型泛型来做到这一点吗?还是我必须使用其他技术?

【问题讨论】:

  • 您的基本前提是错误的,因为对象永远不会“丢失”它们的类型。也许您想使用双重调度机制,例如访问者设计模式
  • @HovercraftFullOfEels 但是,例如,如果我将节点添加到 BaseElements 列表中,我将无法访问节点的方法/字段,在这种情况下我需要这些方法/字段。我仍然需要弄清楚究竟是什么类型以及如何有效地使用它们。然后将研究访问者设计模式,看看它是否解决了我的问题。谢谢!
  • 您应该考虑在 BaseElement 类中创建一个“抽象位置 getLocation()”方法并在您的子类中实现它。

标签: java list types


【解决方案1】:

对代码的建议更改:

  1. public abstract class Main&lt;BaseElement&gt;

  2. private final ArrayList&lt;BaseElement&gt; elements;

  3. protected BaseElement addElement(BaseElement element)

没有必要使用extend,因为它已经是一个抽象类

之后,BaseELement 的任何子类都可以正常工作:)

【讨论】:

  • 是的,这可行,但问题是,如果我将元素从ArrayList 中取出,它将是BaseElement 而不是Node,在这种情况下我需要它。
  • 由于某种原因,第一点在我写它时没有出现,所以它应该是公共抽象类 main 你说节点是 BasElement 的子类,所以以下任何一个都可以x = new 您选择的地图子类; x.addElement(any baseelement object ) 在这里会很好,并且 getElement 将返回 baseElement ,您可以调用 baseElement 中存在的任何方法或 baseelement 的任何类超类,但不是 baseelement 的子类方法,您需要对其进行转换到它的类型,所以使用 instanceof 并将其转换为正确的类
  • 如果 B 类是 A 的子类,并且您将 B 添加到 A 类列表中,您仍然可以强制转换它或询问“instanceof”B 类,它应该可以工作。
【解决方案2】:

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 的超类有两个子类DogCat

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 对象,然后检索它们并将它们视为更具体的更窄类型(NodeWayRelation),只需转换即可。或者让最新的 Java 功能为您进行转换。

【讨论】:

  • 感谢您的精彩和广泛的解释!我不知道他们仍然知道他们的类型 :) 将实现这一点,但仍然在 Java 11 上,所以这些东西中的大多数还不能工作:D 再次感谢您!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多