对于所有对象都通用的方法
1. 覆盖equals时的通用约定
equals所期望的结果
- 类的每个实例本质上都是唯一的。
- 不关心类是否提供了“逻辑相等”的测试功能。
- 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
- 类时私有的或者包私有的,可以确定它的equals方法永远不会调用。
需要覆盖的时机:
父类没有实现所期望的以上的equals实现
"最多只存在一个对象"的类不需要覆盖equals,例如枚举类型
equals的等价关系
-
自反性。任何非null引用值,x.equals(x)返回true。
Set集合中重复添加同意一个引用值会怎样?
-
对称性。对于任何非null的x和y,x.equals(y) 返回true,那么y.equals(x) 也返回true。
自定义类A实现了不区分大小写的比较,new A("Aa").equals("aa")返回true,但是“aa”.equals(new A("Aa"))的返回值却由String类中的equals方法决定。
-
传递性。对于任何非null的x,y,z,如果x.equals(y) 为true,且 y.equals(z) 也为true,那么x.equals(z) 也为true。
存在扩展可实例化与增加主键值的时候,容易违反传递性。考虑使用抽象类并在其子类中添加属性,例如Shape。
-
一致性。对于任何非null引用值x,y,只要x.equals(y)返回true,那么在引用对象信息没有被修改,那么每次返回的仍是一致的true。
要保证一致性,不要使equals依赖于不可靠的的资源。例如java..net.URL的equals依赖于主机的IP,IP可能随着时间的推移而改变。
对于任何非null引用值x,x.equals(null)必须返回false。
高质量equals
- 使用==检查是待比较参数是否是同意对象引用本身
- 使用instanceof检查参数是否是正确类型
- 把参数转换成正确的类型,使用instanceof判断
- 检查参数中域与该对象中的对应域相匹配。
- 除float 、double外的基本类型使用==比较
- 对象引用域可以递归调用equals
- 对于float,double分别使用Float.compare(),Double.compare()
- 数组判等使用Arrays.equals()。
- 对于允许null的域,尽可能避免NullPointException,可以做判空操作
- 对个域比较的时候,或者比较比较步骤较多的时候,比较顺序从最有可能不一致的开始
注意点
- 覆盖equals的时候总要覆盖hashCode
- 不要让equals过于智能。过度的寻求某种不必要的等价关系。
- 不要将equals中参数转换成其他类型。某些情况能增加性能,但是比较复杂性会增加(不推荐)。
//本应该是:
public void equals(Object o){
....
}
//转换参数成其他类型:
public void equals(MyClass o){
....
}
//加上@Override导致变异不通过,因为父类的equals方法不存在此重写方法。
@Override
public void equals(MyClass o){
....
}
2.覆盖equals时总要覆盖hashCode
JavaSE6中的Object规范
- 程序执行期间,对象的equals方法所用到的比较信息没有被修改,对同一个对象的多次调用hashCode始终如一的返回同一个整数,但是多次执行的过程中,所返回的整数可以不一致(信息修改后)。
- 两个对象的equals返回true,那么这两个对象的hashCode返回的整数必须相等。(所以覆盖equals总要覆盖hashCode)
- 两个对象的equals返回false,那么不要求两个对象的hashCode返回不一样的整数结果,但是在使用hash的相关集合框架中,返回不一样的结果能提高性能
HashMap,HashSet等hash集合框架,依赖元素的hashCode进行散列,因此当equals返回true的时候,hashCode返回的结果也一样,可以保证是一个相等的元素。在put ,get,remove等操作均会使用到元素的hashCode。因此hashcode能影响到HashMap等Hash结合框架的性能。
计算hashcode的一些方法:
- boolean 计算(f?1:0)
- byte,char,short,int 计算(int)f
- long 计算(int)(f^(f>>>32))
- float 计算Float.floatToInBits(f);
- double 计算Double.doubleToLongBits(f),得到的结果再按long类型处理
- 对象引用 null则返回0,否则递归调用hashCode
- 数组 用以上规则计算每个元素的hashCode,或则使用Arrays.hashCode()
最后返回将上述计算得到的结果c,(result = 31 * result +c),返回 result。
注意:散列码计算过程中排除掉冗余域,也就是没有参与到equals中的域
上述使用到了31进行最后结果的处理,因为31有一个很好的特性是可以使用移位和减法来代替乘法。31*i = (i<<5)-i 而且VM会自动完成这种优化。
此外散列码的计算可能是开销很大的,可以考虑懒加载,既是只在第一次调用hashCode的时候进行计算,然后结果保存在实例中,下次直接返回保存的结果。但是前提是类是不可变的。
3.始终覆盖toString
Object默认的toString返回的结果是:类名@xxxxxx,其中xxxxxx是该对象的散列码的十六进制表示
覆盖toString方法可以在调试或者打印对象信息的时候更易于阅读理解。
但是在该类被广泛使用的时候要保证toString的返回格式的一致性,可利于维护。
4.谨慎覆盖clone
在JavaSE6中的clone方法的通用约定:
x.clone()!=x | true |
---|---|
x.clone().getClass() == x.getClass() | true(不绝对要求如此) |
x.clone().equals(x) | true(不绝对要求如此) |
Object中的clone方法是protected的,而一个类实现了Cloneable接口改变了clone的行为,是的clone可以返回一个该对象得到逐域拷贝对象,使得不通过构造器就可以生成一个对象,否则抛出CloneNotSupportedExcetion。
如果覆盖了非final类的clone方法,则应该返回一个通过super.clone()得到的对象。而对于实现了Cloneable接口的类,如果所有超类否提供了良好的clone实现,那么我们可以在实现了Cloneable的子类实现一个共有的clone的方法,否则我们不应该提供任何clone实现。
当需要clone的类中存在引用类型且不是final的域的时候,我们仍需要调用该引用对象的clone方法进行clone。
class Wheel implements Cloneable{
int size;
int count;
@Override
public Wheel clone(){
return (Wheel)super.clone();
}
}
class Car implements Cloneable{
Wheel wheel;
String name;
@Override
public Car clone(){
try{
Car car = (Car)super.clone();
car.wheel = wheel.clone();//省略这一行,那么car.wheel == this.wheel(浅拷贝)
return car;
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
}
另一种情况,存在引用链的情况下,我们需要递归的进行复制
public class HashTable implements Cloneable{
private Entry[] buckets = .....;
private static class Entry{
final Object key;
Object value;
Entry next;
Entry(Object key,Object value,Entry e){
....
}
public deepCopy(){
//递归调用可能会栈溢出
return new Entry(key,value,next==null?null:next.deepCopy());
//使用迭代
Entry e = new Entry(key,value,next);
for(Entry p = e;p.next!=null;p=p.next)
p.next = new Entry(key,value,p.next.next);
return e;
}
}
@Override
public HashTable clone(){
try{
HashTable ht =(HashTable)super.clone();
//这样得到的buckets中的元素与this的buckets中的元素时指向同一个对象的(浅拷贝)
//ht.buckets = buckets.clone();
//单独拷贝每个buckets中的链表元素(深拷贝)
ht.buckets = new Entry[buckets.length];
for(int i=0,s=buckets.lenght;i<s;i++){
if(buckets[i]!=null){
ht.buckets[i] = buckets[i].deepCopy();
}
}
return ht;
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
}
多线程环境霞需要自己实现同步的clone
其他实现对象拷贝的方法:
拷贝构造函器
public Car(Car car){
.....
}
静态工厂拷贝方法
public static Car newInstance(Car car){
......
}
Java中的使用例子:
基于接口的拷贝:Collection, Map。HashSet hs = new HashSet(); TreeSet ts = new TreeSet(hs);
4.考虑实现Comparable接口
comparable接口属于一个泛型接口,一个类实现了该接口可以和许多依赖该接口的集合实现协作功能,很明显的一个功能就是内在的排序关系。
Comparable接口中待实现的的方法是compareTo(T t)。返回正整数,0,负整数来表示当前对象大于,等于,小于参数对象。
compareTo同样应该满足:自反性,传递性,对称性
compareTo返回0是,同样应该满足equals返回true
compareTo需要比较的值域较多是,从最右可能产生不一致的域开始比较,依次类推
对于float double类型的比较,使用Float.compare() ,Double.compare();
compareTo与equals的差别是compareTo是参数化的,不必进行参数转化,此外其他的很多equals中的特点同样适用与compareTo。