上一篇主要回顾了一些细枝末节,这篇讲继承与多态。

类的继承

class SubClass extends SuperClass中的extends表示SubClass继承于SuperClass,但是extends本意是扩展的意思。因为翻译问题,子类和父类有各种名字。
子类继承父类的全部成员变量和方法(有关于private的有点小例外),但是构造器不算。

 一个子类只能有一个父类,是单继承关系,但是可以有多层继承关系。所有的类都是java.lang.Object类的子类或间接子类。
 子类扩展(extends)了父类,父类派生(derive)了子类。
 方法的重写要遵循“两同两小一大”的规则,“两同”即方法名,形参列表相同;“两小”即子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;“一大”指子类方法的访问权限要比父类的更大或相等。而且,字类和父类的静态与非静态要对应。

 当子类覆盖了父类后,在子类中就不能直接访问父类中的方法,但是可以通过super(实例方法)和父类类名(类方法,静态的)调用。对于有private的父类方法,子类是不可见的,即使满足上面的规则也不是重写。还有一点要注意的是,当参数列表不同时,就不是重写,而是父类与子类方法的重载

super的使用

 前面写到,如果要在子类中调用父类中被覆盖的实例方法,则要使用super。像this一样,super也不能在static中使用,因为它是用来调用实例方法的。

 如果在构造器中使用super的话,则表示限定改构造器初始化的对象是从父类继承得到的实例变量,该类自己拓展的变量将不会使用。

 上面还写到,当遵循了“两同两小一大”原则时发生了重写。类似的,当实例变量发生覆盖时,也可以使用super来调用父类的实例变量。但是如果没有显式的指定super,那就会根据下面的顺序检查实例变量:

  1. 查找该方法中是否有局部变量a;
  2. 查找当前类中是否有成员变量a;
  3. 查找a的直接父类中是否有成员变量a,一次上溯到a的所有父类,直到java.lang.object,若还没有就出现编译错误。

 当程序创建一个子类对象时,不仅会为该类中定义的实例变量创建内存空间,其父类包括间接父类的实例变量也会被创建,即使使用的是同一个名字。存在一种下面的特殊情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parent {
public String tag = "hello";
}

class Derived extends Parent {
private String tag = "hi";
}

public class HideTest {
public static void main(String[] args) {
Derived d = new Derived();
//System.out.println(d.tag);
System.out.println(((Parent) d).tag);
}
}

 首先这是在main方法中,显然不能用super。这里的写法是将d强制转化为Parent类型。解决方法多的是,我也试着写了一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Parent {
public String tag = "hello";
}

class Derived extends Parent {
private String tag = "hi";

void method() {
System.out.println(super.tag);
}

}

public class HideTest {
public static void main(String[] args) {
Derived d = new Derived();
method();
// System.out.println(super.tag);
}
}

使用父类的构造器

与子类无法获取父类的private的成员变量一样,子类是无法获取父类的构造器的,但是一样可以调用父类的初始化代码,类似于前面讲过的一个构造器调用另一个重载的构造器。在一个构造器中调用别的构造器使用this,在子类中调用父类的构造器则用super。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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(size,name);
this.color=color;
}
public static void main(String[] args) {
Sub s=new Sub(5.6,"测试对象","红色");
System.out.println(s.size+"--"+s.name+"--"+s.color);
}
}

 从上面的代码中可以看出,super和this的行为非常相似。同时,super也必须在第一行,this和super也不能同时出现。
 有意思的是不管是否用super调用来执行父类构造器的代码,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器分如下几种情况:

  1. 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里的参数调用对应的父类构造器。
  2. 子类构造器执行体的第一行代码使用this显式调用本类中重载的构造器,系统将根据this的参数选择合适的本类的构造器。执行本类的另一个构造器前会调用父类的构造器。
  3. 子类构造器执行体既没有super也没有this,系统将会在调用子类构造器之前隐式调用父类构造器。

 不管是哪种情况,父类构造器都要在子类构造器之前调用,而且不止一层,要直至java.lang.object

 如果使用eclipse等IDE写代码,可以自动生成构造函数。但是你会发现,生成出来的都是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
