Experiences with the New Java 5 Language Features

Pages: 1, 2, 3, 4

Enumerations

Enums are a lot like public static final int declarations, which have been used for many years as enum values. The biggest and most obvious improvement over ints is type safety—you cannot mistakenly use one type of enum in place of another, unlike ints, which all look the same to the compiler. With very few exceptions, you should replace all enum-like int constructs with enum instances.

Enums offer a few additional features. Two utility classes, EnumMap and EnumSet, are implementations of standard collections optimized specifically for enums. If you know your collection will contain only enums, you should use these specific collections instead of HashMap or HashSet.

For the most part you can do a drop-in replacement of any public static final ints in your code with enums. They're Comparable, and can be statically imported so that references to them look identical, even in the case of an inner class (or inner enum). Note that when comparing enums, the order in which they are declared indicates their ordinal value.

"Hidden" static methods

Two static methods appear on all enum declarations you write. They don't appear in the javadoc for java.lang.Enum, as they are static methods on enum subclasses, not on Enum itself.

The first, values(), returns an array of all the possible values for an enum.

The second, valueOf(), returns an enum for the provided string, which must match the source code declaration exactly.

Methods

One of our favorite aspects of enums is they can have methods. In the past you may have had some code that performed a switch on a public static final int to translate from a database type into a JDBC URL. Now, you can have a method directly on the enum itself that can clean up code dramatically. Here's an example of how this is done, with an abstract method on the DatabaseType enum, and implementations provided in each enum instance:

public enum DatabaseType {
    ORACLE {
        public String getJdbcUrl() {...}
    },
    MYSQL {
        public String getJdbcUrl() {...}
    };
    public abstract String getJdbcUrl();
}

Now your enum can provide its utility method directly. For instance:

DatabaseType dbType = ...;
String jdbcURL = dbType.getJdbcUrl();

Previously you would have had to have known where the utility method was for getting the URL.

Varargs

When used correctly varargs can really clean up some ugly code. The canonical example is a log method that takes a variable number of String arguments:

Log.log(String code)
Log.log(String code, String arg)
Log.log(String code, String arg1, String arg2)
Log.log(String code, String[] args)

The interesting item to discuss about varargs is the compatibility you get if you replace the first four examples with a new, vararged one:

Log.log(String code, String... args)

All varargs are source compatible—that is, if you recompile all callers of the log() method, you can just replace all four methods directly. If, however, you need backward binary compatibility, you'll need to leave in the first three. Only the final method, taking an array of Strings, is equivalent to, and therefore can be replaced by, the vararged version.

Casting

You should avoid casting with varargs in cases where you simply expect the caller to know what the types should be. Take this example, where the first item is expected to be a String, and the second an Exception:

Log.log(Object... objects) {
    String message = (String)objects[0];
    if (objects.length > 1) {
        Exception e = (Exception)objects[1];
        // Do something with the exception
    }
}

Instead, your method signature should be like the following, with the String and Exception declared separately from the vararg parameter:

Log.log(String message, Exception e, Object... objects) {...}

Don't try to be too clever. Don't use varargs to subvert the type system. If you need strong typing, use it. PrintStream.printf() is one interesting exception to this rule: It provides type information as its first argument so that it can accept those types later.

Covariant Returns

The primary use of covariant returns is to avoid casts when an implementation's return type is known to be more specific than the APIs. In this example, we have a Zoo interface that returns an Animal object. Our implementation returns an AnimalImpl object, but before JDK 1.5 it had to be declared to return an Animal object:

public interface Zoo {
    public Animal getAnimal();
}

public class ZooImpl implements Zoo {
    public Animal getAnimal(){
        return new AnimalImpl();
    }
}

The use of covariant returns replaces three anti-patterns:

  1. Direct field access. To get around the API restriction, some implementations would expose the subclass directly as a field:
    ZooImpl._animal
    
  2. An additional form was to perform the downcast in the caller, knowing that the implementation was really this specific subclass:
    ((AnimalImpl)ZooImpl.getAnimal()).implMethod();
    
  3. The last form I've seen is a special method that avoids the problem by coming up with a different signature entirely:
    ZooImpl._getAnimal();
    

All of these have their problems and limitations. Either they're ugly or expose implementation details that should not be necessary.

With covariance

The covariant return pattern is cleaner, safer, and easier to maintain. No casts or special methods or fields are required:

public AnimalImpl getAnimal(){
    return new AnimalImpl();
}

Using the result:

ZooImpl.getAnimal().implMethod();

Using Generics

We'll look at generics from two angles: using generics and constructing generics. We're not going to talk about the obvious use of List, Set, and Map. Suffice it to say that generic collections are great and should always be used.

We are going to cover using generic methods and how the compiler infers the types. Usually this will just work for you, but when it doesn't the error messages are fairly inscrutable and you will need to know how to fix the problem.

Pages: 1, 2, 3, 4

Next Page»