Java枚举和具有私有构造函数的类之间有什么区别?

64

我试图理解Java枚举的工作原理,我得出的结论是它非常类似于一个普通的Java类,其构造函数声明为私有。

我刚刚得出这个结论,它并没有经过太多的思考,但我想知道是否还有其他东西我没有注意到。

因此以下是一个简单的Java枚举和一个等价的Java类的实现。

public enum Direction {
    ENUM_UP(0, -1),
    ENUM_DOWN(0, 1),
    ENUM_RIGHT(1, 0),
    ENUM_LEFT(-1, 0);


    private int x;
    private int y;

    private Direction(int x, int y){
        this.x = x;
        this.y = y;
    }
    public int getEnumX(){
        return x;
    }
    public int getEnumY(){
        return y;
    }
}

上面的代码和下面的代码有何意义上的区别?

public class Direction{
    public static final Direction UP = new Direction(0, -1) ;
    public static final Direction DOWN = new Direction(0, 1) ;
    public static final Direction LEFT = new Direction(-1, 0) ;
    public static final Direction RIGHT = new Direction(1, 0) ;


    private int x ;
    private int y ;

    private Direction(int x, int y){
        this.x = x ;
        this.y = y ;
    }
    public int getX(){
        return x;
    }
    public int getY(){
        return y;
    }
}

7
好问题,它们在很多方面确实相似 :) 当然也有许多不同之处,我相信好的答案会进行概述。 - Benjamin Gruenbaum
4
我至少能想出三个不同之处:[1] 你将无法切换到后面的实现方式(即 Direction d; switch(d)),[2] Direction.toString() 的实现会有所不同,[3] 枚举方法允许你通过 .values() 获取所有该“类”的实例,而类方法则不行。 - jedwards
3
你将失去 values()valueOf()ordinal() - Eng.Fouad
1
枚举是语言中的一个增强功能。它可以更简洁、更清晰地表达某些内容。你可以以Scala为例:Scala在语言中构建了许多在纯Java中需要大量样板代码来表达的东西,例如单例。在这方面,Java非常(极端)保守。 - PeterMmm
3
已投票重新开放。如果有什么区别的话,应该关闭另一个问题,因为这个更清晰,答案也更好。 - Jonik
显示剩余3条评论
5个回答

60

区别:

  1. 枚举类继承自 java.lang.Enum 并获得了所有其良好特性
    1. 正确序列化时的自动单例行为
    2. 无需复制枚举名称即可自动生成可读的 .toString 方法
    3. .name.ordinal 特殊目的方法
    4. 可用于高性能位集(EnumSet)和映射(EnumMap)类中
  2. 枚举类在语言上被特殊对待:
    1. 枚举使用特殊语法简化实例创建,无需编写大量的 public static final 字段
    2. 可以在 switch 语句中使用枚举
    3. 除使用反射外,无法在枚举列表之外的地方实例化枚举
    4. 无法在枚举列表之外扩展枚举
  3. Java 自动将其他内容编译为枚举类:
    1. public static (Enum)[] values();
    2. public static (Enum) valueOf(java.lang.String);
    3. private static final (Enum)[] $VALUES;values() 方法返回此数组的克隆)

大多数情况下,通过设计合适的类也可以模拟这些特性,但是使用枚举类非常容易创建具有这种特性的类。


单例模式是如何工作的?对于每个值,它都有一个单例吗?如果您创建一个具有setter和getter的枚举类型,会发生什么?在单个枚举变量上使用setter会影响具有相同值的其他变量吗? - android developer
每个枚举值都是单例,因此,在一个枚举值上使用setter将影响所有引用该值的变量。因此,强烈建议您不要在枚举中具有可变状态。 - nneonneo
1
我明白了。看起来很合理。谢谢。 - android developer

10

回答这个问题:实质上,这两种方法之间没有区别。然而,枚举结构为您提供了一些额外的支持方法,例如 values()valueOf() 等等,而使用带私有构造函数的类则需要自己编写这些方法。

但是,我喜欢 Java 枚举基本上就像 Java 中的任何其他类一样,它们可以有字段、行为等等。但对我来说,将枚举与普通类区分开来的想法是:枚举是其实例/成员预先确定的类/类型。与通常的类不同,您可以从中创建任意数量的实例,枚举只限制为已知实例。是的,正如您所示,您也可以使用具有私有构造函数的类来完成此操作,但是枚举使得这更加直观。


7

请看这个博客页面,它描述了Java enum是如何编译成字节码的。你会发现与你的第二个代码示例相比,有一个小的添加,即一个名为VALUESDirection对象数组。该数组包含枚举的所有可能值,因此您将无法执行以下操作:

new Direction(2, 2)

例如使用反射,然后将其用作有效的Direction值。

此外,正如@Eng.Fouad正确地解释的那样,您没有values()valueOf()ordinal()


1
我知道你可以使用反射破解Direction类的私有构造函数并创建新实例,但我不确定是否可以对枚举类型做同样的事情。 - Luiggi Mendoza
1
根据它们编译为字节码的方式,也许你可以。我自己从未尝试过,但这是一个有趣的实验... - mthmulders

6

正如人们所指出的,您会失去values()valueOf()ordinal()。您可以使用MapList的组合相当容易地复制此行为。

public class Direction {

    public static final Direction UP = build("UP", 0, -1);
    public static final Direction DOWN = build("DOWN", 0, 1);
    public static final Direction LEFT = build("LEFT", -1, 0);
    public static final Direction RIGHT = build("RIGHT", 1, 0);
    private static final Map<String, Direction> VALUES_MAP = new LinkedHashMap<>();
    private static final List<Direction> VALUES_LIST = new ArrayList<>();
    private final int x;
    private final int y;
    private final String name;

    public Direction(int x, int y, String name) {
        this.x = x;
        this.y = y;
        this.name = name;
    }

    private static Direction build(final String name, final int x, final int y) {
        final Direction direction = new Direction(x, y, name);
        VALUES_MAP.put(name, direction);
        VALUES_LIST.add(direction);
        return direction;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public static Direction[] values() {
        return VALUES_LIST.toArray(new Direction[VALUES_LIST.size()]);
    }

    public static Direction valueOf(final String direction) {
        if (direction == null) {
            throw new NullPointerException();
        }
        final Direction dir = VALUES_MAP.get(direction);
        if (dir == null) {
            throw new IllegalArgumentException();
        }
        return dir;
    }

    public int ordinal() {
        return VALUES_LIST.indexOf(this);
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 29 * hash + name.hashCode();
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Direction other = (Direction) obj;
        return name.equals(other.name);
    }

    @Override
    public String toString() {
        return name;
    }
}

正如您所看到的,代码很快变得非常臃肿。

我不确定是否有一种方法可以使用该类复制switch语句,因此您将失去它。


0

主要的区别在于每个enum类都隐式继承了Enum<E extends Enum<E>>类。这导致:

  1. enum对象具有name()ordinal()等方法
  2. enum对象具有特殊的toString()hashCode()equals()compareTo()实现
  3. enum对象适用于switch操作符。

所有上述内容都不适用于您的Direction类版本。这就是“意义”的差异。


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