导读:本期聚焦于行者创作的《如何在 Java 中按首次出现的单词(大小写敏感)统计词频》,敬请观看详情。统计一段文本中每个单词出现的次数,是 Java 初学者和面试场景里最常见的练习之一。但很多实现一上来就用 HashMap,结果发现输出的单词顺序完全乱了,和原文中首次出现的先后顺序对不上。这篇文章围绕按首次出现顺序输出词频这个需求,介绍 LinkedHashMap 的插入有序特性、compute 与 merge 方法的计数技巧,以及大小写敏感的分割处理方式,同时对比几种常见写法的优劣,给出可直接运行的完整代码,帮助你彻底搞懂顺序保留与词频统计的结合用法。

词频统计看起来是个简单问题:把一段字符串按空格切分,然后逐个累加计数就行了。可一旦要求输出顺序必须与单词在原文中首次出现的顺序一致,不少人写出来的结果就乱了套。根本原因在于 HashMap 本身不保证任何遍历顺序,元素在内部是按哈希值散列存放的,遍历时得到的顺序既不是插入顺序也不是字母顺序。要解决这个痛点,核心思路是把存储容器换成能记住插入顺序的结构,或者在遍历输出前额外排序。下面从原理、实现到进阶优化,把这个需求完整拆解一遍。

如何在 Java 中按首次出现的单词(大小写敏感)统计词频

一、为什么 HashMap 会打乱单词的首次出现顺序

先看一段典型的问题代码,很多教程都会这么写:

Map<String, Integer> map = new HashMap<>();
for (String word : text.split(" ")) {
    map.put(word, map.getOrDefault(word, 0) + 1);
}
System.out.println(map);

假设输入是 the Quick brown fox the lazy dog quick,这段代码输出的顺序很可能是 brown=1, quick=2, the=2, Quick=1... 之类的乱序结果。这不是 bug,而是 HashMap 的设计决定的:它为了追求查询效率,用哈希函数把 key 分散到不同的桶里,桶的分布与插入时间毫无关系。

更关键的一点是题目要求大小写敏感。Thethe 是两个不同的单词,Quickquick 也是。只要不去调用 toLowerCase() 做归一化,Map 的 key 就会自然把它们区分开,这个要求本身就降低了实现难度,但也意味着首次出现顺序的判定要以原始大小写为准。the 先出现,那么输出时 the 就排在前面,即使后面来了个 The,它作为独立 key 也按它自己的首次出现位置排。

二、用 LinkedHashMap 按首次出现顺序统计

LinkedHashMap 是解决顺序问题的首选。它在 HashMap 的基础上维护了一条双向链表,记录每个条目的插入先后。对一个已经存在的 key 重复 put,只会更新 value,不会改变它在这条链表上的位置,这正好符合按首次出现排序的语义。

import java.util.LinkedHashMap;
import java.util.Map;

public class WordFrequency {

    public static Map<String, Integer> countWords(String text) {
        Map<String, Integer> freq = new LinkedHashMap<>();
        if (text == null || text.isEmpty()) {
            return freq;
        }
        // \\s+ 匹配一个或多个空白字符(空格、制表符等)
        String[] words = text.trim().split("\\s+");
        for (String word : words) {
            Integer old = freq.get(word);
            freq.put(word, old == null ? 1 : old + 1);
        }
        return freq;
    }

    public static void main(String[] args) {
        String text = "The quick brown fox jumps over the lazy dog The end";
        Map<String, Integer> result = countWords(text);
        result.forEach((k, v) -> System.out.println(k + " 出现了 " + v + " 次"));
    }
}

运行上面的代码,输出顺序是 Thequickbrown……与原文首次出现顺序完全一致,其中 The 计 2 次,the 计 1 次,大小写敏感的要求得到满足。注意这里用的是 \\s+ 而不是单个空格来分割,能同时处理连续多个空格和首尾空白,比 split(" ") 稳健得多。

需要说明的是,LinkedHashMap 维护链表会带来一点点额外的内存和插入开销,但在词频统计这种场景下完全可以忽略。它的遍历性能和 HashMap 基本持平,属于无脑可选的方案。

三、用 merge 或 compute 让计数更简洁

JDK 8 之后,Map 接口提供了 mergecompute 两个默认方法,可以把取旧值、判空、加一的逻辑压缩成一行,代码可读性明显更好。

// 写法一:merge,key 不存在时放入初始值 1,存在时执行合并逻辑
for (String word : words) {
    freq.merge(word, 1, Integer::sum);
}

// 写法二:compute,等价但语义略有不同
for (String word : words) {
    freq.compute(word, (k, v) -> v == null ? 1 : v + 1);
}

merge 的三个参数分别是 key、初始值和合并函数。当 key 第一次出现时直接存入初始值 1;再次遇到时,把旧值和初始值交给 Integer::sum 相加后写回。这种写法省去了显式的 null 判断,也避免了自动装箱拆箱过程中可能踩到的空指针坑。

如果追求极致简洁,还可以配合 Stream 的 Collectors.groupingBy,但要注意一个细节:直接用 groupingBy 默认返回 HashMap,顺序同样会乱。必须传入 LinkedHashMap::new 作为工厂才能保留首次出现顺序,这也是实际编码中最容易犯的错误之一:

import java.util.stream.Collectors;
import java.util.LinkedHashMap;

Map<String, Long> freq = text.trim().split("\\s+").length == 0
        ? new LinkedHashMap<>()
        : java.util.Arrays.stream(text.trim().split("\\s+"))
            .collect(Collectors.groupingBy(
                w -> w,
                LinkedHashMap::new,
                Collectors.counting()));

四、边界情况与注意事项

实际使用中还有几个容易忽略的点。第一,标点符号的处理。如果文本里可能出现逗号、句号,fox,fox 会被当成两个单词。可以在分割前用 replaceAll("[^a-zA-Z0-9\\s]", " ") 把非字母数字字符替换成空格,不过要清楚这属于清洗策略,是否执行取决于你的业务定义。

第二,空字符串和 null 输入。代码开头对 text == null || text.isEmpty() 的判断是必要的,否则 "".split("\\s+") 会返回一个包含空字符串的数组,导致结果里混入一个计数为 1 的空单词。第三,如果统计的文本非常大,还可以考虑用 StringTokenizer 或手写逐字符扫描来减少临时数组开销,但对绝大多数场景,splitLinkedHashMap 已经足够高效。

总结一下:按首次出现顺序做大小写敏感的词频统计,核心就是两步,用 \\s+ 正则可靠地分割单词,用 LinkedHashMap 配合 merge 完成有序计数。理解了 HashMap 无序的本质和 LinkedHashMap 插入有序的机制,类似的顺序保留需求就都能举一反三地解决了。

Java词频统计LinkedHashMap大小写敏感修改时间:2026-09-08 10:15:04

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260908/52703.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。