Software Development
Sun’s goal for J2SE 1.5 is to make code “clearer, safer, sorter and easier to develop—without sacrificing compatibility.” Last month, I introduced the generics feature of J2SE 1.5, revealing how you can use parameterized types as well as construct your own. Do parameterized types achieve Sun’s goal? Here’s a report card:
Clearer: Client code is clearer—casts are eliminated; also, when you pass parameterized collections to methods, it’s obvious what type of object they contain. But parameterized type implementations can be far more abstruse, as you’ll see this month. Grade: B
Safer: A common defect in pre-1.5 Java involves stuffing objects of one type in a collection, and in a distant method, thinking the collection holds some other class of objects. Use of parameterized types can eliminate that possible confusion. However, it’s still possible to circumvent generics and get yourself in trouble. Grade: B
Shorter: Little difference overall. Grade: C
Easier to develop: Using parameterized types is easy, but you must fully understand the complexities of erasure in order to develop them. Also, the many limitations mean that it can be difficult to figure out how to solve certain problems. Grade: C
Retaining compatibility: The generics solution meets this goal. There were other possible solutions, but they would not have afforded the required level of compatibility. While there’s something to be said for not breaking existing code, I hope that at some point a major overhaul of Java eliminates its many little warts and flaws. Perhaps in Java 3. Grade: A
Overall grade: C+
This month, I’ll discuss advanced generics constructs and some limitations on the use of generics—the things that led to the not-so-hot report card.
Wildcards
Here’s some of the code for the NumList
type introduced last month:
Suppose you need to support adding a NumList
bound to one type to another
bound to the same type. A wildcard character, ?
, as a bind parameter means
that it may be of
any type. You can also constrain a wildcard type using
the extends
clause. The NumList
method addAll
provides an example:
The method addAll
takes a NumList
as a parameter. This parameter can be bound
to any class that is a subclass of whatever T
resolves to. So, if the receiving
NumList
is bound to Double
, moreNumbers
can be bound to Double
or any Double
subclass.
Parameterized Methods
The class java.util.Collections
has been retrofitted with support for parameterized
collection types. The old sort
method has a simple signature:
The signature in 1.5 looks like:
Things are starting to get a bit involved! With generics, you’ll have to spend a bit more time before you can digest what a method signature is really saying.
The new sort
method is generic—it lets you vary the type of the
list parameter. In 1.5, you can define a type parameter that applies only to
the following method. The 1.5 sort
method takes a List
bound to a class T
.
The class that T
resolves to must also implement the Comparable
interface,
which in turn must be bound to a superclass of List
.
This is a significant capability: Under J2SE 1.4, you could pass a List
containing
anything to the sort
method. If you stuffed non-Comparable objects into the
list, you’d have to wait until runtime to discover the defect. Under
1.5, you’ll uncover your error at compile time.
Additional Bounds
The java.util.Collections
class supports doing binary searches against a List
.
You can pass a List
and a key Object
to the static method binarySearch
. For
1.5, Sun wanted binarySearch
to solve the same problem of ensuring that the
list passed contains objects bound to the appropriate Comparable
type. An
initial workable solution might look like this:
(Expect more long multiline signatures under 1.5.)
The only problem is that this breaks compatibility, since it changes the method
signature. If T
is bound to a Comparable
type, under the covers, the signature
of binarySearch
resolves to the following:
While this change wouldn’t affect most code, any code that presumed the exact signature (such as code using reflection) would now be broken. As with most new language releases, backward compatibility with existing code has always been a primary requirement for new Java releases.
Java 1.5 allows declaring additional bounds for type parameters. A type parameter
can extend a class or interface, plus any number of additional interface bounds.
The significant benefit in this case is that it allows the J2SE 1.5 API library
to maintain backward compatibility while adding a new constraint: The parameter
type T
in binarySearch
can extend Object
(thus preserving the signature of
binarySearch
) and (&
) the appropriate Comparable
interface.
Another potential use is to allow you to constrain a parameter to implementing two or more interfaces. However, this use would fall into the “questionable design” category. If I insist that a parameter object implement two interfaces, I declare the intent that my method will send messages defined in both interfaces to the parameter. This may violate good method and/or composition principles, and suggests that there is probably—but not always—a better design.
Most of us won’t need to worry about backward signature compatibility, and we should know better than to violate simple design principles, so most of us will have little need for additional bounds.
Limitations of Generics
Under C++, binding a parameterized type actually creates a new class, behind
the scenes, for each different type bound to. Anywhere a naked type variable
like T
appears in the source for the parameterized type, it would be replaced
with the bind type.
Erasure doesn’t create additional types. Java compiles a parameterized
type to a single class by replacing the naked type variable with T
’s
upper bounds (Object
by default).
The erasure scheme means that some things just don’t make sense, and
thus are disallowed. For example, constructing new objects of the naked type
is not possible. This would seem to make sense, since most of the time this
would equate to constructing new instances of either Object
or an abstract
type. Yet there are legitimate cases where you need to do so.
Some of the other limitations of generics in Java include: No naked types
in static variables, methods or static nested classes! No binding to primitive
types (but see next month’s discussion of autoboxing). No extending from
or implementing a naked type. No use of naked types in instanceof
, and no arrays
of generic types (but you can declare List <?>[]
).
In most cases, you can code around these restrictions, but having to do so
is enough cause for me to complain about erasure. As an example, one of the
coding patterns I appreciate is Joshua Kerievsy’s "Replace
Multiple Constructors With Creation Methods". Classes with multiple constructors don’t always
impart intent well, whereas I can create a static-side creation method with
a very explicit name. For example, I’d like to be able to add a creation
method to NumList
that allows specification of its capacity:
Unfortunately, the compiler prevents me from doing so. I resent anything that diminishes my ability to make my code expressive.
Subversion
I often encounter clever programmers who don’t want to go along with what I’ve coded. Once they get their hands on my code, they’ll do anything to subvert my intention, never mind the implications. Fortunately for them, and not for me, the generics implementation allows them to continue with their wily ways, as the following piece of code attests:
All it takes is a cast to the raw type, NumList
, and a nefarious programmer
can add any type to nums
—they can ignore the unchecked warning they’ll
receive. When code elsewhere in the system attempts to extract from the NumList
and assign it to an instance of the bind type Integer, I get a ClassCastException
—someone
snuck a Double
into my Integer
collection!
Sun does provide a handful of wrapper methods in the Collections
class, such
as checkedList
, that give you runtime protection from someone inserting a bad
object into your collection. However, you must pass these methods a Class
object
to indicate the type you want to limit the collection to. From within a generic
class such as NumList
, there’s no way to determine the class a raw type
represents. This means you can’t effectively use checked collection methods
within a parameterized type. Stymied again!
Reflection
The reflection classes have been updated to support generics. The class Class
understands parameterized types, so that executing getClass
on an object
will return a Class<T>
object. Class
has several methods
to allow you to extract information on type parameters, including the type
to which a
class is bound. The classes Method
and Constructor
also supply generics information.
The downside? You’re still limited by erasure and the fact that you
can’t obtain the Class
object to which a parameterized type is bound:
there’s no way to determine whether NumList
is bound to Integer
or to
Double
. This has the potential to severely limit dynamic applications.
Covariant Return Types
A J2SE 1.5 modification to the Java language that’s exclusive of generics
is the addition of support for covariant return types: When overriding a method,
the subclass can vary the return type so that it’s a subclass of the
superclass return type. For example, suppose the class Number
were to declare
a squared method
:
In Integer
, which extends Number
, you could declare:
The benefit of covariance is similar to that of generics: Before, the Integer
subclass would have had to return the abstract supertype Number
, resulting
in the need for Integer
clients to cast whenever using the squared
method.
One wonders if covariance alone would have been sufficient, as a small bone,
until Sun is willing to drop backward compatibility.
Steep Curve Ahead
I’ve covered most of what you’ll need to know to use and implement parameterized types. There are still a few esoteric areas that most developers won’t need to concern themselves with; I refer you to Sun’s specification document for those concerns.
Overall, I believe that the J2SE 1.5 implementation of generics will help solve some problems. However, their creation represents an advanced topic for the average Java developer, and the learning curve will be steep. It also has the potential to be easily abused by the overly clever developer. Use these new Java stripes wisely, lest they bite you with higher maintenance costs.
Next month: The rest of the goodies.
Lint Options
When working with raw types (parameterized types not bound to anything), you
may receive an “unchecked” warning. As with deprecation warnings,
you get one warning by default, no matter how many transgressions appear
in your code. This is a good thing, because otherwise, your pre-1.5 code
could generate voluminous warnings. The compiler will tell you that you can
recompile and use the
The —J.L. |
Jeff Langr, owner of Langr Software Solutions, is the author of the book Essential Java Style and several articles, including two appearing in Software Development. He is currently working on a book on Java 1.5 and resides in Colorado Springs, Colo.