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

时间:2018-05-26 00:42:55

标签: java generics wildcard

我一直在做一些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发生了什么,返回类型有一个裸通配符?

1 个答案:

答案 0 :(得分: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);
            }
        };
    }