The Java 1.5 proposal offers programmers a false choice between desirable new features and readability. In fact, all of the proposed new features for 1.5 can be represented by clear, unambiguous, and readable constructs without breaking backwards compatibility. In the rest of this article I will illustrate this point by describing three alternative syntaxes I am proposing.
for LoopBecause of frequent requests for a more compact loop construct that works
with collections and arrays, Sun proposes a new for loop that has
foreach semantics. Using an example from the Joshua Bloch interview, Sun wants to replace this:
void cancelAll(Collection c) {
for (Iterator i = c.iterator(); i.hasNext(); ) {
TimerTask tt = (TimerTask) i.next();
tt.cancel();
}
}
with this:
void cancelAll(Collection c) {
for (Object o : c)
((TimerTask)o).cancel();
}
The collection-iterating syntax is more compact, but the use of a colon to
indicate foreach semantics is cryptic. (Is it supposed to resemble
a squashed equals sign? The ale must have flowed like water when they came up
with that!) There is a belief at Sun that foreach is so commonly
used (it is a keyword in Perl, VBScript, Ruby, PHP, and Tcl) that adding a
foreach keyword now would break a lot of code that uses it as a
method or variable name. This is a strong argument, and I won't question it,
since I am proposing a backwards-compatible syntax, but that doesn't mean that
all other options are exhausted and we should resort to punctuation.
For example, why not simply add an eachof keyword that turns
standard for loops into foreach loops, resulting in
code like this?
void cancelAll(Collection c) {
for (Object o = eachof c)
((TimerTask)o).cancel();
}
Unlike foreach, eachof is not used in any of the
major scripting languages. For the very small group of people who might have
used "eachof" as a method or variable name, there is the javac
-source option.
Besides resolving the backwards-compatible keyword issue, this syntax has several other advantages:
eachof can be reused in other loop constructs. It can be
used more than once in a loop when two or more similar collections must be
traversed simultaneously. For example:
void cancelAll(Collection c, d, e) {
assert c.size() == d.size() == e.size();
while ((Object o = eachof c) != null)
{
((TimerTask)o).cancel();
((TimerTask) eachof d).cancel();
((TimerTask) eachof e).cancel();
}
}eachof loops are readable and easy to understand. The
eachof keyword attaches to the nearest enclosing loop, in the same
way that break and continue do.
This is my proposal. Other people might pick another keyword. The point is
that with a little research, it is possible to find safe new keywords; it isn't
necessary to resort to punctuation marks to add a backwards-compatible
foreach capability to Java.
The major new feature in Java 1.5 will be generics, which will expand the range of static type checking greatly and take out a lot of the drudgery of working with collections. Some argue that in adding generics, Java should follow the syntax established by C++ and the Standard Template Library, which would mean replacing this:
static void expurgate(Collection c) {
for (Iterator i = c.iterator(); i.hasNext(); ) {
String s = (String) i.next();
if(s.length() == 4)
i.remove();
}
}
with this:
static void expurgate(Collection<String> c) {
for (Iterator<String> i = c.iterator(); i.hasNext(); )
if (i.next().length() == 4)
i.remove();
}
However, part of the reason Java has succeeded as well as it has is that James Gosling learned from the mistakes of the C++ project -- when he omitted multiple inheritance of implementation, for example, or when he made all reference types heap-based.
Java should continue this practice by adding the generic types popularized by C++ with cleaner and simplified syntax and semantics.
For example, many programmers seem to prefer a generics syntax with curvy brackets. The Generic Java proposal argues that that isn't possible, because it would lead to confusion between type parameters and value parameters. With a little thought, we can find another solution: put the type parameter in parentheses before the parameterized element, resulting in code like this:
static void expurgate((String) Collection c) {
for ((String) Iterator i = c.iterator(); i.hasNext(); )
if (i.next().length() == 4)
i.remove();
}
It might be said that the parenthesized expressions look too much like casts, but they appear in a different context than casts. Also, given the way Java Generics work (type erasure), it's not misleading for the construct to look like a cast. It can be thought of as a "declaration cast" that specifies in one place what cast the compiler should silently insert whenever extracting an element from the collection.
According to this proposal, the cast operator ((TYPE)) is
generalized into a type expression. In assignment context it performs a cast.
In declaration context it specifies a generic type parameter. The two usages do
not overlap, so the kind of operation being performed is always clear.
It is interesting to note that this syntax closely resembles array declaration. In the following code fragment, for example,
String [] strings1 = new String [10];
(String) List strings2 = new (String) List;
the List declaration and the array declaration are alike
except for the absence of parentheses around the array type declaration. If
Java 1.5 adds statically safe arrays, they can be distinguished from standard
arrays by adding parentheses, thus unifying array declaration syntax with
collection syntax.
To sum up, an inverted generics syntax has several advantages:
It leverages and generalizes existing constructs. The programmer's existing understanding of type casts aids the understanding of this syntax.
It is more readable than angle-bracket type parameters. This is a matter of taste, of course, and your taste may differ from mine.
It doesn't result in type parameters and variable parameters becoming "stacked up."
It reflects what actually happens in a generic system based on type erasure, such as Generic Java.
It starts to unify generic collection syntax with existing array syntax.
It maps simply to the standard Generic Java syntax, making it easy to modify existing tools.
The following example combines inverted generics syntax and the
eachof operator. Where Sun Java 1.5 would say,
void cancelAll(Collection<TimerTask> c) {
for (TimerTask task : c)
task.cancel();
}
the proposed syntax would be:
void cancelAll((Timertask) Collection c) {
for (TimerTask task = eachof c)
task.cancel();
}
|
The latest proposed feature for Java 1.5 is variance, which fixes some flaws in Java's type system and increases the range of static type checking. An introduction to variance is beyond the scope of this article, so I can only refer the reader to the variance white paper and the variance tutorial included with 1.5 Early Access, and move on to the syntax.
The proposed syntax for variance denotes:
Numbers as List<+Number>.Numbers as List<-Number>.Numbers as List<*>.Numbers as List<=Number>.The syntax for statically safe arrays is similar.
I submit that this syntax is confusing, especially for the programmer still trying to grasp the meaning of variance.
Instead of trying to map the types of variance to arithmetic symbols, let's
refer back to a basic feature of inheritance: a superclass precedes its
subclasses, chronologically. Given that fact, we should look for a way to
represent chronology typographically. The idea of "reading order" jumps out
right away. In English, at least, we start reading at the top left, and
continue down and to the right. A suitable syntax that agrees with these
observations is to represent a Number and its superclass
ancestors, for example, as "..Number", and to represent a Number
and all potential subclasses as "Number..". For clarity, these
typeranges can have explicitly closed versions: "Object..Number"
and "Number..final". In either case, the relative position of
"Number" and the two-dot ellipsis determines the direction in the class
hierarchy signified.
Having introduced this construct, it is interesting to look back and notice
that it isn't unprecedented. Java and other C-like languages already use this
kind of positional notation when distinguishing between preincrement
(++i), and postincrement (i++), for example.
Here is an extended code sample from the variance tutorial, translated into my proposed syntax:
public abstract class Shape {
public abstract void drawOn (Canvas c)
}
public class Line extends Shape {
public int x, y, x2, y2
public void drawOn (Canvas c) { ... }
}
public class Polygon extends Shape {
private (Line) List lines;
public void drawOn (Canvas c) { ... }
}
public class Canvas {
public void draw (Shape s) {
s.drawOn (this)
}
// Covariant (read-only) list of shapes
public void drawAll ((Shape..) List shapes) {
for (Shape s = eachof shapes)
s.drawOn (this)
}
}
...
// copy from covariant list (read-only) to contravariant list (write-only)
public void copy ((Shape..) List from, (..Shape) List to) {
for (Shape s = eachof from)
to.add (s);
}
// copy from covariant array (read-only) to contravariant array (write-only)
public void copy ((Shape..)[] from, (..Shape)[] to) {
int i = 0;
for (Shape s = eachof from)
to[i++] = s;
}
This syntax has several advantages.
It can be compact enough to satisfy somebody writing complex type
expressions. The short form of covariance is (Shape..) and the
short form of contravariance is (..Shape).
For beginners, a more explicit and verbose syntax is available:
(Shape..final) and (Object..Shape). Without needing
to understand the underlying type theory, a novice programmer can easily
determine what kind of objects are permitted as method arguments, for
example.
The positional notation is reinforced by Javadoc, which shows subclasses below and to the right of superclasses.
As I hope this article has shown, there are readable alternatives to the syntax developed by Sun for the new features proposed in Java 1.5 — alternatives that can maintain backwards-compatibility with standard Java.
What do you think? As Java programmers, do we need to compromise readability in order to get desirable new features? Are there realistic alternatives to Sun's proposal? Is the author's proposal readable or awkward? Does it improve on Sun's proposed syntax?
For comparison purposes, here is an extended example in Sun's proposed 1.5 syntax and the author's variation on that.
Java early access 1.5:
import java.util.LinkedList;
import java.util.Collections;
import static java.lang.Math.*; // import static
class Test {
// enum
enum Color { red, green, blue };
// varargs
public static void printf(String fmt, Object[] args...) {
int i = 0;
// foreach on primitive array
for (char c : fmt.toCharArray()) {
if (c == '%')
System.out.print(args[i++]);
else
System.out.print(c);
}
}
public static void main(String[] args) {
// Integer list
LinkedList<Integer> xs = new LinkedList<Integer>();
xs.add(new Integer(0)); xs.add(new Integer(1));
Integer x = xs.iterator().next();
Integer mb = Collections.max(xs);
// string list
LinkedList<String> ys = new LinkedList<String>();
ys.add("zero"); ys.add("one");
String y = ys.iterator().next();
// string list list
LinkedList<LinkedList<String>> zss = new LinkedList<LinkedList<String>>();
zss.add(ys);
String z = zss.iterator().next().iterator().next();
// foreach on a collection
for (String s : ys)
System.out.println(s);
// varargs and boxing
printf("Addition: % plus % equals %\n", 1, 1, 2);
// use static import
printf("sin(PI/12) = %\n", sin(PI/12));
// use enums
printf("Colors are %\n", Color.VALUES);
for ( Color c : Color.VALUES ) {
// switch on enum
switch(c) {
case Color.red:
System.out.println("found red.");
break;
case Color.green:
System.out.println("found green.");
break;
case Color.blue:
System.out.println("found blue.");
break;
}
}
}
}
Here it is again with the author's proposed syntax:
import java.util.LinkedList;
import java.util.Collections;
import static java.lang.Math.*; // import static
class Test {
// enum
enum Color { red, green, blue };
// varargs
public static void printf(String fmt, Object[] args...) {
int i = 0;
// foreach on primitive array
for (char c = eachof fmt.toCharArray()) {
if (c == '%')
System.out.print(args[i++]);
else
System.out.print(c);
}
}
public static void main(String[] args) {
// Integer list
(Integer) LinkedList xs = new LinkedList;
xs.add(new Integer(0)); xs.add(new Integer(1));
Integer x = xs.iterator().next();
Integer mb = Collections.max(xs);
// string list
(String) LinkedList ys = new LinkedList;
ys.add("zero"); ys.add("one");
String y = ys.iterator().next();
// string list list
((String) LinkedList) LinkedList zss = new LinkedList;
zss.add(ys);
String z = zss.iterator().next().iterator().next();
// foreach on a collection
for (String s = eachof ys)
System.out.println(s);
// varargs and boxing
printf("Addition: % plus % equals %\n", 1, 1, 2);
// use static import
printf("sin(PI/12) = %\n", sin(PI/12));
// use enums
printf("Colors are %\n", Color.VALUES);
for ( Color c = eachof Color.VALUES ) {
// switch on enum
switch(c) {
case Color.red:
System.out.println("found red.");
break;
case Color.green:
System.out.println("found green.");
break;
case Color.blue:
System.out.println("found blue.");
break;
}
}
}
}
"JSR 14: Add Generic Types To The Java Programming Language"
"Prototype for JSR014: Adding Generics to the Java Programming Language v. 2.2"
"GJ: Extending the Java Programming Language with type parameters"
Stephen Jungels is a computer consultant with over five years of experience, currently specializing in Web applications development.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.