0

I have the following generic test class:

public class BrokenGenerics<T> {

    private T[] genericTypeArray;

    public BrokenGenerics(T... initArray) {
        genericTypeArray = initArray;
    }

    public void setArray(T[] newArray) {
        genericTypeArray = newArray;
    }

    public T get(int idx) {
        return genericTypeArray[idx];
    }

    public Class getType() {
        return genericTypeArray.getClass().getComponentType();
    }



    public static boolean breakThis(BrokenGenerics any) {
        any.setArray(new B[]{new B(2)});
        return false;
    }

    public static void main(String[] args) {
        BrokenGenerics<A> aBreaker = new BrokenGenerics<A>(new A("1"));
        System.out.println(aBreaker.get(0));
        System.out.println(aBreaker.getType());
        breakThis(aBreaker);
        System.out.println(aBreaker.get(0));
        System.out.println(aBreaker.getType());
    }

    private static class A {

        public String val;

        public A(String init) {
            val = init;
        }

        @Override
        public String toString() {
            return "A value: " + val;
        }
    }

    private static class B {

        public int val;

        public B(int init) {
            val = init;
        }

        @Override
        public String toString() {
            return "B value: " + val;
        }
    }
}

When I run it, I get this output, and no errors:

A value: 1
class BrokenGenerics$A
B value: 2
class BrokenGenerics$B

Now, I understand why this compiles; it can't know at compile-time that breakThis is being passed a generic of a bad type. However, once it runs the line any.setArray(new B[]{new B(2)});, shouldn't it throw a ClassCastException (NOTE THAT IT DOES NOT! Try it yourself!) because I'm trying to pass a B[] to a method that expects an A[]? And after that, why does it allow me to get() back the B?

Ben Leggiero
  • 25,904
  • 38
  • 161
  • 267
  • I'm sure the compiler issues a warning. It can't do much else, since `T` is `Object` during runtime. – Kayaman Nov 23 '15 at 14:56
  • @Kayaman the compiler also doesn't issue any warnings. I get a green on my code! No warnings or errors at compile or runtime. – Ben Leggiero Nov 23 '15 at 16:23
  • 2
    @BenC.R.Leggiero It's impossible that you're not getting any warnings. At the very least, you should get a raw type warning for `public static boolean breakThis(BrokenGenerics any) {`. – Powerlord Nov 23 '15 at 16:33
  • @Powerlord have you put it into your IDE and gotten a warning? Perhaps my IDE just doesn't show me them. – Ben Leggiero Nov 23 '15 at 16:34
  • @SoltiriosDelimanolis How is this a duplicate? Can you explain to me? – Ben Leggiero Nov 23 '15 at 16:34
  • @Makoto Please re-read my question. My problem is that I am _**NOT**_ getting a `ClassCastException`. For instance, I say "Why does the JVM _**allow**_ me to pass [...]", and I ask "_**shouldn't it**_ throw a `ClassCastException` [...]?" – Ben Leggiero Nov 23 '15 at 17:36
  • 1
    Okay. Fair point. I'm not as convinced of the duplicality now either. You'd get a `ClassCastException` if you were *adding* elements of different types together, but in this case you're completely blowing away the array. – Makoto Nov 23 '15 at 17:38
  • Concerning _because I'm trying to pass a `B[]` to a method that expects an `A[]`_, the duplicate does address this. The method isn't expecting an `A[]` since it's the generic parameter is erased, since the type of the target reference expression is raw. – Sotirios Delimanolis Nov 23 '15 at 18:07
  • @SotiriosDelimanolis Granted, but even after knowing that type-erasure allows this to be compiled, I was still perplexed that nothing stopped it from running. – Ben Leggiero Nov 23 '15 at 18:14
  • Ironically enough, the answer you've accepted provides nothing new. Everything it explains was already in the canonical duplicate. – Sotirios Delimanolis Nov 23 '15 at 18:15
  • Your second question _why does it allow me to `get()` back the `B`_ might not be in the duplicate, but it can be inferred. The accepted answer here doesn't address that either. – Sotirios Delimanolis Nov 23 '15 at 18:16

2 Answers2

3

After Type Erasure, T will be turned into Object since you didn't specify a bound on T. So, there is no problem at runtime assigning any type of array to genericTypeArray, which is now of type Object[] or calling the function setArray(...), which now also accepts an argument of type Object[]. Also, your get(...) method will simply return an Object.

Trouble starts when you access elements in the array with a wrong type expectation, since this might lead to (implicit or explicit) illegal type casts, for example by assigning the value returned by get(...) to a variable of type A.

You can also get a run-time ClassCastException if you try to type-cast the array itself, but, in my experience, that is a case that tends to come up less often, although it can be very obscure to find or even understand if it does happen. You can find some examples below.

All generics-checking happens only at compile-time. And if you use raw types, these checks can not be performed rigorously, and thus the best the compiler can do is to issue a warning to let you know that you are giving up an opportunity for more meaningful checks by omitting the type argument.

