Wednesday Oct 26, 2011

Generics Return Type Trick

I've been using Java generics for almost five years, since about a year after Java 6 was released. There is a generics feature that I've been using for most of that time that it turns out I didn't fully understand. Consider the following:
Surprising Java Generics
public class Surprising {

    public static <A extends Collection<String>> A foo(A input) {
        return input;
    }
    
    public static void main(String[] args) {
        ArrayList<String> array = new ArrayList<String>();
        List<String> list = array;
        Collection<String> collection = list;
        
        ArrayList<String> assignArray;
        List<String> assignList;
        Collection<String> assignCollection;
        
        assignArray = foo(array);
        assignArray = foo(list);
        assignArray = foo(collection);
        
        assignList = foo(array);
        assignList = foo(list);        
        assignList = foo(collection);

        assignCollection = foo(array);
        assignCollection = foo(list);        
        assignCollection = foo(collection);
    }
}

Several lines of this program are rejected by the compiler due to "incompatible type" errors. The incompatible type error occurs because the statement is attempting to assign a less specific type, such as Collection, to a more specific type, such as ArrayList. The compiler is right to complain!

The tricky bit here is that the other assignments all work. Consider the very first assignment, "assignArray = foo(array);". The foo() method is declared as returning the generic type variable A which is declared as A extends Collection<String>. Wait a second, if A only specifies that it extends Collection<String> how is it that we are assigning a result of foo() to assignArray which is more specific than Collection? You shouldn't be able to assign a Collection<String> result to a ArrayList<String> variable.

The answer is that the exact return type of foo() is not determined solely by the declaration of A. Instead, the compiler looks at all the usages of A in the expression and determines a single type for all of the As in the expression. If a single type for A can't be determined then the compiler will also produce an error. Usually though it is more common that one of the parameters isn't an A.

For our assignment statement, "assignArray = foo(array);", the type of A is being determined by the type of the parameter "input". Once the actual type of A for this expression is determined to be "ArrayList<String>" then the return type of foo() is compatible with assignArray.

Not all of the assignments in this program need this magic. Most of them work by virtue that the assignment type is less specific than the return type of foo() for that particular invocation.

This property of how a generic type such as A interpreted in Java is extremely useful. It's very commonly used with collections where the collection you get back from a method is the same type as the collection you pass in. You probably use this all the time and, like me, didn't think about it.

Looking back over my old code where I used generics I found a number of cases where I used two type variables, one for return type and one for a parameter, in cases where, because of this magic, only one type variable was needed. If you have code which has a separate type variable for a return type, check carefully, you may not need it and it may actually be less safe than with one type variable and this trick.

About

mduigou

Search

Archives
« July 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  
       
Today