如何避免在Java中使用instanceof

22
在我的应用程序中,我有一个二维实体数组来表示一个网格。 网格中的每个位置可以是空的,也可以被实体占用(在这种情况下,它只是一个人或者墙)。目前,我使用 instanceof 来检查实体是人还是墙。
我考虑给每个实体一个方法,返回一个声明他们类型的 enum,例如一堵墙的实体将返回EntityType.WALL 。我想知道是否这是去除使用 instanceof 的最佳方法,还是instanceof 在这种情况下适合?

2
对我来说,从软件设计的角度来看,“枚举”解决方案并不比“instanceof”解决方案更好。你可以使用子类型多态性更加优雅地解决这个问题(即创建子类并覆盖一些方法以实现Person/Wall/Empty特定的行为)。另一个选择是使用动态分派,这可以通过访问者模式在有限的方式下(双重分派)实现。我认为下面的答案中已经涉及了这些方法的每一个。 - DaoWen
1
注意:标题中有一个拼写错误。但是你不能修复它,因为已经有一个名为“避免在Java中使用instanceof”的问题。我建议您重新措辞标题以更具体。 - Tunaki
7个回答

42

使用“告诉,不要询问”原则:不是询问对象是什么,然后根据它们的回答作出反应,而是告诉对象应该做什么,然后墙壁或人们决定如何完成他们需要做的事情。

例如:

不要像这样:

public class Wall {
    // ...
}

public class Person {
    // ...
}

// later
public class moveTo(Position pos) {
    Object whatIsThere = pos.whatIsThere();
    if (whatIsThere instanceof Wall) {
         System.err.println("You cannot move into a wall");
    }
    else if (whatIsThere instanceof Person) {
         System.err.println("You bump into " + person.getName());
    }
    // many more else branches...
}

可以试着这样做:

public interface DungeonFeature {
    void moveInto();
}

public class Wall implements DungeonFeature {
    @Override
    public void moveInto() {
        System.err.println("You bump into a wall");
    }

   // ...
}

public class Person implements DungeonFeature {
    private String name;

    @Override
    public void moveInto() {
        System.err.println("You bump into " + name);
    }

    // ...
}

// and later
public void moveTo(Position pos) {
    DungeonFeature df = currentPosition();
    df.moveTo(pos);
}

这有一些优点。

首先,每次添加新的地牢特征时,您不需要调整一个巨大的if then else树。

其次,地牢特征中的代码是自包含的,逻辑全部在该对象中。您可以轻松测试和移动它。


1
我喜欢这个答案,但我认为你需要再扩展一下。也许你可以添加一个非常简单的例子来说明你的观点。 - DaoWen

9

一种精细的方式中消除instanceof的理论解决方案是使用访问者模式。它的工作原理是需要知道其他元素是墙还是人的对象调用该对象,并将自身作为参数传递,该特定对象回调并提供有关其类型的信息。

例如,

public class Person {
    void magic() {
        if(grid.getAdjacent() instanceof Person) {
            Person otherPerson = (Person)grid.getAdjacent();
            doSomethingWith(otherPerson);
        } else if(grid.getAdjacent() instanceof Wall) {
            Wall wall = (Wall)grid.getAdjacent();
            doOtherThingWith(wall);
        }
    }
}

可以变成

public class Person extends Entity {
    void magic() {
        grid.getAdjacent().visit(this);
    }

    void onVisit(Wall wall) {
        doOtherThingWith(wall);
    }

    void onVisit(Person person) {
        doSomethingWith(person);
    }

    public void visit(Person person) {
        person.onVisit(this);
    }
}

public class Wall extends Entity { 
    public void visit(Person person) {
        person.onVisit(this);
    }
}

1
Empty的情况怎么处理?(我知道对你来说添加一个空的情况可能很明显,但如果提问者以前从未见过访问者模式,那么这可能对他们来说并不那么明显。) - DaoWen

1
我会让人和墙继承一个抽象超类(例如Tile),该超类具有一个返回枚举或整数的getType()方法,并在Wall和Person中实现此方法以返回适当的值。

这个可以吗?public abstract class Entity { public abstract EntityType getEntityType(); }public class Wall extends Entity { private static final EntityType type = EntityType.PEDESTRIAN; @Override public EntityType getEntityType() { return type; }} - John Radke
不要使用继承,最好在这里使用接口(例如Renderable),因为墙壁和人都不是瓷砖。 - Mick Mnemonic
抱歉,愚蠢的5分钟规则 ~我是否可以强制实现Entity接口的类具有EntityType字段且不能为空?如果我将该字段添加到抽象类中,则某个类可以扩展Entity但不设置EntityType,如果这有意义的话。 - John Radke
@MickMnemonic 为什么它不是一个瓦片?它在一个网格中 - 但你在界面方面是正确的 - 这也是一个有效的选项。 - ligi
@JohnRadke 你可以使用“@NonNull”注释。 - ligi
@JohnRadke @ligi 或者让抽象类的构造函数接受 EntityType,并在其为 null 时抛出 IllegalArgumentException - Thierry

