Accumulative
是针对Collector<T, A, R>
的中间累积类型A
提出的接口Collector<T, A, R>
以使定义自定义Java Collector
更加容易。
介绍
如果您曾经使用过Java Stream
,那么很可能使用了一些Collector
,例如:
-
Collectors.toList
-
Collectors.toMap
但是你有没有使用过……
- 组成的
Collector
?- 它使用另一个
Collector
作为参数,例如:Collectors.collectingAndThen
。
- 它使用另一个
- 定制
Collector
?- 其功能在
Collector.of
明确指定。
- 其功能在
这篇文章是关于custom Collector
的。
集电极
让我们回想一下Collector
合同的本质 (我的评论):
/** * @param <T> (input) element type * @param <A> (intermediate) mutable accumulation type (container) * @param <R> (output) result type */ public interface Collector<T, A, R> { Supplier<A> supplier(); // create a container BiConsumer<A, T> accumulator(); // add to the container BinaryOperator<A> combiner(); // combine two containers Function<A, R> finisher(); // get the final result from the container Set<Characteristics> characteristics(); // irrelevant here }
上面的合同本质上是功能性的,这非常好! 这使我们可以使用任意累积类型( A
)创建Collector
,例如:
-
A
:StringBuilder
(Collectors.joining
) -
A
:OptionalBox
(Collectors.reducing
) -
A
:long[]
(Collectors.averagingLong
)
提案
在我提供任何理由之前,我将提出建议,因为它很简短。 该提议的完整源代码可以在GitHub上找到 。
累积接口
我建议将以下称为Accumulative
(名称待讨论)的接口添加到JDK:
public interface Accumulative<T, A extends Accumulative<T, A, R>, R> { void accumulate(T t); // target for Collector.accumulator() A combine(A other); // target for Collector.combiner() R finish(); // target for Collector.finisher() }
与Collector
相反,此接口本质上是面向对象的 ,实现该接口的类必须表示某种可变状态 。
过载收集器
具有Accumulative
,我们可以添加以下Collector.of
重载:
public static <T, A extends Accumulative<T, A, R>, R> Collector<T, ?, R> of( Supplier<A> supplier, Collector.Characteristics... characteristics) { return Collector.of(supplier, A::accumulate, A::combine, A::finish, characteristics); }
普通开发者故事
在本部分中,我将展示该建议会对普通开发人员产生怎样的影响,而一般开发人员仅了解 Collector API的基础知识 。 如果您精通此API,请在继续阅读之前尽力想象您不知道。
例
让我们重用我最近的文章中的示例(进一步简化)。 假设我们有一个Stream
:
interface IssueWiseText { int issueLength(); int textLength(); }
并且我们需要计算问题覆盖率 :
总发行时长
─────────────
总文字长度
此要求转换为以下签名:
Collector<IssueWiseText, ?, Double> toIssueCoverage();
解
一般的开发人员可能会决定使用自定义累积类型A
来解决此问题(不过其他解决方案也是可能的 )。 假设开发人员将其命名为CoverageContainer
这样:
-
T
:IssueWiseText
-
A
:CoverageContainer
-
R
:Double
在下面,我将展示这样的开发人员如何实现CoverageContainer
的结构 。
无累积结构
注意 :本节很长,目的是说明该过程对于没有使用Collector
的开发人员可能有多复杂 。 如果您已经意识到这一点,则可以跳过它
如果没有Accumulative
,则开发人员将查看Collector.of
,并看到四个主要参数:
-
Supplier<A> supplier
-
BiConsumer<A, T> accumulator
-
BinaryOperator<A> combiner
-
Function<A, R> finisher
要处理Supplier <A> supplier
,开发人员应:
- 在
Supplier<A>
中用心理替代A
获得Supplier<CoverageContainer>
- 在精神上将签名解析为
CoverageContainer get ()
- 回想一下JavaDoc for
Collector.supplier()
- 第四种调用方法的引用 ( 对构造函数的引用 )
- 意识到
supplier = CoverageContainer::new
要处理BiConsumer <A, T> accumulator
,开发人员应:
-
BiConsumer<CoverageContainer, IssueWiseText>
-
void accept (CoverageContainer a, IssueWiseText t)
- 在精神上将签名转换为一种实例方法
void accumulate(IssueWiseText t)
- 第三种调用方法的引用 ( 引用特定类型的任意对象的实例方法 )
- 意识到
accumulator = CoverageContainer::accumulate
处理BinaryOperator <A> combiner
:
-
BinaryOperator<CoverageContainer>
-
CoverageContainer apply (CoverageContainer a, CoverageContainer b)
-
CoverageContainer combine(CoverageContainer other)
-
combiner = CoverageContainer::combine
要处理Function <A, R> finisher
:
-
Function<CoverageContainer, Double>
-
Double apply (CoverageContainer a)
-
double issueCoverage()
-
finisher = CoverageContainer::issueCoverage
这个漫长的过程导致:
class CoverageContainer { void accumulate(IssueWiseText t) { } CoverageContainer combine(CoverageContainer other) { } double issueCoverage() { } }
开发人员可以定义toIssueCoverage()
(必须以正确的顺序提供参数):
Collector<IssueWiseText, ?, Double> toIssueCoverage() { return Collector.of( CoverageContainer:: new , CoverageContainer::accumulate, CoverageContainer::combine, CoverageContainer::finish ); }
累积结构
现在, 使用 Accumulative
,开发人员将查看新的Collector.of
重载,并且将仅看到一个主要参数:
-
Supplier<A> supplier
和一个有界类型参数 :
-
A extends Accumulative<T, A, R>
因此,开发人员将自然而然地开始- 实施 Accumulative<T, A, R>
并第一次和最后一次解析T
, A
和R
:
class CoverageContainer implements Accumulative<IssueWiseText, CoverageContainer, Double> { }
此时,一个不错的IDE会抱怨该类必须实现所有抽象方法。 而且,这是最美丽的部分 ,它将提供快速修复。 在IntelliJ中,您单击“ Alt + Enter”→“实施方法”,然后…就完成了!
class CoverageContainer implements Accumulative<IssueWiseText, CoverageContainer, Double> { @Override public void accumulate(IssueWiseText issueWiseText) { } @Override public CoverageContainer combine(CoverageContainer other) { return null ; } @Override public Double finish() { return null ; } }
因此,您不必摆弄类型,手动编写任何内容或命名任何内容!
哦,是的-您仍然需要定义toIssueCoverage()
,但是现在很简单:
Collector<IssueWiseText, ?, Double> toIssueCoverage() { return Collector.of(CoverageContainer:: new ); }
那不是很好吗?
实作
这里的实现无关紧要,因为这两种情况( diff )几乎相同。
基本原理
程序太复杂
我希望我已经演示了如何定义自定义Collector
是一个挑战。 我必须说,即使我总是不愿意定义一个。 但是,我也感觉到-有了Accumulative
,这种勉强就会消失,因为该过程将缩小为两个步骤:
- 实现
Accumulative<T, A, R>
- 调用
Collector.of(YourContainer::new)
推动实施
JetBrains创造了“ 发展动力 ”,我想将其转变为“实施动力”。
由于Collector
是一个简单的功能的设备中,这通常是没有意义的(据我可以告诉)来实现它(也有例外 )。 但是,通过Google搜索“实施收集器”可以看到(约5000个结果)人们正在这样做。
这很自然,因为要在Java中创建“自定义” TYPE
,通常会扩展/实现TYPE
。 实际上,即使是经验丰富的开发人员(例如Java冠军Tomasz Nurkiewicz )也可以做到这一点。
总结起来,人们感到有实现的动力 ,但在这种情况下,JDK没有为他们提供实现的任何东西。 Accumulative
可以填补这一空白……
相关例子
最后,我搜索了一些示例,这些示例可以轻松实现Accumulative
。
在OpenJDK(尽管这不是目标位置)中,我发现了两个:
-
Collectors.reducing
( diff ) -
Collectors.teeing
( diff )
对堆栈溢出,虽然,我发现大量的: 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 , 32 , 33 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 41 , 42 , 43 , 44 , 45 , 46 , 47 , 48 , 49 , 50 , 51 , 52 , 53 。
我还发现了一些基于数组的示例,可以将其重构为Accumulative
以获得更好的可读性: a , b , c 。
命名
Accumulative
不是最好的名字,主要是因为它是一个形容词 。 但是,我选择它是因为:
- 我希望名称以
A
开头(如<T, A, R>
), - 我最好的候选人(
Accumulator
)已经被BiConsumer<A, T> accumulator()
, -
AccumulativeContainer
似乎太长。
在OpenJDK中, A
称为:
- 可变结果容器
- 累积类型
- 容器
- 州
- 框
提示以下替代方法:
-
AccumulatingBox
-
AccumulationState
-
Collector.Container
-
MutableResultContainer
当然,如果这个想法被接受,这个名字将通过“传统”的名字
摘要
在本文中,我建议向JDK添加Accumulative
接口和新的Collector.of
重载。 有了它们,开发人员将不再费劲地创建自定义Collector
。 取而代之的是,它只是成为“执行合同”和“引用构造函数”。
换句话说,该提案旨在降低进入“定制Collector
世界的门槛 !
附录
下面的可选阅读。
解决方案示例:JDK 12+
在JDK 12+中,由于Collectors.teeing
( JDK-8209685 ),我们将toIssueCoverage()
定义为组合的Collector
。
static Collector<IssueWiseText, ?, Double> toIssueCoverage() {return Collectors.teeing(Collectors.summingInt(IssueWiseText::issueLength),Collectors.summingInt(IssueWiseText::textLength),(totalIssueLength, totalTextLength) -> (double) totalIssueLength / totalTextLength);
}
上面的内容很简洁,但是对于Collector API新手来说,可能很难遵循。
示例解决方案:JDK方法
另外, toIssueCoverage()
可以定义为:
static Collector<IssueWiseText, ?, Double> toIssueCoverage() {return Collector.of(() -> new int[2],(a, t) -> { a[0] += t.issueLength(); a[1] += t.textLength(); },(a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },a -> (double) a[0] / a[1]);
}
我称其为“ JDK方式”,因为某些Collector
的实现与OpenJDK中的实现类似(例如Collector.averagingInt
)。
但是,尽管这样的简洁代码可能适用于OpenJDK,但由于可读性高(这很低,我称之为cryptic ),因此它肯定不适合业务逻辑。
翻译自: https://www.javacodegeeks.com/2019/02/accumulative-custom-java-collectors.html