9

So as the title implies my question is a bit odd and complicated. I know what I'm about to do breaks all the rules of "good" programming practices but hey, what's life if we don't live a little?

So what I did was create the following program. (Note this was part of a larger experiment to really try and understand generics so some of the function names maybe a bit out of order)

import java.util.*;

public class GenericTestsClean 
{
    public static void test2()
    {
        BigCage<Animal> animalCage=new BigCage<Animal>();
        BigCage<Dog> dogCage=new BigCage<Dog>();
        dogCage.add(new Dog());
        animalCage.add(new Cat());
        animalCage.add(new Dog());
        animalCage.printList(dogCage);
        animalCage.printList(animalCage);
    }


    public static void main(String [] args)
    {
        //What will this print
        System.out.println("\nTest 2");
        test2();
    }

}

class BigCage<T> extends Cage<T>
{

    public static <U extends Dog> void printList(List<U> list)
    {
        System.out.println("*************"+list.getClass().toString());
        for(Object obj : list)
            System.out.println("BigCage: "+obj.getClass().toString());
    }

}
class Cage<T> extends ArrayList<T>
{
    public static void printList(List<?> list)
    {
        System.out.println("*************"+list.getClass().toString());
        for(Object obj : list)
            System.out.println("Cage: "+obj.getClass().toString());
    }
}

class Animal
{
}
class Dog extends Animal
{
}
class Cat extends Animal
{
}

Now what is confusing me is that this compiles fine with javac 1.6.0_26 but when I run it I get the following class cast exception:

Test 2
*************class BigCage
BigCage: class Dog
*************class BigCage
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog
        at BigCage.printList(GenericTestsClean.java:31)
        at GenericTestsClean.test2(GenericTestsClean.java:13)
        at GenericTestsClean.main(GenericTestsClean.java:21)

A number of things to note here:

  1. The two printList are NOT overriding but overloading each other as expected(They have different types because the generic types of their arguments are different). This can be verified by using the @Override annotation
  2. Changing the void printList(List<?>) method in class Cage to be non-static generates an appropriate compile time error
  3. Changing the method void <U extends Dog> printList(List<U>) in class BigCage to void <U> printList(List<U>) generates an appropriate error.
  4. In main() calling printList() through the class BigCage (ie BigCage.printList(...)) generates the same runtime error
  5. In main() calling printList() through the class Cage (ie Cage.printList(...)) works as expected only calling the version of printList in Cage
  6. If I copy the definition for printList(List<?>) to class BigCage from class Cage, which will hide the definition in class Cage, I get the appropriate compiler error

Now if I had to take a shot in the dark as to what is going on here, I'd say the compiler is screwing up because it's working in multiple phases: Type Checking and Overloaded Method Resolution. During the type checking phase we get through the offending line because class BigCage inherited void printList(List<?>) from class Cage which will match any old List we throw at it, so sure we have a method that will work. However once it comes time to resolve with method to actually call we have a problem due to Type Erasure which causes both BigCage.printList and Cage.printList to have the exact same signature. This means when compiler is looking for a match for animalCage.printList(animalCage); it will choose the first method it matches (and if we assume it starts at the bottom with BigCage and works its why up to Object) it'll find void <U extends Dog> printList(List<U>) first instead of the correct match void printList(List<?>)

Now for my real question: How close to the truth am I here? Is this a known bug? Is this a bug at all? I know how to get around this problem, this is more of an academic question.

**EDIT**

As few people have posted below, this code will work in Eclipse. My specific question deals with javac version 1.6.0_26. Also, I'm not sure if I completely agree with Eclipse in this case, even though it works, because adding a printList(List<?>) to BigCage will result in a compile time error in Eclipse and I can't see reason why it should work when the same method is inherited verses manually added (See Note 6 above).

Bob9630
  • 953
  • 8
  • 20
  • IANALL, but note 3, above, seems like a smoking gun - that it looks like a bug. On the other hand, this stuff is always messy - C++ templates have the same kinds of problem - only more. – Ed Staub Jul 10 '11 at 22:05
  • I copied the same code in Eclipse and ran it. It worked perfectly. No Exceptions. I have copied all the classes in the same file(which I know is a bad practice, but I was just checking your question). – Logan Jul 11 '11 at 05:44
  • @Logan I have edited the original question to better reflect the point you are making about Eclipse vs javac – Bob9630 Jul 11 '11 at 06:41

4 Answers4

8

Consider this trivial problem:

class A
{
    static void foo(){ }
}
class B extends A
{
    static void foo(){ }
}
void test()
{
    A.foo();
    B.foo();
}

