Java Stream distinct() 方法详解:高效实现 List 对象字段去重
1. 项目概述从“去重”这个高频痛点说起在Java开发中处理List集合几乎是每天都要面对的日常。无论是从数据库查询出的结果集还是从外部接口接收到的数据我们常常会遇到一个恼人的问题数据重复。想象一下你从多个数据源聚合了一个用户列表准备批量发送通知结果因为重复数据导致同一个用户收到了好几条一模一样的消息或者你在做数据统计时重复的条目让最终的计数和金额完全失真。这种时候“去重”就成了一个必须立刻解决的技术动作。传统的去重方法比如借助HashSet的无序唯一性或者遍历List进行手动比对虽然能解决问题但代码往往显得冗长且在处理对象集合时不够优雅。特别是当需求变为“根据对象中的某个特定字段进行去重”时比如一个ListUser要求根据用户的id或者name来去重传统方法就需要我们手动实现equals()和hashCode()或者使用TreeSet并传入自定义比较器步骤繁琐。而Java 8引入的Stream API尤其是其中的distinct()方法配合filter、map等操作为我们提供了一种声明式、函数式的解决方案。它让代码的意图更加清晰将“怎么做”的过程式思维转变为了“做什么”的声明式思维。今天我们就来深入聊聊如何利用Java 8 Stream特别是围绕distinct()方法高效、优雅地解决List集合尤其是对象集合根据字段去重的各种场景。你会发现掌握了这些技巧之前那些需要写十几行代码的逻辑现在可能一行就能搞定而且可读性和可维护性都大大提升。2. 核心思路与方案选型背后的考量面对一个List去重的需求我们的大脑里通常会闪过几个方案。选择哪一个不仅仅取决于能否实现更取决于代码的简洁性、性能、可读性以及后续的维护成本。我们来逐一拆解这些方案背后的逻辑。2.1 传统方案回顾与优劣分析在Java 8之前我们主要有以下几种武器使用HashSet进行整体去重这是最经典的方法。HashSet基于哈希表实现其add方法会利用元素的equals()和hashCode()来判断唯一性。将List直接放入HashSet再转回List即可去重。优点时间复杂度接近O(n)性能通常很好。缺点要求集合中的元素正确重写了equals()和hashCode()方法。对于根据对象某个字段去重如果该字段不是对象判断相等的依据此方法无效。除非你为了这次去重临时修改这两个方法但这会破坏对象原有的相等语义是危险的做法。使用TreeSet配合自定义ComparatorTreeSet是基于红黑树实现的有序集合它依赖元素的自然排序或者传入的Comparator来保证唯一性。优点可以非常灵活地根据自定义的比较逻辑比如对象的某个字段来定义“唯一性”完美解决根据字段去重的问题。缺点TreeSet的插入和查询时间复杂度是O(log n)在数据量较大时可能略慢于HashSet。同时它要求元素要么实现Comparable接口要么提供Comparator并且这个比较逻辑必须与equals()保持一致虽然不强制但最佳实践如此否则可能产生违反直觉的行为。双重循环遍历比对最原始的方法外层循环遍历每个元素内层循环检查后续是否有重复发现重复则移除。优点无需任何额外条件逻辑完全可控。缺点时间复杂度是O(n²)性能极差仅适用于极小数据量。代码也最为冗长。2.2 Java 8 Streamdistinct()的方案解析Java 8的Stream API带来了全新的编程范式。distinct()是Stream的一个中间操作它返回一个由原流中不重复元素组成的新流。它的去重依据同样是元素的equals()和hashCode()方法。那么它如何解决“根据字段去重”这个核心痛点呢关键在于Stream的操作是链式的、可组合的。我们不能直接让distinct()去比较字段但我们可以通过其他操作转换流的元素让distinct()作用于我们“希望它比较”的那个部分。核心思路有两种映射去重法使用map操作将对象流映射为某个字段的流比如id的流对这个字段流进行distinct()去重然后再通过某种方式关联回原对象。这通常需要后续的收集操作配合。过滤收集法利用Collectors.collectingAndThen或自定义收集器在收集过程中维持一个“已见字段”的集合如HashSet通过filter操作筛选出首次出现的记录。为什么选择Stream方案声明式编程代码清晰表达了“我要根据id去重”的意图而不是“我要如何循环、如何比较”。链式调用多个操作过滤、映射、去重、收集可以流畅地连接在一起形成一条清晰的数据处理流水线。易于并行化Stream API天然支持并行流parallelStream()对于大数据集可以轻松利用多核优势提升去重性能。更强的表达能力可以轻松组合其他操作例如先去重再排序或者先过滤再根据字段去重。注意distinct()是一个有状态的中断操作在处理无限流或并行流时需要留意其开销。对于并行流distinct()需要维护一个共享的ConcurrentHashMap来跟踪已见元素这会带来一定的性能开销。在数据量不大或顺序流中这个开销通常可以忽略。3. 核心细节解析与多种场景实操理论说再多不如一行代码。下面我们通过一个具体的实体类User来演示各种去重场景。假设我们有如下ListUserData // 使用Lombok简化代码 AllArgsConstructor class User { private Long id; private String name; private String city; // 假设没有重写 equals 和 hashCode } ListUser users Arrays.asList( new User(1L, 张三, 北京), new User(2L, 李四, 上海), new User(1L, 张三, 广州), // id重复 new User(3L, 王五, 深圳), new User(2L, 李四, 杭州) // id重复 );我们的目标是根据id字段去重保留第一次出现的记录。3.1 场景一使用TreeSet与Collectors.collectingAndThen这是最经典、可读性较高的一种方法。思路是利用Collectors.toCollection创建一个TreeSet并传入一个根据id比较的Comparator。TreeSet在添加元素时会自动去重。最后再将这个Set转换回ArrayList。ListUser distinctUsers users.stream() .collect(Collectors.collectingAndThen( Collectors.toCollection(() - new TreeSet(Comparator.comparing(User::getId))), ArrayList::new ));代码拆解Comparator.comparing(User::getId)创建了一个比较器它通过User::getId这个方法引用来提取比较键即id字段。new TreeSet(...)实例化一个TreeSet并传入上面的比较器。这样TreeSet判断两个User对象是否“相等”即是否重复的标准就变成了比较它们的id字段是否相等。Collectors.toCollection(() - new TreeSet(...))这是一个收集器它告诉Stream将元素收集到我们指定的那个TreeSet中。Collectors.collectingAndThen()这是一个包装收集器。它先执行第一个收集器将元素收集到TreeSet然后对其结果即TreeSet应用一个finisher函数ArrayList::new将其转换为ArrayList。优点逻辑清晰一行代码完成去重和收集。缺点由于使用了TreeSet结果列表会按照id字段排序。如果你需要保留原始顺序这个方法就不适用了。3.2 场景二使用filter与临时HashSet维持状态如果我们想保留原始的插入顺序即首次出现的顺序可以使用filter操作配合一个临时的HashSet来记录已经出现过的键。ListUser distinctUsers users.stream() .filter(distinctByKey(User::getId)) .collect(Collectors.toList()); // 需要定义一个静态工具方法 public static T PredicateT distinctByKey(Function? super T, ? keyExtractor) { MapObject, Boolean seen new ConcurrentHashMap(); // 使用ConcurrentHashMap以支持并行流 return t - seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) null; }代码拆解distinctByKey(User::getId)这是一个高阶函数它接收一个函数User::getId作为参数并返回一个Predicate断言。这个Predicate内部维护了一个ConcurrentHashMapseen。Map的键是我们提取的字段值如id值在这里不重要我们用了Boolean.TRUE作为占位符。putIfAbsent(key, value)方法是关键如果key不存在则插入键值对并返回null如果key已存在则返回当前key对应的值不会是null。在filter中对于流中的每个元素t我们应用这个Predicate提取其id尝试放入seenMap。如果返回null说明这个id是第一次出现Predicate结果为true该元素被保留否则结果为false元素被过滤掉。优点完美保留了元素在原始流中的首次出现顺序。方法通用性强可以轻松应用于任何字段。缺点需要额外定义一个静态工具方法代码稍显分散。内部使用了ConcurrentHashMap在单线程顺序流中略有性能开销但保证了并行流的安全性。3.3 场景三复杂场景——根据多个字段组合去重有时候去重的逻辑可能更复杂比如需要根据“姓名城市”两个字段的组合来判定是否重复。我们只需稍微调整上面方法中的“键”即可。使用TreeSet方法ListUser distinctUsers users.stream() .collect(Collectors.collectingAndThen( Collectors.toCollection(() - new TreeSet( Comparator.comparing((User u) - u.getName()) .thenComparing(u - u.getCity()) )), ArrayList::new ));这里我们使用了Comparator.comparing(...).thenComparing(...)来构建一个组合比较器。使用filter与临时Map方法 只需要修改keyExtractor函数使其返回一个能代表组合唯一性的对象比如一个字符串拼接或者更优雅地使用一个List或自定义的键对象。// 方法1拼接字符串简单但可能效率低或产生歧义 ListUser distinctUsers users.stream() .filter(distinctByKey(u - u.getName() # u.getCity())) // 用特殊字符分隔 .collect(Collectors.toList()); // 方法2使用List作为复合键更规范 ListUser distinctUsers users.stream() .filter(distinctByKey(u - Arrays.asList(u.getName(), u.getCity()))) .collect(Collectors.toList());注意使用List作为Map的键是可行的因为ArrayList重写了equals和hashCode会递归比较列表内的元素。但要注意用于构建键的字段值本身应该是不可变或至少在此次去重操作中不会改变的。4. 性能考量与边界情况处理选择了合适的方案我们还需要关注它在不同场景下的表现以及可能遇到的“坑”。4.1 不同数据量下的性能对比为了有个直观感受我们可以简单分析一下小数据量1000几种方法性能差异微乎其微。选择代码最清晰、最易维护的方案即可通常推荐场景二filterMap因为它保留了顺序且逻辑清晰。中大数据量千级到百万级HashSet/TreeSet方案HashSet的O(1)插入查询通常最快TreeSet的O(log n)次之。但它们都需要在内存中完整存储另一个集合Set内存开销是O(n)。filterConcurrentHashMap方案同样有O(n)的内存开销存储键。在顺序流中ConcurrentHashMap的锁开销几乎可以忽略在并行流中它的表现会很好。如果内存紧张且数据源是数据库最优先考虑的是在SQL层面使用DISTINCT或GROUP BY进行去重将计算压力转移到数据库这是最高效的方式。超大数据量/流式数据如果数据源是文件或网络流无法全部加载到内存上述基于内存集合的方法都会失效。此时可能需要考虑使用外部排序去重或者使用支持分布式去重的数据处理框架如Flink/Spark。这超出了本文讨论范围但心里要有这根弦。4.2 保留哪一条重复数据的策略我们之前的例子都是保留首次出现的记录。但业务上可能需要保留最后一次出现的记录或者保留某个字段最大/最小的记录。保留最后一次出现最简单的办法是将原始List反转然后应用“保留首次”的逻辑最后再将结果反转回来。或者在遍历时用一个Map来不断覆盖最终Map里的值就是最后一次出现的记录。// 使用Map覆盖保留最后一次出现 MapLong, User map new HashMap(); users.forEach(user - map.put(user.getId(), user)); // 后面的覆盖前面的 ListUser distinctUsers new ArrayList(map.values()); // 注意HashMap不保证顺序如需顺序可用LinkedHashMap保留某个字段最大/小的记录这需要使用Collectors.toMap并指定一个合并函数merge function。// 假设根据id去重保留name字母序最大的User ListUser distinctUsers new ArrayList(users.stream() .collect(Collectors.toMap( User::getId, // 键id Function.identity(), // 值User对象本身 (u1, u2) - u1.getName().compareTo(u2.getName()) 0 ? u1 : u2 // 合并规则保留name大的 )) .values());4.3 处理null值如果作为去重依据的字段可能为null需要特别注意。TreeSet和作为Map键的ConcurrentHashMap通常可以处理null键TreeSet要看比较器是否支持ConcurrentHashMap不允许null键。在自定义distinctByKey方法中如果键提取器可能返回null需要确保你的逻辑是预期的。一个简单的处理方式是将null转换为一个特殊的标记对象。public static T PredicateT distinctByKey(Function? super T, ? keyExtractor) { MapObject, Boolean seen new ConcurrentHashMap(); return t - { Object key keyExtractor.apply(t); // 处理null将其转换为一个特殊的单例对象避免NPE Object keyToUse (key null) ? NULL_PLACEHOLDER : key; return seen.putIfAbsent(keyToUse, Boolean.TRUE) null; }; } private static final Object NULL_PLACEHOLDER new Object();5. 常见问题排查与实战技巧在实际编码中你可能会遇到一些意想不到的情况。下面是我踩过的一些坑和总结的技巧。5.1distinct()不生效检查你的equals和hashCode如果你直接对对象流调用distinct()但去重效果不符合预期99%的原因是你的实体类没有正确重写equals()和hashCode()方法。默认情况下从Object类继承这两个方法比较的是对象的内存地址。这意味着new User(1L, “张三”)和new User(1L, “张三”)会被认为是两个不同的对象。解决方案使用IDE如IntelliJ IDEA或Eclipse自动生成基于所有字段或关键字段的equals()和hashCode()方法。如果使用Lombok用Data或EqualsAndHashCode注解即可。Data // 这会生成基于所有非静态、非瞬态字段的equals和hashCode class User { private Long id; private String name; }5.2 并行流parallelStream下的去重陷阱使用parallelStream()可以提升性能但distinct()操作在并行流中是有状态的它内部使用一个共享的ConcurrentHashMap来跟踪元素。这通常是线程安全的但如果你在去重过程中用于判断相等的字段被其他线程修改了就会导致不可预知的行为。更常见的问题是并行流会打乱元素顺序即使你用了distinct()最终结果的顺序也是不确定的。技巧如果需要保留顺序请不要使用parallelStream()进行去重操作或者使用forEachOrdered等终端操作来保证顺序但这可能会抵消并行的性能优势。通常去重操作本身不是计算密集型顺序流足矣。5.3 内存溢出OOM风险无论是HashSet、TreeSet还是ConcurrentHashMap方案都需要在内存中维护一个与去重键数量相当的集合。如果去重键的数量极大例如上亿就可能导致OutOfMemoryError。排查与解决评估数据量首先确认是否真的需要在内存中处理如此大量的数据。考虑数据库去重如果数据来自数据库务必在SQL查询时使用DISTINCT或GROUP BY这是最有效率的。分批处理如果必须在Java中处理可以考虑将大List分成多个批次每批次单独去重然后再合并去重。但这需要设计好批次间的去重逻辑。使用磁盘或分布式缓存对于极端情况可以考虑使用像Ehcache、Redis这样的外部缓存来存储已见键但这会引入IO开销和系统复杂性。5.4 去重后类型丢失问题在使用map操作先提取字段去重后你得到的是一个字段值如Long的流。如何找回完整的对象这通常需要后续的步骤比如用去重后的id列表再去查询数据库或者在一开始就使用filter方案而非map方案。示例错误做法// 错误这样得到的是去重后的id列表不是User列表 ListLong distinctIds users.stream() .map(User::getId) .distinct() .collect(Collectors.toList()); // 如何根据id列表获取User可能需要再次查询数据库。正确做法使用前面介绍的filterdistinctByKey或TreeSet方案它们能在去重的同时保留完整对象。5.5 实用工具类封装为了避免重复编写distinctByKey这样的方法我习惯在项目里创建一个StreamUtils工具类public class StreamUtils { private StreamUtils() {} /** * 根据指定的键提取器对流进行去重保留首次出现的元素。 * param keyExtractor 键提取函数 * param T 流元素类型 * return 一个去重的Predicate */ public static T PredicateT distinctByKey(Function? super T, ? keyExtractor) { SetObject seen ConcurrentHashMap.newKeySet(); // Java 8 可以使用newKeySet return t - seen.add(keyExtractor.apply(t)); // add方法返回true表示首次添加 } /** * 根据多个键提取器进行去重。 * param keyExtractors 键提取函数数组 * param T 流元素类型 * return 一个去重的Predicate */ SafeVarargs public static T PredicateT distinctByKeys(Function? super T, ?... keyExtractors) { return t - { ListObject keys Arrays.stream(keyExtractors) .map(extractor - extractor.apply(t)) .collect(Collectors.toList()); // 这里需要一个支持复合键的Set我们可以用拼接字符串或List本身作为键。 // 注意List作为键要求其内容在去重过程中不变。 return seenComposite.add(keys); }; } // 注意distinctByKeys 的实现需要更复杂的seenComposite结构此处仅为示意。 }这样在业务代码中就可以非常简洁地调用ListUser result users.stream() .filter(StreamUtils.distinctByKey(User::getId)) .collect(Collectors.toList());我个人在实际项目中更偏爱filterdistinctByKey的方案。它足够灵活能保留顺序并且通过一个清晰的方法名distinctByKey明确表达了意图代码的可读性非常高。当团队新成员看到这行代码时几乎不需要注释就能理解它在做什么。对于简单的、不关心顺序且字段可比较的场景TreeSet方案也非常简洁。关键是理解每种方法背后的原理和代价才能在做技术选型时游刃有余。

