CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / JAVA_BASICS / P-039 · Java 基础

Java 基础 009:去除不必要的!=

项目中会存在大量判空代码,多么丑陋繁冗!如何避免这种情况?我们是否滥用了判空呢?本文通过五个场景,逐一给出更优雅的处理方式。

场景一:null 无意义之常规判断空

通常是这样的:

private void xxxMethod(String key) {
    if (key != null && !"".equals(key)) {
        // do something
    }
}

初步的改进,是使用 Apache Commons、Guava、Hutool 等类库提供的 StringUtils:

private void xxxMethod(String key) {
    if (StringUtils.isNotEmpty(key)) {
        // do something
    }
}

场景二:null 无意义之使用断言 Assert

考虑用 Assert 断言:

private void xxxMethod(String key) {
    Assert.notNull(key);
    // do something
}

场景三:写 util 类是否都需要逐级判断空

逐级判断空,还是抛出自定义异常,还是不处理?It Depends…

随手翻了下,hutool 的 IdcardUtil 显然是交给调用者判断的:

/**
 * 是否有效身份证号
 *
 * @param idCard 身份证号,支持18位、15位和港澳台的10位
 * @return 是否有效
 */
public static boolean isValidCard(String idCard) {
    idCard = idCard.trim(); // 这里idCard没判断空
    int length = idCard.length();
    switch (length) {
        case 18: // 18位身份证
            return isValidCard18(idCard);
        case 15: // 15位身份证
            return isValidCard15(idCard);
        case 10: { // 10位身份证,港澳台地区
            String[] cardVal = isValidCard10(idCard);
            return null != cardVal && "true".equals(cardVal[2]);
        }
        default:
            return false;
    }
}

再比如 Apache Commons IO 中,并没判断空:

/**
 * Copy bytes from a <code>byte[]</code> to an <code>OutputStream</code>.
 *
 * @param input  the byte array to read from
 * @param output the <code>OutputStream</code> to write to
 * @throws IOException In case of an I/O problem
 */
public static void copy(final byte[] input, final OutputStream output)
        throws IOException {
    output.write(input);
}

场景四:让 null 变的有意义

返回一个空对象(而非 null 对象),比如 NO_ACTION 是特殊的 Action,那么我们就定义一个 Action。下面举个”栗子”,假设有如下代码,其中 Parser 有一个接口 FindAction,这个接口会依据用户的输入,找到并执行对应的动作。假如用户输入不对,可能就找不到对应的动作(Action),因此 findAction 就会返回 null,接下来 action 调用 doSomething 方法时,就会出现空指针:

public interface Action {
    void doSomething();
}

public interface Parser {
    Action findAction(String userInput);
}

解决这个问题的一个方式,就是使用 Null Object pattern(空对象模式)。NullObject 模式首次发表在”程序设计模式语言”系列丛书中。一般的,在面向对象语言中,对对象的调用前需要使用判空检查,来判断这些对象是否为空,因为在空引用上无法调用所需方法。

我们来改造一下。类定义如下,这样定义 findAction 方法后,确保无论用户输入什么,都不会返回 null 对象:

public class MyParser implements Parser {
    private static Action NO_ACTION = new Action() {
        public void doSomething() { /* do nothing */ }
    };

    public Action findAction(String userInput) {
        // ...
        if ( /* we can't find any actions */ ) {
            return NO_ACTION;
        }
    }
}

对比下面两份调用实例。

  1. 冗余:每获取一个对象,就判一次空:
Parser parser = ParserFactory.getParser();
if (parser == null) {
    // now what?
    // this would be an example of where null isn't (or shouldn't be) a valid response
}
Action action = parser.findAction(someInput);
if (action == null) {
    // do nothing
} else {
    action.doSomething();
}
  1. 精简:
ParserFactory.getParser().findAction(someInput).doSomething();

因为无论什么情况,都不会返回空对象,因此通过 findAction 拿到 action 后,可以放心地调用 action 的方法。

顺便再提下一个插件:NR Null Object 插件。NR Null Object 是一款适用于 Android Studio、IntelliJ IDEA、PhpStorm、WebStorm、PyCharm、RubyMine、AppCode、CLion、GoLand、DataGrip 等 IDEA 的 Intellij 插件。其可以根据现有对象,便捷快速生成其空对象模式需要的组成成份,其包含功能如下:

  • 分析所选类可声明为接口的方法;
  • 抽象出公有接口;
  • 创建空对象,自动实现公有接口;
  • 对部分函数进行可为空声明;
  • 可追加函数进行再次生成;
  • 自动的函数命名规范。

场景五:Java8 中使用 Optional

假设我们有一个像这样的类层次结构:

class Outer {
    Nested nested;

    Nested getNested() {
        return nested;
    }
}

class Nested {
    Inner inner;

    Inner getInner() {
        return inner;
    }
}

class Inner {
    String foo;

    String getFoo() {
        return foo;
    }
}

解决这种结构的深层嵌套路径是有点麻烦的。我们必须编写一堆 null 检查来确保不会导致一个 NullPointerException:

Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
    System.out.println(outer.nested.inner.foo);
}

我们可以通过利用 Java 8 的 Optional 类型来摆脱所有这些 null 检查。map 方法接收一个 Function 类型的 lambda 表达式,并自动将每个 function 的结果包装成一个 Optional 对象。这使我们能够在一行中进行多个 map 操作,Null 检查是在底层自动处理的:

Optional.of(new Outer())
        .map(Outer::getNested)
        .map(Nested::getInner)
        .map(Inner::getFoo)
        .ifPresent(System.out::println);

还有一种实现相同作用的方式,就是通过利用一个 supplier 函数来解决嵌套路径的问题:

Outer obj = new Outer();
resolve(() -> obj.getNested().getInner().getFoo())
        .ifPresent(System.out::println);

调用 obj.getNested().getInner().getFoo() 可能会抛出一个 NullPointerException 异常。在这种情况下,该异常将会被捕获,而该方法会返回 Optional.empty():

public static <T> Optional<T> resolve(Supplier<T> resolver) {
    try {
        T result = resolver.get();
        return Optional.ofNullable(result);
    } catch (NullPointerException e) {
        return Optional.empty();
    }
}

请记住,这两个解决方案可能没有传统 null 检查那么高的性能,不过在大多数情况下不会有太大问题。

更多 Optional 相关内容(Optional 类的意义、常用方法、举例贯穿所有知识点等),可以看”Java 8 - Optional 类”这篇。

系列导航

← 软件工程 009:典型的中小团队开发流程详解 目录 集合源码 009:WeakHashMap源码解析 →
← 返回文章列表