Eclipse with its standard settings (and probably the java compiler with the correct flags) shows these warnings for your code:

  • "Class is a raw type" where you define getType() (somewhat unrelated to your question)
  • "BrokenGenerics is a raw type" where you define breakThis(...)
  • "Type safety: The method setArray(Object[]) belongs to the raw type BrokenGenerics" where you call setArray(...) inside breakThis(...).

Examples for causing ClassCastException due to illegal type-cast of the array:

You can get ClassCastExceptions at runtime if you expose the array to the outside world (which can often be a dangerous thing to do, so I try to avoid it) by adding the following to BrokenGenerics<T>:

public T[] getArray() {
    return genericTypeArray;
}

If you then change your main method to:

BrokenGenerics<A> aBreaker = new BrokenGenerics<A>(new A("1"));
A[] array = aBreaker.getArray();
System.out.println(array[0]);
System.out.println(aBreaker.getType());
breakThis(aBreaker);
array = aBreaker.getArray(); // ClassCastException here!
System.out.println(array[0]);
System.out.println(aBreaker.getType());

You get the ClassCastException at runtime at the indicated position due to a cast of the array itself rather than one of its elements.

The same thing can also happen if you set the variable genericTypeArray to protected and use it from code that subclasses your generic class with a fixed type argument:

private static class C extends BrokenGenerics<A> {

    public C(A... initArray) {
        super(initArray);
    }

    public void printFirst() {
        A[] result = genericTypeArray; // ClassCastException here!
        System.out.println(result[0]);
    }
}

To trigger the exception, add the following to you main method:

C cBreaker = new C(new A("1"));
cBreaker.printFirst();
breakThis(cBreaker);
cBreaker.printFirst();

Imagine this case coming up in a bigger project... How on earth would you even begin to understand how that line of code could possible fail?!? :) Especially since the stack trace might be of very little help trying to find the breakThis(...) call that is actually responsible for the error.

For more in-depth example cases, you can take a look at some tests I did a little while back.

Community
  • 1
  • 1
Markus A.
  • 11,761
  • 8
  • 44
  • 102
  • @BenC.R.Leggiero It's not that Java arrays are abstracted from the type they hold, it's that Generics are abstracted from the type they hold. – Powerlord Nov 23 '15 at 18:14
  • 2
    @BenC.R.Leggiero I'm still learning new things about the intricate details of generics every day. It's quite amazing how many strange implications can arise from the erasure approach. Here's something I learned from the generics-ninjas here on SO, which might also be interesting to you: http://stackoverflow.com/questions/17831896/creating-generic-array-in-java-via-unchecked-type-cast – Markus A. Nov 23 '15 at 18:17
  • On a side note, as a general rule Collections (`Collection`, `List`, `Set`, `Map`) work better with generics than Arrays do. – Powerlord Nov 23 '15 at 18:24
  • @Powerlord but don't those have internal arrays powering them? – Ben Leggiero Nov 23 '15 at 18:28
  • 1
    @BenC.R.Leggiero They do, and, in fact, by accessing Collections with raw types, you can blow them up just like you can with your code example. :) – Markus A. Nov 23 '15 at 19:02
  • 1
    The reification that arrays have can be... tricky to work around in generics. Joshua Bloch talks about this a bit in Effective Java: Second Edition, pg 119. This is part of the [free chapter of the book posted online](http://www.eecs.qmul.ac.uk/~mmh/APD/bloch/generics.pdf). Which happens to be the chapter on Generics. It has some other interesting stuff about Generics as well. – Powerlord Nov 23 '15 at 19:46
2

shouldn't it throw a ClassCastException because I'm trying to pass a B[] to a method that expects an A[]?

No. As this post explains, your invocation of setArray in

public static boolean breakThis(BrokenGenerics any) {
    any.setArray(new B[]{new B(2)});
    return false;
}

is done on a reference expression of the raw type BrokenGenerics. When interacting with raw types, all corresponding generic parameters are erased. So setArray is actually expecting a Object[]. A B[] is a Object[].

why does it allow me to get() back the B?

Assuming you're asking about this

System.out.println(aBreaker.get(0));

PrintStream#println(Object) expects an Object, not an A. As such, there is no reason for the compiler to insert a cast here. Since there is no cast, there is no ClassCastException.

If you had instead done

A a = aBreaker.get(0);

or had a method like

void println(A a) {}
...
println(aBreaker.get(0));

then these would cause ClassCastException. In other words, the compiler will insert a cast (checkcast) anywhere a type needs to be converted from a generic type parameter. That was not the case with PrintStream#println.

Similarly,

System.out.println(aBreaker.getType());

doesn't even involve the generic parameter declared in BrokenGenerics

public Class getType() {...}

and also returns a value of the raw type Class. The compiler has no reason to add a checkcast to A.

Community
  • 1
  • 1
Sotirios Delimanolis
  • 252,278
  • 54
  • 635
  • 683
  • I suppose I misworded my second question. I was more interested why the line `return genericTypeArray[idx];` ran just fine. `get()` returns a `T`, right? And at runtime, doesn't it know that when I call `get()`, I want an `A` back, but it gave me a `B`? That said, I think I understand, now, that it just abstracts to being the supertype of `T` (`Object`), and that's why it works. – Ben Leggiero Nov 23 '15 at 18:41