简体   繁体   中英

Return value of multi-level generic type cannot be assigned to an extended type

I have this convenient method (which I have been using for many years without problems). It just converts a List to a Map<SomeKey, List> , grouping them by a key attribute.

To avoid unnecessary casting, I'm passing the key attribute as a String (which refers to a method name) and I'm also specifying the type of that attribute.

@SuppressWarnings({"unchecked"})
@Nullable
public static <K, E> Map<K, List<E>> getMultiMapFromList(Collection<E> objectList, String keyAttribute, Class<K> contentClass)
{
  // creates a map from a list of objects using reflection
  ...
}

The above method has been working flawlessly for many years in many applications. But today the following case raises a problem:

List<? extends MyBean> fullBeanList = getFullBeanList();

Map<MyKey, List<? extends MyBean>> multiMap;

// the following line doesn't compile.
multiMap = Utils.getMultiMapFromList(fullBeanList, "key", MyKey.class); 

During development there are no warnings what so ever from my IntelliJ IDE. But during compilation this appears:

Error:(...,...) java: incompatible types: java.util.Map<mypackage.MyKey, java.util.List<capture #2 of ? extends mypackage.MyBean>> cannot be converted to java.util.Map<mypackage.MyKey, java.util.List<? extends mypackage.MyBean>>

I can't figure this one out though. My guess it has something to do with the ? extends ? extends . But I don't see any violations. And I'm also wondering a bit about why it only appears at compilation time? I would think that due to type erasure it doesn't even matter once it's compiled anyway.

I'm sure I could force this by adding some casts, but I would like to understand what's happening here.

EDIT:

for convenience:

Test.java

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class Test
{
  public static void main(String[] args)
  {
    List<? extends MyBean> input = new ArrayList<>();

    Map<MyKey, List<? extends MyBean>> output;
    output = test(input, MyKey.class); // doesn't compile
  }

  public static <K, E> Map<K, List<E>> test(Collection<E> a, Class<K> b)
  {
    return null;
  }

  private static class MyKey{}
  private static class MyBean{}
}

EDIT 2

To continue one step further in the madness:

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class Test
{
  public static void main(String[] args)
  {
    List<? extends Number> input = new ArrayList<>();

    // compiles fine
    List<? extends Number> output1 = test1(input);

    // doesn't compile
    Map<String, List<? extends Number>> output2 = test2(input);
  }

  public static <E> List<E> test1(Collection<E> a) { return null;}
  public static <E, K> Map<K, List<E>> test2(Collection<E> a) { return null;}
}

I'm not sure what to think of this. As long as I use 1 level of generics then it works fine. But when I use 2-level generics (ie generics in generics, eg Map<K,List<V>> ) then it fails.

This will resolve your problem.

You have to change the method test as like below.

public static <K, E> Map<K, List<? extends E>> test(
    Collection<? extends E> a, Class<K> b) {
return null;
}

The problem is that you are not telling ?s passed to the method and in Java they aren't guaranteed to be the same. Make this method generic, so that you have a generic type parameter to reference and to be the same throughout the method.

Below is the code.

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class Test {
public static void main(String[] args) {
    List<? extends MyBean> input = new ArrayList<>();

    Map<MyKey, List<? extends MyBean>> output;
    output = test(input, MyKey.class); // doesn't compile
}

public static <K, E> Map<K, List<? extends E>> test(
        Collection<? extends E> a, Class<K> b) {
    return null;
}

private static class MyKey {
}

private static class MyBean {
}

}

After reading Dilip Singh Kasana's answer I still didn't get it. But then I came accross this article, which explained it to me.

I am not going to copy the whole thing, but just the part that enlighted me.

Collection< Pair<String,Long> >        c1 = new ArrayList<Pair<String,Long>>(); 
Collection< Pair<String,Long> >        c2 = c1;   // fine   
Collection< Pair<String,?> >           c3 = c1;   // error   
Collection< ? extends Pair<String,?> > c4 = c1;   // fine    

Of course, we can assign a Collection<Pair<String,Long>> to a Collection<Pair<String,Long>> . There is nothing surprising here.

But we can not assign a Collection<Pair<String,Long>> to a Collection<Pair<String,?>> . The parameterized type Collection<Pair<String,Long>> is a homogenous collection of pairs of a String and a Long ; the parameterized type Collection<Pair<String,?>> is a heterogenous collection of pairs of a String and -something of unknown type-. The heterogenous Collection<Pair<String,?>> could for instance contain a Pair<String,Date> and that clearly does not belong into a Collection<Pair<String,Long>> .

For this reason the assignment is not permitted.

Applying it to the question.

If we supply an input of type List<? extends Number> List<? extends Number> to a method public static <E> Map<String, List<E>> test(Collection<E> objectList) then it will actually return a Map<String, List<? extends Number> Map<String, List<? extends Number> .

But this return-value cannot be assigned to a field of the exact same type Map<String, List<? extends Number> Map<String, List<? extends Number> .

The reason for this, is that the returned map could be a Map<String, List<Integer> . If I were to assign it to a Map<String, List<? extends Number> Map<String, List<? extends Number> , then I could later on put a List<Double> in it. That would clearly break it, but nothing would stop me from doing it.

Consider this:

// behind the scenes there's a map containing Integers.
private static Map<String, List<Integer>> myIntegerMap = new HashMap<>;

// both collections return the same thing, but one of them hides the exact type.
public static Map<String, List<? extends Number> getMap() { return myIntegerMap; } 
public static Map<String, List<Integer>> getIntegerMap() { return myIntegerMap; } 

private static void test()
{
  // fortunately the following line does not compile
  Map<String, List<? extends Number> map = getMap();

  // because nothing would stop us from adding other types.
  List<Double> myDoubleList = new ArrayList<>();
  myDoubleList.add(Double.valueOf(666));
  map.put("key", myDoubleList);

  // if it would compile, then this list would contain a list with doubles.
  Map<String, List<Integer>> brokenMap = getIntegerMap();
}

As Dilip Singh Kasana pointed out, it does work if the method would return a Map<String, List<? extends Number>> Map<String, List<? extends Number>> . Adding the extends changes everything.

// still the same map.
private static Map<String, List<Integer>> myIntegerMap = new HashMap<>;

// the return value is an extended type now.
public static Map<String, ? extends List<? extends Number> getMap() { return myIntegerMap; } 
public static Map<String, List<Integer>> getIntegerMap() { return myIntegerMap; } 

private static void test()
{
  // the following compiles now.
  Map<String, ? extends List<? extends Number> map = getMap();

  // if we try to add something now ...
  List<Double> myDoubleList = new ArrayList<>();
  myDoubleList.add(Double.valueOf(666));

  // the following won't compile.
  map.put("key", myDoubleList);
}

So, this time assigning it works, but the resulting type is a "read-only" map. (PS: For the sake of being complete. Stating the obvious: You can't add anything to a collection or map with an ? extends X type. Those collections are "read-only", that makes perfect sense.)

So the compile time error prevents this situation where the map could be broken.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM