避免在Java中使用instanceof

17

在某个阶段的大学里(并且随后在很多地方读到),我被告知只有在“万不得已”的情况下才应该使用 instanceof。基于这个想法,有人能告诉我以下代码是否是最后的选择吗?我在stackoverflow上看了一下,但似乎没有找到类似的场景——也许我错过了?

private void allocateUITweenManager() {
   for(GameObject go:mGameObjects){
      if (go instanceof GameGroup) ((GameGroup) go).setUITweenManager(mUITweenManager);
   }
}

其中

  • mGameObjects是一个数组,其中只有一些元素的类型是GameGroup
  • GameGroup是抽象类GameObject的子类。
  • GameGroup使用接口UITweenable,该接口具有方法setUITweenManager()
  • GameObject不使用接口UITweenable

我想我可以同样(可能应该)将我的代码中的GameGroup替换为UITweenable - 我会问相同的问题。

是否有另一种避免使用instanceof的方法?这段代码不能失败,(我想,对吧?),但是考虑到instanceof似乎经常被人诟病,我在OOP方面犯了什么基本错误使我在这里使用了instanceof吗?

提前致谢!


我认为可以使用 instanceof。 - Arun P Johny
我认为在这种情况下,您可以使用instanceof,因为所有其他解决方法似乎都很复杂(包括多态性)。此外,请看一下这篇文章,您会发现它很有趣。 - Rahul Tripathi
5
循环需要实现UITweenable接口(而非特别是GameGroup的实例),因此最好使用go instanceof UITweenable - Ted Hopp
2
还要提一下,使用 instanceof 不会对性能造成任何显著影响。 - Paul Richter
9个回答

5
我在大学编译课程中学习了“访问者模式”,我认为它可能适用于您的情况。请考虑以下代码:
public class GameObjectVisitor {

    public boolean visit(GameObject1 obj1) { return true; }
    .
    .
    // one method for each game object
    public boolean visit(GameGroup obj1) { return true; }
}

然后您可以在 GameObject 接口中添加一个方法,如下所示:

public interface GameObject {

    .
    .
    public boolean visit(GameObjectVisitor visitor);
}

然后每个GameObject都实现了这个方法:

public class GameGroup implements GameObject {

    .
    .
    .
    public boolean visit(GameObjectVisitor visitor) {
        visitor.visit(this);
    }
}

当您拥有复杂的GameObject继承层次结构时,这尤其有用。对于您的情况,您的方法将如下所示:

private void allocateUITweenManager() {

    GameObjectVisitor gameGroupVisitor = new GameObjectVisitor() {
        public boolean visit(GameGroup obj1) {
            obj1.setUITweenManager(mUITweenManager);
        }
    };

    for(GameObject go:mGameObjects){
      go.visit(gameGroupVisitor);
   }
}

4

编辑

在这里,有两件主要的事情可以做来解决你所遇到的instanceof问题。(双关语?)

  1. 按照我最初的回答所建议的那样,将你要定位的方法移动到你正在迭代的类中。但在这种情况下,这并不理想,因为该方法对于父对象没有意义,并且会像Ted所说的那样造成污染。

  2. 缩小你所迭代的对象的范围,仅限于熟悉目标方法的对象。我认为这是更理想的方法,但在你的代码当前形式下可能行不通。

个人而言,我尽量避免使用instanceof,因为它让我感觉自己完全错过了什么,但有时候确实是必要的。如果你的代码布局就是这样的,并且你没有办法缩小你所迭代的对象的范围,那么instanceof可能就可以正常工作。但这似乎是一个很好的机会,可以看看多态如何使你的代码在未来更易于阅读和维护。

我保留下面的原始回答,以保持评论的完整性。

/编辑

个人而言,我不认为这是使用instanceof的好理由。在我看来,你可以利用多态来实现你的目标。

你考虑过将setUITweenManager(...)方法作为GameObject的一个方法吗?这样做有意义吗?

如果有意义的话,你可以让默认实现什么都不做,并让GameGroup覆盖该方法以执行你想要的操作。此时,你的代码可能看起来像这样:

private void allocateUITweenManager() {
   for(GameObject go:mGameObjects){
       go.setUITweenManager(mUITweenManager);
   }
}

这是多态性的体现,但我不确定它是否是您当前情况下最好的方法。如果可能的话,迭代UITweenable对象的Collection会更有意义。

6
虽然这样做是可行的,但污染基类以应对子类的特殊行为通常不是一个好主意。仅仅为了避免使用“instanceof”,而采用这种方法特别不明智。在这种情况下,基类可能对补间管理器一无所知;你的方法会创建一个不存在的依赖关系。 - Ted Hopp
@TedHopp 我完全同意。这旨在展示如何消除对instanceof的需求,但要实际解决问题,迭代UITweenable对象而不是GameObject对象可能更有意义。我想目前可能不太可能做到这一点,因为我们没有更多的信息。 - nicholas.hauschild

2
instanceof 的问题在于,你可能会受到未来对象层次结构变化的影响。更好的方法是对于你可能使用 instanceof 的情况,使用策略模式。如果使用 instanceof 来解决问题,你就会陷入策略模式试图解决的问题:太多的 if。有些人成立了一个社区。反 if 运动可能只是个玩笑,但是反模式很严肃。在长期项目中,维护 10-20 层 if-else-if 可能会很痛苦。在你的情况下,最好为数组中所有对象制作一个公共接口,并通过接口实现它们所有的 setUITweenManager
interface TweenManagerAware{
   setUITweenManager(UITweenManager manager);
}

