【灰蓝 Java 训练】如何处理空值_JAVA_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > JAVA > 【灰蓝 Java 训练】如何处理空值

【灰蓝 Java 训练】如何处理空值

 2021/2/9 15:32:31  jywhltj  程序员俱乐部  我要评论(0)
  • 摘要:本文也发在我的个人博客上:https://hltj.me/java/2021/01/09/java-exercise-nulls.html。这个系列以练习为主,可能不会有多少讲述(当然本篇例外),可以作为初学者的自学验收之用。Java中有非受限的空值,并且不知哪时会引发NPE(即NullPointerException),解决这个问题对于Android开发来说很简单——用Kotlin就好了。其实不仅限于Android,对于服务端开发来说终极方案也应该是迁移到Kotlin。因为只要用Java
  • 标签:Java 何处

本文也发在我的个人博客上:https://hltj.me/java/2021/01/09/java-exercise-nulls.html?。

这个系列以练习为主,可能不会有多少讲述(当然本篇例外),可以作为初学者的自学验收之用。

Java 中有非受限的空值,并且不知哪时会引发 NPE(即?class="language-plaintext highlighter-rouge" style="font-size: 15px; border: 1px solid #e8e8e8; border-radius: 3px; background-color: #eeeeff; padding: 1px 5px; font-family: SFMono-Regular, Menlo, 'DejaVu Sans Mono', 'Liberation Mono', Consolas, Monaco, 'Courier New', Courier, monospace;">NullPointerException),解决这个问题对于 Android 开发来说很简单——用 Kotlin 就好了。 其实不仅限于 Android,对于服务端开发来说终极方案也应该是迁移到 Kotlin。 因为只要用 Java,空值问题就没办法彻底解决(之前在《现代编程语言系列2:安全表达可选值》中也提到过这点),而 JVM 平台主流工业级语言中只有 Kotlin 很好地解决了这一问题。

但是对于服务端开发来说,常有各种非技术原因不能在项目中以 Kotlin 取代 Java,对于这些项目来说显然没办法彻底解决空值问题。 那么有没有一些方法与工具可以让空值问题处理起来尽可能规范、简易些呢?这里有几点经验分享

NPE 防御

一些典型场景的 NPE 可以通过编码习惯来防御——某些静态分析工具或许也能检测到一些问题,但很难完美覆盖; 没办法,只能通过编码规范、程序员的自律来解决了。 其中比较常见的两个场景是空值比较以及使用不可变集合。

空值比较

对实际值为?null?的变量调用包括?equals()?在内的任何方法都会导致 NPE。 因此比较可空值(通常为变量)与非空值(通常为常量,不尽然)时,以可空值为参数对非空值调?equals()?即可避免这个问题。 例如:

if ("Hello".equals(nullableStr)) {
    ……
}

如果比较两个可空值怎么办呢?用?Objects.equals(),例如:

if (Objects.equals(nullableObj1, nullableObj2)) {
    ……
}

不可变集合不支持空值

Java 9 引入的?List.of()Set.of()Map.of()Map.entry()?以及 Java 10 引入的?List.copyOf()Set.copyOf()Map.copyOf()Collectors.toUnmodifiableList()Collectors.toUnmodifiableSet()Collectors.toUnmodifiableMap()?等均不支持?null,其中构造不可变的?Map?与?Map.Entry?时 key、value 均不能为?null。 还需要注意的一点是不能以?null?值调用不可变集合的?contains()?/?containsKey()?/?containsValue()?/?containsAll()?方法,其中?containsAll()?还要求参数集合中不能有?null

例如:

List.of("a", null); // NPE:元素不可以有空值
Map.of("a", 1, null, 2); // NPE:key 不可有空值
Map.of("a", 1, "b", null); // NPE:value 不可有空值
Map.entry("hello", null); // NPE:value 不可有空值

var map = new HashMap<String, Integer>();
map.put("a", 1);
Map.copyOf(map); // OK
map.put(null, 2);
Map.copyOf(map); // NPE:key 不可有空值

// NPE:元素不可以有空值
Stream.of((String)null).collect(Collectors.toUnmodifiableList()); 

// NPE:不能用 null 调用不可变集合的 containsKey() 方法
Map.of("a", "b").containsKey(null);

千万不要因为这点而放弃不可变集合。 不可变集合本身有很多优势,Java 10 及以后版本也推荐使用不可变集合。 只是需要特别注意上述几点:构造字面值时不能有?null、调用?contains()?/?containsKey()?等之前需要判断参数是否为?null、调用?collect()?/?copyOf()?/?containsAll()?之前去除参数集合中的?null?值。 例如:

