【问题标题】:Java implement accumulator class that provides a CollectorJava实现提供收集器的累加器类
【发布时间】:2022-11-12 10:26:48
【问题描述】:

Collector 具有三种通用类型:

public interface Collector<T, A, R>

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

如果我想创建我的自定义收集器,我需要创建两个类:

  • 一种用于自定义累积类型
  • 一个用于自定义收集器本身

是否有任何库函数/技巧可以采用累积类型并提供相应的收集器?

简单的例子

这个例子更简单地说明了这个问题,我知道我可以在这种情况下使用reduce,但这不是我想要的.这是more complex example,在这里分享会使问题变得太长,但这是相同的想法。

假设我想收集流的总和并将其作为String 返回。

我可以实现我的累加器类:

public static class SumCollector {
   Integer value;

    public SumCollector(Integer value) {
        this.value = value;
    }

    public static SumCollector supply() {
        return new SumCollector(0);
    }

    public void accumulate(Integer next) {
       value += next;
    }

    public SumCollector combine(SumCollector other) {
       return new SumCollector(value + other.value);
    }

    public String finish(){
        return Integer.toString(value);
    }
}

然后我可以从这个类创建一个Collector

Collector.of(SumCollector::supply, SumCollector::accumulate, SumCollector::combine, SumCollector::finish);

但是对我来说似乎很奇怪,它们都指的是另一个类,我觉得有一种更直接的方法可以做到这一点。

为了只保留一个类,我可以做的是implements Collector&lt;Integer, SumCollector, String&gt;,但随后每个函数都会被复制(supplier() 将返回SumCollector::supply,等等)。

【问题讨论】:

  • 我认为你总是需要两节课。一个永远是累加器对象。还有一个将实现Collector 接口。但是累加器对象没有包含所有supply()combine()finish() 方法。它们仅在实现Collector 的类中可用。持有者类也可能是收集器中的私有内部class。同样对于您的示例,您可以只使用 AtomicInteger 作为累加器。为您留下一个必须实现的类 SumCollector implements Collector&lt;Integer, AtomicInteger, String&gt;
  • “持有者类也可能是收集器中的私有内部类。” => 我不认为我可以像implements Collector&lt;Integer, SumCollector.Acc, String&gt; 那样做,我得到SumCollector.Acc' has private access in 'SumCollector'
  • 哦,是的,那么可悲的是它一定是public。你也可以颠倒整个类结构。使Collector 成为累加器的私有内部类。然后仅使用静态方法公开它:public static Collector&lt;Integer, ?, String&gt; collector() {return new SumCollector();}

标签: java java-stream collectors accumulate


【解决方案1】:

不需要将函数实现为容器类的方法。

这就是这种收款人的典型实施方式

public static Collector<Integer, ?, Integer> sum() {
    return Collector.of(() -> new int[1],
        (a, i) -> a[0] += i,
        (a, b) -> { a[0] += b[0]; return a; },
        a -> a[0],
        Collector.Characteristics.UNORDERED);
}

但是,当然,您也可以将其实现为

public static Collector<Integer, ?, Integer> sum() {
    return Collector.of(AtomicInteger::new,
        AtomicInteger::addAndGet,
        (a, b) -> { a.addAndGet(b.intValue()); return a; },
        AtomicInteger::intValue,
        Collector.Characteristics.UNORDERED, Collector.Characteristics.CONCURRENT);
}

您首先必须为您的收集器找到合适的可变容器类型。如果不存在这样的类型,则必须创建自己的类。这些函数可以实现为对现有方法的方法引用或 lambda 表达式。

对于更复杂的示例,我不知道有合适的现有类型来保存intList,但您可以使用盒装的Integer,就像这样

final Map<String, Integer> map = …
List<String> keys = map.entrySet().stream().collect(keysToMaximum());
public static <K> Collector<Map.Entry<K,Integer>, ?, List<K>> keysToMaximum() {
    return Collector.of(
        () -> new AbstractMap.SimpleEntry<>(new ArrayList<K>(), Integer.MIN_VALUE),
        (current, next) -> {
            int max = current.getValue(), value = next.getValue();
            if(value >= max) {
                if(value > max) {
                    current.setValue(value);
                    current.getKey().clear();
                }
                current.getKey().add(next.getKey());
            }
        }, (a, b) -> {
            int maxA = a.getValue(), maxB = b.getValue();
            if(maxA <= maxB) return b;
            if(maxA == maxB) a.getKey().addAll(b.getKey());
            return a;
        },
        Map.Entry::getKey
    );
}

但是您也可以创建一个新的专用容器类作为 ad-hoc 类型,在特定收集器之外不可见

public static <K> Collector<Map.Entry<K,Integer>, ?, List<K>> keysToMaximum() {
    return Collector.of(() -> new Object() {
        int max = Integer.MIN_VALUE;
        final List<K> keys = new ArrayList<>();
    }, (current, next) -> {
        int value = next.getValue();
        if(value >= current.max) {
            if(value > current.max) {
                current.max = value;
                current.keys.clear();
            }
            current.keys.add(next.getKey());
        }
    }, (a, b) -> {
        if(a.max <= b.max) return b;
        if(a.max == b.max) a.keys.addAll(b.keys);
        return a;
    },
    a -> a.keys);
}

要点是,您不需要创建新的命名类来创建Collector