1
如果你按照其他答案实现访问者模式或使用枚举,就不会出错。但是,思考一下你想用切换逻辑(无论是instanceof还是访问者)做什么可能也有帮助,因为有时候有更简单的方法做到这一点。例如,如果你只想检查一个实体是否以阻塞方式占据了网格,那么你可以通过接口为每个实体添加一个方法boolean isSolid()。你可以使用默认方法来增加美观性:
public interface GridPhysics {
    default boolean isSolid() {
        return true;
    }

    // other grid physics stuff
}

public class Wall implements GridPhysics {
    // nothing to do here, it uses the default
}

// in your game logic
public boolean canMoveTo(GridPhysics gridCell) {
    return !gridCell.isSolid() && otherChecks();
}

您可能还想看一下实体组件系统(例如 Artemis),这基本上将“组合优于继承”的思想发挥到了极致。


0

这里的答案非常好,无话可说,但如果我处于这样的情况并且允许的话,我会用可能值为0(默认为空)和1、2(人或墙)的二维整数数组。


0

ligi的回答非常准确。(谁给它点了踩,我想知道他们在想什么。)作为另一种选择,考虑以下内容:

abstract class Tile
{
    public final EntityType type;

    protected Tile( EntityType type )
    {
        this.type = type;
    }
}

abstract class Pedestrian extends Tile
{
    public Pedestrian()
    {
        super( EntityType.PEDESTRIAN );
    }
}

abstract class Wall extends Tile
{
    public Wall()
    {
        super( EntityType.WALL );
    }
}

这背后的原理是,实体的“类型”是实体的永久特征,因此适合在构造函数中指定并在final成员字段中实现。如果它由虚拟方法(Java术语中的非最终方法)返回,则后代可以自由地在某个时间点返回一个值,在另一个时间点返回另一个值,这将导致混乱。
哦,如果你真的无法忍受公共最终成员,请添加一个getter,但我的建议是不要在意纯粹主义者,没有getter的公共最终成员完全没问题。

3
通过添加枚举并提供与instanceOf相同的信息,您并没有增加任何价值。解决这个问题的方法是多态和正确使用面向对象的原则。 - Tim B

-2

此问题所述,现代Java编译器在像instanceof这样的操作上非常高效。您可以放心使用它。

事实上,其他提供的答案之一测试了instanceOf和字符串比较,结果instanceOf明显更快。我建议您继续使用它。


5
instanceof的问题不在于效率,而在于软件设计。使用instanceof被认为是一种“代码异味”,表明您应该真正使用更好的软件抽象来解决问题(根据我的经验,通常是子类多态性或动态分派)。 - DaoWen
1
“instanceof” 是一种设计上的不良迹象,但这又怎样呢?在我的观点里,“你不需要它(YAGNI)”才是最重要的。应用程序是否完成了你需要它完成的任务?是 - 那么你就完成了;不是 - 有什么问题吗?也许修复此代码问题会导致修复“实例化”的问题,也可能不会。 - emory
2
@MikeNakis - 我其实不是最初的投票者。如果下投票的标准仅仅是“这个答案没有用”,那么我认为这绝对应该被投票(OP从未表达过对instanceof效率的担忧)。然而,我一直以来的印象是,投票实际上意味着“这个答案是误导性的错误的”。无论哪种情况,我认为这个答案在推广使用一个非常恶心的临时解决方案,而更优雅(可能也更有效!)的解决方案是使用多态性。 - DaoWen
1
@emory - 我认为在某些情况下,instanceof是合理的(因为使用更“优雅”的解决方案只会导致不必要的复杂性),但在这种情况下,我认为Robert的建议(即使用多态)更简单(在概念上和代码复杂度上),更易于维护,并且可能更有效率 - 因此,坚持使用一系列instanceof检查的临时解决方案对我来说似乎是一个可怕的建议。 - DaoWen
5
为每个行为在每种对象类型上添加一个方法的代码占用和使用每个情况的 instanceof 检查大致相同,但它具有将算法的不变部分与变量(特定于类型)行为分离的附加概念上的好处。以这种方式使用继承也可以允许编译器为您进行检查(例如,如果我添加一个新的“Monster”类型,它也可以占用一个方块,则编译器会在我没有提供所有对应操作的方法时发出警告,但如果我使用一系列的 instanceof,则不会)。 - DaoWen
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接