累积:轻松自定义Java收集器

Accumulative是针对Collector<T, A, R>的中间累积类型A提出的接口Collector<T, A, R>以使定义自定义Java Collector更加容易。

介绍

如果您曾经使用过Java Stream ,那么很可能使用了一些Collector ,例如:

但是你有没有使用过……

  1. 组成的 Collector
  2. 定制 Collector

这篇文章是关于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 ,例如:

提案

在我提供任何理由之前,我将提出建议,因为它很简短。 该提议的完整源代码可以在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这样:

  • TIssueWiseText
  • ACoverageContainer
  • RDouble

在下面,我将展示这样的开发人员如何实现CoverageContainer结构

无累积结构

注意 :本节很长,目的是说明该过程对于没有使用Collector的开发人员可能有多复杂 如果您已经意识到这一点则可以跳过它

如果没有Accumulative ,则开发人员将查看Collector.of ,并看到四个主要参数:

  1. Supplier<A> supplier
  2. BiConsumer<A, T> accumulator
  3. BinaryOperator<A> combiner
  4. Function<A, R> finisher

要处理Supplier <A> supplier ,开发人员应:

  1. Supplier<A>中用心理替代A获得Supplier<CoverageContainer>
  2. 在精神上将签名解析为CoverageContainer get ()
  3. 回想一下JavaDoc for Collector.supplier()
  4. 第四种调用方法的引用对构造函数的引用
  5. 意识到supplier = CoverageContainer::new

要处理BiConsumer <A, T> accumulator ,开发人员应:

  1. BiConsumer<CoverageContainer, IssueWiseText>
  2. void accept (CoverageContainer a, IssueWiseText t)
  3. 在精神上将签名转换为一种实例方法
    void accumulate(IssueWiseText t)
  4. 第三种调用方法的引用引用特定类型的任意对象的实例方法
  5. 意识到accumulator = CoverageContainer::accumulate

处理BinaryOperator <A> combiner

  1. BinaryOperator<CoverageContainer>
  2. CoverageContainer apply (CoverageContainer a, CoverageContainer b)
  3. CoverageContainer combine(CoverageContainer other)
  4. combiner = CoverageContainer::combine

要处理Function <A, R> finisher

  1. Function<CoverageContainer, Double>
  2. Double apply (CoverageContainer a)
  3. double issueCoverage()
  4. 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重载,并且将仅看到一个主要参数:

  1. Supplier<A> supplier

和一个有界类型参数

  • A extends Accumulative<T, A, R>

因此,开发人员将自然而然地开始- 实施 Accumulative<T, A, R>第一次和最后一次解析TAR

 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 ,这种勉强就会消失,因为该过程将缩小为两个步骤:

  1. 实现Accumulative<T, A, R>
  2. 调用Collector.of(YourContainer::new)

推动实施

JetBrains创造了“ 发展动力 ”,我想将其转变为“实施动力”。

由于Collector是一个简单的功能的设备中,这通常是没有意义的(据我可以告诉)来实现它(也有例外 )。 但是,通过Google搜索“实施收集器”可以看到(约5000个结果)人们正在这样做。

这很自然,因为要在Java中创建“自定义” TYPE ,通常会扩展/实现TYPE 。 实际上,即使是经验丰富的开发人员(例如Java冠军Tomasz Nurkiewicz )也可以做到这一点。

总结起来,人们感到有实现动力 ,但在这种情况下,JDK没有为他们提供实现的任何东西。 Accumulative可以填补这一空白……

相关例子

最后,我搜索了一些示例,这些示例可以轻松实现Accumulative

在OpenJDK(尽管这不是目标位置)中,我发现了两个:

  1. Collectors.reducingdiff
  2. Collectors.teeingdiff

对堆栈溢出,虽然,我发现大量的: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253

我还发现了一些基于数组的示例,可以将其重构Accumulative以获得更好的可读性: abc

命名

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.teeingJDK-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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值