Published on dev2dev (http://dev2dev.bea.com/)
http://dev2dev.bea.com/pub/a/2005/09/java_5_features.html
See this if you're having trouble printing code examples
Java 5.0 is here, and many of you will be starting to use some of the new features added to this release of the JDK. Everything from the enhanced for loop to more complex features such as generics will soon start appearing in code that you write. We just completed a large Java 5.0-based assignment, and this article looks at our experiences with many of these features. It's not an introduction but a somewhat deeper examination of the features and how they'll affect you, along with a few tips on how to effectively use these features in your own projects.
During the beta period for JDK 1.5, we worked on a Java 5 compiler for BEA's Java IDE. As we implemented various new features, people began exploiting them in new ways; some were clever and some were clearly candidates for a list of what not to do. The compiler itself used the new language features, so we gained direct experience in maintaining code using these features as well. This article is a look at many of these features and our experiences with them.
We expect that you are already familiar with the new features, so we don't provide a comprehensive introduction to each. Instead, we'll talk about some of the interesting, hopefully non-obvious implications and uses. These tips are a somewhat random collection of things we ran into, loosely grouped by language feature.
We'll start with the simplest features and work our way toward the most advanced ones. Generics is an especially rich subject and therefore occupies about half the article.
The new enhanced for loop provides a simple, consistent syntax for iterating over collections and arrays. A couple items are worth mentioning:
The initialization expression is evaluated only once inside the loop. This means that you can often remove a variable declaration. In this example, we had to create an Integer array to hold the results of computeNumbers() to prevent reevaluation of that method on each pass through the loop. You can see the bottom code is a little cleaner than the above and doesn't leak the variable numbers:
Without Enhanced For:
int sum = 0;
Integer[] numbers = computeNumbers();
for (int i=0; i < numbers.length ; i++)
sum += numbers[i];
With:
int sum = 0;
for ( int number: computeNumbers() )
sum += number;
Sometimes you need access to the iterator or index during iteration. Intuitively it seems like the enhanced for loop should allow this. It doesn't. Take the following example:
for (int i=0; i < numbers.length ; i++) {
if (i != 0) System.out.print(",");
System.out.print(numbers[i]);
}
We want to print out a comma-separated list of the values in the array. We need to know whether we're on the first item in order to know if we should print a comma. With enhanced for, there's no way to get at this information. We'd need to keep an index ourselves, or a boolean indicating whether we're past the first item.
Here's another example:
for (Iterator<integer> it = n.iterator() ; it.hasNext() ; )
if (it.next() < 0)
it.remove();
In this case, we want to remove the negative items from a collection of Integers. To do this, we need to call a method on the iterator, but when using the enhanced for loop, this iterator would be hidden from us. Therefore, we need to just use the pre-Java 5 method of iterating.
Note, by the way, that Iterator is generic, so the declaration is Iterator<Integer>. Many people seem to miss this and use Iterator in its raw form.
Annotation processing is a large topic. We're not going to cover all the possibilities and pitfalls of it, as we're limiting our article to core language features.
We will, however, discuss the built-in annotations (SuppressWarnings, Deprecated, and Override) and limitations of annotation processing in general.
This annotation turns off compiler warnings at a class or method level. Sometimes you know better than the compiler that your code must use a deprecated method, or perform some action that cannot be statically determined to be typesafe but in fact is:
@SuppressWarnings("deprecation")
public static void selfDestruct() {
Thread.currentThread().stop();
}
This is probably the most useful of the built-in annotations. Unfortunately, javac doesn't support it as of 1.5.0_04. It is supported in 1.6, however, and Sun is working on back-porting it to 1.5.
The annotation is supported in Eclipse 3.1 and possibly other IDEs as well. This allows you to keep your code entirely warning-free. If a warning shows up when compiling, you can be certain that you just added it—helping to keep you aware of possibly unsafe code. With the addition of generics, this is even more desirable.
Unfortunately, Deprecated is a little less useful. It's meant to replace the @deprecated javadoc tag, but since it doesn't have any fields, there's no way to suggest to the user of a deprecated class or method what they should be using as a replacement. Most uses require both the javadoc tag and this annotation.
Override indicates that the method it annotates should be overriding a method with the same signature in a superclass:
@Override
public int hashCode() {
...
}
Take the above example—if you were to fail to capitalize the "C" in hashCode, you wouldn't get an error at compile time, but at runtime, your method would not be called as you expected. By adding the Override tag, the compiler complains if it doesn't actually perform an override.
This also helps in the case where the superclass changes. If, say, a new parameter were added to this method and the method itself were renamed, then the subclass will suddenly fail to compile, as it no longer overrides anything in the super.
Annotations can be extremely useful in other situations. They work best for frameworks like EJB and Web services, when behavior is not directly modified, but rather enhanced, especially in the case of adding boilerplate code.
Annotations cannot be used as a preprocessor. Sun's design specifically precludes modifying the byte code of a class directly because of an annotation. This is so that the results of the language can be properly understood and tools such as IDEs can perform deep code analysis and functions like refactoring.
Annotations are not a silver bullet. When first encountered, people are tempted to try all sorts of tricks. Take this next proposal we got from someone:
public class Foo {
@Property
private int bar;
}
The idea here was to automatically create getter and setter methods for the private field bar. Unfortunately, this is a bad idea for two reasons: 1) it doesn't work, and 2) it makes this code harder to read and deal with.
It can't be done because, as we mentioned, Sun specifically precludes modifying the class that an annotation appears in.
Even if it were possible, it's not a good idea because it makes this code harder to understand. Someone looking at this code for the first time will have no idea that this annotation creates methods. Also, if in the future you need to do something inside one of those methods, the annotation is useless.
In summary, don't try to use annotations to do what regular code can do.
|
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.
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.
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.
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.
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.
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:
ZooImpl._animal
((AnimalImpl)ZooImpl.getAnimal()).implMethod();
ZooImpl._getAnimal();
All of these have their problems and limitations. Either they're ugly or expose implementation details that should not be necessary.
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();
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.
|
In addition to generic types, Java 5 introduced generic methods. In this example from java.util.Collections, a singleton list is constructed. The element type of the new List is inferred based on the type of the object passed into the method:
static <T> List<T> Collections.singletonList(T o)
Example usage:
public List<Integer> getListOfOne() {
return Collections.singletonList(1);
}
In the example usage, we pass in an int. The return type of the method is then List<Integer>. The compiler infers Integer for T. This is different from generic types because you do not generally need to explicitly specify the type argument.
This also shows the interaction of autoboxing with generics. Type arguments must be reference types; that's why we get List<Integer> and not List<int>.
The emptyList() method was introduced with generics as a type safe replacement for the EMPTY_LIST field in java.util.Collections:
static <T> List<T> Collections.emptyList()
Example usage:
public List<Integer> getNoIntegers() {
return Collections.emptyList();
}
Unlike the previous example, this one has no parameters, so how does the compiler infer the type for T? Basically, it will try once using the parameters. If that does nothing, it tries again using the return or assignment type. In this case, we are returning List<Integer>, so T is inferred to be Integer.
What if you are invoking a generic method in a place other than in a return statement or assignment statement? Then the compiler is unable to do the second pass of type inferencing. In this example, emptyList() is invoked from within the conditional operator:
public List<Integer> getNoIntegers() {
return x ? Collections.emptyList() : null;
}
The compiler cannot see the return context and cannot infer T, so it gives up and assumes Object. You would see an error message like "cannot convert List<Object> to List<Integer>."
To fix this, you explicitly pass the type argument to the method invocation. This way, the compiler won't try to infer the type arguments for you, and you get the right result:
return x ? Collections.<Integer>emptyList() : null;
The other place where this happens frequently is in method invocation. If a method takes a List<String> and you try to call this passing emptyList() for that param, you will also need to use this syntax.
Here are three examples of Generic types that are not Collections that use generics in a novel way. All of these come from the standard Java libraries:
Class<T>
Class is parameterized on the type of the class. This makes it possible to construct a newInstance without casting.
Comparable<T>
Comparable is parameterized by the actual comparison type. This provides stronger typing on compareTo() invocations. For example, String implements Comparable<String>. Invoking compareTo() on anything other than a String will fail at compile time.
Enum<E extends Enum<E>>
Enum is parameterized by the enum type. An enum called Color would extend Enum<Color>. The getDeclaringClass() method returns the class object for the enum type, which in this case would be a Color. It's different from getClass(), which may return an anonymous class.
The most complex part of generics is understanding wildcards. We'll cover the three types of wildcards and why you may want to use them.
First let's look at how arrays work. You can assign a Number[] from an Integer[]. If you attempt to write a Float into the Number[], it will compile but fail at runtime with an ArrayStoreException:
Integer[] ia = new Integer[5]; Number[] na = ia; na[0] = 0.5; // compiles, but fails at runtime
If we try to translate that example directly into generics, it fails at compile time because the assignment isn't allowed:
List<Integer> iList = new ArrayList<Integer>(); List<Number> nList = iList; // not allowed nList.add(0.5);
With Generics, you will never get a runtime ClassCastException as long as you have code that compiles without warnings.
What we want is a list whose exact element type is unknown, unlike the array case.
A List<Number> is a list whose element type is the concrete type Number, exactly.
A List<? extends Number> is a list whose exact element type is unknown. It is Number or a subtype.
If we update our original example and assign to a List<? extends Number>, the assignment now succeeds:
List<Integer> iList = new ArrayList<Integer>(); List<? extends Number> nList = iList; Number n = nList.get(0); nList.add(0.5); // Not allowed
We can get Numbers out of the list because no matter what the exact element type of the list is (Float, Integer, or Number), we can still assign it to Number.
We still can't insert floats into the list. This fails at compile time because we can't prove this is safe. If we were to add a float into the list, it would violate the original type safety of iList—that it stores only Integers.
Wildcards give us more expressive power than is possible with arrays.
In this example, a wildcard is used to hide type information from the user of the API. Internally, the Set is stored as CustomerImpl. To users of the API, all they know is that they are getting a Set from which they can read Customers.
Wildcards are necessary here because you can't assign from a Set<CustomerImpl> to a Set<Customer>:
public class CustomerFactory {
private Set<CustomerImpl> _customers;
public Set<? extends Customer> getCustomers() {
return _customers;
}
}
|
Another common use for wildcards is with covariant returns. The same rules apply to covariant returns as assignments. If you want to return a more specific generic type in an overridden method, the declaring method must use wildcards:
public interface NumberGenerator {
public List<? extends Number> generate();
}
public class FibonacciGenerator extends NumberGenerator {
public List<Integer> generate() {
...
}
}
If this were to use arrays, the interface could return Number[] and the implementation could return Integer[].
We've talked mostly about upper bounded wildcards. There is also a lower bounded wildcard. A List<? super Number> is a list whose exact "element type" is unknown, but it is MNumber or a super type of Number. So it could be a List<Number> or a List<Object>.
Lower bounded wildcards are not nearly as common as upper bounded wildcards. But when you need them, they are essential.
List<? extends Number> readList = new ArrayList<Integer>(); Number n = readList.get(0); List<? super Number> writeList = new ArrayList<Object>(); writeList.add(new Integer(5));
The first list is a list that you can read numbers from.
The second list is a list that you can write numbers to.
Finally, the List<?> is a list of anything and is almost the same as List<? extends Object>. You can always read Objects, but you cannot write to the list.
To summarize, wildcards are great for hiding implementation details from callers as we saw a few sections back, but even though lower bounded wildcards appear to provide read-only access, they do not, due to non-generic methods such as remove(int position). If you want a truly immutable collection, use the methods on java.util.Collections, like unmodifiableList().
Be aware of wildcards when writing APIs. In general, you should try to use wildcards when passing generic types. It makes the API accessible to a wider range of callers.
In this example, by accepting a List<? extends Number> instead of List<Number>, the method below can be called with many different types of Lists:
void removeNegatives(List<? extends Number> list);
Now we'll cover constructing your own generic types. We'll show example idioms where type safety can be improved by using generics, as well as common problems that occur when trying to implement generic types.
This first example of a generic class is a collection-like example. Pair has two type parameters, and the fields are instances of the types:
public final class Pair<A,B> {
public final A first;
public final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
}
This makes it possible to return two items from a method without having to write special-purpose classes for each two-type combo. The other thing you could have done is return Object[], which isn't type-safe or pretty.
In the usage below, we return a File and a Boolean from a method. The client of the method can use the fields directly without casting:
public Pair<File,Boolean> getFileAndWriteStatus(String path){
// create file and status
return new Pair<File,Boolean>(file, status);
}
Pair<File,Boolean> result = getFileAndWriteStatus("...");
File f = result.first;
boolean writeable = result.second;
In this example generics are used for additional compile-time safety. By parameterizing the DBFactory class by the type of Peer it creates, you are forcing Factory subclasses to return a specific subtype of Peer:
public abstract class DBFactory<T extends DBPeer> {
protected abstract T createEmptyPeer();
public List<T> get(String constraint) {
List<T> peers = new ArrayList<T>();
// database magic
return peers;
}
}
By implementing DBFactory<Customer> the CustomerFactory is forced to return a Customer from createEmptyPeer():
public class CustomerFactory extends DBFactory<Customer>{
public Customer createEmptyPeer() {
return new Customer();
}
}
Whenever you want to place constraints on a generic type between parameters or a parameter and a return type, you probably want to use a generic method.
For example, if you write a reverse function that reverses in place, you don't need a generic method. However, if you want reverse to return a new List, you'd like the element type of the new List to be the same as the List that was passed in. In that case, you need a generic method:
<T> List<T> reverse(List<T> list)
When implementing a generic class, you may want to construct an array, T[]. Because generics is implemented by erasure, this is not allowed.
You may try to cast an Object[] to T[]. This is not safe.
The solution, courtesy of the generics tutorial, is to use a "Type Token." By adding a Class<T> parameter to the constructor, you force clients to supply the correct class object for the type parameter of the class:
public class ArrayExample<T> {
private Class<T> clazz;
public ArrayExample(Class<T> clazz) {
this.clazz = clazz;
}
public T[] getArray(int size) {
return (T[])Array.newInstance(clazz, size);
}
}
To construct an ArrayExample<String>, the client would have to pass String.class to the constructor because the type of String.class is Class<String>.
Having the class objects makes it possible then to construct an array with exactly the right element type.
In summary, the new language features make for a substantial change to Java. By understanding when and how to use them, you'll write better code.
Jess Garms is the Javelin compiler team lead at BEA Systems. Prior to that, Jess worked on BEA's Java IDE, WebLogic Workshop. Additionally, he has a great deal of experience with cryptography, and co-authored "Professional Java Security", published by Wrox Press.
Tim Hanson is the Javelin compiler architect at BEA Systems. Tim developed much of BEA's Java compiler - one of the earliest 1.5-compliant implementations. He has written numerous other compilers, including a CORBA/IDL compiler while at IBM, and an XQuery compiler.
Return to dev2dev.