為什么需要CallSafe?
??我們平時(shí)在使用Java開發(fā)的時(shí)候,遇到過最多的異常就是 NullPointException (NPE),想處理這個(gè)這個(gè)異常很簡(jiǎn)單,只需要在變量、方法返回值等使用前對(duì)其進(jìn)行判空處理之后再使用即可,但是我們又不想書寫再所有地方都進(jìn)行先判空再使用的邏輯代碼,因?yàn)檫@樣不僅讓代碼看起來亂糟糟還影響閱讀,所以我們迫切的希望有一種”系統(tǒng)統(tǒng)一處理NPE“的功能,這些邏輯代碼不侵入我們自己的項(xiàng)目代碼。
帶著這個(gè)想法我們繼續(xù)探討:
①系統(tǒng)幫我們處理NPE;
②這些代碼不要出現(xiàn)在項(xiàng)目代碼中
仔細(xì)想來要實(shí)現(xiàn)這個(gè)功能,無非只有兩個(gè)階段時(shí)能做到:
1.編譯階段,編譯階段將NPE處理的代碼插入到業(yè)務(wù)代碼中,最終生成帶有NPE處理的.class 文件;
2.運(yùn)行階段,我們希望在JVM在執(zhí)行代碼時(shí),遇到NPE不是拋出的NPE異常而中止,而是按照我們預(yù)定義的邏輯繼續(xù)執(zhí)行。
兩種處理方式各有優(yōu)劣,不過目前看來第一種方式明顯好于第二種,因?yàn)榈谝环N方式雖然可能會(huì)導(dǎo)致JVM多執(zhí)行一些額外的邏輯,但是生成的 .class 在任何的標(biāo)準(zhǔn)JVM上都能正確的執(zhí)行,第二種方式再執(zhí)行上可能會(huì)更高效點(diǎn),但是需要干預(yù) JVM 的執(zhí)行,這在某些情況下是不可行的(比如Android程序,你無法干預(yù)所有客戶端的執(zhí)行)。
Kotlin的CallSafe處理
??扯的有點(diǎn)遠(yuǎn),現(xiàn)在正式開始說這個(gè)Kotlin和Groovy處理NPE的方式,在kotlin中,如果我們不想自己做NPE檢查,又不想程序因拋出NPE而終止運(yùn)行,我們可以使用 ‘可選型’類型來處理有可能發(fā)生NPE的情況,舉例:
private fun foo(str:String?):String?{
return str?.toLowerCase()?.substring(1)
}
??這段代碼在方法調(diào)用時(shí)候使用了‘?.’ 來替代我們之前使用的 ‘.’,它的作用是:如果方法的被調(diào)用者為null了,則終止操作,想必這個(gè)很簡(jiǎn)單,大家都知道作用,但是它是如何實(shí)現(xiàn)的呢?我們使用'javap -p -v' 命令來查看這段代碼最終生成的class中是怎么處理的:
Constant pool:
...省略部分常量...
#14 = Utf8 kotlin/TypeCastException
#15 = Class #14 // kotlin/TypeCastException
#16 = Utf8 null cannot be cast to non-null type java.lang.String
#17 = String #16 // null cannot be cast to non-null type java.lang.String
#18 = Utf8 <init>
#19 = Utf8 (Ljava/lang/String;)V
#20 = NameAndType #18:#19 // "<init>":(Ljava/lang/String;)V
#21 = Methodref #15.#20 // kotlin/TypeCastException."<init>":(Ljava/lang/String;)V
#22 = Utf8 java/lang/String
#23 = Class #22 // java/lang/String
#24 = Utf8 toLowerCase
#25 = Utf8 ()Ljava/lang/String;
#26 = NameAndType #24:#25 // toLowerCase:()Ljava/lang/String;
#27 = Methodref #23.#26 // java/lang/String.toLowerCase:()Ljava/lang/String;
#28 = Utf8 (this as java.lang.String).toLowerCase()
#29 = String #28 // (this as java.lang.String).toLowerCase()
#30 = Utf8 kotlin/jvm/internal/Intrinsics
#31 = Class #30 // kotlin/jvm/internal/Intrinsics
#32 = Utf8 checkExpressionValueIsNotNull
#33 = Utf8 (Ljava/lang/Object;Ljava/lang/String;)V
#34 = NameAndType #32:#33 // checkExpressionValueIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
#35 = Methodref #31.#34 // kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
#36 = Utf8 substring
#37 = Utf8 (I)Ljava/lang/String;
#38 = NameAndType #36:#37 // substring:(I)Ljava/lang/String;
#39 = Methodref #23.#38 // java/lang/String.substring:(I)Ljava/lang/String;
#40 = Utf8 (this as java.lang.String).substring(startIndex)
#41 = String #40 // (this as java.lang.String).substring(startIndex)
...省略部分常量...
private final java.lang.String foo(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=4, locals=4, args_size=2
0: aload_1
1: dup
2: ifnull 65
5: astore_2
6: aload_2
7: dup
8: ifnonnull 21
11: new #15 // class kotlin/TypeCastException
14: dup
15: ldc #17 // String null cannot be cast to non-null type java.lang.String
17: invokespecial #21 // Method kotlin/TypeCastException."<init>":(Ljava/lang/String;)V
20: athrow
21: invokevirtual #27 // Method java/lang/String.toLowerCase:()Ljava/lang/String;
24: dup
25: ldc #29 // String (this as java.lang.String).toLowerCase()
27: invokestatic #35 // Method kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
30: dup
31: ifnull 65
34: astore_2
35: iconst_1
36: istore_3
37: aload_2
38: dup
39: ifnonnull 52
42: new #15 // class kotlin/TypeCastException
45: dup
46: ldc #17 // String null cannot be cast to non-null type java.lang.String
48: invokespecial #21 // Method kotlin/TypeCastException."<init>":(Ljava/lang/String;)V
51: athrow
52: iload_3
53: invokevirtual #39 // Method java/lang/String.substring:(I)Ljava/lang/String;
56: dup
57: ldc #41 // String (this as java.lang.String).substring(startIndex)
59: invokestatic #35 // Method kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
62: goto 67
65: pop
66: aconst_null
67: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 68 0 this Ldemo/KotlinMain;
0 68 1 str Ljava/lang/String;
LineNumberTable:
line 15: 0
line 15: 35
下面來逐行解釋每個(gè)指令的意思:
0:aload_1 //將 LocalVariableTable 中的第2個(gè)引用類型變量(也就是str)壓入棧頂
1: dup //復(fù)制棧頂數(shù)據(jù)并將復(fù)制值壓入棧頂
2: ifnull 65 //如果(棧頂數(shù)據(jù))為null,則跳轉(zhuǎn)至 65
5: astore_2 //將棧頂引用型數(shù)值存入第三個(gè)本地變量
6: aload_2 //將第三個(gè)引用類型本地變量推送至棧頂
7: dup //復(fù)制棧頂數(shù)值并將復(fù)制值壓入棧頂
8: ifnonnull 21 //如果不為空則跳轉(zhuǎn)至21(為空則繼續(xù)向下執(zhí)行)
11: new #15 //創(chuàng)建 TypeCastException的實(shí)例,并將其引用壓入棧頂
14: dup //復(fù)制棧頂數(shù)值并將復(fù)制值壓入棧頂
15: ldc #17 //將 ”null cannot be cast to non-null type java.lang.String“壓入棧頂
17: invokespecial #21 //調(diào)用 TypeCastException 的 <init>方法 (初始化)
20: athrow //將棧頂?shù)漠惓伋? 21: invokevirtual #27 //調(diào)用實(shí)例方法 String#toLowerCase()
24: dup //復(fù)制棧頂數(shù)值并將復(fù)制值壓入棧頂(上一步方法調(diào)用后的返回值)
25: ldc #29 //將”(this as java.lang.String).toLowerCase()“壓入棧頂
27: invokestatic #35 //調(diào)用 kotlin.jvm.internal.Intrinsics.checkExpressionValueIsNotNull(Object,String)方法 (參數(shù)的匹配:以當(dāng)前棧頂?shù)脑刈鳛榉椒ǖ淖詈笠粋€(gè)參數(shù),距棧頂深度為2的元素作為倒數(shù)第二個(gè)參數(shù)... 以此類推)
30: dup //復(fù)制棧頂數(shù)值并將復(fù)制值壓入棧頂(也就是上一步方法調(diào)用返回值)
31: ifnull 65 //如果(棧頂數(shù)據(jù))為null,則跳轉(zhuǎn)至 65
34: astore_2 //將棧頂引用型數(shù)值存入第三個(gè)本地變量
35: iconst_1 //將int型1推送至棧頂
36: istore_3 //將棧頂int型數(shù)值存入第四個(gè)本地變量(也就是1)
37: aload_2 //將第三個(gè)引用類型本地變量推送至棧頂(也就是 .toLowerCase()的返回值)
38: dup //復(fù)制棧頂數(shù)值并將復(fù)制值壓入棧頂
39: ifnonnull 52 //不為null時(shí)跳轉(zhuǎn)至 52 (實(shí)際上在判斷.toLowerCase()的返回值是否為null)
42: new #15 //創(chuàng)建 TypeCastException的實(shí)例,并將其引用壓入棧頂
45: dup //復(fù)制棧頂數(shù)據(jù)并將復(fù)制值壓入棧頂
46: ldc #17 //”null cannot be cast to non-null type java.lang.String“壓入棧頂
48: invokespecial #21 //調(diào)用 TypeCastException 的 <init>方法 (初始化)
51: athrow //將棧頂?shù)漠惓伋? 52: iload_3 //將第四個(gè)int型本地變量推送至棧頂(也就是1)
53: invokevirtual #39 //調(diào)用 .substring(1)
56: dup //復(fù)制棧頂數(shù)值并將復(fù)制值壓入棧頂
57: ldc #41 //將”(this as java.lang.String).substring(startIndex)“壓入棧頂
59: invokestatic #35 //調(diào)用kotlin.jvm.internal.Intrinsics.checkExpressionValueIsNotNull()
62: goto 67 //跳轉(zhuǎn)至 67
65: pop //將棧頂數(shù)值彈出(數(shù)值不能是long或double類型的)
66: aconst_null //將null推送至棧頂
67: areturn //從當(dāng)前方法返回對(duì)象引用
通過對(duì)上面 .class中foo方法的分析,可以將以上代碼還原為Java為:
private final String foo2(String str) {
String var10000;
if (str != null) {
if (str == null) {
throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
}
var10000 = str.toLowerCase();
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toLowerCase()");
if (var10000 != null) {
String var2 = var10000;
byte var3 = 1;
if (var2 == null) {
throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
}
var10000 = var2.substring(var3);
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).substring(startIndex)");
return var10000;
}
}
var10000 = null;
return var10000;
}
附:所用到的kotlin.jvm.internal中的其他方法
public static void checkExpressionValueIsNotNull(Object value, String expression) {
if (value == null) {
throw sanitizeStackTrace(new IllegalStateException(expression + " must not be null"));
}
}
private static <T extends Throwable> T sanitizeStackTrace(T throwable) {
return sanitizeStackTrace(throwable, Intrinsics.class.getName());
}
static <T extends Throwable> T sanitizeStackTrace(T throwable, String classNameToDrop) {
StackTraceElement[] stackTrace = throwable.getStackTrace();
int size = stackTrace.length;
int lastIntrinsic = -1;
for (int i = 0; i < size; i++) {
if (classNameToDrop.equals(stackTrace[i].getClassName())) {
lastIntrinsic = i;
}
}
List<StackTraceElement> list = Arrays.asList(stackTrace).subList(lastIntrinsic + 1, size);
throwable.setStackTrace(list.toArray(new StackTraceElement[list.size()]));
return throwable;
}
將這個(gè)反編譯的.java源碼和我們之前寫的 .java源碼對(duì)比,不難發(fā)現(xiàn):
①:訪問修飾符增加了 final;
②:對(duì)"可選型"參數(shù)在使用前進(jìn)行了判空;
③:對(duì)”不可為null“的情況使用了‘Intrinsics.checkExpressionValueIsNotNull()’去檢查,并在拋出異常時(shí)消除掉該條語句的堆棧信息。
??看到這里我們基本上就可以得出結(jié)論了,Kotlin的safeCall并不是使用了”可選型“而是使用”判空“來保證不會(huì)觸發(fā)NPE而終止程序。
??但是,如果你仔細(xì)看了上面的代碼,不難發(fā)現(xiàn),在調(diào)用 .substring()前的判空操作”滯后“了,因?yàn)樵谡{(diào)用此方法之前先調(diào)用了 Intrinsics.checkExpressionValueIsNotNull(),這個(gè)方法最終仍會(huì)拋出異常。猜想: ‘.toLowerCase()’這個(gè)方法的定義是在 ‘java.lang.String'中,它的返回值是”String“ 而不是”String?“(事實(shí)上Java中也沒有String?這樣的方式),驗(yàn)證這個(gè)猜想很簡(jiǎn)單,我們只需要使用Kotlin的類方法擴(kuò)展功能,為String擴(kuò)寫一個(gè)方法,該方法返回 "String?",然后在查看編譯出的”.class“中是否對(duì)我們新定義的方法在調(diào)用前有判空操作即可:
新增擴(kuò)展方法:
public inline fun String.tonull(flag: Boolean): String? = if (flag) {null} else{"abc"}
//這里如果直接返回 null ,在掉用這個(gè)方法之后再繼續(xù)使用返回值調(diào)用其他的方法會(huì)在編譯階段被優(yōu)化掉
改變foo()為:
private fun foo(str:String?,flag:Boolean):String?{
return str?.toLowerCase()?.tonull(flag)?.substring(1)?.tonull(false)
}
將以上代碼編譯為".class"再反編譯回Java:
private final String foo2(String str, boolean flag) {
String var10000;
if (str != null) {
if (str == null) {
throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
}
var10000 = str.toLowerCase();
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toLowerCase()");
if (var10000 != null) {
var10000 = flag ? null : "abc";
if ((flag ? null : "abc") != null) {
String var3 = var10000;
byte var4 = 1;
if (var3 == null) {
throw new TypeCastException("null cannot be cast to non-null type java.lang.String");
}
var10000 = var3.substring(var4);
Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).substring(startIndex)");
if (var10000 != null) {
boolean flag$iv = false;
var10000 = "abc";
return var10000;
}
}
}
}
var10000 = null;
return var10000;
}
??可以看到確實(shí)是這樣,對(duì)于返回值為 ”String“類型的會(huì)強(qiáng)制檢查其值是不是為 null ,如果為null則直接拋出異常,終止執(zhí)行。對(duì)于返回值為 ”String?“的類型則不會(huì)檢查值,但在使用該值之前會(huì)做判空操作。
Groovy的CallSafe處理
??那在Groovy中也是通過 非空判斷來避免NPE的嗎?我們?nèi)匀灰陨厦?foo()為例,將其改寫為Groovy代碼:
private String foo(String str) {
return str?.toLowerCase().substring(1)?.toUpperCase()
}
??還是先將其編譯為 ".class"文件,然后在將其反編譯回Java,(不反編譯回Java也行,不過直接使用javap命令查看到的代碼不是很直觀):
private String foo(String str) {
CallSite[] var2 = $getCallSiteArray();
return (String)ShortTypeHandling.castToString(var2[10].callSafe(var2[11].call(var2[12].callSafe(str), 1)));
}
??對(duì)照我們自己編寫foo()源碼后發(fā)現(xiàn),我們使用的 ”?."變成了”.callSafe“ ,而 ”.“變成了 ”call“,具體是怎么轉(zhuǎn)換的還需要進(jìn)一步分析:
首先,這個(gè)我們自己并沒有定義’$getCallSiteArray()‘這個(gè)方法,而且直接使用 IDEA 自帶的反編譯工具竟然開不到這個(gè)方法,那就只能去”.class“中找這個(gè)方法的定義了:
private static org.codehaus.groovy.runtime.callsite.CallSite[] $getCallSiteArray();
descriptor: ()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=3, locals=1, args_size=0
0: getstatic #233 // Field $callSiteArray:Ljava/lang/ref/SoftReference;
3: ifnull 20
6: getstatic #233 // Field $callSiteArray:Ljava/lang/ref/SoftReference;
9: invokevirtual #239 // Method java/lang/ref/SoftReference.get:()Ljava/lang/Object;
12: checkcast #228 // class org/codehaus/groovy/runtime/callsite/CallSiteArray
15: dup
16: astore_0
17: ifnonnull 35
20: invokestatic #241 // Method $createCallSiteArray:()Lorg/codehaus/groovy/runtime/callsite/CallSiteArray;
23: astore_0
24: new #235 // class java/lang/ref/SoftReference
27: dup
28: aload_0
29: invokespecial #244 // Method java/lang/ref/SoftReference."<init>":(Ljava/lang/Object;)V
32: putstatic #233 // Field $callSiteArray:Ljava/lang/ref/SoftReference;
35: aload_0
36: getfield #247 // Field org/codehaus/groovy/runtime/callsite/CallSiteArray.array:[Lorg/codehaus/groovy/runtime/callsite/CallSite;
39: areturn
StackMapTable: number_of_entries = 2
frame_type = 20 /* same */
frame_type = 252 /* append */
offset_delta = 14
locals = [ class org/codehaus/groovy/runtime/callsite/CallSiteArray ]
??可以看到不僅生成了 “$getCallSiteArray()”這方法,還有其他的方法,以及靜態(tài)字段,由于代碼片段太多,這里就不全貼出來,只把這些生成的方法和字段反編譯后的 Java代碼貼出來:
private static SoftReference<CallSiteArray> $callSiteArray
private static void $createCallSiteArray_1(String[] param) {
param[0] = "substring"
param[1] = "toUpperCase"
param[2] = "trim"
param[3] = "iiclass"
param[4] = "<\$constructor\$>"
param[5] = "substring"
param[6] = "trim"
param[7] = "str"
param[8] = "substring"
param[9] = "toLowerCase"
param[10] = "substring"
param[11] = "toLowerCase"
param[12] = "startsWith"
param[13] = "substring"
param[14] = "toUpperCase"
return
}
private static CallSiteArray $createCallSiteArray() {
String[] v0 = new String[15]
return new CallSiteArray(GroovyMain.class, $createCallSiteArray_1(v0))
}
private static CallSite[] $getCallSiteArray() {
if ($callSiteArray != null) {
CallSiteArray ca = $callSiteArray.get()
if (ca == null) {
$callSiteArray = new SoftReference($createCallSiteArray())
}
} else {
$callSiteArray = new SoftReference($createCallSiteArray())
}
return $callSiteArray.get().array
}
??可以看到它將每個(gè)方法的調(diào)用都轉(zhuǎn)換為了一個(gè) org.codehaus.groovy.runtime.callsite.CallSite實(shí)例,那么Groovy是如何將一個(gè)方法轉(zhuǎn)換為CallSite,并且是如何進(jìn)行方法調(diào)用的呢?
??由于所涉及到代碼過多,就不一一貼出了,下面只給出 foo() 在調(diào)用時(shí)候的堆棧信息(基于IDEA的堆棧顯示,并添加了方法聲明):
12:java.lang.reflect.Constructor# public T newInstance(Object ... initargs)
11:org.codehaus.groovy.reflection.CachedMethod# public CallSite createPojoMetaMethodSite(CallSite site, MetaClassImpl metaClass, Class[] params)
10:org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite# public static CallSite createCachedMethodSite(CallSite site, MetaClassImpl metaClass, CachedMethod metaMethod, Class[] params, Object[] args)
9:org.codehaus.groovy.runtime.callsite.PojoMetaMethodSite# public static CallSite createPojoMetaMethodSite(CallSite site, MetaClassImpl metaClass, MetaMethod metaMethod, Class[] params, Object receiver, Object[] args)
8:groovy.lang.MetaClassImpl# public CallSite createPojoCallSite(CallSite site, Object receiver, Object[] args)
7:org.codehaus.groovy.runtime.callsite.CallSiteArray# private static CallSite createPojoSite(CallSite callSite, Object receiver, Object[] args)
6:org.codehaus.groovy.runtime.callsite.CallSiteArray# private static CallSite createCallSite(CallSite callSite, Object receiver, Object[] args)
5:org.codehaus.groovy.runtime.callsite.CallSiteArray# public static Object defaultCall(CallSite callSite, Object receiver, Object[] args) throws Throwable
4:org.codehaus.groovy.runtime.callsite.AbstractCallSite# public Object call(Object receiver, Object[] args) throws Throwable
3:org.codehaus.groovy.runtime.callsite.AbstractCallSite# public Object call(Object receiver) throws Throwable
2:org.codehaus.groovy.runtime.callsite.AbstractCallSite# public final Object callSafe(Object receiver) throws Throwable
1:public String foo(String str)
??從以上信息可以看出,CallSite的生成最終是使用java中的反射方式 Constructor#newInstance(),而在CachedMethod中緩存了原始的方法 Method,最終方法的調(diào)用即是使用 Method#invoke()
總結(jié)
??Kotlin 與 Groovy 在使用"?."處理NPE的原理是一致的,都是在使用前進(jìn)行判斷是否為 null ,不為null才會(huì)繼續(xù)執(zhí)行調(diào)用;只是在具體實(shí)現(xiàn)細(xì)節(jié)上 Kotlin僅僅只是對(duì)其判空、調(diào)用、再判空、再調(diào)用... ,實(shí)現(xiàn)方式簡(jiǎn)單,但執(zhí)行效率相對(duì)也高。Groovy將方法調(diào)用(屬性訪問)轉(zhuǎn)換為 CallSite ,在使用前仍然是先判空,但是整個(gè)調(diào)用鏈很長(zhǎng),方法執(zhí)行效率會(huì)相應(yīng)降低,但是其相對(duì)更加靈活。