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

2022-09-04 22:17:47

我一直在做一些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)

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

我理解,在一种情况下,Java中的通配符可以转换为C#作为带有.但是上面发生了什么,其中返回类型具有裸通配符?? extends Twhere TWildCard : TtoList


答案 1

收集器有三个类型参数

T- 简化操作的输入元素的类型

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

R- 还原操作的结果类型

对于某些收集器,例如 ,的类型和 是相同的,因为结果本身用于累积。toListAR

从中返回的收集器的实际类型为 。toListCollector<T, List<T>, List<T>>

(收集器的一个示例使用与其结果不同的类型进行累积,这是 Collectors.joining()使用 StringBuilder

大多数时候,类型参数是一个通配符,因为我们通常不关心它到底是什么。它的实际类型仅由收集器内部使用,如果需要用名称引用它,我们可以捕获它A

// 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);
}

您还可以在代码中看到它指定Collectors.CH_ID作为其特征,这指定了标识完成。这意味着它的完成器除了返回传递给它的任何内容之外什么都不做。toList


(本节由我在下面的评论中引用。

以下是为累加器携带类型参数的几种替代设计。我认为这些说明了为什么课程的实际设计是好的。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. 隐藏累加器作为 的实现细节,就像自身在内部进行累积一样。我认为这可能是有道理的,但它不太灵活,合并器步骤变得更加复杂。CollectorCollector

    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);
            }
        };
    }
    

答案 2

推荐