目录
1). 子类对象实例化过程中调用父类构造方法---- this的指向
2). 子类对象调用从父类继承的方法中---- this的指向
1.继承
1). 继承的优势
减少代码的冗余,提高了代码的复用性,便于功能的扩展,为多态的使用提供了前提。
2). Java中继承的特点
[1]. 和C++相比,Java只支持单继承;
[2]. 为了支持类似C++中的多继承的机制,Java采用多实现(接口)的方式来弥补单继承。
3). 继承子类中关键字
[1]. this代表本类对象的引用
[2]. super代表父类对象的引用
4). 子类、父类成员(变量/方法)的特点
[1]. 子类、父类的成员变量全部存放在子类的内存区域;
[2]. 子类和父类出现同名非私有成员时:
{1}. 子类方法内部要访问本类中的该成员,使用关键字this;
{2}. 子类方法内部要访问到从父类继承的该成员,使用的是关键字super。
[3]. 对于父类的私有成员, 子类无法直接访问到;
[4]. 如果子类中的某变量没有在父类中出现过,使用this和super访问到的都是子类成员变量。
5).子类、父类成员变量的关系
class Fu{
int num =4;
void hah(){}
}
class Zi extends Fu{
int num =5;
void show(){
System.out.println("super toString:"+ super.toString());
System.out.println("super toString:"+ this.toString());
System.out.println("super num:"+ super.num);
System.out.println("this num:"+ this.num);
}
}
class Test{
public static void main(String[] args) {
Zi zi =new Zi();
zi.show();
}
}
运行结果:
super 和this 的toString() 打印值都是 Zi@150bd4d,即这两个引用是指向堆内存中同一个地址,说明,Fu类构建的成员变量全部都存放在子类的堆内存区域中。
分析一下内存变化:
[1]. 执行Zi zi =new Zi(); 使用到了Zi的构造函数,Zi.class需要马上进入内存。当装载Zi.class进入内存的方法区的时候,发现Zi extends Fu(调用Zi的构造会先去执行Fu的构造,这样就是使用了Fu的构造方法,所以Fu的类文件也要加载到内存) => 此时Fu.class也被加载到内存的方法区。
【知识点】加载类到内存具体指的是将.class文件等具体信息全部加载到Java内存中的方法区。因此,Fu和Zi全部加载到内存之后,两个类的函数成员的代码存储到方法区中,以供JVM调用。
[2]. 子类、父类的成员变量全部存放在子类的内存区域。
【PS】:继承的含义字面理解就是:
{1}. 儿子从父亲的那边继承父亲的财产 (Java中父类的成员变量相当于财产);
{2}. 儿子从父亲的那边学会父亲做事的方法 (Java中父类的成员函数相当于父亲办事的方法);
{3}. 继承体现的是:财产和做事方法的传承,而不是父亲这个实体被儿子据为己有;
{4}. 继承之后,儿子就可以把父亲类的财产和方法据为己有,所以有上面的结论: 子类、父类的成员变量全部存放在子类的内存区域。
6). 子类重写 (覆盖) 父类中方法
(1). 何时重写
[1]. 当子类继承了父类,子类就具备了父类的所有的非私有方法;
[2]. 当子类具备和父类相同功能的函数,但是功能的实现却和父类中该功能的实现不一样的时候,没有必要在子类中定义新的功能,而是重写这个功能的实现。
PS:子类继承=生物遗传 子类重写=生物变异 子类添加=生物变异
(2). 重写条件
[1]. 重写概念:子类中的函数名字、参数列表和父类的某个非私有方法具有相同的函数名字、参数列表的时候,Java编译器会把子类这个同名同参数的方法作为父类对应方法的重写。
[2]. 重写条件:
{1}. 子类和父类方法的函数名字、参数列表相同,返回值类型一致;
{2}. 子类权限修饰符 >=父类权限修饰符 (子类和父类中对应的方法都不能出现private);
{3}. 子类函数抛出的异常类型必须是父类函数抛出来的异常类型的子类或者子集;
{4}.子类中的静态方法只能覆盖父类中的静态方法。
(3). 重写、重载区别
[1]. 重载:子类具有与父类相同的函数名、不相同的参数列表;
[2]. 重写:子类具有与父类相同的函数名、相同的参数列表,并满足上述重写条件 (否则编译报错)。
2.子类对象的实例化过程
1). 子类能否覆盖父类的构造方法
构造函数的名字是和类名是一样的,子类和父类类名不相同,不满足子类父类之间的函数重载的充分条件,所以子类不能覆盖父类的构造函数。
2). 隐式super语句和显式super语句
[1]. 子类的构造函数中的第一行如果没有通过super语句显式调用父类构造函数的时候,JVM在执行的时候,会默认为子类的该构造函数的第一行加上代表空参数的父类构造函数的super()语句;
【结论】子类的构造方法的第一行默认是super()。
[2]. 由于子类的构造函数的第一行默认是super(),这要求父类的构造方法必须有相应的空参构造函数,如果父类没有相对应的空参构造函数,javac就会报错;
[3]. 当父类中没有空参构造函数的时候,子类的构造方法必须通过super(xx)语句指定要调用父类的哪一个构造函数来对自己进行初始化。
【例1:子类调用默认的父类空参构造函数】
class Fu{
Fu(){
System.out.println("Fu runs");
}
Fu(int x){
System.out.println("Fu runs..."+ x);
}
}
class Zi extends Fu{
Zi(){
System.out.println("Zi runs");//没有指定,默认调用super();
}
Zi(int x){
System.out.println("Zi runs..."+ x); //没有指定,默认调用super();
}
}
class Test{
public static void main(String[] args) {
Zi z =new Zi();
System.out.println("*****");
Zi zX =new Zi(4);
}
}
运行结果:
【例2:子类调用指定的父类构造函数】
class Fu{
Fu(int x){
System.out.println("Fu runs..."+ x);
}
}
class Zi extends Fu{
Zi(){
super(3);
System.out.println("Zi runs");
}
Zi(int x){
super(x);
System.out.println("Zi runs..."+ x);
}
}
class Test{
public static void main(String[] args) {
Zi z =new Zi();
System.out.println("*****");
Zi zX =new Zi(4);
}
}
由于Fu类没有空参构造函数,所以继承自这个类的子类必须在子类的构造方法中的第一行指定要调用的父类的构造函数。
运行结果:
3).子类对象初始化过程
调用子类构造方法之后,运行到子类构造函数时执行顺序如下:
(1). 父类静态相关初始化(仅执行一次)
{1}. 父类的静态成员显式初始化(仅执行一次)
{2}. 父类的静态成员隐式初始化(仅执行一次)
PS:静态成员也是子类继承的对象
{3}. 父类static代码块(仅执行一次)
PS:执行的原理就是静态代码块可能初始化子类要继承的父类的静态变量
(2). 子类静态相关初始化(仅执行一次)
{1}. 子类的静态成员初始化(仅执行一次)
{2}. 子类的静态成员初始化(仅执行一次)
{3}. 子类static代码块(仅执行一次)
PS:静态先执行的原因就是随着相应的类加载而加载,并且静态代码块没有名字,不能被调用,所以也要立刻执行
(3). 父类非静态相关初始化
{1}. 父类成员变量隐式初始化
{2}. 父类成员变量显式初始化
{3}. 父类构造代码块
PS:这步可能涉及到父类成员变量的初始化,所以要执行
{4}. 父类构造方法初始化
PS:执行这步的原理同上
(4). 子类非静态相关初始化
{1}. 子类成员变量隐式初始化
{2}. 子类成员变量显式初始化
{3}. 子类构造代码块
{4}. 子类构造方法初始化
【总结】子类对象初始化的核心就是:
只要是涉及到子类自身的数据(静态或者非静态)以及需要从父类继承的数据(静态或者非静态)的初始化动作,就一定要执行。
代码示例:
package com;
class Fu{
int fuInt;
String fuStr;
static int fuStaticInt;
static String fuStaticStr;
static{
System.out.println(">>>>>>fu static code block fuStaticInt="+ fuStaticInt +", fuStaticStr="+ fuStaticStr);
}
{
System.out.println(">>>>>>fu code block fuInt="+ fuInt +", fuStr="+ fuStr+", fuStaticInt="+ fuStaticInt +", fuStaticStr="+ fuStaticStr);
}
Fu(){
fuInt =10;
fuStr ="fuStr";
fuStaticInt =20;
fuStaticStr ="fuStaticStr";
System.out.println(">>>>>>fu constructor fuInt="+ fuInt +", fuStr="+ fuStr+", fuStaticInt="+ fuStaticInt +", fuStaticStr="+ fuStaticStr);
}
Fu(int fuInt, String fuStr, int fuStaticInt, String fuStaticStr){
this.fuInt = fuInt;
this.fuStr = fuStr;
Fu.fuStaticInt = fuStaticInt;
Fu.fuStaticStr = fuStaticStr;
System.out.println(">>>>>>fu constructor fuInt="+ fuInt +", fuStr="+ fuStr+", fuStaticInt="+ fuStaticInt +", fuStaticStr="+ fuStaticStr);
}
}
class Zi extends Fu{
int ziInt;
String ziStr;
static int ziStaticInt;
static String ziStaticStr;
static{
System.out.println(">>>>>>zi static code block ziStaticInt="+ ziStaticInt +", ziStaticStr="+ ziStaticStr);
}
{
System.out.println(">>>>>>zi code block ziInt="+ ziInt +", ziStr="+ ziStr+", ziStaticInt="+ ziStaticInt +", ziStaticStr="+ ziStaticStr);
}
Zi(){
ziInt =30;
ziStr ="ziStr";
ziStaticInt =40;
ziStaticStr ="ziStaticStr";
System.out.println(">>>>>>zi constructor ziInt="+ ziInt +", ziStr="+ ziStr+", ziStaticInt="+ ziStaticInt +", ziStaticStr="+ ziStaticStr);
}
Zi(int ziInt, String ziStr, int ziStaticInt, String ziStaticStr){
this.ziInt =ziInt;
this.ziStr =ziStr;
Zi.ziStaticInt = ziStaticInt;
Zi.ziStaticStr = ziStaticStr;
System.out.println(">>>>>>zi constructor ziInt="+ ziInt +", ziStr="+ ziStr+", ziStaticInt="+ ziStaticInt +", ziStaticStr="+ ziStaticStr);
}
}
class T {
public static void main(String[] args) {
Zi zi =new Zi();
System.out.println("*****************************");
Zi ziII =new Zi(666, "Ultraman Str", 999, "Ultraman StaticStr");
}
}
运行输出(不再赘述,请对照上述讲解梳理):
4).类构造方法必须访问父类构造方法的原因
{1}. 原因:子类继承父类,那么子类就必须要拥有父类的所有财产(父类的成员变量和成员方法)。
{2}. 成员方法的代码全部存放在方法区内存中。
{3}. 成员变量的值因不同的构造函数而使得有不同的值,所以存放到堆内存中。
{4}. 过程:在构造每一个子类对象的时候,首先要去它的父类中查看一下它的父类有哪些成员变量可以拿来继承。这些成员变量的取值会因为初始化使用不同的父类构造函数而不同, 所以一定要在子类的构造方法中指定要调用的父类的构造方法,以便得到自己的要继承的成员变量及其值。
5).this 和 super 的含义
【1】. super语句
{1}. super语句和this语句含义一样,是调用父类指定的构造方法来初始化子类需要从父类继承的成员变量。
{2}. super语句绝对不是因为要初始化父类的成员变量而来构建父类的对象,调用super语句并没有构建父类对象,仅仅是由于构造函数的目的在于为成员变量进行初始化。
***没有关键字new,就绝对不会有对象产生***
【2】. super关键字
子类对象被创建之后,就可以通过this关键字和super关键字来区分哪些财产是从父类得来,哪些财产是自己的。
6). 使用super语句的注意事项
[1]. super() 语句必须放在子类构造函数的第一行;
即先使用父类的内容(代码复用),如果不喜欢父类的内容,在super() 语句之后再修改或者增加新的内容,非常类似生物学中的遗传和变异。
[2]. 一个子类的构造函数中,只能有super()语句或者this()语句,两者不能同时存在;
{1}.super() 为了获取父类的财产,初始化父类的成员变量,继承老的东西,放在第一行;
{2}.this() 为了在本次构造中进行更细致的构造,放在第一行;
如果一个构造方法中,出现了super() 语句放在第一行,那么要求也在第一行的this()就不能出现,反之亦然。
[3]. 子类中至少有一个子类构造函数能访问父类的构造函数。
例:
class Fu{
Fu(){}
Fu(int x){
System.out.println(x);
}
}
class Zi extends Fu{
Zi(){}
Zi(int x){
this();
System.out.println("x="+ x);
}
}
{1}. 这个构造函数的第一行是this(),那么这个构造方法中就没有super()语句了。
{2}.由于这个构造函数调用了this(),指向的是Zi(){},Zi(){}这个构造函数的第一行没有this语句,所以JVM会为这个构造函数加上一个super(),这样构造函数Zi(int x)通过调用Zi()达到了访问父类构造函数的目的。
7). 总结super语句
【1】. 子类的所有构造函数都会默认访问父类空参的构造函数;
【2】. 父类没有空参数构造函数时,必须手动为子类构造函数指定要调用的父类构造函数;
【3】. 当子类构造函数形成重载的情况时,如果某一个构造函数的第一行被this()语句占用,则这个子类的构造函数中必须保证至少有一个构造函数能够访问父类的构造函数。
PS:父类的构造函数中有super()语句么?有!这个super()语句是Object的默认空参构造函数 Object(){…..}
3.子类this的真实指向
1). 子类对象实例化过程中调用父类构造方法---- this的指向
(1). 误区:认为实例化的过程中,调用父类的构造也同时实例化了父类实例
【注意】子类通过自身构造方法实例化过程中调用父类的构造方法,实际上是通过父类的构造方法对父类的财产“成员变量”进行特有的初始化之后,再来继承父类的特有财产“成员变量”,绝对不是实例化子类对象之后,又同时实例化相应的父类对象!
(2). 进行验证
class Fu {
public Fu(){
System.out.println(this);
}
public void showName(){
System.out.println("Fu..." +this);
}
}
class Zi extends Fu{
public Zi(){
System.out.println(this);
}
public void showNameII(){
this.showName();
super.showName();
}
}
public class Test{
public static void main(String[] args){
Zi zi =new Zi();
zi.showNameII();
}
}
执行 Zi zi =new Zi(); 时,父类的构造中this指向的是Zi@834e7 是子类的实例,说明子类调用父类的构造方法,在父类中的方法的内部this仍然是子类的this,而不是父类的this。
2). 子类对象调用从父类继承的方法中---- this的指向
(1). 此时this同样是指向子类实例对象
由于构造子类实例的时候,仅仅是调用父类的构造并没有进行父类实例化,所以子类调用从父类继承的方法时候,在这个方法的内部this同样也是指向子类的实例。
(2). 进行验证
执行到zi.showNameII() 的时候,在Zi类的showNameII() 中又调用了从父类继承的showName()方法,进入到Fu的showName()代码中的时候,this显示的值是Zi@834e7这个值,证明Fu中的方法被子类调用的时候,this仍然指向的Zi的实例。
每天进步一点点,与君共勉!