«    »

Advanced Uses of Java 5 Enums

Enumerated types - called enums for short - were introduced to the Java language recently in version 5 (the version after 1.4). The name essentially defines what they are: a type that has a fixed set of constant values.

The Java Sun website has a good tutorial on using enums which covers their basic capabilities.

Java enums, however, do have several more advanced features not found in other languages. I have been using these capabilities in some of my recent Java development work and have found them quite useful.

Every enumerated type includes predefined operations - both static methods on the enumerated type itself and instance methods on each instance of the enum. The following example code defines a simple enum and calls these predefined methods.

enum Direction { NORTH, EAST, SOUTH, WEST }

public static void main(String[] args) {
  // Iterate through all direction values
  for (Direction direction : Direction.values()) {
    System.out.println(
        "Direction ordinal=" + direction.ordinal() 
        + " Identifier=" + direction.name());
  }

  // Given identifier, find corresponding direction instance.
  String directionIdentifier = "EAST";
  Direction direction = Direction.valueOf(directionIdentifier);
  System.out.println("\nDirection identifier '"
    + directionIdentifier + "' produces terrain "
    + direction.name());
}

The static values() method lets you iterate through the list of instances for the enumerated type, while the valueOf() method returns a specific instance corresponding to the identifier you provide. For each enum instance, the ordinal() method returns a zero-based integer corresponding to the order in which the instance was declared, and the name() method returns the identifier for the instance.

In addition to the predefined methods you can define your own methods and fields for an enum, just like you do for regular Java classes. In the following example I introduce a new enum called Terrain. I need to display terrain values to the user, so I add a property called displayName to hold the user-readable string. The constructor requires this value to be passed in, and each of the declarations for the enum instances provides this value.

public enum Terrain {
  NONE(""),
  WALL("Wall"),
  PIT("Pit"),
  FOG("Fog");

  private String displayName;

  private Terrain(String displayName) {
    this.displayName = displayName;
  }

  public String getDisplayName() {
    return displayName;
  }
}

Why not use the predefined name() method? This method returns the java identifier which is human-readable by a developer, but isn't appropriate for the general user. Enum identifiers are by convention all upper-case, while I want to display mixed case to the user. I also wanted the NONE terrain type to display an empty string.

Not only can we define methods on an enumerated type, but the implementation of the method can vary across instances. I now want to add some behavior to my Terrain enum: how does each piece of terrain affect movement and line of sight? I add two methods preventsMovement() and blocksLineOfSight() whose implementation will vary.

public enum Terrain {
  NONE(""),

  WALL("Wall") {
    @Override public boolean preventsMovement() {
      return true;
    }
    @Override public boolean blocksLineOfSight() {
      return true;
    }
  },

  PIT("Pit") {
    @Override public boolean preventsMovement() {
      return true;
    }
  },

  FOG("Fog") {
    @Override public boolean blocksLineOfSight() {
      return true;
    }
  };

  private String displayName;

  private Terrain(String displayName) {
    this.displayName = displayName;
  }
  public String getDisplayName() {
    return displayName;
  }
  public boolean preventsMovement() {
    return false;
  }
  public boolean blocksLineOfSight() {
    return false;
  }
}

In this example, I have default implementations of the preventsMovement() and blocksLineOfSight() methods defined for the enum. In cases where there is no reasonable default implementation, you can leave the enum method abstract and define it in each of the enum instances. In this situation, the enum is essentially equivalent to an abstract class, and each enum instance is essentially a subclass. Unlike regular java subclassing, however, one enum instance cannot subclass another instance - they all are direct children of the enum. One situation where it would be useful to be able to do this is if two enum instances had the same implementation for a method and you wanted to share the implementation. There is a way to accomplish this without resorting to static methods: the implementation of one of these methods can simply call the other method. To demonstrate this, I add GLASS_WALL as a new Terrain type, which has the same implementation for preventsMovement() as WALL.

  GLASS_WALL("Glass Wall") {
    @Override public boolean preventsMovement() {
      return WALL.preventsMovement();
    }
  },

For my final example I will show a realistic usage of an enum class that includes all the features discussed above. I have data on people that I want to display to the user in a Swing table with one row per person. Each individual is stored in an instance of a Person class.

// Copyright 2006 by Basil Vandegriend.  All rights reserved.
package com.basilv.examples.enums;
import java.util.Date;

public class Person
{
  private Integer id; // Will be null for new records.

  private String firstName;
  private String lastName;
  private Integer age;
  private Date birthDay;

  public Person(String firstName, String lastName,
    Integer age, Date birthDate) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.birthDay = birthDate;
  }

  public Integer getId() {
    return id;
  }
  public void setId(Integer id) {
    this.id = id;
  }

  public String getLastName() {
    return lastName;
  }
  public void setLastName(String s) {
    this.lastName = s;
  }

  public Date getBirthDay() {
    return birthDay;
  }
  public void setBirthDay(Date date) {
    this.birthDay = date;
  }

  public String getFirstName() {
    return firstName;
  }
  public void setFirstName(String s) {
    this.firstName = s;
  }

  public Integer getAge() {
    return age;
  }
  public void setAge(Integer integer) {
    this.age = integer;
  }
}

I will have a list of Person instances to display in a Swing JTable. The best way to accomplish this is to implement a custom Swing TableModel. For my implementation, I define a Column enum whose instances represent the columns of the table. The PersonTableModel implementation then just delegates to the Column enum as appropriate.

