上一篇博客《JavaSE 拾遗(4)——JavaSE 面向对象程序设计语言基础(4)》说了封装,这次接着说继承和多态。
在人们认识事物时,通常是先有子类,再有父类,再有更多的其他子类,这么一个过程,这个奠定了人们描述事物的体系结构。所以继承不是简单的加法关系,更重要的是语义上的抽象层次的不同,所以在给有继承关系的类命名的时候一定需要体现出语义上的同源关系和层次关系。不应该单纯不为复用而抽象,而是为概念而抽象,正如李氏替换原则“在使用定义的类的时候,子类型必须能够替换它们基类型”。extends 主要表述了 is a 的关系。编程语言中的继承,正好实现了对世界中事物描述的时候,分层,分粒度的描述的方法。如果说封装是对你要描述的现实中的东西进行拆分组合,那么继承可以看做是对面向对象表述方法自身的拆分组合。比如,一个小的封闭空间,里面有 男人小明、女人小芳、一只哈士奇狗、还有一座房子,描述这个整体的系统,我们可以说系统是 动物 和 房子 组成,还可以说是 人、狗、房子 组成,还可以说是男人、女人、狗、房子组成,这就是 分层 分粒度 描述事物的方法。所以面向对象程序设计就有两个问题,一个是怎么学习使用别人建立好的系统,二是怎么正确构造自己的系统。这样描述系统,就产生了两种描述系统的方法:一是纵向,二是横向。一般先是拆分事物,通过 组合 聚合 关联 的关系,描述事物,再是把拆分开的部分分类,比较异同,通过 继承 的方式来描述事物。现实中对事物的描述需要结合着两种方法。拆分事物,我们可以看做是封装来实现的,后面一种方法则对应于继承,其实封装也体现了拆分组合,继承也体现了拆分组合。
在 java 中和继承相关的内容有
继承,就是子类得到父类所有成员,通过 extends 关键字表述父类子类的关系,java只支持单继承,多实现,因为多继承会带来安全隐患。
多继承为什么如此重要,因为越是底层的事物,越是具有多个方面的特性,每个方面是一种看待事物的角度,从每一个角度看事物,都可以有一个纵向的关系。多个纵向的关系的描述就可以用多继承来实现。
在 java 里面我们怎么选择这些多种纵向关系中的一种来作为继承呢?如果说方法的不安全性是 java 不支持多继承的一方面,那么更重要的一方面就是 java 把继承这种机制做了主次分类,主就是 extends ,次就是 implements,意思是说类必须有且只有一个主的分类,用 extends。我们在定义类的时候,一个最重要的原则就是单一职责原则,那么我们表达主纵向关系的那个角度一定要选择类的单一职责所表示的角度,这样才能体现类的主要功能。和 c++ 相比,这或许就是 java 的优越性,java 把这种类的主要功能和扩展功能用 extends 和 implements 分开了。接口是对 扩展功能的抽象,而父类是对主要功能的抽象。
如何使用一个继承体系中的功能?要想使用体系,先查阅体系父类的描述,因为父类中是该体系的共性类容,通过了解共性内容就可以了解体系的基本功能,整个体系的了解过程有点象二叉树查找的过程,在从根类往子类走,了解完一个分支,再学习其他分支。
/**
需求:演示 super、函数多态 的原理
思路:
步骤:
*/
public class Test extends Father{
int i = 10;
public static void main(String[] args) {
// 测试 super.字段和 super.方法
new Test().println();
// 测试方法的多态
Test t = new Test();
Father f = new Test();
t.invokeMethod();
f.invokeMethod();
}
void println(){
// 测试 super.字段
System.out.println(this.i + " " + super.i);
// 测试 super.方法
this.invokeMethod();
super.invokeMethod();
this.privateFunction();
super.privateFunction();
}
void invokeMethod(){
System.out.println("子类 invokeMethod++++" + i);
}
}
class Father
{
int i = 9;
void invokeMethod(){
System.out.println("父类 invokeMethod " + i);
}
public void privateFunction(){
invokeMethod();
System.out.println("父类 privateFunction " + i);
}
}
Compiled from "Test.java"
public class Test extends Father {
int i;
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method Father."":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field i:I
10: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class Test
3: dup
4: invokespecial #4 // Method "":()V
7: invokevirtual #5 // Method println:()V
10: new #3 // class Test
13: dup
14: invokespecial #4 // Method "":()V
17: astore_1
18: new #3 // class Test
21: dup
22: invokespecial #4 // Method "":()V
25: astore_2
26: aload_1
27: invokevirtual #6 // Method invokeMethod:()V
30: aload_2
31: invokevirtual #7 // Method Father.invokeMethod:()V
34: return
void println();
Code:
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #9 // class java/lang/StringBuilder
6: dup
7: invokespecial #10 // Method java/lang/StringBuilder."":()V
10: aload_0
11: getfield #2 // Field i:I
14: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
17: ldc #12 // String
19: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_0
23: getfield #14 // Field Father.i:I
26: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
29: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: aload_0
36: invokevirtual #6 // Method invokeMethod:()V
39: aload_0
40: invokespecial #7 // Method Father.invokeMethod:()V
43: aload_0
44: invokevirtual #17 // Method privateFunction:()V
47: aload_0
48: invokespecial #18 // Method Father.privateFunction:()V
51: return
void invokeMethod();
Code:
0: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #9 // class java/lang/StringBuilder
6: dup
7: invokespecial #10 // Method java/lang/StringBuilder."":()V
10: ldc #19 // String 子类 invokeMethod++++
12: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_0
16: getfield #2 // Field i:I
19: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
22: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return
public void privateFunction();
Code:
0: aload_0
1: invokespecial #18 // Method Father.privateFunction:()V
4: return
}
invokevirtual #6 // Method invokeMethod:()V
传递的 this 对象,那么先根据 this 对象中的 Class 引用,解析函数,如果该 Class 中有这个函数符号,那么就直接使用该 Class 中的函数符号,如果没有,再解析该类的父类中是否有子类可以访问的该函数;如果是函数符号是:
invokespecial #7 // Method Father.invokeMethod:()V
传递的是 this 对象,那么就直接根据 this 对象中的 Class 引用,找到该 Class 的父类 Class ,查看是否有该函数存在,如果没有,再查看 父类的父类的 Class,是否有子类可以访问的该函数符号存在。
/**
需求:演示模板方法模式
思路: 模板方法,又叫方法模板,就是一个方法,虽然我们知道其功能,但是我们
只能确定其中前面部分和结尾部分内容,按照现在的程序设计理论和方法——
顺序 分支 循环,我们没法完整的实现这个函数。那么,我们就把这个这个
方法拆分为两个方法,其中已知的部分写一个方法,未知的部分写一个方法,
在已知部分的内部适当的位置,调用未知的部分,那么就能完成了我们希望
的整体的功能。当未知的部分确定以后再用子类函数覆盖父类函数的方法,
实现未知部分。这种程序的结构,就叫做模板方法模式,已知部分的方法,
就叫做模板方法。模板方法是一种把已知未、知拆分的方法。
函数参数 表达了只有等到程序执行时才能知道的数据
模板方法模式 表达了只有等到程序执行时才能知道的行为
反射 表达了只有等到程序执行时才能知道的类名
模板方法其实完全可以不使用,也能实现想要的功能,就是等已知功能的全
部行为的时候,才在子类把 knownMethod 方法完全实现。
这也说明了,系统模型的设计 和 oop 的设计的不同。系统模型设计要求自然、
易懂,oop 设计的时候,要求易扩展、易复用。我认为不应该牺牲易读性,来
实现 易扩展、易复用。应该尽量实现易读性,还实现易扩展性,不要一味强求
易复用。过早追求易复用和过早优化是一样的道理,过早优化不是优雅的设计。
模板方法模式,可以用来表达现实中功能延迟确定的情况。
继承打破了封装性,这里也有表现,按道理,父类的方法只要不想对外提供接口
的都应该是 private 属性,但是为了让子类可以使用,需要把限制放宽,否则子
类不能覆盖,覆盖也属于在子类中使用父类方法的情况,覆盖就打破了父类封装性
步骤:
*/
abstract class TemplateMethod
{
public final void knownMethod()
{
System.out.println("a");
unknownMethod();
System.out.println("b");
}
void unknownMethod()
{
System.out.println("c");
}
}
class TemplateMethodChild extends TemplateMethod
{
void unknownMethod()
{
System.out.println("d");;
}
}
class TemplateMethodMain
{
public static void main(String[] args)
{
TemplateMethod part = new TemplateMethodChild(); //输出 adb
part.knownMethod();
System.out.println("Hello World!");
}
}
可以说模板方法是以继承的形式把 method 未知部分和已知部分分开,其实还可以用组合的方式把 method 未知的部分和已知的部分分开,把未知的部分单独封装为一个类B,在已知部分的类 A 中定义一个引用指向这个类的对象,在已知部分的方法中调用类B的对象的未知部分功能。就像 Comparator 和 Comparable 接口直接的关系。