
1. 为什么每个 Java 项目里都留着 commons-lang3 这个依赖大部分资深 Java 工程师的代码里commons-lang3 都是那个“一进来就在”的依赖。你很少主动想起它但一旦需要判断字符串是不是空白、想把数组转成打印友好的字符串、或者从一大段日志里抠出某个键的值时你第一个翻开的工具类八成就是它。我在工作中接触过不少老项目第一眼看pom.xml或build.gradle如果里面没有 commons-lang3我基本能猜到后面的代码风格满屏的 null、! null、equals()、手写 for 循环拼字符串以及各种自己封装但功能相近的工具类。倒不是说这样不行而是刻意避开一个成熟、稳定、被几百万项目验证过的基础库纯属重复造轮子。这篇文章就把 commons-lang3 里那些高频方法过一遍重点关注三个东西每个方法解决什么问题、怎么写最顺手、有什么隐藏的坑。先说说为什么在 JDK 已经很完善的今天它依然有存在感。JDK 的java.lang提供的是最基础的对象、包装类、字符序列而实际业务里我们更多处理的是偏业务形态的数据null 安全的字符串、数组辅助操作、数值解析容错、实体类的equals/toString重建等。commons-lang3 做的事情正是给这些最基础的操作补上“安全网”让你不用每次写工具方法时都跟 NPE 较劲。Maven 引入很简单目前稳定版本我习惯用 3.17.0dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.17.0/version /dependencyGradle 对应写法implementation org.apache.commons:commons-lang3:3.17.0有一个容易踩的坑先说在前面3.x 的包名是org.apache.commons.lang3不是老的org.apache.commons.lang。有同事从老项目 copy 代码到新项目import 没改直接NoClassDefFoundError就是因为 2.x 和 3.x 的包名不同两者不能直接替换。有人会问项目里已经用了 Guava还需要 commons-lang3 吗我的看法是两者定位有重叠但不完全一样。commons-lang3 专注于 java.lang 层面的增强体量轻类名直观Guava 提供的是集合、缓存、事件总线这类更大的能力。我一般两个都引字符串判空和数组操作交给 lang3集合处理和缓存交给 Guava并不冲突。它的核心类大致覆盖这些场景类名覆盖范围StringUtils字符串判空、截取、拼接、替换、补全ObjectUtils空值安全、默认值、比较ArrayUtils数组增删改查、转 Map、排序判断NumberUtils数值解析、比较、BigDecimal 处理RandomStringUtils随机字符串生成Validate入参校验ExceptionUtils异常堆栈处理DateFormatUtils / DurationFormatUtils日期格式化和时长格式化下面按使用频率逐个拆开讲。2. StringUtils判空、截取、拼接、替换这些高频操作的推荐写法StringUtils 是 commons-lang3 里出场率最高的类没有之一。它解决的问题很纯粹让字符串操作在 null 输入下不抛异常同时把高频操作封装成一个方法调用。2.1 判空全家桶isBlank 与 isEmpty 千万别混用先看最基础的判空。这两个方法长得像语义差别很大String s ; StringUtils.isEmpty(s); // false 不算空 StringUtils.isBlank(s); // true空白字符都算空 StringUtils.isNotBlank(s); // falseisEmpty只认null和isBlank在null和之外还会把空格、\t、\n等空白字符当成空。做表单校验、入参校验的时候几乎总是该用isBlank/isNotBlank因为用户输入一个纯空格从业务角度看它就是没填。配套使用的还有 trim 家族StringUtils.trimToNull( ); // null StringUtils.trimToNull( abc ); // abc StringUtils.trimToEmpty(null); // trimToNull我很常用。数据库入库之前把纯空白字符串转成 null可以避免表里存一堆意义不明的空格后续查询条件也不用到处trim。再往后是默认值家族StringUtils.defaultString(null); // StringUtils.defaultIfEmpty(, 默认); // 默认 StringUtils.defaultIfBlank( , 默认); // 默认defaultIfBlank的逻辑是字符串为 null、空串、纯空白时都返回默认值。需要从配置里读值、从外部接口拿字符串、展示时想给一个兜底文案的场景这几个方法非常合适。注意一点defaultString(null, null)的返回值就是 null虽然很少这么传但别以为它一定保证非 null。2.2 提取与截取substringBetween 比 indexOf 组合拳省心我在解析日志、报文、模板占位符时经常需要“取出某两个标记之间的内容”。手写的话是indexOfsubstring三件套还要处理找不到标记时返回 -1 的边界情况。StringUtils 直接给了现成方法String msg 当前用户: user_123角色: admin; StringUtils.substringBetween(msg, 当前用户: , ); // user_123 StringUtils.substringBefore(abc:def, :); // abc StringUtils.substringAfter(abc:def, :); // defsubstringBetween找的是第一个左标记和第一个右标记之间的内容输入为 null 时返回 null不会抛异常。解析那种“键: 值”格式的数据时用substringBeforesubstringAfter组合明显比手写indexOf可读性好。还有一个场景是日志脱敏。比如手机号中间四位打码String phone 13812345678; String masked StringUtils.substring(phone, 0, 3) **** StringUtils.substring(phone, 7);当然这只是示例具体脱敏规则还要看业务要求。重点是这类拼接和截取操作StringUtils 的方法名就是你的代码注释可读性非常好。2.3 拼接与切分join 没错但 split 有个容易踩的坑列表转逗号分隔字符串很多人用String.join或手写循环。StringUtils 的join有一个优势它天然 null 安全集合里含 null 元素也不会崩溃而是输出空串占位StringUtils.join(new Object[]{a, null, c}, ,); // a,,c如果你想让 null 元素显示成固定文本还有重载方法可以传自定义 null 占位文本。这在拼接 CSV、构造日志字段时很实用。split的坑我一定要重点说。Java 自带的String.split和 StringUtils 的split行为不完全一样尤其处理连续分隔符时String csv a,,b; csv.split(,); // [a, , b]JDK 会保留中间空串但会丢尾部空串 StringUtils.split(csv, ,); // [a, b]默认把空 token 过滤掉了 StringUtils.splitPreserveAllTokens(csv, ,); // [a, , b]要保留空位得用它实测下来用StringUtils.split切 CSV 时连续逗号之间的空字段会被静默丢掉导致“第 3 列变成第 2 列”这种诡异问题。如果对列顺序有要求比如解析 CSV、解析协议报文一定要用带PreserveAllTokens的版本。这是我实际踩过坑的地方排查半天最后发现是过滤空 token 导致的。2.4 替换、补位与归一化这些方法能帮你少写循环项目里最常见的替换需求有两种替换所有匹配或者只替换第一个。StringUtils 分别对应replace和replaceOnceStringUtils.replace(a-b-c, -, :); // a:b:c全文替换字面量匹配 StringUtils.replaceOnce(a-b-c, -, :); // a:b-c只换第一个 StringUtils.remove(a-b-c, -); // abc StringUtils.removeStart(hello world, hello ); // world StringUtils.removeEnd(index.html, .html); // indexString.replaceAll默认走正则处理带特殊字符的分隔符时要写转义StringUtils 的replace是字面量替换处理 URL 参数里的?、、这类字符时省心很多。removeStart/removeEnd按前缀后缀截断很适合处理路径拼接和文件后缀。normalizeSpace是 3.9 之后提供的把所有连续空白折叠成一个空格同时去掉首尾空白StringUtils.normalizeSpace(我 是 开发者 ); // 我是开发者用户从 Excel 复制文本、从富文本编辑器粘贴内容时经常带各种空白用它做文本归一化再合适不过。补位方法在格式化输出时很常用StringUtils.repeat(ab, 3); // ababab StringUtils.leftPad(7, 3, 0); // 007订单号补零 StringUtils.rightPad(name, 8, -); // name---- StringUtils.center(title, 11, *); // ***title***订单号固定宽度、控制台对齐、日志缩进这几个方法基本够用。另外abbreviate可以按宽度截断并补省略号StringUtils.abbreviate(abcdefghij, 5); // ab...做列表标题截断显示的时候很方便不用自己写“长了就裁一刀”的逻辑。2.5 判断类方法isNumeric 之类的方法别凭直觉用StringUtils 提供了一堆判断字符串特征的布尔方法isAlpha、isAlphanumeric、isNumeric、containsAny、startsWithAny等。这里特别提醒isNumeric的一个直觉陷阱StringUtils.isNumeric(123); // true StringUtils.isNumeric(12.3); // false不认小数点 StringUtils.isNumeric(-1); // false不认负号它只认纯数字字符串不认正负号和小数点。如果你想判断一个字符串是不是“合法数字”用后面的 NumberUtils.isCreatable 更靠谱。isAlpha同理只认纯字母空格和数字都不行。判断白名单、前缀匹配时containsAny/startsWithAny这类方法比手写 for 循环清晰StringUtils.startsWithAny(user_123, admin_, user_); // trueStringUtils 方法太多了没必要全背记住三条核心经验足够判空用isBlank系列切分注意PreserveAllTokens替换优先用字面量版本而不是正则版本。3. ObjectUtils 与 Validate写工具类之前先翻翻这两个类很多项目里自己封装的工具方法其实就是在处理三件事判断空、取默认值、校验参数。这三件事 commons-lang3 已经帮你做好了类名分别是ObjectUtils和Validate。3.1 ObjectUtils.isEmpty一个方法处理四种空ObjectUtils.isEmpty是个非常通用的入口它对 null、空字符串、空数组、空集合、空 Map、空 Optional 都返回 trueObjectUtils.isEmpty(null); // true ObjectUtils.isEmpty(); // true ObjectUtils.isEmpty(new int[]{}); // true ObjectUtils.isEmpty(Collections.emptyList()); // true ObjectUtils.isEmpty(Optional.empty()); // true3.9 之后支持注意它只认空字符串不认纯空白isEmpty( )是 false这点和 StringUtils.isBlank 不一样。我一般会根据自己的场景决定入参校验用StringUtils.isNotBlank统一判空用ObjectUtils.isEmpty。这种统一判空入口最大的价值是让代码风格一致。一个项目里如果有人用list null || list.size() 0有人用CollectionUtils.isEmpty还有人用ObjectUtils.isEmpty代码审查成本很高。定一个规范统一走ObjectUtils.isEmpty理解成本一下子就降下来了。3.2 firstNonNull 与 defaultIfNull配置兜底的标准姿势读取配置时配置项可能缺失可能需要多级兜底。手写的话是嵌套三元表达式嵌套三层以上基本没法看。ObjectUtils 给了一套很清晰的方案ObjectUtils.defaultIfNull(map.get(timeout), 30); ObjectUtils.firstNonNull(systemProp, configFile, defaultVal);defaultIfNull很好理解目标值为 null 就返回默认值。firstNonNull接收多个参数按顺序返回第一个非 null 的值适合“系统参数 配置文件 默认值”这类多级优先级的场景。写配置加载、环境变量覆盖逻辑时这个方法的可读性比三层三元表达式强太多了。另外还有一批批量判空的方法ObjectUtils.anyNull(id, name, age); // 只要有一个 null 就返回 true ObjectUtils.allNull(id, name, age); // 全部为 null 才返回 true批量校验入参、检查一组必要的参数是否齐全时比逐个 null判断干净得多。3.3 Validate入参守卫就该这么写Validate 是那种“用了就回不去”的工具类。它的作用是在方法入口做前置条件校验校验失败直接抛异常不需要手写if throw new IllegalArgumentException。Validate.notNull(user, user cannot be null); Validate.isTrue(count 0, count must be positive, but got %s, count); Validate.notBlank(name, name is required); Validate.matchesPattern(phone, ^1\\d{10}$, invalid phone);几个特点很明显第一异常类型明确。Validate.notNull失败抛NullPointerExceptionValidate.isTrue失败抛IllegalArgumentException不会到处出现含义模糊的 Runtime 异常。第二消息模板支持占位符错误信息可以携带具体值排查问题时直接能看到是哪个参数、什么值导致的失败。第三代码意图一目了然。方法名本身就是断言语义读代码的人不需要把if条件在心里翻转一遍才能理解。我的经验是Controller 或 Service 入口先放几个 Validate 断言再往下传数据比层层if throw短得多。但也不要滥用一个方法里超过四五个断言通常意味着入参对象建模有问题该封装一个入参校验对象了。4. ArrayUtils 与 RandomStringUtils数组操作和随机字符串的正确姿势数组在 Java 里用起来不如集合顺手原因是它没有自带的方法。ArrayUtils 就是把数组操作缺失的能力补回来。4.1 增删改查都返回新数组记住这个核心认知先立一个最重要的认知ArrayUtils 的add、addAll、remove、removeElement返回的是新数组不会修改原数组而reverse、swap、shift是原地修改。这一点和集合 API 的习惯很不一样用混了容易出现“调完方法发现原数组没变”的疑惑。int[] arr {1, 2, 3}; int[] added ArrayUtils.add(arr, 4); // {1,2,3,4}arr 不受影响 int[] removed ArrayUtils.remove(arr, 0); // {2,3}arr 不受影响 ArrayUtils.reverse(arr); // 原地修改此时 arr 是 {3,2,1} ArrayUtils.swap(arr, 0, 2); // 原地修改交换第 0 和第 2 位日常开发中数组长度固定、需要“增删”的场景通常应该优先考虑ArrayList但有些场景必须用数组性能敏感的数值计算、第三方 SDK 方法签名要求、反射调用时构造参数数组等。这种时候 ArrayUtils 提供的增删改查就很关键。还有一个很常用的判断方法ArrayUtils.isSorted(new int[]{1, 2, 3}); // true ArrayUtils.contains(new int[]{1, 2, 3}, 2); // trueisSorted在检查数据是否有序时比手写循环省事。4.2 数组打印、转 Map、判有序调试配置两相宜打印数组内容是调试时的高频操作。Arrays.toString能用但有两个小差异值得注意ArrayUtils.toString(new int[]{1, 2, 3}); // {1,2,3} ArrayUtils.toString(null); // {}不会输出 null数组为 null 时ArrayUtils.toString返回{}而不是字符串null排查问题时更容易看出“数组是空的”而不是“对象本身不存在的”。当然这只是个人偏好两者都能用。数组转 Map 也是一个不多见但很实用的方法String[][] kv {{key1, val1}, {key2, val2}}; MapString, String map ArrayUtils.toMap(kv);做简单的配置表转换、批量设定初始 KV 时比一行行map.put清晰。注意键不能重复重复会抛IllegalArgumentException。还有包装数组和基本类型数组的互转Integer[] wrapperArray ArrayUtils.toObject(new int[]{1, 2, 3}); int[] primitiveArray ArrayUtils.toPrimitive(wrapperArray);跟 JSON 序列化库、第三方 SDK 打交道时经常遇到Integer[]和int[]互相转换的需求这两个方法直接解决。4.3 随机字符串的坑生成安全令牌一定用 secure 版本RandomStringUtils 提供随机字符串生成常见用法如下RandomStringUtils.randomAlphabetic(8); // 纯字母 RandomStringUtils.randomAlphanumeric(8); // 字母数字 RandomStringUtils.randomNumeric(6); // 纯数字最需要注意的坑它默认基于java.util.Random属于非加密安全随机。生成订单号随机片段、验证码这类“对外、可枚举价值不高”的字符串没问题但如果是密码重置 token、API key、支付相关的安全令牌绝对不能直接用默认版本要用 3.6 引入的 secure 工厂方法RandomStringUtils.secure().randomAlphanumeric(16);secure 版本基于SecureRandom安全性有保障代价是生成速度慢一些但安全令牌场景完全值得。还有一个容易忽略的业务细节randomNumeric(6)可能以 0 开头。如果业务要求 6 位验证码不能是 0 开头或不能重复需要在生成后做一次判断或补位不能直接上送。5. NumberUtils配置文件字符串转数字的稳妥姿势项目中经常要从配置文件、环境变量、外部接口参数里拿数字而这些源头几乎都是字符串。直接用Integer.parseInt解析遇到空串、空格、格式异常一个NumberFormatException就能让服务挂在你面前。NumberUtils 的价值就在这里。5.1 toInt/toLong解析失败有默认值就不慌最常用的几个转换方法自带默认值NumberUtils.toInt(123, 0); // 123 NumberUtils.toInt(abc, -1); // -1解析失败返回默认值 NumberUtils.toLong(props.get(timeout), 3000L); // 读配置失败默认 3 秒 NumberUtils.toDouble(score, 0.0); // 非数字默认 0.0这里的关键不是“少写几行”而是不让解析异常往外抛。读取配置时一个配置项格式写错不应该导致整个应用启动失败外部接口返回一个脏数据也不应该让核心链路挂掉。带默认值的解析方法把这种基础设施层的容错做得非常优雅。5.2 isCreatable 与 isNumber改名带来的 NoSuchMethodError判断一个字符串是不是合法数字时网上的老例子会告诉你用StringUtils.isNumeric也有人提到NumberUtils.isNumber。这里必须叮嘱一句3.x 里isNumber已经改名为isCreatable新版本调用isNumber会直接NoSuchMethodError。isCreatable的判定范围比 StringUtils.isNumeric 宽得多NumberUtils.isCreatable(123); // true NumberUtils.isCreatable(12.3); // true支持小数点 NumberUtils.isCreatable(-1); // true支持负号 NumberUtils.isCreatable(0x1F); // true支持十六进制 NumberUtils.isCreatable(1_000); // true支持下划线如果你只是想判断字符串能不能被Integer.parseInt解析用isParsable更严格NumberUtils.isParsable(123); // true NumberUtils.isParsable(0x1F); // falseisCreatable 才认十六进制实际开发中我的习惯是表单校验字段是不是“正整数”用 StringUtils.isNumeric判断字符串整体能不能转成某种 Number 用isCreatable只关心能不能过parseInt用isParsable。三个方法的判定范围完全不一样用之前想清楚业务语义。5.3 createBigDecimal字符串转金额字段的推荐入口金额、利率、费率这类字段在业务代码里经常以字符串形式传输转成BigDecimal时最稳妥的做法是BigDecimal amount NumberUtils.createBigDecimal(0.1); BigDecimal scaled NumberUtils.toScaledBigDecimal(12.345, 2, RoundingMode.HALF_UP); // 12.35千万不要Double.parseDouble之后再转 BigDecimal浮点数的精度问题会在金额计算时给你上深刻一课。createBigDecimal 直接基于字符串解析从源头避免精度损失。toScaledBigDecimal可以在解析的同时完成 scale 设置和舍入规则指定一步到位。NumberUtils 还有一组min/max重载方法NumberUtils.min(1.5, 2.5, 0.5); // 0.5 NumberUtils.max(3, 9, 4); // 9不用自己写Math.min嵌套了尤其是多个参数取极值的时候。6. 容易被忽略的隐藏工具类以及升级到 3.x 时必须记住的差异StringUtils、ObjectUtils、ArrayUtils、NumberUtils 这四类已经覆盖了大部分日常需求但 commons-lang3 里还藏着一些不在头条但关键时刻很有用的类。6.1 异常、时长、日期格式化这三个类平时不起眼但很实用ExceptionUtils 在处理异常日志时很能救命String stackTrace ExceptionUtils.getStackTrace(exception); Throwable root ExceptionUtils.getRootCause(exception);把异常堆栈转成字符串后可以拼进业务日志、写入告警消息、发到消息队列比依赖日志框架自己的printStackTrace格式可控得多。找根因时getRootCause比一层层getCause循环省事。DurationFormatUtils 用于把毫秒数格式化成可读的时长DurationFormatUtils.formatDuration(123456L, HH:mm:ss); // 00:02:03批处理任务耗时统计、接口性能监控、定时任务日志都可以用这个方法来输出“这次跑了多久”而不是一长串毫秒数字。DateFormatUtils 和 FastDateFormat 则是日期格式化场景的补充DateFormatUtils.format(new Date(), yyyy-MM-dd HH:mm:ss);它内部基于 FastDateFormat是 SimpleDateFormat 的线程安全替代。高并发环境里格式化大量时间用 SimpleDateFormat 要小心线程安全FastDateFormat 则没有这个问题。如果项目里还在用SimpleDateFormat做静态工具方法建议直接换成它。6.2 equals/hashCode/toString 的 Builder 家族手写实体类的equals/hashCode/toString是非常枯燥且容易出错的活。commons-lang3 的 Builder 家族可以流程化生成String toStringResult ToStringBuilder.reflectionToString(obj); boolean equalsResult EqualsBuilder.reflectionEquals(obj1, obj2); int hashResult HashCodeBuilder.reflectionHashCode(obj);反射版适合快速生成性能不是最优但胜在简单。如果实体类的字段有特殊格式要求也可以用 Builder 链式逐字段构建。虽然现在很多项目用 Lombok 注解生成这些方法但老项目不能升级 Lombok、或者对输出格式有特殊要求时这些 Builder 类就是很好的替代。6.3 lang2 到 lang3 的迁移差异与版本踩坑最后说升级和迁移。如果你维护的是老项目从 commons-lang 2.x 升级到 3.x下面几个差异要特别注意第一包名从org.apache.commons.lang改为org.apache.commons.lang3。所有 import 都要改这个没商量。第二3.x 要求 Java 8 及以上。如果你的环境还在 Java 7 或更老升级前先确认 JDK 版本。第三StringUtils.EMPTY的语义变了。在 lang2 里它是 null在 lang3 里它是空字符串。老代码如果依赖EMPTY做判空逻辑迁移后行为可能完全反过来这是最容易产生隐蔽 bug 的地方。第四3.x 移除了 DateUtils日期解析和计算建议直接用 java.time 包。迁移时不需要找替代工具类直接用 LocalDate / LocalDateTime 重写相关代码既干净又符合现代 Java 写法。第五RandomStringUtils的安全性问题在 3.6 之后才正式提供 secure 版本如果你是 3.5 及以前的老版本生成 token 时千万别图省事直接用默认方法。这篇内容写到最后说一点我的个人习惯接到一个新项目第一件事就是看一眼 commons-lang3 的版本顺手把 pom 里版本提到一个长期维护的稳定版写新工具方法前先在 StringUtils、ObjectUtils、ArrayUtils 里翻一遍能直接用就绝不自造。工具库这种东西平常看不见它存在但 null 指针、解析异常、格式化错乱出现的时候你会发现它已经替你挡了太多脏活累活。上面这些方法是我在日常开发里反复用到的照着用基本不会踩坑。如果你也有自己私藏的 commons-lang3 用法欢迎在评论区补充交流。