public class Person2 {
private int age;
private String name;
public Person2() {
super();
}
public Person2(int age, String name) {
super();
this.age = age;
this.name = name;
}
}

 一个不带参数,另一个带参数,但是都有一个super()。为什么要有super()调用父类的构造函数?很简单,前面说过,子类是父类的扩充,如果使用构造函数生成子类时,不用super()将父类一并生成,那么子类要从哪里来呢?这就是上面说所有的调用构造器最终都会调用java.lang.object的原因。就像是人一样,生长时通过细胞分化,长出各种不同的细胞,但是最根本的还是离不了干细胞。下面举个例子加深印象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Creature{
public Creature(){
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature{
public Animal(String name){
System.out.println("Animal带一个参数的构造器"+"该动物的name为"+name);
}
public Animal(String name,int age){
this.name;
System.out.println("Animal带两个参数的构造器"+"该动物的age为"+age);
}
}
public class Wolf extends Animal{
public Wolf(){
super("灰太狼",3);
System.out.println("Wolf无参数的构造器");
}
public static void main(String[] args) {
new Wolf();
}
}

多态

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class BaseClass {
public int book = 6;

public void base() {
System.out.println("父类的普通方法");
}

public void test() {
System.out.println("父类的被覆盖的方法");
}
}

public class SubClass extends BaseClass {
//重新定义一个book实例变量来隐藏父类的book实例变量
public String book = "阿拉伯的劳伦斯";

public void test() {
System.out.println("子类的覆盖父类的方法");
}

public void sub() {
System.out.println("子类的普通方法");
}

public static void main(String[] args) {
//下面编译时类型和运行时类型时一样,不存在多态
BaseClass bc = new BaseClass();
//输出6
System.out.println(bc.book);
//执行BaseClass的方法
bc.base();
bc.test();
//下面编译时类型和运行时类型时一样,不存在多态
SubClass sc = new SubClass();
//输出"阿拉伯的劳伦斯"
System.out.println(sc.book);
//执行BaseClass的方法
sc.base();
//执行SubClass的方法
sc.test();
//下面编译时类型与运行时类型不一样,多态发生
BaseClass ploymophicBc = new SubClass();
//输出6---表明访问的是父类对象的实例变量
System.out.println(ploymophicBc.book);
//调用从父类继承的base()方法
ploymophicBc.base();
//调用当前类的test()方法
ploymophicBc.test();
//因为ploymophicBc的编译时类型是BaseClass
//BaseClass没有sub()方法,所以下面的方法会出现错误
//ploymophicBc.sub();
}
}
1
2
3
4
5
6
7
8
9
6
父类的普通方法
父类的被覆盖的方法
阿拉伯的劳伦斯
父类的普通方法
子类的覆盖父类的方法
6
父类的普通方法
子类的覆盖父类的方法

 先不管什么是多态,上面的代码还有点问题,既然ploumophicBc是BaseClass类型,不能用sub方法,那么为什么能用SubClass的test方法?李书讲的很迷啊.对于多态的解释也非常奇怪:当编译时类型与运行时类型不一样时就有可能发生多态.BaseClass ploymophicBc = new SubClass();什么叫ploymophicBc的编译时类型是BaseClass,运行时类型是SubClass???至于林书,emmm,更迷了.例如:当一个方法可以接受多种类型的参数就是多态.这TM都什么解释啊,两个人说的八杆子打不着…

继承与组合

 先不论多态到底是什么,回到上文,继承还有一些要注意的地方。
 以前写过,类应该保证良好的封装性,但是父类无疑破坏了这种封装,而且如果子类对父类进行重写等操作无疑会提高耦合性。这是非常危险的。所以,父类需要遵循一定的原则保证封装性:

  • 尽量隐藏父类的内部数据,成员变量应尽量设置为private的。
  • 不要让父类可以随意直接访问,修改父类的方法。一些辅助性的方法要设置为private的。如果父类的方法需要被外部类使用,则设为public,但是又不要子类修改,则要设为final。而希望子类重写又不要别的类自由访问的方法则设置为protected。
  • 尽量不要在父类构造器中调用要被子类重写额方法,具体原因看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
}
}

 这段代码会引发空指针异常。当创建Sub对象时,会先上溯到Base类,调用Base类的构造器,而在这个构造器中又调用了test()方法,但是这里事调用了子类中的test()方法。为什么会这样,还需要再强调一下什么是“重写(Overriding)”,请看链接:http://www.runoob.com/java/java-override-overload.html。里面有张图,看了后就明白了,“重写”其实相当于一种覆盖,在上面的代码中父类的test()方法已经是不可见的了,除非是用super调用。再来看空指针异常,异常的原因不是表面上我没有给name赋值,而是,name是一个成员变量,这个时候还在类初始化期,s这个对象还没出来。所以对于一个空的东西是没法调用length()方法的。

 这里还要提到一点“初始化”的顺序,这比较复杂。给出链接:https://www.zhihu.com/question/49196023

