如何创建具有相同超类的多种类型的列表,而不会丢失类型 [英] How do I create a list with multiple types that have the same superclass, without losing the type

查看:23
本文介绍了如何创建具有相同超类的多种类型的列表,而不会丢失类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我有一个抽象类 Map 带有 BaseElementArrayList.

So I have an abstract class Map with an ArrayList of BaseElement's.

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

BaseElement is also an abstract class, which is extended by three classes: Node, Way and Relation. It only has an attribute called id.

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

Inside the class Map i want to have a addElement method and a getElement method. The addElement method should add the element to the list and return the element back, without losing it's type. The getElement should find the element inside the list, by it's id and return it (also without losing the type).

现在我尝试使用类型泛型来做到这一点,但我对类型泛型仍然很陌生.

Now I tried to use type generics to do this, but I'm still pretty new to type generics.

目前这是我的 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 添加一个元素,就像这样...:

For example I want to add an element with addElement like so...:

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 再次获取元素,如下所示:

and get the element again with getElement like so:

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

我真的不知道如何做到这一点,而不破坏这里的逻辑.我必须在一个列表中存储从 BaseElement 扩展的多个对象,而不会丢失它们的类型.

I'm really not sure how to do this, without breaking the logic here. I have to store multiple objects that extend from BaseElement in a list, without losing their type.

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

Can I do this with type generics? Or do I have to use another technique?

推荐答案

tl;dr

从精通泛型的集合(具有参数化类型)中检索时,对象不会丢失其类型.您可以使用 instanceof 测试子类型,然后进行转换.

tl;dr

An object does not lose its type when retrieved from a Generics-savvy collection (having a parameterized type). You can test for the subtype by using instanceof and then cast.

您询问了如何对从启用泛型的集合中检索到的对象调用特定于子类的方法:

You asked how to call a subclass-specific method on an object retrieved from Generics-enabled collection:

new EarthMap().getElement(0).getLocation()//getLocation() 定义在类 Node 中,而不是在 BaseElement 中

使用 Java 中的最新特性,这项工作可以简单到:

Using the latest features in Java, this work can be as simple as:

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();
}

... 编译器验证您已涵盖所有可能的情况.

… with the compiler verifying you have covered all possible cases.

可以使用这种方法来代替评论中提到的繁琐的访问者模式这个问题.模式匹配和密封类特性通常会使代码更加透明和直接.

Such an approach can be used instead of the cumbersome Visitor Pattern mentioned in the Comments on this Question. The pattern-matching and sealed-class features will generally make for more transparent and straightforward code.

当您使用 Java 泛型从容器中检索对象时,该对象不会失去它的类型".对象的类型被屏蔽,作为集合上参数化的更通用类型返回.但是对象仍然知道它自己的类型.

When you retrieve an object from a container using Java Generics, the object does not "lose its type". The object’s type is masked, being returned as the more general type parameterized on the collection. But the object still knows its own type.

我们可以通过多态来验证这个事实.如果子类有自己独特的行为,我们应该看到该行为,而不是保留其类型的超类行为.

We can verify this fact by way of polymorphism. If the subclasses have their own distinct behavior, we should see that behavior rather than the superclass behavior if their type is preserved.

让我们举一个简单的例子,Animal 的超类有两个子类,Dog &.

Let's use a simple example, a superclass of Animal with two subclasses, 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 方法的应用.

An app to call eat method on each of the animals in a sample collection.

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.

所以,我们可以看到猫仍然知道它们是猫,而狗仍然知道它是狗.

So, we can see the cats still know they are cats, and the dog still knows it is a dog.

您的问题是关于定义在超类上的方法,特定于特定子类的方法.

Your Question asks about methods not defined on the superclass, methods that are specific to a particular subclass.

为此,让我们添加 Dog#snooze 方法和 Cat#petMe 方法,每个方法都只针对该类.

For that, let’s add Dog#snooze method and a Cat#petMe method, each specific to that class only.

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 转换为更具体的具体类.

To call those specific methods, we need to cast each object from the generic Animal to its more specific concrete class.

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:instanceof 的模式匹配.

Pattern Matching for instanceof

In Java 16 and later, we can now shorten that code testing instanceof. On a single line we can test, cast, and assign to a variable. See 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 中预览的新功能,这项工作变得更加简单.

Pattern Matching for switch

This work gets even simpler using a new feature being previewed in Java 17.

JEP 406:切换模式匹配(预览版)instanceof 模式匹配带入 switch 语句.

JEP 406: Pattern Matching for switch (Preview) brings that instanceof pattern-matching to the switch statement.

注意:您需要设置您的项目 &运行时以启用此尚未官方的功能.默认情况下,禁用.

Beware: You need to set your project & runtime to enable this not-yet-official feature. By default, disabled.

// 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 带来了另一个新特性:密封类.在密封类中,您为特定超类声明所有可能的子类.然后,超类被认为是关闭的,无法被任何其他人扩展.因此,编译器可以在编译时识别所有子类的列表.

Sealed classes

Java 17 brings another new feature: Sealed classes. In a sealed class, you declare for a particular superclass all possible subclasses. The superclass is then considered closed for extension by any others. Therefore, the compiler can identify, at compile-time, a list of all subclasses.

请参阅:JEP 409:密封类

注意上面代码中 switch 底部的 default.如果我们没有考虑到 Animal 的所有可能的子类型,它可以作为一个包罗万象的工具.编译器可以利用已知子类的密封类列表来判断我们的 switch 语句是否涵盖了所有可能的情况.

Notice the default at bottom of that switch in our code above. It serves as a catch-all in case we have not accounted for all possible sub-types of Animal. The compiler can make use of the sealed-class list of known subclasses to tell if our switch statement has covered all possible cases.

如果我们密封我们的动物、狗和猫类:

If we seal our animal, dog, and cat classes:

// 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),编译器同样会发出编译器错误.

… then we can further simplify that code above by omitting the default case. Furthermore, if we later add another subtype, the compiler will alert us by emitting a compiler error. If we edit our code to omit any of the subclasses (Dog case or Cat case here), the compiler likewise emits a compiler error.

// 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 对象,则检索它们并将它们视为更具体的更窄类型(NodeWay 和 <代码>关系),只需投射.或者让最新的 Java 功能为您进行转换.

Conclusion

If you want to collect BaseElement objects, then retrieve them and treat them as their more specific narrower type (Node, Way and Relation), just cast. Or let the latest Java features do the casting for you.

这篇关于如何创建具有相同超类的多种类型的列表,而不会丢失类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