How and when to use Enums and Annotations
This article is part of our Academy Course titled Advanced Java.
This course is designed to help you make the most effective use of Java. It discusses advanced topics, including object creation, concurrency, serialization, reflection and many more. It will guide you through your journey to Java mastery! Check it out here!
Table Of Contents
- 1. Introduction
- 2. Enums as special classes
- 3. Enums and instance fields
- 4. Enums and interfaces
- 5. Enums and generics
- 6. Convenient Enums methods
- 7. Specialized Collections: EnumSet and EnumMap
- 8. When to use enums
- 9. Annotations as special interfaces
- 10. Annotations and retention policy
- 11. Annotations and element types
- 12. Annotations and inheritance
- 13. Repeatable annotations
- 14. Annotation processors
- 15. Annotations and configuration over convention
- 16. When to use annotations
- 17. What’s next
- 18. Download the Source Code
1. Introduction
In this part of the tutorial we are going to cover yet another two great features introduced into the language as part of Java 5 release along with generics: enums (or enumerations) and annotations. Enums could be treated as a special type of classes and annotations as a special type of interfaces.
The idea of enums is simple, but quite handy: it represents a fixed, constant set of values. What it means in practice is that enums are often used to design the concepts which have a constant set of possible states. For example, the days of week are a great example of the enums: they are limited to Monday, Tuesday, Wednesday, Thursday, Friday, Saturday and Sunday.
From the other side, annotations are a special kind of metadata which could be associated with different elements and constructs of the Java language. Interestingly, annotations have contributed a lot into the elimination of boilerplate XML descriptors used in Java ecosystem mostly everywhere. They introduced the new, type-safe and robust way of configuration and customization techniques.
2. Enums as special classes
Before enums had been introduced into the Java language, the regular way to model the set of fixed values in Java was just by declaring a number of constants. For example:
public class DaysOfTheWeekConstants { public static final int MONDAY = 0; public static final int TUESDAY = 1; public static final int WEDNESDAY = 2; public static final int THURSDAY = 3; public static final int FRIDAY = 4; public static final int SATURDAY = 5; public static final int SUNDAY = 6; }
Although this approach kind of works, it is far from being the ideal solution. Primarily, because the constants themselves are just values of type int
and every place in the code where those constants are expected (instead of arbitrary int
values) should be explicitly documented and asserted all the time. Semantically, it is not a type-safe representation of the concept as the following method demonstrates.
public boolean isWeekend( int day ) { return( day == SATURDAY || day == SUNDAY ); }
From logical point of view, the day argument should have one of the values declared in the DaysOfTheWeekConstants
class. However, it is not possible to guess that without additional documentation being written (and read afterwards by someone). For the Java compiler the call like isWeekend (100)
looks absolutely correct and raises no concerns.
Here the enums come to the rescue. Enums allow to replace constants with the typed values and to use those types everywhere. Let us rewrite the solution above using enums.
public enum DaysOfTheWeek { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
What changed is that the class
becomes enum
and the possible values are listed in the enum definition. The distinguishing part however is that every single value is the instance of the enum class it is being declared at (in our example, DaysOfTheWeek
). As such, whenever enum are being used, the Java compiler is able to do type checking. For example:
public boolean isWeekend( DaysOfTheWeek day ) { return( day == SATURDAY || day == SUNDAY ); }
Please notice that the usage of the uppercase naming scheme in enums is just a convention, nothing really prevents you from not doing that.
3. Enums and instance fields
Enums are specialized classes and as such are extensible. It means they can have instance fields, constructors and methods (although the only limitations are that the default no-args constructor cannot be declared and all constructors must be private
). Let us add the property isWeekend
to every day of the week using the instance field and constructor.
public enum DaysOfTheWeekFields { MONDAY( false ), TUESDAY( false ), WEDNESDAY( false ), THURSDAY( false ), FRIDAY( false ), SATURDAY( true ), SUNDAY( true ); private final boolean isWeekend; private DaysOfTheWeekFields( final boolean isWeekend ) { this.isWeekend = isWeekend; } public boolean isWeekend() { return isWeekend; } }
As we can see, the values of the enums are just constructor calls with the simplification that the new
keyword is not required. The isWeekend()
property could be used to detect if the value represents the week day or week-end. For example:
public boolean isWeekend( DaysOfTheWeek day ) { return day.isWeekend(); }
Instance fields are an extremely useful capability of the enums in Java. They are used very often to associate some additional details with each value, using regular class declaration rules.
4. Enums and interfaces
Another interesting feature, which yet one more time confirms that enums are just specialized classes, is that they can implement interfaces (however enums cannot extend any other classes for the reasons explained later in the Enums and generics section). For example, let us introduce the interface DayOfWeek
.
interface DayOfWeek { boolean isWeekend(); }
And rewrite the example from the previous section using interface implementation instead of regular instance fields.
public enum DaysOfTheWeekInterfaces implements DayOfWeek { MONDAY() { @Override public boolean isWeekend() { return false; } }, TUESDAY() { @Override public boolean isWeekend() { return false; } }, WEDNESDAY() { @Override public boolean isWeekend() { return false; } }, THURSDAY() { @Override public boolean isWeekend() { return false; } }, FRIDAY() { @Override public boolean isWeekend() { return false; } }, SATURDAY() { @Override public boolean isWeekend() { return true; } }, SUNDAY() { @Override public boolean isWeekend() { return true; } }; }
The way we have implemented the interface is a bit verbose, however it is certainly possible to make it better by combining instance fields and interfaces together. For example:
public enum DaysOfTheWeekFieldsInterfaces implements DayOfWeek { MONDAY( false ), TUESDAY( false ), WEDNESDAY( false ), THURSDAY( false ), FRIDAY( false ), SATURDAY( true ), SUNDAY( true ); private final boolean isWeekend; private DaysOfTheWeekFieldsInterfaces( final boolean isWeekend ) { this.isWeekend = isWeekend; } @Override public boolean isWeekend() { return isWeekend; } }
By supporting instance fields and interfaces, enums can be used in a more object-oriented way, bringing some level of abstraction to rely upon.
5. Enums and generics
Although it is not visible from a first glance, there is a relation between enums and generics in Java. Every single enum in Java is automatically inherited from the generic Enum< T >
class, where T
is the enum type itself. The Java compiler does this transformation on behalf of the developer at compile time, expanding enum declaration public enum DaysOfTheWeek
to something like this:
public class DaysOfTheWeek extends Enum< DaysOfTheWeek > { // Other declarations here }
It also explains why enums can implement interfaces but cannot extend other classes: they implicitly extend Enum< T >
and as we know from the part 2 of the tutorial, Using methods common to all objects, Java does not support multiple inheritance.
The fact that every enum extends Enum< T >
allows to define generic classes, interfaces and methods which expect the instances of enum types as arguments or type parameters. For example:
public< T extends Enum < ? > > void performAction( final T instance ) { // Perform some action here }
In the method declaration above, the type T
is constrained to be the instance of any enum and Java compiler will verify that.
6. Convenient Enums methods
The base Enum< T >
class provides a couple of helpful methods which are automatically inherited by every enum instance.
Additionally, Java compiler automatically generates two more helpful static
methods for every enum type it encounters (let us refer to the particular enum type as T).
Because of the presence of these methods and hard compiler work, there is one more benefit of using enums in your code: they can be used in switch/case
statements. For example:
public void performAction( DaysOfTheWeek instance ) { switch( instance ) { case MONDAY: // Do something break; case TUESDAY: // Do something break; // Other enum constants here } }
7. Specialized Collections: EnumSet and EnumMap
Instances of enums, as all other classes, could be used with the standard Java collection library. However, certain collection types have been optimized for enums specifically and are recommended in most cases to be used instead of general-purpose counterparts.
We are going to look on two specialized collection types: EnumSet< T >
and EnumMap< T, ? >
. Both are very easy to use and we are going to start with the EnumSet< T >
.
The EnumSet< T >
is the regular set optimized to store enums effectively. Interestingly, EnumSet< T >
cannot be instantiated using constructors and provides a lot of helpful factory methods instead (we have covered factory pattern in the part 1 of the tutorial, How to create and destroy objects).
For example, the allOf
factory method creates the instance of the EnumSet< T >
containing all enum constants of the enum type in question:
final Set< DaysOfTheWeek > enumSetAll = EnumSet.allOf( DaysOfTheWeek.class );
Consequently, the noneOf
factory method creates the instance of an empty EnumSet< T >
for the enum type in question:
final Set< DaysOfTheWeek > enumSetNone = EnumSet.noneOf( DaysOfTheWeek.class );
It is also possible to specify which enum constants of the enum type in question should be included into the EnumSet< T >
, using the of
factory method:
final Set< DaysOfTheWeek > enumSetSome = EnumSet.of( DaysOfTheWeek.SUNDAY, DaysOfTheWeek.SATURDAY );
The EnumMap< T, ? >
is very close to the regular map with the difference that its keys could be the enum constants of the enum type in question. For example:
final Map< DaysOfTheWeek, String > enumMap = new EnumMap<>( DaysOfTheWeek.class ); enumMap.put( DaysOfTheWeek.MONDAY, "Lundi" ); enumMap.put( DaysOfTheWeek.TUESDAY, "Mardi" );
Please notice that, as most collection implementations, EnumSet< T >
and EnumMap< T, ? >
are not thread-safe and cannot be used as-is in multithreaded environment (we are going to discuss thread-safety and synchronization in the part 9 of the tutorial, Concurrency best practices).
8. When to use enums
Since Java 5 release enums are the only preferred and recommended way to represent and dial with the fixed set of constants. Not only they are strongly-typed, they are extensible and supported by any modern library or framework.
9. Annotations as special interfaces
As we mentioned before, annotations are the syntactic sugar used to associate the metadata with different elements of Java language.
Annotations by themselves do not have any direct effect on the element they are annotating. However, depending on the annotations and the way they are defined, they may be used by Java compiler (the great example of that is the @Override
annotation which we have seen a lot in the part 3 of the tutorial, How to design Classes and Interfaces), by annotation processors (more details to come in the Annotation processors section) and by the code at runtime using reflection and other introspection techniques (more about that in the part 11 of the tutorial, Reflection and dynamic languages support).
Let us take a look at the simplest annotation declaration possible:
public @interface SimpleAnnotation { } The @interface keyword introduces new annotation type. That is why annotations could be treated as specialized interfaces. Annotations may declare the attributes with or without default values, for example: public @interface SimpleAnnotationWithAttributes { String name(); int order() default 0; }
If an annotation declares an attribute without a default value, it should be provided in all places the annotation is being applied. For example:
@SimpleAnnotationWithAttributes( name = "new annotation" )
By convention, if the annotation has an attribute with the name value
and it is the only one which is required to be specified, the name of the attribute could be omitted, for example:
public @interface SimpleAnnotationWithValue { String value(); } It could be used like this: @SimpleAnnotationWithValue( "new annotation" )
There are a couple of limitations which in certain use cases make working with annotations not very convenient. Firstly, annotations do not support any kind of inheritance: one annotation cannot extend another annotation. Secondly, it is not possible to create an instance of annotation programmatically using the new
operator (we are going to take a look on some workarounds to that in the part 11 of the tutorial, Reflection and dynamic languages support). And thirdly, annotations can declare only attributes of primitive types, String
or Class< ? >
types and arrays of those. No methods or constructors are allowed to be declared in the annotations.
10. Annotations and retention policy
Each annotation has the very important characteristic called retention policy which is an enumeration (of type RetentionPolicy) with the set of policies on how to retain annotations. It could be set to one of the following values.
Retention policy has a crucial effect on when the annotation will be available for processing. The retention policy could be set using @Retention
annotation. For example:
import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention( RetentionPolicy.RUNTIME ) public @interface AnnotationWithRetention { }
Setting annotation retention policy to RUNTIME
will guarantee its presence in the compilation process and in the running application.
11. Annotations and element types
Another characteristic which each annotation must have is the element types it could be applied to. Similarly to the retention policy, it is defined as enumeration (ElementType) with the set of possible element types.
Additionally to the ones described above, Java 8 introduces two new element types the annotations can be applied to.
In contrast to the retention policy, an annotation may declare multiple element types it can be associated with, using the @Target
annotation. For example:
import java.lang.annotation.ElementType; import java.lang.annotation.Target; @Target( { ElementType.FIELD, ElementType.METHOD } ) public @interface AnnotationWithTarget { }
Mostly all annotations you are going to create should have both retention policy and element types specified in order to be useful.
12. Annotations and inheritance
The important relation exists between declaring annotations and inheritance in Java. By default, the subclasses do not inherit the annotation declared on the parent class. However, there is a way to propagate particular annotations throughout the class hierarchy using the @Inherited
annotation. For example:
@Target( { ElementType.TYPE } ) @Retention( RetentionPolicy.RUNTIME ) @Inherited @interface InheritableAnnotation { } @InheritableAnnotation public class Parent { } public class Child extends Parent { }
In this example, the @InheritableAnnotation
annotation declared on the Parent
class will be inherited by the Child
class as well.
13. Repeatable annotations
In pre-Java 8 era there was another limitation related to the annotations which was not discussed yet: the same annotation could appear only once at the same place, it cannot be repeated multiple times. Java 8 eased this restriction by providing support for repeatable annotations. For example:
@Target( ElementType.METHOD ) @Retention( RetentionPolicy.RUNTIME ) public @interface RepeatableAnnotations { RepeatableAnnotation[] value(); } @Target( ElementType.METHOD ) @Retention( RetentionPolicy.RUNTIME ) @Repeatable( RepeatableAnnotations.class ) public @interface RepeatableAnnotation { String value(); }; @RepeatableAnnotation( "repeatition 1" ) @RepeatableAnnotation( "repeatition 2" ) public void performAction() { // Some code here }
Although in Java 8 the repeatable annotations feature requires a bit of work to be done in order to allow your annotation to be repeatable (using @Repeatable
), the final result is worth it: more clean and compact annotated code.
14. Annotation processors
The Java compiler supports a special kind of plugins called annotation processors (using the –processor
command line argument) which could process the annotations during the compilation phase. Annotation processors can analyze the annotations usage (perform static code analysis), create additional Java source files or resources (which in turn could be compiled and processed) or mutate the annotated code.
The retention policy (see please Annotations and retention policy) plays a key role by instructing the compiler which annotations should be available for processing by annotation processors.
Annotation processors are widely used, however to write one it requires some knowledge of how Java compiler works and the compilation process itself.
15. Annotations and configuration over convention
Convention over configuration is a software design paradigm which aims to simplify the development process when a set of simple rules (or conventions) is being followed by the developers. For example, some MVC (model-view-controller) frameworks follow the convention to place controllers in the ‘controller’ folder (or package). Another example is the ORM (object-relational mappers) frameworks which often follow the convention to look up classes in ‘model’ folder (or package) and derive the relation table name from the respective class.
On the other side, annotations open the way for a different design paradigm which is based on explicit configuration. Considering the examples above, the @Controller
annotation may explicitly mark any class as controller and @Entity
may refer to relational database table. The benefits also come from the facts that annotations are extensible, may have additional attributes and are restricted to particular element types. Improper use of annotations is enforced by the Java compiler and reveals the misconfiguration issues very early (on the compilation phase).
16. When to use annotations
Annotations are literally everywhere: the Java standard library has a lot of them, mostly every Java specification includes the annotations as well. Whenever you need to associate an additional metadata with your code, annotations are straightforward and easy way to do so.
Interestingly, there is an ongoing effort in the Java community to develop common semantic concepts and standardize the annotations across several Java technologies (for more information, please take a look on JSR-250 specification). At the moment, following annotations are included with the standard Java library.
And the Java 8 release adds a couple of new annotations as well.
17. What’s next
In this section we have covered enums (or enumerations) which are used to represent the fixed set of constant values, and annotations which decorate elements of Java code with metadata. Although somewhat unrelated, both concepts are very widely used in the Java. Despite the fact that in the next part of the tutorial we are going to look on how to write methods efficiently, annotations will be often the part of the mostly every discussion.
18. Download the Source Code
This was a lesson on How to design Classes and Interfaces. You may download the source code here: advanced-java-part-5
Hello,
You said : “And thirdly, annotations can declare only attributes of primitive types, String or Class types and arrays of those.”
I think u forgot enum type
Nice post btw.
Hi Nouaman,
That’s correct.
Thank you very much for pointing this out.
Best Regards,
Andriy Redko
Hey dude what about this? (you mentioned: although the only limitations are that the default no-args constructor cannot be declared)
public class TestingEnum{
public static void main(String []args){
System.out.println("Hello World");
Color color = Color.RED;
color.method();
}
}
enum Color{
RED, BLUE, GREEN;
private Color(){
System.out.println("Constructor initialisation for "+this.toString());
}
public void method(){
System.out.println("method invocation");
}
}
## OUTPUT
Constructor initialisation for RED
Constructor initialisation for BLUE
Constructor initialisation for GREEN
method invocation
Hi Musa,
My apologies for not being strict on this statement. The assumption behind “default no-args constructor” was to refer to “public constructor without arguments” (this is the one available by default even if not explicitly declared for public classes). Your example is valid, the private (and package private) constructors are allowed.
Best Regards,
Andriy Redko