var isInSet = nullableStr != null && Set.of("hello", "world").contains(nullableStr);

Stream.of("hello", (String)null, "world").filter(Objects::nonNull).collect(Collectors.toList());

var map = new HashMap<String, Integer>();
map.put("a", 1);
map.put(null, 2);
map.put("b", null);

map.entrySet().stream()
        .filter(entry -> entry.getKey() != null && entry.getValue() != null)
        .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));

Optional

Java 中解决可选值问题,首先应该想到的就是?Optional。?Optional?是 Java 8 引入的可选值类型,用于在很多场景中取代?null?来表达可选值,进而避免?null?所带来问题。

Optional?的用法

Optional?的核心方法有?map()flatMap()orElse()?与?orElseGet()

map():由一个可选值得到另一个可选值

例如对于一个可选的字符串,计算其长度:

var lengthOpt = Optional.ofNullable(nullableString).map(String::length);

flatMap():处理可选值嵌套情况

例如,从可空整数列表中取第一个非零值,从列表中取第一个元素可以用?Stream<Integer>#findFirst(),该方法返回?Optional<Integer>,如果继续用?map()?的话,会得到可选值的可选值:

Optional<Optional<Integer>> intOptOpt = Optional.ofNullable(nullableIntList).map(intList ->
        intList.stream().filter(i -> i > 0).findFirst()
);

而用?flatMap()?会将两层?Optional?打平为一层:

Optional<Integer> intOpt = Optional.ofNullable(nullableIntList).flatMap(intList ->
        intList.stream().filter(i -> i > 0).findFirst()
);

orElse()orElseGet():由可选值得到值,如果无值取默认值

例如,对于可选字符串,有值取长度,无值取?0,可以用?orElse()?得到一个整数:

int length = strOpt.map(String::length).orElse(0);

如果默认值需要惰性求值,那么还可以用?orElseGet()

int lengthOrRandom = strOpt.map(String::length).orElseGet(() ->
        new Random().nextInt()
);

还有一个特别的场景,就是将?Optional<T>?转换为可空的?T?值时千万不能想当然地调?get()——它在无值时抛?NoSuchElementException?而不是返回?null(参见其文档)。 正确的方式是?orElse(null)。例如:

Integer lengthOrNull = strOpt.map(String::length).orElse(null);

Optional?的其他方法在特定场景也很实用,由于方法不多,大家可以直接读其文档,在此不再赘述。

Optional?的问题

很多文章称?Optional?是 Java 8 针对 NPE 问题甚至十亿美元问题的解决方案,实际上远非如此。且不说在 Java 中没办法强制使用?Optional?而不用?null,即便?Optional?自身用起来也有很多局限性。

Optional?对象自身也可能为?null

一个特别坑的问题是?Optional?是引用类型,因此一个?Optional?对象自身也可能为?null,例如:

Optional<String> strOpt = null;
strOpt.map(String::length); // NPE

当然?Optional?的官方文档称?Optional?类型值不应该为?null,在 IDEA 中写上述代码也会标出警告,但 Java 语法与编译器并不会为此提供任何保障或约束。

Optional?不可序列化

Optional?不可序列化,这就意味着需要序列化的场景还得用?null?来表达可选值,这也是上文特别提到由?Optional?转换可空值的主要原因。 鉴于此,Optional?不适合作为类的字段,也不适合作为方法的入参,只适用于局部变量、返回值以及表达式中间结果等场景。 IDEA 也会对?Optional?用作字段或者参数时标记警告。

不够简洁

与受限空值语法相比,Optional?用法要冗长的多,当然这很符合 Java 的历史风格。 对于受限空值的??.?语法,Optional?要用?map()?与?flatMap();对于??:/???语法要用?orElse()?或?orElseGet()。 其实对于采用可选值类型的其他语言来说可能都有类似问题,但是?HaskellScala?有推导式,HaskellScalaOCamlF#?等语言还支持自定义caozuofu.html" target="_blank">操作符,从而简化可选值的用法。 不幸的是 Java 不具备这些语法。

可空性注解

在 Java 中没办法强制区分可空与非空类型,而有时又希望能在字段、参数或者返回值上标注是否可空,以便 IDE 或者其他静态检查工具能够识别并给出提醒。

我们看个示例,实现一个简陋版的??:/??

public static <T> T defaultWith(T obj, T defaultVal) {
    return Optional.of(obj).orElse(defaultVal);
}

