背景 NPE問題,100%的Java程式員都碰到,並且曾經是心中的痛。 1965年英國TonyHoare引入了Null引用,後續的設計語言包括Java都保持了這種設計。 一個例子 業務模型 Person 有車一族, 有Car欄位, Car 車,每個車都有購買保險, 有Insurance欄位; Ins ...
背景
NPE問題,100%的Java程式員都碰到,並且曾經是心中的痛。
1965年英國TonyHoare引入了Null引用,後續的設計語言包括Java都保持了這種設計。
一個例子
業務模型
Person 有車一族, 有Car欄位,
Car 車,每個車都有購買保險, 有Insurance欄位;
Insurance 保險,每個保險都有名字 有name欄位;
需求:獲取某個Person對象的購買保險的名稱;
常規編程
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
檢查式編程
public String getCarInsuranceName_check(Person person) {
if (Objects.nonNull(person)) {
final Car car = person.getCar();
if (Objects.nonNull(car)) {
final Insurance insurance = car.getInsurance();
if (Objects.nonNull(insurance)) {
return insurance.getName();
}
}
}
return "unkown";
}
防禦式編程
public String getCarInsuranceName_protect(Person person) {
if (Objects.isNull(person)) {
return "unkown";
}
final Car car = person.getCar();
if (Objects.isNull(car)) {
return "unkown";
}
final Insurance insurance = car.getInsurance();
if (Objects.isNull(insurance)) {
return "unkown";
}
return insurance.getName();
}
對比一下缺點:
編程方法 | 缺點 |
---|---|
常規編程 | NPE問題 |
檢查式編程 | 1.可讀性不好,多層if嵌套; 2.擴展性不好,需要熟悉全流程,否則不知道應該在哪個if中擴展,極易出錯; |
防禦式編程 | 1. 維護困難,4個不同的退出點,極易出錯,容易遺漏檢查項目 |
NPE的痛點
- java程式中出現最多的Exception;沒有之一;
- 使得代碼量膨脹混亂,對象的空判斷充斥在代碼中,但是卻沒有實際的業務意義;
- 類型系統的一個後門,實際上不屬於任何類型,也可以說是任何類型;
- 本身無意義,標識對缺失值的建模,也破壞了java中弱化指針的理念。
java8中對缺失值的建模對象是Optional,可以基於它解決NPE的痛點,設計更好的API
Optional
領域模型的建模進化
- Person , 含有一個Optional
car欄位,一個人可能有車,也可能沒有車; - Car, 包含有一個Optional
insurance欄位, 一臺車可能買了保險,也可能沒有買保險; - Insurance , 保險公司必定有名字所有,他有一個欄位 name;
構造方法
構造方法 | 說明 | 備註 |
---|---|---|
Optional.empty() | 一定是空的對象 | 跟null有區別,是一個單例對象 |
Optional.of(T t) | 一定是不空的對象 | 如果給了null值會立刻拋出NPE |
Optioanl.ofNullable(T t) | 允許為空的對象放在裡面 | 使用值之前需要做檢查 |
map方法-對象中提取和轉換值
可以把Optional看成一種單元素的Stream, Map,即把其中的元素按照一定規則轉換為其它類型或者進行其它運算後的值,如果沒有元素,則啥也不做。
下麵的代碼是等同的。
public class Test {
public static final String UNKNOWN = "unknown";
/**
* 傳統方法
* @param insurance
* @return
*/
public static String getInsuranceName(Insurance insurance){
if (Objects.isNull(insurance)){
return UNKNOWN;
}
return insurance.getName();
}
/**
* map的方式提取
* @param insurance
* @return
*/
public static String getInsuranceNameOp(Insurance insurance){
return Optional.ofNullable(insurance).map(Insurance::getName).orElse(UNKNOWN);
}
}
flatMap方法 - 轉換為Optional對象輸出;
類似於Stream的flatMap方法,把元素切割或者組合成另外一個流輸出。
public static String getInsuranceName(Person person) {
return Optional.ofNullable(person)
.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName).orElse(UNKNOWN);
}
預設值設置方法(5種適合不同的場景)
預設值方法 | 說明 | 場景 |
---|---|---|
or(Supplier |
為空則延遲構造一個Optional對象 | 可以採用延遲的方式,對接某些代碼來產生預設值 |
orElse(T t) | 為空則採用預設值 | 直接,簡單 |
orElseGet(Supplier |
為空則通過函數返回 | 延遲返回,可以對接某些代碼邏輯 |
orElseThrow() | 為空則跑出異常 | 預設是NoSuchElementException |
orElseThrow(Supplier |
為空則跑出自定義異常 | 異常類型可以自定義 |
使用值(獲取或者消費)
主要分兩種場景,直接獲取值, 採用get()方法;
裡面有值,則消費, ifPresent(Consumer
c)
消費或者獲取方法 | 說明 | 場景 |
---|---|---|
get() | 獲取Optional中的值,如果沒有值,會拋出異常 | 確認裡面有值才會調用該防範 |
ifPresent(Consumer |
有值則執行自定義代碼段,消費該值 | 流式編程,有值繼續處理邏輯 |
ifPresentOrElse(Consumer |
如果有值,則消費,沒有值,進行另外的處理 | 有值或者沒有值都進行處理java9才有 |
多個Optional進行運算
通過使用flatMap,map可以做到,方法里執行的已經做好了對empty的情況進行處理。
實例如下:
public static String getCheapestPrizeIsuranceNameOp(Person person, Car car) {
return Optional.ofNullable(person)
.flatMap(p -> Optional.ofNullable(car).map(c -> getCheapest(p, c)))
.orElse(UNKNOWN);
}
public static String getCheapestPrizeIsuranceName(Person person, Car car) {
if (Objects.nonNull(person) && Objects.nonNull(car)) {
return getCheapest(person, car);
}
return UNKNOWN;
}
/**
* 模擬得到最便宜的保險
*
* @param person 人
* @param car 車
* @return 最便宜的車險名稱
*/
private static String getCheapest(Person person, Car car) {
return "pinan";
}
filter方法 (過濾)
因為Optional中只有一個值,所以這裡的filter實際上是判斷單個值是不是。
對比代碼:
public static Insurance getPinanInsurance(Person person){
Optional<Insurance> insuranceOptional = Optional.ofNullable(person).map(Person::getCar).map(Car::getInsurance);
if (insuranceOptional.isPresent() && Objects.equals("pinan", insuranceOptional.get().getName())){
return insuranceOptional.get();
}
return null;
}
public static Insurance getPinanInsurance_filter(Person person){
return Optional.ofNullable(person)
.map(Person::getCar)
.map(Car::getInsurance)
.filter(item->Objects.equals(item.getName(),"pinan" ))
.orElse(null);
}
empty方法 (構造一個空的Optional對象)
Optional改造歷史代碼
封裝可能潛在為null的對象
public Object getFromMap(String key){
Map<String,Object> map = new HashMap<>(4);
map.put("a", "aaa");
map.put("b", "bbb");
map.put("c", "ccc");
Object value = map.get(key);
if (Objects.isNull(value)){
throw new NoSuchElementException("不存在key");
}
return value;
}
public Object getFromMapOp(String key){
Map<String,Object> map = new HashMap<>(4);
map.put("a", "aaa");
map.put("b", "bbb");
map.put("c", "ccc");
Object value = map.get(key);
return Optional.ofNullable(value).orElseThrow(()->new NoSuchElementException("不存在key"));
}
發生異常的建模可以替換為Optional對象
這種是建模思想的轉變,不一定適用每個人;
/**
* 如果字元串不是數字,會拋出異常
* @param a 字元串
* @return 數字
*/
public Integer string2Int(String a){
return Integer.parseInt(a);
}
/**
* Optional.empty對應異常的情況,後續比較好處理;
* @param a 字元串
* @return 可能轉換失敗的整數,延遲到使用方去處理
*/
public Optional<Integer> string2Int_op(String a){
try{
return Optional.of(Integer.parseInt(a));
}catch (Exception ex){
return Optional.empty();
}
}
儘量不使用封裝的Optional
封裝的OptionalInt, OptionalLong ,因為Optional裡面只有一個元素,使用封裝類沒有性能優勢,而且缺失了重要的flatMap, map,filter方法;
總的來說,Optional的使用,簡化了代碼,使得代碼可讀性和可維護性更好。
最後來個例子:
public Integer getFromProperties(Properties properties, String key) {
String value = properties.getProperty(key);
if (Objects.nonNull(value)) {
try {
Integer integer = Integer.parseInt(value);
if (integer > 0) {
return integer;
}
} catch (Exception ex) {
//無需處理異常
return 0;
}
}
return 0;
}
public Integer getFromProperties_op(Properties properties, String key) {
return Optional.ofNullable(properties.getProperty(key))
.map(item -> {
try {
return Integer.parseInt(item);
} catch (Exception ex) {
return 0;
}
})
.orElse(0);
}
Optional源碼閱讀
一個容器對象,可能有也可能沒有非空值,如果值存在,isPresent()返回true,如果沒有值,則對象被當成空,isPresent()返回false;
更多的方法依賴於容器中是否含有值,比如orElse(返回一個預設值當沒有值)
ifPresent(Consumer c) 是當值存在的時候,執行一個動作;
這是一個基於值的類,使用標識敏感的操作,包含 比較引用的 == , hashcode , synchronization 針對一個Optional對象,可能有無法預料的結果,然後應該避免這類操作。
編寫API的註意點:
Optional最初被用來設計為方法的返回值,當明確需要代表沒有值的情況。
返回null,可能出錯;而返回Optional對象不是一個null對象,它總是指向一個Optional對象實例。
/**
* A container object which may or may not contain a non-{@code null} value.
* If a value is present, {@code isPresent()} returns {@code true}. If no
* value is present, the object is considered <i>empty</i> and
* {@code isPresent()} returns {@code false}.
*
* <p>Additional methods that depend on the presence or absence of a contained
* value are provided, such as {@link #orElse(Object) orElse()}
* (returns a default value if no value is present) and
* {@link #ifPresent(Consumer) ifPresent()} (performs an
* action if a value is present).
*
* <p>This is a <a href="../lang/doc-files/ValueBased.html">value-based</a>
* class; use of identity-sensitive operations (including reference equality
* ({@code ==}), identity hash code, or synchronization) on instances of
* {@code Optional} may have unpredictable results and should be avoided.
*
* @apiNote
* {@code Optional} is primarily intended for use as a method return type where
* there is a clear need to represent "no result," and where using {@code null}
* is likely to cause errors. A variable whose type is {@code Optional} should
* never itself be {@code null}; it should always point to an {@code Optional}
* instance.
*
* @param <T> the type of value
* @since 1.8
*/
其它的代碼比較簡單,模型就是裡面含有一個T類型的值,empty()是一個特殊的Optional對象,裡面的值是null;
public final class Optional<T> {
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();
/**
* If non-null, the value; if null, indicates no value is present
*/
private final T value;
/**
* Constructs an empty instance.
*
* @implNote Generally only one empty instance, {@link Optional#EMPTY},
* should exist per VM.
*/
private Optional() {
this.value = null;
}
小結
- Optional表示一個可能缺失的對象,API可以依據這個進行建模,但是要註意序列化的問題;可以避免空指針的問題,並且提升代碼的可讀性和可維護性。
- Optional的構造方法有3個,of,ofNullable,empty;
- map,flatmap , filter 可以快速的轉換和過濾值;
- 值缺失的處理方法有3個,orElse, orElseGet, orElseThrow;
原創不易,轉載請註明出處。