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表达式。
对于更复杂的例子,我不知道是否有合适的现有类型来保存int
和List
,但是您可以使用盒装的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