Java实现的累加器类提供了一个收集器



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等)。

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

这就是sum收集器的典型实现方式

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

我想把重点放在你问题中一点的措辞上,因为我觉得这可能是潜在困惑的症结所在。

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

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

不,你只需要创建一个类,就是你的自定义累加器。您应该使用适当的工厂方法来实例化您的自定义Collector,正如您在问题中演示的那样。

也许您的意思是需要创建两个实例。这也是不正确的;您需要创建一个Collector实例,但是为了支持一般情况,可以创建多个accumulator实例(例如,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));

现在…它工作了,没有什么可怕的,但是所有的Accumulator<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);
}
}

说真的,为什么要把Accumulator专门用来收集String呢?简化为自定义类型不是更有趣吗?沿着IntSummaryStatistics的路线,有其他有用的方法,如average()toString()?这种方法更强大,只需要一个(可变的)类(结果类型),并且可以将其所有的mutator封装为私有方法,而不是实现公共接口。

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

听起来您只想提供还原函数本身,而不是通用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

相关内容

  • 没有找到相关文章

最新更新