目录

Java 继承与多态

1. 继承

继承是父类与子类之间的关系是 is-a 关系,也就是说,继承是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。通过使用继承,可以快速地创建新的类,提高代码的重用。

下面定义父类为 Person,子类为 Student

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person {

    /** 名字和年龄 */
    protected String name;
    protected int age;

    /**
     * Person 类构造方法
     * @param name 名字
     * @param age 年龄
     */
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Student extends Person {

    /** 分数 */
    protected int score;

    /**
     * Student 类构造方法
     * @param name 名字
     * @param age 年龄
     * @param score 分数
     */
    public Student(String name, int age, int score) {
        super(name, age);
        this.score = score;
    }
}

通过 super(name, age); 可以调用父类构造方法 public Person(String name, int age),其中 super 关键字表示父类(超类),在子类引用父类属性、方法或构造方法时使用。例子中,子类继承了父类的 nameage 属性。

注意
  • 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。

  • Java 的继承是单继承,即一个子类只能继承一个父类,但是可以多重继承,如 A 类继承 B 类,B 类继承 C 类。

  • protected 允许子类访问父类的字段和方法。若父类方法定义为 protected,子类覆写该方法时必须为 protectedpublic

  • 子类的构造方法可以通过 super() 调用父类的构造方法。注意,与 this() 一样需要写在构造方法首行,如果父类没有自定义有参构造方法且子类构造方法首行没有写出,将会默认调用父类的无参构造方法。

2. 对象转型

向上转型:父类引用指向子类对象。

向下转型:指向子类对象的父类引用赋给子类引用(需要强制转换)。

讨论对象转型前先复习下 instanceof 操作符

instanceof 检验某对象是否为某个类的对象,通常用于向下转型的检验,防止出现异常。

student instanceof Person 表示检验 student 是否为 Person 类的对象。 返回类型为 boolean

要求该对象所属的类和某个类是子类与父类的关系。

例如:

1
2
3
4
5
6
7
8
9
// public class Student extends Person {...}
Person person = new Person();
Student student = new Student();
Person p = new Student();

System.out.println(student instanceof Person);
System.out.println(person instanceof Person);
System.out.println(person instanceof Student);
System.out.println(p instanceof Student);

结果为:

1
2
3
4
true
true
false
true

其中 Person p = new Student(); 表示对象向上转型,意思是 p 是上转型对象,既属于 Person 类又属于 Student 类。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// public class Student extends Person {...}
// 向上转型
Person p = new Student();
// p.xxx();

if (p instanceof Student) {
	// 向下转型
    Student student = (Student)p;
    // student.xxx();
}

/* 
不安全的向下转型:
Person person = new Person();
Student student = (Student)person;

错误的向下转型:
Student student = new Person();
*/

向上转型中,只能调用父类自身的方法或者子类里覆写的方法。

向下转型中,主要作用是父类通过实例化子类,可以调用子类独有的方法。

2.1 动态绑定

向上转型的机制可以通过动态绑定来解释。

动态绑定是指在程序运行时,根据具体的对象类型来实现对象和方法绑定(调用)。

  • 编译时类型是由声明该变量时使用的类型决定。

  • 运行时类型由实际赋给该变量的对象决定。

如果编译型类型与运行时类型不一致的话,就会发生运行时动态绑定。对于向上转型:父类引用指向子类对象,这就是一种运行时的动态绑定。

1
2
3
4
// public class Student extends Person {...}
// 向上转型
Person p = new Student();
p.say();

对于上面一段代码,父类 Personp 指向子类 Student 的对象,所以在编译后 p 本来的 Person 类型,但在程序运行时动态指定的 p 的类型是 Student

调用时,如果子类 Student 定义了方法 say(),就直接调用,否则将在其超类中寻找方法。但是这样的话每次调用都要进行搜索,时间开销较大,于是 JVM 采用一个包含所有方法签名和实际调用方法的方法表

动态绑定过程:

  1. JVM 提取对象的实际类型的方法表。

  2. JVM 搜索方法签名(方法名、参数及其类型)进行匹配。

  3. JVM 调用方法。

3. 方法的覆写

在父类和子类中都定义了相同的方法,则子类的方法将覆盖超类的方法。相当于子类对超类方法的修改并复用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Overriding {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.getInfo();
    }
}

class Animal {
    public void getInfo() {
        System.out.println("I am an animal.");
    }
}

class Dog extends Animal {
    @Override
    public void getInfo() {
        System.out.println("I am a dog.");
        super.getInfo();
    }
}

结果为:

1
2
I am a dog.
I am an animal.

例子中 Dog 继承 Animal,在子类 Dog 覆写了方法 getInfo,此时在 main 方法使用子类的对象调用该方法将会覆盖父类的相同方法。

在子类非 static 修饰的代码块或方法中可以使用 super 关键字调用父类被覆写的方法。

注意
  • 父类和子类都必须具有相同的方法名称、相同的返回类型和相同的参数列表。

  • 不能覆写 final 和 static 的方法。

  • 访问修饰符的访问范围需要大于等于父类的访问范围。

对比重载:

  1. 覆写是在子类和父类之间,子类的方法名、返回类型、参数都与父类的方法一致,在继承的情况下才能覆写。重载是同一个类,多个方法的方法名相同但是参数不同(与返回类型,修饰符无关)。

  2. 方法覆写在运行时执行,而方法重载在编译时执行。(动态绑定与静态绑定)

4. 多态

多态(Java 里面主要指动态多态)是同一个行为具有多个不同表现形式或形态的能力。

实现多态的方式:覆写、接口、抽象类和抽象方法。

多态必要条件(表现形式):继承、覆写、父类引用指向子类对象(向上转型),例如 Person p = new Student();

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class Polymorphism {
    public static void main(String[] args) {
        AbstractAnimal dog = new Dog("Dog");
        dog.sound();

        AbstractAnimal cat = new Cat("Cat");
        cat.sound();
    }
}

abstract class AbstractAnimal {
    String name;

    /**
     * 构造方法
     * @param name 动物名
     */
    public AbstractAnimal(String name) {
        this.name = name;
    }

    /**
     * 动物叫声
     */
    public abstract void sound();
}

class Dog extends AbstractAnimal {

    public Dog(String name) {
        super(name);
        System.out.print(name);
    }

    @Override
    public void sound() {
        System.out.println(" goes woof");
    }
}

class Cat extends AbstractAnimal {

    public Cat(String name) {
        super(name);
        System.out.print(name);
    }

    @Override
    public void sound() {
        System.out.println(" goes meow");
    }
}

结果为:

1
2
Dog goes woof
Cat goes meow

以上例子为通过方法的覆写来实现多态。

方法 sound() 在两个不同的类中有不同的实现,在运行时,表达式

dog.sound() 调用 Dog 类的方法,因为 dog 是 Dog 类的对象;

cat.sound() 调用 Cat 类的方法,因为 cat 是 Cat 类的对象。

这里父类 AbstractAnimal 的方法 sound() 本身不需要实现任何功能,仅仅是为了定义方法签名(方法名、参数列表、返回类型),目的是让子类去覆写它,故将父类的方法 sound() 声明为抽象方法。

因为抽象方法本身是无法执行的,故该父类也无法实例化,需要将类声明为 abstract 抽象类才能正常编译。有关抽象类与抽象方法的内容在后续笔记补充。