Search code examples
javagenericsmultimap

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. 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 (i.e. generics in generics, e.g. Map<K,List<V>>) then it fails.


Solution

  • 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 {
    }
    

    }