目录
方法链
方法链(Method Chaining),也被称为命名参数法,是在面向对象的编程语言中调用的调用多个方法的通用语法。每一个方法返回一个对象,在一个单一的声明里,方法链省去了中间变量的需要。
当需要构建一个对象或者设置其初始属性时,往往通过构造参数传入或者 setter 方法。比如:
User user = new User("张三", 34);
// 或者
User user = new User();
user.setName("张三");
user.setAge(34);
但这样存在一些缺点:
- 参数数量:很多情况下我们可能只需要一两个参数,其余参数保持默认。使用构造方法的话需要为各种情况进行声明;
- 中间变量:如果创建后的对象直接被作为其他方法的参数,如果不使用构造方法设定参数,而是只设定其中以一两个个参数,那么必须使用中间变量并通过 setter 实现。
方法链通过返回对象自身,便可以在设定一个参数后直接调用另一个参数而不必使用中间变量。方法链常与建造者模式(Builder)结合,比如 StringBuilder:
String str = new StringBuilder("a")
.append(1)
.append(3.14f)
.toString();
字节码与 Smali 下的编译结果
Java 源代码通过编译为 JVM 可解释的字节码(Byte Code)以便在 JVM 上执行。为了适应移动设备的特点,Android 使用 DalvikVM 而不是 JVM,相应的字节码也需要转变。DalvikVM 字节码码反编译过后就是 Smali 代码。
JVM 是基于栈的虚拟机,而 DalvikVM 是基于寄存器的虚拟机。相比之下,基于寄存器的虚拟机效率更高,更适合在移动设备上运行。另外,JVM 通常使用 JIT(Just In Time, 即时编译)进行加速,而 DalvikVM 的继承者 ART 则采用 AOT(Ahead Of Time, 提前编译)加速。
对于以上 StringBuilder 的例子,使用 javac
编译成字节码后,通过 javap -c
查看,结果如下:
0: new #37 // class java/lang/StringBuilder
3: dup
4: ldc #50 // String a
6: invokespecial #52 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
9: iconst_1
10: invokevirtual #54 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
13: ldc2_w #57 // float 3.14f
16: invokevirtual #59 // Method java/lang/StringBuilder.append:(F)Ljava/lang/StringBuilder;
19: invokevirtual #46 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
可以看到,在第一条 invokevirtual
调用 append
方法之后会把其结果存到栈内,下一条 ldc2_w
则把常数 3.14 再存入栈内,此时栈顶有一个 StringBuilder 对象和一个 double。下一句 invokevirtual
则直接取栈顶两个作为参数进行调用,并再把结果放入栈内。可以看到链式调用不仅在 Java 源代码上可以简化写法,甚至在字节码上没有额外增加指令。
通过 d8
将该字节码编译为 .dex 文件,再通过安卓逆向工具得到 Smali 代码:
new-instance v0, Ljava/lang/StringBuilder;
const-string v1, "a"
invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
.line 28
const/4 v1, 0x1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v0
.line 29
const v1, 0x4048f5c3 # 3.14f
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(F)Ljava/lang/StringBuilder;
move-result-object v0
.line 30
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
Smali 代码中可以看到很多寄存器,有一种看 MIPS 或者 ARM 汇编的感觉。可以看到每次通过 invoke-virtual
调用 append
方法后需要把返回值移动到寄存器 v0
,之后再添加参数,调用下一个 append
方法。注意,每次调用 invoke-virtual
时,第一个参数都是寄存器 v0
,而 v0
都来自于上次调用 append
后的返回值。而实际上在第一句 new-instance
之后,v0
始终为同一个 StringBuilder 对象。也就是说,此处的 move-result-object v0
是多余的(除了最后一个调用 toString
之后的)。每一次调用 append
时,需要指定除 v0
外的另一个参数 v1
(通过 const
等语句),而调用之后则通过 move-result-object
将结果返回给 v0
,当 append
调用很多时,冗余率达 1/3。
所以对 DalvikVM 好一点儿的写法是:
StringBuilder bld = new StringBuilder("a");
bld.append(1);
bld.append(3.14f);
String str = bld.toString();
另外,对于字符串拼接,源代码中使的用加号(+
)实际上是语法糖,会被编译成 StringBuilder
或者 StringBuffer
,而每次至少调用两次 append
(因为脱糖之后不会在构造方法内传入第一个字符串,而是不带参构造,两次调用 append
进行拼接)和一次 toString
。通过 StringBuilder
在处理大量的拼接操作时固然有效,但是对于仅两个字符串的拼接,建议直接用 String.concat
方法以减少参数调用次数,同时效率也不打折扣。
总结
方法链固然带来了很大的方便,但是对于安卓来说会在一定程度上带来冗余的 Dalvik 字节码,因此在做安卓开发时需要慎用。
原文地址:https://www.cnblogs.com/RainbowC0/p/18636744 ,未经作者许可禁止转载
标签:lang,Ljava,调用,Java,StringBuilder,安卓,v0,DalvikVM,append From: https://www.cnblogs.com/RainbowC0/p/18636744