14

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

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

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

4

8 回答 8

22

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

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

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

1.重写抽象方法:

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

2.使用单一方法:

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

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

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

即使那样,直到运行时才发现问题。希望该项目经过适当的测试!

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

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

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

于 2013-04-16T06:42:45.037 回答
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以防万一)

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

  • 干净简洁的代码

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

于 2013-04-16T07:03:20.220 回答
1

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

于 2013-02-13T10:04:51.337 回答
1

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

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

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

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

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

于 2013-02-13T10:10:31.540 回答
0

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

于 2013-02-13T10:07:02.187 回答
0

第一个版本可能要快得多。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)。

于 2013-02-13T10:08:00.590 回答
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];
    }
}
于 2013-02-13T10:09:29.187 回答
0

答案:视情况而定

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

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

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

    即选项1

    为什么?

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

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

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

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

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

    为什么?

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

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

于 2013-04-18T03:06:02.203 回答