
好问题这是所有用过Hutool的人都会纠结的一对。先给出最核心的定论isNotNull只判断一个事情就是“这个对象是不是null”而isNotEmpty在非null的基础之上还会继续判断“这个对象是不是空”这里的“空”包括空字符串、空集合、空数组、空Map等。举一个踩过坑的场景从请求里拿了个参数随手写了个非空校验用了isNotNull字段塞进去一个校验直接放行到了数据库层才炸出来。原因就是isNotNull只拦了null没拦空字符串。这篇就从源码层面把两个方法掰开揉碎结合重载形式、判空改造场景和实战中常见的误用逐条讲清楚。1. 项目概述与两个方法的核心定位在Hutool的工具类体系里ObjectUtil是一个用来处理Object类型对象的“兜底型”工具类。它不像StrUtil、CollUtil那样只针对某一类数据类型而是面向所有对象提供判空、比较、克隆、序列化等通用能力。正是因为处理的对象类型不固定ObjectUtil里的判空方法在实现上必须比普通工具类更谨慎既要兼容常见的String、集合、数组又要对未知类型保持合理的行为。isNotEmpty和isNotNull就是这个类里最基础、也最容易被误用的两个判空入口。从命名上就能看出两者的设计意图isNotNull走的是“严格空指针判断”路线只看对象引用是否为null不看对象内部有没有值。isNotEmpty走的是“业务可用性判断”路线先确认对象不为null再确认对象内部是否有实际内容。用生活化的例子来类比isNotNull相当于只看快递盒子有没有送到盒子到了就算“非空”而isNotEmpty还要打开盒子看一眼里面至少得有一件东西才认为“非空”。如果盒子是空的isNotNull会返回trueisNotEmpty会返回false。这两个方法在整个判断链中的定位也不同。isNotNull适合放在最底层用来防止空指针异常比如在反射、流处理、回调函数里先判断某个对象有没有被正确初始化isNotEmpty适合放在业务入口层用来校验外部传入的参数是否真的可用比如用户注册时传入的昵称、文件上传时的文件名、接口调用方的JSON参数。很多人在实际开发中把两者混用最常见的问题是用isNotNull去校验业务字段结果空字符串、空集合全部漏判。这个误用之所以普遍是因为很多业务场景里“为空”并不等于“等于null”但只要代码写成if (ObjectUtil.isNotNull(field))就默认把“为空”简化成了“不等于null”问题就此埋下。2. 两个方法行为差异与源码级解读源码是最好的老师。看ObjectUtil的源码判断逻辑并不复杂但细节里藏着很多值得注意的设计。2.1 isNotNull的实现与语义边界isNotNull的源码在Hutool里非常简短最终调用的其实是isNull的取反public static boolean isNotNull(Object obj) { return !isNull(obj); } public static boolean isNull(Object obj) { return null obj || obj.equals(null); }这个实现里有几个值得注意的地方第一个条件null obj是常规的引用判断对象为null时返回true。第二个条件obj.equals(null)是一个非常规的防御性判断它考虑了某些对象在重写equals方法时把null当成合法值的情况。虽然这种情况在真实业务中极少见但Hutool在基础方法上宁愿多做一些防御也不希望因为意外重写导致判断失效。所以isNotNull的语义边界非常清晰只要对象引用不是null就返回true。换句话说、空集合、空数组、长度为0的Map在isNotNull看来都不是null全部返回true。这个特征决定了它只适合做“防空指针”级别的判断不适合做“业务非空”级别的判断。2.2 isEmpty与isNotEmpty的递归判定逻辑isNotEmpty的源码同样不复杂但它依赖的isEmpty却是一个面向不同类型分别处理的方法public static boolean isNotEmpty(Object obj) { return !isEmpty(obj); } public static boolean isEmpty(Object obj) { if (null obj) { return true; } if (obj instanceof CharSequence) { return StrUtil.isEmpty((CharSequence) obj); } else if (obj instanceof Map) { return MapUtil.isEmpty((Map?, ?) obj); } else if (obj instanceof Iterable) { return IterUtil.isEmpty((Iterable?) obj); } else if (obj instanceof Iterator) { return IterUtil.isEmpty((Iterator?) obj); } else if (ArrayUtil.isArray(obj)) { return ArrayUtil.isEmpty(obj); } return false; }这个方法的设计逻辑非常清晰按类型分成几类如果是CharSequenceString、StringBuilder等交给StrUtil.isEmpty判断此时空字符串、空白字符串都会被认为是“空”。如果是Map交给MapUtil.isEmpty判断内部逻辑是map为null或者map.isEmpty()。如果是IterableList、Set、Collection等交给IterUtil.isEmpty判断但注意这里有个细节它只判断集合是否为空或长度为0不会去判断集合里的元素本身是否为null。如果是Iterator同样交给IterUtil.isEmpty判断。如果是数组包括对象数组、基本类型数组交给ArrayUtil.isEmpty判断。最后如果以上类型都不是比如传入的是一个自定义对象、一个Integer、一个Javabean那么isEmpty会返回false也就是说isNotEmpty会返回true。这个“最后兜底返回false”的设计非常关键。原因是对于一个自定义对象工具类无法知道它的内部字段是否有值所以干脆默认它不为空把判断权利交还给业务代码。这也意味着ObjectUtil.isNotEmpty的目标对象主要是String、集合、数组、Map这几类如果你拿它去判断一个普通的POJO对象是否为空结果永远是true没有任何参考意义。2.3 一句话记住行为差异用一个表格可以非常直观地看明白两者的差异输入对象isNotNullisNotEmptynullfalsefalsetruefalse 空格truefalseabctruetrue空Listtruefalse非空Listtruetrue长度为0的数组truefalse非空数组truetrue空Maptruefalse自定义POJO对象truetrueInteger(0)truetrueBoolean(false)truetrue这个表格基本就是一次面试题的答案。哪怕只是背下来写代码时大概率也不会再选错。3. 实战判空逻辑改造与代码重构指南理解了行为差异接下来看实际项目中怎么用。这里分享一套我自己的判断方法以及几个真实的重构案例。3.1 ObjectUtil判空重载方法盘点除了通用的isNotEmpty(Object obj)ObjectUtil还为常见类型提供了重载版本方便在类型明确的场景下直接调用不用走instanceof分支public static boolean isEmpty(CharSequence str) public static boolean isEmpty(Map?, ? map) public static boolean isEmpty(Collection? collection) public static boolean isEmpty(Object[] array) public static boolean isEmpty(Iterator? iterator) public static boolean isEmpty(Iterable? iterable)对应的也有isNotEmpty的相同重载public static boolean isNotEmpty(CharSequence str) public static boolean isNotEmpty(Map?, ? map) public static boolean isNotEmpty(Collection? collection) public static boolean isNotEmpty(Object[] array) public static boolean isNotEmpty(Iterator? iterator) public static boolean isNotEmpty(Iterable? iterable)在业务代码里如果你已经知道变量类型是String或者List直接用ObjectUtil.isEmpty(str)、ObjectUtil.isEmpty(list)这样的重载版本即可。两个好处一是编译期就确定了类型分支比通用版本的instanceof链少几次判断二是代码更直观阅读者不需要关心底层走的是哪个分支。3.2 统一请求参数校验的判空标准最常见的改造场景是接口参数校验。假设一个用户注册接口接收JSON参数里面有个昵称字段很多人的第一版代码是这样的if (ObjectUtil.isNotNull(user.getNickname())) { // 执行昵称相关逻辑 }这段代码在测试环境没问题但只要调用方传了一个nickname: 校验就会通过。改造思路很简单把isNotNull换成isNotEmptyif (ObjectUtil.isNotEmpty(user.getNickname())) { // 执行昵称相关逻辑 }这样空字符串、纯空格都会被拦截。如果还要严格控制纯空白字符串建议直接用StrUtil.isNotBlank它会在isNotEmpty基础上进一步剔除\t、\n、\r等空白字符。3.3 集合字段判空的优雅写法再比如从数据库或者远端接口返回一个列表字段想要批量处理。原始代码可能是if (list ! null !list.isEmpty()) { for (String item : list) { // 处理逻辑 } }用ObjectUtil.isNotEmpty改造后if (ObjectUtil.isNotEmpty(list)) { for (String item : list) { // 处理逻辑 } }这里省掉的不只是几行代码更重要的是把“list不为null且非空”这个复合条件收敛成了一个语义清晰的单一方法任何人都能一眼看出这是在判空。同理Map和数组的判空也可以写成if (ObjectUtil.isNotEmpty(map)) { // map非空可以安全遍历 } if (ObjectUtil.isNotEmpty(array)) { // array非空可以安全遍历 }3.4 返回值判空与兜底值设置在实际项目中我更推荐把isNotEmpty用在“返回值需要判断是否可用”的场景。例如一个配置查询服务从缓存里取数据可能有三种情况返回null没查到、返回空字符串查到了但没配置、返回正常字符串查到了且有配置。此时业务上需要区分“完全没查到”和“查到了但没配置”但大多数时候业务并不关心区别只要有可用的配置值就直接使用没有就用默认值String configValue configService.getConfig(timeout); if (ObjectUtil.isEmpty(configValue)) { configValue 3000; }用isNotEmpty配合三元表达式可以写得更紧凑String configValue ObjectUtil.isNotEmpty(cacheValue) ? cacheValue : 3000;这种写法在策略模式、模板方法里也很常用父类先执行公共逻辑拿到一个可能为空的中间结果再由子类决定是直接用还是兜底。4. 常见误用与排查技巧判空失效的典型场景即使理解了源码实际项目里依然存在很多容易出问题的用法。下面是我自己在代码评审和排障中遇到的几个典型误用基本覆盖了80%的“判空失效”现场。4.1 用isNotNull判断字符串导致空串漏判这是最典型的误用也是写这篇文章的初衷。场景复现if (ObjectUtil.isNotNull(order.getRemark())) { logicA(); }当order.getRemark()的值为时逻辑进入logicA。如果logicA内部执行了字符串拼接remark 已备注结果会变成“已备注”和“未备注”的场景混淆如果执行了remark.length()直接抛NullPointerException的几率不大但会得到长度0而调用方可能预期这里是真实的备注内容。排查方法很简单遇到“我明明判了非空但里面还是空字符串”这类问题第一反应就是把isNotNull换成isNotEmpty。4.2 isNotEmpty判断自定义对象永远为true另一个更隐晦的误用是拿isNotEmpty去判断自定义对象。我之前在一个订单服务里见过这样的代码if (ObjectUtil.isNotEmpty(orderInfo)) { // 订单信息不为空执行后续逻辑 }不管orderInfo的字段有没有值只要这个对象本身是new出来的非null实例isNotEmpty就返回true。如果调用方期望的是“订单号不为空”“收货地址不为空”这段代码完全没起到拦截作用。正确的做法是拆字段判断或者给自定义对象加一个isEmptyState之类的判断方法。例如if (ObjectUtil.isNotEmpty(orderInfo.getOrderNo()) ObjectUtil.isNotEmpty(orderInfo.getReceivingAddress())) { // 关键字段都非空 }4.3 误以为isNotEmpty等价于StrUtil.isNotBlank另一个常见混淆是把isNotEmpty当成“去掉空格后的非空判断”。看这两行代码ObjectUtil.isNotEmpty( ) // false StrUtil.isNotBlank( ) // false在这个例子中结果巧合一样但换成含换行符、制表符的字符串ObjectUtil.isNotEmpty( \n ) // true因为\n不是空字符串也不是全角空格 StrUtil.isNotBlank( \n ) // false因为isNotBlank会剔除所有空白字符结论就是如果业务上的非空要求是“必须包含可见字符”用StrUtil.isNotBlank如果只要求“不是空字符串空格也算内容”用ObjectUtil.isNotEmpty。4.4 判空链上混合使用老式判断导致风格混乱团队项目里最常见的不是“某个方法用错”而是“同一个类里判空风格五花八门”。有人写list ! null list.size() 0有人写!CollectionUtils.isEmpty(list)有人写ObjectUtil.isNotEmpty(list)。风格不统一的最大成本是代码评审时需要反复确认意图而且容易在流式处理时忽略了判空逻辑。我的建议是在一个模块内做一次统一重构新的代码全部使用ObjectUtil相关判空方法老的代码逐步替换。替换规则只需要记住一条判断对象本身的空指针用isNotNull/isNull判断业务内容是否可用用isNotEmpty/isEmpty。如果面对的是字符串且要求剔除空白用StrUtil.isNotBlank/isBlank。4.5 数组与集合混用isEmpty重载导致类型不匹配虽然ObjectUtil的方法名相同但重载版本针对的类型不同使用时切忌把类型搞混。比如有一个int[]数组int[] numbers new int[0]; ObjectUtil.isEmpty(numbers); // 走ArrayUtil分支返回true这个没问题。但如果你写了Integer[] numbers new Integer[0]; ObjectUtil.isEmpty((Collection?) numbers); // 编译报错类型不兼容那就是自己给自己找麻烦。数组就是数组集合就是集合不要试图交叉使用。再用isNotEmpty判断时保持同样的类型入口即可。4.6 排查判断失效问题的方法论最后给一套排查“判空失效”问题的基本流程。先看代码用的是什么方法其次看目标对象是什么类型最后看空值具体长什么样。联合起来判断就能定位目标对象是不是null如果是整个问题跟isNotNull无关查数据源头。目标对象是不是“空”的特殊形态比如是空字符串、全空白字符串、空集合、空Map还是空数组用isNotNull判断时这些都会被放行。目标对象是不是自定义类型如果是isNotEmpty可能直接返回true需要拆字段判断。目标对象是不是Optional包装类ObjectUtil对Optional没有特殊处理Optional.empty()会被当成非空对象。需要先isPresent()再取值。按这个顺序排查大部分“判空失效”都能在几分钟内定位。5. 实操总结与个人建议结合使用Hutool的经验再补充几条个人的落地建议。第一项目的代码规范里明确规定判空方法的选用标准。最简单的规则是只防空指针用isNotNull判断业务内容可用性用isNotEmpty字符串要去空白用StrUtil.isNotBlank。把这个写进团队规范能大幅减少GitHub评论区的“为什么这里用isNotNull”。第二写工具类封装时尽量把“业务非空”的判断收敛到一个方法里。比如针对订单号、用户ID、手机号这类核心字段可以封装一个BizCheckUtil.isValidOrderNo(String orderNo)内部调用StrUtil.isNotBlank再叠加正则校验这样业务代码里就不用每次纠结该用哪个工具类。第三如果是为了性能极致优化在千万级循环里还是尽量少用工具类的通用重载而是直接写obj null、str.isEmpty()这种原生判断。Hutool的通用方法里有instanceof分支判断存在少量性能损耗但对绝大多数业务系统来说可以忽略不计。回到标题本身ObjectUtil的isNotEmpty和isNotNull从功能上看只是“为null判断”和“为空判断”的区别但在实际开发中这个差异直接决定了多少bug会在测试环境安然通过、上线后才爆发。我个人现在写代码的习惯是凡是外部传入的参数一律先用isNotEmpty做业务校验凡是内部方法返回的对象判断是否可继续操作时先明确这个“可操作”到底指的是“非null”还是“有内容”再决定用哪个方法。这个小细节看起来微不足道但一旦在项目里统一起来代码的可读性、可维护性都会有很明显的提升。