相关新闻

DS4Windows完全指南:3步让PS4手柄在Windows上完美运行

DS4Windows完全指南:3步让PS4手柄在Windows上完美运行

DS4Windows完全指南:3步让PS4手柄在Windows上完美运行 【免费下载链接】DS4Windows Like those other ds4tools, but sexier 项目地址: https://gitcode.com/gh_mirrors/ds/DS4Windows DS4Windows是一款免费开源软件,专为在Windows系统上使用PS4 …

2026/8/8 1:36:14 阅读更多 →
PCF8591模数转换芯片详解:I2C接口ADC/DAC原理、编程与实战应用

PCF8591模数转换芯片详解:I2C接口ADC/DAC原理、编程与实战应用

1. 项目概述:从“模拟”到“数字”的桥梁在嵌入式开发和电子DIY的世界里,我们常常需要和现实世界打交道。现实世界是连续的、模拟的,比如温度的变化、光线的强弱、声音的大小;而我们的微控制器(MCU)世界&am…

2026/8/8 1:36:14 阅读更多 →
深入解析Android音频架构:从API使用到底层原理与性能优化

深入解析Android音频架构:从API使用到底层原理与性能优化

1. 项目概述:为什么需要深入理解Android音频架构?如果你是一名Android应用开发者,或者正在从事与Android系统底层相关的工作,那么“音频”这个模块绝对是你绕不开的一座大山。表面上看,播放一段音乐、录制一段语音&…

