做 Java 开发时,你是不是总被“原生 API 的反人类设计”搞得崩溃?

  1. 字符串判空要叠三层条件:if (str != null && !str.isEmpty() && !str.trim().isEmpty()),少一层就可能抛 NPE;
  2. 异常处理要手动循环:Throwable root = e; while (root.getCause() != null) { root = root.getCause(); },还得手动拼接堆栈;
  3. 对象比较要写一堆 if-elseif (a == null) return b == null; return a.equals(b);,字段多了根本维护不过来;
  4. 日期增减要算毫秒:new Date(date.getTime() + 24*60*60*1000),忽略时区和闰年,容易出错;
  5. 反射获取类信息要写 try-catch:Class<?> clazz = Class.forName("com.example.User");,代码又长又丑。

其实 Apache Commons Lang 早就把这些“重复劳动”全包了。它是 Java 生态的“基础设施级工具库”,扩展了 java.lang 包的核心能力,用一行代码搞定字符串处理、异常处理、对象操作等高频场景,直接砍掉一半冗余代码,还能从根源上避免空指针和逻辑错误。今天就扒一扒它的 5 个核心用法,让你从“模板代码的泥潭”里解脱出来,开发效率翻倍。

各个知识整理的思维导图

一、先搞懂:Commons Lang 为什么是 Java 开发的“标配”?

Apache Commons Lang 是 Apache 基金会的经典工具库(GitHub 星标 1.5k+,下载量超 10 亿次),专为解决 Java 原生 API 的 5 大痛点而生:

  • null 安全:所有工具方法都能接收 null 输入,不会抛 NPE(如 StringUtils.isEmpty(null) 返回 true,而非报错);
  • 简化模板代码:把 equals 比较、哈希计算、堆栈拼接等重复工作封装成一行 API;
  • 线程安全:日期处理等工具类(如 DateUtils)避免了 SimpleDateFormat 的线程安全问题;
  • 无侵入:无需修改原有类(如无需加注解),直接通过静态方法调用,开箱即用;
  • 兼容性强:支持 Java 8+,与 Spring、MyBatis 等框架无缝集成,几乎所有 Java 项目都能直接用。

和 Java 原生 API 对比,效率天差地别:

