菜鸟笔记 -- Chapter 6.4.2 详解继承
6.4.2 详解继承
6.4.2.1 继承入门
继承使得程序架构具有一定的弹性,在程序中复用一些已经定义完善的类不仅可以减少软件开发周期,也可以提高软件的可维护性和可扩展性。基本思想是基于某个父类的扩展,制定出一个新的子类,子类可以继承父类原有的非私有的属性和方法,也可以增加原来父类所不具备的属性和方法,或者直接重写父类中的某些方法。在Java中使用extends关键字来标识两个类的继承关系,子类会自动得到基类中所有的域和方法,所有不存在选择性地继承父类。这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。
继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。继承可以理解为一个对象从另一个对象获取属性的过程。如果类A是类B的父类,而类B是类C的父类,我们也称C是A的子类,类C是从类A继承而来的。在Java中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类。继承中最常使用的两个关键字是extends(IS-A,是一个什么东西)和implements(Has-A,有一个什么功能)。这两个关键字的使用决定了一个对象和另一个对象是否是IS-A(是一个)关系。通过使用这两个关键字,我们能实现一个对象获取另一个对象的属性。所有Java的类均是由java.lang.Object类继承而来的,所以Object是所有类的祖先类,而除了Object外,所有类必须有一个父类。通过extends关键字可以申明一个类是继承另外一个类而来的,通过使用关键字extends,子类可以继承父类所有的方法和属性,但是无法使用 private(私有) 的方法和属性。我们通过使用instanceof 操作符,能够确定子类 IS-A 父类。
下面通过代码来看一下继承.
package cn.yourick.jicheng; public class ParentClass { private String name; String value; public ParentClass(String name, String value) { super(); this.name = name; this.value = value; System.out.println("name="+name+"----"+"value="+value); } public ParentClass() { super(); System.out.println("父类无参构造!"); } public void test(){ System.out.println("父类test()!"); } private void test2(){ System.out.println("父类私有test2()!"); } public void fun(){ System.out.println("父类fun()!"); } }
package cn.yourick.jicheng; public class SonClass extends ParentClass{ public static void main(String[] args) { SonClass sonClass = new SonClass(); sonClass.test(); } }
通过上面我们对继承有了一个简单的了解,继承就是在一个基本类上进行一个新类的创建和扩展。对于父类中的方法,我们除了继承外,还可以按照自己的思想进行重新编辑,也就是重写,下面我们来对继承的重载Overload进行一些探讨.
6.4.2.2 重写
重写是继承中非常重要的概念,重写也可以称之为覆盖.继承的特性如下:
- l 在子类中可以根据需要对基类中继承来的方法进行重写.
- l 重写的方法和被重写的方法必须具有相同的方法名和参数列表及返回值类型.这是因为当我们extends父类时,子类会自动得到基类中所有的域和方法,虽然子类中没有显示,但他们的确是存在的,我们如果不重写方法,那么类编译时,是会存在一个符号引用的,转变为直接引用后,指向父类的域和方法.如果重写后那么这个符号引用转变为直接引用后指向的是自己的,我们如果改变了返回值和参数列表,那么特征签名就改变了,此时将会存在两个直接引用一个指向父类的同名方法,一个指向自己的方法,此时更合适的称呼应该是重载.
- l 重写方法不能使用比被重写的方法更严格的访问权限.参考分层继承,如果权限一层层降低,那么分层继承就很难实现了.
下面我们通过修改上面的代码来实现一下重写;
package cn.yourick.jicheng; public class SonClass extends ParentClass{ public static void main(String[] args) { SonClass sonClass = new SonClass(); sonClass.test(); System.out.println(sonClass.fun("Youric")); } //方法的重写,要求从父类继承的方法,除了方法的功能可以有自己的实现外,其它的不能更改,如访问修饰符和返回值,参数列表 @Override public void test() { System.out.println("子类的test()!"); } //这是重载 public String fun(String name){ return name; } }
通过重写,可以使一个方法在子类中有不同的实现,这位多态提供了支持,可以根据需求调用将符合条件的子类对象传递给父类对象,然后实现特定功能.
我们在继承入门中写了父类的构造函数,在子类中实例化子类时,发现父类的构造函数被执行了,这又是怎么回事呢?下面我们针对继承中的构造函数来探讨一下;
6.4.2.3 继承中的子父类加载与初始化
我们知道Java是跨平台的,我们的源代码经过编译后生成.class文件。该文件只在需要使用程序代码的时候才会被加载,也可以说“类的代码在初次使用时才加载”,这通常是指加载发生在创建类的第一个对象时,但是当我们访问类的static成员时,也会发生加载。多说一句在Thinking In Java一书中说构造器是static方法,认真想了一下构造器不是static,static中是不能使用this的,但是构造器肿么明显可以,所以这个观点是悖论.
根据子类SonClass为例我们来分析,当我们执行main方法是,main是一个static,满足类加载的要求,此时加载器寻找到SonClass.class文件,然后发现存在父类ParentClass,那么会在加载子类之前首先加载父类,只是由继承的特性决定的,个人分析认为道理如下:子类继承了父类的成员,所以可能要对父类成员进行操作,所以我们要保证操作子类时,父类已经被加载到内存中了.
继承时分层次的,所以我们的父类可能还有父类,那么我们就得一层层的找找到找到顶层,然后从顶层开始加载类直到子类,这就是继承的类加载顺序.
类被加载完后就开始对象的创建了,我们都知道类是对象的载体,对象是类的一个特例,我们对类的操作都是基于对象的,对象创建的顺序和类的加载是一样的,都是先从顶层父类开始初始化的,下面我们通过代码来验证一下,我们在代码中同时添加代码块,查看加载的顺序;
package cn.yourick.jicheng; public class GrandFatherTest { String grandFatherName; int age; public GrandFatherTest(String grandFatherName, int age) { super(); this.grandFatherName = grandFatherName; this.age = age; System.out.println("GrandFatherName:"+grandFatherName); System.out.println("GrandFatherAge:"+age); } public GrandFatherTest() { super(); System.out.println("GrandFatherTest无参构造函数!"); } public void doSomeThing(){ { System.out.println("GrandFather普通代码块--doSomeThing()!"); } System.out.println("耕读传家!"); } public void smoking(){ { System.out.println("GrandFather普通代码块--smoking()!"); } System.out.println("抽烟!"); } private void health(){ System.out.println("some illness!"); } //代码块 { System.out.println("GrandFather构造代码块!"); } static{ System.out.println("GrandFather静态代码块!"); } }
package cn.yourick.jicheng; public class ParentTest extends GrandFatherTest{ String parentName; int age; private String otherthing; public ParentTest(String grandFatherName, int age, String parentName, int age2, String otherthing) { // super(grandFatherName, age); this.parentName = parentName; age = age2; this.otherthing = otherthing; System.out.println("ParentName:"+parentName); System.out.println("ParentAge:"+age); System.out.println("OtherThing:"+otherthing); } public ParentTest() { super(); System.out.println("ParentTest的无参构造!"); } // public ParentTest(String grandFatherName, int age) { // super(grandFatherName, age); // // TODO Auto-generated constructor stub // } @Override public void smoking() { { System.out.println("ParentTest普通代码块--smoking()!"); } System.out.println("不抽烟!"); } { System.out.println("ParentTest构造代码块!"); } static{ System.out.println("ParentTest静态代码块!"); } }
package cn.yourick.jicheng; public class SonTest extends ParentTest{ String sonName; int age; public SonTest(String grandFatherName, int age, String parentName, int age2, String otherthing, String sonName, int age3) { super(grandFatherName, age, parentName, age2, otherthing); this.sonName = sonName; age = age3; System.out.println("SonName:"+sonName); System.out.println("SonAge:"+age); } public SonTest() { System.out.println("SonTest的无参构造函数!"); } //重写父类方法--我们重写了该方法,子类中这个指向指向我们自己重写的方法 @Override public void smoking() { { System.out.println("SonTest普通代码块--smoking()!"); } System.out.println("SomeTimes little Smoking!"); } // //父类没有重写次方法,但父类中是有该方法的指向的, // @Override // public void doSomeThing() { // System.out.println("Java SoftWare Engineer!"); // } { System.out.println("SonTest构造代码块!"); } static{ System.out.println("SonTest的静态代码块!"); } }
package cn.yourick.jicheng; public class Test { public static void main(String[] args) { //我们调用SonTest的smoking方法 SonTest sonTest = new SonTest(); sonTest.smoking(); //调用doSomeThing方法 sonTest.doSomeThing(); } }
上面为执行的结果,我们来分析一下:
- l 当我们实例化SonTest时,此时进行类加载,根据继承的原则,类加载会先从顶层父类开始加载,这里的顶层父类是Object,然后是GrandFatherTest-->ParentTest-->SonTest,我们类加载时,static的类成员会首先被加载,所以此时发现顶层父类开始的静态依次被执行,类加载完成后,实例化SonTest,也是从顶层父类开始实例化,在实例化事前构造代码块先执行,然后才是调用构造函数进行实例化,至于调用什么构造函数,取决于底层调用构造函数的super指向.实例化完毕后,代码结果执行到了SonTest的无参构造函数,此时进行调用方法.
- l 调用方法的时候我们需要明白对象中是不会存储方法的代码块的,而是根据指向,从方法区(本地元数据存储中)调用,那么下面我们通过一幅图来看一下,继承和重写对方法的指向的影响.
根据上面的分析,我们知道了出现此种顺序的原因下面我们针对几个点进行一下备注:
- l 构造函数,我们在子类中创建构造函数时,会有如下的提醒,那么问自己一个问题构造函数是继承的吗,答案很显不是,继承要求方法名称一致,这一点就可以回答,那么构造函数是怎么会事呢?答案是调用,我们知道在初始化子类时会首先初始化父类,该调用就是为了确定初始化父类的时候是通过调用哪种构造函数进行初始化,关于关键字super,我们下节介绍.
6.4.2.4 this&&super
我们在开发中经常使用得两个调用实例的关键字this和super,它们两个都是调用对象,区别在于this是调用当前对象,而super是调用当前对象的父类对象,下面我们先分别探讨一下这两个关键字,最后再对比比较.
6.4.2.4.1 this
如果有同一个类型的两个对象,分别为demoA和demoB.那么我们怎么知道是哪个对象在调用方法呢?如:
demoA.method(1);
demoB.method(2);
为了能简便、面向对象的语法来编写代码--即“发送消息给对象”,编译器做了一些幕后的工作。它暗自把“所操作的对象的引用”作为第一个参数传递给方法,所以上面的两个对象的调用就变成了下面的样子:
demoA.method(demoA,1);
demoB.method(demoB,2);
但是如果你希望在方法的内部获得对当前对象的引用.由于这个引用是由编译器”偷偷”传入的,所以是没有标识符可用的.但是,为此有个专门的关键字:this。This关键字只在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同.但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可.当前方法中的this引用会自动应用于同一类中的其他方法.下面看一小段代码:
package cn.yourick.jicheng; public class DemoThis { String name = "Youric"; int age; public DemoThis() { System.out.println("无参构造函数!"); } public static void main(String[] args) { DemoThis demoThis = new DemoThis(); demoThis.method(26); System.out.println(demoThis.hashCode()); } public void method(int age){ // System.out.println(age+"--"+this.age); method("YouricYou"); this.method("YouricYou"); System.out.println("hashcode;"+this.hashCode()); } public void method(String name){ System.out.println(name); } }
上面我们看到demoTest对象调用,所以这里的this就是指demoTest,两次打印hashcode验证了这一点.但是我们往往在开发中不会显式的使用this调用,编译器会自动添加引用(这是高级语言的特性,能自动帮我们做一些事).所以只需要在需要明确引用的时候才使用this关键字,通常见于四个地方:
- l 方法中局部变量和成员变量名称相同,引用成员变量需要显式声明引用当前对象的成员变量;
- l 返回值是当前对象
- l 将当前对象作为参数传递给其它方法.
- l 在构造方法中调用当前的其它构造方法
下面通过代码来验证一下:
String name = "Youric"; demoThis.method("YouricName"); public void method(String name){ System.out.println(name); System.out.println("打印成员变量:"+this.name); }
public static void main(String[] args) { DemoThis demoThis = new DemoThis(); DemoThis demoThis2 = new DemoThis("YouricAge", 27); DemoThis demoThis3 = demoThis2.method();//返回的对象就是demoThis2对象 demoThis3.method("name"); System.out.println("demoThis2 == demoThis3:"+(demoThis2 == demoThis3)); } public void method(String name){ System.out.println(name); System.out.println("打印成员变量:"+this.name); } public DemoThis method(){ return this; }
public static void main(String[] args) { DemoThis demoThis = new DemoThis(); DemoThis demoThis2 = new DemoThis("YouricAge", 27); demoThis.method("name1"); demoThis.method(demoThis); } public void method(String name){ System.out.println(name); System.out.println("打印成员变量:"+this.name); } public void method(DemoThis demoThis){ this.method("name1"); }
String name = "Youric"; int age; public DemoThis() { //使用this调用构造函数必须放在构造函数的第一行 this("name",27); System.out.println("无参构造函数!"); } public DemoThis(String name, int age) { super(); this.name = name; this.age = age; System.out.println("name:"+name); } public static void main(String[] args) { DemoThis demoThis = new DemoThis(); demoThis.method("name2"); }
使用this在构造函数中调用构造函数可以在调用无参构造时,完成成员的初始化;使用this调用构造函数必须放在第一行;了解完this,下面我们了解一下super.
6.4.2.4.2 super
子类继承父类,可以访问父类中的方法,如果子类重写了父类中的方法,那么此时在子类中调用方法,如果没有特殊的操作,那么就是在调用子类自己重写的方法,如果我们此时仍然希望使用父类中的方法,此时该怎么做呢?Java中提供了一个关键字super用以代表当前对象的父类对象,编译器对其做的工作与this相同,都是将调用方法的对象默认作为方法的第一个参数,只不过this是传递的当前对象,super传递的是当前对象的父类对象.
通过super子类可以调用父类中的非私有实例变量,无论是子类中重写的还是隐藏的.根据调用我们将super分为两种引用:
- l 调用父类的实例成员;
- l 调用父类的构造函数
下面我们通过代码来看一下:
package cn.yourick.jicheng; import org.junit.Test; public class DemoSuper extends ParentDemo{ String name; int age; public DemoSuper(String name,int age) { super("you", 26);//子类中调用父类构造函数必须放在第一行 super.name = name; super.age = age; System.out.println("在构造函数中调用父类的有参构造!"); } public DemoSuper() { System.out.println("子类的无参构造!"); } public static void main(String[] args) { //测试方法1 DemoSuper demoSuper = new DemoSuper(); demoSuper.test(); DemoSuper demoSuper2 = new DemoSuper("YouricYou", 28); demoSuper2.test(); } public void test(){ DemoSuper demoSuper = new DemoSuper(); demoSuper.method("name1");//调用子类重写的方法 super.method("name1");//调用父类的方法 DemoSuper demoSuper2 = new DemoSuper("Youric", 27); System.out.println("输出父类的成员字段[name:"+super.name+"--age:"+super.age+"]"); } @Override public void method(String name) { System.out.println("name:"+name); } }
我们在继承的子父类加载和初始化中已经知道了初始化的顺序这里就不多做介绍了,看一个问题,那就是我们发现两次对象输出的不同,这是因为super实际上是this.super,第一次this是无参构造函数,第二次是有参构造函数,这个要分清.super同this一样不能使用与static方法中.简单认识了this和super,我们下边对比着总结一下.
6.4.2.4.3 this&&super
我们知道this和super有一些相同的功能,this用于指定当前对象,可以通过this调用当前对象的成员字段和方法,super指的是当前对象的父类对象,通过super可以调用当前对象的父类对象的字段和方法.它们都是只能用于方法中,也都可以在构造函数中调用其它构造函数,那么有什么要注意的呢?
- l This和super用于调用构造函数时,必须放在第一行{因为当前构造函数下面可能会用到其它构造函数中的一些初始化},这句话引申--同一个构造函数中不能即引用当前对象的其它构造函数,又引用其它父类的构造函数.{此时只能默认引用父类的无参构造函数}
- l This和super用来调用成员字段和成员方法就没有这种顾虑.
- l 子类构造函数中可以隐式调用父类的无参构造函数,但是调用父类的有参构造必须显式调用.注意我们在构造函数中显式调用无参构造,编译时会省略这一句;下面为两种情况下反编译后的结果,是一样的.
public DemoSuper() { System.out.println("\u5B50\u7C7B\u7684\u65E0\u53C2\u6784\u9020!"); }
如果使用finalize()方法对对象进行清理,需要确保子类的finalize()方法的最后一个动作是调用父类的finalize(),以保证当垃圾回收对象占用内存时,对象的所有部分都能被正常终止.