【问题标题】:Overriding abstract method or using one single method in enums?覆盖抽象方法或在枚举中使用一种方法?
【发布时间】:2013-02-13 10:01:12
【问题描述】:

考虑下面的enums,哪个更好?两者的使用方式完全一样,但是它们的优势是什么

1.覆盖抽象方法:

public enum Direction {
    UP {
        @Override
        public Direction getOppposite() {
            return DOWN;
        }
        @Override
        public Direction getRotateClockwise() {
            return RIGHT;
        }
        @Override
        public Direction getRotateAnticlockwise() {
            return LEFT;
        }
    },
    /* DOWN, LEFT and RIGHT skipped */
    ;
    public abstract Direction getOppposite();
    public abstract Direction getRotateClockwise();
    public abstract Direction getRotateAnticlockwise();
}

2。使用单一方法:

public enum Orientation {
    UP, DOWN, LEFT, RIGHT;
    public Orientation getOppposite() {
        switch (this) {
        case UP:
            return DOWN;
        case DOWN:
            return UP;
        case LEFT:
            return RIGHT;
        case RIGHT:
            return LEFT;
        default:
            return null;
        }
    }
    /* getRotateClockwise and getRotateAnticlockwise skipped */
}

编辑:我真的希望看到一些合理/详尽的答案,以及特定主张的证据/来源。由于缺乏证据,大多数现有的关于性能的答案并不真正令人信服。

可以提出替代方案,但必须清楚它比所述的更好和/或所述的更差,并在需要时提供证据。

【问题讨论】:

  • 第三种选择可能是将oppositecwiseccwise 之类的参数包含到Direction 的构造函数中,将它们分配给最终实例变量并使用它们(通过直接访问或在“类”上定义的吸气剂)。
  • 我也不喜欢,因为我不喜欢在枚举中寻找逻辑。但是第二个更简洁,不涉及枚举成员对抽象方法的覆盖,这对我来说有点痛苦:)
  • @gd1 如果枚举中没有逻辑,您将如何实现类似于我的示例的内容?如果您有其他选择,您可以在答案中提出建议。
  • 我不得不说我会按照 akaIDIOT 建议的方式来做,它更清晰。

标签: java enums


【解决方案1】:

在此比较中忘记性能;要在这两种方法之间产生有意义的性能差异,需要一个真正庞大的枚举。

让我们关注可维护性。假设您完成了 Direction 枚举的编码并最终转移到一个更有声望的项目。同时,另一位开发人员获得了您旧代码的所有权,包括 Direction - 我们称他为 Jimmy。

在某些时候,要求要求 Jimmy 添加两个新方向:FORWARDBACKWARD。 Jimmy 累了,工作过度了,没有费心去研究这会如何影响现有的功能——他就是这么做的。让我们看看现在会发生什么:

1.覆盖抽象方法:

Jimmy 立即得到一个编译器错误(实际上他可能会在枚举常量声明的正下方发现方法覆盖)。在任何情况下,问题都会在编译时发现并修复

2。使用单一方法:

Jimmy 没有收到编译器错误,甚至没有收到来自他的 IDE 的不完整切换警告,因为您的 switch 已经有一个 default 案例。后来,在运行时,某段代码调用FORWARD.getOpposite(),它返回null。这会导致意外行为,充其量会很快导致 NullPointerException 被抛出。

让我们备份并假设您添加了一些面向未来的内容:

default:
    throw new UnsupportedOperationException("Unexpected Direction!");

即便如此,直到 运行时 才会发现问题。希望该项目得到适当的测试!

现在,您的Direction 示例非常简单,因此这种情况可能看起来有些夸张。但在实践中,枚举可以像其他类一样容易地成为维护问题。在具有多个开发人员的更大、更旧的代码库中,重构的弹性是一个合理的问题。许多人都在谈论优化代码,但他们可能忘记了开发时间也需要优化——这包括防止错误的编码。

编辑:JLS Example §8.9.2-4 下的注释似乎同意:

