理论和(手敲一遍)代码必须结合起来才能真正的掌握,如果知识点有遗漏或大家有疑问欢迎随时在评论区讨论,万分感谢!!!
一、this
概念
this通常用来指代当前类的对象,或者指代当前类子类的对象,需要具体情况具体分析,它本质是一个指向当前被调用的方法/属性所属的对象的指针,如何判断this代表的是哪个对象?
this.xxx(); // 当前那个对象调用的xxx方法,this就代表哪个对象
常见用法与注意点
构造方法中用于标识被同名构造参数屏蔽的实例属性
对象的属性因为与构造器参数同名而被构造器参数屏蔽时,如果需要调用对象被屏蔽的属性,需要用用this进行标识,例如this.name指的是我这里的调用的是当前对象的属性name而不是构造器的局部变量参数name
//局部变量name和age屏蔽了name和age属性,用this进行标识做区分
public Person(String name, int age,String nation) {
this.name = name;
this.age = age;
}
用于普通方法中,表示正在调用方法的那个对象
this只能在方法体/非静态代码块块内使用,但是如果在方法内调用同一个类的另一个方法,就不必使用this,直接调用即可,this关键字是能省则省
public class Person{
public void test(){
System.out.println("测试");
}
public void test2(){
//以下这两行是等价,推荐第二种写法
this.test();
test()
}
}
this不能用于静态方法中
static方法是类方法,依附于类而不依赖与任何对象,static属性是指该属性是类中所有对象所共享的,static方法是类方法,先于任何实例(对象)存在,static在类加载时就已经存在了,但对象是在创建时才生成;方法中使用this关键字它的值是当前对象的引用,只能用它调用属于当前对象的属性和方法和。但是,this可以调用static类型的属性,举个例子:一个父亲是不可能向他还未出生的孩子借钱的,但孩子出生后完全可以找他父亲去借钱,总之:static方法或static代码块内不能出现this,但是this可以调用到静态属性和静态方法
public class Person {
public static String nation = "Chinese";
public static String getNation(){
return nation;
}
@Test
public void test(){
System.out.println(this.nation); //Chinese
System.out.println(this.getNation());//Chinese
}
}
如果静态方法中用到了非静态的属性和方法,就会报错无法从静态上下文中引用到非静态的方法/属性,(底层原理下文中有分析)
父类方法中的this也可能代表的是调用该父类方法的子类的实例对象
- 对于一个类中的this,不一定单指这个的对象,也可能是这个类的子类的对象(抽象类里面的this只能是实际调用中它的派生类的实例化对象,因为抽象类本身不能实例化);总之:如果new 父类对象的话,父类方法的this指向的是父类,如果new 子类,那么父类方法的this指向的是子类
class Student extends Person{
private String name;
private int age;
public Student(String name, int age) {
super(name, age);
}
}
public class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person/Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public void print(){
System.out.println(this.toString());
}
public static void main(String[] args) {
Student student = new Student("student", 11);
Person person = new Person("person",12);
//此时print中的this代表的是Student的实例对象
student.print();//Person/Student{name='student', age=11}
//此时print中的this代表的是Person的实例对象
person.print();//Person/Student{name='person', age=12}
}
}
private/fianl/abstract类非抽象方法中,使用this的注意点
父类方法中的this可能是代表自己的实例对象,也可能代表子类的实例对象,这一点要看具体用哪个对象调用的,以下几类特殊方法需要特别关注
- 父类的private方法:private则代表该父类方法只能在父类它本身类范围内调用,子类无法重写也无法调用,private方法中的this对象只能是代表父类自己的实例对象指针
- 抽象类型父类中的非抽象方法:抽象类是无法实例化的,所以abstract类中的非抽象方法中的this对象只能是代表子类自己的实例对象指针
- 父类的final修饰方法,意味着子类无法去重写该方法,但不影响子类对象直接去调用父类的final方法,所以final方法中的this对象既可以代表子类自己的实例对象指针,又可以代表父类本身的实例对象指针
用于在构造函数中调用其他构造函数
- 只能定义在构造函数的第一行,因为初始化动作要先执行,如果是用this调用本列无参构造器可省略,如果用this调用的是有参构造器就不能省略,而且this只能调用一次构造器
public class Person{
private String name;
private int age;
public Person() { }
public Person(String name) {
this(); //作用是调用本类的无参构造,可以省略
this.name = name;
}
public Person(int age) {
this();//作用是调用本类的无参构造,可以省略
this.age = age;
}
public Person(String name, int age) {
this(name);
this.age = age;
}
}
底层原理分析(需要JVM基础)
虚拟机栈
每个线程在创建时都会创建一个虚拟机栈,对应着一次次的Java方法调用,是线程私有的,Java虚拟机栈的基本单位是栈帧,每个方法在执行前都会为其创建一个栈帧,栈帧由局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息五部分组成,功能如下
-
局部变量表,它本质是一个数组,最基本的存储单元是slot,其大小在编译期间确认下来,局部变量表中的变量只在当前方法调用中有效,方法调用结束后局部变量表也会随之销毁,一个slot大小为32bit,主要用于存储方法的入参和方法中定义的局部变量,不同的局部变量类型如下
- 如果方法中创建的是引用类型的局部变量(new对象的形式),这个对象主体是存储在堆上的,局部变量表中存储的其实这个对象的指针,占用1个slot,如果当前帧是由实例方法创建的,那么该对象引用this将会存放在index为0的slot处,即数组的第一个元素的位置,其余的参数按照参数表顺序继续排列。所以结合前文,this可以看做是一个指针,当前栈帧是由那个实例对象创建的,它就指向那个实例对象,抽象类中的非抽象方法中也是可以用到this的,可能大家不太理解这一点,因为抽象类本身是无法实例化的,在实际开发中抽象类从来不是作为一个独立的类存在的,抽象类的本质是作为基类给子类继承的,真正执行抽象类非抽象方法的子类的对象
- 如果是基本数据类型的局部变量,那么这个数值直接存储在局部变量表中,其中byte、short、char 在存储前被转换为int,占用1个slot,long或double占用2个slot
- returnAddress类型的变量
局部变量表是线程私有的,不存在线程安全问题,局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
-
操作数栈,在方法执行过程中,根据字节码指令,将一系列操作数入栈,随后进行一系列操作如运算、复制、交换等…、运算结束后操作数出栈,结果再运算,然后将计算结果再存入局部变量表中
类加载过程
class file字节码文件存在于本地硬盘或其他主机上(需要进行网络传输),可以理解为设计师画在纸上的模板,而最终这个模板需要通过ClassLoader加载到JVM的方法区中,成为DNA元数据模板,然后根据这个文件实例创建出n个一模一样的实例,详细的载过程如下
- 加载阶段:类加载器子系统负责从文件系统或者网络中加载Class文件
- 链接阶段
- 验证:保证被加载类的正确性,不会危害虚拟机自身安全
- 准备:为类变量分配内存并且设置该类变量的默认初始值,即零值,这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化
- 解析:将常量池内的符号引用转换为直接引用的过程
- 初始化阶段: 初始化阶段就是执行类构造器方法
<clinit>()
的过程,javac编译器自动收集类中的所有类变量(static修饰的静态变量)的赋值动作和静态代码块中的语句合并而来,注意:<clinit>()
不同于类的构造器。(关联:构造器是虚拟机视角下的<init>()
),若该类具有父类,JVM会保证子类的<clinit>()
执行前父类的<clinit>()
已经执行完毕
this的本质是什么?
我们前文中说this本质是代表调用当前方法/属性所属的实例对象的指针
我们对类com.exemple.demo.test.Person进行编译后的字节码文件如下,我们先来观察test方法,因为test方法的栈帧是由实例对象person创建的,那么person这个对象的引用this将会存放在index为0的位置,我们观察局部变量表,确实是有this的
再分析test方法的看字节码,其中重点关注test方法的执行,流程如下
0 iconst_1 <!--创建一个数值类型的常量,这里常量的值为1->
1 istore_1 <!--存储在局部变量表索引为1的位置上,即局部变量表数组的第2个位置上,第1个位置默认会分配给this->
<!--以上两行等价于int a = 1 ->
2 new #2 <java/lang/Object> <!--在堆上开辟一块空间分配给对象object ->
5 dup
6 invokespecial #1 <java/lang/Object.<init> : ()V> <!-- 调用Object的构造方法,object对象的信息存储在堆上上 ->
9 astore_2 <!--存储在局部变量表数组索引为2的位置上,即第3个位置上 ->
<!--以上两行等价于Object object = new Object(); ->
10 aload_0 <!--从局部变量表索引为0的位置上取出this,this本质是作为创建这个虚拟机栈的
实例对象的指针,需要通过this来调用实例对象的test方法 ->
11 invokevirtual #3 <com/exemple/demo/test/Person.test : ()V> <!-- 通过this指针来调用并执行它自己的实例方法test->
14 return
为何静态方法/代码块中无法使用this指针?
类加载的过程与class对象生成的时机
- 加载阶段:类加载器ClassLoader从网络或本地文件系统中加载字节码文件,通过一个类的全限定名来获取其定义的二进制字节流,根据字节码在java堆中生成一个代表这个类的java.lang.Class类型的对象
- 链接阶段:将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值)
- 初始化:到了此阶段,才真正开始执行类中定义的java程序代码(字节码层面即执行clinit)。即按在代码中出现的顺序执行该类的静态变量初始化和类的静态代码块,如果该类有父类的话,则优先对其父类进行初始化
- 注意:类执行初始化阶段,即静态代码块内就可以获取到这个类的class对象了,因为类的class对象是在加载过阶段生成的
- 注意:类执行初始化阶段,即静态代码块内就可以获取到这个类的class对象了,因为类的class对象是在加载过阶段生成的
执行clinit过程
如上所述类加载的过程大体分为三步:加载、连接、初始化,初始化过程即是执行clinit的过程,clinit的内容很简单,就是按代码出现的顺序去执行静态属性赋值、静态方法、静态代码块
注意,clinit中不会主动去执行静态方法的,而是在某些特殊情况下去执行静态方法,比如,将静态方法的方法返回值赋值给静态属性
类加载的初始化时期执行clinit,这个时期是远远早于实例对象创建的时间,或者说这一时期的工作和实例对象是没有关系的。
主动调用静态方法的过程
如下图,如果是main方法中主动去调用静态方法test2
public static void main(String[] args) {
Person.test2();
}
我们可以看到,静态方法test2方法的栈帧中的局部变量表中并没有this,原因是这个栈帧并不是由该类自己的实例对象所创建(静态方法栈帧由该类的class对象创建),静态方法不依赖于它自己的实例对象,而是依赖于这个类本身。
静态方法与class对象的关系
每一个类都有一个Class对象,基本类型 (boolean, byte, char, short, int, long, float, and double)也有class对象,数组也有class对象,前文中我们提到普通方法的栈帧是由该方法的实例对象对象所创建的,静态方法的栈帧是由静态方法所在类的class对象创建,而class对象被创建的时机是类加载时期,这个时期是远远早于实例对象创建的时间,虽然对象的创建需要依赖于类信息,但类的加载过程并不伴随着实例对象的创建,所以静态方法/静态代码块中没有this,静态方法体/静态代码块内也不能使用this,super也是类似的道理,静态方法体中也不能使用super,详情继续参考下文
通过this(xxx)代指构造器调用原理
我的另一篇文章,JAVA构造器、静态上下文的执行时机与代码执行顺序详解,对构造器、静态上下文的底层原理与java代码整体的执行顺序作了深入分析,推荐大家阅读、收藏。
二、super
概念
super可以理解为“父类的”,super可以在子类中调用父类的属性,方法,构造器,super关键字和this一样能省就省,super在java的子类中起到父类引用的作用,this的本质是一个指针,super的本质是一个关键字
常见用法与注意点
创建子类对象对象时父类的构造方法会以super的形式被调用
- 父类如果重写了无参构造器或者父类中没有有参构造器,那么子类的构造方法第一行就是super(),可以省略
class Student extends Person{
//这是默认的构造器内容,写出来是为了帮大家理解
public Student(){
super();
}
}
public class Person{
private String name;
private int age;
}
- 如果父类中定义了有参构造器但没有显示写出无参构造器,那么必须通过super调用父类的有参构造函数,如果父类中定义了多个有参构造区,那么用super调用其中一个有参构造器即可
class Student extends Person{
public Student(String name, int age) {
//任选一个父类有参构造
//super(name, age);
super(name);
}
}
public class Person{
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name) {
this.name = name;
}
}
注意:调用子类的构造创建了一个子类对象时,会先调用父类的构造器,证明如下
- 子类重写父类的方法后可以通过super调用到父类的方法
class Student extends Person {
private String name = "wzh2";
@Override
public String getName() {
return "子类" + name;
}
public String getParentName(){
//调用父类的方法
return super.getName();
}
public static void main(String[] args) {
Student student = new Student();
System.out.println(student.getName());
System.out.println(student.getParentName());
}
}
public class Person{
//protected意味着子类和同一包中可以访问
protected String name = "wzh";
protected int age = 20;
public String getName() {
return "父类" +name;
}
}
输出结果
子类通过super获取到父类的属性和方法
class Student extends Person{
public void parentDisplay(){
System.out.println(super.age + super.name);//通过super获取父类属性
super.student(); //通过super调用父类方法
student();
}
@Override
public void student() {
System.out.println("student");
}
}
public class Person{
//protected意味着子类和同一包中可以访问
protected String name = "wzh";
protected int age = 20;
public void student() {
System.out.println("person");
}
}
通过super区分父类的方法a和在子类中被重写的父类方法a
如上述案例中
super.student(); //通过super调用父类方法student()
student();//调用到的是子类中重写的父类的方法的student()
如果一个类没有基础任何父类super相当于调用的是Object中的方法
public class Person{
private String name;
private int age;
public void display(){
//通过this或super调用到了Object的toString();
System.out.println(super.toString());
}
public static void main(String[] args) {
new Person().display(); //输出为Person@452b3a41
}
}
super也不能出现在静态方法/静态代码块中
不再举例
底层分析(需要JVM基础)
创建子类对象时,父类对象会也被一起被创建吗?
如上分析,在创建子类对象时,首先会通过super显示或隐式地调用父类的构造器,但这里其实并没有去创造父类的对象,只不过是调用父类构造方法来初始化属性
new指令开辟空间,用于存放对象的各个属/性引用等,反编译字节码你会发现只有一个new指令,所以开辟的是一块空间,一块空间就放一个对象。
子类可以调用父类的属性,方法,但这并非意味着创建了父类的对象
在字节码中子类会有个u2类型的父类索引,属于CONSTANT_Class_info类型,通过父类索引即可获取到父类的类信息
获取到父类信息后即用来解析父类方法啊,属性名称等,解析完后实际变量内容存储在new出来的空间那里
所以super这个关键字只不过是访问了这个空间特定部分的数据(也就是专门存储父类数据的内存部分)
验证如下,我们使用Object中hashcode和equals判断this和super都是一样的,所以,这根本就在一个(子类实例对象)空间里,并没有额外去创建父类的对象。
方法分类
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法;其中静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法,非虚方法在类加载的解析阶段就可以解析了,其他方法称为虚方法。
虚拟机中提供了以下几条方法调用指令
- invokestatic:调用静态方法
- invokespecial:调用子类私有方法、父类方法、
- invokevirtual:调用所有虚方法
- invokeinterface:调用接口方法
方法的调用:虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
虚方法表是什么时候被创建的呢?虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕
案例代码
package com.exemple.demo.test;
interface Friendly{
void sayHello();
void sayGoodbye();
}
class Dog{
public void sayHello(){}
public String tostring(){
return "Dog";
}
}
class Cat implements Friendly {
public void eat() {}
public void sayHello() {}
public void sayGoodbye() {}
@Override
protected void finalize() throws Throwable {
super.finalize();
}
@Override
public String toString() {
return super.toString();
}
}
class CockerSpaniel extends Dog implements Friendly{
public void sayHello() {
super.sayHello();
}
public void sayGoodbye() {}
public final void test() {
test2();
}
private void test2() {
test();
}
}
如下是Cat类的虚方法表的图解,eat、sayHello、sayGoodbye是Cat类的特有方法,toString和finalize是Cat重写的父类Object的方法,如果想调用父类的这两个被子类重写的方法,需要用super,其它未被Cat重写的Object的方法clone、equals、getClass则更要通过super关键字来调用
super关键字字节码解析
如上图我们可知,super.finalize();被编译后的字节码指令即为
0 aload_0 <!-- 加载局部变量表中的this,this指向创建当前栈帧的cat对象 ->
1 invokespecial #2 <java/lang/Object.finalize : ()V> <!-- super.xxx(),通过super调用父类的xxx方法,这里即代表调用父类(Object)的finalize方法 ->
4 return