// Copyright 2006 by Basil Vandegriend.  All rights reserved.
package com.basilv.examples.enums;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.swing.table.AbstractTableModel;

public class PersonTableModel extends AbstractTableModel
{

  /**
   * Represents a column of the table.
   */
  static enum Column {
    FIRST_NAME("First Name") {
      @Override public Object getValue(Person person) {
        return person.getFirstName();
      }

      @Override public int getWidthInCharacters() {
        return 10;
      }
    },

    LAST_NAME("Last Name") {
      @Override public Object getValue(Person person) {
        return person.getLastName();
      }

      @Override public int getWidthInCharacters() {
        return 20;
      }
    },

    AGE("Age") {
      @Override public Object getValue(Person person) {
        return person.getAge();
      }

      @Override public Class getColumnClass() {
        return Integer.class;
      }

      @Override public int getWidthInCharacters() {
        return 5;
      }
    },

    BIRTHDAY("Birthday") {
      @Override public Object getValue(Person person) {
        return person.getBirthDay();
      }

      @Override public Class getColumnClass() {
        return Date.class;
      }

      @Override public int getWidthInCharacters() {
        return 12;
      }
    };

    
    private String displayName;

    private Column(String displayName) {
      assert displayName != null
        && displayName.length() > 0;
      this.displayName = displayName;
    }

    public String getDisplayName() {
      return displayName;
    }

    /**
     * Return the value for this column for the specified
     * person.
     */
    public abstract Object getValue(Person person);

    /**
     * Return the class of Object returned by this column.
     */
    public Class getColumnClass() {
      return String.class; // Default value
    }

    /**
     * Return the number of characters needed to display the
     * header and data for this column.
     */
    public abstract int getWidthInCharacters();
  }

  private List personList = new ArrayList();

  public void setPersonList(List list) {
    assert list != null;
    personList.clear();
    personList.addAll(list);
    fireTableDataChanged();
  }

  public int getColumnCount() {
    return Column.values().length;
  }

  public int getRowCount() {
    return personList.size();
  }

  public Object getValueAt(int rowIndex, int columnIndex) {
    Person person = getPerson(rowIndex);
    Column column = getColumn(columnIndex);
    return column.getValue(person);
  }

  @Override public String getColumnName(int columnIndex) {
    return getColumn(columnIndex).getDisplayName();
  }

  @Override public Class getColumnClass(int columnIndex) {
    Column column = getColumn(columnIndex);
    return column.getColumnClass();
  }

  Person getPerson(int rowIndex) {
    return personList.get(rowIndex);
  }

  private Column getColumn(int columnIndex) {
    return Column.values()[columnIndex];
  }
}

The following code creates some test data and displays the table.

// Copyright 2006 by Basil Vandegriend.  All rights reserved.
package com.basilv.examples.enums;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;


public class PersonTableModelUsage
{
  public static void main(String[] args) {
    
    List list = new ArrayList();
    list.add(new Person("Dave", "Davidson", 
      60, createDate(1946, 03, 13)));
    list.add(new Person("Bob", "Smith", 
      40, createDate(1966, 04, 30)));
    list.add(new Person("John", "Jones", 
      20, createDate(1986, 01, 15)));
    list.add(new Person("Jimmy", "Xi", 
      30, createDate(1976, 05, 22)));
    
    PersonTableModel tableModel = new PersonTableModel();
    tableModel.setPersonList(list);
    
    JTable table = new JTable(tableModel);

    // Adjust column widths
    char sampleChar = 'x';
    int widthOfTypicalCharacter = 
      table.getFontMetrics(table.getFont()).charWidth(sampleChar);
    
    TableColumnModel columnModel = table.getColumnModel();
    for (int i = 0; i < columnModel.getColumnCount(); i++) {
      TableColumn tableColumn = columnModel.getColumn(i);
      PersonTableModel.Column column = PersonTableModel.Column
        .values()[i];
      tableColumn.setPreferredWidth(widthOfTypicalCharacter
        * column.getWidthInCharacters());
    }

    JScrollPane scrollPane = new JScrollPane(table);
    scrollPane.setPreferredSize(new Dimension(300, 100));
    
    JOptionPane.showMessageDialog(null, scrollPane, 
      "Person Table", JOptionPane.INFORMATION_MESSAGE);
    
    System.exit(0);
  }
  
  private static Date createDate(
    int year, int month, int dayOfMonth) {
    Calendar calendar = Calendar.getInstance();
    calendar.set(year, month - 1, dayOfMonth, 0, 0);
    return calendar.getTime();
  } 
}

As I've shown, you can do a lot with Java enumerated types. There are situations, however, when they are not appropriate. The set of instances for an enum is statically defined at compile-time. If you need a dynamically configured set of values - either loaded at runtime from a configuration file or specified by the user - then an enum will not meet your requirements. If you need a sophisticated inheritance hierarchy, then an enum will be too limiting.

If you have a fixed set of related objects with common behavior, then an enum may be what you need. Give them a try - I think you'll be pleasantly surprised.

The source code listed in this article is provided in the Java Examples project which can be downloaded from the Software page.

If you find this article helpful, please make a donation.

One Comment on “Advanced Uses of Java 5 Enums”

  1. [...] breaking: Of course, a quick search reveals I’m not the first person to think of this. [...]

«    »