特定于常量的类主体将行为附加到常量。 [This] 模式比在基类型中使用 switch 语句更安全...因为该模式排除了忘记为新常量添加行为的可能性(因为枚举声明会导致编译时错误) .

【讨论】:

  • 在你提到之前我真的没有想到这个因素!你是对的,因为我的例子真的是一个简单的例子,但实际上这确实是一个非常重要的因素。 (在接受和奖励赏金之前,让我再等几天等待可能的更多答案......)
  • 很高兴我能提供帮助并感谢您的有趣帖子。如果其他人想参与进来,请随意让赏金结束。
【解决方案2】:

我实际上做了一些不同的事情。您的解决方案存在缺陷:抽象的重写方法会引入大量开销,并且 switch 语句很难维护。

我建议以下模式(适用于您的问题):

public enum Direction {
    UP, RIGHT, DOWN, LEFT;

    static {
      Direction.UP.setValues(DOWN, RIGHT, LEFT);
      Direction.RIGHT.setValues(LEFT, DOWN, UP);
      Direction.DOWN.setValues(UP, LEFT, RIGHT);
      Direction.LEFT.setValues(RIGHT, UP, DOWN);
    }

    private void setValues(Direction opposite, Direction clockwise, Direction anticlockwise){
        this.opposite = opposite;
        this. clockwise= clockwise;
        this. anticlockwise= anticlockwise;
    }

    Direction opposite;
    Direction clockwise;
    Direction anticlockwise;

    public final Direction getOppposite() { return opposite; }
    public final Direction getRotateClockwise() { return clockwise; }
    public final Direction getRotateAnticlockwise() { return anticlockwise; }
}

有了这样的设计,你:

  • 永远不要忘记设置方向,因为它是由构造函数强制执行的(在case 的情况下你可以)

  • 方法调用开销很小,因为方法是最终的,而不是虚拟的

  • 简洁的代码

  • 但是您可能会忘记设置一个方向的值

