Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Quick java generics question

Tags:

java

generics

I don't think I really understand Java generics. What's the difference between these two methods? And why does the second not compile, with the error shown below.

Thanks

static List<Integer> add2 (List<Integer> lst) throws Exception {
    List<Integer> res = lst.getClass().newInstance();
    for (Integer i : lst) res.add(i + 2);
    return res;
}

.

static <T extends List<Integer>> T add2 (T lst) throws Exception {
    T res = lst.getClass().newInstance();
    for (Integer i : lst) res.add(i + 2);
    return res;
}

Exception in thread "main" java.lang.RuntimeException: Uncompilable source code - incompatible types
  required: T
  found:    capture#1 of ? extends java.util.List
like image 505
Luigi Plinge Avatar asked Dec 22 '22 11:12

Luigi Plinge


2 Answers

For the second method to compile, you have to cast the result of newInstace() to T:

static <T extends List<Integer>> T add2 (T lst) throws Exception {
  T res = (T) lst.getClass().newInstance();
  for (Integer i : lst) res.add(i + 2);
  return res;
}

Regarding the difference between the two methods, let's forget about the implementation, and consider only the signature.

After the code is compiled, both methods will have exactly the same signature (so the compiler would give an error if the have the same name). This happens because of what is called type erasure.

In Java, all the type parameters disappear after compilation. They are replaced by the most generic possible raw type. In this case, both methods will be compiled as List add2(List).

Now, this will show the difference between the two methods:

class Main {
  static <T extends List<Integer>> T add1(T lst) { ... }
  static List<Integer> add2(List<Integer> lst) { ... }
  public static void main(String[] args) {
    ArrayList<Integer> l = new ArrayList<Integer>();
    ArrayList<Integer> l1 = add1(l);
    ArrayList<Integer> l2 = add2(l); // ERROR!
  }
}

The line marked as // ERROR! won't compile.

In the first method, add1, the compiler knows that it can assign the result to a variable of type ArrayList<Integer>, because the signature states that the return type of the method is exactly the same as that of the parameter. Since the parameter is of type ArrayList<Integer>, the compiler will infer T to be ArrayList<Integer>, which will allow you to assign the result to an ArrayList<Integer>.

In the second method, all the compiler knows is that it will return an instance of List<Integer>. It cannot be sure that it will be an ArrayList<Integer>, so you have to make an explicit cast, ArrayList<Integer> l2 = (ArrayList<Integer>) add2(l);. Note that this won't solve the problem: you are simply telling the compiler to stop whining and compile the code. You will still get an warning (unchecked cast), which can be silenced by annotating the method with @SuppressWarnings("unchecked"). Now the compiler will be quiet, but you might still get a ClassCastException at runtime!

like image 137
Bruno Reis Avatar answered Dec 24 '22 01:12

Bruno Reis


The first one is specified to accept a List<Integer> and return a List<Integer>. List being an interface, the implication is that an instance of some concrete class that implements List is being passed as a parameter and an instance of some other concrete class that implements List is returned as a result, without any further relationship between these two classes other than that they both implement List.

The second one tightens that up: it is specified to accept some class that implements List<Integer> as a parameter, and return an instance of exactly that same class or a descendant class as the result.

So for example you could call the second one like so:

ArrayList list; // initialization etc not shown
ArrayList result = x.add2(list);

but not the first, unless you added a typecast.

What use that is is another question. ;-)

@Bruno Reis has explained the compile error.

like image 24
user207421 Avatar answered Dec 24 '22 00:12

user207421