anonymous的Java学习笔记(10)——Java面向对象之继承

[toc]

类的继承

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java的继承具有单继承的特点,每个子类只有一个直接父类。

继承的特点

Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类,有的也称其为基类超类

子类继承父类的语法

修饰符 class SubClass extends SuperClass
{
    //子类的代码块    
}
  • Javaextends作为继承的关键宇,extends关键字在英文中是扩展,而不是继承。这个关键字很体现了子类父类的关系:子类是对父类的扩展,子类是一种特殊的父类。
  • Java子类不能通过继承获得父类的构造器。

代码示例

父类(水果类)——Fruit.java

package com.abc.part4;

/**
 * 父类,水果类
 *
 * @author mi
 */
public class Fruit {
    public double weight;

    public void info() {
        System.out.println("我是一个水果, 重" + weight + "kg。");
    }
}

子类(梨类)——Pear.java

package com.abc.part4;

/**
 * 子类:梨类,父类是水果类
 *
 * @author mi
 */
public class Pear extends Fruit {
    public static void main(String[] args) {
        Pear pear = new Pear();
        pear.weight = 0.3;
        //我是一个水果, 重0.3kg。
        pear.info();
    }
}

类继承特点总结

  1. Java语言摒弃了C++中难以理解的多继承特征,即每个类最多只有一个直接父类
  2. 如果定义一个Java并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此,java.lang.Object类是所有类的父类。要么是其直接父类,要么是其间接父类,因此所有Java对象都可调用java.lang.Object类所定义的实例方法。
  3. 从子类角度来看,子类扩展(extends)了父类;但从父类的角度来看,父类派生(derive)出了子类。也就是说,扩展和派生所描述的是同一个动作,只是观察角度不同而己。

重写父类的方法

子类扩展了父类,子类是一个特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的员变量和方法。

但有一种情况例外:子类需要重写父类的方法。例如鸟类都包含了飞翔方法,其中驼鸟是一种特殊的鸟类,因此驼鸟应该是鸟的子类,因此它也将从鸟类获得飞翔方法,但这个飞翔方法明显不适合驼鸟,为此,驼鸟需要重写鸟类的方法。

代码示例

父类(鸟类)——Bird.java

package com.abc.part4;

/**
 * @Auther: ABC
 * @Date: 2020/5/12 22:05
 * @Description: 父类——Bird类
 */
public class Bird {
    public void fly() {
        System.out.println("我会飞!");
    }
}

子类(鸵鸟类)——Ostrich.java

package com.abc.part4;

/**
 * @Auther: ABC
 * @Date: 2020/5/12 22:07
 * @Description:
 */
public class Ostrich extends Bird {
    public void fly() {
        System.out.println("我是一只鸵鸟,我不会飞,只能在地上跑!");
    }

    public static void main(String[] args) {
        Ostrich ostrich = new Ostrich();
        /*
        执行的不再是父类Bird类的fly()方法,而是执行Ostrich类的fly()方法。
        这种子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。
         */
        //输出:我是一只鸵鸟,我不会飞,只能在地上跑!
        ostrich.fly();
    }
}

两同两小一大规则

方法的重写要遵循两同两小一大规则

  • 两同
    • 方法名相同
    • 形参列表相同
  • 两小
    • 子类方法返回值类型应比父类方法返回值类型更小或相等
    • 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等
  • 一大
    • 子类方法的访问权限应比父类方法的访问权限更大或相等
  • 补充一点:覆盖方法被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。

  • 如果需要在子类方法中调用父类中被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。
  • 如果父类方法具有private访问权限,则该方法对其子类隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。
  • 如果子类中定义了一个与父类private方法具有相同的方法名相同的形参列表相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。

方法重载和方法重写

  • 方法重载方法重写在英语中分别是overloadoverride
  • 重载重写放在一起比较本身没有太大的意义,因为重载主要发生在同一个类多个同名方法之间,而重写发生在子类父类同名方法之间。它们之间的联系很少,除二者者都是发生在方法之间,并要求方法名相同之外,没有太大的相似之处。
  • 子类继承自父类子类会获得父类方法,如果子类定义了一个与父类方法有相同的方法名,但参数列表不同的方法,就会形成父类方法子类方法重载

super限定的使用

如果需要在子类方法中调用父类被覆盖的实例方法,则可使用super限定来调用父类被覆盖的实例方法

代码示例

package com.abc.part4;

/**
 * @Auther: ABC
 * @Date: 2020/5/12 22:07
 * @Description:
 */
public class Ostrich extends Bird {
    public void fly() {
        System.out.println("我是一只鸵鸟,我不会飞,只能在地上跑!");
    }

