INTRODUCTION TO JOGL
http://java.sun.com/developer/JDCTechTips/2005/tt0208.html#1
As it's name implies, JOGL, the Java APIs for OpenGL, is a Java programming language binding for the OpenGL 3D graphics API. JOGL is designed to provide hardware-supported 3D graphics to applications written in Java technology. This tip will show you how to use JOGL to include basic two-dimensional and three-dimensional graphics in an application. JOGL is available under the Berkeley Software Distribution (BSD) license on java.net. Because JOGL is a light Java wrapper around the standard OpenGL API, the examples and text in this tip, for the most part, don't cover basic information on OpenGL.
To install JOGL, download the latest version from the jogl documents and files page. You'll find various release builds on the page. This tip was tested against the November 19, 2004 1.1b07 release build. You will need to download and install two sets of files. The article, Jumping into JOGL by Chris Adamson describes what files you need to download and where to put them on your machine. Note that the platform-specific files are in a jar file. You'll need to expand the jar file, and then put the platform-specific files in a directory where your runtime can find them.
After you install JOGL, you can use it to provide an application with hardware-accelerated 3D graphics. Let's start with an application that uses JOGL to display a red square that spins in the center of a black region. The example program, JOGLRotatingSquare
, is shown later in this tip. The Constructor for JOGLRotatingSquare
outlines the steps that are taken to set up the application:
JOGLRotatingSquare() {
GLCanvas canvas = getGLCanvas();
canvas.addGLEventListener(new RotatingSquareListener());
Animator anim = new Animator(canvas);
addCanvasToFrame(canvas);
anim.start();
}
The example begins by obtaining an object of type GLCanvas
through the getGLCanvas()
method. It then creates and attaches a GLEventListener
to the object. Then it creates an Animator
object that does the animation for the scene, adds the GLCanvas
object to the frame, and finally starts the animation.
The getGLCanvas()
method starts by configuring a GLCapabilities
object, an object that specifies the OpenGL capabilities that the rendering context must support. For example, here is how you would request that hardware acceleration be supported in the rendering context:
GLCapabilities capabilities = new GLCapabilities();
capabilities.setHardwareAccelerated(true);
Then the method passes the GLCapabilities
object into a factory to obtain a GLCanvas
. GLCanvas
implements the GLDrawable
interface, an interface that provides OpenGL rendering support. The GLCanvas
object is used to render the scene:
private GLCanvas getGLCanvas() {
GLCapabilities capabilities = new GLCapabilities();
return GLDrawableFactory.getFactory().
createGLCanvas(capabilities);
}
The GLEventListener
performs most of the work. In JOGLRotatingSquare
, the listener is implemented as an inner class. The listener must implement the methods init()
, display()
, displayChanged()
, and reshape()
.
The init()
method is called by the drawable immediately after the OpenGL
context is initialized for the first time. The init()
method is where you do your basic environment set up. For example, you can set up the erasing and drawing color. In the JOGLRotatingSquare
example, erasing and drawing color are both initially set to black:
public void init(GLDrawable drawable) {
GL gl = drawable.getGL();
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //erasing color
gl.glColor3f(0.0f, 0.0f, 0.0f); // drawing color
}
The displayChanged()
method is called with the display mode or display device changes. In the JOGLRotatingSquare
example, the displayChanged()
method body is empty:
public void displayChanged(GLDrawable drawable,
boolean modeChanged,
boolean deviceChanged) {
}
The reshape()
method is called during the first repaint after the component is resized. This includes the first time the component appears on the screen. In the JOGLRotatingSquare
example, the reshape()
method is used to set GLCanvas
to fit the viewable area and to recenter the square if the Frame
is resized:
public class VarGreeter3 {
public void reshape(GLDrawable drawable,
int x,
int y,
int width,
int height) {
GL gl = drawable.getGL();
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrtho(-width, width, -height, height, -1, 1);
}
The display()
method is called to initiate the OpenGL
rendering. It is also the method called by the Animator
object to update the display.
In the JOGLRotatingSquare
program, the display()
method clears the screen, redraws the square, and performs a rotation equal to the current value of angle. This rotates the square quickly in a counter clockwise direction.
public void display(GLDrawable drawable) {
GL gl = drawable.getGL();
gl.glClear(GL.GL_COLOR_BUFFER_BIT);
drawRedCenteredSquare(gl);
angle++;
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glRotatef(angle, 0, 0, 1);
}
The parameters to the glRotatef()
method are an angle and three other floats that indicate the axis on which the rotation takes place. In this example, the rotation is about the z-axis. This is equivalent to a counter-clockwise rotation in the x-y plane.
Here is the entire code listing for JOGLRotatingSquare
:
import net.java.games.jogl.GLCanvas;
import net.java.games.jogl.GLCapabilities;
import net.java.games.jogl.GLDrawableFactory;
import net.java.games.jogl.Animator;
import net.java.games.jogl.GL;
import net.java.games.jogl.GLEventListener;
import net.java.games.jogl.GLDrawable;
import java.awt.Frame;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class JOGLRotatingSquare {
private static float angle = 0;
private static final int SIZE = 160;
JOGLRotatingSquare() {
GLCanvas canvas = getGLCanvas();
canvas.addGLEventListener(new RotatingSquareListener());
Animator anim = new Animator(canvas);
addCanvasToFrame(canvas, anim);
anim.start();
}
private void addCanvasToFrame(
GLCanvas canvas, final Animator anim) {
Frame f = new Frame("JOGL Rotating Square");
f.setSize(600, 400);
f.add(canvas);
f.setVisible(true);
f.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
anim.stop();
System.exit(0);
}
});
}
private GLCanvas getGLCanvas() {
GLCapabilities capabilities = new GLCapabilities();
return GLDrawableFactory.getFactory().
createGLCanvas(capabilities);
}
public static void main(String[] args) {
new JOGLRotatingSquare();
}
private void drawRedCenteredSquare(GL gl) {
gl.glColor3f(1, 0, 0);
gl.glRecti(-SIZE / 2, -SIZE / 2, SIZE / 2, SIZE / 2);
gl.glColor3f(0.0f, 0.0f, 0.0f);
}
class RotatingSquareListener implements GLEventListener {
public void init(GLDrawable drawable) {
GL gl = drawable.getGL();
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //erasing color
gl.glColor3f(0.0f, 0.0f, 0.0f); // drawing color
}
public void display(GLDrawable drawable) {
GL gl = drawable.getGL();
gl.glClear(GL.GL_COLOR_BUFFER_BIT);
drawRedCenteredSquare(gl);
angle++;
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glRotatef(angle, 0, 0, 1);
}
public void reshape(GLDrawable drawable,
int x,
int y,
int width,
int height) {
GL gl = drawable.getGL();
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrtho(-width, width, -height, height, -1, 1);
}
public void displayChanged(GLDrawable drawable,
boolean modeChanged,
boolean deviceChanged) {
}
}
}
Compile and run JOGLRotatingSquare
. You should see a red square that spins in the center of a black region.
If you increase the size of the Frame, the square will recenter and the animation will slow. You can also slow the animation further by replacing the last line of the display() method with the line
gl.glRotatef(angle/16, 0, 0, 1);
You can experiment with values other than 16.
The method drawRedCenteredSquare() uses one of the family of specific methods for drawing a rectangle. It first specifies that the current color is red. It then draws a rectangle centered at the origin with sides of length equal to SIZE. Finally, it resets the current color to black:
private void drawRedCenteredSquare(GL gl) {
gl.glColor3f(1, 0, 0);
gl.glRecti(-SIZE / 2, -SIZE / 2, SIZE / 2, SIZE / 2);
gl.glColor3f(0.0f, 0.0f, 0.0f);
}
This works because the square is drawn in the x-y plane. In other words, the method ignores the third dimension. Another way to draw the rectangle is as follows:
private static void drawRedCenteredSquare(GL gl) {
gl.glColor3f(1, 0, 0);
gl.glBegin(GL.GL_QUADS);
gl.glTexCoord2f(0, 0);
gl.glVertex3f(-SIZE / 2, SIZE / 2, 0);
gl.glTexCoord2f(0, 1);
gl.glVertex3f(-SIZE / 2, -SIZE / 2, 0);
gl.glTexCoord2f(1, 1);
gl.glVertex3f(SIZE / 2, -SIZE / 2, 0);
gl.glTexCoord2f(1, 0);
gl.glVertex3f(SIZE / 2, SIZE / 2, 0);
gl.glEnd();
gl.glColor3f(0.0f, 0.0f, 0.0f);
}
Here the four vertices of a square are explicitly set. This approach seems a bit more verbose than the previous approach, but it will be useful in the next example (which moves from two to three dimensions). There is not much more work to be done in three dimensions than there was in two. First, you need to change the last line of the reshape() method so that the visible range in the z direction is increased to include the entire cube. If you omit this step, you will only see the horizontal slice of the cube that is within one unit of the x-y plane. Here is the revised line:
gl.glOrtho(-width, width, -height, height, -SIZE, SIZE);
Next, change the end of the display() method to tilt the cube so that the corner is facing forward. Then rotate the cube on a diagonal. Here is the new end of the display() method:
gl.glRotatef(-80, 1, 1, 0);
gl.glRotatef(angle /16, 1, -1, 1);
drawCenteredCube(gl);
Last, you will need to revise the method that draws the square as described above. The new method draws a square that would be the nearest face of the cube:
private void drawSquareFace(GL gl) {
gl.glBegin(GL.GL_QUADS);
gl.glTexCoord2f(0, 0);
gl.glVertex3f(-SIZE / 2, -SIZE / 2, SIZE / 2);
gl.glTexCoord2f(0, 1);
gl.glVertex3f(-SIZE / 2, SIZE / 2, SIZE / 2);
gl.glTexCoord2f(1, 1);
gl.glVertex3f(SIZE / 2, SIZE / 2, SIZE / 2);
gl.glTexCoord2f(1, 0);
gl.glVertex3f(SIZE / 2, -SIZE / 2, SIZE / 2);
gl.glEnd();
}
The addition to the three-dimensional version is the following method, drawCenteredCube(). It actually only draws three faces of the cube because that is all that a viewer will see in this example.
private void drawCenteredCube(GL gl) {
gl.glColor4f(1, 0, 0, 0);
drawSquareFace(gl);
gl.glColor4f(1, 1, 0, 0);
gl.glRotatef(90, 1, 0, 0);
drawSquareFace(gl);
gl.glColor4f(0, 0, 1, 0);
gl.glRotatef(90, 0, 1, 0);
drawSquareFace(gl);
gl.glColor3f(0.0f, 0.0f, 0.0f);
}
First the color is set to red and a face is drawn. Then the model is rotated 90 degrees. The color is changed to yellow, and a second face is drawn. The model is then rotated 90 degrees in another direction so that the three faces meet at a single vertex. The color is changed to blue and the third face is drawn.
Here is the entire code listing:
import net.java.games.jogl.GLCanvas;
import net.java.games.jogl.GLCapabilities;
import net.java.games.jogl.GLDrawableFactory;
import net.java.games.jogl.Animator;
import net.java.games.jogl.GL;
import net.java.games.jogl.GLEventListener;
import net.java.games.jogl.GLDrawable;
import java.awt.Frame;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class JOGLRotatingCube {
private static final int SIZE = 160;
private static float angle = 0;
JOGLRotatingCube() {
GLCanvas canvas = getGLCanvas();
canvas.addGLEventListener(new RotatingCubeListener());
Animator anim = new Animator(canvas);
addCanvasToFrame(canvas, anim);
anim.start();
}
private void addCanvasToFrame(
GLCanvas canvas, final Animator anim) {
Frame f = new Frame("JOGL Rotating Half - Cube");
f.setSize(600, 400);
f.add(canvas);
f.setVisible(true);
f.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
anim.stop();
System.exit(0);
}
});
}
private GLCanvas getGLCanvas() {
GLCapabilities capabilities = new GLCapabilities();
return GLDrawableFactory.getFactory().
createGLCanvas(capabilities);
}
public static void main(String[] args) {
new JOGLRotatingCube();
}
private void drawCenteredCube(GL gl) {
gl.glColor4f(1, 0, 0, 0);
drawSquareFace(gl);
gl.glColor4f(1, 1, 0, 0);
gl.glRotatef(90, 1, 0, 0);
drawSquareFace(gl);
gl.glColor4f(0, 0, 1, 0);
gl.glRotatef(90, 0, 1, 0);
drawSquareFace(gl);
gl.glColor3f(0.0f, 0.0f, 0.0f);
}
private void drawSquareFace(GL gl) {
gl.glBegin(GL.GL_QUADS);
gl.glTexCoord2f(0, 0);
gl.glVertex3f(-SIZE / 2, -SIZE / 2, SIZE / 2);
gl.glTexCoord2f(0, 1);
gl.glVertex3f(-SIZE / 2, SIZE / 2, SIZE / 2);
gl.glTexCoord2f(1, 1);
gl.glVertex3f(SIZE / 2, SIZE / 2, SIZE / 2);
gl.glTexCoord2f(1, 0);
gl.glVertex3f(SIZE / 2, -SIZE / 2, SIZE / 2);
gl.glEnd();
}
class RotatingCubeListener implements GLEventListener {
public void init(GLDrawable drawable) {
GL gl = drawable.getGL();
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //erasing color
gl.glColor3f(0.0f, 0.0f, 0.0f); // drawing color
}
public void display(GLDrawable drawable) {
GL gl = drawable.getGL();
gl.glClear(GL.GL_COLOR_BUFFER_BIT);
angle++;
gl.glMatrixMode(GL.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glRotatef(-80, 1, 1, 0);
gl.glRotatef(angle /16, 1, -1, 1);
drawCenteredCube(gl);
}
public void reshape(GLDrawable drawable,
int x,
int y,
int width,
int height) {
GL gl = drawable.getGL();
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL.GL_PROJECTION);
gl.glLoadIdentity();
gl.glOrtho(-width, width, -height, height, -SIZE, SIZE);
}
public void displayChanged(GLDrawable drawable,
boolean modeChanged,
boolean deviceChanged) {
}
}
}
Compile and run JOGLRotatingCube. You should see a rotating multicolored cube.
In these examples you have seen how to quickly get up and running with JOGL. In the first example, you added a rectangle to a GLCanvas
and animated it. In the second example, you extended this to creating three faces of a cube and rotated them about the common vertex. You can experiment with extending these examples by adding the other faces to the cube or by adding different shapes to the two dimensional example.
CHANGING BEHAVIOR BASED ON THE VALUE OF AN ENUMERATED TYPE
When you first use enumerated types, you might simply see them as an alternative to using constants. However they have other interesting uses. For example, you can change the behavior of a class based on the value of an enumerated type. This tip will show you how to implement enumeration constant-specific behavior in an elegant way.
Begin by creating a simple enum type that represents the four basic types of U.S. coins:
public enum Coin1 {
PENNY,
NICKEL,
DIME,
QUARTER;
}
You can now switch on the value of an enum. For example, you can create a method getValue()
that accepts an object of type Coin1
:
static int getValue(Coin1 coin) {
switch( coin ) {
case PENNY: //do something
// .. remaining cases
}
}
You can also iterate through the different values in Coin1
using the enhanced for
:
for (Coin1 coin: Coin1.values())
Let's put those two techniques together into a client for Coin1
:
public class Dispenser1 {
static int getValue(Coin1 coin){
switch(coin) {
case PENNY:return 1;
case NICKEL:return 5;
case DIME: return 10;
case QUARTER: return 25;
default: return 0;
}
}
public static void main(String[] args) {
for (Coin1 coin: Coin1.values()) {
System.out.println("A " + coin + " has value = "
+ getValue(coin) + ".");
}
}
}
In this example, the default method returns a value of 0. In production code, you might prefer to have the default case throw an exception.
The value of coin.toString()
is the name of the enum instance, so the output will look like this:
A PENNY has value = 1.
A NICKEL has value = 5.
A DIME has value = 10.
A QUARTER has value = 25.
In a straightforward example like the previous one, the switch
statement can easily be moved from the client class to the enumeration class, so that the switch
information is part of the Coin enum's functionality. Later, you will see an example where you need the client to switch
on the enumeration type and decide what to do itself. In Coin1
, each type has a single behavior, so let's move the switch
statement to the enum. This is shown in the enum Coin2
, where the switch
statement is in the value()
method of the enum class.
Newcomers to enumerated types think of them as representing constants, and forget that you can add methods to the definition. Remember that enums have the flexibility of classes and so you have many choices regarding how or when to implement different functionality. The value()
method belongs to the type and is available to each instance.
public enum Coin2 {
PENNY,
NICKEL,
DIME,
QUARTER;
int value(){
switch(this) {
case PENNY:return 1;
case NICKEL:return 5;
case DIME: return 10;
case QUARTER: return 25;
default: return 0;
}
}
}
Now the client code can be simplified:
public class Dispenser2 {
public static void main(String[] args) {
for (Coin2 coin: Coin2.values()) {
System.out.println("A " + coin + " has value = "
+ coin.value() + ".");
}
}
}
You can eliminate the switch
statement entirely by specifying different behavior within constant-specific methods. Make value()
abstract so that it can be called by client code. Defining the method in the enum itself guarantees that it can be called on any constant. Declaring the method abstract ensures each constant will override the implementation.
public enum Coin3 {
PENNY { int value(){ return 1;}},
NICKEL { int value(){ return 5;}},
DIME { int value() { return 10;}},
QUARTER { int value() {return 25;}};
abstract int value();
}
You can call Coin3
by changing the appropriate line of Dispenser2
to:
for (Coin3 coin: Coin3.values())
Enumerated types can also contain constructors. You can supply Coin3
with a field named coinValue
that holds an int
and is set by a constructor:
private Coin3( int value) {
coinValue = value;
}
The private
keyword is superfluous here because enum constructors are implicitly private. It is included here as a visual reminder that the constructor cannot be explicitly called (it is dropped in the code listing below).
Now the value()
method just returns coinValue
. The other change is that each class must set coinValue
. You do this by changing the list from:
PENNY, NICKEL, DIME, QUARTER;
to:
PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
Here is the modified version of Coin3
:
public enum Coin3 {
PENNY(1),
NICKEL(5),
DIME(10),
QUARTER(25);
private int coinValue;
int value() {return coinValue;}
Coin3(int value){
coinValue = value;
}
}
Let's return to the case where you might want different behavior in the client depending on the enumeration type. For example, if you had an Employee
enum, you might need to vary behavior in different parts of a human resources application depending on whether the Employee
type was SALARY
, HOURLY
, or HOURLY_PART_TIME
. This difference might be applicable in the calculation of days off, bonuses, benefits, and pay. You don't want to implement a switch
statement in each client that calls different methods based on the type that is called. Ideally, you would like the correct method to be called automatically based on the type.
One way to accomplish this is by implementing the Visitor pattern. A classic reference is Design Patterns, Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides. There are variants on the Visitor pattern -- only one version is discussed in this tip. The point of using it here is that if you use the Visitor pattern with enums, any enum constant-specific function (and any number of these functions) can be implemented by using a visitor.
If you have never seen the Visitor pattern before, you might find it a bit confusing. An instance of the enumerated type is created, and a method is called -- in this case, accept()
. The class containing the behavior is passed in as an argument. The enum instance then calls the appropriate method in the class containing the behavior. In this way, different behavior can be injected into a client class.
The first component is the enumerated type. This time accept()
is an abstract method that is implemented in each specialized enum constant. For example, in PENNY
, accept()
is implemented as follows:
void accept( CoinVisitor cv) {cv.visitPenny(this);
In other words, this instance of accept
calls the visitPenny()
method in the CoinVisitor
instance that was passed in as a parameter. Here is the revised enum class:
public enum Coin4 {
PENNY {
void accept( CoinVisitor cv) {cv.visitPenny(this);}
},
NICKEL {
void accept( CoinVisitor cv) {cv.visitNickel(this);}
},
DIME {
void accept( CoinVisitor cv) {cv.visitDime(this);}
},
QUARTER {
void accept( CoinVisitor cv) {cv.visitQuarter(this);}
};
abstract void accept(CoinVisitor cv);
}
In this case, there is only a single implementation of CoinVisitor
so you could create a concrete class. But the point of this example is to show how you can set up the infrastructure to vary the behavior in a client class. So you will need the following abstract class:
public abstract class CoinVisitor {
void visitPenny(Coin4 c){}
void visitNickel(Coin4 c){}
void visitDime(Coin4 c){}
void visitQuarter(Coin4 c){}
}
Now any time you want to specify a particular behavior that varies for each of the Coin4
types, you extend CoinVisitor
. For example, CoinValueVisitor
prints out the name and value of the appropriate coin.
public class CoinValueVisitor extends CoinVisitor {
void visitPenny(Coin4 c){
System.out.println("A penny has value = 1.");
}
void visitNickel(Coin4 c){
System.out.println("A nickel has value = 5.");
}
void visitDime(Coin4 c) {
System.out.println("A dime has value = 10.");
}
void visitQuarter(Coin4 c) {
System.out.println("A quarter has value = 25.");
}
}
The client code still iterates through the values of the enum, but this time, if coin
is an instance of Coin4
and cvv
is an instance of the CoinValueVisitor
, you initiate the correct behavior with the following call:
coin.accept(cvv);
If coin
is an instance of PENNY
, then this call has the effect of calling the accept()
method in PENNY
, and passing in cvv
. Then the visitPenny()
method is called on cvv
. This results in the printing of "A penny has value = 1." to standard out. Here is the revised class.
public class Dispenser4 {
public static void main(String[] args) {
CoinValueVisitor cvv = new CoinValueVisitor();
for (Coin4 coin: Coin4.values()) {
coin.accept(cvv);
}
}
}
The result is the following output:
A penny has value = 1.
A nickel has value = 5.
A dime has value = 10.
A quarter has value = 25.
For more examples of changing behavior based on the value of an enumerated type, see the followings:
http://java.sun.com/j2se/1.5.0/docs/guide/language/enums.html
In prior releases, the standard way to represent an enumerated type was the int Enum pattern:
// int Enum Pattern - has severe problems!
public static final int SEASON_WINTER = 0;
public static final int SEASON_SPRING = 1;
public static final int SEASON_SUMMER = 2;
public static final int SEASON_FALL = 3;
This pattern has many problems, such as:
-
Not typesafe - Since a season is just an
int
you can pass in any other int value where a season is required, or add two seasons together (which makes no sense).
-
No namespace - You must prefix constants of an int enum with a string (in this case
SEASON_
) to avoid collisions with other int enum types.
-
Brittleness - Because int enums are compile-time constants, they are compiled into clients that use them. If a new constant is added between two existing constants or the order is changed, clients must be recompiled. If they are not, they will still run, but their behavior will be undefined.
-
Printed values are uninformative - Because they are just ints, if you print one out all you get is a number, which tells you nothing about what it represents, or even what type it is.
It is possible to get around these problems by using the Typesafe Enum pattern (see Effective Java Item 21), but this pattern has its own problems: It is quite verbose, hence error prone, and its enum constants cannot be used in switch
statements.
In 5.0, the Java™ programming language gets linguistic support for enumerated types. In their simplest form, these enums look just like their C, C++, and C# counterparts:
enum Season { WINTER, SPRING, SUMMER, FALL }
But appearances can be deceiving. Java programming language enums are far more powerful than their counterparts in other languages, which are little more than glorified integers. The new enum
declaration defines a full-fledged class (dubbed an enum type). In addition to solving all the problems mentioned above, it allows you to add arbitrary methods and fields to an enum type, to implement arbitrary interfaces, and more. Enum types provide high-quality implementations of all the Object methods. They are Comparable and Serializable, and the serial form is designed to withstand arbitrary changes in the enum type.
Here is an example of a playing card class built atop a couple of simple enum types. The Card
class is immutable, and only one instance of each Card
is created, so it need not override equals
or hashCode
:
import java.util.*;
public class Card {
public enum Rank { DEUCE, THREE, FOUR, FIVE, SIX,
SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING, ACE }
public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
private final Rank rank;
private final Suit suit;
private Card(Rank rank, Suit suit) {
this.rank = rank;
this.suit = suit;
}
public Rank rank() { return rank; }
public Suit suit() { return suit; }
public String toString() { return rank + " of " + suit; }
private static final List<Card> protoDeck = new ArrayList<Card>();
// Initialize prototype deck
static {
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
protoDeck.add(new Card(rank, suit));
}
public static ArrayList<Card> newDeck() {
return new ArrayList<Card>(protoDeck); // Return copy of prototype deck
}
}
The toString
method for Card
takes advantage of the toString
methods for Rank
and Suit
. Note that the Card
class is short (about 25 lines of code). If the typesafe enums (Rank
and Suit
) had been built by hand, each of them would have been significantly longer than the entire Card
class.
The (private) constructor of Card
takes two parameters, a Rank
and a Suit
. If you accidentally invoke the constructor with the parameters reversed, the compiler will politely inform you of your error. Contrast this to the int
enum pattern, in which the program would fail at run time.
Note that each enum type has a static values
method that returns an array containing all of the values of the enum type in the order they are declared. This method is commonly used in combination with the for-each loop to iterate over the values of an enumerated type.
The following example is a simple program called Deal
that exercises Card
. It reads two numbers from the command line, representing the number of hands to deal and the number of cards per hand. Then it creates a new deck of cards, shuffles it, and deals and prints the requested hands.
import java.util.*;
public class Deal {
public static void main(String args[]) {
int numHands = Integer.parseInt(args[0]);
int cardsPerHand = Integer.parseInt(args[1]);
List<Card> deck = Card.newDeck();
Collections.shuffle(deck);
for (int i=0; i < numHands; i++)
System.out.println(deal(deck, cardsPerHand));
}
public static ArrayList<Card> deal(List<Card> deck, int n) {
int deckSize = deck.size();
List<Card> handView = deck.subList(deckSize-n, deckSize);
ArrayList<Card> hand = new ArrayList<Card>(handView);
handView.clear();
return hand;
}
}
$ java Deal 4 5
[FOUR of HEARTS, NINE of DIAMONDS, QUEEN of SPADES, ACE of SPADES, NINE of SPADES]
[DEUCE of HEARTS, EIGHT of SPADES, JACK of DIAMONDS, TEN of CLUBS, SEVEN of SPADES]
[FIVE of HEARTS, FOUR of DIAMONDS, SIX of DIAMONDS, NINE of CLUBS, JACK of CLUBS]
[SEVEN of HEARTS, SIX of CLUBS, DEUCE of DIAMONDS, THREE of SPADES, EIGHT of CLUBS]
Suppose you want to add data and behavior to an enum. For example consider the planets of the solar system. Each planet knows its mass and radius, and can calculate its surface gravity and the weight of an object on the planet. Here is how it looks:
public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7),
PLUTO (1.27e+22, 1.137e6);
private final double mass; // in kilograms
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
public double mass() { return mass; }
public double radius() { return radius; }
// universal gravitational constant (m3 kg-1 s-2)
public static final double G = 6.67300E-11;
public double surfaceGravity() {
return G * mass / (radius * radius);
}
public double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
}
The enum type Planet
contains a constructor, and each enum constant is declared with parameters to be passed to the constructor when it is created.
Here is a sample program that takes your weight on earth (in any unit) and calculates and prints your weight on all of the planets (in the same unit):
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight/EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Your weight on %s is %f%n",
p, p.surfaceWeight(mass));
}
$ java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413
Your weight on PLUTO is 11.703031
The idea of adding behavior to enum constants can be taken one step further. You can give each enum constant a different behavior for some method. One way to do this by switching on the enumeration constant. Here is an example with an enum whose constants represent the four basic arithmetic operations, and whose eval
method performs the operation:
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// Do arithmetic op represented by this constant
double eval(double x, double y){
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}
This works fine, but it will not compile without the throw statement, which is not terribly pretty. Worse, you must remember to add a new case to the switch statement each time you add a new constant to Operation. If you forget, the eval method with fail, executing the aforementioned throw statement
There is another way give each enum constant a different behavior for some method that avoids these problems. You can declare the method abstract in the enum type and override it with a concrete method in each constant. Such methods are known as constant-specific methods. Here is the previous example redone using this technique:
public enum Operation {
PLUS { double eval(double x, double y) { return x + y; } },
MINUS { double eval(double x, double y) { return x - y; } },
TIMES { double eval(double x, double y) { return x * y; } },
DIVIDE { double eval(double x, double y) { return x / y; } };
// Do arithmetic op represented by this constant
abstract double eval(double x, double y);
}
Here is a sample program that exercises the Operation
class. It takes two operands from the command line, iterates over all the operations, and for each operation, performs the operation and prints the resulting equation:
public static void main(String args[]) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.eval(x, y));
}
$ java Operation 4 2
4.000000 PLUS 2.000000 = 6.000000
4.000000 MINUS 2.000000 = 2.000000
4.000000 TIMES 2.000000 = 8.000000
4.000000 DIVIDE 2.000000 = 2.000000
Constant-specific methods are reasonably sophisticated, and many programmers will never need to use them, but it is nice to know that they are there if you need them.
Two classes have been added to java.util
in support of enums: special-purpose Set
and Map
implementations called EnumSet
and EnumMap
. EnumSet
is a high-performance Set
implementation for enums. All of the members of an enum set must be of the same enum type. Internally, it is represented by a bit-vector, typically a single long
. Enum sets support iteration over ranges of enum types. For example given the following enum declaration:
enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }
you can iterate over the weekdays. The EnumSet
class provides a static factory that makes it easy:
for (Day d : EnumSet.range(Day.MONDAY, Day.FRIDAY))
System.out.println(d);
Enum sets also provide a rich, typesafe replacement for traditional bit flags:
EnumSet.of(Style.BOLD, Style.ITALIC)
Similarly, EnumMap
is a high-performance Map
implementation for use with enum keys, internally implemented as an array. Enum maps combine the richness and safety of the Map
interface with speed approaching that of an array. If you want to map an enum to a value, you should always use an EnumMap in preference to an array.
The Card
class, above, contains a static factory that returns a deck, but there is no way to get an individual card from its rank and suit. Merely exposing the constructor would destroy the singleton property (that only a single instance of each card is allowed to exist). Here is how to write a static factory that preserves the singleton property, using a nested EnumMap:
private static Map<Suit, Map<Rank, Card>> table =
new EnumMap<Suit, Map<Rank, Card>>(Suit.class);
static {
for (Suit suit : Suit.values()) {
Map<Rank, Card> suitTable = new EnumMap<Rank, Card>(Rank.class);
for (Rank rank : Rank.values())
suitTable.put(rank, new Card(rank, suit));
table.put(suit, suitTable);
}
}
public static Card valueOf(Rank rank, Suit suit) {
return table.get(suit).get(rank);
}
The EnumMap
(table
) maps each suit to an EnumMap
that maps each rank to a card. The lookup performed by the valueOf
method is internally implemented as two array accesses, but the code is much clearer and safer. In order to preserve the singleton property, it is imperative that the constructor invocation in the prototype deck initialization in Card
be replaced by a call to the new static factory:
// Initialize prototype deck
static {
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
protoDeck.add(Card.valueOf(rank, suit));
}
It is also imperative that the initialization of table
be placed above the initialization of protoDeck, as the latter depends on the former.
So when should you use enums? Any time you need a fixed set of constants. That includes natural enumerated types (like the planets, days of the week, and suits in a card deck) as well as other sets where you know all possible values at compile time, such as choices on a menu, rounding modes, command line flags, and the like. It is not necessary that the set of constants in an enum type stay fixed for all time. The feature was specifically designed to allow for binary compatible evolution of enum types.