Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why I cannot use wildcard type in parameter to compute

Going straight to the point (I know that one should avoid using wildcard types as the returning type)

I was writing this answer and the following code:

public static Map<?, Long> manualMap(Collection<?> c){
    Map<?, Long> map = new HashMap<>();
    c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

which gets the following warning:

Required type: capture of ?

Provided: capture of ?

and a suggestion from IntelliJ to

Change variable 'map' to 'Map<?, Object'

which makes even less sense. Naturally, it fails when I try to apply the suggestion.

Originally, I though "okey it has to do with the fact that it does not match the compute signature", namely:

default V compute(K key, ...) 

So I tried

public class MyMap <T>{
    public static <T> void nothing(Collection<T> c){
         // Empty
    }
}

and

 public static Map<?, Long> manualMap(Collection<Collection<?>> c, Map<?, Long> map){
     c.forEach(MyMap::nothing);
     return map;
 }

and I had no issue.

The following two versions:

public static <T> Map<?, Long> manualMap(Collection<?> c){
    Map<T, Long> map = new HashMap<>();
    c.forEach(e -> map.compute((T) e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

and

public static Map<?, Long> manualMap(Collection<?> c){
    Map<Object, Long> map = new HashMap<>();
    c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

work without any problem (except for a warning in the (T) the case).

So the question is

Why the first version does not work?

like image 988
dreamcrash Avatar asked Feb 22 '21 19:02

dreamcrash


People also ask

Can I use wildcard types with method parameters?

The use of wildcard types should be limited to method parameters. This rule raises an issue when a method returns a wildcard type. Noncompliant Code Example List<? extends Animal> getAnimals () {...}

What is the use of wildcard in C++?

The wildcard can be used in a variety of situations such as the type of a parameter, field, or local variable; sometimes as a return type. Unlike arrays, different instantiations of a generic type are not compatible with each other, not even explicitly. This incompatibility may be softened by the wildcard if ? is used as an actual type parameter.

How do I decide whether to use a wildcard or not?

You can use the "in" and "out" principle when deciding whether to use a wildcard and what type of wildcard is appropriate. The following list provides the guidelines to follow: An "in" variable is defined with an upper bounded wildcard, using the extends keyword.

How do you define a wildcard variable in a class?

Wildcard Guidelines: An "in" variable is defined with an upper bounded wildcard, using the extends keyword. An "out" variable is defined with a lower bounded wildcard, using the super keyword. In the case where the "in" variable can be accessed using methods defined in the Object class, use an unbounded wildcard.


3 Answers

The first method does not compile because of "capture conversion", that happens at each declaration. You can read my other answer about this, or this one. But to put it simply, you will have two separate types there, which you can see when you compile via:

 javac --debug=verboseResolution=all

And the output will contain:

.....
CAP#1 extends Object from capture of ?
CAP#2 extends Object from capture of ?
...

which means there are two types that have been capture converted. These types are unrelated to each other, the way you have it.

This on the other hand:

public static <T> void nothing(Collection<T> c){

}

is called wildcard capture method (it "captures" a wildcard) and is documented in official tutorial on why it works and how; thus you have no problem with that.


But the main problem here is that you can't assign anything (other than null) to a wildcard. So in your compute example, the first argument is going to be inferred to a ? and you can't assign anything to that.

like image 188
Eugene Avatar answered Oct 11 '22 00:10

Eugene


public static Map<?, Long> manualMap(Collection<?> c){
    Map<?, Long> map = new HashMap<>();
    c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

Here, you have three wildcards: the one on the parameter, the one on the map variable, and the one on the return value. (The return value one isn't super relevant).

You are trying to pass elements from the collection (of one wildcard type) into a method of the map (of another wildcard type).

The compiler doesn't know that those two are "meant to be" the same, so it doesn't accept a "collection" wildcard where it needs a "map" wildcard.

You can indicate that they are the same type via a type variable:

public static <T> Map<?, Long> manualMap(Collection<T> c){
    Map<T, Long> map = new HashMap<>();
    c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

Here, T is a type that you don't know; but you know it's the same unknown type in both cases.

Or you can just declare it in a way that doesn't need two wildcards:

return c.stream().collect(groupingBy(a -> a, counting()));
like image 44
Andy Turner Avatar answered Oct 11 '22 01:10

Andy Turner


The problem is that your method is accepting a collection of type "I don't care", but you're also creating a map with keys of "I don't care". That does not mean the types you do not care about are the same types. For that you need to actually bind the type, by using a type variable e.g.

public static <T> Map<T, Long> manualMap(Collection<T> c){
    Map<T, Long> map = new HashMap<>();
    c.forEach(e -> map.compute(e, (k, v) -> (v == null) ? 1 : v + 1));
    return map;
}

The method will still accept any types of collections, but it cares enough to keep track of the type and there's no wildcards or warnings anywhere, yay!

like image 4
Kayaman Avatar answered Oct 11 '22 01:10

Kayaman