Java Stream Collector 高级用法:定制化累加器与收集器实现

java stream collector 高级用法:定制化累加器与收集器实现

本文深入探讨了 Java Stream API 中 Collector 的高级定制化实现,重点讲解了如何灵活选择和设计累加器类型 (A),以及如何使用 Collector.of() 方法构建自定义收集器。通过具体示例,展示了利用基本类型数组、现有可变类型、匿名内部类等作为累加器,有效避免了不必要的类创建,提升了代码的简洁性和灵活性。

理解 Collector 接口及其泛型

java.util.stream.Collector 接口是 Java Stream API 中一个核心组件,用于将流中的元素聚合成一个结果。它定义了三种泛型类型:

T:流中元素的类型。A:累加器(accumulator)的类型,这是一个可变的中间结果类型,用于在收集过程中存储状态。它通常是实现细节,不对外暴露。R:最终结果的类型,即收集操作完成后返回的类型。

要创建一个自定义的 Collector,最常用的方法是使用 Collector.of() 工厂方法,它需要四个核心函数式接口作为参数:

supplier: 一个 Supplier,用于创建新的累加器实例。accumulator: 一个 BiConsumer,用于将流中的单个元素 T 合并到累加器 A 中。combiner: 一个 BinaryOperator,用于将两个累加器 A 合并成一个(在并行流中特别有用)。finisher: 一个 Function,用于将最终的累加器 A 转换成最终结果 R。

一个常见的误解是,累加器类型 A 必须是一个独立的、专门为此目的创建的具名类,并且其方法(如 supply、accumulate、combine、finish)需要直接在 Collector 的实现中引用该类的静态方法或实例方法。实际上,Collector.of() 的函数参数提供了极大的灵活性,允许我们使用各种方式来定义这些行为,而无需强制创建额外的具名类。

灵活选择累加器类型

累加器类型 A 的选择是实现自定义 Collector 的关键。它不一定非得是一个复杂的自定义类,可以是以下几种类型:

立即学习“Java免费学习笔记(深入)”;

1. 使用基本类型数组作为累加器

对于简单的数值累加,一个单元素的基本类型数组(如 int[])是一个非常高效且简洁的累加器。它提供了可变性,并且避免了装箱拆箱的开销。

示例:计算流中整数的和

