【问题标题】:Java two-level parameterized type inferenceJava 两级参数化类型推断
【发布时间】:2016-11-14 22:34:25
【问题描述】:

我有一些返回通配符参数化类型的代码。我正在尝试将其传递给参数方法,但出现编译器错误。有人可以向我解释为什么类型不匹配,以及解决此问题的最佳方法是什么?

static <L extends List<T>,T extends Number>
void useList(List<L> list) {}

public static void main(String[] args) {
    List<List<? extends Number>> list = null;
    useList(list);
}

编译错误:

demo/GenericsHell.java:75: error: method useList in class GenericsHell cannot be applied to given types;
        useList(list);
        ^
  required: List<L>
  found: List<List<? extends Number>>
  reason: inference variable L has incompatible bounds
    equality constraints: List<? extends Number>
    upper bounds: List<CAP#1>
  where L,T are type-variables:
    L extends List<T> declared in method <L,T>useList(List<L>)
    T extends Number declared in method <L,T>useList(List<L>)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
1 error

在我的真实代码中,list 是由一个复杂的方法生成的,该方法可以返回多种类型的“列表”(实际上是一个自定义泛型类)。是否有另一种方法来参数化 useList 函数,以便它以类型安全的方式接受它?


编辑 1

我正在尝试将一大段代码简化为一个简洁、连贯的问题。从答案中我可以看到上面的代码过度简化了我遇到的确切问题。我将尝试用一个更复杂的例子来重申,同时更好地指定我的约束。

首先,我实际上并没有使用 List,而是使用双参数化的更复杂的类。我想不出任何标准类可以做到这一点,所以我将定义一个用于示例:

static class NumberLists<L extends List<T>,T extends Number> extends ArrayList<L> {}

我会避免多次重复使用一个类型,这样更容易保持水平。 useList() 方法确实需要双重参数化,因为它在内部使用这两种类型:

static <L extends List<T>,T extends Number>
Set<NumberLists<L,T>> useList(L list) {
    NumberLists<L,T> nl = new NumberLists<L, T>();
    nl.add(list);
    return Collections.singleton(nl);
}

只要你有一个具体的类,这个框架就可以很好地工作:

    // Everything works with a concrete class
    List<Integer> intList = new ArrayList<Integer>();

    // Use with parametric functions
    Set<NumberLists<List<Integer>,Integer>> concreteSet = useList(intList);
    // Access & use elements
    NumberLists<List<Integer>,Integer> concreteNL = concreteSet.iterator().next();
    concreteNL.add(intList);

问题是我有一个输入列表可以是几种不同的类型:

static List<? extends Number> makeList() {
    if(Math.random()<.5) {
        return new ArrayList<Integer>();
    } else {
        return new ArrayList<Double>();
    }
}

List<? extends Number> numList = makeList();

这打破了上述许多可能的模式。

    // What is the type here? This is obviously an error
    Set<NumberLists<? extends List<? extends Number>>,? extends Number> paramSet = useList(numList);
    // Type should ideally be compatible with numList still
    NumberLists<?,?> paraNL1 = paramSet.iterator().next();
    // Captures don't match
    paraNL1.add(numList);

所以问题是我在编译时无法知道 numList 的具体类。而且我不觉得我在做任何实际上类型不安全的事情,因为我知道 numList 的类型必须与 useList 等返回的类型相匹配。

我确实可以控制大部分代码的类型签名。但是,我更喜欢以下内容:

  • 它应该能够以类型安全的方式与具体类(例如 intList)很好地工作
  • 它还将接收在运行时之前无法明确知道其类型的输入
  • 如果执行强制转换或其他未经检查的操作,它们应该发生在输入构造附近。应检查以下操作。

有关完整(但非编译)的 java 文件,请参阅https://gist.github.com/sbliven/f7babb729e0b1bee8d2dabe5ee979431。由于我对这个问题感兴趣,请参阅https://github.com/biojava/biojava/issues/354

【问题讨论】:

  • 在我看来,您的列表包含一个列表,而 useList() 期望该列表包含 Number,而不是 List
  • @markspace 在顶部代码块中,我希望让 L 绑定 List。我认为这是正确的级别数。
  • 我已更新问题并避免使用嵌套列表。希望这可以减少类型签名的混乱。

标签: java generics parametric-polymorphism


【解决方案1】:

对于第一个编译问题,您的问题来自您尝试将其分配给某个类型不匹配的变量。

最简单的方法是从useList() 调用开始,以确保这个简单的代码能够编译:

List<? extends Number> numList = makeList();
useList(numList);

如果您检查此代码,它已经编译。

现在请您的 IDE 将结果分配给局部变量。 IDE 将为您计算表达式的类型以声明变量,然后 voilà:

Set<? extends NumberLists<? extends List<? extends Number>, ? extends Number>> paramSet =
    useList(numList);

(如果可能,去掉 NumbersList 上的 L 类型,这会简化很多事情)

对于paraNL1.add(numList); 调用,由于通配符,它​​无法工作:编译器无法检查paraNL1 是否接受您未知类型的列表。

即使您按照相同的流程修复声明:

NumberLists<? extends List<? extends Number>, ? extends Number> paraNL1 =
    paramSet.iterator().next();

paraNL1.add(numList); // will still not compile

您看到您正在尝试将paraNL1 用作消费者(它消耗numList),而PECS 告诉您必须使用super 声明它才能工作。

确实,编译器知道您的L extends List&lt;? extends Number&gt; 必须存在某个类型NumberLists,但它不知道并且无法检查它是否与numList 的类型匹配。例如L 可以是LinkedList&lt;Float&gt;numList 可以是ArrayList&lt;Integer&gt;

编辑:为了让它工作,你可以像这样使用wilcard capture helper method

private static <T extends Number, L extends List<T>> void helper(L numList) {
    Set<NumberLists<L, T>> paramSet = useList(numList);
    NumberLists<L, T> paraNL1 = paramSet.iterator().next();
    paraNL1.add(numList);
}

(这甚至可以让您摆脱 Set 声明中的 ? extends NumerLists

并调用它

List<? extends Number> numList = makeList();
helper(numList);

【讨论】:

  • 我想我需要一个更好的 IDE——Eclipse Luna 建议 Set&lt;?&gt;。那么有什么方法可以使用 super 重新定义类,以便这段代码可以工作?我不确定这在我的真实代码中是否可行,但它可能会给我一些想法。
  • Luna 现在 2 岁多了。 Java 8 中的类型推断发生了很大变化,因此 IDE 的支持有了很大改进。最简单的编译方法是避免使用通配符。也许使用wildcard capture helper method
  • @Quantum7 我添加了一个带有通配符捕获辅助方法的示例
  • 通配符捕获是我缺少的技术。谢谢!
【解决方案2】:

问题在于编译器想要将具体类型分配给T。在您的声明中,T 需要根据 list 的声明变为 ? extends Number,这并不具体。

您可以将useList 更改为以下声明之一,编译器会很高兴:

static <L extends List<? extends Number>> void useList(List<L> list) {
}

(没有T,没问题)或

static <L extends List<? extends T>, T extends Number> void useList(List<L> list) {
}

(这里的T 将变为Number)。

如果您可以在变量声明中替换 ? extends Number,一切都会好起来的:

public static <T extends Number> void main(String[] args) {
    List<List<T>> list = null;
    useList(list);
}

【讨论】:

  • 我认为这是朝着我所寻找的方向迈出的一步,但这些选项似乎都不适合我。我已经编辑了我原来的问题,以更清楚地解释我的限制。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多