在我看来,面向对象程序设计,不应该拘泥于具体到语言层面的继承规则,而应该单纯的去分析OOP的概念。每种语言对于OOP的理念,都有不同的处理,具体到继承,具体到访问控制。以目前而言相对经典的两种OOP语言C++和java来讲,它们在面向对象的问题上,如果在你眼里是相去甚远的,只能说你对面向对象的理解还是停留在具体的语法和语义上。
面向对象程序设计(OOP)的精髓在于:对象驱动,向上转型,后期绑定。
所谓面向对象,是对具体业务的层层抽象,形成概念,以概念为核心,增加对外接口,以接口为通道,联系业务。面向对象的理念和面向过程的理念的区别是,后者认为程序是一系列动作的集合,而前者认为程序是一系列业务的集合。
java提供了基本类型的包装类,在java中,所有的事物都是对象或者对象的一部分。并且,所有的类都直接或者间接地继承自Object,Object是一个根类。这也是java彻底的对向驱动的根据。
相对于面向过程的程序设计,面向对象更安全,更容易设定访问权限的边界。
java用于指定权限边界的关键字有三个public private 和protected。public是最广泛的访问权限,允许无条件的访问对应的域和方法;private是最严格的访问权限,只允许当前类中对该域和方法的访问;protect是一种折衷的选择,允许当前类和被继承之后的类对当前域和方法的访问。除此之外,还有一种缺省的访问权限,即不使用任何关键字指定访问权限级别,这个时候是包访问权限。
然而,在很多时候,理解访问权限不能只根据public、private和protected关键字,很多缺省的权限也许更重要。
对于普通类而言,只存在两种访问权限,缺省的包访问权限和public访问权限。这是很容易理解的,如果是protected或者private,以为这类是不可被外界访问的,而这个类自然失去了存在的意义。只有一种情况下是例外,那就是类属于内部类,这个时候因为有了相应的外部类包装,从而可以有对应的继承机制,可以有相应的private 和protected权限控制。
在继承机制里面,任何基类的方法必须是public的。这也很容易理解,因为派生类必须实现基类的所有方法,即使使用基类版本,如果不是public的,那么会产生编译错误。
对于单例模式而言,我们需要该类不允许被自由创建,这个时候可以使用指定所有的构造方法为private,并且用static在类中初始化一个构造方法。
在这里,需要介绍static和final两个关键词,因为对于static和final而言,经常有一些和权限控制交叉的意义。
static的含义是,当声明一个事物是static时,就意味着这个域或者方法不会与包含它的那个类的任何实例关联在一起。
final的含义是,无法改变的,这个逻辑最初是为了设计和效率,但是随着软硬件技术的提升,后者的意义变得越来越不明显。
相比于static,final更为复杂。
final数据有两方面含义:1.一个永不改变的编译时常量 2.一个在运行时被初始化的值,你并且不希望它被改变。
final方法也有两方面含义:第一个方面是要锁定方法,以防止任何继承类修改它的含义。第二方面的含义是出于效率的考虑,这涉及到java虚拟机原理方面的知识。
不知道你有没有发现,final和private在某些方面是类似的。是的,所有的private方法,都会被隐式地指定为final。
final类的含义你可以类比推测,即你不认为该类是应该被继承的。不同的是,final类中只有方法是隐式final的,每一个数据域,仍然有final和非final之分,如果你仔细思考,也能自己理解这个细微差别。
因为static和final经常混用,也许你并不知道static和final的区别所在。对于任何final域,static版本和非static版本是有很大区别的。简而言之,对于每一个对象,都会在内存中保存一份final的数据,而对于static final而言,所有的对象共用一份static final数据。例如下面代码:
1 package oop;
2 import java.util.Random;
3 public class oop {
4 private static Random rand=new Random(100);
5 private static final int onlyOne=rand.nextInt(20);
6 private final int mightMore=rand.nextInt(30);
7 public oop(){
8 }
9 public void getOnlyOne() {
10 System.out.println("onlyOne is :"+this.onlyOne);
11 }
12 public void getMightMore() {
13 System.out.println("mightMore is:"+this.mightMore);
14 }
15 public static void main(String []args){
16 oop op1=new oop();
17 oop op2=new oop();
18 op1.getOnlyOne();
19 op1.getMightMore();
20 op2.getOnlyOne();
21 op2.getMightMore();
22 }
23 }
24 //result:
25 onlyOne is :15
26 mightMore is:10
27 onlyOne is :15
28 mightMore is:4
缺省的权限控制除了上面介绍过的,对于接口和抽象类,也有自己的规则。
抽象类和接口是java中面向对象、概念与业务分离的关键环节。抽象类必须使用abstract关键字修饰类名,意味着类中包含着没有定义的方法(只有类名),换句话说,包含抽象方法的类,一定是抽象类。对于接口,则做了更加严格的限制。接口使用interface而非class这个关键字,成为一个极度抽象的类,意味着接口内部所有的域都隐式地指定为static final,所有的方法都是抽象方法。
由于抽象类和接口都包含没有定义的方法,所以不能初始化一个抽象类或者接口,他们从创建之初,目的就是被继承的。
下面介绍java OOP中后期绑定的概念。所谓后期绑定,是OOP中特别关键的概念,即编译阶段并不能确定所要执行的代码,只有运行阶段才能确定。这在多态机制中,意味着运行过程中会根据对象的类型去执行对应的方法,而避免错误地执行基类版本,这是符合逻辑的。
阐述后期绑定的概念,我们应该先理清一个对象初始化的顺序,先看代码:
1 package oop;
2 public class Father {
3 public Father() {
4 System.out.println("基类版本构造函数");
5 }
6 }
7 package oop;
8 public class Child extends Father{
9 public Child() {
10 System.out.println("子类版本构造函数");
11 }
12 }
13 public static void main(String []args){
14 Father f=new Child();
15 Child c=new Child();
16 }
17 //result:
18 基类版本构造函数
19 子类版本构造函数
20 基类版本构造函数
21 子类版本构造函数
根据上面的代码,我相信你能理解最基本的构造方法的构造顺序,一定程度上也能理解后期绑定的原理。
构造方法的后期绑定相对简单,下面介绍普通方法的后期绑定机制:
1 package oop;
2 public class Father {
3 private static int mem=1;
4 public Father() {
5 System.out.println("基类版本构造函数");
6 }
7 public void Out() {
8 System.out.println("这里是父类的输出方法"+mem);
9 }
10 }
11
12 package oop;
13 public class Child extends Father{
14 private static int mem;
15 public Child() {
16 System.out.println("子类版本构造函数");
17 System.out.println("子类的mem初始值"+mem);
18 this.mem=2;
19 }
20 // public void Out() {
21 // System.out.println("这里是子类的输出方法"+this.mem);
22 // }
23 }
24
25 public static void main(String []args){
26 Father f=new Child();
27 Child c=new Child();
28 f.Out();
29 c.Out();
30 //加注释版本:
31 基类版本构造函数
32 子类版本构造函数
33 子类的mem初始值0
34 基类版本构造函数
35 子类版本构造函数
36 子类的mem初始值2
37 这里是父类的输出方法1
38 这里是父类的输出方法1
39 //不加注释版本:
40 基类版本构造函数
41 子类版本构造函数
42 子类的mem初始值0
43 基类版本构造函数
44 子类版本构造函数
45 子类的mem初始值2
46 这里是子类的输出方法2
47 这里是子类的输出方法2
通过上面的运行结果可以提炼下面几条:
子类对象会先寻找子类中的对应方法,没有的时候会向上访问基类的版本;
基本类型静态变量没有指定初始值的时候为基本类型的初始值,对象引用静态变量的初始值为null;
对于静态变量,基类版本和子类版本并不保持静态一致性,这意味着在内存中,静态变量只使用一份内存,是说每一个类的静态变量只使用一份内存;
上面同时可以看出,静态域访问的是当前对象的实际类型的版本,但是对于静态方法而言,并非如此:
1 package oop;
2 public class Father {
3 private static int mem=1;
4 public Father() {
5 System.out.println("基类版本构造函数");
6 }
7 public void Out() {
8 System.out.println("这里是父类的输出方法"+mem);
9 }
10 public static void OutLn() {
11 System.out.println("这是父类的静态函数");
12 }
13 }
14
15
16 package oop;
17 public class Child extends Father{
18 private static int mem;
19 public Child() {
20 System.out.println("子类版本构造函数");
21 System.out.println("子类的mem初始值"+mem);
22 this.mem=2;
23 }
24 public void Out() {
25 System.out.println("这里是子类的输出方法"+this.mem);
26 }
27 public static void OutLn() {
28 System.out.println("这是子类的静态方法");
29 }
30 }
31 public static void main(String []args){
32 Father f=new Child();
33 Child c=new Child();
34 f.OutLn();
35 c.OutLn();
36 }
37 //result:
38 基类版本构造函数
39 子类版本构造函数
40 子类的mem初始值0
41 基类版本构造函数
42 子类版本构造函数
43 子类的mem初始值2
44 这是父类的静态函数
45 这是子类的静态方法
上面这种情况被称为是java的多态陷阱!
对于静态域和静态方法的访问机制是不同的。静态域的访问符合我们一般意义上的后期绑定,但是静态方法是前期绑定的,也就是说:
静态方法与类相关,而非与对象相关。
实际上,在java中只有两种方法是不是后期绑定。那就是static方法和final方法,final方法不需要多说,因为他不能被子类覆盖,所以子类执行final方法,只能回溯到父类。
当然,如果牵扯到另外一个问题,也许会推翻之前你所有的理解,事实上,只应该是你理解的不全面,而非逻辑的不完美,如下面代码:
1 package oop;
2 public class Father {
3 private static int mem=1;
4 public Father() {
5 System.out.println("基类版本构造函数");
6 this.Cg();
7 System.out.println("基类的mem值为"+mem);
8 }
9 public void Cg() {
10 System.out.println("基类的Cg方法"+mem);
11 this.mem=5;
12 }
13 }
14 package oop;
15 public class Child extends Father{
16 private static int mem;
17 public Child() {
18 System.out.println("子类版本构造函数");
19 System.out.println("子类的mem初始值"+mem);
20 this.mem=2;
21 System.out.println("子类的mem最终值"+mem);
22 }
23 public void Cg() {
24 this.mem=6;
25 System.out.println("子类的Cg方法"+mem);
26 }
27 }
28
29
30 public static void main(String []args){
31 Father f=new Child();
32 Child c=new Child();
33 }
34 //result:
35 基类版本构造函数
36 子类的Cg方法6
37 基类的mem值为1
38 子类版本构造函数
39 子类的mem初始值6
40 子类的mem最终值2
41 基类版本构造函数
42 子类的Cg方法6
43 基类的mem值为1
44 子类版本构造函数
45 子类的mem初始值6
46 子类的mem最终值2
是的,在基类构造方法中绑定的是子类的普通方法,访问的是子类的静态域,这是值得思考的。
相对而言,向上转型是比较好理解的,前面也使用了向上转型。
假设有一个乐器的基类,吉他继承自乐器类,那么一个吉他对象就属于乐器类,这就是向上转型。向上转型是安全的,因为基类的方法和域一定会有对应的子类版本。而向下转型则是危险的,因为java虚拟机不能确定一个乐器一定是吉他。因此向下转型只能由程序员强制操作,程序员也必须确定对象确实是强制转型后的类型。