Java继承详解
目录
继承树
注意到我们在定义Person的时候,没有写extends。在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,任何类,除了Object,都会继承自某个类。下图是Person、Student的继承树:
┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Person │ └───────────┘ ▲ │ ┌───────────┐ │ Student │ └───────────┘
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。
类似的,如果我们定义一个继承自Person的Teacher,它们的继承树关系如下:
┌───────────┐ │ Object │ └───────────┘ ▲ │ ┌───────────┐ │ Person │ └───────────┘ ▲ ▲ │ │ │ │ ┌───────────┐ ┌───────────┐ │ Student │ │ Teacher │ └───────────┘ └───────────┘
单继承和多重继承
- Java的继承是单继承,但是不可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,一个子类可以继承多个父类,这是java继承区别于C++继承的一个特性。
- Java中却允许多层继承。例如,子类A可以有父类B,父类B同样也可以再拥有父类C。因此子类都是“相对”的;
- 在Java中,Object类为特殊超类或基类,所有的类都直接或间接地继承Object。
extends关键字
在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。
public class Animal { private String name; private int id; public Animal(String myName, String myid) { //初始化属性值 } public void eat() { //吃东西方法的具体实现 } public void sleep() { //睡觉方法的具体实现 } } public class Penguin extends Animal{ }
final关键字
final 关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写:
声明类:
final class 类名 {//类体}
声明方法:
修饰符(public/private/default/protected) final 返回值类型 方法名(){//方法体}
注:实例变量也可以被定义为 final,被定义为 final 的变量不能被修改。被声明为 final 类的方法自动地声明为 final,但是实例变量并不是 final
super 与 this 关键字
super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。
this关键字:指向自己的引用。
实例
class Animal { void eat() { System.out.println("animal : eat"); } } class Dog extends Animal { void eat() { System.out.println("dog : eat"); } void eatTest() { this.eat(); // this 调用自己的方法 super.eat(); // super 调用父类方法 } } public class Test { public static void main(String[] args) { Animal a = new Animal(); a.eat(); Dog d = new Dog(); d.eatTest(); } }
输出结果为:
animal : eat dog : eat animal : eat
向上转型与向下转型
向上转型
如果一个引用变量的类型是Student,那么它可以指向一个Student类型的实例:
Student s = new Student();
如果一个引用类型的变量是Person,那么它可以指向一个Person类型的实例:
Person p = new Person();
现在问题来了:如果Student是从Person继承下来的,那么,一个引用类型为Person的变量,能否指向Student类型的实例?
Person p = new Student(); // ???
测试一下就可以发现,这种指向是允许的!
这是因为Student继承自Person,因此,它拥有Person的全部功能。Person类型的变量,如果指向Student类型的实例,对它进行操作,是没有问题的!
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
Student s = new Student(); Person p = s; // upcasting, ok Object o1 = p; // upcasting, ok Object o2 = s; // upcasting, ok
注意到继承树是Student > Person > Object,所以,可以把Student类型转型为Person,或者更高层次的Object。
向下转型
和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。例如:
Person p1 = new Student(); // upcasting, ok Person p2 = new Person(); Student s1 = (Student) p1; // ok Student s2 = (Student) p2; // runtime error! ClassCastException!
如果测试上面的代码,可以发现:
Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。
因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException。
为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型:
Person p = new Person(); System.out.println(p instanceof Person); // true System.out.println(p instanceof Student); // false Student s = new Student(); System.out.println(s instanceof Person); // true System.out.println(s instanceof Student); // true Student n = null; System.out.println(n instanceof Student); // false
instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false。
利用instanceof,在向下转型前可以先判断:
Person p = new Student(); if (p instanceof Student) { // 只有判断成功才会向下转型: Student s = (Student) p; // 一定会成功 }
从Java 14开始,判断instanceof后,可以直接转型为指定变量,避免再次强制转型。例如,对于以下代码:
Object obj = "hello"; if (obj instanceof String) {
String s = (String) obj; System.out.println(s.toUpperCase());
}
可以改写如下:
// instanceof variable:
public class Main { public static void main(String[] args) { Object obj = "hello"; if (obj instanceof String s) { // 可以直接使用变量s: System.out.println(s.toUpperCase()); } } }
使用instanceof variable这种判断并转型为指定类型变量的语法时,必须打开编译器开关--source 14和--enable-preview。
总结
- 向上转型一定成功
- 向下转型不一定成功,因为子类功能比父类多。
方法的重写
当子类继承父类,而子类中方法与父类中方法的名称、返回类型及参数都完全一致时,就称子类中的方法覆盖了父类中的方法,有时也称为方法的“重写”(Override)。
例如,在Person类中,我们定义了run()方法:
class Person { public void run() { System.out.println("Person.run"); } }
在子类Student中,覆写这个run()方法:
class Student extends Person { @Override public void run() { System.out.println("Student.run"); } }
重写和重载
在上一节我们讲过重载。
重写和重载不同的是,如果方法签名如果不同,就是重载(Overload),重载(Overload)方法是一个新方法;如果重写(Override)是重新以自己的方式实现父类的方法,方法的签名相同,并且返回值也相同。
注意:方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
class Person { public void run() { … } } class Student extends Person { // 不是Override,因为参数不同: public void run(String s) { … } // 不是Override,因为返回值不同: public int run() { … } }
加上@Override可以让编译器帮助检查是否进行了正确的重写。希望进行重写,但是不小心写错了方法签名,编译器会报错。
// override
public class Main { public static void main(String[] args) { } } class Person { public void run() {} } public class Student extends Person { @Override // Compile error! public void run(String s) {} }
但是@Override不是必需的。
在上一节中,我们已经知道,引用变量的声明类型可能与其实际类型不符,例如:
Person p = new Student();
现在,我们考虑一种情况,如果子类覆写了父类的方法,那么,一个实际类型为Student,引用类型为Person的变量,调用其run()方法,调用的是Person还是Student的run()方法?
public class Main { public static void main(String[] args) { Person p = new Student(); p.run(); // 应该打印Person.run还是Student.run? } } class Person { public void run() { System.out.println("Person.run"); } } class Student extends Person { @Override public void run() { System.out.println("Student.run"); } }
思考:重写方法,调用的究竟是子类的 还是父类的呢 ?
运行一下上面的代码就可以知道,实际上调用的方法是Student的run()方法。因此可得出结论:
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。
请参见Java的多态及用法