Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Generics Wildcard capture in a generic method

I'm studying Java Generics and I'm reading the (very good) book by Naftalin and Wadler, and I got where he's talking about capturing the wildcard in a generic method like the implementation in Collections.reverse :

public static <T> void reverse(List<T> list){
    List<T> temp=new ArrayList<>(list);
    for(int i=0;i<list.size();i++)
        list.set(i,temp.get(list.size()-1-i));
}

he says that the method in the Collections class is implemented using a wildcard for simplicity:

public static void reverse(List<?> list){
    //code..
}

but using the first method body wouldn't work:

public static void reverse(List<?> list){
    List<Object> temp=new ArrayList<Object>(list);  //legal
    for(int i=0;i<list.size();i++)
        list.set(temp.get(list.size()-1-i));        //illegal
}

it doesn't work because it attempts to put a Object type element in a list whose type is unknown(?) and it could be everything extending Object (which is ..well,everything)

so calling the first method from the second should do the trick:

 public static void reverse1(List<?> list){
    reverse2(list);
 }

 public static <T> void reverse2(List<T> list){
    List<T> temp=new ArrayList<T>(list);
    for(int i=0;i<list.size();i++)
        list.set(i,temp.get(list.size()-1-i));
}

now,following what happens on method call,for example passing a

 List<String> myList  

1) List<String> myList is up-casted to a local variable String<?> list (String extending Object, which is the upper bound of the wildcard, makes List<String> subtype of List<?>)

2) list is now passed to reverse2() and the parameter T is inferred to be ? extends Object,now how could I use this as a parameter when I instantiate new ArrayList<T>() ??? this is something clearly illegal in Java code, so something else must be happening,please can youtell me what is it??

thanks

Luca

like image 687
Luca Avatar asked Aug 21 '26 11:08

Luca


1 Answers

The T parameter in reverse2() isn't inferred to be ? extends Object, and no instantiation is performed using the wildcard, ?.

Inference would only occur in a method that calls reverse2(). For example, if you call Collections.emptyList(), what is the type parameter? In that example, it's unknown, but it can usually be inferred at the calling site:

List<String> empty = Collections.emptyList();

is inferred to be a call to Collections.<String>emptyList() (the explicit form).

In your case, T has no restriction, so any type is compatible. If the type variable were declared as T extends String, however, the wildcard ? would be too general to satisfy that restriction, and the call would be illegal.


ok, I got it,so what is it T then? I mean, what does T is inferred to be?

T is a type variable in reverse2(), and as I explained above, type inference happens in the caller, not the callee, so T isn't "inferred" to be anything.

Maybe what you mean is what type shows up in the compiled byte code? In this case, no variables of type T are declared; T is never used, and no type checking is done. So, consider the following contrived example:

final class Reverse {

  static <T extends String> void reverse(List<T> list) {
    List<T> tmp = new ArrayList<>(list);
    for (int i = 0; i < list.size(); ++i)
      list.set(i, tmp.get(list.size() - 1 - i));
  }

}

Now a client that calls that method:

final class Test {

  public static void main(String... argv) {
    List<String> list = Arrays.asList("A", "B", "C");
    Reverse.reverse(list);
    System.out.println(list);
  }

}

Compile these classes together and run Test, and you'll get [C, B, A], as expected. Now, without recompiling Test, change the signature of the reverse() method and recompile only the Reverse class:

static <T extends Integer> void reverse(List<T> list)

Re-running Test will produce the same result, not a failure!

Now change the implementation of the reverse() method, and again, recompile only the Reverse class:

static <T extends Integer> void reverse(List<T> list) {
  List<T> tmp = new ArrayList<>(list);
  for (int i = 0; i < list.size(); ++i) {
    T el = tmp.get(list.size() - 1 - i);
    list.set(i, el);
  }
}

This time, running Test will produce the failure you might have expected last time:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

That's because T was actually referenced now:

T el = tmp.get(list.size() - 1 - i);

Into this assignment the compiler will insert a cast to the upper bound of the type parameter, which in this case is Integer:

T el = (Integer) tmp.get(list.size() - 1 - i);

If the type T is unrestricted (its upper bound is Object) no cast is performed, since it could never fail.

like image 163
erickson Avatar answered Aug 24 '26 01:08

erickson



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!