前言
李刚老师《JAVA疯狂讲义》第5版,第5章学习笔记。
1.什么是继承
继承是面向对象编程的三大特征之一,是实现代码复用的重要手段。
编程时,相同的一段代码最好只写一次。试想,比如有两个类,一个NBA球员类,一个湖人球员类(湖人总冠军!!!),NBA球员类里面定义了一个成员变量,叫球员底薪。我湖人球员类也不搞继承这一套,自己也定义一个成员变量,球员底薪。要是有一天,NBA的规则变了,球员底薪变了,那么,由于不存在继承关系,NBA球员类、湖人球员类里面的球员底薪都要手动的改,NBA一共30支球队的成员变量都要改,这无疑大大的增加了工作量,并且极易出错!因此,继承是必须的。
JAVA中的继承是单继承,也就是说,一个类只能有一个直接父类。但是,这并不是说一个类的父类只能有一个,可以有多个间接父类。比如:湖人首发球员类的直接父类是湖人球员类,湖人球员类的直接父类是NBA球员类,NBA球员类的父类是运动员类。那么湖人首发球员类就有NBA球员类、运动员类两个间接父类。
JAVA使用extends关键字作为继承的关键字:
public class LakersPlayer extends NBAplayer{
//类定义部分
}
JAVA中,java.lang.Object类是所有类的父类。一个类若未指定父类,则其默认为java.lang.Object的子类。
2.父类方法的重写
子类实际上是对父类的拓展,从父类到子类是从一般到特殊,因此在子类中,往往会增加新的成员变量或方法。子类中也会对父类的方法进行重写。
例如,NBAplayer这个类里面有个方法叫上篮,说明NBA球员都会上篮,其中PG(控球后卫)是一种特殊的NBA球员,因此PG也将从父类NBA球员中获得上篮的方法,但是PG会的上篮是更花里胡哨的花式上篮,就需要对从父类继承来的这个方法进行重写(Override),如下:
public class demo02 {
public static class NBAplayer{
//NBAplayer的上篮方法
public void layup(){
System.out.println("NBA球员的上篮方法");
}
}
public static class PointGuard extends NBAplayer{
//PG的花式上篮方法
public void layup(){
System.out.println("NBA控球后卫的花式上篮方法");
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
PointGuard Lebron = new PointGuard();
//执行控球后卫的layup()方法,输出:NBA控球后卫的花式上篮方法
Lebron.layup();
NBAplayer Kobe = new NBAplayer();
//执行NBA球员的layup()方法,输出:NBA球员的上篮方法
Kobe.layup();
}
}
子类重写父类的方法要遵守以下规则:
- 方法名、形参列表相同
- 子类方法返回值类型应小于等于父类方法返回值类型
- 子类方法抛出异常类型应小于等于父类方法抛出异常类型
- 子类方法访问权限应大于等于父类方法访问权限
- 两个方法要么都是类方法,要么都是实例方法,不可一个是类方法,一个是实例方法。
注意:
若父类中某个方法由private修饰,则该方法无法被子类访问,自然也无法被子类重写,即使子类中定义了一个方法名相同、形参列表相同、返回值类型相同的方法,依旧不是方法的重写,只是在子类中重新定义了一个方法。
3.super关键字
我们有时会遇见这样的需求,虽然子类中,对父类的方法进行了重写,但是子类中依旧需要调用父类中的方法。例如控球后卫是会花式上篮,但是有时候也需要正常的上个篮,这时就可以借助super关键字来实现这一功能。
super类似于this,只不过this指向调用该方法的对象,super指向调用该方法的父类对象。例如:
public static class PointGuard extends NBAplayer{
//PG的花式上篮方法
public void layup(){
System.out.println("NBA控球后卫的花式上篮方法");
}
//调用父类中的方法
public void normalLayup(){
super.layup();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
PointGuard Lebron = new PointGuard();
//执行控球后卫的normalLayup()方法,输出:NBA球员的上篮方法
Lebron.normalLayup();
}
不仅仅是方法,子类有可能定义和父类同名的实例变量,这个子类实例变量就会隐藏父类实例变量,这种情况下也可以借助super关键词调用被隐藏的父类实例变量。例如:
public class demo02 {
public static class BaseClass{
public int a = 5;
}
public static class SubClass extends BaseClass{
public int a = 7;
public void accessOwner() {
System.out.println(a);
}
public void accessBase() {
//接用了super关键字
System.out.println(super.a);
}
}
public static void main(String args[]) {
SubClass s = new SubClass();
//输出5
s.accessOwner();
//输出7
s.accessBase();
}
}
为什么会这样呢?
因为实际上,当JAVA程序创建一个子类对象时,系统不仅仅会为该子类中的成员变量分配内存空间,也会为其从所有父类(直接父类+间接父类)继承得到的实例变量分配内存空间,即使子类的成员变量和父类的成员变量同名。
例如:A有两个父类,直接父类B,间接父类C,A中定义了2个成员变量、B中定义了3个成员变量、C中定义了1个成员变量。则在定义一个A的对象时,系统实际上在内存中为6个(2+3+1)成员变量分配了内存空间。所以,被子类隐藏掉的父类的成员变量实际还是存在在内存中的,可以通过super关键词访问。
4.父类的构造器
JAVA中,子类不会直接获得父类的构造器,但是子类构造器可以调用父类构造器,类似于构造器的重载,例如:
public class demo02 {
public static class Base{
public double size;
public String name;
public Base(double size, String name) {
this.size = size;
this.name = name;
}
}
public static class Sub extends Base{
public String color;
public Sub(double size, String name, String color) {
//通过super调用父类的构造器
super(size,name);
this.color = color;
}
}
public static void main(String args[]) {
Sub s = new Sub(10,"测试对象","蓝色");
System.out.println(s.size);
System.out.println(s.name);
System.out.println(s.color);
}
}
在子类Sub中,借助super关键字,利用了父类Base的构造器。
但是,实际上,子类的构造器总会调用父类的构造器一次。子类调用父类的构造器分以下几种情况:
- 子类的构造器第一行,借助super调用父类构造器
- 子类的构造器第一行,借助this调用子类中重载的构造器,系统将调用本类中相应的构造器,此时,会调用父类的构造器
- 子类的构造器无super、this关键字进行调用,系统将会在该构造器执行前,隐式的调用父类无参构造器。
引申一步,子类构造器一定会调用父类的构造器,JAVA中,java.lang.Object是所有类的父类,因此,JAVA中,无论创建任何对象,肯定都会调用java.lang.Object的构造器。
为了更好的理解子类的构造器是如何调用父类的构造器的,请见下方代码:
public class demo02 {
public static class Creature{
public Creature(){
System.out.println("Creature的无参数构造器");
}
}
public static class Animal extends Creature{
public Animal(String name) {
System.out.println("Animal带一个参数的构造器,该动物的name为:"+name);
}
public Animal(String name,int age) {
this(name);
System.out.println("Animal带两个参数的构造器,该动物的age为:"+age);
}
}
public static class Wolf extends Animal{
public Wolf() {
super("大灰狼",3);
System.out.println("Wolf的无参构造器");
}
}
public static void main(String[] args) {
Wolf w = new Wolf();
}
}
这段代码的输出结果为:
Creature的无参数构造器
Animal带一个参数的构造器,该动物的name为:大灰狼
Animal带两个参数的构造器,该动物的age为:3
Wolf的无参构造器
为什么会这样呢?
因为JAVA中创建任何对象都总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后执行本类的构造器,如果某个父类中通过this调用了同类中重载的构造器,那就会依次执行此父类的多个构造器。
因此,本程序中,首先,创建Wolf对象,调用Wolf构造器:
super(“大灰狼”,3);
到了这里,就会去找Wolf的父类Animal,找Animal的带有两个参数的构造器,找到之后,遇见:
this(name);
就会接着找Animal中,带有一个参数的构造器。找到了之后,虽然这个构造器并未借助super引用父类的构造器,但其实会隐形的引用父类的构造器,因此首先第一行输出的是:
Creature的无参数构造器
然后跳转到Animal中,带有一个参数的构造器,输出:
Animal带一个参数的构造器,该动物的name为:大灰狼
然后跳转到Animal中,带有两个参数的构造器,输出:
Animal带两个参数的构造器,该动物的age为:3
最后跳转到Wolf()自己的构造器,输出:
Wolf的无参构造器