【问题标题】:Java nested generic typeJava 嵌套泛型类型
【发布时间】:2014-04-02 08:54:08
【问题描述】:

为什么下面的test() 方法必须使用泛型类型Map<?, ? extends List<?>> 而不是更简单的Map<?, List<?>>

public static void main(String[] args) {
    Map<Integer, List<String>> mappy =
        new HashMap<Integer, List<String>>();

    test(mappy);
}

public static void test(Map<?, ? extends List<?>> m) {}

// Doesn't compile
// public static void test(Map<?, List<?>> m) {}

注意以下方法有效,并且这三种方法无论如何都具有相同的擦除类型。

public static <E> void test(Map<?, List<E>> m) {}

【问题讨论】:

  • 你得到的编译错误信息到底是什么?
  • Main类型中的方法test(Map&lt;?,List&lt;?&gt;&gt;)不适用于参数(Map&lt;Integer,List&lt;String&gt;&gt;)
  • 不完全是答案 - 但您可以使用 Map&lt;?, List&lt;?&gt;&gt;。然后,您需要使用带有 Map&lt;Integer, List&lt;?&gt;&gt; mappy = new HashMap&lt;&gt;(); 作为参数的方法。
  • 确实可以,但是我失去了对我放入 mappy 的内容的类型检查!例如,我可以写mappy.put(123, new ArrayList&lt;Integer&gt;()) 没有错误,这是不想要的。

标签: java generics bounded-wildcard unbounded-wildcard


【解决方案1】:

从根本上说,List&lt;List&lt;?&gt;&gt;List&lt;? extends List&lt;?&gt;&gt; 具有不同的类型参数。

实际上,一个是另一个的子类型,但首先让我们详细了解它们各自的含义。

理解语义差异

一般来说,通配符?代表一些“缺失信息”。这意味着“这里曾经有一个类型参数,但我们不知道它是什么了”。而且因为我们不知道它是什么,所以限制了我们如何使用任何引用该特定类型参数的东西。

目前,让我们使用List 代替Map 来简化示例。

  • List&lt;List&lt;?&gt;&gt; 持有任何类型的 List 以及任何类型的参数。所以即:

    List<List<?>> theAnyList = new ArrayList<List<?>>();
    
    // we can do this
    theAnyList.add( new ArrayList<String>() );
    theAnyList.add( new LinkedList<Integer>() );
    
    List<?> typeInfoLost = theAnyList.get(0);
    // but we are prevented from doing this
    typeInfoLost.add( new Integer(1) );
    

    我们可以将任何List 放入theAnyList,但这样做我们就失去了它们的元素的知识。

  • 当我们使用? extends 时,List 包含 List 的某些特定子类型,但我们不再知道它是什么。所以即:

    List<? extends List<Float>> theNotSureList =
        new ArrayList<ArrayList<Float>>();
    
    // we can still use its elements
    // because we know they store Float
    List<Float> aFloatList = theNotSureList.get(0);
    aFloatList.add( new Float(1.0f) );
    
    // but we are prevented from doing this
    theNotSureList.add( new LinkedList<Float>() );
    

    theNotSureList 添加任何内容不再安全,因为我们不知道其元素的实际类型。 (最初是List&lt;LinkedList&lt;Float&gt;&gt;?还是List&lt;Vector&lt;Float&gt;&gt;?我们不知道。)

  • 我们可以将这些放在一起并拥有一个List&lt;? extends List&lt;?&gt;&gt;。我们不知道它里面有什么类型的List,我们也不知道那些 Lists 的元素类型。所以即:

    List<? extends List<?>> theReallyNotSureList;
    
    // these are fine
    theReallyNotSureList = theAnyList;
    theReallyNotSureList = theNotSureList;
    
    // but we are prevented from doing this
    theReallyNotSureList.add( new Vector<Float>() );
    // as well as this
    theReallyNotSureList.get(0).add( "a String" );
    

    我们丢失了both关于theReallyNotSureList的信息,以及其中Lists的元素类型。

    (但您可能会注意到,我们可以分配任何类型的List持有列表给它......)

所以分解一下:

//   ┌ applies to the "outer" List
//   ▼
List<? extends List<?>>
//                  ▲
//                  └ applies to the "inner" List

Map 的工作方式相同,只是有更多类型参数:

//  ┌ Map K argument
//  │  ┌ Map V argument
//  ▼  ▼
Map<?, ? extends List<?>>
//                    ▲
//                    └ List E argument

为什么需要? extends