当?obj?非空时该方法返回?obj,否则返回?defaultVal。 但是实际上事与愿违,这个方法在?obj?为空时不会返回?defaultVal,而是抛 NPE。 原因是?Optional.of()?只接受非空参数,如果参数为空就会抛 NPE。 遗憾的是编写与编译这段代码都不会收到任何警告或提醒,因为无论 IDE 还是编译器都无从知晓这个方法的入参?obj?可能为空。

此时,如果我们给?obj?加一个?edu.umd.cs.findbugs.annotations.Nullable?注解,IDEA 就会对方法中使用?obj?调用?Optional.of()?标出警告提醒:“Argument ‘obj’ might be null”。 改为调用?Optional.ofNullable()?警告就消失了。 而如果希望?defaultVal?要求非空的话,还可以对?defaultVal?标注?edu.umd.cs.findbugs.annotations.NonNull,这样一来方法的返回值也会非空,同样可以标注?NonNull

@NonNull
public static <T> T defaultWith(@Nullable T obj, @NonNull T defaultVal) {
    return Optional.ofNullable(obj).orElse(defaultVal);
}

当然,这只是关于可空性注解使用场景的一个示例,实际上并不需要我们自己造一个这样的轮子,因为有现成的轮子可用(见下文)。 上述?Nullable?与?NonNull?两个注解来自于?spotbugs-annotations。类似的还有?Checker Framework 的 Nullness Checker、JetBrains 的 java-annotations、Lombok 的 NonNull?等。

Apache Commons

Apache Commons?有多个库提供了简化空值使用的工具。例如实现类似??:/???的功能,使用?Optional?需要这样写:

var nonNullVal = Optional.ofNullable(obj).orElse(nonNullDefault);

而使用 Apache Commons 只需调用一个类似上文实现的静态方法就可以了。

StringUtils

StringUtils?是?Commons Lang?中的一个工具类,其中包含一系列与字符串空值相关的方法。

isEmpty()?与?isNotEmpty()

前者判断一个字符序列是否为?null?或空序列,后者与之相反——判断一个字符序列既非?null?也非空序列。 这两个方法非常实用,现实业务中对字符串为?null?或空串时走同样处理分支的场景很常见。

defaultString()

defaultString(str, defaultStr)?如果?str?非?null?返回?str,否则返回?defaultStr。 还有一个单参重载版本:defaultString(str)?如果非?null?返回?str,否则返回空串。

defaultIfEmpty()

defaultIfEmpty(str, defaultStr)?如果?str?既非?null?也非空串返回?str,否则返回?defaultStr

getIfEmpty()

相当于默认值采用惰性求值的?defaultIfEmpty()getIfEmpty(str, () -> ……)?当?str?既非?null?也非空串时返回?str,否则对 lambda 表达式求值并以其返回值作为?getIfEmpty()?的返回值。

firstNonEmpty()

对于一系列值,返回第一个既非?null?也非空串的。

isAllEmpty()isAnyEmpty()isNoneEmpty()

对于一系列值,判断是否全都是其中有全都不是?null?或空串。

除了这些明显与空值相关的方法外,StringUtils?的其他方法也都会对?null?特殊处理而不是引发 NPE(个别的会抛其他异常)。

Commons Collections

与?StringUtils?类似,Commons Collections?中的?CollectionUtilsIterableUtilsListUtilsSetUtilsMapUtils?等也有提供?null?与空集合合并处理的方法,只是没有那么丰富。

emptyIfNull()

CollectionUtilsIterableUtilsListUtilsSetUtilsMapUtils?等均有提供该方法,如果参数非?null?返回参数本身,否则返回对应类型的空集合。

isEmpty()?与?isNotEmpty()

CollectionUtils(可以用于?Collection?及其子类型如?ListSet) 与?MapUtils?提供了这两个方法。前者判断是否为?null?或为空集合,后者相反——判断既非?null?也非空集合。

CollectionUtilsIterableUtilsListUtilsSetUtilsMapUtils?等提供的大多数其他方法也都会对?null?特殊处理而不是引发 NPE,个别会抛 NPE 的方法文档中也有说明。

ObjectUtils

更通用的情况还可以用 Commons Lang 中的工具类?ObjectUtils

defaultIfNull()

类似上文自行实现的?defaultWith(),不过并没有标可空性注解。?defaultIfNull(obj, defaultVal)?当?obj?非空时返回?obj?否则返回?defaultVal

getIfNull()

