Forum Stats

  • 3,851,385 Users
  • 2,263,969 Discussions
  • 7,904,691 Comments

Discussions

Do generics add type safety to the language?

843793
843793 Member Posts: 41,732 Green Ribbon
edited May 2, 2003 12:29PM in Generics
I'm probably missing something, but I do not quite understand why generics are supposed to add type safety to the language.

Consider the following example:

public class Tuple<Elem> {
private Elem p1, p2;
...
public <T extends Tuple> Tuple(T other) {
try {
this.p1 = (Elem) other.p1;
}
catch (ClassClastException e) {
this.p1 = null;
}
... same for p2 ...
}
}

I am trying to cast a variable of type T to a variable of type Elem, where both T and Elem are type parameters. This cast never fails. In fact, the following compiles and runs although one would hope that it would fail:

Tuple<String> pairOfAliens =
new Tuple<String>("Dick","Doof");

Tuple<Exception> pairOfExceptions =
new Tuple<Exception>(pairOfAliens);

The demonstrated behavior is correct, because the type parameters are mapped to Object references. But what do generics buy me in that case? I don't see that generics add much to the language in terms of type safety, except in the simplest of all conceivable cases (like the proposed generic collections).

Angelika

«1

Comments

  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Hi,

    If you use

    public <T extends Tuple<Elem>> Tuple(T other)

    the line

    new Tuple<Exception>(pairOfAliens);

    will be rejected by the compiler.


    -- Dennis
  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Well, yes. But the constructor you suggest

    public<T extends Tuple<Elem>> Tuple(T other) { ... }

    has different semantics, different from what I was trying to do. It accepts a Tuple with fields of type Elem.

    My intent was to accept a Tuple that has fields of any type as long as the type of the fields is assignment-compatible to Elem. The cast was supposed to check whether the field types of the two Tuples are compatible.

    Quite obviously this level of genericity cannot be achieved with Java generics. Or am I missing anything?

    I would still have to resort to reflection in order to perform the desired type check.
  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Hi,

    I think

    public<T extends Elem> Tuple(Tuple<T> other) { ... }

    should work for you, i.e.

    new Tuple<Number>(new Tuple<Integer>(...));

    will compile,

    new Tuple<Number>(new Tuple<Exception>(...));

    will NOT compile.


    -- Dennis
  • 843793
    843793 Member Posts: 41,732 Green Ribbon

    I think

    public <T extends Elem> Tuple(Tuple<T> other) { ... }

    should work for you, i.e.

    new Tuple<Number>(new Tuple<Integer>(...));

    will compile,

    new Tuple<Number>(new Tuple<Exception>(...));

    will NOT compile.
    I don't think that a constructor

    <T extends Elem> Tuple(Tuple<T> other)

    will behave any differently from the constructor

    <T extends Tuple> Tuple(T other).

    If I understand you correctly, your Tuple class and
    its "generic" constructor would look like this:


    public class Tuple<Elem> {
    private Elem p1, p2;
    ...
    public <T extends Elem> Tuple(Tuple<T> other) {
    try {
    this.p1 = (Elem) other.p1;
    }
    catch (ClassCastException e) {
    this.p1 = null;
    }
    ... same for p2 ...
    }
    }

    The compiler will happily construct a Tuple<Number>
    from a Tuple<Exception>, as before in my original implementation.
    You will end up with a tuple of numbers that contains exceptions
    although the source code suggests that it would contain null references
    if the fields of this and other are incompatible.


    The described behavior of the code above is intended.
    At least, that's how I understand the spec.

    "Java generics are syntactic sugar that elides a couple of casts."
    as someone phrased it.

    And indeed, the cast in this.p1 = (Elem) other.p1 is elided.
    The assignement will be performed without any ClassCastException ever.
    The compiler removes the cast and treats both fields p1 of
    type reference to T and type reference to Elem as references to
    their erasure, which in this case is Object. Why would that ever fail?
    It doesn't.

    The surprise is actually in the source code, because it suggests that
    due to the cast a type check is performed at runtime while in reality
    no check is ever performed. In fairness one should add that the compiler
    issues a warning ("unchecked cast to type Elem"), but it is only a warning.

    There are two aspects that caught my attention when I started exploring
    Java generics and implemented the Tuple class:

    1. Why is the "unchecked cast" permitted at all? Wouldn't it be much more
    intutitive if the compiler issued an error instead of a warning in a case
    like the one above?

    2. Is it conceivable that "generics" is a misnomer? Java generics have
    little to do with genericity. Seemingly you cannot implement a generic
    constructor for a generic class using Java generics. That's not quite
    what I had expected of Java generics.
  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Hi,

    did you actualy check out what the compiler is doing? ;)

    The class will look like this:

    public class Tuple<Elem>
    {
    private Elem p1, p2;

    public Tuple(Elem p1, Elem p2)
    {
    this.p1 = p1;
    this.p2 = p2;
    }

    public <T extends Elem> Tuple(Tuple<T> other)
    {
    p1 = other.p1; // This is type safe!!!
    p2 = other.p2;
    }

    public static void main(String[] args)
    {
    new Tuple<Number>(new Tuple<Exception>(new Exception(), new Exception())); // Compiler Error!
    new Tuple<Number>(new Tuple<Integer>(new Integer(1), new Integer(2))); // NO Error
    }
    }

    You don't need to catch any ClassCastException inside the constructor with this signature, since the compiler will reject illegal calls.

    With your original signature:

    <T extends Tuple> Tuple(T other).

    you'll need the ClassCastException because the compiler will not reject odd calls. This is becaus you are using Tuple as a raw type.


    -- Dennis
  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Hi,

    did you actualy check out what the compiler is doing?
    ;)
    I thought I did - with an equivalent Pair class. It's just that the "equivalent" class wasn't equivalent enough. ;-)

    With your original signature:

    <T extends Tuple> Tuple(T other).

    you'll need the ClassCastException because the
    compiler will not reject odd calls. This is becaus you
    are using Tuple as a raw type.
    Yes, I would need the ClassCastException, but to my very disappointment it is never raised. I understand why, but I still find it counter-intuitive. I would expect that the code does not compile. After all the resulting class implementation is extremely misleading. I would hope that stuff like my initial implementation cannot be written in Java.

    Basically I don't understand the rationale behind the "unchecked cast". Why is it permitted?


  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Basically I don't understand the rationale behind the
    "unchecked cast". Why is it permitted?
    You cannot cast to a type that is not known at compile time. Therefore, there is no way the compiler can insert a cast to Elem. Therefore, the cast is unchecked.

    Simply don't write the cast -- you're casting an Elem to an Elem, which is not needed.
  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Basically I don't understand the rationale behind the
    "unchecked cast". Why is it permitted?
    You cannot cast to a type that is not known at compile
    time. Therefore, there is no way the compiler can
    insert a cast to Elem. Therefore, the cast is
    unchecked.

    Simply don't write the cast -- you're casting an Elem
    to an Elem, which is not needed.
    No, now I'm entirely confused. And I was so close to believing that I'm gonna understand this ... :-)


    The point is that I am NOT casting an Elem to an Elem although the source code suggests it. I am not casting at all (despite of the cast in the source code) and if I were casting it would be a cast from an Object to an Object.


    Here is the my original example, in a more condensed form:

    public class GenericWrapper<Elem> {
    private Elem theObject;
    public GenericWrapper(Elem arg) {
    theObject = arg;
    }
    public <T extends GenericWrapper> GenericWrapper (T other) {
    this.theObject = other.theObject;
    }
    public static void main(String[] args) {

    GenericWrapper<String> wrappedString =
    new GenericWrapper<String>("abc");

    GenericWrapper<Exception> wrappedException =
    new GenericWrapper<Exception>(wrappedString);
    }
    }


    The assignment

    this.theObject = other.theObject;

    does not compile, because the left hand side is of "type" Elem (which is a "placeholder" for an unknown type) and the right hand side is of another unknown type (treated as if of type Object, interestingly).

    I need to insert a cast; otherwise it does not compile. After insertion of the required cast the generic constructor looks like this:

    public <T extends GenericWrapper> GenericWrapper (T other) {
    this.theObject = (Elem)other.theObject;
    }


    Now, if you insert the cast and try to catch the potentially thrown ClassCastException you find that the cast is elided altogether and the exception is never raised. Hence this piece of code is correct, yet misleading:

    public <T extends GenericWrapper> GenericWrapper (T other) {
    try { this.theObject = (Elem)other.theObject; }
    catch (ClassCastException e) { System.err.println(e); }
    }

    The cast is required, but elided in the bytecode. Personally, I find this behavior of generics neither intuitive nor understandable. And I am wondering why such code would be permitted.


    Getting back to your advice: "Simply don't write the cast --" The prototype compiler requires the cast and you cannot leave it out. Is there anything in the spec that makes you believe the cast would not be needed?

    I kind of agree with you. Reading the spec I would think that both the right hand side expression and the left hand side expression of the assignment are of the same type after translation of the expressions to their erasures. But that's seemingly a misconception from my side; the compiler translates them to "Elem" and "Object" and complains about a type mismatch.

  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Hi,

    here's the type safe version of your sample prog:

    public class GenericWrapper<Elem> {
    private Elem theObject;
    public GenericWrapper(Elem arg) {
    theObject = arg;
    }
    public <T extends Elem> GenericWrapper (GenericWrapper<T> other) {
    this.theObject = other.theObject;
    }
    public static void main(String[] args) {

    GenericWrapper<String> wrappedString =
    new GenericWrapper<String>("abc");

    GenericWrapper<Exception> wrappedException =
    new GenericWrapper<Exception>(wrappedString);
    }
    }


    Again, the trick is to change the signature of the constructor so that the compiler can reject illegal calls. Thus the cast is no longer required.

    -- Dennis
  • 843793
    843793 Member Posts: 41,732 Green Ribbon
    Does that prevent anyone from building classes like the one I provided?

    Or, to phrase it differently: what are raw types for? what is their semantic meaning? what is their purpose in the language? why can I cast between two unknown types? what's the rationale?
This discussion has been closed.