最直接方法是用Collectors.groupingBy配合安全提取首字母的函数,需判空避免NPE,推荐str -> str.isEmpty() ? "其他" : String.valueOf(str.charAt(0)).toUpperCase()。

用 Collectors.groupingBy 提取首字母分组最直接
Java 8+ 的 Stream 分组能力足够应付这个需求,核心就是用 Collectors.groupingBy 配合一个提取首字母的函数。别写循环、别手动建 Map,那是倒退。
常见错误是直接对字符串调用 charAt(0) —— 一旦遇到空字符串或 null 就抛 StringIndexOutOfBoundsException 或 NullPointerException。必须先判空。
- 推荐用
str -> str.isEmpty() ? "其他" : String.valueOf(str.charAt(0)).toUpperCase()作为分类器 - 如果想按 Unicode 字母首字符归类(比如支持中文拼音首字),得引入
Collator或第三方库,charAt(0)不够用 - 返回的
Map<character list>></character>默认不保证顺序;需要有序结果就用Collectors.groupingBy(..., LinkedHashMap::new, ...)
分组后还要映射值?套一层 Collectors.mapping
比如不是要原始字符串列表,而是要每个字符串的长度、转大写、或去重后的首字母集合——这时候不能在分组外再遍历 Map 值做处理,效率低还破坏流式语义。
Collectors.groupingBy 的第二个参数支持下游收集器,Collectors.mapping 就是干这个的。
立即学习“Java免费学习笔记(深入)”;
- 把每个分组内的字符串转成大写:
Collectors.mapping(String::toUpperCase, Collectors.toList()) - 只保留每个字符串的长度:
Collectors.mapping(String::length, Collectors.toList()) - 注意:如果映射函数可能返回
null(比如str -> str.trim().isEmpty() ? null : str),下游收集器会抛NullPointerException;得先过滤或用Optional包裹
性能敏感时小心 String::charAt 和 substring(0,1) 的差异
看起来都取首字符,但底层行为不同:charAt(0) 是 O(1) 索引访问,substring(0,1) 在 Java 7u6 之后虽不再共享底层数组,但仍涉及新建对象和边界检查,开销略高。
尤其在百万级字符串分组场景下,微小差异会被放大。
- 优先用
str -> str.isEmpty() ? "其他" : String.valueOf(str.charAt(0)) - 避免
str.substring(0, 1).toUpperCase()—— 先截再转,创建了两个临时对象 - 如果要统一大小写且区分语言环境(比如土耳其语),必须用
str.substring(0, 1).toUpperCase(Locale.ENGLISH),这时charAt就不够用了
中文字符串怎么分组?别硬套 charAt(0)
中文没有“首字母”概念,但业务常要求按拼音首字母分组(如“北京”→“B”,“上海”→“S”)。Java 标准库不提供拼音转换,charAt(0) 拿到的是汉字 Unicode 码点(如“北”是 \u5317),完全没法用。
得依赖外部库,主流选 pinyin4j 或 hutool 的拼音工具。
- 用
pinyin4j:先PinyinHelper.toHanYuPinyinStringArray(ch, new HanyuPinyinOutputFormat())取首音,再取第一个字母 - 注意多音字(如“重庆”可能是
Chong或Zhong),默认返回第一个读音,业务需确认是否可接受 - 拼音转换是 CPU 密集型操作,大数据量时建议加缓存(比如用
ConcurrentHashMap缓存已转换过的字符串)