2026/8/8 1:36:14 阅读更多 →

最新新闻

从大语言模型到神经解码:技术范式转变与工程实践

从大语言模型到神经解码:技术范式转变与工程实践

在实际技术领域,从一家大型、成熟的AI研究机构转向一家专注于前沿、甚至有些科幻色彩技术(如“心灵感应技术”)的初创公司,是一个极具话题性的职业选择。这背后往往涉及对技术栈、工程挑战、个人愿景和行业趋势的深刻思考。虽然我…

2026/8/8 3:31:17 阅读更多 →
Matlab/Simulink混合动力汽车建模与仿真实践

Matlab/Simulink混合动力汽车建模与仿真实践

1. 项目概述:混合动力汽车建模的意义与挑战在汽车工业向电气化转型的大背景下,插电式混合动力汽车(PHEV)凭借其兼顾燃油经济性和续航能力的特性,已成为市场主流选择之一。作为一名长期从事汽车电控系统开发的工程师&am…

2026/8/8 3:31:17 阅读更多 →
华中师范大学研究生学位论文排版全攻略:从规范解读到Word/LaTeX高效实操

华中师范大学研究生学位论文排版全攻略:从规范解读到Word/LaTeX高效实操

1. 项目概述:从“规范”到“效率”的实战之路又到了一年一度的毕业季,对于华中师范大学的研究生同学来说,除了实验数据、理论分析和文献综述,还有一座绕不过去的大山——学位论文的格式排版。我见过太多同学,学术内容扎…

