The current major release of the Java platform is version 5, codenamed "Tiger." Tiger was formally released in 2004 as version number 1.5, and later renumbered version 5.0. This paper takes a brief look at the most important language enhancements that Tiger has to offer. The relevant JFastTrack courses have already been updated to reflect this information.
Most of Tiger's language enhancements offer new ways to do things that Java programmers have been doing for years. Some of the enhancements (enumerations and generics) provide added type safety, making it easier for programmers to write more robust programs. The new for statement syntax gives the compiler the job of automatically generating some boilerplate code, creating and handling iterators.
Because earlier versions of the language have not offered true enumerated types, many Java programmers have tried to implement their own. There are two popular ways to do this, each with its own shortcomings.
Some Java programmers implement an enumeration as a class or interface containing only integer constants. Such constants can be used in switch statements. They offer the advantages of named constants without the benefits of type safety.
Others use the Enumeration pattern as described by Joshua Bloch in his excellent book Effective Java. Using the Enumeration pattern, an enumeration is a class. Its constants are objects that are members of the class. While this pattern offers type safety, it does not allow an enumeration's constants to be used in switch statements. Depending on how it is implemented, there may also be serialization issues.
With JDK 5, Java programmers finally have true enumerated types. An enumerated type in JDK 5 might look like this:
public enum Suit {
CLUBS(0), DIAMONDS(1), HEARTS(2), SPADES(3);
private final int value;
private Suit(int value) {
this.value = value;
}
public int value() {
return value;
}
}
The second line of this code is actually a series of constructor calls and constant declarations. For example, HEARTS is declared as a constant of enum Suit having a value 2.
Most enumerations will use consecutive integer values, but that is not a requirement.
This arrangement provides several advantages:
An enumeration can also be declared inside another class. For example:
public class Card {
private enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
private Suit s;
...
}
The enumeration in this example looks like an enumeration in C or C++. That's the only similarity. This enumeration is essentially a class like the one in the first example.
enum becomes a keyword as of JDK 5. While we expect not many programmers have used the word enum as an identifier, the -source compiler directive can be used to ensure that older code does not have any problems under JDK 5.
While the normal import statement imports classes from a package, the static import statement imports static members from a class.
Programmers have had to use a class name every time they use a constant defined in another class. For example, every time we want to use the CLUBS constant in our first example, we would have to write Suit.CLUBS. If we do this enough times, our code gets harder to write as well as harder to read and maintain.
Some programmers have worked around this by putting their constants into an interface:
// Bad code! Do not use!!!
public interface Suit {
public static final int CLUBS = 0;
public static final int DIAMONDS = 1;
public static final int HEARTS = 2;
public static final int SPADES = 3;
}
Then they create classes that implement the interface:
// Bad code! Do not use!!!
public class CardPlayer implements Suit {
Card firstCard = new Card(10, SPADES);
...
}
Some programmers like this arrangement because it lets them write constants like SPADES without having to write out Suit.SPADES. It can be a time saver.
However, this is not a good practice; In fact, it is a well-known anti-pattern. The biggest problem is that it makes those constants into public members of each implementing class. This makes the constants available in places where they may not be appropriate and lets us write meaningless expressions like CardPlayer.SPADES.
The static import makes static members available. It lets us use static constants of a class without writing the class name every time. For example, given the import
import static java.awt.Color.*;
We might write a paint method that contains the following:
g.setColor(BLACK);
g.fillRect(0, 0, 100, 100);
g.setColor(WHITE);
g.drawString("Welcome!", 20, 20);
Since the import of class Color is static, we do not need to specify that constants BLACK and WHITE are members of Color. This may look a bit like the bad code in the previous example. However, using the static import, constants like WHITE are just imports, they do not become members of the importing class.
Generics finally give Java programmers type safety in collections. The syntax for generics is based on the syntax for C++ templates.
Java's collection classes store references to class Object. Each reference stored in a collection is implicitly typecast; It becomes a reference to class Object. There is no type checking. A collection can hold references to any class of object, or -- worse yet -- any combination of classes.
For example, to create a Vector of Transaction objects, we might write:
Vector allSales = new Vector();
Adding a Transaction to the Vector is straightforward:
allSales.add(sale);
There are several ways to get this Transaction from the Vector, for example:
Transaction nextSale = (Transaction) allSales.get(index);
This code has two drawbacks:
The better way is to use generics. Using generics, the Vector declaration becomes:
Vector<Transaction> allSales = new Vector<Transaction>();
This declares a Vector that will contain only references to Transaction objects or objects that are subclasses of Transaction. If a programmer accidentally tries to add an object of an unrelated class (e.g., String) to this Vector, the error will be reported at compile time.
Another advantage of generics is that there is no need to do a typecast when an element is accessed. Generics let us write:
Transaction nextSale = allSales.get(index);
For example, we might write the following code to iterate through a Collection of Transactions:
double getBalance(Collection c) {
double total = 0;
for (Iterator it = c.iterator(); it.hasNext(); ) {
Transaction t = (Transaction) it.next();
total += t.getAmount();
}
return total;
}
The parameter to this method must be a collection of Transactions, otherwise the typecast inside the loop will fail. Without generics, the Java compiler has no way to enforce this.
We can add type safety to this method using generics:
double getBalance(Collection<Transaction> c) {
double total = 0;
for (Iterator<Transaction> it = c.iterator(); it.hasNext(); ) {
total += it.next().getAmount();
}
return total;
}
Generics give us two advantages in this code:
One common task for programmers is the creation of iterators. To iterate through a collection, we need to create an iterator, advance it through the collection, and test it to see whether there are more elements to be processed.
This tends to be boilerplate, repetitious code that experienced Java programmers dash off with little thought. It's more complicated than it needs to be, though. As the example above shows, the code can get needlessly complex when dealing with generics.
JDK 5 introduces a new form of the for loop that makes it easier to iterate through collections. The example without generics can be rewritten:
double getBalance(Collection c) {
double total = 0;
for (Object t : c) {
total += ((Transaction) t).getAmount();
}
return total;
}
Notice the colon in the for statement. This new for statement might be read "for every object t in collection c." The compiler automatically generates the code needed to create an iterator, move it through the collection, and test the loop ending condition.
Our final form of this loop combines generics with the enhanced for loop syntax:
double getBalance(Collection<Transaction> c) {
double total = 0;
for (Transaction t : c) {
total += t.getAmount();
}
return total;
}
We use generics to specify that the parameter is a collection of Transactions. Then we use the enhanced for statement to iterate through the entire collection.
This is much simpler to read and write than the previous examples, and it is less error prone. It gives us type checking at compile time, without risking a ClassCastException at run time.
JDK 5 provides automatic conversion between primitive types and wrapper classes.
The wrapper classes are commonly used to store primitive values in collections. Code can become awkward when we need to do frequent conversions between primitive values and their corresponding wrapper objects.
For example, to store an int value in a Vector, we would create an object of class Integer:
Integer intTotal = new Integer(total);
Vector totals = new Vector();
totals.add(intTotal);
Objects of wrapper classes are immutable. To add to this total, we need to retrieve the Integer object from the collection, convert it back to an int, do the addition, create a new Integer object containing the sum, then store the value back into the collection. It might look something like this:
Integer intTotal = (Integer) totals.get(index);
int total = intTotal.intValue();
total += newEntry;
totals.set(index, new Integer(total));
That's a lot of work to perform a relatively simple operation.
Starting in JDK 5, the compiler can do the conversions for us. If totals has been properly declared as a Vector<Integer>, the code above can be reduced to:totals.set(index, totals.get(index) + newEntry);
The compiler automatically generates the code needed to convert the wrapper type to a primitive and back.
We at JFastTrack are very pleased with the language additions provided in JDK 5. These additions address language shortcomings that have existed since the earliest days of Java.
Our training materials have already been updated based on this information. The code samples above have been tested with JDK 5.0 compiler and run-time interpreters.
If you have any questions or comments on this material, please feel free to contact us.