本文也发在我的个人博客上: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 可以通过编码习惯来防御——某些静态分析工具或许也能检测到一些问题,但很难完美覆盖; 没办法,只能通过编码规范、程序员的自律来解决了。 其中比较常见的两个场景是空值比较以及使用不可变集合。
对实际值为?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));
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()
。 其实对于采用可选值类型的其他语言来说可能都有类似问题,但是?Haskell
、Scala
?有推导式,Haskell
、Scala
、OCaml
、F#
?等语言还支持自定义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?有多个库提供了简化空值使用的工具。例如实现类似??:
/??
?的功能,使用?Optional
?需要这样写:
var nonNullVal = Optional.ofNullable(obj).orElse(nonNullDefault);
而使用 Apache Commons 只需调用一个类似上文实现的静态方法就可以了。
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(个别的会抛其他异常)。
与?StringUtils
?类似,Commons Collections?中的?CollectionUtils
、IterableUtils
、ListUtils
、SetUtils
、MapUtils
?等也有提供?null
?与空集合合并处理的方法,只是没有那么丰富。
emptyIfNull()
CollectionUtils
、IterableUtils
、ListUtils
、SetUtils
、MapUtils
?等均有提供该方法,如果参数非?null
?返回参数本身,否则返回对应类型的空集合。
isEmpty()
?与?isNotEmpty()
CollectionUtils
(可以用于?Collection
?及其子类型如?List
、Set
) 与?MapUtils
?提供了这两个方法。前者判断是否为?null
?或为空集合,后者相反——判断既非?null
?也非空集合。
CollectionUtils
、IterableUtils
、ListUtils
、SetUtils
、MapUtils
?等提供的大多数其他方法也都会对?null
?特殊处理而不是引发 NPE,个别会抛 NPE 的方法文档中也有说明。
更通用的情况还可以用 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
。
allNotNull
、allNull
、anyNotNull
、anyNull
对于一系列值,判断是否全都非、全都是、其中有非、其中有空值。
ObjectUtils
?中的大多数其他方法也都会对空值特殊处理而不是引发 NPE。除了?StringUtils
、ObjectUtils
?之外,Commons Lang 中的?BooleanUtils
、NumberUtils
?也都提供了一系列空安全的工具方法。
抛砖引玉,欢迎补充。
Java 语言自身目前没办法彻底解决空值问题,不过有一些方法、工具可以用:
Optional
光说不练无异于纸上谈兵,接下来的练习才是重中之重。
以下练习中未标注解的变量,如果变量名以?x
?开头也表示可能为空。
为什么不直接用?
nullableXyz
?这种更明显的方式?因为现实代码中通常更不明显。
if (xMethod.equals("POST")) {
doPost();
}
if (xArg1.equals(xArg2)) {
System.out.println("arg1 == arg2");
}
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'");
}
public static <T, U> U mapSome(T x, Function<T, U> mapper) {
return x == null ? null: mapper.apply(x);
}
Optional
?重构上题?mapSome()
注:只是练习?
Optional
?的使用,上题的实现并不需要以?Optional
?取代。
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);
}
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();
}
StringUtils
?将?getChoice()
?的实现重构成一行代码String getChoice(String choice, boolean highest) {
if (choice != null && !choice.isEmpty())
return choice;
if (highest)
return "High";
return "Low";
}
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());
}
BooleanUtils
?重构public static String toHex(int n, @Nullable Boolean useUpper) {
String s = Integer.toString(n, 16);
return useUpper != null && useUpper ? s.toUpperCase() : s;
}
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