Search code examples
javagenericscompatibilitylegacy-code

Compatibility of Generics with legacy code - why does the foreach blow at runtime while iterator works fine


I have the following test code. I'm trying to understand the interoperability between Generics and legacy.

List myList = new ArrayList();
myList.add("abc");
myList.add(1);
myList.add(new Object());

System.out.println("Printing the unchecked list");
Iterator iterator = myList.iterator();
while (iterator.hasNext()) {
  System.out.println(iterator.next());
}

List<String> strings = myList;

System.out.println("Printing the unchecked list assigned to List<String> using iterator");
Iterator stringIterator = strings.iterator();
while (stringIterator.hasNext()) {
  System.out.println(stringIterator.next());  // this works fine! why? shouldn't it fail converting the 1 (int/Integer) to String?
}

System.out.println("Printing the unchecked list assigned to List<String> using for");
for (int i = 0; i != strings.size(); i++) {
  System.out.println(strings.get(i));  // blows up as expected in the second element, why?
}

System.out.println("Printing the unchecked list assigned to List<String> using foreach");
for (String s : strings) {
  System.out.println(s);  // blows up as expected in the second element, why?
}

Why does the iterator.next work fine when I try to print it while the System.out.println blows up as expected when I iterate using for loops?


Solution

  • The key thing to remember about generics is that it's simply a way to omit casts from the source code. The casts are inserted by the compiler. So, code fails (or does not fail) because of the presence (or absence) of a cast.

    1. this works fine! why?

    Because stringIterator is raw, stringIterator.next() isn't cast to anything: it's just read as an Object, which is the erased return type.

    1. blows up as expected in the second element, why?

    strings is a List<String>, so the result of strings.get(i) is assumed to be String, and println(String) is chosen to be invoked, rather than println(Object). As such, a cast is inserted. strings.get(1) isn't a String, so this fails with a ClassCastException.

    Interestingly, had you been trying this with a List<Integer>, this wouldn't have failed, because println(Object) would be invoked, and no cast would then have been necessary.

    1. blows up as expected in the second element, why?

    Because there's a cast to String inserted, to assign the element to String s.

    Refer to JLS 14.14.2 for the desugared form of the enhanced for loop:

    for (I #i = Expression.iterator(); #i.hasNext(); ) {
        {VariableModifier} TargetType Identifier =
            (TargetType) #i.next();
        Statement
    }
    

    So, your code is equivalent to:

    for (Iterator<String> it = strings.iterator(); it.hasNext(); ) {
      String s = (String) it.next();  // Actually, your code fails on this line.
      System.out.println(s);
    }