有些类不希望被实例化,对它们实例化也没有意义。
因为声明的构造函数是私有的,所以它在类的外部不可访问。假设构造函数不会被类自身从内部调用,即能保证类永远不会被实例化。 例:
class F {
private F() {
...
}
...
}
重用同一对象比每次都创建功能等同的新对象通常更适合。重用方式既快速也更加时尚。
终结程序(finalizer)的行为是不可预测的,而且是危险的,通常也不必要。不要把终结程序作为C++析构函数(destructor)的类似物。
终结程序无法保证能别及时的执行,这意味着不能用终结程序来处理时间关键(time-critical)性的操作。例如,依赖终结程序去关闭打开的文件是一个严重的错误,因为打开的文件描述符是一种有限资源,而JVM不会及时地安排终结程序执行,如果多个文件处于打开状态,程序有可能会因为无法再打开文件而执行失败。
JLS不仅不保证终结程序的及时执行,它甚至不保证终结程序会获得执行。永远不要依赖终结程序去更新关键的持续状态(persistent state)。例如,依靠终结程序释放一个共享资源如数据库上的持续锁,将是导致所有分布系统跨掉的绝佳方法。
非可变类就是类的实例不能被修改的类。如String。
为了使类成为非可变的,要遵循下面5条原则
下面是一个稍微复杂的例子:
public final class Complex {
private final float re;
private final float im;
public Complex(float re, float im) {
this.re = re;
this.im = im;
}
public float realPart() { return re; }
public float imaginaryPart() { return im; }
public Complex add(Complex c) {
return new Complex(re+c.re,im+c.im);
}
...
public boolean equals(Object o) {
if(o==this)
return true;
if(!(o instanceOf Complex))
return false;
Complex c = (Complex) 0;
return(Float.floatToIntBits(re) == Float.floatToIntBits(c.re) ) &&
(Float.floatToIntBits(im) == Float.floatToIntBits(c.im));
}
public int hashCode() {
int result = 17 + Float.floatToIntBits(re);
result = 37*result + Float.floatToIntBits(im);
return result;
}
public String toString() {
return "("+re+" + "+im+"i)";
}
}
这个类表示复数,注意到算术操作创建和返回一个新的复数实例,而不是修改了这个实例。
继承是实现代码重用的有力途径,但它不总是完成这项工作的最后的工具。与方法调用不同,继承打破了封装性。子类的特有的功能,依赖于它的超类的实现细节。超类的实现会随着版本而改变,如果出现这样的情况,即使不触动子类的代码,它也会被破坏。
不去扩展现有的类,而是给类增加一个引用现有类实例的新的私有域,这种设计方法被成为复合(composition),因为现有的类成为了新的类的一部分。
继承只有当子类确实是超类的“子类型”(subtype)时,才是适合的。换句话说,对两个类A和B,如果“B是A”的关系存在,那么B应该扩展A。在把B扩展A时,问这样一个问题:“任何的B都是A吗?”,如果答案是否定的,那么通常应该把A作为B的一个私有实例,然后暴露更小、更简单的API:A不是B的基本部分,只是它的实现细节。
类必须提供文档准确地描述重载任一方法的效果。类必须说明它的可重载方法的自用性(self-use):对每个公共的或受保护的方法或构造函数,它的文档信息都必须要表明它在调用哪一个可重载的方法、以什么顺序调用及每一个调用的结果如何影响后面的处理。
为了允许程序员有效地进行子类化处理而不必承受不必要的痛苦,类必须用认真选择的受保护方法提供它内部实现的钩子(hook)。
构造函数一定不能调用可重载的方法,无法直接地还是间接地。超类构造函数会在子类构造函数之前运行,所以子类中的重载方法会在子类构造函数运行之前被调用。如果重载方法依赖于由子类构造函数执行的初始化,那么该方法将不会按期望的方式执行。
clone和readObject方法都不能调用可重载的方法,无论是直接的还是间接的。如果确定要用来继承的类实现Serializable,并且类中有一个readResolve或writeReplace方法,那么必须使readResolve或writeReplace方法成为受保护的而不是私有的。一旦这些方法是私有的,它们就将被子类悄然忽略。
对那些不是专门设计用于安全地实现子类化并具有文档说明的类,禁止子类化。
禁止子类化的方法有两种
Java语言为定义允许有多种实现的类型提供了两种机制:接口和抽象类。
现有的类可以很容易的被更新以实现新的接口。所有需要做的工作是增加还不存在的方法并在类的声明中增加一个implement语句。
接口是定义混合类型(mixins)的理想选择。mixin是这样的类型:除了它的“基本类型(primary type)”外,类还可以实现额外的类型,以表明它提供了某些可选的功能。
接口允许非层次类型框架的构造。对于组织某些事物,类型层次是极好的选择,但有些事物不能清楚地组织成严格的层次。
接口通过使用封装类方式,能够获得安全、强大的功能。如果使用抽象类定义类型,那么程序员在增加功能是,除了使用继承外别无选择,而且得到的类与封装类相比功能更差、更脆弱。
尽管接口不允许方法实现,但使用接口定义类型并不妨碍给程序员提供实现上的帮助。可以通过抽象的构架实现类与希望输出的所有重要的接口配合,从而将接口与抽象类的优点组合在一起。
使用抽象类定义具有多个实现的类型与使用接口相比有一个明显优势:演进抽象类比演进接口更容易。如果在以后的版本重,需要给抽象类增加新方法,那么总可以增加一个包含正确的缺省实现的具体方法。此时所有现存的该抽象类的实现都会具有这个新的方法。
嵌套类(nested class)是一种定义在其他类内部的类。嵌套类应该仅仅为包容它的类而存在。
- 有四种类型嵌套类:
静态成员类是最简单的嵌套类。它最好被看做是普通的类碰巧被声明在其他的类的内部。它对所有封闭类中的成员有访问权限,甚至那些私有成员。静态成员类的一种通常用法是作为公共的辅助类,仅当和它的外部类协作时才有意义。
如果声明了一个不要求访问封闭实例的成员类,切记要在它的声明里使用static修饰符,把成员类变为静态的。 私有静态成员类的一般用法是用来表示它们的封闭类对象的组件。
每一个非静态的成员类与它包含类的封闭实例(enclosing instance)隐式地关联。如果嵌套类的实例可以在它的封闭类实例之外单独存在,那么嵌套类不能成为非静态成员类:创建没有封闭实例的非静态成员类实例是不可能的。非静态成员实例与它的封闭实例之间的关联在前者被创建时即建立,在此之后它不能被修改。
过长的匿名类会伤害程序的可读性。
//Typical use of an anonymous class
Arrays.sort(args, new Comparator() {
public int compare(Object o1,Object o2) {
return ((String)o1).length() - ((String)o2).length();
}
}
每个类实例本质上是唯一的。
不关心类是否提供了“逻辑意义的等同”(logical equality)测试。例如java.util.Random本来可以重载equals方法,用以检查两个Random实例是否会产生相同的随机数序列,但设计者不认为客户会需要或想要这个功能。这种情况下,使用从Object继承的equals实现就够了。
超类已经重载了equals,而从超类继承的行为适合该类。
类是私有的或包内私有(package-private)的,而且可以确定它的equals方法永远不会被调用。
在重载equal方法时要重载hashCode方法。
不要使自己聪明过头。把任何的同义形式考虑在比较的范围内一般是糟糕的想法,例如File类不应该与指向同一文件的符号链接进行比较,实际上File类也没有这样做。
不要设计依赖于不可靠资源的equals方法。
不要将equals声明中的Object替换为其他类型。程序员编写出形如下面所示的equals方法并不少见,它会让人摸不清头脑:所设计方法为什么不能正确工作:
public boolean equals(Myclass o) {
...
}
问题出在这个方法没有重载(override)参数为Object类型的Object.equals方法。而是过载(overload)了它。这在正常的equals方法中,又提供了一个“强类型”的equals方法。
一定要在每一个重载了equals的类中重载hashCode方法。不这样做会违背Object.hashCode的一般约定,并导致你的类与所有基于散列的集合一起作用时不能正常工作,这些集合包括HashMap、HashSet和Hashtable。
不重载hashCode方法违背了java.lang.Object的规范:相等的对象必须有相等的散列码。两个截然不同的实例根据类的equals方法也许逻辑上是等同的,但对于Object类的hashCode方法,它们就是两个对象,仅此而已。因而对象的hashCode方法返回两个看上去是随机的数值,而不是约定中要求的相等的值。
好的hash函数倾向于为不相等的对象生成不相等的hash码。理想的情况下,hash函数应该把所有不相等的实例的合理集合均一地分布到所有可能的hash值上去。达到理想状态很难,但是下面有一种相对合适的方法
i.如果域是boolean型,计算(f?0:1)。
ii.如果域是byte型、char型、short型或int型,计算(int)f。
iii.如果域是long型,计算(int)(f^(f>>>32))。
iv.如果域是float型,计算Float.floattoIntBits(f)。
v.如果域是double型,计算Double.doubleToLongBits(f),然后如2.a.iii所示,对long型结果进一步处理。
vi.如果域是对象引用,而且这个类的equals方法又递归地调用了equals方法对域进行比较,那么对这个域递归地调用hashCode方法。如果需要一种更复杂的比较方式,那么先为这个域计算出“范式表示”,然后在该“范式表示”上调用hashCode方法。如果域为null,则返回0。
vii.如果域是数组,则把每个元素作为分离的域对待。即递归地使用这些规则,为每个“主要元素”计算hash码。然后用2.b所示方法复合这些值。
如果方法没有对参数做检查,会出现几种情形。
对那些不被方法使用但会被保存以供使用的参数,检查有效性尤为重要。
一种重要的例外是有效性检查开销高,或者不切实际,而且这种有效性检查在计算的过程中会被隐式地处理的情形。
总的来说,每次在设计方法或设计构造函数时,要考虑它们的参数有什么限制。要在文档中注释出这些限制,并在方法体的开头通过显示的检查,对它们进行强化。养成这样的习惯是重要的,有效性检查所需要的不多的工作会从它的好处中得到补偿。
必须在客户会使用一切手段破坏类的约束的前提下,保护性地设计程序。
为了保护Period实例的内部细节免于这种攻击,对构造函数的每一个可变参数使用保护性拷贝是必要的。
保护性拷贝要在参数的有效性检查的前面,并且有效性检测要在副本而不是初值上执行。
几乎每次返回null而不是返回0长度数组的方法时,都需要这种多余地处理。返回null是易出错的,因为写代码的程序员可能会忘记设计处理返回null的特殊情形的代码。
如果存在合适的接口类型,那么参数、返回值、变量和域应该用接口类型声明。
应该养成下面这样的程序习惯:
//Good - uses interface as type
List subscribers = new Vector();
而不要这样做:
//Bad - uses class as type!
Vector subscribers = new Vector();
如果养成了使用接口作为类型的习惯,程序就会有更好的扩展性。当希望转换实现时,需要做的全部工作就是改变构造函数中类的名字。
联系客服