tags:

views:

78

answers:

3

Hi there,

i found a (for my state of knowledge) strange behavior during the adding of a Class type to a list.

I have a list which holds all implementing classes of an Abstract class List<Class<MyAbstractClass>> myImplementations. I added a type of a non-derived class and there was no error. Can anyone explain why i can do something like myImplementations.add(SomeOtherClass.class); without any exception? It seems that the second generic type (MyAbstractClass) has no effect at all.

--- edit ---

public abstract class MyAbstractClass{
  public static String getMyIdentification(){ throw new RuntimeException("implement method");}
}

public class MyImplementation extends MyAbstractClass{
  public static String getMyIdentification(){ return "SomeUUID";}
}

public class OtherClass{}

// in another class:
List<Class<MyAbstractClass>> myImplementations = new ArrayList<Class<MyAbstractClass>>();
myImplementations.add(MyImplementation.class); // does not cause any error
myImplementations.add(OtherClass.class); // does not cause any error, but should in my opinion??

---- edit end ---

Thank you, el

+3  A: 

The type is erased during compilation, so you won't see any exception at runtime. The compiler should complain in your case or give a warning.

List<Class<String>> list = new ArrayList<Class<String>>();
list.add(Integer.class);                      // should create a compiletime error
list.add(Class.forName("java.lang.Integer")); // should create a warning
                                              // and run due to type erasure

The type parameter Class<String> is erased during compilation - it is only used by the compiler to check if the java source code is valid. The compiled bytecode doesn't contain this information anymore, on byte code level the list will hold and accept Object or any subclass of Object. And because Integer.class is a subclass of Object, the code will run - until the runtime throws ClassCastExceptions at the programmer, just because it expected Class<String> instances.

Andreas_D
+1  A: 

This behaves as expected, using eclipse compiler:

List<Class<? extends CharSequence>> myImplementations = 
    new ArrayList<Class<? extends CharSequence>>();
myImplementations.add(String.class);
myImplementations.add(Vector.class);

i.e. the compiler complains only for the second add. If it passes compilation, however, the list is transformed into a raw list, and you won't get an exception until you get elements out of the list - using the foreach loop, for example.

without the ? extends the compilation fails even for String. And that's how it should be. I'm surprised that you don't have any errors, since java generics are invariant - i.e. you cannot add a Subclass instance to a List<Superclass>.

Bozho
yep! this is a solution for me, but does not explain why the second type has no effect. Thanks anyway!
elCapitano
@Bozho - I suspect he has turned off those "pesky" unsafe conversion, etc warning messages.
Stephen C
"don't touch the defaults unless you know what you are doing" is a very sensible policy :)
Bozho
A: 

On type erasure

Java's generics are non-reified. A List<String> and a List<Integer> are different types at compile time, but both types are erased to simply List at run-time. This means that by circumventing the compile-time checks, you can insert an Integer into List<String> at run-time, which by itself may not generate ClassCastException. Here's an example:

List<String> names = new ArrayList<String>();
List raw = names; // generates compiler warning about raw type!

raw.add((Integer) 42); // does not throw ClassCastException! (yet!)

// but here comes trouble!
for (String s : names) {
    // Exception in thread "main" java.lang.ClassCastException:
    //    java.lang.Integer cannot be cast to java.lang.String
}

Note that you'd have to deliberately circumvent the compile-time check to violate the generic type invariant: the compiler will do its best to ensure that List<String> will indeed contain only String, and will generate as many warnings and errors as necessary to enforce this.


On checked collections

Sometimes we want to enforce the type safety at run-time. For most scenarios, the checked collection wrappers from java.util.Collections can facilitate this behavior. From the documentation:

<E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)

Returns a dynamically typesafe view of the specified collection. Any attempt to insert an element of the wrong type will result in an immediate ClassCastException. Assuming a collection contains no incorrectly typed elements prior to the time a dynamically typesafe view is generated, and that all subsequent access to the collection takes place through the view, it is guaranteed that the collection cannot contain an incorrectly typed element.

The generics mechanism in the language provides compile-time (static) type checking, but it is possible to defeat this mechanism with unchecked casts. Usually this is not a problem, as the compiler issues warnings on all such unchecked operations. There are, however, times when static type checking alone is not sufficient. For example, suppose a collection is passed to a third-party library and it is imperative that the library code not corrupt the collection by inserting an element of the wrong type.

Another use of dynamically typesafe views is debugging. Suppose a program fails with a ClassCastException, indicating that an incorrectly typed element was put into a parameterized collection. Unfortunately, the exception can occur at any time after the erroneous element is inserted, so it typically provides little or no information as to the real source of the problem. If the problem is reproducible, one can quickly determine its source by temporarily modifying the program to wrap the collection with a dynamically typesafe view. For example, this declaration:

    Collection<String> c = new HashSet<String>();     

may be replaced temporarily by this one:

    Collection<String> c = Collections.checkedCollection(
         new HashSet<String>(), String.class);

Running the program again will cause it to fail at the point where an incorrectly typed element is inserted into the collection, clearly identifying the source of the problem.

Here's a modification of the previous snippet:

List<String> names = Collections.checkedList(
    new ArrayList<String>(), String.class
);
List raw = names; // generates compiler warning about raw type!

raw.add((Integer) 42); // throws ClassCastException!
// Attempt to insert class java.lang.Integer element into collection
// with element type class java.lang.String

Note that as a "bonus", a Collections.checkedList will throw NullPointerException at run-time on an attempt to add(null).


On Class.isAssignableFrom

Unfortunately the behavior we want in this scenario is not supported by a Collections.checkedList: you can use it to ensure that only java.lang.Class instances can be added at run-time, but it will not ensure that the given Class object is a subclass of another Class.

Fortunately the Class.isAssignableFrom(Class) method allows you to make this check, but you'd have to write your own checked List wrapper to enforce this. The idea is illustrated here in a static helper method instead of a full List implementation:

static void add(List<Class<?>> list, Class<?> base, Class<?> child) {
    if (base.isAssignableFrom(child)) {
        list.add(child);
    } else {
        throw new IllegalArgumentException(
            String.format("%s is not assignable from %s",
                base.getName(),
                child.getName()
            )
        );
    }
}

Now we have:

List<Class<?>> list = new ArrayList<Class<?>>();

add(list, CharSequence.class, String.class);         // OK!
add(list, CharSequence.class, StringBuffer.class);   // OK!
add(list, CharSequence.class, StringBuilder.class);  // OK!

add(list, CharSequence.class, Integer.class);        // NOT OK!
// throws IllegalArgumentException:
// java.lang.CharSequence is not assignable from java.lang.Integer
polygenelubricants