Practical Java
实践1、 参数是以by value方式而非by reference方式传递
一个普通存在的误解是:java中的参数是以 by value 方式传递。其实不是这样的,参数是以 by value 方式传递的。请看示例:
class PassByValue {
public static void modifyPoint(Point pt, int j) {
pt.setLocation(5,5); //1
j = 15;
System.out.println("During modifyPoint " + "pt = " + pt +
" and j = " + j);
}
public static void main(String args[]) {
Point p = new Point(0,0); //2
int i = 10;
System.out.println("Before modifyPoint " + "p = " + p +
" and i = " + i);
modifyPoint(p, i); //3
System.out.println("After modifyPoint " + "p = " + p +
" and i = " + i);
}
}
这显示,modifyPoint()改变了 //2 所建立的Point对象,却没有改变 int i。在main()之中,i被赋值为10。由于参数通过by value方式传递,所以modifyPoint()收到i的一个副本,然后它将这个副本必为15并返回。main()内的原值i并没有受到影响。
对于Point对象,其实modifyPoint()是与“Point对象的reference p 的复件”打交道,而不是与“Point 对象的复件”打交道。记住,p是个object reference,并且Java以by value方式传递参数,或者更准确点说,Java以by value方式传递object reference。当p从main()被传入modifyPoint()时,传递的是p(也就是一个reference)的复件。所以modifyPoint()是在与同一个对象打交道,只不过通过别名pt罢了。在进入modifyPoint()之后和执行//1之前,内存中应该是这样的:
如果你并不想modifyPoint()改变传进的Point对象,你可以传递一个克隆对象(见64、66)或者将Point设计成不可变的(见65)。
实践2、 对不变的data和object references使用final
class FinalTest {
static final int someInt = 10;
static final StringBuffer objRef = new StringBuffer("sring");
static void prt() {
System.out.println("someInt=" + someInt + " - objRef=" + objRef);
}
public static void main(String[] args) {
prt();
//不能重新分配 FinalTest.someInt
//!!someInt = 20;//1
objRef.append(" other");//2
//不能重新分配 FinalTest.objRef
//!!objRef = new StringBuffer(); //3
prt();
}
}
输出:
someInt=10 - objRef=sring
someInt=10 - objRef=sring other
程序里的//1处理我们应该很清楚了,但//2处又是为什么呢?我们不是已经声明objRef声明成final,为什么还能改变?不,我们确实没有改变objRef的值,我们改变的是objRef所指对象的值,objRef并无变化,仍然指向同一个对象。变量objRef是一个object reference,它指向对象所在的heap位置。而//3处正是我们想的那样,编译出错了,因为你试图改objRef的值,换而言之,它企图令objRe指向其他物体。然而objRef所指对象并不受关键词final的影响,因此所指向的对象本身是可变的。
关键词final只能防止变量值的改变。如果被声明为final的变量是个object reference,那么该reference不能被改变,必须永远指向同一个对象,然而被指向的那个对象是可以随意改变的。
实践3、 缺省情况下所有non-private、non-static函数都可以被覆写
缺省情况下,类中任何non-private、non-static函数都允许被子类覆写。类的设计者如果希望阻止子类覆写(修改)某个函数,则必须采取明确的动作,也就是将该函数声明为final。
关键字final在Java中有多重用途,即可被用于变量(不能修改),也可用于类(不能继承)与方法(不能覆写)。
声明某个类为final,也就暗暗声明了这个类的所有函数都为final。这种做法可以阻止它派生类,从而禁止任何人覆写此类的所有函数。如果这种设计对你而言过于严苛,也可以考虑只将某些方法声明成final,这种方式允许你派生出类,但不允许你覆写你声明成final的方法。另外,final比non-final方法的性能要高。
实践4、 在arrays和ArrayList之间慎重选择
在新建一个数组时,每个元素都将依据其自己类型而被赋予缺省值:boolean-false,char- '\u0000',byte、short、int、long-0,float、double-0.0,object reference-null。
数组的容量是固定的,一旦指定了就不可更改,但ArrayList的容量是可变的,它会随着元素的增加自动的增长。数组即可存放基本类型也可存储引用类型,而ArrayList只能存放引用类型元素(虽然1.5可以,但这是借助于自动装箱特性实现的)。
数组比ArrayList拥有更高的性能。
实践5、 多态(polymorphism)优于instanceof
//员工
interface Employee {
public int salary();//工资计算
}
//经理
class Manager implements Employee {
private static final int mgrSal = 40000;//工资
public int salary() {
return mgrSal;
}
}
//程序员
class Programmer implements Employee {
private static final int prgSal = 50000;
private static final int prgBonus = 10000;//奖金
public int salary() {
return prgSal;
}
public int bonus() {
return prgBonus;
}
}
//薪水册
class Payroll {
public int calcPayroll(Employee emp) {
int money = emp.salary();
if (emp instanceof Programmer)
//如果是程序员,则计算奖金
money += ((Programmer) emp).bonus();
return money;
}
public static void main(String args[]) {
Payroll pr = new Payroll();
Programmer prg = new Programmer();
Manager mgr = new Manager();
System.out.println("程序员的薪水 " + pr.calcPayroll(prg));
System.out.println("经理的薪水 " + pr.calcPayroll(mgr));
}
}
避免使用instanceof,现重构上面的程序。在Employee中可以增加一个bonus()接口,然后员工都实现他,经理没奖金时直接返回0,这样就不用在calcPayroll方法里使用instanceof了。
使用instanceof首先缺乏性能,不够优雅,也不易扩充(如在以后版本中增加另一种Employee,并有另外的福得怎么办?),其次要求程序员写代码去做Java运行期该做的事。而多态则完全可以避免这些问题。
如果这里要为使用instanceof找个理由,那就是Employee不是你设计的,你不能去重构它们。
instanceof操作符很容易被误用。很多场合都应该以多态来替代instanceof。无论何时当你看见instanceof,都请判断是否可以改进设计以消除它。以多态方式改进设计,会产生更合逻辑、更经得起推敲的设计,以及更易维护的代码。
实践6、 必要时使用instanceof
除了实践5中面对一个设计不当的class程序库时,你可能无法避免使用instanceof。事实上有更多常见情形,使你除了使用instanceof以外另无选择,如当你必须从一个基础类型向下转型为派生类型时。
将一个类型强转为另一个不相关的类型时,在编译时就会出错。而将一个基本类型强转为派生类型时在运行时可能出错。
使用instanceof可以消除强转型在运行时的错误。
在1.5以前,如果我们将不同类型的对象放入到集合中后,在取出时都是一个Object类型,这是instanceof就可以排上用场了,因为它可以消除运行期的错误。
实践7、 一旦不再需要object reference,就将它设为null
当不再需要某对象时,你可以将其reference设为null,以协助垃圾回收器取回内存,如果你的程序执行于一个内存受限环境中,这可能很有益处。你可以试着这么做:
public static void main(String args[]){
LargeObject lo = new LargeObject();//大对象
// 使用大对象
//...
// 大对象不再需要,置为null
lo = null;
// 如果确实紧需内存(但不能保证立刻加收,只是建议)
System.gc();
// 后面程序还需要运行很长一段时间...
//...
}
为了尽量降低内存乃是,与程序同寿的对象必须尽可能体积小。此外,大块头对象应该尽量“速生速灭”。
检阅代码时,请注意大块头对象,尤其是那些存在于完整(或大部分)程序生命中的对象。你得要仔细研究这此对象的建立与运用,以及它们使用多少内存,如果它们引用了大量内存,请确定是否所有那些在对象生命周期内都真的被需要。也许某些大对象可以解甲归田,从而使其后执行的代码能够更高效地运行。
任何刻候你都可以通过System.gc()要求垃圾回收器起身运行(注,还是只是建议,当控制从方法调用中返回时,虚拟机已经尽最大努力回收了所有丢弃的对象)。如果想将一个对象解除引用,则可以通过调用System.gc()要求垃圾回收器立刻运转,在代码继承执行前先回收被解除引用的那块内存,但你应当仔细都考虑这种做法为你的程序性能带来的潜在影响。
许多垃圾回收算法会在它们运转之前先虚悬其他所有线程,这种做法可以确保一旦垃圾回收器开始运转,就能够拥有heap的完整访问权,可以安全完成任务而不受其他线程的威胁。一旦垃圾回收器完成了任务,便恢复此前被虚悬的所有线程。
因此,通过System.gc()显示调用,要求垃圾回收器起而运行,你得冒“因执行回收工作而带来延迟”的风险,延迟程度取决于JVM所采用的垃圾回收算法。
大多数JVM的垃圾回收器都会足够的运行,因此你实在不必显式地调用它。然而,如果你的代码有些部分期望在继续进行前先释放所有可能的内存,则可以考虑调用System.gc()。
第二章 对象与相等性
实践8、 区别reference类型与primitive类型
int i = 5;//基本类型
Integer j = new Integer(10);//引用类型
这两个变量都存储在局部变量表(即栈,Stack),它们的操作都在Java操作数堆栈(还是栈)中进行,但二者所表述的意义完全不同。不论是基本类型int或object reference,它们都是static中占据32bits空间,但Integer对象在stack中记录的并不是对象本身,而是对象的reference。
所有Java对象都是通过Object reference来访问的,那是某种形式的指针,该指针指向heap中的某块区域,heap则为对象的生存提供了真实存储场所。当你声明了一个基本类型后,你就为它声明了一份存储空间。前面两行代码可以这样表示:
如果你使用primitive类型,便免除了“调用new以创建包装对象”的需要,这可节省时间和空间。
下面看看输出结果是否你是预想的:
class Assign{
public static void main(String args[]) {
int a = 1;
int b = 2;
Point x = new Point(0,0);
Point y = new Point(1,1);//1
System.out.println("a is " + a);
System.out.println("b is " + b);
System.out.println("x is " + x);
System.out.println("y is " + y);
System.out.println("Performing assignment and " +
"setLocation...");
a = b;
a++;
x = y;//2
x.setLocation(5,5);//3
System.out.println("a is " + a);
System.out.println("b is " + b);
System.out.println("x is " + x);
System.out.println("y is " + y);
}
}
输出结果如下:
a is 1
b is 2
x is java.awt.Point[x=0,y=0]
y is java.awt.Point[x=1,y=1]
Performing assignment and setLocation...
a is 3
b is 2
x is java.awt.Point[x=5,y=5]
y is java.awt.Point[x=5,y=5]
上面代码运行进内存表示如下,经过//1后的情形:
经过//2赋值动作后情形:
当//3调用setLocation()时,函数作用于x所指的对象。由于x和y指向同一个对象,故而形成:
由于x和y指向同一个对象,所有执行于x身上的函数,就好像执行于y身上一样。
弄清楚reference类型和primitive类型之间的差异,以及理解references语义到关重要,否则会导致代码的行为和预想不同。
实践9、 区别==和equals()
“==”用于基本类型时,比较的就是它们所存储值的大小;如果是用于引用类型,它比较的还是所存储值的大小,不过这时这个值是个特殊的值——它们是对象在heap中的地址,所以当它用于对象时比较的是对象地址,如果被比较的对象指向同一对象,则相等,否则不等。
Integer ia = new Integer(10);
Float fa = new Float(10.0f);
System.out.println(ia.equals(fa));//false
System.out.println(fa.equals(ia));//false
为什么上面打印的最是false?这不同基本类型的数值彼止可能相等(如果不同类型则会先提升类型后再比),但不同类型的对象则不然。打开类库源就会发现,它们都是先使用instanceof来测试传进被比较对象是否是同一个类型,如果不是则直接返回false。虽然我们可以自己订制一个equals让不现类型的对象也相等,但并不推荐你这么做,这违反equals业界规范。
这里再次说明的是:请使用“==”测试两个基本类型是否完全相同,或测试两个object references是否指向同一个对象;请使用equals()比较两个对象是否一致——基准点是其属性(此处是指对象的实值内容,也就是数据值域field)。我们把“根据属性来比较两个对象是否相等”称为“等值测试”,或称为“语义上的相等测试”。
实践10、 不要依赖equals()的缺省实现
如果你设计的类没有重写equals()方法,那么你在使用equals时将会使用Object中的equals默认实现,它们比较的是否指向同一个对象,而不是对象的逻辑值是否相等,源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
String类正确的实现了equals()方法,但StringBuffer类根本就没有重写Object中的equals()方法。如果遇到StringBuffer,则需要将它先转换为String再使用equals相互比较。1.5中的StringBuilder与StringBuffer一样。
String、StringBuffer、StringBuilder都是final类。
equals()使用准则:
1、 若要比较对象是否相等,其class有责任提供一个正确有equals()。
2、 要“想当然地调用equals()”之前,应该先检查并确保你所使用的class的确实现了equals()。
3、 如果你所使用的class并未实现equals(),请判断java.lang.Object的缺省函数是否可胜任。
4、 如果无法胜任,就应该在某个外覆类(wrapper class)或subclass中撰写你自己的equals()方法(比如你设计的类中关键域是StringBuffer之类的类型,你得需要继承它重写它的equals方法,但它是final类,所以使用组合的方式设计功能类似的StringBuffer,并提供equals方法;或者不重新设计功能类似StringBuffer的类而是直接在使用StringBuffer的类中提供一个比较方法对StringBuffer进行专门的比较)。
实践11、 不要依赖equals()的缺省实现