这个系列以练习为主,可能不会有多少讲述(当然本篇例外),可以作为初学者的自学验收之用。
Java 中有非受限的空值,并且不知哪时会引发 NPE(即 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()
。 其实对于采用可选值类型的其他语言来说可能都有类似问题,但是 Haskell
、Scala
有推导式,Haskell
、Scala
、OCaml
、F#
等语言还支持自定义操作符,从而简化可选值的用法。 不幸的是 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 中的一个工具类,其中包含一系列与字符串空值相关的方法。
前者判断一个字符序列是否为 null
或空序列,后者与之相反——判断一个字符序列既非 null
也非空序列。 这两个方法非常实用,现实业务中对字符串为 null
或空串时走同样处理分支的场景很常见。
defaultString(str, defaultStr)
如果 str
非 null
返回 str
,否则返回 defaultStr
。 还有一个单参重载版本:defaultString(str)
如果非 null
返回 str
,否则返回空串。
defaultIfEmpty(str, defaultStr)
如果 str
既非 null
也非空串返回 str
,否则返回 defaultStr
。
相当于默认值采用惰性求值的 defaultIfEmpty()
,getIfEmpty(str, () -> ……)
当 str
既非 null
也非空串时返回 str
,否则对 lambda 表达式求值并以其返回值作为 getIfEmpty()
的返回值。
对于一系列值,返回第一个既非 null
也非空串的。
isAllEmpty()
、isAnyEmpty()
、isNoneEmpty()
对于一系列值,判断是否全都是、其中有、全都不是 null
或空串。
除了这些明显与空值相关的方法外,StringUtils
的其他方法也都会对 null
特殊处理而不是引发 NPE(个别的会抛其他异常)。
Commons Collections
与 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 的方法文档中也有说明。
ObjectUtils
更通用的情况还可以用 Commons Lang 中的工具类 ObjectUtils
。
类似上文自行实现的 defaultWith()
,不过并没有标可空性注解。 defaultIfNull(obj, defaultVal)
当 obj
非空时返回 obj
否则返回 defaultVal
。
相当于默认值采用惰性求值的 defaultIfNull()
,getIfNull(obj, () -> ……)
当 obj
非空时返回 obj
,否则对 lambda 表达式求值并以其返回值作为 getIfNull()
的返回值。
对于一系列值,返回第一个非空的。
可以看作是惰性求值版的 firstNonNull()
,对于一系列求值过程返回第一个求值结果非空的结果。 例如 getFirstNonNull(() -> null, () -> "hello", () -> throw new IllegalStateException())
会返回 "hello"
而不会执行后面的求值过程,因此不会抛 IllegalStateException
。
allNotNull
、allNull
、anyNotNull
、anyNull
对于一系列值,判断是否全都非、全都是、其中有非、其中有空值。
ObjectUtils
中的大多数其他方法也都会对空值特殊处理而不是引发 NPE。除了 StringUtils
、ObjectUtils
之外,Commons Lang 中的 BooleanUtils
、NumberUtils
也都提供了一系列空安全的工具方法。
其他
抛砖引玉,欢迎补充。
小结
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));
}
相关推荐
欧美简洁灰蓝PPT
《ShopEx商店 灰蓝e城:网页模板解析与应用》 ShopEx商店是一款知名的电子商务平台,而“灰蓝e城”则是该平台上的一款特色模版,它以灰蓝色调为主,营造出专业且舒适的在线购物环境。本文将深入探讨这款模版的设计...
灰蓝格调模板》是一款专为Discuz! 论坛系统设计的界面主题,它以其简洁而优雅的灰蓝色调,为用户提供了舒适且专业的浏览体验。这款模板旨在提升论坛的整体视觉效果,吸引更多的访问者,并促进用户在论坛中的互动。 ...
4. **图片文件**(.jpg、.png等):包含主题所需的图标、背景图片和其他视觉元素,符合灰蓝主题的设计。 5. **语言文件**(如languages文件夹):支持多语言,使主题能够适应不同地区的用户。 6. **函数文件**...
【灰蓝移动数字网络网站模板】是一个专门设计用于创建移动友好、数字网络相关的网页的模板。这个模板采用了灰蓝色调,旨在提供一个专业且现代的视觉体验,同时确保在不同设备上的良好显示和操作性。它可能是由网页...
借助【灰蓝-PPT模板.pptx】,求职者可以轻松打造一个兼具艺术美感与专业度的简历,不仅能为自己的职业形象加分,还能有效吸引招聘者的目光。 该模板采用水彩艺术风格,色彩以灰蓝为主调,呈现出一种淡雅而又不失...
【标题】"扁平化灰蓝白商业通用ppt图表模板.rar"所代表的知识点主要集中在设计风格、商业演示以及PPT图表的应用上。扁平化设计是近年来在UI设计领域非常流行的一种趋势,它强调简洁、清晰,去除冗余的装饰效果,使...
PPT模板通常包含多种功能,如文字处理、大纲构建、绘图、图表制作以及演示管理工具。这些工具都是为了便于用户使用和学习,使设计师能够清晰地表达设计理念和工程细节。在2020年1月1日创建的这个模板中,"Presenter ...
34.灰蓝色时间轴.docx
为满足广大专业用户对于高质量演示文稿的需求,"蓝色弧型 PPT模板(灰蓝与深蓝相间)"应运而生,它以其独特的设计理念和现代感十足的色彩搭配,为使用者带来全新的展示体验。 这款PPT模板采用了以灰蓝色和深蓝色为主...
开发者可以通过PHP处理表单数据、生成动态网页内容、与数据库交互等。例如,在WordPress中,PHP脚本可以用来调用数据库中的文章、评论,实现用户登录、权限管理等功能。 【MySQL数据库】 WordPress与MySQL数据库...
根据部分提供的内容示例,可以看出模板采用了统一的风格和格式,包括目录结构的设定以及各个部分标题的标准化处理。例如,在“添加标题文本”部分,使用了固定的标题格式(如“C l i c k a d d y o u r t i t l e t ...
包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、python、web、C#、EDA、proteus、RTOS等项目的源码。【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。【适用人群】:适用...
一个优秀的答辩PPT模板不仅需要视觉上的吸引力,如【灰蓝】色调的搭配,更需要清晰的逻辑结构和内容的准确性。 1. **绪论** - **现状分析**:在这一部分,你需要概述当前研究领域的状况,比如在体育运动科学中,...
简约灰蓝色项目分析报告PPT模板.ppt
用户通过下载获取这些图标,可能需要了解如何保存网络链接,使用下载工具,以及处理下载后的文件格式等问题。此外,网络安全和版权问题也是用户在下载资源时需要注意的。 5. **文件管理**:文件名为"201306121540...
jQuery是一个轻量级、高性能的JavaScript库,它简化了HTML文档遍历、事件处理、动画和Ajax交互,使得开发者能够更方便地实现动态网页效果。 在这个项目中,开发者巧妙地运用了jQuery的特性来实现菜单的动画效果。当...
在WordPress中,主题是控制网站外观和用户体验的关键组件,而PHP作为其后端编程语言,负责处理动态内容和功能。 【描述】提到的“两栏经典灰蓝色博客wordpress主题”意味着该主题具有两个主要的页面区域:主栏和...
通过分析源码,学生可以学习到JavaScript事件处理、DOM操作等动态网页开发知识。 二、毕业设计与论文 1. 设计原则:模板的设计遵循了一定的美学原则和用户体验原则,如色彩搭配、布局平衡、信息层次等,这些都是...