Methods Common to All Objects
item 7:当你覆盖equals方法的时候一定要遵守general contact
覆盖equals的时候一定要加倍的小心,其实最好的办法就是不覆盖这个方法。比如在下面的情况下就可以不覆盖
1这个类的每个实例都是唯一的,例如Thread类
2 如果你不关心这个类是否该提供一个测试逻辑相等的方法
3超类已经覆盖了equals方法,并且它合适子类使用
4如果这个类是private或者是package-private的,并且你确信他不会被调用
但是当我们要为这个类提供区分逻辑相等和引用相等的方法的时候,我们就必须要覆盖这个方法了。例如String类,Date类等,覆盖的时候我们一定要遵从general contact,说白了就是一个合同。合同的主要内容是
1.x.equals(x)必须返回true
2.x.equals(y)当且仅当y.equals(x)返回true的时候返回true
3.x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)必须返回true
4.如果没有任何修改得话那么多次调用x.equals(y)的返回值应该不变
5.任何时候非空的对象x,x.equals(null)必须返回false
下面是作者的建议如何正确的覆盖equals方法
1. 用==检查是否参数就是这个对象的引用
2. 用instanceof判断参数的类型是否正确
3. 把参数转换成合适的类型
4. 比较类的字段是不是匹配
例如:
public boolean equals(Object o)
{
if(o== this) return true;
if(!(o instanceof xxxx) return false;
xxx in = (xxx)o;
return ……..
}
最后一点要注意的时候不要提供这样的方法public boolean equals(MyClass o)这样是重载并不是覆盖Object的equals方法
item 8 :当你覆盖equals的时候必须覆盖hashCode方法
这点必须切忌,不然在你和hash-based集合打交道的时候,错误就会出现了。关键问题在于一定要满足相等的对象必须要有相等的hashCode。如果你在PhoneNumber类中覆盖了equals方法,但是没有覆盖hashCode方法,那么当你做如下操作的时候就会出现问题了。
Map m = new HashMap();
m.put(new PhoneNumber(408,863,3334),”ming”)
当你调用m.get(new PhoneNumber(408,863,3334))的时候你希望得到ming但是你却得到了null,为什么呢因为在整个过程中有两个PhoneNumber的实例,一个是put一个是get,但是他们两个逻辑相等的实例却得到不同的hashCode那么怎么可以取得以前存入的ming呢。
Item 9:永远覆盖toString方法
在Object的toString方法返回的形式是Class的类型加上@加上16进制的hashcode。你最好在自己的类中提供toString方法更好的表述实例的信息,不然别人怎么看得明白呢。
Item 10:覆盖clone()方法的时候一定要小心
一个对象要想被Clone,那么要实现Clone()接口,这个接口没有定义任何的方法,但是如果你不实现这个接口的话,调用clone方法的时候会出现CloneNotSupportedException,这就是作者叫做mixin的接口类型。通常clone()方法可以这样覆盖
public Object clone()
{
try
{
return super.clone();
}
catch(CloneNotSupportedException e)
{}
}
但是当你要clone的类里面含有可修改的引用字段的时候,那么你一定要把整个类的蓝图进行复制,如果对你clone得到的对象进行修改的时候还会影响到原来的实例,那么这是不可取的。所以应该这样clone()
public Object clone() throws CloneNotSupportedException
{
Stack Result = (Stack)super.clone();
Result.elements = (Object[])elements.clone();
Return result;
}
其中elements是stack类中可修改的引用字段,注意如果elements是final的话我们就无能为力了,因为不能给他重新赋值了.其实如果不是必须的话,根本就不用它最好。
Item 11:考虑适当的时候覆盖Comparable接口
Thinking in java上说的更清楚,这里不多少了。
越来越发现这是一本难得的好书,Java程序员不看这本书的话真是很遗憾。本章讲述的是类和接口相关的问题。这几个Item都非常重要. Item 12:把类和成员的可访问范围降到最低
好的模块设计应该尽最大可能封装好自己的内部信息,这样可以把模块之间的耦合程度降到最低。开发得以并行,无疑这将加快开发的速度,便于系统地维护。Java中通过访问控制符来解决这个问题。
- public表示这个类在任何范围都可用。
- protected表示只有子类和包内的类可以使用
- private-package(default)表示在包内可用
- private表示只有类内才可以用
你
在设计一个类的时候应该尽量的按照4321得顺序设计。如果一个类只是被另一个类使用,那么应该考虑把它设计成这个类的内部类。通常public的类不应
该有public得字段,不过我们通常会用一个类来定义所有的常量,这是允许的。不过必须保证这些字段要么是基本数据类型要么引用指向的对象是不可修改
的。不然他们将可能被修改。例如下面的定义中data就是不合理的,后面两个没有问题。
public class Con
{
public static final int[] data = {1,2,3};// it is bad
public static final String hello = "world";
public static final int i = 1;
}
Item 13:不可修改的类更受青睐
不可修改的类意思是他们一经创建就不会改变,例如String类。他们的设计、实现都很方便,安全性高——它们是线程安全的。设计不可修改类有几点规则:
- 不要提供任何可以修改对象的方法
- 确保没有方法能够被覆盖,可以通过把它声明为final
- 所有字段设计成final
- 所有字段设计成private
- 确保外部不能访问到类的可修改的组件
不可修改类也有个缺点就是创建不同值得类的时候要创建不同的对象,String就是这样的。通常有个解决的办法就是提供一个帮助类来弥补,例如StringBuffer类。
Item 14:化合(合成)比继承更值得考虑
实现代码重用最重要的办法就是继承,但是继承破坏了封装,导致软件的键壮性不足。如果子类继承了父类,那么它从父类继承的方法就依赖父类的实现,一旦他改
变了会导致不可预测的结果。作者介绍了InstrumentedHashSet作为反例进行说明,原因就是没有明白父类的方法实现。作者给出的解决办法是
通过化合来代替继承,用包装类和转发方法来解决问题。把想扩展的类作为本类的一个private
final得成员变量。把方法参数传递给这个成员变量并得到返回值。这样做的缺点是这样的类不适合回掉框架。继承虽然好,我们却不应该滥用,只有我们能确
定它们之间是is-a得关系的时候才使用。
Item 15:如果要用继承那么设计以及文档都要有质量保证,否则就不要用它
为了避免继承带来的问题,你必须提供精确的文档来说明覆盖相关方法可能出现的问题。在构造器内千万不要调用可以被覆盖的方法,因为子类覆盖方法的时候会出现问题。
import java.util.*;
public class SubClass extends SuperClass
{
private final Date date;
public SubClass()
{
date = new Date();
}
public void m()
{
System.out.println(date);
}
public static void main(String[] args)
{
SubClass s = new SubClass();
s.m();
}
}
class SuperClass
{
public SuperClass()
{
m();
}
public void m()
{
}
}
由于在date被初始化之前super()已经被调用了,所以第一次输出null而不是当前的时间。
由于在Clone()或者序列化的时候非常类似构造器的功能,因此readObject()和clone()方法内最好也不要包括能被覆盖的方法。
Item 16:在接口和抽象类之间优先选择前者
接口和抽象类都用来实现多态,不过我们应该优先考虑用接口。知道吗?James说过如果要让他重新设计java的话他会把所有都设计成接口的。抽象类的优点是方便扩展,因为它是被继承的,并且方法可以在抽象类内实现,接口则不行。
Item 17:接口只应该用来定义类型
接口可以这样用的 Collection c = new xxxx();这是我们最常用的。不要把接口用来做其他的事情,比如常量的定义。你应该定义一个类,里面包含public final static 得字段。
Item 18: 在静态和非静态内部类之间选择前者
如果一个类被定义在其他的类内部那么它就是嵌套类,可以分为静态内部类、非静态内部类和匿名类。
static
member class 得目的是为enclosing class服务,如果还有其他的目的,就应该把它设计成top-level
class。nonstatic member class是和enclosing class
instance关联的,如果不需要访问enclosing class
instance的话应该把它设计成static得,不然会浪费时间和空间。anonymous
class是声明和初始化同时进行的。可以放在代码的任意位置。典型应用是Listener 和process object例如Thread。
由于以前学过C语言,所以对C还是蛮有感情,而JAVA和C又有很多相似之处,很多从C转过来学习JAVA的兄弟,可能一开始都不是很适应,因为很多在C里面的结构在JAVA里面都不能使用了,所以下面我们来介绍一下C语言结构的替代。
Item 19:用类代替结构
JAVA刚面世的时候,很多C程序员都认为用类来代替结构现在太复杂,代价太大了,但是实际上,如果一个JAVA的类退化到只包含一个数据域的话,这样的类与C语言的结构大致是等价的。
比方说下面两个程序片段:
class Point
{
private float x;
private float y;
}
实际上这段代码和C语言的结构基本上没什么区别,但是这段代码恐怕是众多OO设计Fans所不齿的,因为它没有体现封装的优异性,没有体现面向对象设计的优点,当一个域被修改的时候,你不可能再采取任何辅助的措施了,那我们再来看一看采用包含私有域和共有访问方法的OO设计代码段:
class Point
{
private float x;
private float y;
public Point(float x,float y)
{
this.x=x;
this.y=y;
}
public float getX(){retrun x;}
public float getY(){return y;}
public void setX(float x){this.x=x;}
public void setY(float y){this.y=y;}
}
单从表面上看,这段代码比上面那个多了很多行,还多了很多函数,但是仔细想一下,这样的OO设计,似乎更人性化,我们可以方面的对值域进行提取,修改等操作,而不直接和值域发生关系,这样的代码不仅让人容易读懂,而且很安全,还吸取了面向对象程序设计的灵活性,试想一下,如果一个共有类暴露它的值域,那么想要在将来的版本中进行修改是impossible的,因为共有类的客户代码已经遍布各处了。
需要提醒一点的是,如果一个类是包级私有的,或者是一个私有的嵌套类,则直接暴露其值域并无不妥之处。
Item 20:用类层次来代替联合
我们在用C语言来进行开发的时候,经常会用到联合这个概念,比如:
typedef struct{
double length;
double width;
}rectangleDimensions_t;
那我们在JAVA里面没有联合这个概念,那我们用什么呢?对!用继承,这也是JAVA最吸引我的地方之一,它可以使用更好的机制来定义耽搁数据类型,在Bruce Eckel的Thinking in java里面也多次提到了一个和形状有关的例子,我们可以先笼统的定义一个抽象类,即我们通常所指的超类,每个操作定义一个抽象的方法,其行为取决于标签的值,如果还有其他的操作不依赖于标签的值,则把操作变成根类(继承的类)中的具体方法。
这样做的最重要的优点是:类层次提供了类型的安全性。
其次代码非常明了,这也是OO设计的优点。
而且它很容易扩展,即使是面向多个方面的工作,能够同样胜任。
最后它可以反映这些类型之间本质上的层次关系,从而允许更强的灵活性,以便编译时类型检查。
Item 21:用类来代替enum结构
Java程序设计语言提出了类型安全枚举的模式来替代enum结构,它的基本思想很简单:定义一个类来代表枚举类型的单个元素,并且不提供任何公有的构造函数,相反,提供公有静态final类,使枚举类型中的每一个常量都对应一个域。
类型安全枚举类型的一个缺点是,装载枚举类的和构造常量对象时,需要一定的时间和空间开销,除非是在资源很受限制的设备比如蜂窝电哈和烤面包机上,否则在实际中这个问题不会被考虑。
总之,类型安全枚举类型明显优于int类型,除非实在一个枚举类型主要被用做一个集合元素,或者主要用在一个资源非常不受限的环境下,否则类型安全枚举类型的缺点都不成问题,依次,在要求使用一个枚举类型的环境下,我们首先应考虑类型安全枚举类型模式。
Item 22:用类和接口来代替函数指针
众所周知,JAVA语言和C的最大区别在于,前者去掉了指针,小生第一次接触JAVA的时候觉得好不习惯,因为突然一下子没了指针,觉得好不方面啊,C语言的精髓在于其指针的运用,而JAVA却把它砍掉了,让人好生郁闷,不过随着时间的推移,我渐渐明白了用类和接口的应用也同样可以提供同样的功能,我们可以直接定义一个这样一个类,他的方法是执行其他方法上的操作,如果一个类仅仅是导出这样一个方法,那么它实际上就是一个指向该方法的指针,举个例子:
class StringLengthComprator{
public int compare(String s1,String s2)
{
return s1.length()-s2.length();
}
}
这
个类导出一个带两个字符串的方法,它是一个用于字符串比较的具体策略。它是无状态的,没有域,所以,这个类的所有实例在功能上都是等价的,可以节省不必要
的对象创建开销。但是我们不好直接把这个类传递给可户使用,因为可户无法传递任何其他的比较策略。相反,我们可以定义一个接口,即我们在设计具体策略类的
时候还需要定义一个策略接口:
public interface Comparator{
public int compare(Object o1,Object o2);
}
我们完全可以依照自己的需要来定义它。
具体的策略类往往使用匿名类声明。
在JAVA中,我们为了实现指针的模式,声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类,如果一个具体策略只被使用一次的话,那么通常使用匿名类来声明和实例化这个具体策略类,如果一个策略类反复使用,那么它的类通常是一个私有的的静态成员类。
下面我们来讨论一下有关方法设计的几个方面,下面说的几个要点大多数都是应用在构造函数中,当然也使用于普通方法,我们追求的依然是程序的可用性,健壮性和灵活性。
Item 23:检查参数的有效性
非公有的方法我们应该用断言的方法来检查它的参数,而不是使用通常大家所熟悉的检查语句来检测。如果我们使用的开发平台是JDK1.4或者更高级的平台,我们可以使用assert结构;否则我们应该使用一种临时的断言机制。
有些参数在使用过程中是先保存起来,然后在使用的时候再进行调用,构造函数正是这种类型的一种体现,所以我们通常对构造函数参数的有效性检查是非常仔细的。
Item 24:需要时使用保护性拷贝
众所周知,JAVA在代码安全性方面较C/C++有显著的提高,缓冲区溢出,数组越界,非法指针等等,我们的JAVA都有一个很完善的机制来进行免疫,但是这并不代表我们不必去考虑JAVA的安全性,即便在安全的语言,如果不采取措施,还是无法使自己与其他类隔开。假设类的客户会尽一切手段来破坏这个类的约束条件,在这样的前提下,你必须从保护性的方面来考虑设计程序。通过大量的程序代码研究我们得出这样的结论:对于构造性函数的每个可变参数进行保护性拷贝是必要的。需要注意的是,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是原始的对象。对于“参数类型可以被不可信方子类化”的情况,不要用clone方法来进行参数的保护性拷贝。
对
于参数的保护性拷贝并不仅仅在于非可变类,当我们编写一个函数或者一个构造函数的时候,如果它要接受客户提供的对象,允许该对象进入到内部数据结构中,则
有必要考虑一下,客户提供的对象是否是可变的,如果是,则要考虑其变化的范围是否在你的程序所能容纳的范围内,如果不是,则要对对象进行保护性拷贝,并且
让拷贝之后的对象而不是原始对象进入到数据结构中去。当然最好的解决方法是使用非可变的对象作为你的对象内部足见,这样你就可以不必关心保护性拷贝问题
了。):
Item 25:谨慎使用设计方法的原型
(1)谨慎的选择方法的名字:即要注意首先要是易于理解的,其次还要与该包中的其他方法的命名风格相一致,最后当然要注意取一个大众所认可的名字。
(2)
不要追求提供便利的方法:每一个方法都应该提供其应具备的功能点,对于接口和类来方法不要过多,否则会对学习使用维护等等方面带来许多不必要的麻烦,对于
每一个类型所支持的每一个动作,都提供一个功能完全的方法,只有一个方法过于频繁的使用时,才考虑为它提供一个快捷方法。
(3)
避免过长的参数列表:通常在实践中,我们以三个参数作为最大值,参数越少越好,类型相同的长参数列尤其影响客户的使用,两个方法可以避免过长的参数这样的
情况发生,一是把一个方法分解成多个,每一个方法只要求使用这些参数的一个子集;二是创建辅助类,用来保存参数的聚集,这些辅助类的状态通常是静态的。
对于参数类型,优先使用接口而不是类。
这样做的目的是避免影响效能的拷贝操作。
谨慎的使用函数对象。
创建函数对象最容易的方法莫过于使用匿名类,但是那样会带来语法上混乱,并且与内联的控制结构相比,这样也会导致功能上的局限性。
Item 26:谨慎的使用重载
到底是什么造成了重载机制的混淆算法,这是个争论的话题,一个安全而保守的方法是,永远不要导出两个具有相同参数数目的重载方法。而对于构造函数来说,一个类的多个构造函数总是重载的,在某些情况下,我们可以选择静态工厂,但是对于构造函数来说这样做并不总是切合实际的。
当涉及到构造函数时,遵循这条建议也许是不可能的,但我们应该极力避免下面的情形:
同一组参数只需要经过类型的转换就可以传递给不同的重载方法。如果这样做也不能避免的话,我们至少要保证一点:当传递同样的参数时,所有的重载方法行为一致。如果不能做到这一点,程序员就不能有效的使用方法或者构造函数。
Item 27:返回零长度的数组而不是null
因为这样做的原因是编写客户程序的程序员可能忘记写这种专门的代码来处理null返回值。没有理由从一个取数组值的方法中返回null,而不是返回一个零长度数组。
Item 28:为所有导出的API元素编写文档注释
不爱写注释可能是大多数程序员新手的通病(包括偶哈~),但是如果想要一个API真正可用,就必须写一个文档来说明它,保持代码和文档的同步是一件比较烦琐的事情,JAVA语言环境提供了javadoc工具,从而使这个烦琐的过程变得容易,这个工具可以根据源代码自动产生API文档。
为了正确得编写API文档,我们必须每一个被导出的类,接口,构造函数,方法和域声明之前加一个文档注释。
每一个方法的文档注释应该见解的描述它和客户之间的约定。
我们接下来讨论一下Java语言的细节,包括局部变量的处理,库的使用,以及两种不是语言本身提供的机制的使用等等一些大家平时可能忽略的问题。
Item 29:将局部变量的作用域最小化
和C语言要求局部变量必须被生命在代码的开始处相比,Java程
序设计语言宽松得多,它允许你在代码的任何位置声明。要想使一个局部变量的作用域最小化,最高小的技术是在第一次需要使用它的地方声明,变量的作用域是从
声明它的地方开始到这个声明做在的代码块的结束位止,如果我们把变量的声明和代码的使用位置分开的过大,那么对于读这段代码的人来说,是很不幸的。
我们几乎都是在一个局部变量声明的地方同时给它初始化,注意这是很重要的,甚至有时候,如果我们的初始化应该推迟到下一个代码的位置,我们同时应该把声明也往后延迟。这条规则唯一的例外是try-catch这个语句,因为如果一个变量被方法初始化,那么这个方法很有可能抛出一个异常,那我们最常用的方法就是把它置于try块的内部去进行初始化。由此我们可以得出,for循环优于while循环,我们在能使用for循环的地方尽量使用for而不使用while,因为for循环是完全独立的,所以重用循环变量名字不会有任何伤害。
最后我们要记住的是尽量把我们的函数写的小而集中,这样才能真正组做到”最小化局部变量的作用域”这一要旨。
Item 30:了解和使用库
使用标准库,我们可以充分利用编写这些库的Java专家的知识,以及在你之前其他人的使用经验,这就是所谓站在巨人的肩膀上看世界吧~
在每一个Java平台的发行版本里面,都会有许多新的包的加入,和这些更新保持一直是值得的,比如说我们J2ME的开发,在MIDP 1.0的时代,我们要写个Game还要自己动手写工具类,现在MIDP2.0推出之后,大多数写游戏的人都觉得方便了很多,因为在这个版本里面加入了游戏包,为我们的开发节省了大量的人力物力。
Item 31:如果想要知道精确的答案,就要避免使用double和float
对于金融行业来说,对数据的严整性要求是很高的,不容半点马虎,那大家都知道再我们的Java语言里面有两个浮点数类型的变量float和double,可能大家会认为他们的精度对于金融行业这样对数字敏感的行业来说,已经够用了,但是在开发当中,我们要尽量少使用double和float,因为让他们精确的表达0.1是不可能的。那我们如何解决这个问题呢,答案是使用BigDecimal,int或者long进行货币计算。在这里对大家的忠告是:对于商务运算,我们尽量使用BigDecimal,对于性能要求较高的地方,我们有能力自己处理十进制的小数点,数值不太大的时候,我们可以使用int或者long,根据自己的需要来判定具体使用哪一个,如果范围超过了18位数,那我们必须使用BigDecimal。
Item 32:如果其他类型更适合,则尽量避免使用字符串
在偶看到这条建议之前,我就很喜欢用字符串,不管在什么场合下,先String了再说,但是实际上很多情况下,我们要根据实际情况来判定到底使用什么类型,而且字符串不适合替代枚举类型,类型安全枚举类型和int值
都比字符串更适合用来表示枚举类型的常量。字符串也不适合替代聚集类型,有一个更好的方法就是简单的写一个类来描述这个数据集,通常是一个私有的静态成员
类最好。字符串也不适合代替能力表,总而言之,如果可以适合更加适合的数据类型,或者可以编写更加适当的数据类型,那么应该避免使用字符串来表示对象。
Item 33:了解字符串的连接功能
我们经常在使用System.out.println()的时候,往括号里写一串用“+”连接起来的字符串,这是我们最常见的,但是这个方法并不适合规模较大的情形,为连接N个字符串而重复地使用字符串连接操作符,要求N的平方级的时间,这是因为字符串是非可变的,这就导致了在字符串进行连接的时候,前后两者都要拷贝,这个时候我们就提倡使用StingBuffer替代String。
Item 34:通过接口引用对象
通俗的说就是尽量优先使用接口而不是类来引用对象,如果有合适的接口存在那么对使用参数,返回值,变量域都应该使用接口类型养成使用接口作为对象的习惯,会使程序变得更加灵活。
如果没有合适的接口,那么,用类而不是接口来引用一个对象,是完全合适的。
Item 35:接口优先于映像机制
java.lang.relect提供了“通过程序来访问关于已装载的类的信息”,由此,我们可以通过一个给定的Class实例,获得Constructor,Method和Field实例。
映像机制允许一个类使用另一个类,即使当前编译的时候后者还不存在,但是这种能力也要付出代价:
我们损失了了编译时类型检查的好处,而且要求执行映像访问的代码非常笨拙和冗长,并且在性能上大大损失。
通常,普通应用在运行时刻不应以映像方式访问对象。
Item 36:谨慎的使用本地方法
JNI允许Java应用程序调用本地方法,所谓本地方法是指用本地程序设计语言(如C,C++)来编写的特殊方法,本地方法可以在本地语言执行任何计算任务,然后返回到Java程序设计语言中。但是随着JDK1.3及后续版本的推出这种通过使用本地方法来提高性能的方法已不值得提倡,因为现在的JVM越来越快了,而且使用本地方法有一些严重的缺点,比如使Java原本引以为傲的安全性荡然无存,总之在使用本地方法的时候要三思。
Item 37:谨慎使用优化
不要因为性能而牺牲合理的代码结构,努力编写好的程序而不是快的程序,但是避免那些限制性能的设计决定,同时考虑自己设计的API决定的性能后果,为了获得更好的性能而对API进行修改这也是一个非常不好的想法,通常我们在做优化之后,都应该对优化的程度进行一些测量。
Item 38:遵守普遍接受的命名惯例
Java有一套比较完善的命名惯例机制,大部分包含在《The Java Language Specification》,严格得讲这些惯例分成两类,字面的和语法的。
字面涉及包,类,接口,方法和域,语法的命名惯例比较灵活,所以争议更大,字面惯例是非常直接和明确的,而语法惯例则相对复杂,也很松散。但是有一个公认的做法是:“如果长期养成的习惯用法与此不同的话,请不要盲目遵从
Item 12:把类和成员的可访问范围降到最低
好的模块设计应该尽最大可能封装好自己的内部信息,这样可以把模块之间的耦合程度降到最低。开发得以并行,无疑这将加快开发的速度,便于系统地维护。Java中通过访问控制符来解决这个问题。
- public表示这个类在任何范围都可用。
- protected表示只有子类和包内的类可以使用
- private-package(default)表示在包内可用
- private表示只有类内才可以用
你
在设计一个类的时候应该尽量的按照4321得顺序设计。如果一个类只是被另一个类使用,那么应该考虑把它设计成这个类的内部类。通常public的类不应
该有public得字段,不过我们通常会用一个类来定义所有的常量,这是允许的。不过必须保证这些字段要么是基本数据类型要么引用指向的对象是不可修改
的。不然他们将可能被修改。例如下面的定义中data就是不合理的,后面两个没有问题。
public class Con
{
public static final int[] data = {1,2,3};// it is bad
public static final String hello = "world";
public static final int i = 1;
}
Item 13:不可修改的类更受青睐
不可修改的类意思是他们一经创建就不会改变,例如String类。他们的设计、实现都很方便,安全性高——它们是线程安全的。设计不可修改类有几点规则:
- 不要提供任何可以修改对象的方法
- 确保没有方法能够被覆盖,可以通过把它声明为final
- 所有字段设计成final
- 所有字段设计成private
- 确保外部不能访问到类的可修改的组件
不可修改类也有个缺点就是创建不同值得类的时候要创建不同的对象,String就是这样的。通常有个解决的办法就是提供一个帮助类来弥补,例如StringBuffer类。
Item 14:化合(合成)比继承更值得考虑
实现代码重用最重要的办法就是继承,但是继承破坏了封装,导致软件的键壮性不足。如果子类继承了父类,那么它从父类继承的方法就依赖父类的实现,一旦他改
变了会导致不可预测的结果。作者介绍了InstrumentedHashSet作为反例进行说明,原因就是没有明白父类的方法实现。作者给出的解决办法是
通过化合来代替继承,用包装类和转发方法来解决问题。把想扩展的类作为本类的一个private
final得成员变量。把方法参数传递给这个成员变量并得到返回值。这样做的缺点是这样的类不适合回掉框架。继承虽然好,我们却不应该滥用,只有我们能确
定它们之间是is-a得关系的时候才使用。
Item 15:如果要用继承那么设计以及文档都要有质量保证,否则就不要用它
为了避免继承带来的问题,你必须提供精确的文档来说明覆盖相关方法可能出现的问题。在构造器内千万不要调用可以被覆盖的方法,因为子类覆盖方法的时候会出现问题。
import java.util.*;
public class SubClass extends SuperClass
{
private final Date date;
public SubClass()
{
date = new Date();
}
public void m()
{
System.out.println(date);
}
public static void main(String[] args)
{
SubClass s = new SubClass();
s.m();
}
}
class SuperClass
{
public SuperClass()
{
m();
}
public void m()
{
}
}
由于在date被初始化之前super()已经被调用了,所以第一次输出null而不是当前的时间。
由于在Clone()或者序列化的时候非常类似构造器的功能,因此readObject()和clone()方法内最好也不要包括能被覆盖的方法。
Item 16:在接口和抽象类之间优先选择前者
接口和抽象类都用来实现多态,不过我们应该优先考虑用接口。知道吗?James说过如果要让他重新设计java的话他会把所有都设计成接口的。抽象类的优点是方便扩展,因为它是被继承的,并且方法可以在抽象类内实现,接口则不行。
Item 17:接口只应该用来定义类型
接口可以这样用的 Collection c = new xxxx();这是我们最常用的。不要把接口用来做其他的事情,比如常量的定义。你应该定义一个类,里面包含public final static 得字段。
Item 18: 在静态和非静态内部类之间选择前者
如果一个类被定义在其他的类内部那么它就是嵌套类,可以分为静态内部类、非静态内部类和匿名类。
static
member class 得目的是为enclosing class服务,如果还有其他的目的,就应该把它设计成top-level
class。nonstatic member class是和enclosing class
instance关联的,如果不需要访问enclosing class
instance的话应该把它设计成static得,不然会浪费时间和空间。anonymous
class是声明和初始化同时进行的。可以放在代码的任意位置。典型应用是Listener 和process object例如Thread。
由于以前学过C语言,所以对C还是蛮有感情,而JAVA和C又有很多相似之处,很多从C转过来学习JAVA的兄弟,可能一开始都不是很适应,因为很多在C里面的结构在JAVA里面都不能使用了,所以下面我们来介绍一下C语言结构的替代。
Item 19:用类代替结构
JAVA刚面世的时候,很多C程序员都认为用类来代替结构现在太复杂,代价太大了,但是实际上,如果一个JAVA的类退化到只包含一个数据域的话,这样的类与C语言的结构大致是等价的。
比方说下面两个程序片段:
class Point
{
private float x;
private float y;
}
实际上这段代码和C语言的结构基本上没什么区别,但是这段代码恐怕是众多OO设计Fans所不齿的,因为它没有体现封装的优异性,没有体现面向对象设计的优点,当一个域被修改的时候,你不可能再采取任何辅助的措施了,那我们再来看一看采用包含私有域和共有访问方法的OO设计代码段:
class Point
{
private float x;
private float y;
public Point(float x,float y)
{
this.x=x;
this.y=y;
}
public float getX(){retrun x;}
public float getY(){return y;}
public void setX(float x){this.x=x;}
public void setY(float y){this.y=y;}
}
单从表面上看,这段代码比上面那个多了很多行,还多了很多函数,但是仔细想一下,这样的OO设计,似乎更人性化,我们可以方面的对值域进行提取,修改等操作,而不直接和值域发生关系,这样的代码不仅让人容易读懂,而且很安全,还吸取了面向对象程序设计的灵活性,试想一下,如果一个共有类暴露它的值域,那么想要在将来的版本中进行修改是impossible的,因为共有类的客户代码已经遍布各处了。
需要提醒一点的是,如果一个类是包级私有的,或者是一个私有的嵌套类,则直接暴露其值域并无不妥之处。
Item 20:用类层次来代替联合
我们在用C语言来进行开发的时候,经常会用到联合这个概念,比如:
typedef struct{
double length;
double width;
}rectangleDimensions_t;
那我们在JAVA里面没有联合这个概念,那我们用什么呢?对!用继承,这也是JAVA最吸引我的地方之一,它可以使用更好的机制来定义耽搁数据类型,在Bruce Eckel的Thinking in java里面也多次提到了一个和形状有关的例子,我们可以先笼统的定义一个抽象类,即我们通常所指的超类,每个操作定义一个抽象的方法,其行为取决于标签的值,如果还有其他的操作不依赖于标签的值,则把操作变成根类(继承的类)中的具体方法。
这样做的最重要的优点是:类层次提供了类型的安全性。
其次代码非常明了,这也是OO设计的优点。
而且它很容易扩展,即使是面向多个方面的工作,能够同样胜任。
最后它可以反映这些类型之间本质上的层次关系,从而允许更强的灵活性,以便编译时类型检查。
Item 21:用类来代替enum结构
Java程序设计语言提出了类型安全枚举的模式来替代enum结构,它的基本思想很简单:定义一个类来代表枚举类型的单个元素,并且不提供任何公有的构造函数,相反,提供公有静态final类,使枚举类型中的每一个常量都对应一个域。
类型安全枚举类型的一个缺点是,装载枚举类的和构造常量对象时,需要一定的时间和空间开销,除非是在资源很受限制的设备比如蜂窝电哈和烤面包机上,否则在实际中这个问题不会被考虑。
总之,类型安全枚举类型明显优于int类型,除非实在一个枚举类型主要被用做一个集合元素,或者主要用在一个资源非常不受限的环境下,否则类型安全枚举类型的缺点都不成问题,依次,在要求使用一个枚举类型的环境下,我们首先应考虑类型安全枚举类型模式。
Item 22:用类和接口来代替函数指针
众所周知,JAVA语言和C的最大区别在于,前者去掉了指针,小生第一次接触JAVA的时候觉得好不习惯,因为突然一下子没了指针,觉得好不方面啊,C语言的精髓在于其指针的运用,而JAVA却把它砍掉了,让人好生郁闷,不过随着时间的推移,我渐渐明白了用类和接口的应用也同样可以提供同样的功能,我们可以直接定义一个这样一个类,他的方法是执行其他方法上的操作,如果一个类仅仅是导出这样一个方法,那么它实际上就是一个指向该方法的指针,举个例子:
class StringLengthComprator{
public int compare(String s1,String s2)
{
return s1.length()-s2.length();
}
}
这
个类导出一个带两个字符串的方法,它是一个用于字符串比较的具体策略。它是无状态的,没有域,所以,这个类的所有实例在功能上都是等价的,可以节省不必要
的对象创建开销。但是我们不好直接把这个类传递给可户使用,因为可户无法传递任何其他的比较策略。相反,我们可以定义一个接口,即我们在设计具体策略类的
时候还需要定义一个策略接口:
public interface Comparator{
public int compare(Object o1,Object o2);
}
我们完全可以依照自己的需要来定义它。
具体的策略类往往使用匿名类声明。
在JAVA中,我们为了实现指针的模式,声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类,如果一个具体策略只被使用一次的话,那么通常使用匿名类来声明和实例化这个具体策略类,如果一个策略类反复使用,那么它的类通常是一个私有的的静态成员类。
下面我们来讨论一下有关方法设计的几个方面,下面说的几个要点大多数都是应用在构造函数中,当然也使用于普通方法,我们追求的依然是程序的可用性,健壮性和灵活性。
Item 23:检查参数的有效性
非公有的方法我们应该用断言的方法来检查它的参数,而不是使用通常大家所熟悉的检查语句来检测。如果我们使用的开发平台是JDK1.4或者更高级的平台,我们可以使用assert结构;否则我们应该使用一种临时的断言机制。
有些参数在使用过程中是先保存起来,然后在使用的时候再进行调用,构造函数正是这种类型的一种体现,所以我们通常对构造函数参数的有效性检查是非常仔细的。
Item 24:需要时使用保护性拷贝
众所周知,JAVA在代码安全性方面较C/C++有显著的提高,缓冲区溢出,数组越界,非法指针等等,我们的JAVA都有一个很完善的机制来进行免疫,但是这并不代表我们不必去考虑JAVA的安全性,即便在安全的语言,如果不采取措施,还是无法使自己与其他类隔开。假设类的客户会尽一切手段来破坏这个类的约束条件,在这样的前提下,你必须从保护性的方面来考虑设计程序。通过大量的程序代码研究我们得出这样的结论:对于构造性函数的每个可变参数进行保护性拷贝是必要的。需要注意的是,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是原始的对象。对于“参数类型可以被不可信方子类化”的情况,不要用clone方法来进行参数的保护性拷贝。
对
于参数的保护性拷贝并不仅仅在于非可变类,当我们编写一个函数或者一个构造函数的时候,如果它要接受客户提供的对象,允许该对象进入到内部数据结构中,则
有必要考虑一下,客户提供的对象是否是可变的,如果是,则要考虑其变化的范围是否在你的程序所能容纳的范围内,如果不是,则要对对象进行保护性拷贝,并且
让拷贝之后的对象而不是原始对象进入到数据结构中去。当然最好的解决方法是使用非可变的对象作为你的对象内部足见,这样你就可以不必关心保护性拷贝问题
了。):
Item 25:谨慎使用设计方法的原型
(1)谨慎的选择方法的名字:即要注意首先要是易于理解的,其次还要与该包中的其他方法的命名风格相一致,最后当然要注意取一个大众所认可的名字。
(2)
不要追求提供便利的方法:每一个方法都应该提供其应具备的功能点,对于接口和类来方法不要过多,否则会对学习使用维护等等方面带来许多不必要的麻烦,对于
每一个类型所支持的每一个动作,都提供一个功能完全的方法,只有一个方法过于频繁的使用时,才考虑为它提供一个快捷方法。
(3)
避免过长的参数列表:通常在实践中,我们以三个参数作为最大值,参数越少越好,类型相同的长参数列尤其影响客户的使用,两个方法可以避免过长的参数这样的
情况发生,一是把一个方法分解成多个,每一个方法只要求使用这些参数的一个子集;二是创建辅助类,用来保存参数的聚集,这些辅助类的状态通常是静态的。
对于参数类型,优先使用接口而不是类。
这样做的目的是避免影响效能的拷贝操作。
谨慎的使用函数对象。
创建函数对象最容易的方法莫过于使用匿名类,但是那样会带来语法上混乱,并且与内联的控制结构相比,这样也会导致功能上的局限性。
Item 26:谨慎的使用重载
到底是什么造成了重载机制的混淆算法,这是个争论的话题,一个安全而保守的方法是,永远不要导出两个具有相同参数数目的重载方法。而对于构造函数来说,一个类的多个构造函数总是重载的,在某些情况下,我们可以选择静态工厂,但是对于构造函数来说这样做并不总是切合实际的。
当涉及到构造函数时,遵循这条建议也许是不可能的,但我们应该极力避免下面的情形:
同一组参数只需要经过类型的转换就可以传递给不同的重载方法。如果这样做也不能避免的话,我们至少要保证一点:当传递同样的参数时,所有的重载方法行为一致。如果不能做到这一点,程序员就不能有效的使用方法或者构造函数。
Item 27:返回零长度的数组而不是null
因为这样做的原因是编写客户程序的程序员可能忘记写这种专门的代码来处理null返回值。没有理由从一个取数组值的方法中返回null,而不是返回一个零长度数组。
Item 28:为所有导出的API元素编写文档注释
不爱写注释可能是大多数程序员新手的通病(包括偶哈~),但是如果想要一个API真正可用,就必须写一个文档来说明它,保持代码和文档的同步是一件比较烦琐的事情,JAVA语言环境提供了javadoc工具,从而使这个烦琐的过程变得容易,这个工具可以根据源代码自动产生API文档。
为了正确得编写API文档,我们必须每一个被导出的类,接口,构造函数,方法和域声明之前加一个文档注释。
每一个方法的文档注释应该见解的描述它和客户之间的约定。
我们接下来讨论一下Java语言的细节,包括局部变量的处理,库的使用,以及两种不是语言本身提供的机制的使用等等一些大家平时可能忽略的问题。
Item 29:将局部变量的作用域最小化
和C语言要求局部变量必须被生命在代码的开始处相比,Java程
序设计语言宽松得多,它允许你在代码的任何位置声明。要想使一个局部变量的作用域最小化,最高小的技术是在第一次需要使用它的地方声明,变量的作用域是从
声明它的地方开始到这个声明做在的代码块的结束位止,如果我们把变量的声明和代码的使用位置分开的过大,那么对于读这段代码的人来说,是很不幸的。
我们几乎都是在一个局部变量声明的地方同时给它初始化,注意这是很重要的,甚至有时候,如果我们的初始化应该推迟到下一个代码的位置,我们同时应该把声明也往后延迟。这条规则唯一的例外是try-catch这个语句,因为如果一个变量被方法初始化,那么这个方法很有可能抛出一个异常,那我们最常用的方法就是把它置于try块的内部去进行初始化。由此我们可以得出,for循环优于while循环,我们在能使用for循环的地方尽量使用for而不使用while,因为for循环是完全独立的,所以重用循环变量名字不会有任何伤害。
最后我们要记住的是尽量把我们的函数写的小而集中,这样才能真正组做到”最小化局部变量的作用域”这一要旨。
Item 30:了解和使用库
使用标准库,我们可以充分利用编写这些库的Java专家的知识,以及在你之前其他人的使用经验,这就是所谓站在巨人的肩膀上看世界吧~
在每一个Java平台的发行版本里面,都会有许多新的包的加入,和这些更新保持一直是值得的,比如说我们J2ME的开发,在MIDP 1.0的时代,我们要写个Game还要自己动手写工具类,现在MIDP2.0推出之后,大多数写游戏的人都觉得方便了很多,因为在这个版本里面加入了游戏包,为我们的开发节省了大量的人力物力。
Item 31:如果想要知道精确的答案,就要避免使用double和float
对于金融行业来说,对数据的严整性要求是很高的,不容半点马虎,那大家都知道再我们的Java语言里面有两个浮点数类型的变量float和double,可能大家会认为他们的精度对于金融行业这样对数字敏感的行业来说,已经够用了,但是在开发当中,我们要尽量少使用double和float,因为让他们精确的表达0.1是不可能的。那我们如何解决这个问题呢,答案是使用BigDecimal,int或者long进行货币计算。在这里对大家的忠告是:对于商务运算,我们尽量使用BigDecimal,对于性能要求较高的地方,我们有能力自己处理十进制的小数点,数值不太大的时候,我们可以使用int或者long,根据自己的需要来判定具体使用哪一个,如果范围超过了18位数,那我们必须使用BigDecimal。
Item 32:如果其他类型更适合,则尽量避免使用字符串
在偶看到这条建议之前,我就很喜欢用字符串,不管在什么场合下,先String了再说,但是实际上很多情况下,我们要根据实际情况来判定到底使用什么类型,而且字符串不适合替代枚举类型,类型安全枚举类型和int值
都比字符串更适合用来表示枚举类型的常量。字符串也不适合替代聚集类型,有一个更好的方法就是简单的写一个类来描述这个数据集,通常是一个私有的静态成员
类最好。字符串也不适合代替能力表,总而言之,如果可以适合更加适合的数据类型,或者可以编写更加适当的数据类型,那么应该避免使用字符串来表示对象。
Item 33:了解字符串的连接功能
我们经常在使用System.out.println()的时候,往括号里写一串用“+”连接起来的字符串,这是我们最常见的,但是这个方法并不适合规模较大的情形,为连接N个字符串而重复地使用字符串连接操作符,要求N的平方级的时间,这是因为字符串是非可变的,这就导致了在字符串进行连接的时候,前后两者都要拷贝,这个时候我们就提倡使用StingBuffer替代String。
Item 34:通过接口引用对象
通俗的说就是尽量优先使用接口而不是类来引用对象,如果有合适的接口存在那么对使用参数,返回值,变量域都应该使用接口类型养成使用接口作为对象的习惯,会使程序变得更加灵活。
如果没有合适的接口,那么,用类而不是接口来引用一个对象,是完全合适的。
Item 35:接口优先于映像机制
java.lang.relect提供了“通过程序来访问关于已装载的类的信息”,由此,我们可以通过一个给定的Class实例,获得Constructor,Method和Field实例。
映像机制允许一个类使用另一个类,即使当前编译的时候后者还不存在,但是这种能力也要付出代价:
我们损失了了编译时类型检查的好处,而且要求执行映像访问的代码非常笨拙和冗长,并且在性能上大大损失。
通常,普通应用在运行时刻不应以映像方式访问对象。
Item 36:谨慎的使用本地方法
JNI允许Java应用程序调用本地方法,所谓本地方法是指用本地程序设计语言(如C,C++)来编写的特殊方法,本地方法可以在本地语言执行任何计算任务,然后返回到Java程序设计语言中。但是随着JDK1.3及后续版本的推出这种通过使用本地方法来提高性能的方法已不值得提倡,因为现在的JVM越来越快了,而且使用本地方法有一些严重的缺点,比如使Java原本引以为傲的安全性荡然无存,总之在使用本地方法的时候要三思。
Item 37:谨慎使用优化
不要因为性能而牺牲合理的代码结构,努力编写好的程序而不是快的程序,但是避免那些限制性能的设计决定,同时考虑自己设计的API决定的性能后果,为了获得更好的性能而对API进行修改这也是一个非常不好的想法,通常我们在做优化之后,都应该对优化的程度进行一些测量。
Item 38:遵守普遍接受的命名惯例
Java有一套比较完善的命名惯例机制,大部分包含在《The Java Language Specification》,严格得讲这些惯例分成两类,字面的和语法的。
字面涉及包,类,接口,方法和域,语法的命名惯例比较灵活,所以争议更大,字面惯例是非常直接和明确的,而语法惯例则相对复杂,也很松散。但是有一个公认的做法是:“如果长期养成的习惯用法与此不同的话,请不要盲目遵从