【讨论】:

  • 临时匿名类的有趣用法。无法引用该类的类型,但 java 可以正确推断类型。为了可读性,建议创建一个命名类。
  • @Lino 我想说,如果函数简短且彼此接近,那么您可以一目了然地概述声明和所有用途,这是可以接受的。这个具有更长功能的特定示例已经处于临界状态。更多的是为了完整性。
  • 我的目标不是单线,也不是要摆脱累加器类,我的目标是通过仅实现累加器类来实现Collector,如果可能的话。但从答案来看,这似乎是不可能的。在我看来,Collector.of(或.collect(...))是最干净/最易读的选项。
  • 收集器有容器供应商是有原因的。它必须能够在需要时生成多个实例,例如当与groupingBy 结合使用或用于并行评估时。从单个实例创建收集器将与整个概念相矛盾。另一个矛盾是收集器的用户确实不是想处理临时容器。在您的两个示例中,最终结果类型与容器类型不同,我什至为每个示例提供了两个具有不同容器类型的收集器实现。
  • 为什么你声称封装容器类是不可能的?我的所有四个示例都能够隐藏容器类。前两个字面上使用Collector&lt;Integer, ?, Integer&gt; 作为工厂方法的返回类型,就像Collectors 中的所有工厂方法一样。调用者看不到实际的容器类,因此,它可以是任何东西,包括私有内部类。在我的第四个示例中,容器类私有内部类,甚至是匿名类。
【解决方案2】:

听起来你想提供只要归约函数本身,而不是通用Collector 附带的所有其他东西。也许您正在寻找Collectors.reducing

public static <T> Collector<T,?,T> reducing(T identity, BinaryOperator<T> op)

然后,要对值求和,你会写

Collectors.reducing(0, (x, y) -> x + y);

或者,在上下文中,

Integer[] myList = new Integer[] { 1, 2, 3, 4 };
var collector = Collectors.reducing(0, (x, y) -> x + y);
System.out.println(Stream.of(myList).collect(collector)); // Prints 10

【讨论】:

  • 顺便说一句:除了减少收集器,也可以只使用reduce 方法。
  • 我故意提供了一个简单的例子,我确实写了“我知道我可以在这种情况下使用 reduce”。请查看stackoverflow.com/questions/74401764/… 以获得更详细的示例
  • 完整的Collector API 很详细设计.如果您在归约期间使用可变状态做一些混乱的事情,您希望您的代码发出巨大的红色信号弹,尖叫“我是可变的,请仔细阅读我”。如果你的归约函数很好并且引用透明,那么它绝对可以是单行的。但如果它是混乱和复杂的,那么它应该成为一个单独的班级。
【解决方案3】:

我想把重点放在你问题的某一点上,因为我觉得这可能是潜在混乱的症结所在。

如果我想创建我的自定义收集器,我需要创建两个类:

一种用于自定义累积类型 一个用于自定义收集器本身

不,您只需要创建一个班级,您的自定义累加器的那个。您应该使用the appropriate factory method 来实例化您的自定义Collector,就像您在问题中展示自己一样。

也许您的意思是说您需要创建两个实例。这也是不正确的;您需要创建一个Collector 实例,但要支持一般情况,许多可以创建累加器的实例(例如,groupingBy())。因此,您不能简单地自己实例化累加器,您需要将其Supplier 提供给Collector,并将根据需要实例化尽可能多的实例的能力委托给Collector

现在,想想你觉得缺少的重载Collectors.of() 方法,“更直接的方法来做到这一点”。显然,这种方法仍然需要Supplier,它会创建自定义累加器的实例。但是Stream.collect() 需要与您的自定义累加器实例交互,以执行累加和合并操作。所以Supplier 必须实例化类似Accumulator 接口的东西:

public interface Accumulator<T, A extends Accumulator<T, A, R>, R> {

    /**
     * @param t a value to be folded into this mutable result container
     */
    void accumulate(T t);

    /**
     * @param that another partial result to be merged with this container
     * @return the combined results, which may be {@code this}, {@code that}, or a new container
     */
    A combine(A that);

    /**
     * @return the final result of transforming this intermediate accumulator
     */
    R finish();

}

这样,就可以直接从Supplier&lt;Accumulator&gt; 创建Collector 实例:

    static <T, A extends Accumulator<T, A, R>, R> 
    Collector<T, ?, R> of(Supplier<A> supplier, Collector.Characteristics ... characteristics) {
        return Collector.of(supplier, 
                            Accumulator::accumulate, 
                            Accumulator::combine, 
                            Accumulator::finish, 
                            characteristics);
    }

然后,您就可以定义您的自定义Accumulator

final class Sum implements Accumulator<Integer, Sum, String> {

    private int value;

    @Override
    public void accumulate(Integer next) {
        value += next;
    }

    @Override
    public Sum combine(Sum that) {
        value += that.value;
        return this;
    }

    @Override
    public String finish(){
        return Integer.toString(value);
    }

}

并使用它:

String sum = ints.stream().collect(Accumulator.of(Sum::new, Collector.Characteristics.UNORDERED));

现在……它起作用了,而且什么都没有太可怕了,但是所有Accumulator&lt;A extends Accumulator&lt;A&gt;&gt; mumbo-jumbo 都比这“更直接”吗?

final class Sum {

    private int value;

    private void accumulate(Integer next) {
        value += next;
    }

    private Sum combine(Sum that) {
        value += that.value;
        return this;
    }

    @Override
    public String toString() {
        return Integer.toString(value);
    }

    static Collector<Integer, ?, String> collector() {
        return Collector.of(Sum::new, Sum::accumulate, Sum::combine, Sum::toString, Collector.Characteristics.UNORDERED);
    }

}

真的,为什么有一个Accumulator 专门收集到String?简化为自定义类型不是更有趣吗?类似于IntSummaryStatistics 的东西还有其他有用的方法,比如average()toString()?这种方法要强大得多,只需要一个(可变)类(结果类型),并且可以将其所有修改器封装为私有方法,而不是实现公共接口。

因此,欢迎您使用 Accumulator 之类的东西,但它并不能真正填补核心 Collector 曲目中的真正空白。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多