22
loading...
This website collects cookies to deliver better user experience
An enum type is a special data type that enables for a variable to be a set of predefined constants. The variable must be equal to one of the values that have been predefined for it. Common examples include compass directions (values of NORTH, SOUTH, EAST, and WEST) and the days of the week.
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY
}
Colour
(the British spelling, because I'm not in the US). The actual enum might be something different and complicated, but the example of Colour
works for the purposes of this article.Colour
has three colours defined inside it -- RED
, GREEN
, and BLUE
.public enum Colour {
RED(255, 0, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255);
int r, g, b;
Colour(int r, int g, int b) {
this.r = r;
this.g = g;
this.b = b;
}
// getters and toString()
}
Colour
enum is being used across lots of applications as a dependency. First, you wonder why someone needed to put the Colours as an Enum datatype. But unable to do anything about it, you just accept it and work with whatever is in your fate.Colour.RED
. We do not want to meddle with existing usages of Colour.RED
in our code.enum
to class
, your IDE should instantly throw up an error. This is because Enums are compile time constructs which are being referred in other parts of you application. But not to worry!Colour.RED
same, we would need to create constants for all the colours defined in the enum (now class).public final class Colour {
public static final Colour RED = new Colour(255, 0, 0);
public static final Colour GREEN = new Colour(0, 255, 0);
public static final Colour BLUE = new Colour(0, 0, 255);
private final int r, g, b;
private Colour(int r, int g, int b) {
this.r = r;
this.g = g;
this.b = b;
}
// getters and toString()
}
Colour
to be created outside the class. If we do not do this, we are essentially allowing anyone to create Colour
objects, and defeats the purpose of mimicing the enum. Thus, we would mark the constructor as private
.Colour
class atleast). The above refactoring makes sure that you can still refer to Colour.RED
as earlier. Also, now that we made the variables of r
, g
, and b
as private final
as we do not want them to change after object creation.values()
and valueOf()
are used quite often with enums. To ensure that these usages do not break, we would need to "mimic" these methods. How can we do that?values()
and valueOf()
.valueOf(String)
returns in instance of Colour
defined by the name provided as a parameter.values()
returns an array of Colour
, i.e. Colour[]
valueOf()
method, and it will lead us to the solution of values()
valueOf()
accepts a String
as an argument, and returns an instance of Colour
. What can we use to preserve the mapping between a String
and Colour
? A Map!! HashMap
or ConcurrentHashMap
for this.HashMap
or ConcurrentHashMap
, instead use LinkedHashMap
, for reasons that will be explained later in the article.public static final Colour RED = new Colour(255, 0, 0);
Colour.RED
or Colour.GREEN
? Is there a way we can get the "text"/"name" of the variable as a String
? Not directly, no. It allows an executing Java program to examine or "introspect" upon itself, and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the names of all its members and display them.
Map<String, Colour>
as a HashMap
or ConcurrentHashMap
. We will then use Java Reflection to load up the values inside the Map through a static
block.public final class Colour {
public static final Colour RED = new Colour(255, 0, 0);
public static final Colour GREEN = new Colour(0, 255, 0);
public static final Colour BLUE = new Colour(0, 0, 255);
private static final Map<String, Colour> map = new LinkedHashMap<>();
static {
loadClassData();
}
private static void loadClassData() {
Arrays.stream(Colour.class.getDeclaredFields())
.filter(declaredField -> declaredField.getType() == Colour.class)
.forEach(Colour::putInMap);
}
private static void putInMap(Field declaredField) {
try {
map.putIfAbsent(declaredField.getName(), (Colour) declaredField.get(null));
} catch (IllegalAccessException e) {
System.err.println("Could not initialize Colour Map value: " + declaredField.getName() + " " + e);
}
}
private final int r, g, b;
private Colour(int r, int g, int b) {
this.r = r;
this.g = g;
this.b = b;
}
// getters and toString()
}
Colour.class.getDeclaredFields()
fetches all the fields declared inside the Colour
class.Colour
. (The previous statement would also return Map<String, Colour>
)Colour
, we would call the putInMap()
method.putInMap()
takes a parameter of type Field
, and loads the data in the map. The variable name is obtained by declaredField.getName()
, and the actual object is returned by declaredField.get(null)
private static void loadClassData() {
for (Field declaredField : Colour.class.getDeclaredFields()) {
if (declaredField.getType() == Colour.class) {
putInMap(declaredField);
}
}
}
valueOf()
method as follows:public static Colour valueOf(String name) {
Colour colour = map.get(name);
if (colour == null) {
throw new IllegalArgumentException("No Colour by the name " + name + " found");
}
return colour;
}
valueOf()
returns an IllegalArgumentException
if no value is found within the enum. Similarly, we will ensure that our implementation also returns the same exception.values()
method. We can implement it by using the map.values()
method.public static Colour[] values() {
return map.values().toArray(Colour[]::new).clone();
}
values()
method produces an array in the order in which the Enum values are defined, we need to preserve the order in the Map as well. This is why we need to use a LinkedHashMap
, rather than another Map implementation.values()
is called, it returns the clone of the array values in the map.colourName
.public final class Colour {
public static final Colour RED = new Colour("RED", 255, 0, 0);
public static final Colour GREEN = new Colour("GREEN", 0, 255, 0);
public static final Colour BLUE = new Colour("BLUE", 0, 0, 255);
private static final Map<String, Colour> map = new LinkedHashMap<>();
// other implemented methods
// new field
private final String colourName;
private final int r, g, b;
private Colour(String colourName, int r, int g, int b) {
this.colourName = colourName;
this.r = r;
this.g = g;
this.b = b;
}
// getters and toString()
}
static {
loadClassData();
loadDataFromDb();
}
private static void loadDataFromDb() {
List<ColourDB.ColourData> colourData = new ColourDB().getColours();
for (ColourDB.ColourData colourDatum : colourData) {
map.putIfAbsent(colourDatum.getColourName(), new Colour(colourDatum));
}
}
ColourDB
contains the static class ColourData
, which is exactly the same as the Colour
POJO. As we cannot create Colour
objects, we need another type to put the data in, and get the data from.ColourDB
is like the following:public class ColourDB {
public List<ColourData> getColours() {
// data from DB
}
static class ColourData {
String colourName;
int r;
int g;
int b;
public ColourData(String colourName, int r, int g, int b) {
this.colourName = colourName;
this.r = r;
this.g = g;
this.b = b;
}
// getters
}
}
Colour
that accepts ColourData
.private Colour(ColourDB.ColourData colourDatum) {
this.colourName = colourDatum.getColourName();
this.r = colourDatum.getR();
this.g = colourDatum.getG();
this.b = colourDatum.getB();
}
Colour.RED
. If we add the data for the colour "BLACK" in the DB, we cannot refer to it as Colour.BLACK
after this change. We would need to refer to it as Colour.valueOf("BLACK")
, and get the value. This is a trade off required to make it dynamic. However, this allows us to ensure that the existing code is not impacted.colourName
, refactor it from getColourName()
to name()
.ordinal()
method of the enum, ensure that you introduce the ordinal field in the Colour
class as well. You would need to store the ordinal field in the DB too.equals()
method and change it to compare using ==
as is done in Enums. One would also need to implement the Comparable
interface with the compareTo()
method.Enum
functionality. Also, Enums cannot be cloned. Hence, we will also implement the clone method and throw CloneNotSupportedException
.public final class Colour implements Comparable<Colour>, Serializable {
public static final Colour RED = new Colour("RED", 255, 0, 0, 0);
public static final Colour GREEN = new Colour("GREEN", 0, 255, 0, 1);
public static final Colour BLUE = new Colour("BLUE", 0, 0, 255, 2);
private static final Map<String, Colour> map = new LinkedHashMap<>();
static {
loadClassData();
loadDataFromDb();
}
private static void loadClassData() {
Arrays.stream(Colour.class.getDeclaredFields())
.filter(declaredField -> declaredField.getType() == Colour.class)
.forEach(Colour::putInMap);
}
private static void loadDataFromDb() {
List<ColourDB.ColourData> colourData = new ColourDB().getColours();
for (ColourDB.ColourData colourDatum : colourData) {
map.putIfAbsent(colourDatum.getColourName(), new Colour(colourDatum));
}
}
public static Colour[] values() {
return map.values().toArray(Colour[]::new).clone();
}
public static Colour valueOf(String name) {
Colour colour = map.get(name);
if (colour == null) {
throw new IllegalArgumentException("No Colour by the name " + name + " found");
}
return colour;
}
private final String colourName;
private final int r, g, b;
private final int ordinal;
private Colour(String colourName, int r, int g, int b, int ordinal) {
this.colourName = colourName;
this.r = r;
this.g = g;
this.b = b;
this.ordinal = ordinal;
}
private Colour(ColourDB.ColourData colourData) {
this.colourName = colourData.getColourName();
this.r = colourData.getR();
this.g = colourData.getG();
this.b = colourData.getB();
this.ordinal = colourData.getOrdinal();
}
private static void putInMap(Field declaredField) {
try {
map.putIfAbsent(declaredField.getName(), (Colour) declaredField.get(null));
} catch (IllegalAccessException e) {
System.err.println("Could not initialize Colour Map value: " + declaredField.getName() + " " + e);
}
}
public String name() {
return colourName;
}
public int ordinal() {
return ordinal;
}
// getters
// ..
// ..
@Override
public boolean equals(Object o) {
return this == o;
}
@Override
public int hashCode() {
return Objects.hash(colourName, r, g, b, ordinal);
}
@Override
public final int compareTo(Colour o) {
Colour self = this;
return self.ordinal - o.ordinal;
}
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
@Override
public String toString() {
return "Colour{" +
"colourName='" + colourName + '\'' +
", r=" + r +
", g=" + g +
", b=" + b +
", ordinal=" + ordinal +
'}';
}
}
ColourDB
aspublic class ColourDB {
public List<ColourData> getColours() {
// data from DB
}
static class ColourData {
String colourName;
int r;
int g;
int b;
int ordinal;
public ColourData(String colourName, int r, int g, int b, int ordinal) {
this.colourName = colourName;
this.r = r;
this.g = g;
this.b = b;
this.ordinal = ordinal;
}
// getters
}
}
private static void loadDataFromDb() {
List<ColourDB.ColourData> colourData = new ColourDB().getColours();
for (ColourDB.ColourData colourDatum : colourData) {
map.putIfAbsent(colourDatum.getColourName(), new Colour(colourDatum));
}
}
new ColourDB()
is hard-coded inside the class. Testing would require an actual Database connection. We would want to Mock it during testing. If we were using a DI framework like Spring, we could have injected it. However, using pure Java code would require some more refactoring.ColourDB
into an interface and include the actual implementation as ColourDbImpl
.public interface ColourDB {
List<ColourData> getColours();
class ColourData {
// existing
}
}
public class ColourDBImpl implements ColourDB {
@Override
public List<ColourData> getColours() {
// get from DB
}
}
DB
:public class DB {
private static ColourDB COLOUR_DB;
public DB(ColourDB colourDB) {
COLOUR_DB = colourDB;
}
public static ColourDB getColourDb() {
if (COLOUR_DB == null) {
COLOUR_DB = new ColourDBImpl();
}
return COLOUR_DB;
}
public static void setColourDb(ColourDB colourDb) {
COLOUR_DB = colourDb;
}
}
ColourDB
in loadDataFromDb
with DB.getColourDb()
. The code now looks likeprivate static void loadDataFromDb() {
List<ColourDB.ColourData> colourData = DB.getColourDb().getColours();
for (ColourDB.ColourData colourDatum : colourData) {
map.putIfAbsent(colourDatum.getColourName(), new Colour(colourDatum));
}
}
ColourTest
as follows@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ColourTest {
public static final String BLACK = "BLACK";
public static final String WHITE = "WHITE";
public static final String YELLOW = "YELLOW";
public static final String RED = "RED";
@Mock
ColourDBImpl colourDB;
@InjectMocks
DB db;
@BeforeAll
void setUp() {
MockitoAnnotations.openMocks(this);
ColourDB.ColourData black = new ColourDB.ColourData(BLACK, 255, 255, 255, 3);
ColourDB.ColourData white = new ColourDB.ColourData(WHITE, 0, 0, 0, 4);
ColourDB.ColourData yellow = new ColourDB.ColourData(YELLOW, 255, 255, 0, 5);
Mockito.when(colourDB.getColours()).thenReturn(List.of(black, white, yellow));
}
@Test
void test_values() {
Colour[] values = Colour.values();
assertEquals(Colour.RED.name(), values[0].name());
assertEquals(Colour.valueOf(YELLOW).name(), values[values.length - 1].name());
assertTrue(Arrays.stream(values).anyMatch(colour -> colour.name().equals(Colour.RED.name())));
assertTrue(Arrays.stream(values).anyMatch(colour -> colour.name().equals(Colour.valueOf(WHITE).name())));
assertEquals(6, values.length);
}
@Test
void test_if_instances_are_same() {
assertSame(Colour.RED, Colour.valueOf(RED));
assertSame(Colour.valueOf(RED), Colour.valueOf(RED));
assertEquals(Colour.valueOf(RED), Colour.valueOf(RED));
assertSame(Colour.valueOf(WHITE), Colour.valueOf(WHITE));
assertEquals(Colour.valueOf(WHITE), Colour.valueOf(WHITE));
}
@Test
void test_ordinal() {
assertEquals(0, Colour.RED.ordinal()); // static
assertEquals(5, Colour.valueOf(YELLOW).ordinal()); // dynamic
}
@Test
void test_invalid_colour() {
String magenta = "MAGENTA";
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> Colour.valueOf(magenta));
assertEquals("No Colour by the name " + magenta + " found", exception.getMessage());
}
@Test
void test_compareTo() {
int red = Colour.RED.compareTo(Colour.valueOf("WHITE"));
assertTrue(red < 0);
int yellow = Colour.valueOf("YELLOW").compareTo(Colour.RED);
assertTrue(yellow > 0);
}
@Test
void test_name() {
assertEquals("RED", Colour.RED.name());
assertEquals("YELLOW", Colour.valueOf("YELLOW").name());
}
}
@BeforeAll
with @TestInstance(TestInstance.Lifecycle.PER_CLASS)
as the static block is only executed once.UnnecessaryStubbingException
because the Mock would not execute every test.name
of the field is serialized, and deserialization uses valueOf()
method to get the Enum constant back.Colour
as well. We can do that by implementing the readResolve
1 method. This will ensure that we only receive an instance of the same class as the one that we have already created. We already store the colour name. So when a new object is created, it will still return the already existing objects that we expect.public final class Colour implements Comparable<Colour>, Serializable {
// Existing code
//
//
private Object readResolve() {
return Colour.valueOf(colourName);
}
}
@Test
void test_serialization_deserialization() throws IOException, ClassNotFoundException {
serialize(Colour.valueOf(BLACK));
Colour black = deserialize();
assertNotNull(black);
assertEquals(Colour.valueOf(BLACK), black);
assertSame(Colour.valueOf(BLACK), black);
serialize(Colour.RED);
Colour red = deserialize();
assertNotNull(red);
assertEquals(Colour.valueOf(RED), red);
assertSame(Colour.valueOf(RED), red);
assertEquals(Colour.RED, red);
assertSame(Colour.RED, red);
}
void serialize(Colour colour) throws IOException {
try (FileOutputStream fos = new FileOutputStream("data.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(colour);
}
}
Colour deserialize() throws IOException, ClassNotFoundException {
try (FileInputStream fis = new FileInputStream("data.obj");
ObjectInputStream ois = new ObjectInputStream(fis)) {
return (Colour) ois.readObject();
}
}
Colour
. The values in DB must be referred through Colour.valueOf()
. Moreover, you would need to change any switch-case statements to use the value of the the Constant, instead of static enum types supported by switch-case. An example:switch (Colour.RED.name()) {
case "RED" :
System.out.println("RED");
break;
default:
}