M
malcolm.ryan
I've read a number of articles which highlight the reason why you can't
type cast generics in Java. The classic example is casting a
List<Integer> to List<Number> or vice versa. This can't be done because
you can add a Float to a List<Number> but not to a List<Integer>. I
understand this. But why does the analysis stop here? Sure, this is a
problem, but why was it never solved?
It seems to me that the problem lies in the mixing up of input and
output types. A List<Integer> has methods which input Integers and
methods which output Integers. You can't cast it to a List<Number>
because, while it output Numbers, it cannot input them.
So why don't we deliberately distinguish input and output types in our
definitions? Suppose we have a new notation T<+I, -O> which defines I
as an input type and O as an output type. Input types can only be used
for method parameters. Output types can only be used for return types,
eg:
interface List<+I, -O> {
public boolean add(I element);
public O get(int i);
public O set(int i, I element);
// etc
}
Now a List<Integer> becomes a List<+Integer, -Integer>. Which can
happily be cast to a List<+Integer, -Number>, because any Integer the
list outputs can be cast to a Number. So:
List<+Integer, -Integer> lii = new ArrayList<+Integer, -Integer>();
lii.add(new Integer(1));
List<+Integer, -Number> lin = lii;
Number n = lin.get(1); // this is legal
lin.add(new Float(3.14)) // this is illegal and would return a
// compilation error because input type
// Float does not match +Integer
Vice versa, you can cast a List<+Number,-Number> to a
List<+Integer,-Number> because any Integer you input can be be cast to
a Number. So:
List<+Number, -Number> lnn = new ArrayList<+Number, -Number>();
List<+Integer, -Number> lin = lnn;
lin.add(new Integer(1));
And then we could have a kind of "don't care" indicator if we never
want to use a particular mode (input or output). Eg:
List<+Integer, -Integer> lii = new ArrayList<+Integer, -Integer>();
List<_, -Number> lun = lii;
for (Number n : lun) {
System.out.println(n);
}
This would be very useful. Collections are pecular insofar as their
input and output types match. Even so, often you only want to read from
a collection and not write to it. In other cases you have a class that
only inputs or only outputs a given type. Casting these would be type
safe, but Java doesn't allow it because it is afraid of cases that
simply aren't there. For example:
interface Measurer<T> {
public float area(T thing);
}
If Circle extends Shape then there is no good reason why a
Measurer<Shape> should not be cast to a Measurer<Circle>.
In general T<+I,-O> can be safely cast to T<+J,-P> if J can be cast to
I, and O
can be cast to P. This analysis seems simple and straightforward to me.
Am I missing something? Is there an reason why it was not implemented
this way?
Malcolm
type cast generics in Java. The classic example is casting a
List<Integer> to List<Number> or vice versa. This can't be done because
you can add a Float to a List<Number> but not to a List<Integer>. I
understand this. But why does the analysis stop here? Sure, this is a
problem, but why was it never solved?
It seems to me that the problem lies in the mixing up of input and
output types. A List<Integer> has methods which input Integers and
methods which output Integers. You can't cast it to a List<Number>
because, while it output Numbers, it cannot input them.
So why don't we deliberately distinguish input and output types in our
definitions? Suppose we have a new notation T<+I, -O> which defines I
as an input type and O as an output type. Input types can only be used
for method parameters. Output types can only be used for return types,
eg:
interface List<+I, -O> {
public boolean add(I element);
public O get(int i);
public O set(int i, I element);
// etc
}
Now a List<Integer> becomes a List<+Integer, -Integer>. Which can
happily be cast to a List<+Integer, -Number>, because any Integer the
list outputs can be cast to a Number. So:
List<+Integer, -Integer> lii = new ArrayList<+Integer, -Integer>();
lii.add(new Integer(1));
List<+Integer, -Number> lin = lii;
Number n = lin.get(1); // this is legal
lin.add(new Float(3.14)) // this is illegal and would return a
// compilation error because input type
// Float does not match +Integer
Vice versa, you can cast a List<+Number,-Number> to a
List<+Integer,-Number> because any Integer you input can be be cast to
a Number. So:
List<+Number, -Number> lnn = new ArrayList<+Number, -Number>();
List<+Integer, -Number> lin = lnn;
lin.add(new Integer(1));
And then we could have a kind of "don't care" indicator if we never
want to use a particular mode (input or output). Eg:
List<+Integer, -Integer> lii = new ArrayList<+Integer, -Integer>();
List<_, -Number> lun = lii;
for (Number n : lun) {
System.out.println(n);
}
This would be very useful. Collections are pecular insofar as their
input and output types match. Even so, often you only want to read from
a collection and not write to it. In other cases you have a class that
only inputs or only outputs a given type. Casting these would be type
safe, but Java doesn't allow it because it is afraid of cases that
simply aren't there. For example:
interface Measurer<T> {
public float area(T thing);
}
If Circle extends Shape then there is no good reason why a
Measurer<Shape> should not be cast to a Measurer<Circle>.
In general T<+I,-O> can be safely cast to T<+J,-P> if J can be cast to
I, and O
can be cast to P. This analysis seems simple and straightforward to me.
Am I missing something? Is there an reason why it was not implemented
this way?
Malcolm