# Java教程 - 7 面向对象
# 7.5 继承
在现实世界,有麻雀和鸽子,它们都属于鸟类,麻雀、鸽子和鸟类的关系是父类和子类的关系。
麻雀和鸽子都会飞,我们可以在麻雀类中定义一个飞的方法,在鸽子类中定义一个飞的方法,但是这两个飞的方法是一样的,都是用翅膀飞。那么我们可以直接在鸟类中定义一个飞的方法,让麻雀和鸽子类都继承这个鸟类,那么它们就拥有了飞的方法,就不用再定义了。
# 1 继承的语法
在 Java 中,使用extends
关键字实现继承。
class 子类名 extends 父类名 {
类内容
}
2
3
举个栗子:
麻雀继承鸟类,鸽子继承鸟类:
class Bird {
// 定义一个鸟类
public int age; // 鸟都有年龄
protected void fly() {
// 定义了一个飞的方法
System.out.println("我" + age + "岁了,我会飞");
}
protected void tweet() {
// 定义了一个叫的方法
System.out.println("我会叫");
}
}
class Sparrow extends Bird {
// 定义一个麻雀类,继承自鸟类
}
class Pigeon extends Bird {
// 定义一个鸽子类,继承自鸟类
}
public class ExtendTest {
public static void main(String[] args) {
Sparrow sparrow = new Sparrow(); // 创建一个麻雀对象
sparrow.age = 1;
sparrow.fly();
sparrow.tweet();
Pigeon pigeon = new Pigeon(); // 创建一个鸽子对象
pigeon.age = 2;
pigeon.fly();
pigeon.tweet();
}
}
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
在上面的代码中,我们先定义了一个鸟类,然后在鸟类中定义了 age
属性 ,并定义了两个方法 fly方法
和 tweet方法
;
然后定义类麻雀类,继承鸟类,那么就拥有了鸟类的属性和方法。鸽子类也同样。
执行结果:
我1岁了,我会飞 我会叫 我2岁了,我会飞 我会叫
子类继承父类后,就拥有了父类中声明的属性和方法。但是注意,继承不能继承父类私有的成员变量和方法。如果父类的成员变量和属性不想被子类继承,可以设置为私有成员。
一个类如果没有写明继承哪个类,那么默认都是继承自Object类,所以最终的结果就是Object类是所有类的父类!
# 2 重写父类方法
所以继承父类就拥有了父类非私有的成员变量和方法,如果父类不是我想要的方法,我还可以进行覆盖。
例如,鸟类提供了叫的方法,但是我麻雀有自己的叫法,我要啾啾叫,那么我们可以重写父类的方法,实现自己的个性化。
class Bird {
// 定义一个鸟类
int age; // 鸟都有年龄
protected void fly() {
// 定义了一个飞的方法
System.out.println("我" + age + "岁了,我会飞");
}
protected void tweet() {
// 定义了一个叫的方法
System.out.println("我会叫");
}
}
class Sparrow extends Bird {
// 定义一个麻雀类,继承自鸟类
@Override
public void tweet() {
System.out.println("我会啾啾叫");
}
}
class Pigeon extends Bird {
// 定义一个鸽子类,继承自鸟类
}
public class ExtendTest {
public static void main(String[] args) {
Sparrow sparrow = new Sparrow(); // 创建一个麻雀对象
sparrow.age = 1;
sparrow.fly();
sparrow.tweet();
Pigeon pigeon = new Pigeon(); // 创建一个鸽子对象
pigeon.age = 2;
pigeon.fly();
pigeon.tweet();
}
}
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
在上面的 Sparrow 类中重写了 tweet()
方法,那么子类就有了自己定义的方法,可以实现个性化的功能。
在重写的时候,可以在重写的方法上添加 @Override
注解,表示这个方法是重写的方法,当然不加也完全没问题。
执行结果:
我1岁了,我会飞 我会啾啾叫 我2岁了,我会飞 我会叫
关于方法重写,有几点需要注意:
子类重写的方法使用的访问权限修饰符不能小于父类被重写的方法的访问权限修饰符;
子类方法抛出的异常不能大于父类被重写的方法抛出的异常,异常后面的章节再讲;
父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类;如果父类被重写的方法返回的是基本数据类型,那么子类必须返回相同的基本数据类型;
如果父类方法是 private 的,因为子类都无法调用它,子类是无法重写该方法的。那么可以在子类中定义与父类完全相同签名的方法,但是与父类的方法只是具有相同的名称而已,不是重写。private 方法是被隐式指定为final的,也说明不可“覆盖”。所以遇到父类中的 private 方法,子类应尽量避免相同的方法签名,以免难发现错误。
静态方法是不能被重写的,但是子类可以定义和父类相同签名的方法,但这两个方法相互独立,没有关系,用父类的引用调用的就是父类的方法,不会因为引用指向子类而不同。子类引用调用的就是子类的静态方法。构造方法是隐式声明为静态的,所以构造方法也不能重写,但是子构造方法可以且必须调用父构造。
那么如果我在父类的 fly()
方法中调用 tweet()
方法,然后子类对象调用 fly()
方法,那么会如何执行呢?
class Bird {
void fly() {
// 定义了一个飞的方法
System.out.println("我会飞");
tweet();
}
void tweet() {
// 定义了一个叫的方法
System.out.println("我会叫");
}
}
class Sparrow extends Bird {
void tweet() {
System.out.println("我会啾啾叫");
}
}
public class ExtendTest {
public static void main(String[] args) {
Sparrow sparrow = new Sparrow(); // 创建一个麻雀对象
sparrow.fly();
}
}
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
sparrow.fly()
调用的是继承自父类的方法,在 fly()
方法中调用了 tweet()
方法,那么调用的是父类的tweet()
方法还是子类的tweet()
方法呢?
是调用子类的方法。
执行结果:
我会飞 我会啾啾叫
# 3 调用父类方法
我们在重写父类方法的时候,应该尽量做到在父类方法的基础上进行扩展。
所以很有可能需要调用父类被重写的方法,然后在父类原来的方法上进行扩展。
那么如果我们想要在子类中调用父类被重写的属性和方法呢?
使用 super
关键字来调用父类的属性或方法。
举个栗子:
class Bird {
// 定义一个鸟类
int age = 1; // 鸟都有年龄
void fly() {
// 定义了一个飞的方法
System.out.println("我" + age + "岁了,我会飞");
}
void tweet() {
// 定义了一个叫的方法
System.out.println("我会叫");
}
}
class Pigeon extends Bird {
int age = 10;
// 定义一个鸽子类,继承自鸟类
void tweet() {
// 重写了父类叫的方法
System.out.println("我会咕咕叫");
}
void test() {
System.out.println("age:" + super.age); // 调用父类的属性
super.tweet(); // 调用父类的方法
System.out.println("age:" + this.age);
this.tweet();
}
}
public class ExtendTest {
public static void main(String[] args) {
Pigeon pigeon = new Pigeon(); // 创建一个鸽子对象
pigeon.test();
}
}
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
上面子类中定义了一个和父类中相同名称的属性 age,如果要在子类中调用父类的属性 ,可以通过 super.父类属性
来调用,同样,如果要在子类中调用父类的方法,可以通过 super.父类方法()
来调用。
执行结果:
age:1 我会叫 age:10 我会咕咕叫
# 4 调用父类构造方法
子类继承父类,是没办法直接继承构造方法的。
但是子类的构造方法是会隐式的调用父类的无参构造方法。
举个栗子:
class Bird {
public Bird() {
System.out.println("执行了 Bird 无参构造方法");
}
public Bird(int age) {
System.out.println("执行了 Bird 一个参构造方法");
}
}
class Pigeon extends Bird {
public Pigeon() {
System.out.println("执行了 Pigeon 无参构造方法");
}
public Pigeon(int age) {
System.out.println("执行了 Pigeon 一个参数构造方法");
}
}
public class ExtendTest {
public static void main(String[] args) {
new Pigeon(); // 创建对象,没有传入参数
new Pigeon(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
执行结果:
执行了 Bird 无参构造方法
执行了 Pigeon 无参构造方法
执行了 Bird 无参构造方法
执行了 Pigeon 一个参数构造方法
2
3
4
通过结果可以看到,子类的构造方法默认会先调用父类的无参构造方法。
如果父类定义了有参的构造方法,那么就没有无参的构造方法了,那么子类就会报错。
举个栗子:
class Bird {
/**
* 只有有参构造方法,没有无参构造方法
* @param age
*/
public Bird(int age) {
System.out.println("执行了 Bird 一个参构造方法");
}
}
class Pigeon extends Bird {
/**
* 子类的构造方法默认会调用父类的无参构造方法
* 父类没有了无参构造方法,所以子类的构造方法都会报错
*/
public Pigeon() {
System.out.println("执行了 Pigeon 无参构造方法");
}
public Pigeon(int age) {
System.out.println("执行了 Pigeon 一个参数构造方法");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
上面的代码是会报错的,因为子类 Pigeon 类的构造方法默认会调用父类 Bird 的无参构造方法,但是 Bird 类没有无参构造方法,所以会报错。
当然我们在子类的构造方法中,可以显式的调用父类的构造方法,这样就没事了:
class Bird {
public Bird() {
System.out.println("执行了 Bird 无参构造方法");
}
public Bird(int age) {
System.out.println("执行了 Bird 一个参构造方法");
}
}
class Pigeon extends Bird {
public Pigeon() {
super(); // 调用父类无参构造方法
System.out.println("执行了 Pigeon 无参构造方法");
}
public Pigeon(int age) {
super(age); // 调用父类一个参数构造方法
System.out.println("执行了 Pigeon 一个参数构造方法");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在子类的构造方法中,可以使用 super([参数])
的方式调用父类的构造方法,这句代码必须在子类构造方法的第一句。
所以子类的构造方法无论如何都会调用父类的构造方法的,任何类最终都会继承自 Object 类,所以最终都会调用 Object 类的无参构造函数。
所以 Java 中的初始化顺序是
首先会初始化父类,因为没有父类子类也无从谈起。第一步初始化 static 变量 或者 静态初始化块;
初始化子类的 static 变量 或者 静态初始化块;
初始化父类成员变量,然后是构造函数;
初始化子类成员变量,然后是构造函数;
# 5 类型判断
我们可以判断某个对象的类型,以及是否是某个对象的实例。
有两种方式可以判断某个对象是否是某个类的对象:instanceof 、isInstance(obj) 。
首先有如下代码:
class Bird {
// 鸟类
}
class Sparrow extends Bird {
// 麻雀类,继承自鸟类
}
class Pigeon extends Bird {
// 鸽子类,继承自鸟类
}
2
3
4
5
6
7
8
9
10
11
# instanceof
instanceof 是一个操作符,可以判断一个对象是否是自身类、父类和接口的实例对象。
举个栗子,基于上面的代码:
public class Test {
public static void main(String[] args) {
Bird bird = new Sparrow();
System.out.println(bird instanceof Sparrow); // true
System.out.println(bird instanceof Bird); // true
System.out.println(bird instanceof Pigeon); // false
// 判断是某个类的实例,就可以转换了,如果不是的话,强制转换会报错
if (bird instanceof Sparrow) {
Sparrow sparrow = (Sparrow) bird; // 向下转换
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
注意:所有对象都是 Object 类的实例对象,所以所有对象执行 instanceof Object
都是 true,凡是 null 有关的都是 false(例如:null instanceof Class
)。
instanceof
只能用来判断具有继承关系的实例和类的关系,上面 bird 变量是 Bird 类型, Bird 类型和 Sparrow 有继承关系,所以才能执行 bird instanceof Sparrow
,但是 bird 是 String 类没有继承关系,所以 bird instanceof String
是报错的。
# isInstance()
isInstance() 是类的 class 对象的一个方法。
举个例子,基于上面的代码:
public static void main(String[] args) {
Bird bird = new Sparrow();
System.out.println(Sparrow.class.isInstance(bird)); // true
System.out.println(Bird.class.isInstance(bird)); // true
System.out.println(Pigeon.class.isInstance(bird)); // false
}
2
3
4
5
6
7
和 instanceof
关键字不同的是,判断的时候,对象和类之间可以没有任何关系,例如 String.class.isInstance(bird)
,String 类和 Bird 没有任何关系。
*.class.isInstance
和 instanceof
关键字在功能上是等价的,它们都会检查一个对象是否是某个类或接口的实例。因此,在大多数情况下,如果你用 instanceof
和 isInstance()
来检查同一个对象是否属于同一个类或接口,会得到相同的结果。
推荐使用 instanceof,因为更简洁一些。