    public void callOverrideMethod() {
        //在子类中通过super显示调用父类中被覆盖的实例方法。
        super.fly();
    }

    public static void main(String[] args) {
        Ostrich ostrich = new Ostrich();
        /*
        执行的不再是父类Bird类的fly()方法,而是执行Ostrich类的fly()方法。
        这种子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。
         */
        //输出:我是一只鸵鸟,我不会飞,只能在地上跑!
        ostrich.fly();
    }
}

通过super关键字来访问父类中被覆盖的实例变量/实例方法

  • super用于限定该对象调用它从父类继承得到的实例变量方法
  • this不能出现在static修饰的方法中一样,super也不能出现在static修饰的方法中。
  • 如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量
  • 如果子类定义了和父类同名的实例变量,则会发生子类实例变量覆盖父类实例变量的情形。在子类定义的实例方法中可以通过super来访问父类中被覆盖的实例变量

代码示例
父类——BaseClass.java

package com.abc.part4;

/**
 * @Auther: ABC
 * @Date: 2020/5/12 22:49
 * @Description:
 */
public class BaseClass {
    public String name = "大花花";
    public int age = 21;

    public void info(String sportName) {
        System.out.println("我叫大花花, 我喜欢" + sportName);
    }
}

子类——SubClass.java

package com.abc.part4;

import sun.java2d.Surface;

/**
 * @Auther: ABC
 * @Date: 2020/5/12 22:50
 * @Description:
 */
public class SubClass extends BaseClass {
    public String name = "小花花";
    public int age = 18;

    public void accessOwner() {
        //输出:我叫小花花, 我的年龄是:18
        System.out.println("我叫" + name + ", 我的年龄是:" + age);
    }

    public void accessBase() {
        /*
        通过super调用父类BaseClass的实例属性:name和age
         */
        //输出:我姐姐叫大花花, 我姐姐的年龄是21
        System.out.println("我姐姐叫" + super.name + ", 我姐姐的年龄是" + super.age);
    }

    public void info(String sportName) {
        System.out.println("我叫小花花, 我喜欢" + sportName);
    }

    public void testInfo(String sportName) {
        //通过super调用父类BaseClass的实例方法:info
        super.info(sportName);
        //调用子类SubClass的实例方法:info
        info(sportName);
    }

    public static void main(String[] args) {
        SubClass subClass = new SubClass();
        //输出:我叫小花花, 我的年龄是:18
        subClass.accessOwner();
        //输出:我姐姐叫大花花, 我姐姐的年龄是21
        subClass.accessBase();
        /*
        输出:
        我叫大花花, 我喜欢踢足球
        我叫小花花, 我喜欢踢足球
         */
        subClass.testInfo("踢足球");
    }
}

如果子类里没有包含和父类同名的成员变量,那么在子类实例方法中访问该成员变量时,则无须显式使用super父类名作为调用者。如果在某个方法中访问名为a的成员变量,但没有显式指定调用者,则系统查找a的顺序为:

  1. 查找该方法中是否有名为a的局部变量。
  2. 查找当前类中是否包含名为a的成员变量。
  3. 查找a的直接父类中是否包含名为a的成员变量,依次上溯a的所有父类,直到java.lang.Object类,如果最终不能找到名为a的成员变量,则系统出现编译错误。

通过父类名访问父类中被覆盖的类变量/类方法

代码示例

父类——BaseClass1.java

package com.abc.part4;

/**
 * @Auther: ABC
 * @Date: 2020/5/12 23:56
 * @Description:
 */
public class BaseClass1 {
    public static int a = 10;

    public static void info(String b) {
        System.out.println("BaseClass1 info:" + b);
    }
}

子类——SubClass1.java

package com.abc.part4;

/**
 * @Auther: ABC
 * @Date: 2020/5/12 23:56
 * @Description:
 */
public class SubClass1 extends BaseClass1 {
    public static int a = 5;

    public static void info(String b) {
        System.out.println("SubClass1 a:" + a);
        System.out.println("BaseClass1 a:" + BaseClass1.a);
        System.out.println("SubClass1 info:" + b);
        BaseClass1.info(b);
    }

}

测试类——SubClass1Test.java

package com.abc.part4;

import javax.sound.midi.Soundbank;

/**
 * @Auther: ABC
 * @Date: 2020/5/12 23:59
 * @Description:
 */
public class SubClass1Test {
    public static void main(String[] args) {
        /*
        输出:
        SubClass1 a:5
        BaseClass1 a:10
        SubClass1 info:哈哈
        BaseClass1 info:哈哈
         */
        SubClass1.info("哈哈");
    }

}

当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。

