1. 从字节码文件看this
当我们在方法内部使用this调用属性、方法的时候,你有没有考过this是怎么来的?作为引用变量,this的数据类型是什么?我们在源码中书写的this,编译成字节码文件后变成了什么?jvm又是如何处理this的呢?想弄清楚这几个问题,不懂点儿编译知识和JVM怕是不行的。
字节码分析
众所周知,java源码”.java”首先会被编译为字节码文件”.class”。而我们最常用的java编译器就是jdk自带的javac。
看如下代码,我们对其生成的字节码进行必要分析:
示例
package test;
class Base {
public String name = "I am Base";
void instanceMethod(String str1){
//do something
}
static void classMethod(String str2) {
//do something
}
}
上述代码编译后,我们在字节码所在文件夹中执行如下命令:
javap -verbose Base
编译后的字节码如下(只截取了一部分):
public java.lang.String name;
flags: ACC_PUBLIC
test.Base();
flags:
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String I am Base
7: putfield #3 // Field name:Ljava/lang/String;
10: return
LineNumberTable:
line 8: 0
line 9: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Ltest/Base;
void instanceMethod(java.lang.String);
flags:
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Ltest/Base;
0 1 1 str1 Ljava/lang/String;
static void classMethod(java.lang.String);
flags: ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 17: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 str2 Ljava/lang/String;
- 1行:定义了一个String类型的实例变量name,注意只是定义,并没有赋值。默认为null。
- 2行:name变量的访问控制符,没有表示默认访问权限。
- 4-19行:Base类的默认的构造方法。因为我们并没有显式的定义constructor,所以编译器会生成一个默认的无参constructor,和我们手动定义一个方法体为空的无参构造方法效果完全一样。
其中7-13行是构造方法的代码区域;第17-19行描述的是Java源码中定义的局部变量与该方法被执行时栈帧中的局部变量表中的变量的对应关系。第7行args_size=1,说明构造方法有一个参数,那我们就看下第17-19行的LocalVariableTbale中这个参数到底是什么。我们发现,这个唯一的局部变量就是this!且其静态类型为Base!:
10-12行,是给实例变量name进行初始化赋值。即实例变量的初始赋值操作,是在构造方法中进行的。 - 21-31行:instanceMethod实例方法。同样我们看到字节码文件中却有两个参数,args_size=2,第一个参数是this,且参数的静态类型为Base!
- 33-39行:classMethod静态方法,属于类的。我们可以明确的看到args_size=1,即只有我们自己定义的参数str2。类方法(静态方法)中,并没有this局部变量!当然,我们也就无法在类方法中使用this关键字了!
上述示例,总结如下:
1. 构造方法、实例方法的第一个参数是this!这是由编译器自动添加的。
2. this引用变量的数据类型是,this所在方法的所属类。即,编码时,this出现在哪个类中,this的数据类型就是这个类。
3. 既然编译器会自动给实例方法添加一个this参数,那么就不难理解,当调用某个实例对象的方法时,编译器会将该实例对象当做参数传递到调用方法中了。
手动传递this参数
注意,此章节是我的一家之言,我从来没见过其他人这么描述过。但是我感觉这非常有助于我理解、使用this。欢迎提出意见。
既然第一个参数是编译器自动添加的this。那么,为了更好的理解,我们就假设,如果编译器不给我们添加this参数,我们自己写的话应该怎么写呢?模拟如下(不可能通过编译的,只是为了理解):
class Base {
public String name = "I am Base";
//假设编译器不会自动添加this参数,实例方法需要手动添加this参数,参数类型为方法所在类
void instanceMethod(Base this, String str1) {
//do something
}
//静态方法,无this参数
static void classMethod(String str2) {
//do something
}
public static void main(String[] args) {
//定义一个静态类型为Base的引用变量b,生成一个Base对象,并将其引用赋值给变量b
Base b = new Base();
//引用变量b调用实例方法,该方法接受两个参数,其中第一个就是引用变量b本身
b.instanceMethod(b, "str1");
//引用变量b调用类方法,类方法没有this参数,所以不需要传递引用变量b本身
b.classMethod("str2");
}
}
我想,这会儿大家应该知道,在方法体内部使用的this,是怎么来的了吧。
this其实就是通过方法体的第一个参数传递过来的,它指代调用该方法的对象。通过这个例子应该能够更加直观具体的体会this的含义了。
2. this的本质
- 实例方法中(非构造非静态):this指代的是正在调用当前方法的对象
- 构造方法中:this指代的是正在new的对象本身
关键点:
1. this指代的一定是对象,是jvm堆中的某个class的实例对象的引用;this代表的不是class或者其他。
2. this出现的位置一般是在方法体内部(包括方法参数位置),假设this在methodA中出现,那么this指代的就是正在调用methodA的那个对象;即便this出现在方法体外部。如,假设在class A 中定义成员变量 A a = this; 最终这行代码仍然会被放入class A 的到构造函数中执行。
3. 为了便于理解,对this在普通方法和构造方法中进行了区分。其实,构造方法的真正功能是对对象进行初始化。
实际上,在jvm层面上,在执行构造方法之前,jvm已经为在堆中给对象分配好了内存空间,并将该对象的引用当做参数传递到构造方法中,构造方法的执行,只是为了给对象进行初始化。所以,我们仍然可以按照第一条表述来理解,即,构造方法是由新创建的对象来调用的,构造方法中的this,仍然指代的是正在调用该构造方法的对象。
4. 静态方法以及静态代码块中,不能使用this。原因很简单,从逻辑上讲,静态方法是可以直接通过类名进行调用的,根本就没有实例对象,所以在静态方法中,不能使用this;静态代码块,是在jvm第一次加载该类时执行的,此时更不会有实例对象,所以this也不能出现在静态代码块中。从java语法上讲,静态方法的参数列表中是没有this参数的。
至此,关于什么是this就已经讲清楚了。看似简单,但是,在使用this调用实例变量、方法,特别是存在继承关系时,会出现很多令人不解的语法现象。要彻底理解这些问题,还需要其他的一些java底层知识作为支撑。时间允许的话我会继续更新。