|
The Java Specialists' Newsletter
Issue 113 2005-08-29
Category:
Language
Java version: Sun JDK 5.0 Enum Inversion Problemby Dr. Heinz M. KabutzAbstract:
A problem that I encountered when I first started using enums was how
to serialize them to some persistent store. My initial approach was to
write the ordinal to the database. In this newsletter, I explore some
ideas of a more robust approach. It will also show you some applications
of Java generics.
Welcome to the 113th edition of The Java(tm) Specialists' Newsletter. First off, I would
like to send a special welcome to Uzbekistan, bringing the
total
number of countries on our list to 111. In Europe we
still need subscribers in Albania, so if you know of Java
programmers there, please forward them our newsletter and ask
them to subscribe :)
I would like to thank Dr Wolfgang Laun from Alcatel Austria
for the question that led to this newsletter. We had an
inspiring exchange of emails that has culminated in this
newsletter. According to Dr Laun's colleagues, he is the
resident guru at Alcatel Austria, so I was honoured to join
him on this quest for an answer to this problem.
One of the benefits of you coming to one
of our courses is that you get free life-time email
support from The Java Specialists for Java related
questions. Most of our courses are run in-house at companies,
so please let us know if your company is desperate to spend
a little bit of their training budget :)
Upcoming Java Specialist Master Courses:
- please click here to sign up.
As from May 2010, we are also offering this course on the island of Crete. We
only accept 6 students per class in Crete, due to the size of our conference
room. Please book early to avoid disappointment!
San Jose CA, Mar 16-19 2010, $3500 Ottawa, Canada, Mar 22-25 2010, $3500 Oslo, Norway, Apr 13-16 2010, Kr 24500 Montreal, Canada, Apr 20-23 2010, $3500 Toronto, Canada, May 17-20 2010, $3500 Chania, Crete, May 25-28, Jun 29-Jul 2 or Aug 24-27 2010, €2500
In-house courses if these dates or locations do not suit you - click here for more information. Enum Inversion Problem
A problem that I encountered when I first started using
enums was how to serialize them to some persistent store.
My initial approach was to write the ordinal to the database.
(The ordinal is an integer representing the order of the
enum value, starting at zero.) Then I started receiving
phone calls from clients, asking what value 3 meant in the
Status column of the Postboxsendlog table. Since I was using
the automatic values of the ordinal, I did not know the
answer immediately.
Another problem is that when you change the order of the enum
values or add an enum in the middle, you also modify the
ordinal value. Instead of using the ordinal int, you could
use the toString() value, and then convert the String back to
the enum using Enum.valueOf(). Whilst this
would be less brittle than using the ordinal, it will break
if we rename the enum values.
A solution to the problem is to have a reverse lookup from
some value to an enum. Ideally we would like this to be
type safe using the new generics construct. We are limited
with the Java 5 enums, since we cannot subclass them.
The initial idea was to embed a reverse lookup table inside
each enum, but I rejected that because it did not result in a
clean design and would cause the copy-and-paste anti-pattern.
We could map the enum to any value that we wanted to, but in
this example, I am using a byte. If
you had some weird application where you needed more than 256
different enum values, you could use either a
short or an
int.
This interface defines the convert method that will be
implemented by each of the enums and should return a
byte that would be used to represent
this enum.
public interface EnumConverter {
public byte convert();
}
Here are two example enums, Animal and Tree:
public enum Animal implements EnumConverter {
Ape(100), Bee(50), Cat(80);
private final byte value;
Animal(int value) {
this.value = (byte) value;
}
public byte convert() {
return value;
}
}
public enum Tree implements EnumConverter {
Acorn(30), Birch(60), Cedar(40);
private final byte value;
Tree(int value) {
this.value = (byte) value;
}
public byte convert() {
return value;
}
}
In order to lookup the enum based on a byte value, we define
the ReverseEnumMap, for which you have to pass in a class
object that is an enum and implements EnumConverter. This
allows us to get all the enum values from the class, and
to then call the convert() method on them. Note the special
syntax that allows us to specify that a generic must
implement several interfaces is done with the &, such as
<V extends Enum<V> & EnumConverter>.
import java.util.*;
public class ReverseEnumMap<V extends Enum<V> & EnumConverter> {
private Map<Byte, V> map = new HashMap<Byte, V>();
public ReverseEnumMap(Class<V> valueType) {
for (V v : valueType.getEnumConstants()) {
map.put(v.convert(), v);
}
}
public V get(byte num) {
return map.get(num);
}
}
You could also hold the reverse map inside an array of size
256. Even though you cannot directly construct an instance
of an array of generics, you can do so using reflection. We
have the enum type class available in the constructor of the
ReverseEnumMap, so it is a matter of simply calling
Array.newInstance(valueType, 256);
We can now use the ReverseEnumMap to lookup the enum matching
a byte value. For example, it could be used in the
Communication class as follows:
import java.io.*;
public class Communication<E extends Enum<E> & EnumConverter> {
private final ReverseEnumMap<E> reverse;
public Communication(Class<E> ec) {
reverse = new ReverseEnumMap<E>(ec);
}
public void sendOne(OutputStream out, E e) throws IOException {
out.write(e.convert());
}
public E receiveOne(InputStream in) throws IOException {
int b = in.read();
if (b == -1) throw new EOFException();
return reverse.get((byte) b);
}
}
We can use this by creating a ByteArrayOutputStream, then
write an Animal Enum to that, and read it back again:
import java.io.*;
public class CommunicationTest {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Communication<Animal> acom = new Communication<Animal>(Animal.class);
acom.sendOne(baos, Animal.Ape);
baos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Animal animal = acom.receiveOne(bais);
System.out.println(animal);
}
}
This is one approach that we can use to read back enums from
some byte value.
Alternative Approach
Enums in Java have a disadvantage, in that you cannot have
a common superclass for your own enums. They are all
automatically derived from the java.lang.Enum
class, which you cannot modify.
Dr Laun was not happy that we were moving the complexity of
constructing the ReverseEnumMap inside our client code (in
this case the Communication class). I was not happy with
putting it into each enum because that would result in
unnecessary copy & paste code. However, Dr Laun has
convinced me that the fault lies with the enum and
therefore any copy & paste code belongs inside them and
not in the client code.
There is a slight catch. You cannot declare static methods
inside interfaces. Ideally we would declare that
EnumConverter has a static method that we need to implement
and which could then contain the functionality for converting
from a byte to the enum value. My idea was to
simply declare it as a non-static method, and let the client
call the method on any instance of the enum.
First, we define an additional method inside EnumConverter:
public interface EnumConverter <E extends Enum<E> & EnumConverter<E>> {
byte convert();
E convert(byte val);
}
We also move the ReverseEnumMap into the Animal definition:
public enum Animal implements EnumConverter<Animal> {
Ape(100), Bee(50), Cat(80);
private static ReverseEnumMap<Animal> map =
new ReverseEnumMap<Animal>(Animal.class);
private final byte value;
Animal(int value) {
this.value = (byte) value;
}
public byte convert() {
return value;
}
public Animal convert(byte val) {
return map.get(val);
}
}
We now construct the Communication instance with a sample
enum of the type that we want to convert, such as:
Communication<Animal> acom = new
Communication<Animal>(Animal.Ape). The
Communication class would need to change slightly as well:
import java.io.*;
public class Communication<E extends Enum<E> & EnumConverter<E>> {
private final E enumSample;
public Communication(E enumSample) {
this.enumSample = enumSample;
}
public void sendOne(OutputStream out, E e) throws IOException {
out.write(e.convert());
}
public E receiveOne(InputStream in) throws IOException {
int b = in.read();
if (b == -1) throw new EOFException();
return enumSample.convert((byte) b);
}
}
Neither approach satisfies me completely, but both will work.
Kind regards
Heinz
Language Articles
Related Java Course
|