2 继承
2.1 继承的基本概念
我们让 Cat 类和 Dog 类继承自 Animal 这个类。这样,Animal 类与 Cat 和 Dog类之间,就形成了继承关系。而被继承的类 Animal,被称为“父类”;而 Cat 与 Dog 这两个类继承自 Animal,被称为“子类”。
由于父类是子类的共性的抽象,是一个一般的类,因此,我们在进行继承关系设计的时候,应当尽量把子类的共性放在父类,特性放在子类。
这是一个非常典型的继承关系。生物,是一个很宽泛的概念,包括植物、动物等,都可以看做是生物。而动物,就是一种特殊的生物,因此,动物类是生物类的子类。同样的,哺乳动物是特殊的动物,而人又是特殊的哺乳动物。
父类是对子类共性的抽象,父类和子类的关系,是由一般到特殊的关系。在设计类的继承关系时,应当把共性放在父类,特性放在子类。
2.2 继承的基本语法
从语法上说,继承使用关键字:extends。在定义子类的时候,可以用 extends 关键字说明这个类的父类是哪一个类。代码如下:
class Animal{ int age; boolean sex; public void eat(){ System.out.println("Animal eat"); } public void sleep(){ System.out.println("sleep 8 hours"); } } class Dog extends Animal{ public void lookAfterHouse(){ System.out.println("look after house"); } } class Cat extends Animal{ public void catchMouse(){ System.out.println("catch mouse"); } } public class TestDog { public static void main(String args[]){ Dog d = new Dog(); d.sex = true; d.age = 3; d.eat(); d.lookAfterHouse(); } }
父类中的属性和方法,被子类继承之后,相当于子类中也有了相应的属性和方法。
在父类中定义了属性和方法之后,子类中就能够直接继承,这样,就让父类中的代码得到了重用,从而提高了代码的可重用性。同时,子类也能够写一些子类的特性,这样,就在父类的基础上增加一些功能,体现了面向对象的可扩展性。
2.3 什么能被继承?
class Parent{ int value1; private int value2; } class Child extends Parent{ public void method(){ value1 = 100; // 编译正确 Child 类从父类中继承到 value1 属性 value2 = 200; // 编译错误,Child 没有继承到 value2 属性 } }
那么 Child 类中有没有 value2 属性呢?从空间上来说,在创建 Child 对象的时侯,会在 Child 对象的内部,包含着一个 Parent 对象。
只有子类能够访问的属性和方法,才能够被子类继承。
2.4 访问权限修饰符
在 Java 中,总共有四种访问修饰符。除了我们之前介绍的 private 和 public 两个修饰符之外,还有两个跟访问权限相关的修饰符:default 以及 protected。要注意的是,default修饰符指的是:如果在属性或方法前面,不加任何的访问修饰符(即不加 private、public、protected),则访问权限是 default 权限。
要注意的是,要把一个属性设为 default 时,千万不能写上“default”这个单词。
访问权限为 default 的属性或者方法,只能被本类或者同包的其他类访问。
对于 default 的属性和方法而言,只有同包的类才能够访问。
package p1; public class Parent{ int value = 20; } package p1; //与 Parent 同一个包 public class Child1 extends Parent{ public void m1(){ System.out.println(value); //继承了 Parent 的 value 属性 } } package p2; //与 Parent 不同包 public class Child2 extends p1.Parent{ public void m2(){ //编译错误!Child2 类和 Parent 不同包,没有继承 value 属性 System.out.println(value); } }
用 protected 修饰符修饰的属性和方法,能够被本类内部、同包的类以及非同包的子类访问。
package p1; public class Parent{ protected int value; } package p1; public class SamePackage{ public void m1(){ Parent p = new Parent(); System.out.println(p.value); //同包的类可以访问 } } package p2; public class Child extends p1.Parent{ public void m2(){ //非同包的子类可以访问父类的 value 属性 System.out.println(value); } } package p2; public class Other { public void m2(){ p1.Parent p = new p1.Parent(); //编译错误!Other 类和 Parent 类并不同包,也没有继承关系,不能访问 System.out.println(p.value); } }
private default protected public
访问权限依次变宽
2.5 方法覆盖
class Dog extends Animal{ public void lookAfterHouse(){ System.out.println("Dog can look after house"); } public void sleep(){ System.out.println("sleep 6 hours"); } } public class TestDog { public static void main(String args[]){ Dog d = new Dog(); d.sleep(); } }
子类中用一个特殊实现,来替换从父类中继承到的一般实现,这种语法叫做“方法覆盖”。
从语法上说,方法覆盖对方法声明的五个部分都有要求。
1. 访问修饰符相同或更宽
例如,父类的方法如果是 protected 方法,子类如果想要覆盖这个方法,则修饰符至少是 protected,也可以是 public,但是不能是 default 或者 private 的。
2. 返回值类型相同。
如果返回值类型不同,则会产生一个编译错误。
3. 方法名相同。
4. 参数表相同。
2.6 对象创建的过程
在有了继承关系之后,对象创建过程如下:
1. 分配空间。要注意的是,分配空间不光是指分配子类的空间,子类对象中包含的父类对象所需要的空间,一样在这一步统一分配。在分配空间的时候,会把所有的属性值都设为默认值。
2. 递归的构造父类对象。
3. 初始化本类属性。
4. 调用本类的构造方法。
class A{ int valueA = 100; public A(){ valueA=150; } } class B extends A{ int valueB = 200; public B(){ valueB=250; } } class C extends B{ int valueC = 300; public C(){ valueC=350; } } public class TestInherit{ public static void main(String args[]){ C c = new C(); } }
我们在主方法中创建了一个 C 对象,则创建时的过程如下。
1. 分配空间。在分配空间时,会把 C、B、A 这三个对象的空间一次性都分配完毕,然后把这三个对象的属性都设为默认值。这样,value1,value2,value3 这三个属性都被设置为 0
2. 递归构造 C 对象的父类对象。在这里,要 C 对象的父类对象,就是 B 对象。因此,在这一步需要创建一个 B 对象。
3. 初始化 C 的属性,即把 valueC 赋值为 300
4. 调用 C 的构造方法。
其中,第 2 步,C 对象的父类为 B 对象,因此必须要先创建一个 B 对象。创建 B 对象不用重新分配空间,需要以下几步:
2.1 递归的构造 B 对象的父类对象
2.2 初始化 B 属性:把 vauleB 赋值为 200
2.3 调用 B 的构造方法。
在 2.1 这个步骤中,递归的创建 B 对象的父类对象,也就是创建 A 对象。创建 A对象不需要分配空间,因此,A 对象的创建有这样几步:
2.1.1 创建 A 对象的父类对象。这一步在运行时,没有任何的输出。
2.1.2 初始化 A 的属性,把 valueA 赋值为 100
2.1.3 调用 A 的构造方法。
总结一下:创建 C 对象的步骤一共有 7 步:
1. 分配空间。在一次分配空间时,会把整个继承关系中涉及到的类所需要的空间,都分配完毕,并把所有属性都设为默认值 0。
2. 初始化 A 类的属性。把 valueA 赋值为 100。
3. 调用 A 类的构造方法。会把 valueA 的值设为 150。
4. 初始化 B 类的属性,把 vauleB 赋值为 200。
5. 调用 B 类的构造方法,会把 valueB 的值设为 250。
6. 初始化 C 类的属性,把 valueC 赋值为 300。
7. 调用 C 类的构造方法,会把 valueC 的值设为 350。
2.7 super 关键字
在默认情况下,创建子类对象时,都会调用父类的无参构造方法。
super 关键字用法一:super 用在构造方法上
super 关键字的第一种用法,就是可以指定在递归构造父类对象的时候,调用父类的哪一个构造方法。
要格外注意的是,super 用在构造方法中时,只能作为构造方法的第一句。
然而,我们曾经介绍过,this 关键字可以在构造方法中,指明调用本类的其他构造方法。并且,对 this()来说,这个语句也只能作为构造方法的第一个语句。这样,在构造方法中,就不能够既使用 this(),又使用 super()。
这样,我们构造方法的第一个语句,就有了三种可能
1. super (参数) 指明调用父类哪个构造方法
2. this (参数) 指明调用本类哪个构造方法
3. 既不是 this(参数)又不是 super(参数)。
编译器会自动在这个构造方法中增加一个语句:“super();” ,即调用父类的无参构造方法。
super 关键字用法二:super 用作引用
最典型的用途是,使用 super在子类中,调用父类被覆盖的方法。
class Parent{ public void m(){ System.out.println(“m in Parent”); } } class Child extends Parent{ public void m (){ System.out.println(“m in Child”); } public void m1 (){ this.m(); } public void m2(){ super.m(); } } public class TestSuper{ public static void main(String args[]){ Child c = new Child(); c.m1(); c.m2(); } }
super 可以调用父类被覆盖的方法,这种特性在实际编程中有着广泛的应用。
2.8 单继承
Java 语言规定,每一个类只能有一个直接父类。
因此,Java 语言中单继承的特性, 被认为是 Java 语言相对于 C++语言,“简单性”的一个重要体现。