Kotlin與Groovy的CallSafe處理比較

為什么需要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ì)更加靈活。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容