Luigi Plinge
Luigi Plinge

Reputation: 51109

Quick java generics question

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

Upvotes: 4

Views: 381

Answers (5)

Nesan Mano
Nesan Mano

Reputation: 2166

Generics are a way to guarantee type safety. Eg:

int[] arr = new int[4];
   arr[0] = 4; //ok
   arr[1] = 5; //ok
   arr[2] = 9; //ok
   arr[3] = "Hello world"; // you will get an exception saying incompatible 

types.

By default arrays in Java are typeSafe. An integer array is only meant to contain integer and nothing else.

Now:

ArrayList arr2 =new ArrayList();
   arr2.add(4); //ok
   arr2.add(5); //ok
   arr2.(9); //ok


   int a = arr2.get(0);
   int b = arr2.get(1);
   int c = arr3.get(2);

You willa gain get an exception like what it is not possible to cast Object instance to integer.

The reason is that ArrayList stores object and not primitive like the above array.

The correct way would be to explicitly cast to an integer.You have to do this because type safety is not yet guaranteed. eg:

int a = (int)arr2.get(0);

To employ type safety for collections, you simply specify the type of objects that your collection contains. eg:

 ArrayList<Integer> a = new ArrayList<Integer>();


After insertion into the data structure, you can simply retrieve it like you 
would do with an array.

eg:

int a = arr2.get(0);

Upvotes: 0

irreputable
irreputable

Reputation: 45433

"I don't think I really understand Java generics."

Nobody does...

The issue is related to the interesting return type of getClass(). See its javadoc. And this recent thread.

In both of your examples, lst.getClass() returns Class<? extends List>, consequently, newInstance() returns ? extends List - or more formally, a new type parameter W introduced by javac where W extends List

In your first example, we need to assign W to List<Integer>. This is allowed by assignment conversion. First, W can be converted to List because List is a super type of W. Then since List is raw type, the optional unchecked conversion is allowed, which converts List to List<Integer>, with a mandatory compiler warning.

In the 2nd example, we need to assign W to T. We are out of luck here, there's no path to convert from W to T. It makes sense because as far as javac knows at this point, W and T could be two unrelated subclass of List.

Of course, we know W is T, the assignment would have been safe if allowed. The root problem here, is that getClass() loses type information. If x.getClass() returns Class<? extends X> without erasure, both of your examples will compile without even warning. They indeed are type safe.

Upvotes: 1

Bruno Reis
Bruno Reis

Reputation: 37822

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!

Upvotes: 6

Stephen C
Stephen C

Reputation: 718816

And why does the second not compile, with the error shown below.

The error shown is actually reporting that you have tried to run code that failed to compile. It is a better idea to configure your IDE to not run code with compilation errors. Or if you insist on letting that happen, at least report the actual compilation error together with the line number, etc.

Upvotes: 1

user207421
user207421

Reputation: 310913

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.

Upvotes: 3

Related Questions