How a type is derived, where the return type is also the upper and lower bounds for the method parameters

Suppose we have the following code:

class A {} class B extends A {} class C extends B {} public static <T> T testMe(List<? super T> list1,List<? extends T> list2) { return null; } public static void main(String[] args) { List<B> listB = new ArrayList<>(); List<C> listC = new ArrayList<>(); // All three variants are possible: A a=testMe(listB, listC); B b=testMe(listB, listC); C c=testMe(listB, listC); } 

Question about public static <T> T testMe(List<? super T> list1,List<? extends T> list2) . How does the compiler determine type T if it has three classes: A,B,C, ,? This question arose when I analyzed Collections.copy .

+7
java generics inheritance type-inference
source share
4 answers

The compiler infers type C for a parameter of type T in all three cases.

This is the most specific type that is suitable for restrictions.

The [T] inference algorithm tries to find the most specific type that works with all arguments.

For the first two statements

 A a = testMe(listB, listC); B b = testMe(listB, listC); 

Both B and C match because a List<B> matches List<? super B> List<? super B> and List<C> matches List<? extends B> List<? extends B> , and List<B> corresponds to List<? super C> List<? super C> and List<C> matches List<? extends C> List<? extends C> . The compiler selects the most suitable type that matches, C

You can force this to compile with an explicit type parameter to force the compiler to resolve it to B :

 A a = Super.<B>testMe(listB, listC); B b = Super.<B>testMe(listB, listC); 

The third line matches only C , so this compiler chooses for T

 C c = testMe(listB, listC); 

This is because the assigned variable is of type C , and B cannot be assigned to C

+4
source share

If you change the signature that requires a token for T , it quickly becomes apparent what happens:

 public static <T> T testMe(Class<T> c, List<? super T> list1, List<? extends T> list2) { return null; } public static void main(String[] args) { List<B> listB = new ArrayList<>(); List<C> listC = new ArrayList<>(); A a = testMe(A.class, listB, listC); // compile error B b = testMe(B.class, listB, listC); // OK. T == B C c = testMe(C.class, listB, listC); // OK. T == C } 

The reason for this compilation in your example:

 A a = testMe(listB, listC); 

is that T is output as B (or C - it does not matter), but B also an instance of A , so an object of class B can be assigned to a variable of type A

+1
source share

From a purely theoretical point of view, why does it matter what a type argument is? The type is not encoded in compiled bytecode. Its only relevance is whether the compile-time type check passes or not. To this end, if the compiler can guarantee that some choice of type argument forces the code to compile, which one and whether there are more should not be relevant. In the first two cases, there are several types ( B and C ) that work; the compiler should not decide on this. In the third case, only one type ( C ) works. In all cases, if the compiler is satisfied that at least one type is working, it must compile it.

0
source share

The compiler at compile time knows what type of object you are passing, for example, List<B> , since generics is a compile-time function. He can use this information to check for type errors. After it is compiled, generics are thrown out due to type erasure, and this is actually the code:

  public static Object testMe(List<Object> list1, List<Object> list2) { //some code return null; } 
-one
source share

All Articles