1.先看一下继承的基本定义:继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
特征:1.Java 中通过 extends 关键字可以申明一个类是从另外一个类继承而来的。
2.子类拥有父类非 private 的属性、方法。
3.子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
4.子类可以用自己的方式实现父类的方法。(方法的重写)
5.Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 A 类继承 B 类,B 类继承 C 类,所以按照关系就是 C 类是 B 类的父类,B 类是 A 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
问题 1 先看代码:
父类,Person private修饰的name,age属性,有参 和 无参构造函数
package com.newland.draw.extents;
public class Person {
public Person() {
}
public Person(String name) {
this.name = name;
}
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
子类:Young 私有属性hight,有参无参构造函数,注意:子类中的构造函数一定有super(),放在第一行,有时可以隐式表达,不显示出来。
package com.newland.draw.extents;
public class Young extends Person{
public Young() {
}
public Young(String name) {
super(name);
// TODO Auto-generated constructor stub
}
private int hight;
public int getHight() {
return hight;
}
public void setHight(int hight) {
this.hight = hight;
}
}
Client类,调用,执行 输出结果:2222
package com.newland.draw.extents;
public class Client {
public static void main(String[] args) {
Young young = new Young();
young.setName("2222");
System.out.println(young.getName());
}
}
上面可以看到,Young.java 继承 Person.java 并没有继承name属性,却可以调用get和set方法操作name。
注(后来经过查证,揭秘java虚拟机一书发现,父类私有的成员变量确实是被子类继承了,已经存在子类的内存空间,所以上面这句话是错的)
问题1,当我们new Young() 创建子类对象的实例时,父类对象也被创建了吗?
问题2,子类继承了父类的的public修饰的get set方法,操作成员变量 name,那么成员变量 name 是什么时候被加载到内存初始化的?
问题3,当子类继承父类的方法和变量时,是如何存在于内存中的?
解决问题1:网上找了很多资料,只能参考网上的资料,却没有权威的认证,让我很是痛苦。发现很多博客中写到当实例化子类对象的时候,父类对象也会被实例化。说实话,我对这种写法持怀疑态度,因为如果父类对象是抽象类的话,是没有办法实例化的。而且,如果父类对象被实例化,那么java中继承存在的意义就大打折扣了,所以我更倾向于父类没有被初始化,但是我们看到父类中的 name属性确实被赋值成功了,那么可以肯定父类中的属性被加载了 ,(猜测)应该是在 执行super()的时候加载的
2.解决问题2:找到了下面这段话:
关于类的初始化
初始化是类使用前的最后一个阶段,在初始化阶段Java虚拟机真正开始执行类中定义的Java程序代码。初始化:只会初始化与类相关的静态赋值语句和静态语句,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。如果一个类被直接引用,就会触发类的初始化。
表 4主动引用和被动引用
如果一个类被直接引用,而对象没有初始化时,就会触发类的初始化
初始化的过程其实就是一个执行类构造器<clint>方法的过程,类构造器执行的特点和注意事项:
(1)类构造器<clint>方法是由编译器自动收集类中所有类变量(静态非final变量)赋值动作和静态初始化块(static{……})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定。静态初始化块中只能访问到定义在它之前的类变量,定义在它之后的类变量,在前面的静态初始化中可以赋值,但是不能访问。
(2)类构造器<clint>方法与实例构造器<init>方法不同,它不需要显式地调用父类构造器方法,虚拟机会保证在调用子类构造器方法之前,父类的构造器<clinit>方法已经执行完毕。
(3)由于父类构造器<clint>方法先与子类构造器执行,因此父类中定义的静态初始化块要先于子类的类变量赋值操作。
(4) 类构造器<clint>方法对于类和接口并不是必须的,如果一个类中没有静态初始化块,也没有类变量赋值操作,则编译器可以不为该类生成类构造器<clint>方法。
(5)接口中不能使用静态初始化块,但可以有类变量赋值操作,因此接口与类一样都可以生成类构造器<clint>方法。接口与类不同的是:
首先,执行接口的类构造器<clint>方法时不需要先执行父接口的类构造器<clint>方法,只有当父接口中定义的静态变量被使用时,父接口才会被初始化。
其次,接口的实现类在初始化时同样不会执行接口的类构造器<clint>方法。
(6)Java虚拟机会保证一个类的<clint>方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,只会有一个线程去执行这个类的<clint>方法,其他线程都需要阻塞等待,直到活动线程执行<clint>方法完毕。
初始化阶段,当执行完类构造器<clint>方法之后,才会执行实例构造器的<init>方法,实例构造方法同样是按照先父类,后子类,先成员变量,后实例构造方法的顺序执行。
JVM在类初始化完成后,根据类的信息在堆区实例化类对象,初始化非静态变量和默认构造方法。(看到这句话忽然又觉得父类对象被实例化了,我也太难了。。。好吧,这个暂且保留,我更觉得父类的成员变量只是在执行实例构造器的时候被初始化了,父类的实例对象没有被创建。如果有人能确定可以留言)
解决问题3:
这是我找到唯一一个解答的内存图解:暂且相信它是对的吧,因为学习就是不断接收知识,并找验证其中的内容,寻求真理的过程:
当主函数和局部变量进栈后,开始创建对象,这时候Zi类就开始加载了。但是有继承的关系,所以应该是父类先进来。原因是子类在用父类的东西,父类不先进来,子类就用不了。
父类加载进来之后,有一个父类的构造函数(这点说明什么,即使没有创建,构造函数也是一直都存在的),在方法区的父类空间里,num=4,或者说num进不进来呢?父类加载之后,就是子类加载进来。
类加载进来后,就开始创建对象了。我们都知道对象中肯定有一个num=5,但是num=4在哪?如果它在方法区里面的话,它是不开辟空间,那怎么运算?我们都知道方法区里面存的都是代码,运算区都应该在栈堆里面。父类是没有对象的,所以父类是没有必要存储num的(怎么理解),其实是存在子类里面的,怎么存的呢?子一继承,就意味着它里面多了一个属性。
子类对象中的两个属性表现如下,先有的fu的num属性,后有的zi的num属性。再进行分别的初始化。
紧接着,初始化完毕,我们就把地址赋值给this,指向完毕以后,我们都知道成员变量一定存放在堆里面,堆里面就这么一个对象,那么两个num只能在这一个地方分空间存储,有自己表示。
当我们进行c.show()的时候,show()方法进栈了,show()有this所属,this是当前对象,就把地址编号拿过来,它就指向了当前对象。
打印的时候,执行的时候,输出this的num,就找到this的对象,里面有两个num,找谁?this是本类的,就找zi的。接下来,打印super当中的时候,这个子类正因为和加载完成的父类有关系,它有一个super引用,代表的是这个父类,代表的是父类这片空间。
父类过程当中,成员变量随着子类对象的建立,已经存储在了对象当中了,接下来打印super.num的时候,走的是父类的num,fu的那片空间。对象中有子和父区分的,不区分,调用存在不确定性,它们有自己的存储。
在这:给自己的解答打个60分,安慰一下自己受伤的心灵,
请看下面的代码:
package com.shi;
public class Person1 {
public void say() {
System.out.println("我是Person1 say()方法");
}
public void eat() {
System.out.println("我是Person1 eat()方法");
}
public void sleep() {
System.out.println("我是Person1 sleep()方法");
}
}
package com.shi;
public class Person2 extends Person1{
public void say() {
System.out.println("我是Person2 say()方法");
}
public void eat() {
System.out.println("我是Person2 eat()方法");
}
}
package com.shi;
public class Person3 extends Person2{
public void say() {
System.out.println("我是Person3 say()方法");
}
}
package com.shi;
public class Client {
public static void main(String[] args) {
Person1 p1 = new Person3();
p1.say();
p1.eat();
p1.sleep();
System.out.println();
Person2 p2 = new Person3();
p2.say();
p2.eat();
p2.sleep();
System.out.println();
Person3 p3 = new Person3();
p3.say();
p3.eat();
p3.sleep();
}
}
运行结果:
我是Person3 say()方法
我是Person2 eat()方法
我是Person1 sleep()方法
我是Person3 say()方法
我是Person2 eat()方法
我是Person1 sleep()方法
我是Person3 say()方法
我是Person2 eat()方法
我是Person1 sleep()方法
可以看到上面:person2继承person1,person3 继承person2 ,不管使用哪个类型去接收,调用方法都是一样,所有方法的调用还是看实例化的对象。
那么问题来了:say()方法和eat()方法被子类重写了,那么程序是如何找到正确的那个方法并执行?
1.这里就要引入另一东西了,静态分派,与动态分派 也 有人说是静态绑定和动态绑定。具体意思就是说,当我们加载类中的方法的时候,static ,private修饰的方法 只能本类的对象调用,在类加载的时候,这些都是已经确定的,实例对象确定执行哪些方法,这些方法叫做非虚方法,执行过程叫做静态分派。当存在方法的重写的时候,内存中存在多个同名的方法,jvm在调用方法是,需要确定执行子类或者父类的方法的过程 叫做动态分派。
2.动态分派的过程是如何执行的:看下面这个代码
public class DynamicTest {
static abstract class Car{
protected abstract void driveCar();
}
static class Train extends Car{
@Override
protected void driveCar() {
System.out.println("I am driving train");
}
}
static class Bus extends Car{
@Override
protected void driveCar() {
System.out.println("I am driving bus");
}
}
public static void main(String[] args) {
Car train = new Train();
Car bus = new Bus();
train.driveCar();
bus.driveCar();
}
}
结果显示
I am driving train
I am driving bus
分析过程
Car train = new Train();
Car bus = new Bus();
需要搞清楚,代码运行的时候,train与bus所指向的类型是什么,无外乎两种,一种是静态类型(Car),一种是实际类型(Train或Bus)。通过上面的结果,很明显,它们指向的是实际类型。不再根据静态类型来决定,那也就肯定不是编译期就做好的指定。接下面,反编译这个类,一探究竟。
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/erayt/DynamicTest$Tra
in
3: dup
4: invokespecial #3 // Method com/erayt/DynamicTest$Tr
ain."<init>":()V
7: astore_1
8: new #4 // class com/erayt/DynamicTest$Bus
11: dup
12: invokespecial #5 // Method com/erayt/DynamicTest$Bu
s."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method com/erayt/DynamicTest$Ca
r.driveCar:()V
20: aload_2
21: invokevirtual #6 // Method com/erayt/DynamicTest$Ca
r.driveCar:()V
24: return
LineNumberTable:
line 30: 0
line 31: 8
line 32: 16
line 33: 20
line 34: 24
}
dup:复制栈顶数值并将复制值压入栈顶
invokespecial:调用超类构造方法,实例初始化方法,私有方法
astore_1:将栈顶引用型数值存入第一个本地变量
astore_2:将栈顶引用型数值存入第二个本地变量
aload_1:将第二个引用类型本地变量推送至栈顶
aload_2:将第三个引用类型本地变量推送至栈顶
invokevirtual:调用实例方法
从反编译看,0-15行做的事情是建立train和bus的内存空间,调用Train和Bus类型的实例构造器,将这两个实例引用存放在局部变量表中的第1、2的slot槽中。对应的代码就是上面的两句new对象的过程。
接下来,16和20两句分别将刚创建的两个对象的引用压到栈顶,这两个对象是将要执行driveCar()方法的所有者,17和21句是方法调用指令,但是这两个指令最终执行的目标方法不行,就要说说invokevirtual指令的多态性查找过程了,它在运行的时候到底是如何解析的。
invokevirtual指令的多态性查找过程
1.找到操作数栈顶的第一个元素所指向的实际类型(上面的16和20两句分别将刚创建的两个对象的引用压到栈顶)
2.如果在这个实际类型中找到与常量中的描述符号和简单名称都相符的方法,则进行权限校验,通过则返回这个方法的直接引用,查找结束,不通过,报错,返回java.lang.IllegalAccessError异常
3.如果在这个实际类型中没有找到与常量中的描述符号和简单名称都相符的方法,就按照继承关系从上到下依次对该实际类型的各个父类进行第2步的搜索和验证(从上到下不清楚是从最顶层父类,还是直接继承的父类,感觉是直接继承的父类)
4.如果始终没有找到合适的方法,抛出错误java.lang.AbstractMethodError异常