Java学习之旅第二季-11:继承
11.1 继承的概念
有时我们可能需要在应用程序的多个位置实现相同的功能。实现这一目标有多种不同的方法,一种方法是在所有需要相同功能的地方复制相同的代码。如果这么做,那么当功能有任何变更时,就需要在所有地方进行修改。
继承(Inheritance)是面向对象编程的一个特征,在这种情况下有助于避免在多个地方复制相同的代码,从而促进代码复用。继承还允许在不更改现有代码的情况下自定义代码。当然继承提供的远不止代码复用和自定义。
继承是面向对象编程语言的基石之一,它允许开发人员通过重用现有类的代码来创建新类。新类被称为子类(sub class),现有类被称为父类(parent class)。父类包含被子类重用和定制的代码。可以说子类继承自父类。父类也被称为基类(base class)或超类(super class)。子类也被称为派生类(derived class)。后续我将使用父类和子类来称呼它们。
从技术上讲,可能从任何现有类继承一个类;然而,实际上这样做并不总是好主意。软件开发中的继承与正常人类生活中的继承方式大致相同。我们从父母那里继承某些东西;我们的父母从他们的父母那里继承某些东西,等等…。如果观察人类生活中的继承现象,会发现存在一种关系使得继承得以发生。同样,父类的对象和子类的对象之间也存在一种关系。为了使继承有效,父类和子类之间必须存在的这种关系被称为 “is-a” 的关系。我们决定让类 A 继承自类B之前,需要先问自己一个简单的问题:类 A 的对象是否也是类 B 的对象?换句话说,类 A 的对象是否表现得像类 B 的对象?如果答案是肯定的,那么类 A 可以从类 B 继承。
考虑以下三个类:Animal(动物)、Dog(狗)和 Flower(花)。让我们依次用这三个类来问同样的问题:
- 花是动物吗? 也就是说,花和动物之间是否存在一种 “is-a” 的关系呢?答案是否定的。那么动物是花吗?答案也是否定的。
- 一朵花能算是一条狗吗?答案是否定的。而一条狗又是否能算是一朵花呢?答案当然也是否定的。
- 动物是狗吗?答案是有可能。动物也可以是猫,马等。然而,动物并不总是狗。那么狗是动物吗?答案是肯定的。
我们用这三种分类方式提出了六个问题,但只有一次的答案是肯定的。这就是唯一适合使用继承的方式:狗类应当从动物类继承而来。
下图是一个相对复杂的继承关系:
可以看到,类的继承可以有多层,比如:狗类继承自杂食性动物类,而杂食性动物类又继承自动物类,至于具体如何设计类及其各种继承关系,就取决与系统期望实现什么功能。
11.2 使用继承
Java中使用extends关键字实现继承,语法如下:
[修饰符] class 类名 extends 父类名{
}
具体示例,首先声明一个Animal类:
public class Animal{}
然后声明Dog类继承自Animal类:
public class Dog extends Animal{}
当然如果Animal类与Dog类不在同一个包中,还需要使用 import 导入。
使用继承的语法虽然简单,但是在 Java 中有几个注意点:
1、Java中只支持单继承
所谓单继承,就是类的父类最多只允许一个,下面的类声明就是错误的:
public class Dog extends Animal,Pet{ // 语法错误}
这个类声明想表达的是狗既是动物也是宠物,可惜 Java 并不支持这种语法。
2、Java 中所有的类直接或间接继承自 java.lang.Object 类
Object是所有Java中类的"老祖宗",刚才我们声明的Dog继承自Animal,而Animal看起来没有任何父类,但 实际上它的父类是 Object,在Object类的源码中有注释:
/*** Class {@code Object} is the root of the class hierarchy.* Every class has {@code Object} as a superclass. All objects,* including arrays, implement the methods of this class.** @see java.lang.Class* @since 1.0*/
public class Object {
}
可以看到,每一个类都将 Object 作为其超类,包括数组在内的所有对象都实现了该类的方法。
11.3 继承的父类成员
子类并非会继承其父类的所有成员。以下是规则:
- 父类中的私有(private)成员不能被子类继承。
- 父类中的构造方法不能被子类继承
- 父类中的代码块/静态代码块不能被子类继承
考虑以下的类声明,类成员包括代码块、静态代码块,构造方法,普通方法,私有方法及静态方法:
public class Animal {{System.out.println("Animal 代码块");}static {System.out.println("Animal 静态代码块");}public Animal() {System.out.println("Animal 构造方法");}public void method() {System.out.println("Animal 普通方法");}private void privateMethod() {System.out.println("Animal 私有方法");}public static void staticMethod() {System.out.println("Animal 静态方法");}
}
下面声明一个类继承它,其中没有任何显式成员:
public class Dog extends Animal{
}
实例化子类并使用点运算符访问其成员:
Dog dog = new Dog();
dog.method();
可以发现只有普通方法能被访问到,换句话来说,只有普通方法被子类继承下来了。
但是在运行子类实例化的代码时,父类的某些成员也会执行,比如:代码块、静态代码块及构造方法都执行了。
Animal 静态代码块
Animal 代码块
Animal 构造方法
Animal 普通方法
11.4 类的初始化顺序
当一个类中成员包括属性,静态属性,代码块,静态代码块,构造方法时,我们将其实例化,那么这些成员的执行顺序是怎么样的?下面以一个Parent类为例:
public class Parent {int num1 = getNum1();static int num2 = getNum2();{System.out.println("Parent 代码块");}static {System.out.println("Parent 静态代码块");}public Parent() {System.out.println("Parent 构造方法");}public int getNum1() {System.out.println("Parent 获取属性值");return 1;}public static int getNum2() {System.out.println("Parent 获取静态属性值");return 2;}
}
测试代码如下:
new Parent();
System.out.println("-------------");
new Parent();
执行之后,可以看到结果是:
Parent 获取静态属性值
Parent 静态代码块
Parent 获取属性值
Parent 代码块
Parent 构造方法
-------------
Parent 获取属性值
Parent 代码块
Parent 构造方法
上面的输出说明,静态属性及静态代码块无论实例化多少次只会执行一次,但普通属性、代码块、构造方法会多次执行。
如果此时我们为该类声明一个子类,子类中的成员与父类类似:
public class Child extends Parent {int num1 = getNum1();static int num2 = getNum2();{System.out.println("Child 代码块");}static {System.out.println("Child 静态代码块");}public Child() {System.out.println("Child 构造方法");}public int getNum1() {System.out.println("Child 获取属性值");return 1;}public static int getNum2() {System.out.println("Child 获取静态属性值");return 2;}
}
测试代码如下:
new Child();
System.out.println("-------------");
new Child();
执行之后,可以看到结果是:
Parent 获取静态属性值
Parent 静态代码块
Child 获取静态属性值
Child 静态代码块
Child 获取属性值
Parent 代码块
Parent 构造方法
Child 获取属性值
Child 代码块
Child 构造方法
-------------
Child 获取属性值
Parent 代码块
Parent 构造方法
Child 获取属性值
Child 代码块
Child 构造方法
从输出结果可以看到这段代码会依次执行父类静态属性及静态代码块,然后是子类静态属性及静态代码块,当然这是按照出现在类中的顺序来执行的,但只会执行一次;而每实例化一次子类,都会先从父类普通属性初始化、代码块、父类构造方法开始执行,然后再执行子类普通属性初始化、子类代码块、子类构造方法代码
11.5 小结
本小节介绍了面向对象编程中的继承概念及其实现要点。继承通过子类继承父类实现代码复用,避免重复代码,同时支持功能扩展。主要内容包括:1)继承基于"is-a"关系建立类间层次结构;2)Java使用extends关键字实现单继承,所有类均继承自Object类;3)子类继承父类的非私有成员(普通方法等),但不继承私有成员、构造方法和代码块;4)类初始化时遵循"父类静态→子类静态→父类实例→子类实例"的顺序执行。通过继承,可以实现代码的高效复用和系统功能的灵活扩展。