实现一个Java累加器类,该类提供了一个收集器(Collector)。

3

Collector有三个泛型类型:

public interface Collector<T, A, R>

A 是可变累加类型的缩写(通常作为实现细节而隐藏)。

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

  • 一个是自定义累加类型
  • 一个是自定义收集器本身

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

简单例子

这个例子非常简单,仅用于说明问题,我知道我可以在这种情况下使用reduce方法,但这不是我要找的。这里有一个更复杂的例子,分享它会使问题变得太长,但是它是相同的思路。

假设我想将流的总和收集并返回为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<Integer, SumCollector, String>,但然后每个函数都会重复(supplier()将返回SumCollector::supply等)。


我认为你总是需要两个类。一个将始终是累加器对象。另一个将实现“Collector”接口。但是,累加器对象不包含所有这些“supply()”,“combine()”和“finish()”方法。它们只能在实现“Collector”的类中使用。持有者类也可以是收集器中的私有内部“class”。此外,对于您的示例,您可以只使用“AtomicInteger”作为累加器。这样,您就只需要实现一个单一的类“SumCollector implements Collector<Integer, AtomicInteger, String>”。 - Lino
持有者类也可以是收集器中的私有内部类。 => 我认为我无法这样做,因为如果我执行implements Collector<Integer, SumCollector.Acc, String>,我会得到SumCollector.Acc'在'SumCollector'中具有私有访问权限的错误。 - Ricola
1
哦,是的,那么它必须是“public”。你也可以颠倒整个类结构。将“Collector”作为累加器的私有内部类。然后仅通过静态方法公开它:“public static Collector<Integer, ?, String> collector() {return new SumCollector();}” - Lino
3个回答

3

函数无需实现为容器类的方法。

这是通常实现这样一个总和收集器的方式。

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

但您也可以创建一个新的专用容器类作为临时类型,不会在特定收集器外可见。

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

2
@Lino 我认为,如果函数很短并且彼此靠近,以便您可以一眼查看声明和所有用途,那么这是可以接受的。对于这个具体的例子,由于函数较长,已经处于边缘地带了。这更多是为了完整性。 - Holger
1
@Ricola,我不太清楚你所说的“累加器类”是什么意思。也许像这个答案中的SummaryStatistics?它作为容器类,并提供了一个用于收集器的工厂方法。对于已经存在或未公开的容器类,您只需要提供工厂方法,就像Collectors一样。 - Holger
类似于“Collector.fromAccumulator(new SumCollector())”这样的东西,但显然它并不存在。 - Ricola
2
收集器有一个容器供应商是有原因的。它必须能够在需要时生成多个实例,例如与 groupingBy 结合使用或进行并行评估时。从单个实例创建收集器将违背整个概念。另一个矛盾之处在于,收集器的用户不想处理临时容器。在您的两个示例中,最终结果类型与容器类型不同,我甚至为每个示例提供了两个具有不同容器类型的收集器实现。 - Holger
2
为什么你声称封装容器类不可能?我的四个例子都能隐藏容器类。前两个字面上使用Collector<Integer, ?, Integer>作为工厂方法的返回类型,就像Collectors中的所有工厂方法一样。调用者看不到实际的容器类,因此它可以是任何东西,包括私有内部类。在我的第四个例子中,容器类一个私有内部类,甚至是匿名类。 - Holger
显示剩余4条评论

2

看起来你只想提供减少函数本身,而不是与通用收集器一起提供的所有其他内容。也许你正在寻找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

1
只是顺便提一下:除了使用reducing collector之外,也可以只使用reduce方法。 - Lino
我特意提供了一个简单的例子,并且我确实写道“我知道我可以在这种情况下使用reduce”。请查看https://dev59.com/Ecv6oIgBc1ULPQZFu9WO#74401856以获取更详细的示例。 - Ricola
2
完整的Collector API是有意设计成冗长的。如果在规约过程中使用可变状态进行混乱操作,您希望您的代码发出巨大的红色信号火箭,呼喊着“我是可变的,请仔细阅读我”。如果您的规约函数很好地保持了引用透明性,那么它绝对可以成为一行代码。但如果它很混乱和复杂,那么它应该是一个单独的类。 - Silvio Mayolo

2
我想重点关注你问题中的一个要点,因为我觉得它可能是潜在混淆的关键。
如果我想创建自定义收集器,我需要创建两个类:
一个用于自定义积累类型 一个用于自定义收集器本身
不,你只需要创建一个自定义积累器的类。你应该使用适当的工厂方法来实例化你的自定义收集器,就像你在问题中所演示的那样。
也许你的意思是需要创建两个实例。但这也是不正确的;你需要创建一个 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<Accumulator>创建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));

现在它可以工作了,而且没有什么太可怕的地方,但是所有这些“累加器<A extends Accumulator<A>>”的术语比这更“直接”吗?
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);
    }

}

“而且,为什么要专门使用累加器来收集字符串呢?将其缩减至自定义类型会更有趣吧?比如说类似于IntSummaryStatistics的东西,它除了toString()方法外还有其他有用的方法,例如average()。这种方法更加强大,只需要一个(可变)类(结果类型),并且可以将所有的修改器封装在私有方法中,而不是实现公共接口。”
“因此,你可以使用像Accumulator这样的东西,但它实际上并没有填补核心Collector库中的一个真正空缺。”

“为什么要有一个专门用于收集字符串的累加器” => 当然,这只是为了举例而已。你提到 IntSummaryStatistics 很好,因为我看到它们使用 collect(IntSummaryStatistics::new, IntSummaryStatistics::accept, IntSummaryStatistics::combine),这暗示着这样的方法可能会有用。但是,如果我们想在标准库中拥有这样的方法/接口,每个流类型都需要一个,正如你所指出的那样,这可能不值得。 - Ricola

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接