也就是说,当系统创建一个Java对象时,如果该Java类有两个父类(一个直接父类A,一个间接父类B),假设A类中定义了2个实例变量,B类中定义了3个实例变量,当前类中定义了2个实例变量,那么这个Java对象将会保存2+3+2个实例变量。

使用super调用父类的构造器

子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码。

  • 在同一个类中,在一个构造器中调用另一个重载的构造器使用this调用来完成。
  • 在子类构造器中调用父类的构造器使用super调用来完成。

代码示例

父类——Base.java

package com.abc.part4;

/**
 * @Auther: ABC
 * @Date: 2020/5/13 00:17
 * @Description:
 */
public class Base {
    public String name;
    public int age;

    public Base(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

子类——Sub.java

package com.abc.part4;

import javax.sound.midi.Soundbank;

/**
 * @Auther: ABC
 * @Date: 2020/5/13 00:17
 * @Description:
 */
public class Sub extends Base {
    public char gender;

    public Sub(String name, int age, char gender) {
        /**
         * 父类的构造方法不能继承,但我们可以用super来调用。
         * 如父类的构造方法为:public A(){};那么子类调用为super();
         * 如果为public A(int a);那么子类调用为super(1);
         */
        super(name, age);
        this.gender = gender;
    }

    public static void main(String[] args) {
        Sub sub = new Sub("小花花", 21, '女');
        //输出:我的名字叫小花花, 我今年21岁了, 我的性别是女。
        System.out.println("我的名字叫" + sub.name + ", 我今年" + sub.age + "岁了, 我的性别是" + sub.gender + "。");
    }
}

结论

从上面程序中不难看出,使用super调用和使用this调用也很像,区别在于super调用的是其父类的构造器,this调用的是同一个类中重载的构造器。

因此,使用super调用父类构造器也必须出现在子类构造器执行体的第一行,所以this调用和super调用不会同时出现。

  • 父类的构造方法不能继承,但我们可以用super来调用。
  • 如父类的构造方法为:public A(){};那么子类调用为super();
  • 如果为public A(int a);那么子类调用为super(a);
  • 如果为public A(int a, String b);那么子类调用为super(a, b);

不管是否使用super调用来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次。

子类构造器调用父类构造器分如下几种情况:

  1. 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器。
  2. 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this调用里传入的实参列表调用本类中的另一个构造器。执行本类中另一个构造器时即会调用父类构造器。
  3. 子类构造器执行体中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器
  • 不管上面哪种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;
  • 不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器……依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器。

创建任何对象总是从该类所在的继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行到本类的构造器。

使用继承的注意点

为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则。

  • 尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置成private访问类型,不要让子类直接访问父类的成员变量
  • 不要让子类可以随意访问、修改父类的方法,父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法。
    • 如果父类中的方法需要被外部类调用,则必须以public修饰。但又不希望子类重写该方法,可以使用final修饰符(该修饰符后面会有更详细的介绍)来修饰该方法。
    • 如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。
    • 尽量不要在父类构造器中调用将要被子类重写的方法。

什么时候开始使用继承

  1. 子类需要额外增加属性,而不仅仅是属性值的改变。例如从Person类派生出Student子类,Person类里没有提供grade(年级)属性,而Student类需要grade属性来保存Student对象就读的年级,这种父类到子类的派生,就符合Java继承的前提。
  2. 子类需要增加自己独有的行为方式(包括增加新的方法或重写父类的方法)。例如从Person派生出Teacher,其中Teacher类需要增加teaching()方法,该方法用于描述Teacher对象独有的行为方式:教学。
  3. 继承代码示例
package com.abc.part4;

/**
 * @author mi
 */
public class Animal {
    private void beat() {
        System.out.println("心脏跳动...");
    }

    public void breath() {
        beat();
        System.out.println("呼吸中...");
    }
}
package com.abc.part4;

/**
 * @author mi
 */
public class Bird1 extends Animal {
    public void fly() {
        System.out.println("鸟在飞翔...");
    }

}
package com.abc.part4;

/**
 * @author mi
 */
public class Wolf extends Animal{
    public void run() {
        System.out.println("狼在奔跑...");
    }

}
package com.abc.part4;

/**
 * @author mi
 */
public class AnimalTest {
    public static void main(String[] args) {
        Bird1 bird1 = new Bird1();
        bird1.breath();
        bird1.fly();

        System.out.println("**********************分割线**********************");

        Wolf wolf = new Wolf();
        wolf.breath();
        wolf.run();
        /**
        输出:
        心脏跳动...
        呼吸中...
        鸟在飞翔...
        **********************分割线**********************
        心脏跳动...
        呼吸中...
        狼在奔跑...
         */
    }
}