shouldn't this code produce a ClassCastException
Asked Answered
T

5

8

The following code compiles and runs successfully without any exception

import java.util.ArrayList;

class SuperSample {}

class Sample extends SuperSample {

  @SuppressWarnings("unchecked")
  public static void main(String[] args) {

    try {

      ArrayList<Sample> sList = new ArrayList<Sample>();
      Object o = sList;
      ArrayList<SuperSample> ssList = (ArrayList<SuperSample>)o;
      ssList.add(new SuperSample());

    } catch (Exception e) {
      e.printStackTrace();
    }

  }
}

shouldn't the line ArrayList<SuperSample> ssList = (ArrayList<SuperSample>)o; produce a ClassCastException ?

while the following code produces a compile time error error to prevent heap pollution, shouldn't the code mentioned above hold a similar prevention at runtime?

ArrayList<Sample> sList = new ArrayList<Sample>();
ArrayList<SuperSample> ssList = (ArrayList<SuperSample>) sList;

EDIT:

If Type Erasure is the reason behind this, shouldn't there be additional mechanisms to prevent an invalid object from being added to the List? for instance

String[] iArray = new String[5];
Object[] iObject = iArray;
iObject[0]= 5.5;  // throws ArrayStoreException

then why,

ssList.add(new SuperSample());

is not made to throw any Exception?

Theophilus answered 22/11, 2013 at 11:16 Comment(5)
I deleted my answer because I felt that it didn't answer your question fully and I didn't want a semi-right or a semi-wrong answer to be here. I would thought say that it might not be directly possible to cast it(in reference to your last comment on my answer), but you can do something like this ArrayList<SuperSample> ssList = (ArrayList<SuperSample>) ((Object)sList); and this works. I feel a more experienced user could answer your query properly. Do consider adding that to your question. You've asked a great question today(after a long time I'm seeing one).Intuit
thx for your response..:) this question is bugging me from the morning..Theophilus
Are such templates ever castable directly into each other, without casting up to an object reference first?Meditate
@Meditate : no.. there will be a compile-time errorTheophilus
I updated my answer to explain this ArrayStoreException for arrays.Sauerkraut
S
1

In your code example,

    class SuperSample { }
    class Sample extends SuperSample { }
    ...
    ArrayList<Sample> sList = new ArrayList<Sample>();
    Object o = sList;
    ArrayList<SuperSample> ssList = (ArrayList<SuperSample>)o;

Shouldn't the last line produce a ClassCastException?

No. That exception is thrown by the JVM when it detects incompatible types being cast at runtime. As others have noted, this is because of erasure of generic types. That is, generic types are known only to the compiler. At the JVM level, the variables are all of type ArrayList (the generics having been erased) so there is no ClassCastException at runtime.

As an aside, instead of assigning to an intermediate local variable of type Object, a more concise way to do this assignment is to cast through raw:

    ArrayList<SuperSample> ssList = (ArrayList)sList;

where a "raw" type is the erased version of a generic type.

Shouldn't there be additional mechanisms to prevent an invalid object from being added to the List?

Yes, there are. The first mechanism is compile-time checking. In your own answer you found the right location in the Java Language Specification where it describes heap pollution which is the term for an invalid object occurring in the list. The money quote from that section, way down at the bottom, is

If no operation that requires a compile-time unchecked warning to be issued takes place, and no unsafe aliasing occurs of array variables with non-reifiable element types, then heap pollution cannot occur.

So the mechanism you're looking for is in the compiler, and the compiler notifies you of this via compilation warnings. However, you've disabled this mechanism by using the @SuppressWarnings annotation. If you were to remove this annotation, you'd get a compiler warning at the offending line. If you absolutely want to prevent heap pollution, don't use @SuppressWarnings, and add the options -Xlint:unchecked -Werror to your javac command line.

The second mechanism is runtime checking, which requires use of one of the checked wrappers. Replace the initialization of sList with the following:

    List<Sample> sList = Collections.checkedList(new ArrayList<Sample>(), Sample.class);

This will cause a ClassCastException to be thrown at the point where a SuperSample is added to the list.

Stairway answered 23/11, 2013 at 8:5 Comment(1)
served the question right the way it should. i posted my own answer after i went through the JLS. But your answer is way more clearer than my extracted quote. +1 for Collection.checkedListTheophilus
C
6

No it should not, at run time both lists have the same type ArrayList. This is called erasure. Generic parameters are not part of compiled class, they all are erased during compilation. From JVM's perspective your code is equal to:

public static void main(String[] args) {
    try {
      ArrayList sList = new ArrayList();
      Object o = sList;
      ArrayList ssList = (ArrayList)o;
      ssList.add(new SuperSample());
    } catch (Exception e) {
      e.printStackTrace();
    }
}

Basically generics only simplify development, by producing compile time errors and warnings, but they don't affect execution at all.

EDIT:

Well, the base concept behind this is Reifiable Type. Id strongly recomend reading this manual:

A reifiable type is a type whose type information is fully available at runtime. This includes primitives, non-generic types, raw types, and invocations of unbound wildcards.

Non-reifiable types are types where information has been removed at compile-time by type erasure

