来看一下Kotlin
中sum函数的字节码是怎样的:
可以看到,在上面的sum函数中并没有存在对sum自身的调用,而取而代之的是,是第17条的goto命令。所以,Kotlin
尾递归背后的黑魔法就是将递归改成循环结构。上面的代码翻译成我们容易理解的代码就是如下形式:
public int sum(int start, int end , int acc){
for(;;){
if(start > end){
return acc;
}else{
acc = start + acc;
start = start + 1;
}
}
}
通过上述的分析我们可以看到,递归是通过转化为循环来降低内存的占用。但这并不意味着写递归就是很差的编程习惯。在Java
这种面向对象的语言中我们更倾向于将递归改成循环,而在Haskell
这类函数式编程语言中是将循环都改为了递归。在思想上并没有优劣之分,只是解决问题的思维上的差异而已,具体表现就是落实到具体语言上对这两种方法的支持程度不同而已(Java没有尾递归,Haskell没有for、while循环)。
类的初始化顺序是怎样的?
这个问题对于正在找工作的人可能比较有感觉,起码当时我在毕业准备面试题时就遇到了这个问题,并且也机械的记忆了答案。不过我们更期待的是这个答案背后的理论依据是什么。我们尝试从class文件中找到答案。来看这样的一段代码:
public class InitialOrderTest {
public static String staticField = " StaticField";
public String fieldFromMethod = getStrFromMethod();
public String fieldFromInit = " InitField";
static {
System.out.println( "Call Init Static Code" );
System.out.println( staticField );
}
{
System.out.println( "Call Init Block Code" );
System.out.println( fieldFromInit );
System.out.println( fieldFromMethod );
}
public InitialOrderTest()
{
System.out.println( "Call Constructor" );
}
public String getStrFromMethod(){
System.out.println("Call getStrFromMethod Method");
return " MethodField" ;
}
public static void main( String[] args )
{
new InitialOrderTest();
}
}
它运行后的结果是什么呢?结果如下:
我们来一一来看一下它的class文件中的内容,首先是有一个static方法区:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc #14 // String StaticField
2: putstatic #15 // Field staticField:Ljava/lang/String;
5: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #16 // String Call Init Static Code
10: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
16: getstatic #15 // Field staticField:Ljava/lang/String;
19: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
22: return
Java编译器在编译阶段会将所有static的代码块收集到一起,形成一个特殊的方法,这个方法的名字叫做<clinit>
, 这个名字容易让我们联想到构造函数的名称叫做<init>
,但与构造函数不同,这个方法在Java层中是调用不到的,并且,这个函数是在这个类被加载时,由虚拟机进行调用。注意的是,是类被加载,而不是类被初始化成实例。所以,静态代码块的加载优先于普通的代码块,也优先于构造函数。这属于虚拟机规定的范畴,我们不做更深入的探讨。
在Class文件中,是没有为普通方法区开辟类似于<clinit>
这种方法的,而是将所有普通方法区的代码都合并到了构造函数中,我们直接来看构造函数:
public InitialOrderTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_0
6: invokevirtual #2 // Method getStr:()Ljava/lang/String;
9: putfield #3 // Field field:Ljava/lang/String;
12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
15: aload_0
16: getfield #3 // Field field:Ljava/lang/String;
19: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
22: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
25: ldc #6 // String Init Block
27: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
33: ldc #7 // String Constructor
35: invokevirtual #5 // Method java/io/Prin