What are the differences between a Java enum and a class with private constructor?

JavaEnumsEnumeration

Java Problem Overview


I was trying to understand how Java enum really works and I have come to the conclusion that it is very similar to a normal Java class that has its constructor declared private.

I have just come to this conclusion and it isn't based on much thinking, but Id like to know whether I miss anything.

So below is an implementation of a simple Java enum and an equivalent Java class.

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

What is the difference in meaning between the code above and below?

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

Java Solutions


Solution 1 - Java

Differences:

  1. Enums extend java.lang.Enum and gain all of its nice features:
    1. Automatic singleton behaviour through correct serialization
    2. Automatic human-readable .toString method on enum values without the need to duplicate your enum names
    3. .name and .ordinal special-purpose methods
    4. Usable in high-performance bitset-based EnumSet and EnumMap classes
  2. Enums are treated by the language specially:
    1. Enums use a special syntax which simplifies instance creation without writing dozens of public static final fields
    2. Enums can be used in switch statements
    3. Enums cannot be instantiated outside the enumeration list except by using reflection
    4. Enums cannot be extended outside the enumeration list
  3. Java automatically compiles extra stuff into enums:
    1. public static (Enum)[] values();
    2. public static (Enum) valueOf(java.lang.String);
    3. private static final (Enum)[] $VALUES; (values() returns a clone of this)

Most of these can be emulated with a suitably designed class, but Enum just makes it really easy to create a class with this set of particularly desirable properties.

Solution 2 - Java

To answer the question: essentially, there's no difference between the two approaches. However, enum construct provides you with some additional supporting methods like values(), valueOf(), etc. which you'd have to write on your own with the class-with-private-constructor approach.

But yeah, I like how Java enums are mostly just like any other classes in Java, they can have fields, behaviors, etc. But to me what separates enums from the plain classes is the idea that enums are classes/types whose instances/members are predetermined. Unlike usual classes where you can create any number of instances from, enums only limit creation to known instances. Yes, as you've illustrated, you can also do this with classes with private constructors, but enums just make this more intuitive.

Solution 3 - Java

Take a look at this blogpage, it describes how Java enums are compiled into bytecode. You'll see that there's a small addition compared to your second code sample, which is an array of Direction objects called VALUES. This array holds all possible values for your enum, so you won't be able to do

new Direction(2, 2)

(for example using reflection) and then use that as a valid Direction value.

Plus, as @Eng.Fouad correctly explains, you don't have values(), valueOf() and ordinal().

Solution 4 - Java

As people have pointed out you lose values(), valueOf() and ordinal(). You can replicate this behaviour fairly easily using a combination of a Map and a List.

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

As you can see; the code becomes very clunky very quickly.

I'm not sure if there is a way for replicate a switch statement with this class; so you will lose that.

Solution 5 - Java

The main difference is the each enum class implicitly extends Enum<E extends Enum<E>> class. This leads to that:

  1. enum objects have such methods as name() and ordinal()
  2. enum objects have special toString(), hashCode(), equals() and compareTo() implementations
  3. enum objects are suitable for switch operator.

All mentioned above is not applicable for your version of Direction class. This is the "meaning" difference.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionuser9349193413View Question on Stackoverflow
Solution 1 - JavanneonneoView Answer on Stackoverflow
Solution 2 - JavaPsycho PunchView Answer on Stackoverflow
Solution 3 - JavamthmuldersView Answer on Stackoverflow
Solution 4 - JavaBoris the SpiderView Answer on Stackoverflow
Solution 5 - JavaAndremoniyView Answer on Stackoverflow