a. 加载类

1 为父类静态属性分配内存并赋值 / 执行父类静态代码段 (按代码顺序)

2 为子类静态属性分配内存并赋值 / 执行子类静态代码段 (按代码顺序)

b. 创建对象

1 为父类实例属性分配内存并赋值 / 执行父类非静态代码段 (按代码顺序)

2 执行父类构造器

3 为子类实例属性分配内存并赋值 / 执行子类非静态代码段 (按代码顺序)

4 执行子类构造器

 注意,静态属性就是类变量,静态代码段就是静态初始化块。
 按照这个顺序,注意这个顺序是初始化类的顺序,这个时候还没有对象出来。由于并没有静态属性和静态代码块等东西,所以直接进入b的2,调用了子类的test()方法,但是问题是4还没执行,不存在成员变量,所以出现了空指针异常。
 这个话题暂时打住,类的加载比较复杂,以后会专门说。
 回到前文,不仅要考虑继承时的规则,也要考虑什么时候什么时候应该派生子类,什么时候没有必要派生。用简单两句话可以概括:子类需要增加额外的属性,子类要增加自己独有的行为方式。
 还有另外一种实现复用的方式:组合。先来看用继承实现的代码:

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
28
29
class Animal{
private void beat(){
System.out.println("心脏跳动...");
}
public void breath(){
beat();
System.out.println("吸气,吐气...");
}
}
class Bird extends Animal{
public void fly(){
System.out.println("在天空飞翔");
}
}
class Wolf extends Animal{
public void run(){
System.out.println("在陆地奔跑");
}
}
public class InheritTest{
public static void main(String[] args) {
Bird b=new Bird();
b.breath();
b.fly();
Wolf w=new Wolf();
w.breath();
w.run();
}
}

再给出用组合的方式实现:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Animal {
private void beat() {
System.out.println("心脏跳动...");
}

public void breath() {
beat();
System.out.println("吸气,吐气...");
}
}

class Bird {
private Animal a;

public Bird(Animal a) {
this.a = a;
}

public void breath() {
a.breath();
}

public void fly() {
System.out.println("在天空飞翔");
}
}

class Wolf {
private Animal a;

public Wolf(Animal a) {
this.a = a;
}

public void breath() {
a.breath();
}

public void run() {
System.out.println("在陆地奔跑");
}
}

public class InheritTest {
public static void main(String[] args) {
Animal a1 = new Animal();
Bird b = new Bird(a1);
b.breath();
b.fly();
Animal a2 = new Animal();
Wolf w = new Wolf(a2);
w.breath();
w.run();
}
}

 组合的方式显然是在原来的子类中包含另外的原来的父类。继承和组合都是实现复用,但是对于上面的代码来说,继承更合适。而多余Person类和Arm类更加适合用组合。继承表达的是“是(is-a)”的关系,组合表达的是“有(has-a)”的关系。

