如果equals方法被覆盖,hashCode方法也必须被覆盖

Java语言规范中为Object.hashCode方法制定了须遵守的合约:

1.在程序的同一次运行(execution)中,给定某个对象,如果此对象用于计算equals返回值的信息没有改变,那么多次执行其hashCode方法,都必须返回同样的哈希值。当然,不同的运行之间,哈希值可以不同。

2.如果两个对象,依据equals方法的判定,是相等的,那么他们的哈希值必须相同。

3.如果两个对象,依据equals方法的判定,是不相等的,那么他们的哈希值不一定不等。如果此情况下可以保证两哈希值不等,那么所有依赖哈希值的数据结构(HashMap,HashSet和Hashtable)的性能将被优化。

如果上述合约被破坏,在依赖哈希值的数据结构中,程序的行为将会出错。

覆盖equals方法时要遵守的通用合约

覆盖equals方法并不像看上去那样简单,所以除非必要,还是不要覆盖为好。以下情况下,equals方法是不需要被覆盖的:

1.一个类的所有对象本质上说都是唯一的。比如Thread类,它的对象全都用来执行指令,因而他们在对象层面上的数据并不重要。这种情况下Object本身的equals实现已经足够。

2.对于某些类,我们并不在意对其对象间的"逻辑相等性"。比如java.util.Random。

3.父类已经定义了可正确应用于子类的equals。

4.私有类,并且可以确定其equals方法永远不会被执行。

避免使用finalizer

Java语言规范里没有要求finalizer何时被执行,甚至没要求它一定要被执行。各种不同的jvm实现完全可能以它们自己的方式来处理finalizer,所以finalizer里的代码执行没有任何保障,从这点上来说,很多c++程序员把finalizer作为c++析构函数在Java中的对应功能是完全错误的。如果错误地依赖finalizer来释放被占用的资源或者执行某些时序任务,很可能会造成不可预料的错误行为。

Java面试里面一个烂大街的问题就是System.gc()方法有什么作用,其实还有一个类似的方法System.runFinalization(),两者没太大区别,都是向jvm提出gc请求,但根本无法保证jvm会及时作出响应。当然,这两个方法还是提高了finalizer被执行的可能性。

finalizer的另一个问题是异常处理。当其中的代码抛出异常时,如果没有显式定义catch处理,这些异常将被完全忽略,所在线程会瞬间终止,连stack trace都不会打印。

移除过期的对象引用

这节讲的是关于内存泄露的问题。Java有三大主要的内存泄露来源:

1.开发者自己维护的内存

2.缓存

3.listener和callback

第一类:开发者自己维护的内存。这类问题通常是开发者从其它编程语言(例如c++)带来的坏习惯导致,比如下面这段代码:

避免创建不必要的对象

最常见的例子是String,一个小小的String对象看起来不起眼,但在复杂的大型系统中,如果不恰当创建,可能会带来极大的性能开销,比如:

String s = new String("stringette"); // DON'T DO THIS!

这行代码的问题是,作为参数的"stringette"本身已经是所需要的String对象了,菜鸟程序员却调用一个String类的构造函数来再次构造它。这行代码每执行一次,就会在栈里面建立一个一模一样的"stringette"对象。在高频执行的环境下,内存开销会非常大,甚至引起stack overflow。正确的做法是:

又是简单而常用的东西

通过私有构造函数来禁止类被对象化

主要用于各种helper或者factory类,这些类只包含一些工具性的方法,通过静态调用提供服务,完全没必要被对象化。但如果不做特殊处理,编译器会为这些类提供一个默认的无参数构造函数,所以仍然会偶尔被粗心的用户对象化。需要注意的是,把它们定义为抽象类(abstract)并不管用,因为只要定义一个子类,对象化仍然可以完成,而且抽象关键字很容易引起误导,让人觉得这些类就是用来继承的,这就与我们的目的背道而驰了。

正确的方法是定义一个私有的无参数构造函数:

第3页 共4页