import java.util.stream.Collector;import java.util.stream.Stream;public class CustomSumCollector {    /**     * 创建一个收集器,用于计算整数流的总和。     * 累加器类型为 int[1],其中数组的第一个元素存储当前和。     *     * @return 收集整数到其总和的收集器。     */    public static Collector sum() {        return Collector.of(            () -> new int[1], // supplier: 创建一个长度为1的int数组作为累加器            (a, i) -> a[0] += i, // accumulator: 将元素i加到数组的第一个元素            (a, b) -> { a[0] += b[0]; return a; }, // combiner: 合并两个累加器            a -> a[0], // finisher: 返回数组的第一个元素作为最终结果            Collector.Characteristics.UNORDERED // 标识收集器不关心元素顺序        );    }    public static void main(String[] args) {        Stream numbers = Stream.of(1, 2, 3, 4, 5);        Integer totalSum = numbers.collect(sum());        System.out.println("Sum using custom collector: " + totalSum); // Output: Sum using custom collector: 15    }}

在这个例子中,int[1] 作为累加器 A,简洁地完成了累加任务。

2. 使用现有可变类型作为累加器

Java 标准库中提供了许多可变的类,它们可以直接用作累加器,特别是当需要考虑线程安全时。

示例:使用 AtomicInteger 计算流中整数的和(并发安全)

import java.util.concurrent.atomic.AtomicInteger;import java.util.stream.Collector;import java.util.stream.Stream;public class ConcurrentSumCollector {    /**     * 创建一个并发安全的收集器,用于计算整数流的总和。     * 累加器类型为 AtomicInteger。     *     * @return 并发收集整数到其总和的收集器。     */    public static Collector concurrentSum() {        return Collector.of(            AtomicInteger::new, // supplier: 创建一个新的 AtomicInteger 实例            AtomicInteger::addAndGet, // accumulator: 将元素i原子地加到 AtomicInteger 中            (a, b) -> { a.addAndGet(b.intValue()); return a; }, // combiner: 合并两个 AtomicInteger            AtomicInteger::intValue, // finisher: 返回 AtomicInteger 的值作为最终结果            Collector.Characteristics.UNORDERED,            Collector.Characteristics.CONCURRENT // 标识收集器支持并发        );    }    public static void main(String[] args) {        Stream numbers = Stream.of(1, 2, 3, 4, 5);        Integer totalSum = numbers.collect(concurrentSum());        System.out.println("Concurrent sum using custom collector: " + totalSum); // Output: Concurrent sum using custom collector: 15    }}

AtomicInteger 天然提供了线程安全的原子操作,非常适合在并行流中使用。

3. 使用匿名内部类或 Ad-hoc 类型作为累加器

当没有合适的现有类型,并且不想创建新的具名类时,可以使用匿名内部类或 Object 的子类(通常通过 new Object() {} 语法)作为累加器。这种方式将累加器的定义局部化在 Collector 的实现内部,增加了封装性

示例:收集 Map.Entry 流中具有最大值的键列表

假设我们有一个 Map.Entry 的流,我们希望找出所有具有最大值的键,并以 List 的形式返回。

方法一:使用 AbstractMap.SimpleEntry 作为累加器

AbstractMap.SimpleEntry 可以作为一个简单的键值对容器,这里我们用它来存储当前找到的最大值 (Integer) 和对应的键列表 (List)。

import java.util.*;import java.util.stream.Collector;import java.util.stream.Stream;public class KeysToMaximumCollector {    /**     * 收集 Map.Entry 流中具有最大值的键列表。     * 累加器类型为 AbstractMap.SimpleEntry<List, Integer>。     *     * @param  键的类型     * @return 收集器,返回具有最大值的键列表。     */    public static  Collector<Map.Entry, ?, List> keysToMaximum() {        return Collector.of(            () -> new AbstractMap.SimpleEntry(new ArrayList(), Integer.MIN_VALUE), // supplier: 初始累加器            (current, next) -> { // accumulator: 处理每个 Map.Entry                int max = current.getValue();                int value = next.getValue();                if (value >= max) {                    if (value > max) { // 找到更大的值,清空旧列表,更新最大值                        current.setValue(value);                        current.getKey().clear();                    }                    current.getKey().add(next.getKey()); // 添加当前键                }            },            (a, b) -> { // combiner: 合并两个累加器                int maxA = a.getValue();                int maxB = b.getValue();                if (maxA < maxB) return b; // B 的最大值更大,返回 B                if (maxA == maxB) a.getKey().addAll(b.getKey()); // 最大值相同,合并键列表                return a; // 否则返回 A            },            Map.Entry::getKey // finisher: 返回累加器中的键列表        );    }    public static void main(String[] args) {        Map map = new HashMap();        map.put("A", 10);        map.put("B", 20);        map.put("C", 15);        map.put("D", 20);        map.put("E", 5);        List keys = map.entrySet().stream().collect(keysToMaximum());        System.out.println("Keys with maximum value: " + keys); // Output: Keys with maximum value: [B, D] (顺序可能不同)    }}

方法二:使用匿名内部类作为累加器

这种方式创建了一个只在 Collector 内部可见的、具有特定字段的匿名对象作为累加器。

import java.util.*;import java.util.stream.Collector;import java.util.stream.Stream;public class KeysToMaximumAdHocCollector {    /**     * 收集 Map.Entry 流中具有最大值的键列表。     * 累加器类型为匿名内部类。     *     * @param  键的类型     * @return 收集器,返回具有最大值的键列表。     */    public static  Collector<Map.Entry, ?, List> keysToMaximum() {        return Collector.of(            () -> new Object() { // supplier: 创建一个匿名内部类实例作为累加器                int max = Integer.MIN_VALUE;                final List keys = new ArrayList();            },            (current, next) -> { // accumulator: 处理每个 Map.Entry                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) -> { // combiner: 合并两个匿名内部类累加器                if (a.max  a.keys // finisher: 返回累加器中的键列表        );    }    public static void main(String[] args) {        Map map = new HashMap();        map.put("A", 10);        map.put("B", 20);        map.put("C", 15);        map.put("D", 20);        map.put("E", 5);        List keys = map.entrySet().stream().collect(keysToMaximum());        System.out.println("Keys with maximum value (ad-hoc): " + keys); // Output: Keys with maximum value (ad-hoc): [B, D] (顺序可能不同)    }}

这两种方法都避免了为累加器单独创建一个具名类,使代码更加紧凑和局部化。

注意事项与总结

累加器类型的选择

简单场景:考虑使用基本类型数组(如 int[])或现有可变容器(如 ArrayList、StringBuilder)作为累加器,它们通常更高效且简洁。并发场景:使用 AtomicInteger、ConcurrentHashMap 等并发工具类作为累加器,并添加 Collector.Characteristics.CONCURRENT 特性。复杂场景或封装需求:当需要封装多个状态时,可以考虑 AbstractMap.SimpleEntry 或匿名内部类。如果逻辑非常复杂,或者该累加器类型需要在多个 Collector 中复用,那么创建一个具名类仍然是更好的选择。

函数式接口的实现

supplier、accumulator、combiner、finisher 可以是方法引用 (ClassName::methodName) 或 Lambda 表达式。它们不要求是累加器类型 A 自身的成员方法,这提供了极大的灵活性。combiner 函数的正确实现对于并行流至关重要。它必须能够正确地将两个部分累加器合并为一个。finisher 函数负责将中间的累加器状态转换为最终结果。如果累加器本身就是最终结果,finisher 可以简单地返回累加器本身(即 Function.identity())。

Collector.Characteristics

UNORDERED:表示收集器不关心流的顺序。CONCURRENT:表示收集器可以支持并行执行,即 accumulator 可以被多个线程调用,并且 combiner 可以被跳过(如果 supplier 返回一个线程安全的可变结果容器)。IDENTITY_FINISH:表示 finisher 函数是一个恒等函数(Function.identity()),即累加器类型 A 和结果类型 R 相同。

通过灵活运用 Collector.of() 方法,并根据实际需求选择合适的累加器类型,开发者可以创建出高效、简洁且功能强大的自定义收集器,从而更好地利用 Java Stream API 的能力。无需为每个自定义收集器都创建独立的累加器类,这大大简化了代码结构和维护成本。

以上就是Java Stream Collector 高级用法:定制化累加器与收集器实现的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/67259.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
Excel中插入文件附件方法
上一篇 2025年11月12日 11:19:06
缺少任务追踪机制会导致什么问题
下一篇 2025年11月12日 11:20:28

相关推荐

  • composer require-dev和require有什么不同_Composer Require与Require-Dev区别解析

    require用于声明项目运行必需的依赖,如框架、数据库组件和第三方SDK,这些包会随项目部署到生产环境;2. require-dev用于声明仅在开发和测试阶段需要的工具,如PHPUnit、PHPStan、Faker等,不会默认部署到生产环境;3. 安装时composer install根据环境决定…

    2026年5月10日
    1000
  • 修复Django电商项目中AJAX过滤产品列表图片不显示问题

    在Django电商项目中,当使用AJAX动态加载过滤后的产品列表时,常遇到图片无法正常显示的问题。这通常是由于前端模板中图片加载方式(如data-setbg属性结合JavaScript库)与AJAX动态内容更新机制不兼容所致。解决方案是直接在AJAX返回的HTML中使用标准的标签来渲染图片,确保浏览…

    2026年5月10日
    000
  • Matplotlib 地图中多类型图例的创建与优化

    Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化

    本教程旨在解决matplotlib地图可视化中,如何在一个图例中同时展示颜色块(如区域分类)和自定义标记(如特定兴趣点)的问题。文章详细介绍了当传统`patch`对象无法正确显示标记时,如何利用`matplotlib.lines.line2d`创建标记图例句柄,并将其与颜色块图例句柄合并,从而生成一…

    2026年5月10日 用户投稿
    100
  • Golang JSON序列化:控制敏感字段暴露的最佳实践

    本教程探讨golang中如何高效控制结构体字段在json序列化时的可见性。当需要将包含敏感信息的结构体数组转换为json响应时,通过利用`encoding/json`包提供的结构体标签,特别是`json:”-“`,可以轻松实现对特定字段的忽略,从而避免敏感数据泄露,确保api…

    2026年5月10日
    000
  • 利用海象运算符简化条件赋值:Python教程与最佳实践

    本文旨在探讨Python中海象运算符(:=)在条件赋值场景下的应用。通过对比传统if/else语句与海象运算符,以及条件表达式,分析海象运算符在简化代码、提高可读性方面的优势与局限性。并通过具体示例,展示如何在列表推导式等场景下合理使用海象运算符,同时强调其潜在的复杂性及替代方案,帮助开发者更好地掌…

    2026年5月10日
    000
  • Debian syslog性能优化技巧有哪些

    提升Debian系统syslog (通常基于rsyslog)性能,关键在于精简配置和高效处理日志。以下策略能有效优化日志管理,提升系统整体性能: 精简配置,高效加载: 在rsyslog配置文件中,仅加载必要的输入、输出和解析模块。 使用全局指令设置日志级别和格式,避免不必要的处理。 自定义模板: 创…

    2026年5月10日
    000
  • 比特币新手教程 比特币交易平台有哪些

    比特币是一种去中心化的数字货币,基于区块链技术实现点对点交易,具有匿名性、有限发行和不可篡改等特点;新手可通过交易所购买,P2P交易获得比特币,常用平台包括Binance、OKX和Huobi;交易流程包括注册账户、实名认证、绑定支付方式、充值法币并下单购买,可选择市价单或限价单;比特币存储方式有交易…

    2026年5月10日
    000
  • c++中的SFINAE技术是什么_c++模板编程中的SFINAE原理与应用

    SFINAE 是“替换失败不是错误”的原则,指模板实例化时若参数替换导致错误,只要存在其他合法候选,编译器不报错而是继续重载决议。它用于条件启用模板、类型检测等场景,如通过 decltype 或 enable_if 控制函数重载,实现类型特征判断。尽管 C++20 引入 Concepts 简化了部分…

    2026年5月10日
    000
  • Go语言mgo查询构建:深入理解bson.M与日期范围查询的正确实践

    本文旨在解决go语言mgo库中构建复杂查询时,特别是涉及嵌套`bson.m`和日期范围筛选的常见错误。我们将深入剖析`bson.m`的类型特性,解释为何直接索引`interface{}`会导致“invalid operation”错误,并提供一种推荐的、结构清晰的代码重构方案,以确保查询条件能够正确…

    2026年5月10日
    100
  • RichHandler与Rich Progress集成:解决显示冲突的教程

    在使用rich库的`richhandler`进行日志输出并同时使用`progress`组件时,可能会遇到显示错乱或溢出问题。这通常是由于为`richhandler`和`progress`分别创建了独立的`console`实例导致的。解决方案是确保日志处理器和进度条组件共享同一个`console`实例…

    2026年5月10日
    000
  • 修复点击时按钮抖动:CSS垂直对齐实践

    本文探讨了在Web开发中,交互式按钮(如播放/暂停按钮)在点击时发生意外垂直位移的问题。通过分析CSS样式变化对元素布局的影响,我们发现这是由于按钮不同状态下的边框样式和内边距改变,以及默认的垂直对齐行为共同作用所致。核心解决方案是利用CSS的vertical-align属性,将其设置为middle…

    2026年5月10日
    000
  • Golang goroutine与channel调试技巧

    使用go run -race检测数据竞争,结合runtime.NumGoroutine监控协程数量,通过pprof分析阻塞调用栈,利用select超时避免永久阻塞,有效排查goroutine泄漏、死锁和数据竞争问题。 Go语言的goroutine和channel是并发编程的核心,但它们也带来了调试上…

    2026年5月10日
    000
  • 《魔兽世界》将于6月11日开启国服回归技术测试

    《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试

    《%ign%ignore_a_1%re_a_1%》官方宣布,将于6月11日开启国服回归技术测试,时间为7天,并称可以在6月内正式开服,玩家们可以访问官网下载战网客户端并预下载“巫妖王之怒”客户端,技术测试详情见下图。 WordAi WordAI是一个AI驱动的内容重写平台 53 查看详情 以上就是《…

    2026年5月10日 用户投稿
    200
  • 使用 Jupyter Notebook 进行探索性数据分析

    Jupyter Notebook通过单元格实现代码与Markdown结合,支持数据导入(pandas)、清洗(fillna)、探索(matplotlib/seaborn可视化)、统计分析(describe/corr)和特征工程,便于记录与分享分析过程。 Jupyter Notebook 是进行探索性…

    2026年5月10日
    000
  • 如何在HTML中插入表单元素_HTML表单控件与输入类型使用指南

    HTML表单通过标签构建,包含action和method属性定义数据提交目标与方式,常用input类型如text、password、email等适配不同输入需求,配合label、required、placeholder提升可用性,结合textarea、select、button等控件实现完整交互,是…

    2026年5月10日
    000
  • 前端缓存策略与JavaScript存储管理

    根据数据特性选择合适的存储方式并制定清晰的读写与清理逻辑,能显著提升前端性能;合理运用Cookie、localStorage、sessionStorage、IndexedDB及Cache API,结合缓存策略与定期清理机制,可在保证用户体验的同时避免安全与性能隐患。 前端缓存和JavaScript存…

    2026年5月10日
    100
  • 网站标题关键词更新后,搜索引擎为何仍显示旧标题?

    网站标题更新后,搜索引擎为何显示旧标题? 网站SEO优化中,站长常修改网站标题关键词,期望搜索结果显示自定义标题。然而,即使更新标签、meta keywords、meta description和结构化数据中的name属性后,搜索结果仍显示旧标题,这令人费解。本文将对此进行解释。 问题:站长修改了网…

    2026年5月10日
    100
  • HTML5网页如何实现手势操作 HTML5网页移动端交互的处理技巧

    首先利用原生touch事件实现滑动判断,再通过preventDefault解决滚动冲突,接着引入Hammer.js处理复杂手势,最后通过优化点击区域、避免事件冲突和增加视觉反馈提升体验。 在移动端浏览器中,HTML5网页可以通过触摸事件实现手势操作,提升用户体验。虽然原生JavaScript提供了基…

    2026年5月10日
    000
  • 创建指定大小并填充特定数据的Golang文件教程

    本文将介绍如何使用Golang创建一个指定大小的文件,并用特定数据填充它。我们将使用 `os` 包提供的函数来创建和截断文件,从而实现快速生成大文件的目的。示例代码展示了如何创建一个10MB的文件,并将其填充为全零数据。掌握这些方法,可以方便地在例如日志系统或磁盘队列等场景中,预先创建测试文件或初始…

    2026年5月10日
    000
  • Python命令怎样使用profile分析脚本性能 Python命令性能分析的基础教程

    使用Python的cProfile模块分析脚本性能最直接的方式是通过命令行执行python -m cProfile your_script.py,它会输出每个函数的调用次数、总耗时、累积耗时等关键指标,帮助定位性能瓶颈;为进一步分析,可将结果保存为文件python -m cProfile -o ou…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信