Blog-Archiv

Sonntag, 4. April 2021

Remarks About Java Enums

Enumerations are a finite number of distinct values of same type, typically colors, fonts, days of week, month names and so on. Java enums are type-safe. That means you should prefer them to string- or integer-constants, because the compiler can then check for their correct usage in parameters.

Facts

  1. You can not extend an enum, it doesn't support inheritance.
  2. An enum can not extend a class. The compiler will derive it from the JRE class Enum.
  3. You can not add or construct an enum value programmatically at runtime.
  4. Enum values are perfect singletons, thus you can compare them using == instead of equals(), which is fast and null-safe

More detailed:

  • Enums can have constructors, but they are private.
  • Enums can have fields and methods just like a class.
  • Enums can contain even abstract methods, these must be implemented by each enum value individually (see example below).
  • Enums support method overloading.

Mind:

  • You can not overwrite equals() or hashCode(), the only Object method you can override is toString().
  • You can use enum values in annotations, but not their fields and methods (unless they are static).

Further:

  • Enums have an automatically assigned name() which is the identifier of the enum value, e.g. "RED" for Color.RED.
  • Enums have an automatically assigned ordinal() number, starting from 0 (zero), so their order is significant. Do not change the order in case an enum has been stored as integer into a database!
  • Enums have a static valueOf(String) method that converts a string to an enum value.
  • Enums have a generated static values() method that lists all enum values.

When to Not Use

Do not model an enum when you don't know at compile-time how many values there will be at runtime.

Example


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum Color
{
    RED(16711680, "#FF0000"),
    GREEN(65280, "#00FF00"),
    BLUE(255, "#0000FF"),
    ;
    
    public boolean matches(Object color)    {
        return color == this ||
                color.equals(name()) ||
                color.equals(decimal) ||
                color.equals(cssHex);
    }
    
    public final int decimal;
    public final String cssHex;
    
    private Color(int decimal, String cssHex)    {
        this.decimal = decimal;
        this.cssHex = cssHex;
    }
}

This color-enum wraps an integer and a CSS value. It has a constructor and exposes immutable (final) fields.

Mind the semicolon on line 6. It closes the list of enum values. The compiler tolerates a preceding comma.

The matches() method and the fields can be used from outside:

    public static void main(String [] args)    {
        System.out.println("RED matches "+Color.RED+": "+
                Color.RED.matches(Color.RED));
        System.out.println("RED matches \""+Color.RED.cssHex+"\": "+
                Color.RED.matches(Color.RED.cssHex));
        System.out.println("GREEN matches "+GREEN.decimal+": "+
                Color.GREEN.matches(Color.GREEN.decimal));
        System.out.println("BLUE matches \""+Color.BLUE.name()+"\": "+
                Color.BLUE.matches(Color.BLUE.name()));
    }

You can put this this main() directly into the enum and run it with command-line "java Color". Output would be:

RED matches RED: true
RED matches "#FF0000": true
GREEN matches 65280: true
BLUE matches "BLUE": true

No Inheritance

Following would not compile:

public enum SpecificColor extends Color
{
    ....
}

Error message would be:

Syntax error on token "extends", implements expected

Functional Enums

Enums are not meant to carry functionality, but as they can have methods, it is possible. Following example implements different kinds of space-trimming:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public enum Trim
{
    START    {
        @Override
        public String trim(String toTrim)    {
            int i = 0;
            while (i < toTrim.length() && Character.isWhitespace(toTrim.charAt(i)))
                i++;
            return toTrim.substring(i);
        }
    },
    END    {
        @Override
        public String trim(String toTrim)    {
            int i = toTrim.length() - 1;
            while (i >= 0 && Character.isWhitespace(toTrim.charAt(i)))
                i--;
            return toTrim.substring(0, i + 1);
        }
    },
    BOTH    {
        @Override
        public String trim(String toTrim)    {
            return toTrim.trim();
        }
    },
    ;
    
    public abstract String trim(String toTrim);
}

Three different trim() methods are available from this enum, removing spaces from start, from end, or from both. The enum itself just declares an abstract method, which is required, and the individual enum values implement different logics.

Let's try it out:

    public static void main(String [] args)    {
        final String HELLO = " Hello World ";
        
        System.out.println("Trim.START(\""+HELLO+"\"): >"+
                Trim.START.trim(HELLO)+"<");
        System.out.println("Trim.END  (\""+HELLO+"\"): >"+
                Trim.END.trim(HELLO)+"<");
        System.out.println("Trim.BOTH (\""+HELLO+"\"): >"+
                Trim.BOTH.trim(HELLO)+"<");
   }

Output is:

