DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Microservices Testing: Key Strategies and Tools
  • Testing in DevOps – The Basic and Critical Things You Need to Know
  • Why I Prefer Flutter Over React Native for App Development
  • Top 10 Open Source Projects for SREs and DevOps

Trending

  • System Coexistence: Bridging Legacy and Modern Architecture
  • After 9 Years, Microsoft Fulfills This Windows Feature Request
  • Introduction to Retrieval Augmented Generation (RAG)
  • Useful System Table Queries in Relational Databases
  1. DZone
  2. Popular
  3. Open Source
  4. Enum Tricks: Two Ways to Extend Enum Functionality

Enum Tricks: Two Ways to Extend Enum Functionality

Learn more about the power of enums!

By 
Alexander Radzin user avatar
Alexander Radzin
·
Mar. 15, 19 · Tutorial
Likes (9)
Comment
Save
Tweet
Share
128.6K Views

Join the DZone community and get the full member experience.

Join For Free

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

Introduction

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

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

Here is a simple enum:

enum Color {red, green, blue}


This class tries to extend it:

class SubColor extends Color {}


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

$ 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." Enumcan implement methods. For example, enumColor 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 not always possible to implement method in enumitself because:

  1. theenummay belong to a third-party library or another team in the company

  2. theenumis probably overloaded with other data and functions, so it becomes unreadable

  3. the enumbelongs to a 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 enumColor? No problem! Let's create enumDrawableColor that has exactly the 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.Color is its mirror.

But how doe we use the newenum? All our code uses Color, not DrawableColor. 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();


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

The code above contains obvious problem. If source enumColor is changed, the secondary enumDrawableColordoes not know this fact, so the trick with name()and valueOf() will fail at runtime. We do not want this to happen. But how to prevent possible failure? We have to letDrawableColor 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. Enum can implement a static initializer that is executed when enum is mentioned in any code. This actually means that if static initializer validates that enumDrawableColor fits Color, it is enough to implement a test like the following to be sure that the code will be never broken in production environment:

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


Static initializer just has to compare elements ofDrawableColorand Color and throw an exception if they do not match. This code is simple and can be written for each particular case. Fortunately, a 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 and DrawableColor do not fit it any more. Utility class Mirror has other methods that gets two arguments: classes of two 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 Draweras following:

public interface Drawer {
    void draw();
}


The next examples assume that all elements of enum Color are statically imported.

Now, let's create mapping between enum elements and implementation of interfaceDrawer:

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:

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 enum element. But it is enough to check that size of the map is equal to number ofenum elements:

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:

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, Supplier, etc.) 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:

Map<Color, Supplier<Void>> drawers = new EnumMap<Color, Supplier<Void>>(Color.class) {{
    put(red, new Supplier<Void>() { @Override public Void get() {return null;}});
    put(green, new Supplier<Void>() { @Override public Void get() {return null;}});
    put(blue, new Supplier<Void>() { @Override public Void get() {return null;}});
}}


The previous code snippet can be simplified:

Map<Color, Supplier<Void>> drawers = new EnumMap<Color, Supplier<Void>>(Color.class) {{
    put(red, () -> null);
    put(green, () -> null);
    put(blue, () -> null);
}};


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

drawers.get(color).get();


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

Conclusion

This article shows how powerful Java enumscan be if we put some logic inside. It also demonstrates two ways to expand the functionality ofenumsthat 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.

unit test Open source

Opinions expressed by DZone contributors are their own.

Related

  • Microservices Testing: Key Strategies and Tools
  • Testing in DevOps – The Basic and Critical Things You Need to Know
  • Why I Prefer Flutter Over React Native for App Development
  • Top 10 Open Source Projects for SREs and DevOps

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!