【讨论】:

  • ...这个解决方案已经在评论中说明了,对不起@akaIDIOT,我刚才读了:(
  • 不幸的是,这不能编译:“非法前向引用”(javac),“在定义之前无法引用字段”(eclipse)。一些误导是必要的,例如在Aaron's bottom example
  • @PaulBellora 同意,更改了解决方案;现在更糟了,但 IMO 仍然比那些 OP 提议的要好。
  • 恕我直言,这不是一个很好的解决方案。构造函数/方法接受相同类型的不同参数,所以除非我们有命名参数,否则你必须参考构造函数来理解它。此外,我不相信有关性能开销的点,除非你对它们进行基准测试并且它被证明是重要的。
【解决方案3】:

第一个变体更快并且可能更易于维护,因为方向的所有属性都在定义方向本身的位置进行了描述。然而,将非平凡的逻辑放入枚举中对我来说看起来很奇怪。

【讨论】:

    【解决方案4】:

    第二个变体可能会快一点,因为 >2 元多态性将强制对接口进行完整的虚函数调用,而后者则直接调用和索引。

    第一种形式是面向对象的方法。

    第二种形式是模式匹配方法。

    因此,第一种形式是面向对象的,可以很容易地添加新的枚举,但很难添加新的操作。第二种形式则相反

    我认识的大多数有经验的程序员都会推荐使用模式匹配而不是面向对象。由于枚举已关闭,因此无法添加新枚举;因此,我自己肯定会采用后一种方法。

    【讨论】:

    • 枚举中的方法在设计上都是最终的(即使您省略了关键字),因此编译器可以积极优化它们。因此,第一个变体应该快得多,因为它将替换为对枚举常量的引用(根本没有方法调用)。
    • 然而关键问题不是方法是否是最终的,而是有多少类实现了被调用的接口。问题不在于 JVM 如何优化 Direction.UP.getOpposite(),而在于它如何优化 myfunc(Direction d) { return d.getOpposite(); },在这种情况下,您将面临全虚拟呼叫和直接呼叫之间的选择。
    • 这完全取决于 JIT 的智能程度。它可以创建一个引用值的表并将d.getOpposite() 转换为Orientation.opposites[d.ordinal()]
    【解决方案5】:

    枚举值可以被认为是独立的类。因此,考虑到面向对象的概念,每个枚举都应该定义自己的行为。所以我会推荐第一种方法。

    【讨论】:

      【解决方案6】:

      您也可以像这样简单地实现一次(您需要以适当的顺序保持枚举常量):

      public enum Orientation {
      
          UP, RIGHT, DOWN, LEFT; //Order is important: must be clock-wise
      
          public Orientation getOppposite() {
              int position = ordinal() + 2;
              return values()[position % 4];
          }
          public Orientation getRotateClockwise() {
              int position = ordinal() + 1;
              return values()[position % 4];
          }
          public Orientation getRotateAnticlockwise() {
              int position = ordinal() + 3; //Not -1 to avoid negative position
              return values()[position % 4];
          }
      }
      

      【讨论】:

      • @AaronDigulla 很公平:-)
      • 它是可读的,因为它的格式很好,但它不可读,因为它涉及到模数学。
      • @Elemental 在两个月前的原始讨论中,我认为这是一个有用的,如果是轶事的话,评论太长了,不能像这样发布。我会把它留给后代。
      • REDO 1st BLOCK Code 1 static { // ------------------------ Direction.UP.setValues ();方向.RIGHT.setValues (); Direction.DOWN.setValues(); Direction.LEFT.setValues(); } 私有无效 setValues(){ 最终方向 [] vals = values() ; // 每个枚举调用一次。 int 位置 = 序数(); // 从 0 开始 this.clock = vals[(++position) % 4]; this.opposite = vals[(++position) % 4]; this.逆时针 = vals[(++position) % 4]; // 没有否定!! = +3 }
      【解决方案7】:

      第一个版本可能要快得多。 Java JIT 编译器可以对其进行积极的优化,因为enums 是最终的(所以它们中的所有方法也是final)。代码:

      Orientation o = Orientation.UP.getOppposite();
      

      实际上应该变成(在运行时):

      Orientation o = Orientation.DOWN;
      

      即编译器可以消除方法调用的开销。

      从设计的角度来看,使用 OO 做这些事情的正确方法是:将知识移到需要它的对象附近。所以 UP 应该知道它的反面,而不是其他地方的一些代码。

      第二种方法的优点是它更易读,因为所有相关的东西都被更好地分组(即所有与“相反”相关的代码都在一个地方,而不是这里和那里一点点)。

      编辑 我的第一个论点取决于 JIT 编译器的智能程度。我的问题解决方案如下所示:

      public enum Orientation {
          UP, DOWN, LEFT, RIGHT;
      
          private static Orientation[] opposites = {
              DOWN, UP, RIGHT, LEFT
          };
      
          public Orientation getOpposite() {
              return opposites[ ordinal() ];
          }
      }
      

      无论 JIT 能做什么或能做什么,这段代码都紧凑而快速。它清楚地传达了意图,并且根据序数规则,它始终有效。

      我还建议添加一个测试,以确保在为枚举的每个值调用 getOpposite() 时,您总是得到不同的结果,并且没有一个结果是 null。这样,您就可以确定您收到了所有案例。

      剩下的唯一问题是更改值的顺序。为了防止在这种情况下出现问题,请为每个值分配一个索引,并使用该索引在数组甚至Orientation.values() 中查找值。

      这是另一种方法:

      public enum Orientation {
          UP(1), DOWN(0), LEFT(3), RIGHT(2);
      
          private int opposite;
      
          private Orientation( int opposite ) {
              this.opposite = opposite;
          }
      
          public Orientation getOpposite() {
              return values()[ opposite ];
          }
      }
      

      不过,我不喜欢这种方法。 它太难读了(你必须在脑海中计算每个值的索引)并且太容易出错。它需要对枚举中的每个值和您可以调用的每个方法进行单元测试(因此在您的情况下为 4*3 = 12)。

      【讨论】:

      • 第一个版本不会更快,实际上如果有什么会更慢,请参阅我的答案以获取详细信息。
      • @Recurse:这个假设是错误的,正如我在对您的回答的评论中解释的那样。
      • @AaronDigulla 如果在编译时不知道o 怎么办?例如传递给函数?
      • JIT 可以创建一个引用值的表并将o.getOpposite() 转换为Orientation.opposites[d.ordinal()]
      • 同意,JIT 可以做到这一点,尽管我不知道最近是否有任何 JIT 可以做到。在这种情况下,OOP 方法将变得与 PM 方法一样快。无论哪种方式,您对 OOP 的“更快”的说法都是不正确的——它所希望的最好的结果是“不慢”。当然,主要关注点应该是可读性,并且 PM 是一种更灵活、更易读且不易出错的方法,因为正如您所指出的,枚举是封闭的。
      【解决方案8】:

      答案:视情况而定

      1. 如果您的方法定义很简单

        您非常简单的示例方法就是这种情况,它只是为每个枚举输入硬编码一个枚举输出

        • 实现特定于枚举值旁边的枚举值的定义
        • 在“公共区域”的类底部实现所有枚举值通用的定义;如果所有枚举值都可以使用相同的方法签名,但没有一个/部分逻辑是通用的,请在公共区域使用抽象方法定义

        即选项 1

        为什么?

        • 可读性、一致性、可维护性:与定义直接相关的代码就在定义旁边
        • 编译时检查是否在公共区域中声明了抽象方法,但未在枚举值区域中指定

        请注意,North/South/East/West 示例可以被认为表示一个非常简单的状态(当前方向),并且可以考虑使用 reverse/rotateClockwise/rotateAntilateral 方法来表示用户更改状态的命令。这就提出了一个问题,你要为现实生活中通常复杂的状态机做什么?

      2. 如果您的方法定义很复杂:

        状态机通常很复杂,依靠当前(枚举值)状态、命令输入、计时器以及相当多的规则和业务异常来确定新的(枚举值)状态。在其他罕见的情况下,方法甚至可以通过计算确定枚举值输出(例如科学/工程/保险评级分类)。或者它可以使用数据结构,例如地图,或适合算法的复杂数据结构。当逻辑复杂时,需要格外小心,并且“通用”逻辑和“特定于枚举值”的逻辑之间的平衡会发生变化。

        • 避免在枚举值旁边放置过多的代码量、复杂性和重复的“剪切和粘贴”部分
        • 尝试将尽可能多的逻辑重构到公共区域 - 可能将 100% 的逻辑放在这里,但如果不可能,则采用四人组“模板方法”模式来最大化公共逻辑的数量,但灵活地允许针对每个枚举值的少量特定逻辑。 即尽可能多地使用选项 1,允许少量选项 2

        为什么?

        • 可读性、一致性、可维护性:避免代码膨胀、重复、错误的文本格式以及散布在枚举值之间的大量代码,允许快速查看和理解整组枚举值
        • 编译时检查是否使用模板方法模式和公共区域中声明但未在枚举值区域中指定的抽象方法

        • 注意:您可以将所有逻辑放入单独的帮助程序类中,但我个人认为这没有任何优势(不是性能/可维护性/可读性)。它稍微打破了封装,一旦你将所有逻辑都放在一个地方,将一个简单的枚举定义添加回类的顶部有什么区别?跨多个类拆分代码是另一回事,应在适当时鼓励。

      【讨论】:

        猜你喜欢
        • 2011-08-31
        • 2016-09-23
        • 1970-01-01
        • 1970-01-01
        • 2021-07-18
        • 1970-01-01
        • 1970-01-01
        • 2013-09-12
        • 1970-01-01
        相关资源
        最近更新 更多