Chapter 6: 访问控制权限
Java访问权限限制的动机:方便库程序员修改工具类的具体实现而不用知会客户端程序员修改客户端代码。作为一个库程序员,你应该尽可能地把客户端程序员不需要知道的变量和方法以及辅助类都设为private,这样你就可以随心所欲地优化代码,只要你的暴露给客户端程序员的public方法声明是不变的。
一般而言,库程序员写代码时应该把public的成员(数据和方法)都放在类的定义的前面,这样是方便客户端程序员阅读。接着是protected、包默认权限、以及Private。
这几个访问权限分别对应:
1、 public就是所有人都能去读取改变你的东西;
2、默认不加的话就只有同一个社区(包)内的人可以去读取,改变你的东西;
3、protected则是自己的孩子可以去读取和修改的;
4、private就是自己的。(继承同样受这样的限制)。
所以,一个很重要的编程习惯是,如果你需要你的方法或者数据成员能被子类继承,那么你只能把这个方法或者数据设为public或者protected权限。
Java发明package的动机和C++的名字空间的动机是相同的,都是避免代码调用时引起的名字冲突。Java对于每个类的调用实际上调用的都是类的全名:如java.util.ArrayList。而如果是自己写的代码,如我自己写的代码被我放在了我的com.jasonxjie的package下时,引用我写的类MyClass时,实际上调用的是com.jasonxjie.MyClass。由于前面两个点的部分是自己网站域名的反转,网站域名是唯一的,所以就确定了整个完整的类型com.jasonxjie.MyClass的是唯一。需要注意的是,java的package的命名习惯是都要小写。如果需要把当前代码至于某个包下,需要在非注释的首行就写上“package com.jasonxjie;”当然,如果不想每一次调用类的时候都写那么长的类名,可以使用import某个package。如import com.jasonxjie.*; 这样就把该package下的所有类都import进来了。这么做的情况下需要注意的是,两个import进来的package不能有同名类,否则编译器不知道你直接写的MyClass具体是调用的哪一个。
Java程序的运行基于正确设置的CLASSPATH变量。Java不同于C/C++,后者是编译型语言。C/ C++的编译器产生一个中间文件(obj文件),然后通过链接器或者类库产生器产生的其它同类文件捆绑一起运行。但Java的运行方式是把.class文件打包压缩为一个JAR文档文件或者针对某个包含main函数的.class文件,解释执行这些文件的同时,选择hotspot编译执行一些热点代码。
Java解释器首先会找出CLASSPATH变量,CLASSPATH变量的每一个目录都是查找.class文件的根目录。然后再根据程序员自定义的包名,如com.jasonxjie这样包,会自动转换成目录:/com/jasonxjie/。然后组合两者,形成查找.class文件的路径。例如,CLASSPATH变量的值是:/usr/local/projectjava。那么查找.class文件的路径是:/usr/local/projectjava/com/jasonxjie/。但是,如果你想解释器找的是一个JAR文件,则需要在CLASSPATH当中包含这个JAR文件的全路径:如/usr/local/projectjava/example.jar。
善用import关键词可以方便编程:
1/ import static
可以将某个类下的所有static的方法和变量都import到新的程序文件的static区。就可以直接使用该类的静态方法和变量,而不需要显示调用该类或者创建该类的对象。如我们可以写这么一个工具类:
package net.mindview.util;
import java.io.*;
public class Print{
public static print(Object obj) {
System.out.println(obj);
}
}
这样就可以在之后的代码中,直接通过import static net.mindview.util.Print.*; 来直接使用print来打印字符串到控制台。
注意,需要在Print后面加*,以导入所有的静态方法和变量。
2/ 利用import不同的package来实现debug。一个package是调试版,能调用debug函数,另一个则没有。
Chapter 7、8: 组合/ 继承与多态
组合和继承都是复用某些已经写好的代码的方式。组合是在新的类中,加入一个已有的类作为类数据成员的方式。通过这个类数据成员可以使用现有类的公有方法和成员数据来实现某些功能。继承是利用extend去创建一个已有类的子类,但是,必须使子类具有一样的接口。一般而言,推荐使用组合,原因是组合的灵活性较高,不必固守原有类的方法接口。组合是(has a)的关系,而继承是(is a)的关系。但,普遍而言,程序员用到继承时,已经大多数是(like a, with more)的关系。
一般而言,组合用于想在新类中使用现有类的功能而非它的接口。一般在组合中,在新类中嵌入一个Private的原有类的对象,然后只让用户看到新类的接口,利用原有类去实现功能。 设计模式当中的适配器模式便利用了这一原理。
继承用于开发某个原有类的特殊版本。继承是需要慎用的技术,判断是否使用继承的一个重要标准是:是否需要从新类向基类进行向上转型(多态)。如果必须向上转型,则继承是必要的。意思是,是否有写编写的现有类需要在运行时,可能与其他的几个准备编写的新类中一起,再随机抽一个作为该事件运行的类。如果有这种需求,需要使用继承以达到多态的目的。
使用组合需要注意的地方:
编译器不会为你的自定义类对象初始化。所以,一般而言,你有三种选择:① 在定义时初始化(C++不允许这么做);② 在构造器中初始化;③ 惰性初始化,在使用前在利用调用它的成员函数中初始化(因为它是private的,一般只能由成员函数调用赋值)。不初始化的话,使用这个成员变量时会产生NullPointer Exception。但打印的话还是能显示Null.
使用继承需要注意的地方:
只有Public和Protected方法和成员数据能被继承,其它的数据成员/ 函数在子类中看似的“覆盖”其实只是重载。为了预防在子类的假“覆盖”却是重载这种情况,建议使用Java的注释功能,如@Override。将@Override放在子类定义覆盖父类的函数前,如果你不小心编写错误,或者方法的名字参数对不上,编译器就会发出错误提示。如果在子类覆盖了父类函数后,却又想调用父类版本的函数,使用super关键词即可。
P.S. 其它两个常用的系统注解:@Deprecated,中文翻译是:不推荐的。如果客户端程序员使用了它注解的元素,编译器会发出警告信息。@SuppressWarnings,关闭编译器的警告信息。
关于继承的初始化需要注意:
对于继承方式的子类,其初始化的顺序是:根类子类(Object的子类)--> 根类子类的子类 --> ... --> 当前子类的静态数据成员/ 方法成员(只加载一次) --> 当前子类的普通数据成员/ 方法成员 -->当前子类的构造函数。原理是:构建子类对象前,要先准确构造基类对象,因为子类的对象很有可能会用到基类对象的数据。注意,无论是静态的数据成员还是非静态的,都是按照书写顺序进行加载。
所以,一般编译器会自动调用父类的无参默认构造器。但是,如果父类的没有默认的无参默认构造器时,则需要手动地在子类的构造器中,明确使用super去调用父类的构造器。否则,编译器会报错。
如下例显示:
class Game{
Game(int i){
print("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i){
super(i);
print("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
print("Chess");
}
}
通过每个类的构造器都调用父类的构造器,确保父类对象的初始化。
关于继承的清理需要注意:
Java的GC只会在内存不足时再清理内存,而且其针对的只有堆区的对象。对于一些如打开的I/O设备或者文件的确保关闭,需要程序员手动确保。Java的try..catch...finially的机制可以很好的确保这点。关键词try的意思是,try块所示的区域是保护区,意味着无论它怎样退出,都有catch帮它兜着所有抛出的问题,而且总有finially帮它收尾。(无论如何都会运行finnally)。所以,可以把手动在finally当中调用自己写的dispose()清理函数是确保所有东西都妥善清理的保证。手写自己的dispose()函数需要注意清理的顺序,与初始化顺序相反:首先是当前子类的引用成员的dispose(),注意与书写顺序相反;然后是调用super.dispose()。一直递归上去根类。
介绍一个特性:名字屏蔽
在C++中,如果在子类重载了基类的某个函数,则会屏蔽这个函数的所有基类重载版本。但如果是在Java中,不会有这种屏蔽现象。在子类的对象中,可以使用父类的重载版本。
关于多态:
多态的意思是,针对同一个引用,可以根据其实际指向的对象不同而产生不同的行为。其实现的基础是Java的动态联编原理。在Java中,除了static方法和final方法,所有的方法都是在运行时再动态绑定。
在Java中,实现多态的方式是通过向上转型。利用基类的引用,通过实际在运行中绑定不同的子类对象,调用各自覆盖的基类方法时,可以产生不同的行为。这种结果就是多态。但是,向上转型不能调用基类没有的方法,但也不会被子类先添加的方法所影响。
使用向上转型实现多态需要注意以下几点:
1)对于基类的私有方法,是无法覆盖的。在子类中,任何试图覆盖父类的私有方法,都是重载。而且子类无法继承父类的私有方法。可以通过添加@Override来避免这种错误。
2)对于数据成员和静态方法,是无法实现多态的。对于public的父类数据成员,不能通过向上转型,在子类中通过父类引用获得子类的值。虽然继承是将父类的所有的public和protected的数据成员和方法都继承下来。但是,即使覆盖了原有的数据成员,数据成员是无法实现多态的。但是,一般良好的编程习惯要求我们,①数据成员一般设为private;②父类和子类的数据成员不要同名,以免混淆。另外,静态的方法虽然是公有的或者Protected的,也是无法再子类覆盖后实现多态。
3)不要尝试在当前父类的对象构造器中调用当前子类要覆盖的父类方法。这样做的后果是如果子类创建对象时,会调用父类的构造器。然而,父类构造器内的成员函数的实际动作却是子类的动作。更糟糕的是,如果父类的这个函数所调用的数据是子类的数据初始化后未赋值的数据成员(假设它是在子类的构造器中赋值的),产生无可预计的错误。详见Page 163。所以,我们一般规定:编写构造器时,避免调用其它的方法。如果调用,也只能调用private和final的方法。
说说Final:
① Final数据:
final在java中表示——“这是无法改变的。”一般用于告诉编译器,这一块数据是恒定不变的。对于一个永不改变的编译时常量或者一个在运行时被初始化后,就不希望它再改变的变量,final的出现是告诉编译器你想让它们永不改变。
1)对于编译期常量,使用了final后,编译器就能直接把常量值代入任何该常量出现的计算式,可以在编译时,而非运行时,执行计算式。减轻了运行的负担。但是在java中,这些编译期常量务必是基本类型,且对这个常量进行定义时,必须要赋值;
2)一个既是static又是final的数据只占据了一段不能改变的存储空间,且命名必须是大写表示,用下划线分隔各个单词;但是对于知识final的成员数据,不需要大写命名。需要鉴别的是,普通的final成员数据在每个对象初始化时可以再次赋值,但是static的对象只会创建一次,所以只能赋值一次。static在这里,表示这个变量只有一份。
3)对于final一个引用,其实只是规定这个引用一旦指向了一个对象后,就不能再赋其它值。但并没有规定对象不能改变自己的数据成员等等。注意,在java中,除了基本类型,String、数组等都是引用。
4)相反,Java并没有提出任何限制对象final不变的方法。
需要注意的是:对于final的成员变量的赋值只有两种选择,一是在定义时赋值,二是在构造器中赋值。
另外,final也可以在形参列表中使用,说明该参数不能再被改变。当传用的是一个引用时,代表该引用不能再改变指向。这个final参数在内部类的使用中,可以用来向匿名内部类传递数据。
② Final方法
当你把一个方法定义成final时,代表着你不想任何人覆盖这个方法。也曾有人说过,把方法定义为final,告诉编译器此方法可以静态联编,提高效率。但是,有了Hot-spot的JVM后,并没有什么提高。
所以,在不想别的子类覆盖这个方法的意义上,final和private的意义是相同的。但是,private的方法不能类外调用,final是可以的。对于private和final的方法,除了本身父类能看到用到,对于其它类的意义都不大。
③ Final 类
当把类定义为final时,代表你不想该类被继承。当一整个类是final是,可以创建对象,但对象中所有的数据成员和方法都是final的。整个类都保护的很好。
P.S. 如果将一个类的构造函数设置为private的话,代表着不希望这个类能创建对象。