你可能知道"concrete"泛型类型具有不变性,即List&lt;Dog&gt; is not a subtype of List&lt;Animal&gt;即使class Dog extends Animal。相反,通配符是我们如何获得协方差,即List&lt;Dog&gt;List&lt;? extends Animal&gt;的子类型。

// Dog is a subtype of Animal
class Animal {}
class Dog extends Animal {}

// List<Dog> is a subtype of List<? extends Animal>
List<? extends Animal> a = new ArrayList<Dog>();

// all parameterized Lists are subtypes of List<?>
List<?> b = a;

所以将这些想法应用到嵌套的List

  • List&lt;String&gt;List&lt;?&gt; 的子类型,但 List&lt;List&lt;String&gt;&gt; 不是 List&lt;List&lt;?&gt;&gt; 的子类型。如前所示,这可以防止我们通过向 List 添加错误元素来损害类型安全。
  • List&lt;List&lt;String&gt;&gt; List&lt;? extends List&lt;?&gt;&gt; 的子类型,因为有界通配符允许协方差。也就是说,? extends 允许考虑 List&lt;String&gt;List&lt;?&gt; 的子类型这一事实。
  • List&lt;? extends List&lt;?&gt;&gt; 实际上是一个共享超类型:

         List<? extends List<?>>
              ╱          ╲
    List<List<?>>    List<List<String>>
    

审核中

  1. Map&lt;Integer, List&lt;String&gt;&gt;仅接受List&lt;String&gt; 作为值。
  2. Map&lt;?, List&lt;?&gt;&gt; 接受 any List 作为值。
  3. Map&lt;Integer, List&lt;String&gt;&gt;Map&lt;?, List&lt;?&gt;&gt; 是具有不同语义的不同类型。
  4. 不能将一个转换为另一个,以防止我们以不安全的方式进行修改。
  5. Map&lt;?, ? extends List&lt;?&gt;&gt; 是一个共享超类型,它施加了安全限制:

            Map<?, ? extends List<?>>
                 ╱          ╲
    Map<?, List<?>>     Map<Integer, List<String>>
    

泛型方法的工作原理

通过在方法上使用类型参数,我们可以断言List 有一些具体的类型。

static <E> void test(Map<?, List<E>> m) {}

此特定声明要求Map 中的所有 Lists 具有相同的元素类型。我们不知道该类型实际上是什么,但我们可以以抽象的方式使用它。这允许我们执行“盲”操作。

例如,这种声明可能对某种积累有用:

static <E> List<E> test(Map<?, List<E>> m) {
    List<E> result = new ArrayList<E>();

    for(List<E> value : m.values()) {
        result.addAll(value);
    }

    return result;
}

我们不能在m 上调用put,因为我们不再知道它的键类型是什么。但是,我们可以操纵它的,因为我们知道它们都是List,具有相同的元素类型

只是为了好玩

问题未讨论的另一个选项是为List 提供有界通配符和泛型类型:

static <E> void test(Map<?, ? extends List<E>> m) {}

我们可以使用Map&lt;Integer, ArrayList&lt;String&gt;&gt; 之类的名称来调用它。如果我们只关心 E 的类型,这是最宽容的声明。

我们也可以使用bounds来嵌套类型参数:

static <K, E, L extends List<E>> void(Map<K, L> m) {
    for(K key : m.keySet()) {
        L list = m.get(key);
        for(E element : list) {
            // ...
        }
    }
}

这既允许我们传递给它的内容,也允许我们如何操纵m 及其中的所有内容。


另见

【讨论】:

  • 我知道List&lt;List&lt;?&gt;&gt;List&lt;? extends List&lt;?&gt;&gt; 不同,这对我来说很清楚。我的问题更多是关于为什么在我的情况下我不能使用前者,什么时候我可以使用后者甚至List&lt;List&lt;E&gt;&gt;。编辑:“对于通用方法,采用 Map, List> 的方法略有不同,但基本相似。”对不起?
  • Louis,? extends 适用于地图的V。您的地图已将列表参数化为V。这意味着它只能在其中存储List&lt;String&gt;。采用 List&lt;?&gt; 的 Map 采用任何类型的 List。
  • 好的,知道了。这实际上有点合乎逻辑。
  • 您,先生,是个天才。谢谢你这么好的解释!
【解决方案2】:

这是因为泛型的子类化规则与您的预期略有不同。特别是如果您有:

class A{}
class B extends A{}

然后

List&lt;B&gt; 不是List&lt;A&gt; 的子类

详细说明here,通配符(“?”字符)的用法说明here

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-03-10
    • 1970-01-01
    • 2020-08-05
    • 1970-01-01
    • 2022-11-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多