覆盖抽象方法或在枚举中使用单个方法?

时间:2013-02-13 10:01:13

标签: java enums

考虑下面的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 */
}

编辑:我真的希望看到一些有充分理由/精心解答的答案,并提供特定声明的证据/来源。由于缺乏证据,大多数关于性能的现有答案并不真正令人信服。

可以建议替代方案,但必须明确它是如何比所陈述的更好和/或所述方法更糟糕,并在需要时提供证据。

8 个答案:

答案 0 :(得分:21)

忘记这次比较中的表现;如果两种方法之间存在有意义的性能差异,那将需要一个真正庞大的枚举。

让我们关注可维护性。假设您完成了Direction枚举的编码,并最终转向更有声望的项目。与此同时,另一位开发人员拥有您的旧代码,包括Direction - 让我们称他为吉米。

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

<强> 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语句更安全...因为模式排除了忘记为新常量添加行为的可能性(因为枚举声明会导致编译时误差)。

答案 1 :(得分:3)

我实际上做了不同的事情。你的解决方案有挫折:抽象的重写方法引入了相当多的开销,并且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情况下你可以)

  • 几乎没有方法调用开销,因为该方法是最终的,而不是虚拟的

  • 干净简短的代码

  • 但是你可以忘记设置一个方向的值

答案 2 :(得分:1)

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

答案 3 :(得分:1)

第二个变体可能会快一点,因为&gt; 2-ary多态将强制在接口上进行完整的虚函数调用,而不是后者的直接调用和索引。

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

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

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

我认识的大多数有经验的程序员都会建议在面向对象上使用模式匹配。当枚举关闭时,添加新的枚举不是一种选择;因此,我一定会采用后一种方法。

答案 4 :(得分:0)

枚举值可以视为独立类。因此,考虑到面向对象的概念,每个枚举应该定义自己的行为。所以我建议采用第一种方法。

答案 5 :(得分:0)

第一个版本可能要快得多。 Java JIT编译器可以对其应用积极的优化,因为enum是最终的(因此其中的所有方法都是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 ];
    }
}
但是,我不喜欢这种方法。 它太难读了(你必须计算你脑中每个值的索引)并且太容易出错。在enum和per方法中,你需要对每个值进行单元测试(在你的情况下为4 * 3 = 12)。

答案 6 :(得分:0)

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

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];
    }
}

答案 7 :(得分:0)

答案:取决于

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

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

    • 在枚举值
    • 旁边实现特定于枚举值的定义
    • 在&#34;公共区域&#34;中实现类底部所有枚举值共有的定义;如果相同的方法签名可用于所有枚举值但没有/部分逻辑是常见的,则在公共区域中使用抽象方法定义

    即。备选方案1

    为什么?

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

    请注意,北/南/东/西示例可以被视为表示非常简单的状态(当前方向),并且可以认为与/ rotateClockwise / rotateAnticlockwise相反的方法表示用户命令以更改状态。这提出了一个问题,你如何处理现实生活中通常很复杂的状态机?

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

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

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

    为什么?

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

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