为什么需要CallSafe?
我们平时在使用Java开发的时候,遇到过最多的异常就是 NullPointException (NPE),想处理这个这个异常很简单,只需要在变量、方法返回值等使用前对其进行判空处理之后再使用即可,但是我们又不想书写再所有地方都进行先判空再使用的逻辑代码,因为这样不仅让代码看起来乱糟糟还影响阅读,所以我们迫切的希望有一种”系统统一处理NPE“的功能,这些逻辑代码不侵入我们自己的项目代码。
带着这个想法我们继续探讨:
①系统帮我们处理NPE;
②这些代码不要出现在项目代码中
仔细想来要实现这个功能,无非只有两个阶段时能做到:
1.编译阶段,编译阶段将NPE处理的代码插入到业务代码中,最终生成带有NPE处理的.class 文件;
2.运行阶段,我们希望在JVM在执行代码时,遇到NPE不是抛出的NPE异常而中止,而是按照我们预定义的逻辑继续执行。
两种处理方式各有优劣,不过目前看来第一种方式明显好于第二种,因为第一种方式虽然可能会导致JVM多执行一些额外的逻辑,但是生成的 .class 在任何的标准JVM上都能正确的执行,第二种方式再执行上可能会更高效点,但是需要干预 JVM 的执行,这在某些情况下是不可行的(比如Android程序,你无法干预所有客户端的执行)。
Kotlin的CallSafe处理
扯的有点远,现在正式开始说这个Kotlin和Groovy处理NPE的方式,在kotlin中,如果我们不想自己做NPE检查,又不想程序因抛出NPE而终止运行,我们可以使用 ‘可选型’类型来处理有可能发生NPE的情况,举例:
private fun foo(str:String?):String?{
return str?.toLowerCase()?.substring(1)
}
这段代码在方法调用时候使用了‘?.’ 来替代我们之前使用的 ‘.’,它的作用是:如果方法的被调用者为null了,则终止操作,想必这个很简单,大家都知道作用,但是它是如何实现的呢?我们使用'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
#19 = Utf8 (Ljava/lang/String;)V
#20 = NameAndType #18:#19 // "":(Ljava/lang/String;)V
#21 = Methodref #15.#20 // kotlin/TypeCastException."":(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."":(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."":(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
下面来逐行解释每个指令的意思:
0:aload_1 //将 LocalVariableTable 中的第2个引用类型变量(也就是str)压入栈顶
1: dup //复制栈顶数据并将复制值压入栈顶
2: ifnull 65 //如果(栈顶数据)为null,则跳转至 65
5: astore_2 //将栈顶引用型数值存入第三个本地变量
6: aload_2 //将第三个引用类型本地变量推送至栈顶
7: dup //复制栈顶数值并将复制值压入栈顶
8: ifnonnull 21 //如果不为空则跳转至21(为空则继续向下执行)
11: new #15 //创建 TypeCastException的实例,并将其引用压入栈顶
14: dup //复制栈顶数值并将复制值压入栈顶
15: ldc #17 //将 ”null cannot be cast to non-null type java.lang.String“压入栈顶
17: invokespecial #21 //调用 TypeCastException 的 方法 (初始化)
20: athrow //将栈顶的异常抛出
21: invokevirtual #27 //调用实例方法 String#toLowerCase()
24: dup //复制栈顶数值并将复制值压入栈顶(上一步方法调用后的返回值)
25: ldc #29 //将”(this as java.lang.String).toLowerCase()“压入栈顶
27: invokestatic #35 //调用 kotlin.jvm.internal.Intrinsics.checkExpressionValueIsNotNull(Object,String)方法 (参数的匹配:以当前栈顶的元素作为方法的最后一个参数,距栈顶深度为2的元素作为倒数第二个参数... 以此类推)
30: dup //复制栈顶数值并将复制值压入栈顶(也就是上一步方法调用返回值)
31: ifnull 65 //如果(栈顶数据)为null,则跳转至 65
34: astore_2 //将栈顶引用型数值存入第三个本地变量
35: iconst_1 //将int型1推送至栈顶
36: istore_3 //将栈顶int型数值存入第四个本地变量(也就是1)
37: aload_2 //将第三个引用类型本地变量推送至栈顶(也就是 .toLowerCase()的返回值)
38: dup //复制栈顶数值并将复制值压入栈顶
39: ifnonnull 52 //不为null时跳转至 52 (实际上在判断.toLowerCase()的返回值是否为null)
42: new #15 //创建 TypeCastException的实例,并将其引用压入栈顶
45: dup //复制栈顶数据并将复制值压入栈顶
46: ldc #17 //”null cannot be cast to non-null type java.lang.String“压入栈顶
48: invokespecial #21 //调用 TypeCastException 的 方法 (初始化)
51: athrow //将栈顶的异常抛出
52: iload_3 //将第四个int型本地变量推送至栈顶(也就是1)
53: invokevirtual #39 //调用 .substring(1)
56: dup //复制栈顶数值并将复制值压入栈顶
57: ldc #41 //将”(this as java.lang.String).substring(startIndex)“压入栈顶
59: invokestatic #35 //调用kotlin.jvm.internal.Intrinsics.checkExpressionValueIsNotNull()
62: goto 67 //跳转至 67
65: pop //将栈顶数值弹出(数值不能是long或double类型的)
66: aconst_null //将null推送至栈顶
67: areturn //从当前方法返回对象引用
通过对上面 .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 sanitizeStackTrace(T throwable) {
return sanitizeStackTrace(throwable, Intrinsics.class.getName());
}
static 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 list = Arrays.asList(stackTrace).subList(lastIntrinsic + 1, size);
throwable.setStackTrace(list.toArray(new StackTraceElement[list.size()]));
return throwable;
}
将这个反编译的.java源码和我们之前写的 .java源码对比,不难发现:
①:访问修饰符增加了 final;
②:对"可选型"参数在使用前进行了判空;
③:对”不可为null“的情况使用了‘Intrinsics.checkExpressionValueIsNotNull()’去检查,并在抛出异常时消除掉该条语句的堆栈信息。
看到这里我们基本上就可以得出结论了,Kotlin的safeCall并不是使用了”可选型“而是使用”判空“来保证不会触发NPE而终止程序。
但是,如果你仔细看了上面的代码,不难发现,在调用 .substring()前的判空操作”滞后“了,因为在调用此方法之前先调用了 Intrinsics.checkExpressionValueIsNotNull(),这个方法最终仍会抛出异常。猜想: ‘.toLowerCase()’这个方法的定义是在 ‘java.lang.String'中,它的返回值是”String“ 而不是”String?“(事实上Java中也没有String?这样的方式),验证这个猜想很简单,我们只需要使用Kotlin的类方法扩展功能,为String扩写一个方法,该方法返回 "String?",然后在查看编译出的”.class“中是否对我们新定义的方法在调用前有判空操作即可:
新增扩展方法:
public inline fun String.tonull(flag: Boolean): String? = if (flag) {null} else{"abc"}
//这里如果直接返回 null ,在掉用这个方法之后再继续使用返回值调用其他的方法会在编译阶段被优化掉
改变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;
}
可以看到确实是这样,对于返回值为 ”String“类型的会强制检查其值是不是为 null ,如果为null则直接抛出异常,终止执行。对于返回值为 ”String?“的类型则不会检查值,但在使用该值之前会做判空操作。
Groovy的CallSafe处理
那在Groovy中也是通过 非空判断来避免NPE的吗?我们仍然以上面 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)));
}
对照我们自己编写foo()源码后发现,我们使用的 ”?."变成了”.callSafe“ ,而 ”.“变成了 ”call“,具体是怎么转换的还需要进一步分析:
首先,这个我们自己并没有定义’$getCallSiteArray()‘这个方法,而且直接使用 IDEA 自带的反编译工具竟然开不到这个方法,那就只能去”.class“中找这个方法的定义了:
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."":(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()”这方法,还有其他的方法,以及静态字段,由于代码片段太多,这里就不全贴出来,只把这些生成的方法和字段反编译后的 Java代码贴出来:
private static SoftReference $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
}
可以看到它将每个方法的调用都转换为了一个 org.codehaus.groovy.runtime.callsite.CallSite实例,那么Groovy是如何将一个方法转换为CallSite,并且是如何进行方法调用的呢?
由于所涉及到代码过多,就不一一贴出了,下面只给出 foo() 在调用时候的堆栈信息(基于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,最终方法的调用即是使用 Method#invoke()
总结
Kotlin 与 Groovy 在使用"?."处理NPE的原理是一致的,都是在使用前进行判断是否为 null ,不为null才会继续执行调用;只是在具体实现细节上 Kotlin仅仅只是对其判空、调用、再判空、再调用... ,实现方式简单,但执行效率相对也高。Groovy将方法调用(属性访问)转换为 CallSite ,在使用前仍然是先判空,但是整个调用链很长,方法执行效率会相应降低,但是其相对更加灵活。