为什么Java Collector.toList()在其返回类型中需要通配符类型占位符?

问题描述 投票:13回答:1

我一直在做一些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中的通配符可以转换为C#作为带有where TWildCard : T的新泛型类型参数。但是上面的toList发生了什么,返回类型有一个裸通配符?

java generics wildcard
1个回答
9
投票

收藏家have three type parameters

T - 缩减操作的输入元素的类型

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

R - 减少操作的结果类型

对于一些收藏家,例如toListAR的类型是相同的,因为结果本身用于积累。

toList返回的收集器的实际类型将是Collector<T, List<T>, List<T>>

(以与其结果不同的类型积累的收集器的一个例子是Collectors.joining()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); } }; }
© www.soinside.com 2019 - 2024. All rights reserved.