类的继承
1. 语法
Java的继承只允许单继承,每个子类只有一个直接父类。继承通过关键字extends
来实现。
[修饰符] class SubClass extends SuperClass {
// 类定义部分
}
2. 继承的含义
继承意味着子类扩展了父类,将可以获得父类的全部成员变量和方法,只有一个除外:Java的子类不能获得父类的构造器。
因而可以认为一个派生类的对象中是包含一个超类的对象的。初始化一个派生类对象时,必须先初始化一个超类的对象。如果派生类的构造方法中没有显示调用超类的构造方法,则会默认调用超类的无参构造方法去创建超类对象。
父类和子类的关系,是一种一般到特殊的关系,子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,可认为父类是大类,而子类是小类。
“继承后子类获得了父类的全部成员变量和方法”意味着,父类中定义的所有实体在子类中其实依旧是占据内存的,所谓各种修饰符的限定,只是表明了子类对父类的访问权限,而非“是否继承”的权限。
![]() | ![]() |
---|
总结:在Java中,当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承到的所有实例变量分配内存,即使子类定义了与父类中同名的实例变量。
清华大学郑莉
超类的初始化完成之前,子类的成员是不会做任何初始化的。
尽管有诸多这样类的重用的方式,但还是要意识到——并不是现实世界中所有我们认为是类属关系的都可以用继承来表示,比如说圆和椭圆。我们既不能让圆继承椭圆(长轴、短轴数据的冗余带来数据的不一致性),也不能让椭圆继承圆(当然的,不符合逻辑),这便是面向对象程序设计的局限性。我们学习一种东西既要熟悉它的优势来激发兴趣,也是意识到它的不足以弥补现实问题。
3. Object基类
若定义一个Java类时并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object
类。因此,java.lang.Object
类是所有类的父亲,要么是其直接父类,要么是其间接父类,所有的Java对象都可以调用java.lang.Object
类所定义的实例方法。
所有的数组类型,不管是对象数组还是基本类型的数组,也都扩展了Object
类。
4. 方法覆盖
当子类覆盖了父类的方法后,子类的对象将无法访问父类中被覆盖的方法。但在子类内部,方法中依然可以借助super
关键字/父类类名调用父类中被覆盖的实例/类方法。
但要注意的是,如果父类方法是private
的,该方法对其子类是隐藏的,子类对其根本无法访问,故而根本不存在子类覆盖该方法一说。
尽管返回类型不是方法签名的一部分,但在覆盖一个方法时,需要保证返回类型的兼容性——允许子类将覆盖方法的返回类型改为原返回类型的子类型。此时,我们说父类与子类的这两个方法有可协变的返回类型。
在覆盖一个方法时,子类方法不能低于父类方法的可见性。
当e
引用Employee
对象对,e.getSalary()
调用的是Employee
类中的getSalary
方法;当e引用Manager
对象时,e.getSalary()
调用的是Manager
类中的getSalary
方法。JVM知道e
实际引用的对象类型,因此能够正确地调用相应的方法。
5. super关键字
super
关键字用于限定该对象调用它从父类继承得到的可被访问的实例字段/实例方法,这通常用于父类的变量/方法被覆盖时的情况。
- 那也就是说,如果父类中的某个成员限定的访问权限都不允许被子类访问,即使
super
也不能调用该成员; - 正如
this
不能出现在static
修饰的方法中一样,super
也不能出现在static
修饰的方法中。 - 如果在构造器中使用
super
,则super
用于限定该构造器初始化的是该对象从父类继承得到的实例变量,而不是该类自己定义的实例变量。
有些人认为super
与this
引用是类似的概念,但这样比较不是很恰当。这是因为super
不是一个对象的引用。例如,不能将值super
赋给另一个对象变量,它只是一个指示编译器调用超类方法/字段的特殊关键字。
6. 子类变量的回溯顺序
如果在某个方法中访问名为a
的成员变量,但没有显式地指定调用者,则系统查找a
的顺序为:
- 查找该方法中是否有名为
a
的局部变量; - 查找当前类中是否包含名为
a
的成员变量; - 查找
a
的直接父类中是否包含名为a
的成员变量; - 依次上溯
a
的所有间接父类,直到java.lang.Ojbect
类; - 若最终都不能找到名为
a
的成员变量,则编译错误。
7. 继承中的构造方法
子类不会获得父类的构造器,但子类构造器里可以调用父类构造器的初始化代码,类似于“在一个类内一个构造器调用另一个重载的构造器”这种情形,只是关键字换成了super
。
class Base {
public double size;
public String name;
public Base(double size, String name) {
this.size = size;
this.name = name;
}
}
public 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(5.6, "测试对象", " 红色");
// 输出Sub 对象的三个实例变量
System.out.println(s.size + "--" + s.name + "--" + s.color);
}
}
使用super
调用父类构造器也必须出现在子类构造器执行体的第一行,所以this
调用和super
调用不会同时出现;
事实上,无论是否在构造器中使用super
,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行。依次上溯的话,这意味着创建任意Java对象总是最先执行java.lang.Object
类的构造器。
如果子类的构造器没有显式地调用超类的构造器,将自动调用超类的无参数构造器。如果超类没有无参构造,并且在子类的构造器中又没有显式地调用超类的其他构造器,Java编译器将会报告一个错误。
概述而言,子类构造器总会如下调用父类构造器:
- 若子类构造器执行体的第一行使用了
super
显式调用父类构造器,系统将根据super
调用里传入的实参列表调用父类中相应的构造器; - 若子类构造器执行体的第一行使用了
this
显式调用本类中重载的构造器,系统将先调用this
匹配的另一个构造器,而在执行另一个构造器时即会调用父类构造器; - 若子类构造器执行体中既无
super
又无this
,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。
8. 注意事项
设计父类时,尽量不要在父类构造器中调用将要被子类重写的方法。
class Base {
public Base() {
test();
}
public void test() {
System.out.println("将要被子类重写的方法");
}
}
public class Sub extends Base {
private String name;
public void test() {
System.out.println("子类重写父类的方法," + "其name字符串长度" + name.length());
}
public static void main(String[] args) {
// 下面代码会引发空指针异常
Sub s = new Sub();
/*
控制台输出:
Exception in thread "main" java.lang.NullPointerException
at com.chuan.Sub.test(Sub.java:17)
at com.chuan.Base.<init>(Sub.java:5)
at com.chuan.Sub.<init>(Sub.java:13)
at com.chuan.Sub.main(Sub.java:22)
*/
}
}
当系统试图创建
Sub
对象时,同样会先执行其父类构造器,而此时父类构造器调用了被其子类重写的方法,就导致父类调用的是被子类重写后的方法。由于此时子类的name
实例变量是null
,因此将引发空指针异常。
9. final修饰符
如果要把某些类设置成最终类,即不能被当成父类,可以使用final
修饰这个类。
java.lang.String
类和java.lang.System
类就是两个final
类。
除了final
,还可用一种技巧禁止子类调用父类的构造器,从而导致无法继承该类:使用private
修饰这个类的所有构造器。此时对于父类而言,可另外提供一个静态方法用于创建该类的实例。