2026/8/8 3:31:17 阅读更多 →
扑克牌游戏策略与心理战术全解析

扑克牌游戏策略与心理战术全解析

1. 玩牌的精彩瞬间解析扑克牌作为流传数百年的经典游戏,在不同文化背景中演化出无数种玩法。从家庭聚会到专业赛事,从休闲娱乐到心理博弈,纸牌游戏总能创造出令人难忘的精彩时刻。这些瞬间往往包含着策略、运气与心理的完美结合。2. 经典牌局…

2026/8/8 3:31:17 阅读更多 →
Node.js Event Loop机制与异步任务处理详解

Node.js Event Loop机制与异步任务处理详解

1. Node Event Loop 机制深度解析作为 Node.js 的核心机制,Event Loop 决定了 JavaScript 代码的执行顺序和异步任务的处理方式。我在实际开发中遇到过不少由于对 Event Loop 理解不深导致的 bug,比如定时器不准、回调顺序错乱等问题。今天就来详细拆解这…

2026/8/8 3:31:17 阅读更多 →
基于Flask+Vue的红色旅游管理系统架构设计

基于Flask+Vue的红色旅游管理系统架构设计

1. 项目概述:河南红色旅游景点管理系统的技术架构这个项目是一个典型的旅游行业信息化解决方案,采用前后端分离架构实现。前端使用Vue.js构建响应式管理界面,后端采用Flask框架提供RESTful API服务,数据库层使用Django ORM进行抽象…