Suppose we remove the foo method from B, and we only recompile B itself, what could happen when we run test()? Should it throw linkage error because B.foo() is no found?

According to JLS3 #13.4.12, removing B.foo doesn't break binary compatibility, because A.foo is still defined. This means, when B.foo() is executed, A.foo() is invoked. Remember, there's no recompilation of test(), so this forwarding must be handled by JVM.

Conversely, let's remove foo method from B, and recompile all. Even though compiler knows statically that B.foo() actually means A.foo(), it still generate B.foo() in the bytecode. For now, JVM will forward B.foo() to A.foo(). But if in future B gains a new foo method, the new method will be invoked at runtime, even if test() isn't recompiled.

In this sense, there is a overriding relation among static methods. When compile sees B.foo(), it must compile it to B.foo() in bytecode, regardless whether B has a foo() today.

In your example, when compiler sees BigCage.printList(animalCage), it correctly infer that it's actually calling Cage.printList(List<?>). So it needs to compile the call into bytecode as BigCage.printList(List<?>) - the target class must be BigCage here instead of Cage.

Oops! Bytecode format hasn't been upgrade to handle method signature like that. Generics information are preserved in bytecode as auxilary information, but for method invocation, it's the old way.

Erasure happens. The call is actually compiled into BigCage.printList(List). Too bad BigCage also has a printList(List) after erasure. At runtime, that method is invoked!

This problem is due to the mismatch between Java spec and JVM spec.

Java 7 tightens up a little; realizing bytecode and JVM can't handle such situations, it no longer compiles your code:

error: name clash: printList(List) in BigCage and printList(List) in Cage have the same erasure, yet neither hides the other

Another fun fact: if the two methods have different return types, your program will work correctly. This is because in byte code, method signature includes return type. So there is no confusion between Dog printList(List) and Object printList(List). See also Type Erasure and Overloading in Java: Why does this work? This trick is only allowed in Java 6. Java 7 forbids it, probably for reasons other than technical ones.

Community
  • 1
  • 1
irreputable
  • 44,725
  • 9
  • 65
  • 93
  • So if I follow correctly, the compiler is adding a `BigCage.printList(List>)` to **class BigCage** since **class Cage** has a `printList(List>)` defined and **BigCage** extends **Cage**. I guess my only follow up question then is why can't I manually add `printList(List>)` to **class BigCage** without a compile time error (note 6)? Would it not be possible for the compiler to see that one of the static methods in **BigCage** has a type erasure identical to one of the base class and generate an error? – Bob9630 Jul 11 '11 at 06:02
  • two separated methods in 2 classes. can't put them in one class. see http://stackoverflow.com/questions/5527235/type-erasure-and-overloading-in-java-why-does-this-work/5528802#5528802 javac6 is unable to stuff two methods with same bytecode signature in one class – irreputable Jul 11 '11 at 06:31
  • I apologize if I'm still missing something obvious. I still don't understand why I get a compiler error if I change the method `void printList(List)` in **BigCage** to `void printList(List)` (Note 3). Doesn't `void printList(List)` and `void printList(List)` both result in the same erasure? Why does U extending anything result in the error going away when without the extension we get one? I get that the JVM can't properly pick which overloaded method to select, but can't the compiler check for this condition and generate an error? – Bob9630 Jul 11 '11 at 06:57
  • 1
    That error is due to method ambiguity, has nothing to do with bytecode. If U is bounded by `Dog`, `print(List)` is more specific than `print(List>)`. If someone calls `print(list)` and both methods match the signature, the more specific one is chosen. If `U` is unbounded, then neither method is more specific than the other. If a call matches both methods, compiler gave up due to ambiguity. – irreputable Jul 11 '11 at 07:45
  • 1
    That discussion is true under java6. This is some intriguing peculiarities of Java 6 which is soon to be obsolete. In java 7, such two methods aren't even allowed to be both declared, regardless whether U is bounded. Your program won't compile under java7. There is really not much value in discussing some of the fun things only observed under java 6. – irreputable Jul 11 '11 at 07:56
  • I have chosen your answer as the best. Thanks for clearing things up. I guess this is just a loophole in java 6 soon to be closed in java 7. – Bob9630 Jul 11 '11 at 16:59
2

This is not a bug. The method is static. You cannot override static methods, you only hide them.

When you call "printList" on bigCage, you really are calling printList on BigCage class and not the object, which will always call your static method declared in BigCage class.

