Core Java

Two ways to extend enum functionality

Preface

In my previous article I explained how and why to use enums instead of switch/case control structure in Java code. Here I will show how to extend functionality of existing enums.

Introduction

Java enum is a kind of a compiler magic. In the byte code anyenum is represented as a class that extends abstract class java.lang.Enum and has several static members. Therefore enum cannot extend any other class or enum: there is no multiple inheritance.

Class cannot extend enum as well. This limitation is enforced by compiler.

Here is a simple enum:

1
enum Color {red, green, blue}

This class tries to extend it:

1
class SubColor extends Color {}

This is the result of an attempt to compile class SubColor:

1
2
3
4
5
6
7
8
$ javac SubColor.java
SubColor.java:1: error: cannot inherit from final Color
class SubColor extends Color {}
                       ^
SubColor.java:1: error: enum types are not extensible
class SubColor extends Color {}
^
2 errors

Enum cannot either extend or be extended. So, how is it possible to extend its functionality? The key word is “functionality”. Enum can implement methods. For example enum Color may declare abstract method draw() and each member can override it:

enum Color {
    red { @Override public void draw() { } },
    green { @Override public void draw() { } },
    blue { @Override public void draw() { } },
    ;
    public abstract void draw();
}
Popular usage of this technique is explained here. Unfortunately it is no always possible to implement method in enum itself because:
  1. the enum may belong to third party library or other team in the company
  2. the enum is probably overloaded with too many other data and functions so it becomes not readable
  3. the enum belongs to module that does not have dependencies required for implementation of method draw().

This article suggests the following solutions for this problem.

Mirror enum

We cannot modify enum Color? No problem! Let’s create enum DrawableColor that has exactly same elements as Color. This new enum will implement our method draw():
enum DrawableColor {
    red { @Override public void draw() { } },
    green { @Override public void draw() { } },
    blue { @Override public void draw() { } },
    ;
    public abstract void draw();
}

This enum is a kind of reflection of source enum Color, i.e.Coloris its mirror.But how to use the new enum? All our code usesColor, notDrawableColor. The simplest way to implement this transition is using built-in enum methods name() and valueOf() as following:

Color color = ...
DrawableColor.valueOf(color.name()).draw();

Sincename()method is final and cannot be overridden andvalueOf()is generated by a compiler these methods are always fit each other, so no functional problems are expected here. Performance of such transition is good also: method name() even does not create new String but returns pre-initialized one (see source code ofjava.lang.Enum). MethodvalueOf()is implemented using Map, so its complexity is O(1).

The code above contains obvious problem. If source enum Color is changed the secondary enum DrawableColor does not know this fact, so the trick withname()andvalueOf()will fail at runtime. We do not want this to happen. But how to prevent possible failure?

We have to letDrawableColorto know that its mirror is Color and enforce this preferably at compile time or at least at unit test phase. Here we suggest validation during unit tests execution.Enumcan implement static initializer that is executed whenenumis mentioned in any code. This actually means that if static initializer validates that enum DrawableColor fits Color it is enough to implement test like following to be sure that the code will be never broken in production environment:

@Test
public void drawableColorFitsMirror {
    DrawableColor.values();
}

Static initializer just have to compare elements of DrawableColor and Color and throw exception if they do not match. This code is simple and can be written for each particular case. Fortunately simple  open source library named enumus already implements this functionality, so the task becomes trivial:

enum DrawableColor {
    ....
    static {
        Mirror.of(Color.class);
    }
}

That’s it. The test will fail if source enum andDrawableColordo not fit it any more. Utility classMirrorhas other method that gets 2 arguments: classes of 2 enums that have to fit. This version can be called from any place in code and not only from enum that has to be validated.

EnumMap

Do we really have to define another enum that just holds implementation of one method? In fact, we do not have to. Here is an alternative solution. Let’s define interface Drawer as following:

public interface Drawer {
    void draw();
}

Now let’s create mapping between enum elements and implementation of interface Drawer:

Map<Color, Drawer> drawers = new EnumMap<>(Color.class) {{
    put(red, new Drawer() { @Override public void draw();});
    put(green, new Drawer() { @Override public void draw();})
    put(blue, new Drawer() { @Override public void draw();})
}}

The usage is simple:

1
drawers.get(color).draw();

EnumMap is chosen here as a Map implementation for better performance. Map guaranties that each enum element appears there only once. However, it does not guarantee that there is entry for each enumelement. But it is enough to check that size of the map is equal to number of enumelements:

1
drawers.size() == Color.values().length

Enumus suggests convenient utility for this case also. The following code throws IllegalStateException with descriptive message if map does not fit Color:

1
EnumMapValidator.validateValues(Color.class, map, "Colors map");

It is important to call the validator from the code which is executed by unit test. In this case the map based solution is safe for future modifications of source enum.

EnumMap and Java 8 functional interface

In fact, we do not have to define special interface to extend
enum functionality. We can use one of functional interfaces provided by JDK starting from version 8 (Function,BiFunction,Consumer,BiConsumer,
Supplieretc
.) The choice depends on parameters that have to be sent to the function. For example, Supplier can be used instead of Drawable defined in the previous example:

1
2
3
4
5
Map<Color, Supplier<Void>> drawers = new EnumMap<>(Color.class) {{
    put(red, new Supplier<Void>() { @Override public void get();});
    put(green, new Supplier<Void>() { @Override public void get();})
    put(blue, new Supplier<Void>() { @Override public void get();})
}}

Usage of this map is pretty similar to one from the previous example:

1
drawers.get(color).get();

This map can be validated exactly as the map that stores instances of
Drawable. 

Conclusions

This article shows how powerful can be Java enums if we put some logic inside. It also demonstrates two ways to expand the functionality of enums that work despite the language limitations. The article introduces to user the open source library named enumus that provides several useful utilities that help to operate enums easier.

Published on Java Code Geeks with permission by Alexander Radzin, partner at our JCG program. See the original article here: Two ways to extend enum functionality

Opinions expressed by Java Code Geeks contributors are their own.

Alexander Radzin

Alex is an experienced software engineer coding java and JVM based languages and technologies since 2000. Besides his work at the office he develops his own open source project at github and contributes code to other projects. Actively participated in StackOverflow community during several years.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button