To be short: arrays are rifiable and generic collections are not. So when you store smth in the array, type is checked by JVM, because array's type is present at runtime. Array represents just a piece of memmory, while collection is an ordinary class, which might have any sort of implementation. For example it can store data in db or on the disk under the hood. If you'd like to get deeper, I suggest reading Java Generics and Collections book.

Claustrophobia answered 22/11, 2013 at 12:24 Comment(2)
thx for the response.. your explanation is satisfactory. please see my question, i have edited it, would you elaborate on that too.? i am very curious on knowing this..Theophilus
thx for the edit.. and i just ordered java generics and collections yesterday.. :)Theophilus
J
1

The key here to answer your question is Type Erasure in java

You have a warning at compile time for your first case and not in the second because of your indirection by an object which prevent the compiler to raise you a warning (I'm guessing that this warning is raised when casting a parametrized type to another one which is not done on your second case, if anyone can confirm that I would be glad to here about it). And your code run because, in the end sList ssList et o are all ArrayList

Julius answered 22/11, 2013 at 12:26 Comment(1)
thank you for the response. i have edited my question. i would be glad if you could check it out and answer my edited question.Theophilus
S
1

I think that this cant produce ClassCastException because of backward compatibility issue in Java.

Generic information is not included in bytecode (compiler get rids of it during compilation).

Imagine scenario that you use in your project some old legacy code (some old library writen in java 1.4) and you pass generic List to some method in this legacy code. You can do this.

In time before generics legacy code was allowed to put anything at all (except primitives) into a collection. So this legacy code cant get ClassCastException even if it try to put String to List<Integer>. From the legacy code perspective it is just List.

So this strange behaviour is a consequence of type erasure and to allow backward compatibility in Java.

EDIT:

You get ArrayStoreException for arrays because at runtime the JVM KNOWS the type of arrays, and you dont get any exception for collections because of type erasure and this backward compatibility issue JVM doesnt know the type of collection at runtime.

You can read about this topic in "SCJP Sun® Certified Programmer for Java™ 6 Study Guide" book in chapter 7 "Generics and Collections"

Sauerkraut answered 22/11, 2013 at 12:41 Comment(2)
thx for the response. i understand the concept behind Type Erasure and compatibility with legacy code, but still no reason to allow a heap pollution right..? would you check my edited question an answer? it would be very helpful if you could.Theophilus
thx for the answer.. :) i just now referred the JLS and it seems this scenario is inevitable... +1 for your response..Theophilus
S
1

In your code example,

    class SuperSample { }
    class Sample extends SuperSample { }
    ...
    ArrayList<Sample> sList = new ArrayList<Sample>();
    Object o = sList;
    ArrayList<SuperSample> ssList = (ArrayList<SuperSample>)o;

Shouldn't the last line produce a ClassCastException?

No. That exception is thrown by the JVM when it detects incompatible types being cast at runtime. As others have noted, this is because of erasure of generic types. That is, generic types are known only to the compiler. At the JVM level, the variables are all of type ArrayList (the generics having been erased) so there is no ClassCastException at runtime.

As an aside, instead of assigning to an intermediate local variable of type Object, a more concise way to do this assignment is to cast through raw:

    ArrayList<SuperSample> ssList = (ArrayList)sList;

where a "raw" type is the erased version of a generic type.

Shouldn't there be additional mechanisms to prevent an invalid object from being added to the List?

Yes, there are. The first mechanism is compile-time checking. In your own answer you found the right location in the Java Language Specification where it describes heap pollution which is the term for an invalid object occurring in the list. The money quote from that section, way down at the bottom, is

If no operation that requires a compile-time unchecked warning to be issued takes place, and no unsafe aliasing occurs of array variables with non-reifiable element types, then heap pollution cannot occur.

So the mechanism you're looking for is in the compiler, and the compiler notifies you of this via compilation warnings. However, you've disabled this mechanism by using the @SuppressWarnings annotation. If you were to remove this annotation, you'd get a compiler warning at the offending line. If you absolutely want to prevent heap pollution, don't use @SuppressWarnings, and add the options -Xlint:unchecked -Werror to your javac command line.

The second mechanism is runtime checking, which requires use of one of the checked wrappers. Replace the initialization of sList with the following:

    List<Sample> sList = Collections.checkedList(new ArrayList<Sample>(), Sample.class);

This will cause a ClassCastException to be thrown at the point where a SuperSample is added to the list.

Stairway answered 23/11, 2013 at 8:5 Comment(1)
served the question right the way it should. i posted my own answer after i went through the JLS. But your answer is way more clearer than my extracted quote. +1 for Collection.checkedListTheophilus
T
0

From the JLS (4.12.2)

It is possible that a variable of a parameterized type refers to an object that is not of that parameterized type. This situation is known as heap pollution. This situation can only occur if the program performed some operation that would give rise to an unchecked warning at compile-time.

For example, the code:

List l = new ArrayList<Number>();
List<String> ls = l; // unchecked warning

gives rise to an unchecked warning, because it is not possible to ascertain, either at compile- time (within the limits of the compile-time type checking rules) or at run-time, whether the variable l does indeed refer to a List<String>. If the code above is executed, heap pollution arises, as the variable ls, declared to be a List<String>, refers to a value that is not in fact a List<String>. The problem cannot be identified at run-time because type variables are not reified, and thus instances do not carry any information at run-time regarding the actual type parameters used to create them.

Theophilus answered 22/11, 2013 at 19:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.