初始化块

 关于初始化块,我的疑问非常多,求助了许多地方都没有得到解决,无奈只能放一放,先往下进行。

 一个类中可以有多个初始化块,但是最好还是放在一起。初始化块只在创建对象时隐式的执行,且在构造器之前执行。请看下面的代码:

1
2
3
4
5
6
7
8
9
10
public class InstanceInitTest {
{
a=6;
}
int a=9;
public static void main(String[] args)
{
System.out.println(new InstanceInitTest().a);
}
}

a. 加载类

1 为父类静态属性分配内存并赋值 / 执行父类静态代码段 (按代码顺序)

2 为子类静态属性分配内存并赋值 / 执行子类静态代码段 (按代码顺序)

b. 创建对象

1 为父类实例属性分配内存并赋值 / 执行父类非静态代码段 (按代码顺序)

2 执行父类构造器

3 为子类实例属性分配内存并赋值 / 执行子类非静态代码段 (按代码顺序)

4 执行子类构造器

 按照上面的顺序,a=6int a=9先执行,结果出来也确实是9.但是,我的问题是,在执行a=6时,a还未声明,它是怎么执行的?注意:a=6是在{}中的,所以如果在那里声明变量的话,a就是个局部变量,现在a没有在里面声明,说明a在{}之外已被声明过了,但是int a=9又在后面,到底是怎么回事呢?

 初始化块和构造器有点类似,都是对对象进行初始化操作。实际上,初始化块可以看成把几个重载的构造器中共同的部分提取出来,减少代码量。同时,在编译后初始化块会被还原到构造器中,且位于最前面。

 再回到上面的问题,根据李刚本人的回答,int a=9被拆成两截,int aa=9,a=9是被还原到构造器中的,而int a没有,不然就是两次声明同一个变量。我猜测,应该是在类加载时,将这种声明提前了。查看相应的字节码文件:
javap

 可以看到,int a在构造器之前。更详细的内容:
javap

 当把代码改为下面的后:

1
2
3
4
5
6
7
8
9
10
public class StaticInitTest {
static {
a = 6;
}
static int a = 9;

public static void main(String[] args) {
System.out.println(new StaticInitTest().a);
}
}

javap

 在这几张图片中出现了<init>这个东西,具体是什么请看链接https://blog.csdn.net/u013309870/article/details/72975536。按链接所说,这个东西会对非静态的代码合并,<clinit>会对静态的合并,看第二个代码的图,合并的非常明显。但是图里没有出现clinit,是因为某种“美化”,实际上是有的:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// class version 53.0 (53)
// access flags 0x21
public class test3/StaticInitTest {

// compiled from: StaticInitTest.java

// access flags 0x8
static I a

// access flags 0x8
static <clinit>()V
L0
LINENUMBER 5 L0
BIPUSH 6
PUTSTATIC test3/StaticInitTest.a : I
L1
LINENUMBER 7 L1
BIPUSH 9
PUTSTATIC test3/StaticInitTest.a : I
RETURN
MAXSTACK = 1
MAXLOCALS = 0

// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init>()V
RETURN
L1
LOCALVARIABLE this Ltest3/StaticInitTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 10 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW test3/StaticInitTest
INVOKESPECIAL test3/StaticInitTest.<init>()V
GETSTATIC test3/StaticInitTest.a : I
INVOKEVIRTUAL java/io/PrintStream.println(I)V
L1
LINENUMBER 11 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}

 忽略这些不相关的,a=6为什么可以,是因为类加载时将a的声明放在了前面。但是还是有着疑问,如果是这样的话,我将a=6放在代码块外为什么就不行?这个问题还未结束。