【问题标题】:Why does Java Collector.toList() require a wildcard type placeholder in its return type?为什么 Java Collector.toList() 在其返回类型中需要通配符类型占位符?
【发布时间】:2018-05-26 00:42:54
【问题描述】:

我一直在做一些 Java Streams 操作,当然它不喜欢我的代码并且拒绝提供有用的错误消息。 (作为参考,我对 C# 和 Linq 没有任何问题,所以我从概念上理解我正在尝试做的一切。)所以我开始深入研究将显式泛型类型添加到我的代码中的每个方法,这样我就可以找到源代码问题,因为过去的经验告诉我,这是一条成功的前进道路。

在环顾四周时,我遇到了一些我不明白的东西。考虑 Java 源代码中的以下代码(稍微重新格式化):

public static <T> Collector<T, ?, List<T>> toList() {
    return new Collectors.CollectorImpl<>(
        (Supplier<List<T>>) ArrayList::new,
        List::add,
        (left, right) -> {
             left.addAll(right);
             return left;
        },
        Collectors.CH_ID
    );
}

为什么toList 方法签名在其返回类型中需要通配符??当我删除它时,我得到了

Wrong number of type arguments: 2; required: 3

和

Incompatible types.
Required: Collector<T, List<T>, >
Found: CollectorImpl<java.lang.Object, List<T>, java.lang.Object>

当我将? 更改为Object 时,我得到(参考上述代码中的这些行/方法):

List::add – Cannot resolve method 'add'
left.addAll – Cannot resolve method 'addAll(java.lang.Object)'

当我放回通配符并检查这两个时,它们是:

List – public abstract boolean add(T e)
List – public abstract boolean addAll(Collection<? extends T> c)

进一步摆弄并没有教会我更多东西。

我了解在? extends T 等一种情况下,Java 中的通配符可以转换为带有where TWildCard : T 的新泛型类型参数的C#。但是上面的toList 是怎么回事,返回类型有一个裸通配符?

【问题讨论】:

  • 澄清一下,你问的是返回类型Collector&lt;T, ?, List&lt;T&gt;&gt;中的通配符,对吧?
  • @Radiodef 正确。澄清。
  • 很好的问题,直到现在才考虑

标签: java generics wildcard


【解决方案1】:

收藏家have three type parameters:

T - 归约操作的输入元素类型

A - 归约操作的可变累积类型(通常作为实现细节隐藏)

R - 归约操作的结果类型

对于一些收集器,比如toList,A和R的类型是一样的,因为结果本身就是用来累加的。

从toList 返回的收集器的实际类型是Collector&lt;T, List&lt;T&gt;, List&lt;T&gt;&gt;。

(以与其结果不同的类型进行累积的收集器的示例是Collectors.joining() which uses a StringBuilder。)

A 的类型参数大多数时候是通配符,因为我们通常不关心它实际上是什么。它的实际类型仅供收集器内部使用,we can capture it if we need to refer to it by a name:

// Example of using a collector.
// (No reason to actually write this code, of course.)
public static <T, R> collect(Stream<T> stream,
                             Collector<T, ?, R> c) {
    return captureAndCollect(stream, c);
}
private static <T, A, R> captureAndCollect(Stream<T> stream,
                                           Collector<T, A, R> c) {
    // Create a new A, whatever that is.
    A a = c.supplier().get();

    // Pass the A to the accumulator along with each element.
    stream.forEach(elem -> c.accumulator().accept(a, elem));

    // (We might use combiner() for e.g. parallel collection.)

    // Pass the A to the finisher, which turns it in to a result.
    return c.finisher().apply(a);
}

您还可以在toList 的代码中看到,它指定Collectors.CH_ID 作为其特征,这指定了一个身份完成。这意味着它的终结者除了返回传递给它的任何东西之外什么都不做。


(此部分由我下面的评论引用。)

这里有几个替代设计来为累加器携带类型参数。我认为这些说明了为什么 Collector 类的实际设计是好的。

  1. 只需使用Object,但我们最终会投射很多。

    interface Collector<T, R> {
        Supplier<Object> supplier();
        BiConsumer<Object, T> accumulator();
        BiFunction<Object, Object, Object> combiner();
        Function<Object, R> finisher();
    }
    
    static <T> Collector<T, List<T>> toList() {
        return Collector.of(
            ArrayList::new,
            (obj, elem) -> ((List<T>) obj).add(elem),
            (a, b) -> {
                ((List<T>) a).addAll((List<T>) b);
                return a;
            },
            obj -> (List<T>) obj);
    }
    
  2. 将累加器隐藏为Collector 的实现细节,因为Collector 本身在内部进行累加。我认为这可能是有道理的,但它不太灵活,并且组合器步骤变得更加复杂。

    interface Collector<T, R> {
        void accumulate(T elem);
        void combine(Collector<T, R> that);
        R finish();
    }
    
    static <T> Collector<T, List<T>> toList() {
        return new Collector<T, List<T>>() {
            private List<T> list = new ArrayList<>();
            @Override
            public void accumulate(T elem) {
                list.add(elem);
            }
            @Override
            public void combine(Collector<T, List<T>> that) {
                // We could elide calling finish()
                // by using instanceof and casting.
                list.addAll(that.finish());
            }
            @Override
            public List<T> finish() {
                return new ArrayList<>(list);
            }
        };
    }
    

【讨论】:

  • 到目前为止我学到了很多东西。谢谢。链接很棒。在接受答案之前,我会先研究一下。我确实想知道,通配符什么时候重要?为什么累加器类型是泛型类型参数的一部分?只要输出结果对输入正确,收集器方法的调用者如何关心累加器是什么?
  • 累加器有一个类型参数,以便 API 代码可以捕获和使用它,就像我在示例中使用它的方式一样。例如,并行流收集将涉及多个线程,每个线程都有自己的累加器对象,最后还有一个组合器步骤。 (我还在我的回答中添加了一些示例,我认为这有助于说明这一点。)为什么选择Collector 的实际设计对于设计师来说可能是一个有趣的问题。
  • 您的新更新成功了!我现在看到,虽然让累加器成为实现细节没有内在问题,但通过让收集器编写器这样做,Java 说收集器应该支持的累加器周围的不变量可能不会得到支持。不过,我仍然有想做的事情。但是为什么不直接将累加器类型参数指定为 'List`?为什么需要通配符?
  • 除了更短之外,我想它可以让特定收集器的实现以某种方式改变,而不必担心破坏依赖它的代码。 Collectors.joining() 可以更改为使用 StringJoiner 而不是 StringBuilder。我认为这可能就是官方 API 这样做的原因。 (如果 Stuart Marks 看到 Q&A 就好了,因为他可能有什么要补充的。I don't think an @ notification would work 直接问他。)
  • 有趣。对我来说,这意味着收集器可能有两个版本。一个用于构造如CollectorDefinition&lt;T, A,R&gt;,一个用于使用,Collector&lt;T, R&gt;,系统取一个给你另一个,这样可以保证不变量。将泛型类型参数中的实现细节暴露给不仅不关心而且不应该关心这些细节的代码对我来说似乎是一种代码味道。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-12-16
  • 1970-01-01
  • 2020-12-18
  • 1970-01-01
  • 1970-01-01
  • 2018-09-30
  • 1970-01-01
相关资源
最近更新 更多