Kal
  • 24,724
  • 7
  • 65
  • 65
  • 1
    I agree. Nothing is overriden (as I stated above). However the methods are overlaoded. The problem I am seeing is that the call `animalCage.printList(animalCage)` should be a compiler error since in BigCage.printList(...) requires the passed List to be `List`which animalCage is not. animalCage is of type `List`. This is why we wind up with the RunTime exception. The BigCage.printList is expecting only Dogs (or subtypes) while the animalCage List passed has a Cat on it – Bob9630 Jul 10 '11 at 21:54
  • You are correct. The compiler assumes ( correctly ) that when you call animalCage.printList(List) that you are calling the printList from the super class. – Kal Jul 10 '11 at 22:26
  • Then why doesn't it actually call Cage.printList(...) method instead of calling the version in BigCage.printList(...) which produces the RunTime exception? – Bob9630 Jul 10 '11 at 22:28
  • Your code compiled and ran correctly without any classcastexceptions .. for what its worth. – Kal Jul 10 '11 at 22:28
  • Did you use javac (version 1.6.0_26) or Eclipse to build? They are two different compilers. I have tested the same code in Eclipse and found it to work there. I suppose another question is which compiler has the correct behavior? In this case I would side with javac because if I add a definition for printList(List>) to BigCage Eclipse will report an error. See Note 6. – Bob9630 Jul 10 '11 at 23:04
  • I think the error is not in different compiled versions of BigCage but in invocation. It calls BigCage#printList method with BigCage parameter. BigCage don't fit List since Animal does not extend Dog. – zacheusz Jul 10 '11 at 23:14
2

This is simplest version of this code with the same problem:

import java.util.*;

public class GenericTestsClean {
    public static void main(String[] args) {
        List<Animal> animalCage = new ArrayList<Animal>();
        animalCage.add(new Cat());
        animalCage.add(new Dog());
        BigCage.printList(animalCage);
    }
}

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

class BigCage extends Cage {
    public static <U extends Dog> void printList(List<U> list) {
        System.out.println("BigCage#printList");
        for (Object obj : list) {
            System.out.println("BigCage: " + obj.getClass().toString());
        }
    }
}

class Cage {
    public static void printList(List list) {
        System.out.println("Cage#printList");
        for (Object obj : list) {
            System.out.println("Cage: " + obj.getClass().toString());
        }
    }
}

I think that compiller should return error:

    GenericTestsClean.java:8: <U extends Dog>printList(java.util.List<U>) in BigCage cannot be applied to (java.util.List<Animal>)
        BigCage.printList(animalCage);
               ^
1 error

(or sth about name clash with the same errasure) but it doesn't.
After dissasembling (javap -c GenericTestsClean) we got:

invokestatic    #9; //Method BigCage.printList:(Ljava/util/List;)V

Calling java GenericTestsClean:

javac 1.6.0_10 version

BigCage#printList
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog
        at BigCage.printList(GenericTestsClean.java:19)
        at GenericTestsClean.main(GenericTestsClean.java:8)

Eclipse compiller version

BigCage#printList
BigCage: class Cat
BigCage: class Dog

IMHO this results are both incorrect.

zacheusz
  • 8,750
  • 3
  • 36
  • 60
1

IMHO this code may be incorrect. Method printList in class BigCage should cause name clash coz printList in Cage have the same erasure, yet neither overrides the other. Strange that compiller compiles it :)

The resulting bytecode (javac 1.6.0_10) is equivalent to this:

class BigCage extends Cage {

    public static void printList(List list){
        System.out.println((new StringBuilder()).append("*************").append(list.getClass().toString()).toString());
        Dog dog;
        for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println((new StringBuilder()).append("BigCage: ").append(dog.getClass().toString()).toString()))
            dog = (Dog)iterator.next();
    }
}

The cast in loop causes exception. Eclipse built-in compiller generates such code (wich works without exception):

class BigCage extends Cage{

    public static void printList(List list){
        System.out.println((new StringBuilder("*************")).append(list.getClass().toString()).toString());
        Object obj;
        for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println((new StringBuilder("BigCage: ")).append(obj.getClass().toString()).toString()))
            obj = iterator.next();
    }
}

Or maybe source is OK, but compiler is creating bad bytecode? The fact is that we call method <U extends Dog> void printList(List<U> list) with parameter BigCage<Animal> animalCage and Animal does not extend Dog.

zacheusz
  • 8,750
  • 3
  • 36
  • 60
  • I agree the compiler should flag this since the two methods will result in the same erasure type. Funny thing is that it will if you remove the requirment that U extend Dog in BigCage. See Note 3. Either way, the code above breaks the type safety of generics. – Bob9630 Jul 10 '11 at 22:48
  • Yes. I think that in this case call of BigCage#printList with BigCage parameter is incorrect. But removing cast inside method removes problem. – zacheusz Jul 10 '11 at 23:19