相当于默认值采用惰性求值的?defaultIfNull()getIfNull(obj, () -> ……)?当?obj?非空时返回?obj,否则对 lambda 表达式求值并以其返回值作为?getIfNull()?的返回值。

firstNonNull()

对于一系列值,返回第一个非空的。

getFirstNonNull()

可以看作是惰性求值版的?firstNonNull(),对于一系列求值过程返回第一个求值结果非空的结果。 例如?getFirstNonNull(() -> null, () -> "hello", () -> throw new IllegalStateException())?会返回?"hello"?而不会执行后面的求值过程,因此不会抛?IllegalStateException

allNotNullallNullanyNotNullanyNull

对于一系列值,判断是否全都非全都是其中有非其中有空值。

ObjectUtils?中的大多数其他方法也都会对空值特殊处理而不是引发 NPE。除了?StringUtilsObjectUtils?之外,Commons Lang 中的?BooleanUtilsNumberUtils?也都提供了一系列空安全的工具方法。

其他

抛砖引玉,欢迎补充。

小结

Java 语言自身目前没办法彻底解决空值问题,不过有一些方法、工具可以用:

  • NPE 防御:空值比较、不可变集合不支持空值
  • Optional
  • 可空性注解
  • Apache Commons

光说不练无异于纸上谈兵,接下来的练习才是重中之重。

练习

以下练习中未标注解的变量,如果变量名以?x?开头也表示可能为空。

为什么不直接用?nullableXyz?这种更明显的方式?因为现实代码中通常更不明显。

1、 纠错

if (xMethod.equals("POST")) {
   doPost();
}

if (xArg1.equals(xArg2)) {
    System.out.println("arg1 == arg2");
}

2、纠错

var map1 = Map.of("abc", 10, "def", 20, xStr, 30);

var list1 = Arrays.asList(1, 2, -3, 9, null, 15);
var set1 = Set.copyOf(list1);

if (Map.of("hello", 1, "world", 2).containsKey(xStr)) {
    System.out.println("either 'hello' or 'world'");
}

3、加注可空性注解

public static <T, U> U mapSome(T x, Function<T, U> mapper) {
    return x == null ? null: mapper.apply(x);
}

4、用?Optional?重构上题?mapSome()

注:只是练习?Optional?的使用,上题的实现并不需要以?Optional?取代。

5、用?Optional?重构

Integer xInt = Math.random() > 0.8 ? null : Math.random() > 0.5 ? 5 : 12;

// 重构以下代码
var x1 = (xInt == null || xInt % 2 != 0) ? null : xInt / 2;
if (x1 != null) {
    System.out.println(x1);
}

6、用?Optional?重构?getTitledContent()

@NonNull
public static String getUpperTitle(@Nullable Post post) {
    if (post == null || post.getTitle() == null) {
        log.warning("no title")
        return "- UNTITLED -";
    }

    return post.getTitle().toUpperCase();
}

public class Post {
    @Nullable
    public String getTitle();
}

7、用?StringUtils?将?getChoice()?的实现重构成一行代码

String getChoice(String choice, boolean highest) {
    if (choice != null && !choice.isEmpty())
        return choice;

    if (highest)
        return "High";

    return "Low";
}

8、使用 Common Collections 重构?getIdsString()

private static final List<Integer> IMPLICIT_IDS = List.of(101, 111, 191);
public static String getIdsString(@Nullable Collection<@NonNull Integer> ids) {
    if (ids == null) {
        return IMPLICIT_IDS.stream()
                .map(Object::toString)
                .collect(Collectors.joining());
    }

    return Stream.concat(ids.stream(), IMPLICIT_IDS.stream())
            .map(Object::toString)
            .collect(Collectors.joining());
}

9、使用?BooleanUtils?重构

public static String toHex(int n, @Nullable Boolean useUpper) {
    String s = Integer.toString(n, 16);
    return useUpper != null && useUpper ? s.toUpperCase() : s;
}

10、使用?ObjectUtils?重构

public static Instant tomorrowOf(@Nullable Instant x) {
    if (x == null) {
        log.debug("the base Date is null");
        x = Instant.now();
    }
    
    return x.plus(Duration.ofDays(1));
}
灰蓝天际?灰蓝天际

转载请勿修改,并注明作者:灰蓝天际 及许可协议:署名-非商业性使用-禁止演绎。


?

欢迎关注:
GitHub:hltj    微博:灰蓝天际(@hltj)    Twitter:@jywhltj

weibo_qr.png weibo_qr.png 公众号 微博
发表评论
用户名: 匿名