|
The Java Specialists' Newsletter
Issue 017a 2001-04-26
Category:
Language
Java version: Switching on Object Handlesby Dr. Heinz M. Kabutz
Welcome to the 17th issue of "The Java(tm) Specialists'
Newsletter", after a few very busy days in the Mauritian
Paradise. Mauritius is a wonderful place to go to, with
extremely friendly people all around, treating you like kings
even in business. It's definitely worth a vacation, I wish I had
gotten one while I was there ;-)
Please remember to forward this newsletter to anyone who might be
interested, friends and foe.
Simulating Switch Statements on Handles with try-catch
This week I will talk about a really completely useless idea, how
to use switch/case statements on type-safe constants in Java.
This idea occurred to me while I was talking to a bunch of
programmers about TCP/IP programming, I don't think my topic of
conversation had anything to do with the hare-brained idea
presented in this newsletter. My listeners saw my eyes take on
a distant gaze and I muttered "hmmm, I wonder if..." so here you
go.
First I need to bore you with a monologe of why switch statements
are bad and why you should never use them. Switch statements
herald from a time before we used Object Orientation,
Encapsulation and Polymorphism, and were mostly used to write
methods which would do different things depending on the type of
record we had passed the method. For example, say we had a CAD
system, with a triangle, rectangle and circle, we could say:
public interface Constants {
int TRIANGLE_SHAPE = 0;
int RECTANGLE_SHAPE = 1;
int CIRCLE_SHAPE = 2;
}
Without encapsulation, we would then have a struct or class
without methods, looking like this:
public class Shape {
public int type;
public java.awt.Color color;
}
We would then have a CAD system for drawing these shapes, such as
public class CADSystem implements Constants {
public void draw(Shape shape) {
switch(shape.type) {
case TRIANGLE_SHAPE: // some code which draws a triangle
System.out.println("Triangle with color " + shape.color);
break;
case RECTANGLE_SHAPE: // some code which draws a rectangle
System.out.println("Rectangle with color " +shape.color);
break;
case CIRCLE_SHAPE: // some code which draws a circle
System.out.println("Circle with color " + shape.color);
break;
default: // error only found at runtime
throw new IllegalArgumentException(
"Shape has illegal type " + shape.type);
}
}
}
This was the old procedural way of writing such code. The result
was code where it was extremely challenging to add new types. In
addition, in Java such code is very dangerous because we don't
have enumerated types and you cannot switch on object references
(well, have a look further down on how you actually "can"). You
therefore could not be sure at compile time if you had defined
the method for all the various types in your CADSystem.
The answer in OO is to use inheritance, polymorphism and
encapsulation, the above example would thus be written as:
public abstract class Shape {
private final java.awt.Color color;
protected Shape(java.awt.Color color) { this.color = color; }
public java.awt.Color getColor() { return color; }
public abstract void draw();
}
public class Triangle extends Shape {
public Triangle(java.awt.Color color) { super(color); }
public void draw() {
System.out.println("Triangle with color " + getColor());
}
}
public class Rectangle extends Shape {
public Rectangle(java.awt.Color color) { super(color); }
public void draw() {
System.out.println("Rectangle with color " + getColor());
}
}
public class Circle extends Shape {
public Circle(java.awt.Color color) { super(color); }
public void draw() {
System.out.println("Circle with color " + getColor());
}
}
public class CADSystem {
public void draw(Shape shape) {
shape.draw();
}
}
Now if we forget to implement one of the draw methods, we'll
immediately get a compile-time error. Of course, if we extend
Rectangle and forget to implement the draw method we'll get the
wrong shape, so a certain level of diligence in testing is still
required.
It is possible to take a switch statements and transform it to
polymorphism using various refactorings. In previous newsletters
I mentioned the book "Refactoring" by Martin Fowler. In case you
hadn't noticed, I am a fan (of the book, that is). In that book
you can find refactorings to transform a switch/case statement to
polymorphism or polymorphism back to a switch/case statement.
So, in the unfortunate case (haha, pun intended) that you want to
use a switch-type of construct but you don't want to worry about
the anonymity of using int's as type identifiers, how do you do
it?
We demonstrate by using a TransactionType class which defines
the transaction isolation levels you find in most enterprise
systems. The isolation types are None, Read Uncommitted, Read
Committed, Repeatable Read and Serializable. The point of this
newsletter is not to describe transaction isolations, so I won't
go into what they all mean. Rumour has it though, that if you
use them without knowing what they mean, you will get a system
which doesn't work, HA.
We define a TransactionType superclass with a private
constructor, so that it is not possible to construct instances of
these types or to subclass it, except from within the type. The
constructor takes a name as a description, which can be returned
via the toString() method. The reason why the type class has to
be Throwable will become clear in the example.
We then make public static inner classes for each of the types,
again with private constructors, and make public static final
instances of these types in each of the inner classes. The
reason why we need classes and instances will also become clearer
in the example.
//: TransactionType.java
public class TransactionType extends Throwable {
private final String name;
private TransactionType(String name) {
this.name = name;
}
public String toString() { return name; }
public static class None extends TransactionType {
public static final TransactionType type = new None();
private None() { super("None"); }
}
public static class ReadUncommitted extends TransactionType {
public static final TransactionType type =
new ReadUncommitted();
private ReadUncommitted() { super("ReadUncommitted"); }
}
public static class ReadCommitted extends TransactionType {
public static final TransactionType type =
new ReadCommitted();
private ReadCommitted() { super("ReadCommitted"); }
}
public static class RepeatableRead extends TransactionType {
public static final TransactionType type =
new RepeatableRead();
private RepeatableRead() { super("RepeatableRead"); }
}
public static class Serializable extends TransactionType {
public static final TransactionType type =
new Serializable();
private Serializable() { super("Serializable"); }
}
}
How does such a type help us to make safe types which we can
switch on? The answer is that we use a construct which is not
really meant to be used as a switch, but which acts as one
nevertheless, namely the throw-catch construct. We simply throw
the type, which can be a handle to a TransactionType object, and
the exception handling mechanism sorts out which catch to call.
Yes, I can hear you all groaning now with pearls of sweat caused
by fear, but this really does work. For syntactic sugar, we can
import the inner classes using "import TransactionType.*" after
which we can refer to the inner class simply by their name
"ReadCommitted". We can of course also use the full name such as
Transaction.None instead of importing the inner classes.
//: SwitchingOnObjects.java
import TransactionType.*;
public class SwitchingOnObjects {
public static void switchStatement(TransactionType transact) {
try {
throw transact;
} catch(TransactionType.None type) {
System.out.println("Case None received");
} catch(ReadUncommitted type) {
System.out.println("Case Read Uncommitted");
} catch(ReadCommitted type) {
System.out.println("Case Read Committed");
} catch(RepeatableRead type) {
System.out.println("Case Repeatable Read");
} catch(TransactionType type) {
System.out.println("Default");
}
}
public static void main(String[] args) {
switchStatement(TransactionType.None.type);
switchStatement(ReadUncommitted.type);
switchStatement(ReadCommitted.type);
switchStatement(RepeatableRead.type);
switchStatement(Serializable.type);
}
}
Try it out, the exception handling mechanism works quite well for
this. There are a few pointers you have to follow if you want to
use this:
- Don't ever catch "Throwable" as the default case. You should
rather catch the type base class, such as TransactionType.
Otherwise you run the risk of catching RuntimeException and
Error classes, such as OutOfMemoryError.
- Make sure that ALL the types are public inner classes of the
type base class.
- Make sure that all the constructors are private.
- Lastly, rather use polymorphism to achieve this effect.
Switch/Case code is really messy to maintain and very error-
prone.
I've already donned my asbethos suit for the criticisms from
hard-nosed C/C++ programmers who think switch/case is great and
from weeny Java purists who think that switch/case is completely
unacceptable. Flame away...
Until next week, and please remember to forward this newsletter
in its entirety to as many Java users as you know.
Heinz
Language Articles
Related Java Course
Discuss at The Java Specialist Club
|