Trim.START(" Hello World "): >Hello World <
Trim.END  (" Hello World "): > Hello World<
Trim.BOTH (" Hello World "): >Hello World<

Singleton

The singleton pattern guarantees that there is only one instance of the singleton class in memory of the application. Singletons are needed for management of restricted resources like printers or databases. They are also used as factories for runtime-binding of custom-classes that are unknown at compile-time ("component loading"). Java static final fields are not perfect singletons, because they can be broken through reflection.

Enum values are safe against

  • construction of new instances
  • non-identical instances through serialization, except when serialization happens between two JVMs or different class-loaders
  • multi-threaded instantiation pitfalls of the singleton instance, see "double-checked locking"
  • corruption through reflection by calling constructor.setAccessible(true)
  • immediate instantiation on class-loading, every individual enum value is loaded lazily

Thus it is safe to use the == identity operator instead of equals() on enums:

Color color = ....;

if (color == Color.RED)
    return error();

....

switch(color) {
    case RED: return error(); 
}

As this is null-safe, it makes your code better readable. Additionally it is faster than calling equals(). Mind also that the switch case syntax has been simplified for enums.

Caveat: enum values are not identical across multiple class-loaders! Multiple class loaders appear in by web- and application-servers, which use one class-loader per application. In case two applications (with different class-loaders) communicate with each other through serialization, the == operator may not work any more.


Constants in Enums

Imagine you want to define following enum that exposes a type field:

public enum PersistenceMedium
{
    H2("database"),
    MYSQL("database"),
    ALFRESCO("document store"),
    ;
    
    public final String type;
    
    private PersistenceMedium(String type)    {
        this.type = type;
    }
}

To avoid code duplication, you want to put the types "database" and "document store" into static constants:

public enum PersistenceMedium
{
    public static final String DATABASE = "database";
    public static final String DOCUMENTSTORE = "document store";

    H2(DATABASE),
    MYSQL(DATABASE),
    ALFRESCO(DOCUMENTSTORE),
    ;
    
    ....
}

The compiler rejects this with an error message, it's a grammar problem: there must not be anything between the enum header and its value definitions. When you put the string constants below the enum values, the compiler tells you that they can't be used before they are defined. So is there no way to have constants inside an enum?

There is a workaround for this:

public enum PersistenceMedium
{
    H2(Constants.DATABASE),
    MYSQL(Constants.DATABASE),
    ALFRESCO(Constants.DOCUMENTSTORE),
    ;
    
    public interface Constants
    {
        String DATABASE = "database";
        String DOCUMENTSTORE = "document store";
    }
    
    public final String type;
    
    private PersistenceMedium(String type)    {
        this.type = type;
    }
}

The fields of the inner interface Constants (may be also an inner static class) can be used as constants, as they would be compiled before the enum values These constants then can be used also outside of the enum:

    public static void main(String[] args) {
        PersistenceMedium medium = PersistenceMedium.valueOf(args[0]);    // given "ALFRESCO"
        
        if (medium.type.equals(PersistenceMedium.Constants.DATABASE))
            System.out.println("It is a database!");
        else if (medium.type.equals(PersistenceMedium.Constants.DOCUMENTSTORE))
            System.out.println("It is a document store!");
    }

The static valueOf(String) method turns a string into an enum value, in this case whatever you pass to the application as command line argument. The input must be identical with one of the enum names (return of name() method).

If you run this main() with argument ALFRESCO, output would be:

It is a document store!

Fields of Enum Values are not Static

Mind that fields (constructor arguments) of enums are NOT available statically at compile time, thus enums are not always suitable containers for constants. This especially strikes with annotations.

This enum

public enum LaunchContext
{
    DESKTOP,
    WEB,
    SERVER,
    ;
}

and this annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MainContext
{
    LaunchContext value();
}

work together perfectly here:

@MainContext(LaunchContext.WEB)
public class Main
{
    public static void main(String[] args) {
        System.out.println(
            "Launch context: "+
            Main.class.getAnnotation(MainContext.class).value()
        );
    }
}

But you can not use the enum's fields or methods in the annotation. Assuming the data-type of the annotation's value() was changed to String, following would not compile either:

@MainContext(LaunchContext.WEB.value())
public class Main
{
    ....
}

Error message is:

The method value() is undefined for the type LaunchContext

This is because value() is not a static method. The same would happen on public fields.

Conclusion

Since its introduction in Java 1.5, enum has received increasing attention, and I've seen lots of big enum implementations, carrying both field- and method-logic. Developers see it as an alternative to classes, with the singleton feature for free. Let's hope they won't become roots of a new monolith culture.




Keine Kommentare: