Developer J2SE
Part 2: Big Changes Coming for Java
By Jason Hunter
Enhanced For Loop
Remember this earlier example?
public void drawAll(Collection<Shape> c) {
Iterator<Shape> itr = c.iterator();
while (itr.hasNext()) {
itr.next().draw();
}
}
You'll never have to type so much again! Here's the new form in Tiger:
public void drawAll(Collection<Shape> c) {
for (Shape s : c) {
s.draw();
}
}
You read the code as "foreach Shape s in c". Notice how the designers slyly avoided adding any new keywords. Considering how many people use
"in" for input streams, we should all
be very happy about this. The compiler auto-expands the new syntax to its iterative form:
for (Iterator<Shape> $i = c.iterator();
$i.hasNext(); ) {
Shape s = $i.next();
s.draw();
}
You can use this syntax to iterate against raw (nonparameterized) types, but the compiler outputs a warning that the necessary cast may fail. You can use "foreach" on any array or any object implementing the new interface java.lang.Iterable:
public interface Iterable<T> {
SimpleIterator<T> iterator();
}
public interface SimpleIterator<T> {
boolean hasNext();
T next();
}
The new interfaces in java.lang avoid any language dependencies on java.util. The Java language must have no dependencies outside java.lang. Notice the clever use of covariant return types with the next() method. It's true that with this "foreach" syntax and the SimpleIterator interface, you lose the ability to call
iterator.remove(). If you need that,
you must iterate the collection yourself.
Comparing to C#, we see a similar syntax, but C# uses "foreach" and "in" keywords, since they were reserved in the initial release:
// C#
foreach (Color c in colors) {
Console.WriteLine(c);
}
The C# "foreach" works on any collection or array, or anything that implements Ienumerable. Again, we see a close match between C# and Java.
A Typesafe Enum
Enums are a way to define a type with certain named constant values. You've seen them in C, C++, and C#, but they've been distinctly lacking in Java. Now, after eight years, Java's getting them and doing them probably better than any language previously. Let's first examine how we solved the enum problem before. Have you ever written code like this?
class PlayingCard {
public static final int SUIT_CLUBS
= 0;
public static final int SUIT_DIAMONDS
= 1;
public static final int SUIT_HEARTS
= 2;
public static final int SUIT_SPADES
= 3;
// ...
}
It's easy and common but loaded with problems. First, it's not typesafe. You can pass the literal "5" to a method expecting a suit, and it will compile. Also, the values get compiled directly into every class using these constants. Java does
this "inlining" with constant values for optimization, but the risk is that if you reorder the values and recompile just this class, other classes will start mishandling the suits. Moreover, the types are primitives and can't be extended or enhanced, and if you output one of these values, you'll see a cryptic integer rather than a useful mnemonic name. This approach is easy, and that's why we do it, but it's not very elegant. So, maybe you went with the following approach?
class PlayingCard {
class Suit {
public static final Suit CLUBS
= new Suit();
public static final Suit DIAMONDS
= new Suit();
public static final Suit HEARTS
= new Suit();
public static final Suit SPADES
= new Suit();
protected Suit() { }
}
}
It's typesafe and more extensible, and is taught in object-oriented-design classes. However, a simple approach such as this doesn't support serialization, doesn't expose a list of legal values, doesn't allow you to order the values, and doesn't let you print a value as a meaningful string. You can surely add
all these features, and Josh Bloch shows you exactly how in his Effective Java book (chapter 5, item 21). However, you end up with pages of boilerplate code.
Java's new enum feature has an easy one-line syntax:
class PlayingCard {
public enum Suit { clubs,
diamonds, hearts, spades }
}
The suit, called the enum type, has a member for each enum constant. Every enum type is an actual class that auto-extends the new class java.lang.Enum. The compiler gives the enum class meaningful toString(), hashCode(), and equals() methods, and auto-provides Serializable and Comparable abilities. The enum class declaration is enjoyably recursive:
public class Enum<E extends Enum<E>>
implements Comparable<E>,
Serializable {
Using the new enum provides numerous perks, including performance comparable to int operations, compile-time typesafety, constants that aren't compiled into clients and can be renamed and reordered, printed values that are informative, capability for use in collections or even in a switch, the ability to add fields and methods, and the ability to implement an arbitrary interface.
Each enum has a string name and an int ordinal value:
out.println(Suit.clubs); // "clubs"
out.println(Suit.clubs.name); // "clubs"
out.println(Suit.clubs.ordinal); // 0
out.println(Suit.diamonds.ordinal); // 1
Suit.clubs == Suit.clubs // true
Suit.clubs == Suit.diamonds // false
Suit.clubs.compareTo(Suit.diamonds) // -1
Enums can have constructors and methods. Even a main() method is legitimate. The following example assigns values to Roman numerals:
public enum Roman {
I(1), V(5), X(10), L(50), C(100),
D(500), M(1000);
private final int value;
Roman(int value) { this.value = value; }
public int value() { return value; }
public static void main(String[] args) {
System.out.println(Roman.I);
}
}
Oddly, you can't assign ordinal values to an enum, such as "enum Month {jan=1, feb=2, ...}". You can, however, add behaviors to enum constants. For example, in JDOM the XMLOutputter supports several modes of whitespace handling.
If JDOM were to be built against J2SE 1.5, these modes could be defined with an enum, and the enum type itself could hold the handling behavior. Whether
this coding pattern proves useful, we
will learn over time. It's certainly an
interesting concept.
public abstract enum Whitespace {
raw {
String handle(String s) { return s; }
},
trim {
String handle(String s) {
return s.trim(); }
},
trimFullWhite {
String handle(String s) {
return s.trim().equals("") ? "":s; }
};
abstract String handle(String s);
public static void main(String[] args) {
String sample = " Test string ";
for (Whitespace w : Whitespace.VALUES)
System.out.println(w + ": '"
+ w.handle(sample) + "'");
}}
There are a few open issues regarding Java's new enum. Should enum constant names be all caps? It's the standard for constants, but the spec points out that lowercase names "make for nicer string forms, generally obviating the need for
a custom toString method." Also,
should the name and ordinal be fields
or methods? It's the old encapsulation debate all over again. This feature also holds the ignominious honor of being the one feature to add a keyword to J2SE 1.5. Sadly, it's a word that's frequently used to store Enumerator instances. If you have code that already uses "enum," you'll need to change it before compiling for J2SE 1.5. You've now been given a year's warning.
Looking at C#, all enums extend System.Enum. Each has an integer (or byte, or whatever) value that can be assigned. Enums also have static methods to initialize an enum from a string constant, get a list of valid values, and see whether a value is supported. By marking an enum with the [Flags] attribute, you can ensure the values support bit masking, and the system takes care of printing useful output for a masked value:
// C#
[Flags]
public enum Credit : byte {
Visa = 1, MC = 2, Discover = 4 }
Credit accepted = Credit.Visa | Credit.MC;
c.WriteLine(accepted); // 3
c.WriteLine(accepted.Format());//"Visa|MC"
Static Imports
A static import lets you put into scope a set of static methods and fields. It's a shorthand that permits the omission of qualifying class names. For example, a call to Math.abs(x) can become simply abs(x). To statically import all static fields and methods, you write "import static java.lang.Math.*;" or to be specific about what to import, you write "import static java.lang.System.out;"there are no exciting new features here, just shorthand. It lets you do math without Math.
import static java.lang.Math.*;
import static java.lang.System.out;
public class Test {
public static void main(String[] args) {
out.println(abs(-1) * PI);
}
}
Notice the reuse of the "static" keyword to avoid any new keywords. The more mature the language, the more uses one finds for the "static" keyword. If there's a conflict between static members, it's an ambiguity compile-time error, the same as with class imports. There was discussion
of adding java.lang.Math.* as an implicit import, but it's not going to happen, given the ambiguity errors it would trigger.
Varargs
"Varargs," which stands for "variable number of arguments," exists in C and supports the popular printf() and scanf() functions. In Java, we've simulated this feature by writing methods that accept an Object[], List, Properties, or other single data structure that can represent multiple valuesfor example, Method.invoke(Object obj, Object[] args). This requires the caller to wrap the data into this single container structure. Varargs allows the caller to pass an arbitrary
list of values, and the compiler will convert it into an array for the receiver. The syntax is to add "..." to the parameter declaration after the parameter name to make it vararg. It must be
the last parameterfor example, to write a sum() method that adds up an arbitrary number of ints:
out.println(sum(1, 2, 3));
public static int sum(int args...) {
int sum = 0;
for (int x : args) { sum += x; }
return sum;
}
In the early access release, the varargs signature uses square brackets as sum(int[] args...). However, in later discussion, and at James Gosling's suggestion, the square brackets have been dropped.
In the examples here, we don't use square brackets, but you'll need to add them back in if you're running this code against the early access release. By accepting an Object args..., you accept any type of arguments, including primitives, thanks
to autoboxing. This works great with printf() style methods that must accept any number of anything. In fact, the Tiger release may use this feature to provide a Formattable interface with a format() method that behaves like printf(). That's a subject for a later article. For now, we can just write our own simple printf():
public static void printf(String fmt,
int args...) {
int i = 0;
for (char c : fmt.toCharArray()) {
out.print(c == '%' ? args[i++] : c);
}
}
public static void main(String[] args) {
printf("My values are % and %\n",
1, 2);
}
In Tiger, you'll find invoke() with a new signature: invoke(Object obj, Object args...). This should make reflection more natural.
Conclusion
The J2SE 1.5 release strives to make
Java programming easier, safer, and more expressive. The features fit together nicely and operate in harmony. If you're like me, every time you write an old-style "for" loop, you're going to wish you had Tiger.
Remember, however, that the specs aren't yet final and are subject to change. The expert groups managing these changes (JSR-14, JSR-175, and JSR-201) will undoubtedly make tweaks before the beta release, scheduled for late
2003, and before the final release, expected sometime in 2004. However, Sun expressed confidence at JavaOne that the overall outlines won't change much. If you want to experiment, you can get the early access release from the URL listed below. It has the kind of
bugs you'd expect from an early access release, but it's a lot of fun to see the future. I encourage you to try it out.
Jason Hunter ( jasonhunter@servlets.com) is a
consultant and publisher of Servlets.com. He is author of
Java Servlet Programming and coauthor of Java Enterprise Best Practices (both from O'Reilly & Associates).