2
instanceof不被鼓励的原因是因为在面向对象编程中,我们不应该从外部检查对象的类型。相反,惯用的方式是让对象自己使用重写的方法来行动。在您的情况下,一个可能的解决方案是在GameObject上定义boolean setUITweenManager(...)并让它返回true,如果对于特定对象设置管理器是可能的。然而,如果这种模式在许多地方出现,顶层类可能会变得非常混乱。因此,有时instanceof是“较小的恶”。这种面向对象编程的问题在于每个对象必须“知道”所有可能的用例。如果您需要一个适用于您的类层次结构的新功能,则必须将其添加到类本身,无法将其放在不同的模块中。这可以通过使用visitor pattern以一般方式解决,正如其他人建议的那样。访问者模式描述了检查对象的最通用方式,并且当与多态结合使用时变得更加有用。
请注意,其他语言(尤其是函数式语言)使用不同的原则。它们不是让对象“知道”如何执行每个可能的操作,而是声明没有自己方法的数据类型。相反,使用它们的代码通过对代数数据类型进行模式匹配来检查它们是如何构造的。据我所知,最接近Java并具有模式匹配功能的语言是Scala。有一篇有趣的论文介绍了Scala如何实现模式匹配,并比较了几种可能的方法:Matching Objects With Patterns. Burak Emir, Martin Odersky, and John Williams. 在面向对象编程中,数据以类的层次结构组织。面向对象模式匹配的问题是如何从外部探索这个层次结构。这通常涉及按运行时类型对对象进行分类、访问它们的成员或确定一组对象的某些其他特征。本文比较了六种不同的模式匹配技术:面向对象分解、访问者、类型测试/类型转换、类型案例、案例类和提取器。这些技术根据与简洁性、可维护性和性能相关的九个标准进行比较。本文介绍了案例类和提取器作为两种新的模式匹配方法,并表明它们的组合对所有已建立的标准都很有效。
总之,在面向对象编程中,您可以轻松修改数据类型(例如添加子类),但添加新功能(方法)需要对许多类进行更改。使用ADT可以轻松添加新功能,但修改数据类型则需要修改许多函数。

1

在同一个集合中混合不同类别的对象总是让我感到有些“可疑”。是否有可能/有意义将单个游戏对象集合拆分为多个集合,一个仅包含游戏对象,另一个包含UITweenable对象?(例如使用按类键入的MultiMap)。然后你可以这样做:

for (UITweenable uit : myMap.get(UITweenable.class)) {
  uit.setUITweenManager(mUITweenManager);
}

现在,在将对象插入Map时,您仍然需要使用instanceof,但它已更好地封装 - 对于不需要知道这些细节的客户端代码是隐藏的。
附注:我并不是一个对所有SW“规则”都狂热的人,但请看看“里氏替换原则”的Google搜索结果。

0
你可以在GameObject中声明 `setUITweenManager`,并实现一个什么也不做的方法。
你可以创建一个方法,返回所有GameObject实例数组中的UITweenable实例的迭代器。
还有其他有效地隐藏分派在某些抽象层里面的方法,例如访问者或适配器模式。
“...我在面向对象编程中犯了什么基本错误,导致我在这里使用了instanceof吗?”
“实际上并没有(在我看来)。”
instanceof最糟糕的问题是当你开始使用它来测试实现类时。这样做的原因特别糟糕,是因为它使添加额外的类变得困难。在这里,instanceof UITweenable似乎并没有引入这个问题,因为UITweenable似乎更基本地涉及到设计。”
当你做出这些判断时,最好要“理解”为什么(据称)不良的结构或用法被认为是不好的。然后你看看你具体的使用情况,判断这些原因是否适用,并且你正在考虑的替代方案是否在你的使用情况下真的更好。

0

当你需要对所有游戏对象执行某些操作并保持单独的容器仅用于GameGroup对象时,可以使用mGameObjects容器。

这将使用更多的内存,并且当您添加/删除对象时,必须更新两个容器,但它不应该是明显的开销,并且它可以让您非常高效地循环遍历所有对象。


0
这种方法的问题在于它通常不仅出现在代码中的一个地方,因此在未来添加接口的另一实现时会更加痛苦。是否避免这种情况取决于您的考虑。有时可以应用YAGNI,这是最直接的方法。
其他人提出了替代方案,例如访问者模式。

0

我有另一种避免使用 instanceof 的建议。

除非你正在使用通用工厂,否则在创建 GameObject 时,你知道它是什么具体类型。因此,你可以传递任何你创建的 GameGroup 可观察对象,并允许它们向其添加监听器。它将像这样工作:

public class Game {
    private void makeAGameGroup() {
        mGameObjects.add(new GameGroup(mUITweenManagerInformer));
    }

    private void allocateUITweenManager() {
        mUITweenManagerInformer.fire(mUITweenManager);
    }

    private class OurUITweenManagerInformer extends UITweenManagerInformer {
        private ArrayList<UITweenManagerListener> listeners;

        public void addUITweenManagerListener(UITweenManagerListener l) {
            listeners.add(l);
        }

        public void fire(UITweenManager next) {
            for (UITweenManagerListener l : listeners)
                l.changed(next);
        }
    }
    private OurUITweenManagerInformer mUITweenManagerInformer = new OurUITweenManagerInformer();
}

public interface UITweenManagerInformer {
    public void addUITweenManagerListener(UITweenManagerListener l);
}

public interface UITweenManagerListener {
    public void changed(UITweenManager next);
}

吸引我使用这个解决方案的原因是:

  • 因为UITweenManagerInformerGameGoup的构造函数参数,所以你不会忘记传递它,而使用实例方法可能会忘记调用它。

  • 对我来说,将对象需要的信息(例如GameGroup需要当前UITweenManager的知识)作为构造函数参数传递是很直观的。我喜欢把这些看作对象存在的先决条件。如果你没有当前UITweenManager的知识,就不应该创建GameGroup,而这个解决方案强制执行了这一点。

  • instanceof从未被使用。


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