Java Generics: What is the benefit of using wildcards here?
Asked Answered
C

3

6

The Collections.fill method has the following header:

public static <T> void fill(List<? super T> list, T obj)

Why is the wildcard necessary? The following header seems to work just as well:

public static <T> void fill(List<T> list, T obj)

I cannot see a reason why the wildcard is needed; code such as the following works with the second header as well as the first:

List<Number> nums = new ArrayList<>();
Integer i = 43;
fill(nums, i); //fill method written using second header

My question is: For what specific call of fill would the first header work but not the second? And if there is no such call, why include the wildcard? In this case, the wildcard does not make the method more concise nor add to readability (in my opinion).

Coppins answered 6/1, 2022 at 0:30 Comment(4)
In the first case, T is inferred to be Integer (with the wildcard), whereas it's inferred to be Number in the second case (without the wildcard). Not positive when that would make a practical difference, but there is a difference.Brokerage
I suspect there's something in the type inference rules that makes the signature with a wildcard work in some cases where the signature without a wildcard fails. An example would probably need to involve type inference in the arguments to fill.Larentia
My best guess is that this notation clarifies or suggests that you can use fill on superclass objects in the list with those of type T, which you might not know if you had the second method signature, even though both seem to be the same practicallyEarnestineearnings
I think, the author just followed the PECS rule like with other methods, without considering that for this specific method, it is unnecessary.Bedeck
H
4

This is a really good question and the simple answer was guessed already:

For the current version of the fill(List<? super T> list, T obj) there is no such input that would be rejected given the signature is changed to fill(List<T> list, T obj), so there is no benefit and the devs are likely followed the PECS principle

The above statement derives from the principle that: if there is a such type X so that X is a supertype of T then List<X> is a supertype of List<? super T> because of type contravariance. Since we can always find such X (at the worst case it's the Object class) - the compiler can infer a suitable List<X> argument type given either form of fill.

So, knowing that fact we can interfere with the compiler and infer the type ourselves using "type witness" so the code breaks:

List<Object> target = new ArrayList<>();
//Compiles OK as we can represent List<Object> as List<? super Integer> and it fits
Collections.<Integer>fill(target, 1);

//Compilation error as List<Object> is invariant to List<Integer> and not a valid substitute
Collections.<Integer>fillNew(target, 1);

This is all of course purely theoretical and nobody in their right mind would use the type argument there.

HOWEVER

While answering the question "What is the benefit of using wildcards here?" we yet considered only one side of the equation - us, consumers of the method and our experience but not library developers.

Hence this question is somewhat similar to why Collections.enumeration(final Collection<T> c) is declared the way it is and not enumeration(Collection<T> c) as final seems superfluous for the end-user.

We can speculate here about the real intention, but I can give a few subjective reasons:

  1. First: using List<? super T> (as well as final for enumeration) immediately disambiguates the code that tiny bit more and for the <? super T> specifically - it useful to show that only partial knowledge about the type parameter is required and the list cannot be used to produce values of T, but only to consume them. Quote:

Wildcards are useful in situations where only partial knowledge about the type parameter is required. JLS 4.5.1. Type Arguments of Parameterized Types

  1. Second: it gives some freedom to the library owners to improve/update the method without breaking backward compatibility while conforming to the existing constraints.

Now let's try make up some hypothetical "improvements" to see what I mean (I'll call the form of fill that uses List<T> as fillNew):

#1 The decision is to make method to return the obj value (used to fill up the list) back:

public static <T> void fill(List<? super T> list, T obj)
//becomes ↓↓↓
public static <T> T fill(List<? super T> list, T obj)

The updated method would work just fine for fill signature, but for fillNew - the inferred return type now isn't that obvious:

List<Number> target = new ArrayList<>();
Long val = fill(target, 1L); //<<Here Long is the most specific type that fits both arguments
//Compilation error
Long val = fillNew(target, 1L); //<<Here Number is, so it cannot be assigned back

//More exotic case:
Integer val = fill(asList(true), 0); //val is Integer as expected
Comparable<?> val = fillNew(asList(true), 0); //val is now Comparable<?> as the most specific type 

#2 The decision to add an overloaded version of fill that is 10x more performant in cases when T is Comparable<T>:

/* Extremely performant 10x version */
public static <T extends Comparable<T>> void fill(List<? super T> list, T value)
/* Normal version */
public static void fill(List<? super T> list, T value)

List<Number> target = new ArrayList<>();
fill(target, 1);  //<<< Here the more performant version is used as T inferred to Integer and it implements Comparable<Integer>
fillNew(target, 1); //<< Still uses the slow version just because T is inferred to Number which is not Comparable
    

To sum up - the current signature of fill is more flexible/descriptive in my opinion for all parties (developers and library designers)

Hoffer answered 7/1, 2022 at 20:53 Comment(0)
H
-1

For your example, the reason it 'works' with your basic <T> signature, is that an Integer is also a Number. The only 'T' that works is T = Number, and then the whole thing just works out.

In this case, the expression you have for the T obj parameter is a reified type: You have an Integer. You could have a T instead. Perhaps you have this:

class AtomicReference<T> {
  // The actual impl of j.u.concurrent.AtomicReference...
  // but with this one additional method:

  public void fillIntoList(List<? super T> list) {
    T currentValue = get();
    Collections.fill(list, currentValue);
  }
}

I may perhaps want to write something like this:

AtomicReference<String> ref = new AtomicReference<String>("hello");
List<CharSequence> texts = new ArrayList<>();

...

ref.fillIntoList(texts);

If my hypothetical fillIntoList method simply had List<T> in the signature that wouldn't compile. Fortunately it does, so the code does compile. Had the Collections.fill method not done the <? super T> thing, the invocation of the Collections.fill method in my fillIntoList method would have failed.

It's highly exotic for any of this to come up. But it can come up. List<? super T> is the strictly superior signature here - it can do everything List<T> does, and more, and it is also semantically correct: Of course I can fill a list-of-foos by writing into every slot a ref to something that I know for sure is a bar, if bar is a child of foo.

Hussein answered 6/1, 2022 at 1:54 Comment(3)
What happens at T currentValue = get()?Earnestineearnings
When I try it, fillIntoList compiles just fine even with a fill with the signature proposed in the question.Larentia
fillIntoList needs a wildcard, but that's because T is pinned down separately from the type of list. Collections.fill doesn't need a wildcard for this example to work.Larentia
C
-3

That is because the inheritance is useful is some cases.

For example, if you have the following class structure:

public class Parent {
  //some code
}

public class Child extends Parent {
  //some another code
}

You could use the first method writing:

List<Child> children = new ArrayList<>();
Parent otherParentObject = new Parent(); //after this line, set the values for the class
List<Parent> outParentList = new ArrayList<>();
fill(children, otherParentObject); //fill method using first signature;
Continuative answered 6/1, 2022 at 2:38 Comment(5)
But wouldn't you get the same result if it were not ? super T in the method signature?Earnestineearnings
@RichardKYu is not the same: If you use only the second signature, using my example: fill(outParentList, otherParentObject); //this compile fill(children, otherParentObject); //this fails in compilation timeContinuative
fill(children, otherParentObject); is supposed to fail. You're not allowed to put a Parent in a List<Child>.Larentia
It fails with either the actual Collections.fill signature or the signature proposed in the question.Larentia
Fails with the signature proposed in the question.Continuative

© 2022 - 2024 — McMap. All rights reserved.