Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java determines which method to call using a generic type argument?

Tags:

java

generics

To my knowledge, Java drops the generic type argument information during runtime. It is only used on compilation to perform checks, for example, is this particular method call valid or not.

Today I came across the following piece of code, in which, seemingly, Java determines by the collection/list type argument, which constructor to call:

public static class MyClass {
    public MyClass(final Collection<String> coll) {
        System.out.println("Constructor 1");
    }
    public MyClass(final List<Integer> list) {
        System.out.println("Constructor 2");
    }
}

The following calls are made:

new MyClass(new HashSet<String>()); // Constructor 1
new MyClass(new ArrayList<String>()); // Constructor 1
new MyClass(new ArrayList<Integer>()); // Constructor 2

Now, if I erase the type arguments:

public static class MyClass2 {
    public MyClass2(final Collection coll) {
        System.out.println("Constructor 1");
    }
    public MyClass2(final List list) {
        System.out.println("Constructor 2");
    }
}

...The same calls act as I would expect them to; constructor invocation which uses a list argument goes for the constructor which meets its needs "most precisely":

new MyClass2(new HashSet<String>()); // Constructor 1
new MyClass2(new ArrayList<String>()); // Constructor 2
new MyClass2(new ArrayList<Integer>()); // Constructor 2

It appears that generics information is being stored in the compiled class (in this case, MyClass) and is not being discarded after all, but it should be discarded. What am I misunderstanding?

like image 342
Sergejs Pogorelovs Avatar asked Sep 09 '25 20:09

Sergejs Pogorelovs


2 Answers

What happens here is that the compiler can distinguish the two constructors by using generics, so it does before creating the byte code and before stripping generics.

In the middle case, it will tell the VM to invoke MyClass2<init>(Collection) (i.e. generate byte code which matches this specific constructor).

The VM doesn't try to determine which method matches at runtime. That would be way too slow. Instead, it relies on the compiler creating very specific instructions.

This is why the code above works even though the generic information has been erased at runtime.

[EDIT] To clarify: The byte code contains additional information which the compiler can see and use. You can get the same information via reflection.

Erasure means that the byte code interpreter and JIT doesn't care about generics which is why you can't have setFoo(List<String>) and setFoo(List<Integer>) in the same class: While the compiler could distinguish the two, the runtime can't.

Specifically, when you examine a method via reflection, you will get generics information but the byte code interpreter / JIT doesn't use reflection. Instead, it uses the compressed method signature which reads something like Method com/pany/Type/setFoo(Ljava.util.List;)V - no generics here anymore.

Related:

  • How the Java virtual machine handles method invocation and return
  • Why do the Java bytecodes for invoking methods implicitly acquire and release monitors?
  • invokevirtual docs
like image 53
Aaron Digulla Avatar answered Sep 12 '25 09:09

Aaron Digulla


Didn't notice that you are dealing with constructor. Anyways, the below arguments is valid even for constructor.


Method invocation for the overloaded method is resolved at compile time by the compiler. And generics are used for type check at compile time only. So, this has nothing to do with the type erasure, which is completely a runtime business.

Consider your first case:

public MyClass(final Collection<String> coll)
public MyClass(final List<Integer> list)

Now, when you invoke the method as:

new MyClass(new HashSet<String>()); // Constructor 1
new MyClass(new ArrayList<String>()); // Constructor 1
new MyClass(new ArrayList<Integer>()); // Constructor 2

Compiler will decide which method has the more specific type for the passed argument. Consider case 2, where is your main doubt I guess.

ArrayList<String> is a subtype of Collection<String>, but it's not a subtype of List<Integer>. Generics are invariant. So, compiler will bind the 2nd method invocation to the first method.

Now, consider your 2nd case, where you are using raw types:

public MyClass2(final Collection coll)
public MyClass2(final List list)

Now, ArrayList<String> is a subtype of List and Collection both. But List is more specific to ArrayList than Collection. So, compiler will bind the method call:

new MyClass2(new ArrayList<String>());

with the one taking List as argument.


References:

  • Java Generics FAQs - Angelika Langer
    • Why doesn't method overloading work as I expect it?
    • Why doesn't method overriding work as I expect it?
like image 37
Rohit Jain Avatar answered Sep 12 '25 09:09

Rohit Jain