Effective Java Note(对于所有对象都通用的方法)

时间:2022-12-03 16:01:38

对于所有对象都通用的方法

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

  1. 使用==检查是待比较参数是否是同意对象引用本身
  2. 使用instanceof检查参数是否是正确类型
  3. 把参数转换成正确的类型,使用instanceof判断
  4. 检查参数中域与该对象中的对应域相匹配。
  5. 除float 、double外的基本类型使用==比较
  6. 对象引用域可以递归调用equals
  7. 对于float,double分别使用Float.compare(),Double.compare()
  8. 数组判等使用Arrays.equals()。
  9. 对于允许null的域,尽可能避免NullPointException,可以做判空操作
  10. 对个域比较的时候,或者比较比较步骤较多的时候,比较顺序从最有可能不一致的开始

注意点

  • 覆盖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。