场景原生写法(代码量)Commons Lang 写法(代码量)核心优势
字符串判空(含空格)3 行(`null“”
获取完整异常堆栈6 行(StringWriter+PrintWriter1 行(ExceptionUtils.getStackTrace(e)无需手动流操作
对象安全比较4 行(null判断+equals1 行(ObjectUtils.equals(a,b)自动处理 null,无 NPE
日期加 1 天3 行(计算毫秒+new Date1 行(DateUtils.addDays(date,1)无需手动算毫秒,兼容时区
反射获取类信息3 行(try-catch+forName1 行(ClassUtils.getName(clazz)简化异常处理,支持 null

简单说:Commons Lang 把 Java 开发中“重复、易错、繁琐”的模板代码,变成了“一行调用”的工具方法,你不用再关心“null 怎么处理”“异常怎么拼接”“日期怎么计算”,专注业务逻辑即可。

二、5 个核心用法:从“冗余模板”到“一行搞定”

按“使用频率”排序,每个用法都附“原生写法 vs Commons Lang 写法”对比,搭配实际业务场景,学完就能用。

1. StringUtils:字符串处理的“瑞士军刀”(最常用)

核心场景:字符串判空、拼接、分割、替换、截取(所有操作均 null 安全)。

示例:判断用户输入是否为空、拼接多个字符串(含 null)

传统写法(约 5 行代码,冗余且易抛 NPE):
public class StringHandleTraditional {
    public static void main(String[] args) {
        String username = "  "; // 用户输入的空格
        String nickname = null; // 可能为 null
        String suffix = "先生";

        // 1. 判空(含 null、空字符串、纯空格)
        boolean isUsernameValid = false;
        if (username != null) {
            String trimmed = username.trim();
            isUsernameValid = !trimmed.isEmpty();
        }

        // 2. 拼接字符串(避免 null 被拼为"null")
        String fullName = "";
        if (nickname != null) {
            fullName = nickname + suffix;
        } else {
            fullName = "未知" + suffix;
        }

        System.out.println("用户名是否有效:" + isUsernameValid); // 输出:false
        System.out.println("完整昵称:" + fullName); // 输出:未知先生
    }
}

问题:判空需要多层嵌套,拼接需要手动处理 null,代码冗余;若遗漏 null 判断,直接调用 username.trim() 会抛 NPE。

Commons Lang 写法(约 2 行代码,null 安全):
import org.apache.commons.lang3.StringUtils;

public class StringHandleCommonsLang {
    public static void main(String[] args) {
        String username = "  ";
        String nickname = null;
        String suffix = "先生";

        // 1. 一行判空(isBlank 包含 null、""、纯空格)
        boolean isUsernameValid = StringUtils.isNotBlank(username);

        // 2. 一行拼接(自动忽略 null,用默认值替代)
        String fullName = StringUtils.defaultIfBlank(nickname, "未知") + suffix;

        System.out.println("用户名是否有效:" + isUsernameValid); // 输出:false
        System.out.println("完整昵称:" + fullName); // 输出:未知先生
    }
}

扩展用法(高频)

  • 安全截取:StringUtils.substring("abcdef", 2, 4)"cd"(null 输入返回 null,不抛异常);
  • 移除前缀/后缀:StringUtils.removeStart("file://test.txt", "file://")"/test.txt"
  • 重复字符串:StringUtils.repeat("ab", 3)"ababab"
  • 分割保留空元素:StringUtils.splitPreserveAllTokens("a,,b", ",")["a", "", "b"](原生 split 会忽略空元素)。

业务场景:校验用户注册表单

public boolean validateRegisterForm(String username, String phone) {
    // 用户名非空且长度 2-20,手机号非空且匹配正则
    return StringUtils.isNotBlank(username) && username.length() >= 2 && username.length() <= 20
            && StringUtils.isNotBlank(phone) && StringUtils.match(phone, "^1[3-9]\\d{9}$");
}

2. ExceptionUtils:异常处理的“效率工具”

核心场景:获取完整堆栈、定位根异常、检查异常链类型(避免手动循环 getCause)。

示例:记录异常堆栈、找到嵌套异常的根因

传统写法(约 8 行代码,循环+流操作):
import java.io.PrintWriter;
import java.io.StringWriter;

public class ExceptionHandleTraditional {
    public static void main(String[] args) {
        try {
            // 模拟嵌套异常:业务异常→SQL异常→IO异常
            throw new BusinessException("保存失败", 
                new SQLException("数据库连接超时", 
                    new IOException("网络中断")));
        } catch (Exception e) {
            // 1. 手动获取完整堆栈
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            String stackTrace = sw.toString();

            // 2. 手动定位根异常
            Throwable rootCause = e;
            while (rootCause.getCause() != null) {
                rootCause = rootCause.getCause();
            }

            System.out.println("堆栈信息:" + stackTrace);
            System.out.println("根异常:" + rootCause.getMessage()); // 输出:网络中断
        }
    }

    static class BusinessException extends RuntimeException {
        public BusinessException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

问题:获取堆栈需要创建流对象,定位根异常需要手动循环,代码冗余且易因循环引用陷入死循环。

Commons Lang 写法(约 2 行代码,一键搞定):
import org.apache.commons.lang3.exception.ExceptionUtils;

public class ExceptionHandleCommonsLang {
    public static void main(String[] args) {
        try {
            throw new BusinessException("保存失败", 
                new SQLException("数据库连接超时", 
                    new IOException("网络中断")));
        } catch (Exception e) {
            // 1. 一行获取完整堆栈
            String stackTrace = ExceptionUtils.getStackTrace(e);

            // 2. 一行定位根异常(自动处理循环引用)
            Throwable rootCause = ExceptionUtils.getRootCause(e);

            // 3. 一行检查异常链是否包含 IOExceptions
            boolean hasIoException = ExceptionUtils.containsThrowable(e, IOException.class);

            System.out.println("堆栈信息:" + stackTrace);
            System.out.println("根异常:" + rootCause.getMessage()); // 输出:网络中断
            System.out.println("是否含 IO 异常:" + hasIoException); // 输出:true
        }
    }

    static class BusinessException extends RuntimeException {
        public BusinessException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

业务场景:全局异常处理器

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        // 拼接完整异常消息链(外层→中层→根因)
        String errorMsg = ExceptionUtils.getRootCauseMessage(e);
        // 记录完整堆栈
        log.error("系统异常:{}", ExceptionUtils.getStackTrace(e));
        return ResponseEntity.status(500)
                .body(new ErrorResponse(500, "操作失败:" + errorMsg));
    }
}

3. ObjectUtils:对象操作的“安全卫士”

核心场景:对象判空、安全比较、设置默认值、计算哈希值(避免 NPE)。

示例:比较两个对象、为 null 对象设置默认值

传统写法(约 6 行代码,多层 null 判断):
public class ObjectHandleTraditional {
    public static void main(String[] args) {
        Integer a = null;
        Integer b = 100;
        User user1 = new User(1L, "张三");
        User user2 = null;

        // 1. 安全比较两个对象(避免 NPE)
        boolean isEqual;
        if (a == null && b == null) {
            isEqual = true;
        } else if (a == null || b == null) {
            isEqual = false;
        } else {
            isEqual = a.equals(b);
        }

        // 2. 为 null 对象设置默认值
        User defaultUser = user2 != null ? user2 : new User(0L, "默认用户");

        System.out.println("a 和 b 是否相等:" + isEqual); // 输出:false
        System.out.println("默认用户:" + defaultUser.getName()); // 输出:默认用户
    }

    static class User {
        private Long id;
        private String name;
        // 构造器、getter 省略
    }
}

问题:对象比较需要多层 null 判断,代码冗余;若直接调用 a.equals(b),当 a 为 null 时抛 NPE。

Commons Lang 写法(约 2 行代码,自动处理 null):
import org.apache.commons.lang3.ObjectUtils;

public class ObjectHandleCommonsLang {
    public static void main(String[] args) {
        Integer a = null;
        Integer b = 100;
        User user1 = new User(1L, "张三");
        User user2 = null;

        // 1. 一行安全比较(null 视为小于非 null)
        boolean isEqual = ObjectUtils.equals(a, b);

        // 2. 一行设置默认值(null 时返回默认对象)
        User defaultUser = ObjectUtils.defaultIfNull(user2, new User(0L, "默认用户"));

        // 3. 一行计算哈希值(null 返 0)
        int hashCode = ObjectUtils.hashCode(user1);

        System.out.println("a 和 b 是否相等:" + isEqual); // 输出:false
        System.out.println("默认用户:" + defaultUser.getName()); // 输出:默认用户
        System.out.println("user1 哈希值:" + hashCode); // 输出:非 0 整数
    }

    static class User {
        private Long id;
        private String name;
        // 构造器、getter 省略
    }
}

业务场景:处理查询结果默认值

public User getUserById(Long id) {
    // 数据库查询可能返回 null,用默认用户替代
    User user = userMapper.selectById(id);
    return ObjectUtils.defaultIfNull(user, new User(0L, "匿名用户"));
}

4. DateUtils:日期处理的“简化工具”

核心场景:日期增减、格式化、解析、比较(替代线程不安全的 SimpleDateFormat)。

示例:日期加 3 天、解析多格式日期字符串

传统写法(约 8 行代码,try-catch+毫秒计算):
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateHandleTraditional {
    public static void main(String[] args) {
        Date now = new Date();
        String dateStr = "2023年10月01日";

        // 1. 日期加 3 天(手动计算毫秒,易出错)
        long threeDays = 3 * 24 * 60 * 60 * 1000L;
        Date nextThreeDays = new Date(now.getTime() + threeDays);

        // 2. 解析日期字符串(需指定格式,处理 ParseException)
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
        Date parsedDate = null;
        try {
            parsedDate = sdf.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        System.out.println("三天后:" + sdf.format(nextThreeDays));
        System.out.println("解析后的日期:" + parsedDate);
    }
}

问题:日期增减需手动计算毫秒(忽略时区、闰年),解析需指定格式(不支持多格式),SimpleDateFormat 线程不安全(多线程下会错乱)。

Commons Lang 写法(约 2 行代码,自动适配):
import org.apache.commons.lang3.time.DateUtils;
import java.util.Date;

public class DateHandleCommonsLang {
    public static void main(String[] args) {
        Date now = new Date();
        String dateStr = "2023年10月01日";

        // 1. 一行日期加 3 天(自动处理时区、闰年)
        Date nextThreeDays = DateUtils.addDays(now, 3);

        // 2. 一行解析多格式日期(自动匹配格式,无需 try-catch)
        String[] patterns = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyy年MM月dd日"};
        Date parsedDate = DateUtils.parseDate(dateStr, patterns);

        // 3. 一行比较是否同一天
        boolean isSameDay = DateUtils.isSameDay(parsedDate, nextThreeDays);

        System.out.println("三天后:" + DateUtils.format(nextThreeDays, "yyyy年MM月dd日"));
        System.out.println("解析后的日期:" + parsedDate);
        System.out.println("是否同一天:" + isSameDay); // 输出:false
    }
}

扩展用法

  • 日期截断:DateUtils.truncate(now, Calendar.DAY_OF_MONTH) → 清除时分秒,保留日期;
  • 日期差计算:DateUtils.getFragmentInDays(nextThreeDays, now) → 计算两个日期的天数差;
  • 线程安全格式化:DateUtils.format(date, "yyyy-MM-dd") → 替代 SimpleDateFormat

业务场景:计算订单有效期

public Date getOrderExpireTime(Date createTime) {
    // 订单创建后 7 天过期
    return DateUtils.addDays(createTime, 7);
}

// 校验订单是否过期
public boolean isOrderExpired(Date createTime) {
    Date expireTime = getOrderExpireTime(createTime);
    return DateUtils.isAfter(new Date(), expireTime); // 当前时间是否在过期时间之后
}

5. ClassUtils:反射与类操作的“辅助工具”

核心场景:获取类信息、检查类关系、加载类(简化反射代码,避免繁琐 try-catch)。

示例:获取类名、检查类是否为子类、加载类

传统写法(约 6 行代码,try-catch+手动判断):
public class ClassHandleTraditional {
    public static void main(String[] args) {
        Class<?> userClass = User.class;
        String className = "com.example.User";

        // 1. 获取类的全限定名(原生 API 无简化方法)
        String fullClassName = userClass.getName();

        // 2. 检查 Integer 是否为 Number 的子类
        boolean isSubclass = Number.class.isAssignableFrom(Integer.class);

        // 3. 加载类(需处理 ClassNotFoundException)
        Class<?> loadedClass = null;
        try {
            loadedClass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println("类名:" + fullClassName); // 输出:com.example.User
        System.out.println("是否子类:" + isSubclass); // 输出:true
        System.out.println("加载的类:" + loadedClass);
    }

    static class User {}
}

问题:加载类需要手动处理 ClassNotFoundException,检查类关系的方法名晦涩(isAssignableFrom 不易理解)。

Commons Lang 写法(约 2 行代码,直观简洁):
import org.apache.commons.lang3.ClassUtils;

public class ClassHandleCommonsLang {
    public static void main(String[] args) {
        Class<?> userClass = User.class;
        String className = "com.example.User";

        // 1. 一行获取类名(支持 null,返回"null")
        String fullClassName = ClassUtils.getName(userClass);
        String nullClassName = ClassUtils.getName(null); // 输出:"null"

        // 2. 一行检查是否为子类(方法名直观)
        boolean isSubclass = ClassUtils.isAssignable(Integer.class, Number.class);

        // 3. 一行加载类(无需手动 try-catch,抛 unchecked 异常)
        Class<?> loadedClass = ClassUtils.getClass(className);

        // 4. 一行获取所有父类和接口
        Iterable<Class<?>> allTypes = ClassUtils.getAllSuperclassesAndInterfaces(userClass);

        System.out.println("类名:" + fullClassName); // 输出:com.example.User
        System.out.println("是否子类:" + isSubclass); // 输出:true
        System.out.println("加载的类:" + loadedClass);
    }

    static class User {}
}

业务场景:反射实例化对象

public Object createInstance(String className) {
    try {
        // 加载类并实例化(无参构造)
        Class<?> clazz = ClassUtils.getClass(className);
        return clazz.getDeclaredConstructor().newInstance();
    } catch (Exception e) {
        log.error("实例化对象失败:{}", ExceptionUtils.getRootCauseMessage(e));
        return null;
    }
}

三、避坑指南:这 5 个细节 90% 的人会踩

Commons Lang 虽简单,但新手容易因“误解方法逻辑”踩坑,整理实战中最常遇到的问题:

1. StringUtils.isBlank() vs isEmpty():别用混!

  • isEmpty(str):仅判断 str == nullstr.equals("")" " 视为非空);
  • isBlank(str):判断 str == nullstr.equals("")str.trim().equals("")" " 视为空)。

错误示例:用户输入空格视为“未填写”,却误用 isEmpty

String userInput = "  ";
if (StringUtils.isEmpty(userInput)) {
    System.out.println("未填写"); // 不执行(isEmpty 认为"  "非空)
}

正确做法:用户输入、文本内容判空优先用 isBlank

2. DateUtils 解析多格式时,注意格式顺序

DateUtils.parseDate(str, patterns) 按数组顺序匹配格式,优先级:前面的格式先匹配。
错误示例:先放 yyyy/MM/dd,再放 yyyy-MM-dd,导致 2023-10-01 匹配失败(被误认为 yyyy/MM/dd):

String[] patterns = {"yyyy/MM/dd", "yyyy-MM-dd"};
DateUtils.parseDate("2023-10-01", patterns); // 抛 ParseException

正确做法:将“更具体”或“更常用”的格式放在前面。

3. ExceptionUtils.getStackTrace(e) 堆栈可能很长,注意日志大小

生产环境中,频繁打印超长堆栈会占用大量磁盘空间,建议:

  • 非关键异常(如用户输入错误)仅打印消息,不打印完整堆栈;
  • 关键异常(如数据库连接失败)打印堆栈,但可通过 StringUtils.abbreviate 截断:
    String stackTrace = StringUtils.abbreviate(ExceptionUtils.getStackTrace(e), 2000); // 截断为 2000 字符
    

4. ObjectUtils.defaultIfNull 注意默认值类型匹配

默认值类型需与目标类型一致,否则可能触发类型转换异常:
错误示例Integer a = null; Integer b = ObjectUtils.defaultIfNull(a, "0");(默认值是 String,目标类型是 Integer);
正确做法:默认值类型与目标类型一致:Integer b = ObjectUtils.defaultIfNull(a, 0);

5. ClassUtils.getClass() 加载类时,依赖类路径

ClassUtils.getClass(className) 用当前线程的类加载器加载类,若类不在类路径(如第三方 jar 未引入),会抛 ClassNotFoundException(unchecked 异常),需提前确保依赖齐全。

四、和其他工具对比:为什么 Commons Lang 是“标配”?

Java 生态中还有 Guava、Hutool 等工具库,Commons Lang 的核心优势在哪?

工具核心优势劣势适用场景
Apache Commons Lang兼容性强(Java 8+)、生态成熟、无依赖、Spring 默认集成功能较基础(无复杂集合运算)所有 Java 项目(尤其是传统项目、Spring 项目)
Guava功能全面(集合、缓存、并发)、API 优雅依赖较大(JAR 包 2MB+)、学习成本高复杂业务场景(如分布式缓存、高并发)
Hutool中文文档友好、功能聚合(含 AI、Excel 等)生态较新、部分场景稳定性待验证国内项目、快速开发场景

结论

  • 传统项目/ Spring 项目:优先用 Commons Lang(无依赖、无缝集成);
  • 复杂业务场景:用 Guava 补充(如集合运算、缓存);
  • 国内快速开发:用 Hutool(中文文档、功能全面)。

总结

Apache Commons Lang 的核心价值,是用“简洁、安全、无侵入”的工具方法,解决了 Java 原生 API 的 5 大痛点:

  1. 字符串处理:StringUtils 一行判空、拼接,无 NPE;
  2. 异常处理:ExceptionUtils 一键获取堆栈、定位根因;
  3. 对象操作:ObjectUtils 安全比较、设置默认值;
  4. 日期处理:DateUtils 简化增减、解析,线程安全;
  5. 类操作:ClassUtils 简化反射、类信息获取。

核心用法记住这 5 句口诀:

  • 字符串用 StringUtils,判空优先 isBlank
  • 异常用 ExceptionUtils,堆栈根因一键拿;
  • 对象用 ObjectUtils,比较默认不 NPE;
  • 日期用 DateUtils,增减解析多格式;
  • 类操作 ClassUtils,反射加载少 try-catch。

下次写模板代码时,别再手动判空、循环拼接了——Commons Lang 早就把这些工作做成了“开箱即用的 API”,让你少写代码,少踩坑。

需要我针对“Commons Lang 在 Spring Boot 中的实战配置(如全局日期格式化、参数校验)”补充更详细的案例吗?

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