2026/8/8 3:30:17 阅读更多 →

日新闻

AI多智能体时代来临,读懂MCP与A2A架构,抢占企业数字化新风口

AI多智能体时代来临,读懂MCP与A2A架构,抢占企业数字化新风口

当下AI应用飞速普及,无数企业下场搭建智能体系统,可落地阶段难题接踵而至:上下文无限堆积频繁爆栈、AI工具调用准确率低下、Token成本居高不下、企业数据权限混乱暗藏安全隐患……很多团队卡在架构搭建环节,空有前沿技术概念&…

2026/8/8 0:00:07 阅读更多 →
PHP二维码生成终极指南:用chillerlan/php-qrcode打造专业级二维码

PHP二维码生成终极指南:用chillerlan/php-qrcode打造专业级二维码

PHP二维码生成终极指南:用chillerlan/php-qrcode打造专业级二维码 【免费下载链接】php-qrcode A PHP QR Code generator and reader with a user-friendly API. 项目地址: https://gitcode.com/gh_mirrors/ph/php-qrcode 在当今数字时代,二维码已…

2026/8/8 0:00:08 阅读更多 →
UniApp微信小程序隐私保护组件开发:从原理到实战

UniApp微信小程序隐私保护组件开发:从原理到实战

1. 项目缘起:为什么我们需要一个隐私保护通用组件?最近在维护一个基于uniapp开发的微信小程序矩阵时,我遇到了一个非常棘手的问题。随着平台对用户隐私保护的要求越来越严格,几乎每一个新版本发布,或者在某些特定机型&…

2026/8/8 0:00:08 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/6 22:02:27 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/6 22:02:27 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/7 23:24:08 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/7 17:02:37 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/7 23:54:54 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/7 17:02:36 阅读更多 →