【灰藍 Java 訓練】如何處理空值
本文原發於我的個人部落格:https://hltj.me/java/2021/01/09/java-exercise-nulls.html。本副本只用於圖靈社群,禁止第三方轉載。
這個系列以練習為主,可能不會有多少講述(當然本篇例外),可以作為初學者的自學驗收之用。
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));
【注】:這些 Java 9-11 新引入的集合方法,可通過《Java實戰(第2版)》第8章學習。
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
的其他方法在特定場景也很實用,由於方法不多,大家可以直接讀其文件,亦可通過《Java 8函數語言程式設計》第4章第10節、《Java實戰(第2版)》第11章學習,在此不再贅述。
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));
}
相關文章
- Java中如何處理空指標異常Java指標
- 藍橋杯 (java)演算法訓練 數對Java演算法
- 藍橋杯訓練2
- 藍橋杯 演算法訓練 素因子去重(Java)演算法Java
- 模型訓練:資料預處理和預載入模型
- [Java] 藍橋杯ALGO-117 演算法訓練 友好數JavaGo演算法
- 藍橋杯—演算法訓練演算法
- 藍橋杯--演算法訓練演算法
- 一窺Habana的推理和訓練神經處理器
- Java培訓簡述如何處理沒有被捕獲的異常Java
- java小白訓練營Java
- 如何處理JavaScript 中的貨幣值?JavaScript
- 自然語言處理中的語言模型預訓練方法自然語言處理模型
- 視覺化影像處理 | 視覺化訓練器 | 影像分類視覺化
- java工廠模式訓練Java模式
- 2024SMU藍橋訓練2補題
- 藍橋杯:入門訓練 Fibonacci數列
- 大語言模型訓練資料常見的4種處理方法模型
- Python 影像處理 OpenCV (6):影像的閾值處理PythonOpenCV
- java大資料處理:如何使用Java技術實現高效的大資料處理Java大資料
- 運用預訓練 Keras 模型來處理影像分類請求,學習如何使用從 Keras 建立 SavedModelKeras模型
- [藍橋杯][演算法訓練VIP]方格取數演算法
- 藍橋杯訓練--母牛的故事(很清晰的思路)
- Java處理emojiJava
- Java每日基礎恢復訓練Java
- 【scikit-learn基礎】--『預處理』之 缺失值處理
- Java如何使用實時流式計算處理?Java
- 翻譯 | Java流中如何處理異常Java
- kotlin小白日記2「工具類的封裝,Anko簡化吐司,空值處理」Kotlin封裝
- Java判斷欄位是否為空,為空賦值 ?Java賦值
- 同花色同值牌處理
- JSP 異常處理如何處理?JS
- 大模型如何提升訓練效率大模型
- 長沙Java培訓:JAVA練手專案分享Java
- 藍橋杯 演算法訓練 操作格子 (線段樹)演算法
- 從Word Embedding到Bert模型——自然語言處理預訓練技術發展史模型自然語言處理
- sysaux 表空間爆滿處理方法UX
- Laravel 處理 MySQL geometry 空間型別LaravelMySql型別