10-8-04 Templates vs. Generics
Note: an earlier version of this article was posted briefly, then
a reader pointed something out that required rewriting and which lead to other
insights, so I took the article down and rewrote it. So you might have seen the
earlier version before it disappeared. I have also removed comments that no
longer apply to the rewritten version of this article.
This article will be part of my presentation at the
Colorado Software Summit.
A number of readers have pointed out that they would benefit from seeing an
example of what I've been trying to say about the limitations that erasure has
imposed on Java Generics in relation to other parameterized type systems,
especially the most widely-known and used system: C++ templates. In this article
I shall show a sequence of examples that demonstrate the issues, as well as a
technique to compensate for the lack of latent typing in Java Generics.
New Generics FAQ
But first, an announcement. If you still think Java Generics are trivial, a
new resource has just been published that plumbs the depths of Java Generics,
and may just convince you otherwise: Angelika Langer's Java
Generics FAQ. You can find out more about Angelika on her web site, but
she's been a long-time member of the C++ Standards Committee and has been
writing and teaching the tricky parts of both C++ and Java for quite awhile, so
she knows her stuff. And she's been researching Java Generics for at least 1.5
years, which is where this FAQ has come from.
Here's are some puzzles adapted from the
FAQ. The first one is rather mindblowing:
import java.util.*;
public class Puzzles {
public static <T> void fill(T[] array, T elem) {
for(int i = 0; i < array.length; i++)
array[i] = elem;
}
public static void main(String[] args) {
fill(new String[5], "A string");
fill(new String[5], 100); // What the ...???
// Won't compile:
// Class<String> cs = "abc".getClass();
Object obj = new LinkedList<Long>();
// Won't compile:
// System.out.println(obj instanceof List<Long>);
}
}
Yes, the calls to fill() compile and represent conformant
generic code (an exception is generated at runtime, so it's an example of where
generics do not produce static type safety). You'll have to look it up in the
FAQ if you want to know the answer.
The example
The example that I will start with is adapted from something I wrote for Thinking in C++,
Volume 2. In this article, I'll look at the C++ version of the example and
then see what it looks like in Java. In this case, the Java version happens to
be able to use an interface (Iterable) and so things work out
pretty well. However, I will then look at a second example where no such
interface happens to be used, and show how Java's lack of latent typing prevents
you from writing truly "generic" code. Finally, I'll show a way around the lack
of latent typing in Java by introducing more code in the form of the
Adapter design pattern.
What I found in modifying the C++ example and translating it to Java was
surprising. As far as generics go, it does in fact support my thesis that
erasure limits the expressivity of generics. What is surprising is that I was
expecting the usual result that C++ code is succinct (although arguably more
complex, and certainly more difficult to get working right) while the Java code
tends to be long and rambling. But in this case, the Java code is small and
elegant, and far less verbose than the C++ code, so I found the Java result
quite delightful. Alas, generics had nothing to do with this succinctness
it was actually reflection and varargs that were responsible. And so the power
of reflection continues to be one of the most interesting features of Java, even
though its runtime uncertainty is at odds with the Java Generics goal of
static type safety.
The example is to create an apply() function that will apply any
method to every object in a sequence. This is somewhat similar to the idea of
closures (without the ability to implicitly capture context) that Martin Fowler recently
explained in Ruby, and a number of other writers provided solutions in other
languages (see links from Martin's entry).
The only constraint on the sequence is that it have (in the C++ case) a
begin() method that produces an iterator. While this
is the way that the C++ STL collections are defined to work, any class that
produces an iterator this way will work with the apply() function.
This is an important point the C++ code defined here is truly "generic"
because of latent typing. It can be applied to any sequence that satisfies the
protocol, without requiring that the sequence be of any particular class or
implement any particular interface. Because of this, one piece of code can be
applied across more different types, which, I postulate, is the core intent of
parameterized types.
Here is the C++ code, which is modified from the
C05:ApplySequence.h example in Thinking in C++,
Volume 2. It is presented as a single file to make it easier to compile and
experiment with (you can download the file here):
// This part represents the "apply" library:
// const, 0 arguments, any type of return value. The only
// "bound" is that Seq must produce an iterator via a
// begin() method, that produces pointers to objects:
template<class Seq, class T, class R>
void apply(Seq& sq, R (T::*f)() const) {
typename Seq::iterator it = sq.begin();
while(it != sq.end())
((*it++)->*f)();
}
// const, 1 argument, any type of return value:
template<class Seq, class T, class R, class A>
void apply(Seq& sq, R(T::*f)(A) const, A a) {
typename Seq::iterator it = sq.begin();
while(it != sq.end())
((*it++)->*f)(a);
}
// const, 2 arguments, any type of return value:
template<class Seq, class T, class R,
class A1, class A2>
void apply(Seq& sq, R(T::*f)(A1, A2) const,
A1 a1, A2 a2) {
typename Seq::iterator it = sq.begin();
while(it != sq.end())
((*it++)->*f)(a1, a2);
}
// Non-const, 0 arguments, any type of return value:
template<class Seq, class T, class R>
void apply(Seq& sq, R (T::*f)()) {
typename Seq::iterator it = sq.begin();
while(it != sq.end())
((*it++)->*f)();
}
// Non-const, 1 argument, any type of return value:
template<class Seq, class T, class R, class A>
void apply(Seq& sq, R(T::*f)(A), A a) {
typename Seq::iterator it = sq.begin();
while(it != sq.end())
((*it++)->*f)(a);
}
// Non-const, 2 arguments, any type of return value:
template<class Seq, class T, class R,
class A1, class A2>
void apply(Seq& sq, R(T::*f)(A1, A2),
A1 a1, A2 a2) {
typename Seq::iterator it = sq.begin();
while(it != sq.end())
((*it++)->*f)(a1, a2);
}
// Etc., to handle maximum likely arguments
// The rest of the code uses the "apply" library:
#include<iostream>
using namespace std;
class Shape {
public:
void rotate() { cout << (int)this << " rotate" << endl; }
void resize(int newSize) {
cout << (int)this << " resize " << newSize << endl;
}
};
class Square : public Shape {};
#include <vector>
// A vector that fills itself:
template<class T> class FilledList : public vector<T*> {
public:
FilledList(int size) {
for(int i = 0; i < size; i++)
// The existence of the default constructor
// is checked at compile time:
push_back(new T());
}
};
int main() {
vector<Shape*> shapes;
for(int i = 0; i < 10; i++)
shapes.push_back(new Shape());
apply(shapes, &Shape::rotate);
apply(shapes, &Shape::resize, 5);
vector<Square*> squares;
for(int i = 0; i < 10; i++)
squares.push_back(new Square());
apply(squares, &Shape::rotate);
apply(squares, &Shape::resize, 5);
FilledList<Shape> shapes2(10);
FilledList<Square> squares2(10);
apply(shapes2, &Shape::rotate);
apply(squares2, &Shape::rotate);
// (I'm using the "end-of-program-garbage-collection"
// idiom here :-)
}
The first section of the code represents the reusable template library. You
can see that it is a series of overloaded versions of the apply()
function. The reason this is necessary is because there are variations in the
use of apply() that I could not figure out how to encapsulate in a
single template. I asked Andrei Alexandrescu whether there's a way to do this,
and he replied:
The trick is to have apply() take a functor with zero parameters
(which makes apply() become really for_each()), and
then construct the functor outside of apply with the desired number of
parameters.
Writing a functor that accomodates pointers to member functions and multiple
parameters is tricky, but feasible in less-than ideal ways. (Multiple
parameters kill you). See the Functors chapters in Modern C++ Design for
considerations about doing that.
Perhaps one of my readers with their brain currently more in C++ and less in
Java (where mine has been) can rewrite the example in this way.
In the above code, there's an overloaded apply() for each case
of the member function being called having zero, one and two arguments, and
const and non-const member functions. To be complete,
you'd probably want to keep going to at least six or eight arguments.
So the C++ version is verbose, which you can justify if you're creating it as
a library you solve the problem once, and in the future all you have to
do is include the header file and call apply(). It's also very
efficient at runtime. However, the verbosity reduces the likelihood that you'll
just dash off this code, whereas we'll see that the Java solution (again to my
surprise) encourages such coding.
The apply() functions also require C++ pointer-to-member syntax,
which is the way that you pass around the identity of a member function within a
class that you'd like to call. In the apply() function, pointer to
member is passed as R (T::*f)(), where f is the
identifier and R is the parameterized return value. The call looks
like this: ((*it++)->*f)(), which dereferences the iterator,
then dereferences the pointer to member function in order to call it with the
resulting object pointer. At the call site, you specify the address of the
member function you wish to call using the relatively straightforward (for C++,
anyway) &Shape::rotate.
Once the apply() library has been created, it can be used with a
typical list of objects. Here, I create Shape classes, and to
further the example, a generic collection called FilledList that
can fill itself with a specified quantity of any type you desire. The constraint
for FilledList is that the type parameter must have a default (no-
arg) constructor, but this is checked at compile time so you get type safety
without erasure.
In main(), various collections are created and filled, and the
apply() function is applied to them. The resulting syntax is
reasonably direct, and all operations are checked at compile time so the results
are very efficient.
One important issue to notice: C++ templates do not need wildcards. Because
of latent typing you don't need to care whether a
vector<Square> behaves as if it subclasses a
vector<Shape>. The only thing the compiler cares about is
whether the operations can be performed successfully.
The Java Version
The first thing I discovered when translating the example to Java is that
this is a situation where interfaces don't seem to fit. I want to be able to
apply any method to a collection of objects, and interfaces constrain you too
much to be able to describe "any method." C# has delgates which can be used for
this purpose. C++ has pointers to members, shown above. How do you do this in
Java? The solution seems to be reflection [1] (see the Wiki feedback page for one alternative).
As I mentioned at the beginning of this article, the syntax using reflection
is far more elegant than I imagined, and with varargs all of the overloaded
apply() functions that we saw in C++ collapse into a single method
(you can download the file here):
//: cY:Apply.java
import java.util.*;
import java.lang.reflect.*;
public class Apply {
public static <T, S extends Iterable<? extends T>>
void apply(S seq, Method f, Object... args) {
try {
for(T t: seq)
f.invoke(t, args);
} catch(Exception e) {
// Failures are programmer errors
throw new RuntimeException(e);
}
}
}
class Shape {
public void rotate() {
System.out.println(this + " rotate");
}
public void resize(int newSize) {
System.out.println(this + " resize " + newSize);
}
}
class Square extends Shape {}
class FilledList<T> extends ArrayList<T> {
public FilledList(Class<? extends T> klass, int size) {
try {
for(int i = 0; i < size; i++)
add(klass.newInstance()); // Assumes default constructor
} catch(Exception e) {
// Failures are programmer errors
throw new RuntimeException(e);
}
}
}
// A different kind of container that is Iterable
class SimpleQueue<T> implements Iterable<T> {
private LinkedList<T> storage = new LinkedList<T>();
public void add(T t) { storage.offer(t); }
public T get() { return storage.poll(); }
public Iterator<T> iterator() { return storage.iterator(); }
}
class ApplyTest {
public static void main(String[] args) throws Exception {
List<Shape> shapes = new ArrayList<Shape>();
for(int i = 0; i < 10; i++)
shapes.add(new Shape());
Apply.apply(shapes, Shape.class.getMethod("rotate"));
Apply.apply(shapes,
Shape.class.getMethod("resize", int.class), 5);
List<Square> squares = new ArrayList<Square>();
for(int i = 0; i < 10; i++)
squares.add(new Square());
Apply.apply(squares, Shape.class.getMethod("rotate"));
Apply.apply(squares,
Shape.class.getMethod("resize", int.class), 5);
Apply.apply(new FilledList<Shape>(Shape.class, 10),
Shape.class.getMethod("rotate"));
Apply.apply(new FilledList<Shape>(Square.class, 10),
Shape.class.getMethod("rotate"));
SimpleQueue<Shape> shapeQueue = new SimpleQueue<Shape>();
for(int i = 0; i < 5; i++) {
shapeQueue.add(new Shape());
shapeQueue.add(new Square());
}
Apply.apply(shapeQueue, Shape.class.getMethod("rotate"));
}
}
In Apply, we get lucky because there happens to be an
Iterable interface built into Java and the Java collections
library. Because of this, the apply() method can accept anything
that implements the Iterable interface, which includes all the
Collection classes such as List, but it can also
accept anything else as long as you make it Iterable for
example, the SimpleQueue class defined above and used in
main().
Exceptions are converted to RuntimeExceptions because there's
not much of a way to recover from exceptions they really do represent
programmer errors in this case. However, the equivalent C++ code detects the
problems at compile time.
Those who read my writings know that I do not fear dynamic behavior,
and in fact believe it to be one of Java's greater strengths, so this aspect of
things does not bother me. In Java, some tests do happen at runtime, and
that's a good thing. Choose your battles sometimes discovering problems
at runtime is not so bad. It's a lot better than not finding them at all, or
trying to track down a C++ memory leak.
Note that I had to put in bounds and wilcards in order for Apply
and FilledList to be used in all desired situations. You can
experiment by taking these out and you'll discover that some applications of
Apply and FilledList will not work.
FilledList presents a bit of a quandary. In order for a type to
be used, it must have a default (no-arg) constructor. In C++ this is checked at
compile time, but Java has no way to assert such a thing at compile time, so it
becomes a runtime issue. A common suggestion to ensure compile-time checking is
to define a factory interface that has a method that generates objects, then
FilledList would accept that interface rather than the "raw
factory" of the class token. The problem with this is that all the classes you
use in FilledList must then implement your factory interface. Alas,
most classes are created without knowledge of your interface, and therefore do
not implement it. Later, I'll show one solution using Adapters.
But the approach shown, of using a class token, is perhaps a reasonable
tradeoff (at least as a first-cut solution) since it makes the use of something
like FilledList just easy enough that it may be used rather than
ignored. Of course, you need confidence that errors will appear
early in the development process.
Note that the class token technique is recommended in the Java literature,
such as Gilad Bracha's
Generics in the Java Programming Language, where he notes: "It's an idiom
that's used extensively in the new APIs for manipulating annotations, for
example." However, I've discovered some inconsistency in people's comfort level
with this technique; some people strongly prefer the factory approach, which
we'll look at later.
Also, as elegant as the Java solution turns out to be, we must observe that
the use of reflection (as much as it has been improved in recent versions of
Java) is likely to be distinctly slower than the C++ implementation, since so
much is happening at runtime. Not that this should stop you from using the
solution (at least as a first cut, lest you fall sway to premature optimization)
but it's certainly a distinction between the two approaches.
When you don't happen to have the right interface...
The above example benefitted because the Iterable interface was
already built in, and was exactly what we needed. But what about the general
case, when there isn't an interface that just happens to fit your needs, already
in place?
For example, let's generalize the idea in FilledList and create
a parameterized fill() method that will take a sequence and fill it
using a Generator. Here's the C++ version:
#include <vector>
#include <iostream>
#include <cassert>
using namespace std;
// Because of latent typing, doesn't need an interface for A:
template<class A, class T> void fill(A& addable, int size) {
for(int i = 0; i < size; i++)
// Checks for default constructor at compile-time:
addable.push_back(new T());
}
class Contract {
public:
virtual void print() {
cout << "Contract " << (int)this << endl;
}
};
class TitleTransfer : public Contract {
public:
virtual void print() {
cout << "TitleTransfer " << (int)this << endl;
}
};
// A different container that also has a push_back:
template<class T> class Stack {
enum { ssize = 100 };
T* stack[ssize];
int top;
public:
Stack() : top(0) {}
void push_back(T* t) {
assert(top < ssize);
stack[top++] = t;
}
T* pop() {
assert(top > 0);
return stack[--top];
}
int size() { return top; }
};
int main() {
typedef vector<Contract*> Contracts;
Contracts contracts;
fill<Contracts, Contract>(contracts, 3);
fill<Contracts, TitleTransfer>(contracts, 2);
Contracts::iterator it = contracts.begin();
while(it != contracts.end())
(*it++)->print();
cout << "--------------" << endl;
// Works because of latent typing:
Stack<Contract> contractStack;
fill<Stack<Contract>, TitleTransfer>(contractStack, 7);
while(contractStack.size())
contractStack.pop()->print();
}
When we convert it to Java, we run into a problem, because there is no
convenient Addable interface. So instead of saying "anything that
you can call add() on," you have to say "subtype of
Collection." The resulting code is not particularly generic, since
it must be constrained to work with Collection implementations.
When I did the same thing in C++ by adding the Stack class the new
class still worked with my existing template, without requiring changes to the
template code (since all the template required is a push_back()
method, not an interface). In Java, if I create a new class that doesn't
implement Collection, my generic code won't work. Here's the translation:
//: cY:Fill.java
// Generalizing the FilledList idea
import java.util.*;
import java.lang.reflect.*;
// Doesn't work with "anything that has an add()."
// There is no "Addable" interface so we are narrowed
// to using a Collection.
// We cannot generalize using generics in this case.
public class Fill {
public static <T> void
fill(Collection<T> collection, Class<? extends T> classToken, int size) {
for(int i = 0; i < size; i++)
// Assumes default constructor:
try {
collection.add(classToken.newInstance());
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
class Contract {}
class TitleTransfer extends Contract {}
class FillTest {
public static void main(String[] args) {
List<Contract> contracts = new ArrayList<Contract>();
Fill.fill(contracts, Contract.class, 3);
Fill.fill(contracts, TitleTransfer.class, 2);
for(Contract c: contracts)
System.out.println(c);
SimpleQueue<Contract> contractQueue = new SimpleQueue<Contract>();
// Won't work. fill() is not generic enough:
// Fill.fill(contractQueue, Contract.class, 3);
}
}
This is where a parameterized type mechanism with latent typing is valuable,
because you are not at the mercy of the past design decisions of any particular
library creator, so you do not have to rewrite your code every time you
encounter a new library that didn't take your situation into account (thus the
code is truly "generic"). In the above case, since the Java designers
(understandably) did not see the need for an "Addable" interface, we are
constrained within the Collection hierarchy, and
SimpleQueue, even though it has an add() method, will
not work. Because it is thus constrained to working with
Collection, the code is not particularly "generic." With latent
typing, this would not be the case.
Working around the lack of latent typing
So Java Generics doesn't have latent typing, and we need something like
latent typing in order to write code that can be applied across class boundaries
(that is, "generic" code). Is there some way to get around this limitation?
To figure this out, I asked myself "what is latent typing accomplishing
here?" It means that you are able to write code saying "I don't care what type
I'm using here as long as it has these methods." In effect, latent typing
creates an implicit interface containing the desired methods. So it follows that
if we write the necessary interface by hand (since Java doesn't do it for us),
that should solve the problem.
Writing code to produce an interface that we want from an interface that we
have is an example of the Adapter design pattern. We can use adapters to
adapt existing classes to produce the desired interface, with a relatively small
amount of code. The solution demonstrates
different ways of writing adapters, including one that is quite surprising:
//: cY:Fill2.java
// Using adapters to compensate for lack of latent typing.
import java.util.*;
import java.lang.reflect.*;
interface Generator<T> { T next(); }
interface Addable<T> { void add(T t); }
public class Fill2 {
// Classtoken version:
public static <T> void fill(Addable<T> addable,
Class<? extends T> classToken, int size) {
for(int i = 0; i < size; i++)
try {
addable.add(classToken.newInstance());
} catch(Exception e) {
throw new RuntimeException(e);
}
}
// Generator version:
public static <T> void
fill(Addable<T> addable, Generator<T> generator, int size) {
for(int i = 0; i < size; i++)
addable.add(generator.next());
}
}
// If you're adapting a base type, you must use composition.
// Make any Collection Addable using composition:
class AddableCollectionAdapter<T> implements Addable<T> {
private Collection<T> c;
public AddableCollectionAdapter(Collection<T> c) {
this.c = c;
}
public void add(T item) { c.add(item); }
}
// Helper method for above, captures the type automatically:
class Adapter {
public static <T>
Addable<T> collectionAdapter(Collection<T> c) {
return new AddableCollectionAdapter<T>(c);
}
}
// If you're adapting a specific type, you can use inheritance:
// Make a SimpleQueue Addable using inheritance:
class AddableSimpleQueue<T>
extends SimpleQueue<T> implements Addable<T> {
public void add(T item) { super.add(item); }
}
// Another type of container created by someone else:
class RandomList<T> {
private ArrayList<T> storage = new ArrayList<T>();
private Random rand = new Random();
public void insert(T item) { storage.add(item); }
public T select() {
return storage.get(rand.nextInt(storage.size()));
}
}
// Abstract composition adapter:
abstract class AddableAdapter<S, T> implements Addable<T> {
protected S seq;
public AddableAdapter(S sequence) { seq = sequence; }
public abstract void add(T item);
}
class Coffee {}
class Latte extends Coffee {}
class Mocha extends Coffee {}
class Cappucino extends Coffee {}
class Americano extends Coffee {}
class Breve extends Coffee {}
// Generate different types of Coffee:
class CoffeeGenerator implements Generator<Coffee> {
private Class[] types = { Latte.class, Mocha.class,
Cappucino.class, Americano.class, Breve.class, };
private Random rand = new Random();
public Coffee next() {
try {
return (Coffee)types[rand.nextInt(types.length)].newInstance();
// Report programmer errors at runtime:
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
class Fill2Test {
public static void main(String[] args) {
// Adapt a Collection:
List<Coffee> coffeeCarrier = new ArrayList<Coffee>();
Fill2.fill(new AddableCollectionAdapter<Coffee>(coffeeCarrier),
Coffee.class, 3);
// Helper method captures the type:
Fill2.fill(Adapter.collectionAdapter(coffeeCarrier),
Latte.class, 2);
for(Coffee c: coffeeCarrier)
System.out.println(c);
System.out.println("----------------------");
// Use an adapted class:
AddableSimpleQueue<Coffee> coffeeQueue =
new AddableSimpleQueue<Coffee>();
Fill2.fill(coffeeQueue, Mocha.class, 4);
Fill2.fill(coffeeQueue, Latte.class, 1);
for(Coffee c: coffeeQueue)
System.out.println(c);
System.out.println("++++++++++++++++++++++");
RandomList<Coffee> rl = new RandomList<Coffee>();
// An on-the-fly adapter via an anonymous class:
Addable<Coffee> ac =
new AddableAdapter<RandomList<Coffee>, Coffee>(rl) {
public void add(Coffee item) { seq.insert(item); }
};
Fill2.fill(ac, new CoffeeGenerator(), 7);
for(int i = 0; i < 10; i++)
System.out.println(rl.select());
}
}
Fill2 doesn't require a Collection like
Fill did. Instead, it only need something that implements
Addable, and Addable has been written just for
Fill it is a manifestation of the latent type that I had
wished the compiler would make for me.
In this version, I've also added an overloaded version of fill()
that takes a Generator rather than a class token. A
Generator is simply a class that produces a new object when you
call next(), but notice that it's type-safe at compile time: the
compiler ensures that you pass it a proper Generator, so no
exceptions can be thrown.
The first adapter, AddableCollectionAdapter, works with the base
type Collection, which means that any implementation of
Collection can be used. This version simply stores the
Collection reference and uses it to implement add().
If you have a specific type rather than the base class of a hierarchy, you
can write somewhat less code when creating your adapter by using inheritance as
you can see in AddableSimpleQueue.
To see the most interesting result of this code, I make a new kind of
container called RandomList that doesn't implement any interfaces
and is not part of any hierarchy. Let's assume it was created by someone else as
a library and the could not anticipate any of our needs.
Now I create something very interesting: AddableAdapter, which I
call an Abstract composition adapter. This is a general-purpose adapter
that works with some type S which has a way to add items to itself.
Notice two things about this code: S has no bounds, which
ordinarily means that we couldn't call anything except Object
methods on S. And what I do in the constructor for
AddableAdapter is in fact very safe and generic I simply
store the reference to the sequence.
The second thing to notice is that the add() method, which
satisfies the Addable<T> interface, is abstract,
and there's no definition. Remember that when we put it to use later.
In order to produce an interesting set of types to work with, I created a
Coffee hierarchy, and a CoffeeGenerator that produces
random types of Coffee when you call next().
In Fill2Test.main(), you can see the various types of adapters
at work. First, a Collection type is adapted with
AddableCollectionAdapter. A second version of this uses a generic
helper method, and you can see how the generic method is able to capture the
type without the programmer having to explicitly write it out this is a
convenient trick that produces more elegant code.
Next, the pre-adapted AddableSimpleQueue is used. Note that in
both cases the adapters allow the classes that previously didn't implement
Addable to be used with Fill2.fill()
Finally, a RandomList is created and adapted using an anonymous
class to produce an Addable<Coffee> that can then be used
with Fill2.fill().
Why is it surprising that this works? Look closely at the definition of the inner class:
new AddableAdapter<RandomList<Coffee>, Coffee>(rl) {
public void add(Coffee item) { seq.insert(item); }
};
The overridden add() method is calling insert(), a
specific, non Object method on seq, but
seq has no bound which says that insert() exists! We
can only assume that when the inner class is created, the compiler analyzes
and/or generates something (perhaps some kind of implicit bound) that allows it
to make the call to insert() without requiring explicit bounds. In
any event, this feature seems like it might allow some other interesting coding.
Using adapters like this would seem to compensate for the lack of latent
typing, and thus allow you to write genuinely generic code. However, it's an
extra step and something that must be understood both by the library creator and
the library consumer, and the concept may not be grasped as readily by less
experienced library consumers. By removing the extra step, latent typing makes
generic code easier to apply, and this is its value.
Coincidentally, Rüdiger Klaehn wrote me and pointed out that .NET also has
limitations because of the lack of latent typing. Even though C# does retain
type information, it loses C++'s straightforward latent typing it has to
forget some things about the code because .NET generics must be language-
neutral. Rüdiger has written a tool that automatically generates proxy classes
to produce the effect of latent typing. You can find details in the following
articles:
Daniel Bonniot (creator of the Nice language) gave another insight into the
difference between C++ templates and Java Generics:
Nice generics are closer to Java than to C++. To me, C++ templates serve two
purposes: enable generic types, and allow some kind of compile-time code
generation. I believe they should be handled separately (using templates to do
generics is inferior, because it leads to some errors to be detected later
than they could be). The second aspect is also useful, but I think that rather
calls for (hygenic) macros, in the spirit of Scheme. This is definitely
something that will be investigated for Nice at a later point.
This is particularly intriguing, because using the "code generation" aspect
of C++ templates, while powerful, is often confusing. If these really are two
different issues, then separating them and using a Scheme/Lisp style of
macros may add a great deal to the potential of programming with these
tools by making it much simpler. Although generics are already part of Nice,
macros will not be introduced until after version 1.0 (which should appear
within the next 6 months).
For those who argue that Java Generics are only about
Collection<T> and that using these is all the average
programmer needs to know, I put forward that even small tools similar to
Apply, FilledList, and Fill2 are useful
tools to generify if you go to the trouble to write them, why not write
them reusably?
My point in these articles has been to show that there are limits to Java
Generics, and if you understand the limits well enough, you can either figure
out how to work around them (as I did here, with adapters) or at least know when
to stop trying (and save yourself a lot of time and effort).
[1] Java could have a
syntax to support a compile-time equivalent of pointers to members, based on the
syntax that now produces class references at compile time. So you could not only
say Shape.class to produce the class reference, you could also say
Shape.class.rotate to produce the method reference at compile time.
Of course, there would also need to be a compile-time syntax to apply the method
reference.