Object 通用方法
Object是一个具体类,但是设计它主要是为了扩展,其所有的非final方法(equals、hashCode、toString、clone、finalize)都有明确的通用约定,我们可以在遵守通用约定的前提下override这些方法。
注:不遵守通用约定(general contract)复写Object类的非final方法,可以会导致依赖于这些约定的类(hashMap之类的)无法正常工作。
第8条:覆盖equals时请遵守通用约定
Object的equals原意:类的实例只与它本身相同,即比较地址相同,不比较值。
在以下任意情况下不应覆盖equals方法:
- 类的每个实例本质上都是唯一的,即比较地址不比较值(如Thread)
- 不关心类是否提供了“逻辑相等(值相等)”的测试功能
- 超类已覆盖了equals方法,且适用于子类(如Set实现类从AbstractSet继承equals方法,List实现类从AbstractList继承equals方法)
- 类是私有的或是包级私有的,可以确定其equals不会被调用。
Tip:禁止调用可以在对应的方法体内抛出错误
throw new AssertionError()
如果类具有其特有的“逻辑相等”概念。并且超类没有实现期待的行为,此时我们需要覆盖equals方法。这通常属于“值类(value class)”的情况。值类仅仅是一个表示值的类,如Integer或者Date。当我们调用值类的equals时,往往是为了判断其值是否相等,而不关心是否为同一个对象(可以使用==判断是否为同一实例)。
Tip: Set、List、Map等集合类多使用equals判断key是否相等,所以我们需要在使用这些类时确保我们的equals方法是符合我们的预期的。
equals方法的通用约定(JavaSE6):
x、y、z皆为非null引用值
- 自反性(reflextive):
assert x.equals(x);
- 对称性(symmetric):
if(x.equals(y)) assert y.equals(x);
- 传递性(transitive):
if(x.equals(y)&&y.equals(z)) assert x.equals(z);
- 一致性(consistent):如果x、y未被修改,则调用x.equals(y)应一直返回true或者一直返回false
- 非空性(Non-nullify):x.equals(null)必须返回false,
assert !x.equals(null)
实现高质量equals的诀窍:
- 使用==操作符检查“参数是否为这个对象的引用”
- 使用instanceof操作符检查“参数是否为正确的类型”
- 把参数转换为正确的类型
- 对于该类的“关键域”,逐一比较(见以下Tip)
- 回顾并进行单元测试:对称性、传递性、一致性
Tip: 逐一比较的诀窍:
1、对于不是float、double的基本类型域,可以直接使用==进行比较;
2、对于对象引用域,可以递归地调用equals方法进行比较;
3、对于float域,可以使用Float.compare方法;对于double域,可以使用Double.compare方法;
4、对于数组域,可以使用Array.equals方法
5、使用field == null ? o.field == null : field.equals(o.field)
或(field == o.field) || (field != null) && (field.equals(o.field))
习惯写法
6、优化比较顺序:优先比较最有可能不一致的域、比较开销最低的域。不比较不属于对象逻辑状态(值)的域,或者冗余域(除非其能代表大量关键域,比较其能节省大量时间)
最后的告诫:
- 覆盖equals时总要覆盖hashCode(见第9条);
- 不要企图让equals过于智能;
- 不要将equals声明中的Object对象替换为其他的类型(这样是overload而不是override),可以添加
@override
注解帮助规避错误。
第9条:覆盖equals时总要覆盖hashCode
在Object类中,hashCode方法是一个native方法,返回值与对象的存储地址相关,计算方法由jvm决定。
hashCode通用公约(JavaSE6):
- 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个程序多次执行过程中,每次执行所返回的整数可以不一致;
- 如果两个对象根据equals方法比较是相等的,那么调用者两个对象的hashCode方法必须返回同一个整数;
- 如果两个对象根据equals方法比较是不相等的,那么调用者两个对象的hashCode方法不一定返回不同整数。给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能;
注: HashTable、HashMap、HashSet等散列集合类的散列算法实现往往依赖于hashCode。假设我们覆盖了equals方法,但没有覆盖hashCode,那么在我们理解中equals的对象,对于散列集合类而言,可能是不相等。
高质量的hashCode方法的原则:
- 对于equals的对象,返回同一个整数;
- 对于非equals的对象,尽可能返回不一样的整数;
- 为不相等的对象均匀产生不相等的散列码。
编写好质量的hashCode的诀窍:
- 把某个非零的常数值(如17)保存在一个名为result的int类型的变量中;
- 对于对象中的每个关键域f,完成以下步骤:
1、为该域计算int类型的散列码c(见以下Tip);
2、计算result = 31 * result +c
,合并c - 返回result
- 编写单元测试,保证相等的实例能得到同一个散列值
TIp: 计算int类型的散列码c
1、如果该域是boolean类型,则计算f ? 1 : 0
;
2、如果该域是byte、char、short、int类型,则计算(int) f
;
3、如果该域是long类型,则计算(int)( f ^ (f>>>32))
;
4、如果该域是float类型,则计算Float.floatToTintBits(f)
;
5、如果该域是long类型,则计算'Double.doubleToLongBits(f)',然后按照步骤3为long计算散列值;
6、如果该域是一个对象引用,为null则返回0,否则递归调用hashCode;
7、如果该域是一个数组,调用Arrays.hashCode
;
在计算散列码时,只能用到覆盖的equals函数比较的关键域,否则相等(equals)的对象可以会得到不同的散列值。
初始化常数值17是任选的,不为0就可以了,目的在于增加散列值为0的关键域的影响。
31是一个比较特殊的数字,首先它是一个奇素数,如果乘数为偶数,则相当于移位,会增加冲突。再者,其乘法可以被jvm自动优化:31 * i == (i << 5 ) - i
。
Tip:如果一个对象是不可变的,或者其散列值计算开销比较大,可以考虑将其散列值在实例初始化时计算后缓存在对象内部。
第10条:始终要覆盖toString
在Object类中,toString会返回类名,一个@符号,和散列码的无符号十六进制表示法,这通常不是我们所希望看到的。
toString的通用约定指出,被返回的字符串应该是一个简洁的,信息丰富的,易于阅读的表达形式,并建议所有的子类都override这个方法。
提供好的toString实现可以使类用起来更加舒适,特别是在打印对象信息的时候,我们可以之间将对象作为参数直接传入print函数,打印出对象信息。
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息
在实现toString时,还应考虑是否在文档中指定返回值的格式。若是指定格式,最好再提供一个静态工厂方法或者构造器,以便从这种表示法中转换对象;若是不指定返回值的格式,则可以保持toString方法的灵活性,以便在后期改进格式。
第11条:谨慎覆盖clone方法
Cloneable接口的目的在于表明这个对象是允许被clone的,然而它并没有成功地达到这个目的。其主要原因在于它并没有包含任何方法,而Object的clone方法是受保护的,需要反射调用,而且反射调用也不一定会成功。
Cloneable接口的作用在于表明:实现了Cloneable接口的类,应该覆盖了Object的clone方法。
clone方法的通用公约:
创建和返回该对象的一个拷贝。这个拷贝的精确含义由该对象的类决定。一般有以下含义:
- 对于任何对象x,表达式
x.clone() != x
将会是true - 对于任何对象x,表达式
x.clone().getClass() == x.getClass
将会是true - 对于任何对象x,表达式
x.clone().equals(x)
将会是true
拷贝对象往往会导致创建它的类的一个新实例,但它同时要求拷贝类内部的数据结构。这个过程没有调用构造器。
覆盖clone方法,必须保证其拷贝是深度拷贝,这往往需要其超类实现了良好的clone方法。其次,对于拷贝对象内部的引用对象,我们需要实例化一个新的对象,并修改其值,而不能简单复制引用。覆盖clone方法是一件十分吃力不讨好的事。
所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法首先调用super.clone()
,然后修正任何需要修正的域。
除非你扩展了一个实现Cloneable接口的类,否则最好提供某些其他的途径代替clone方法,或者干脆不提供这样的功能。
我们可以提供一个拷贝构造器或者拷贝工厂来替代clone方法:
// 拷贝构造器:以拷贝对象为参数
public Yum(Yum yum)
// 拷贝工厂
public static Yum newInstance(Yum yum)
更进一步,我们可以提供一个转换构造器,其参数是一个超类/接口的对象。按照惯例,所有通用集合实现都提供了一个转换构造器,如一个HashSet s
,可以通过TreeSet(s)
转换类型。
注意:对于一个专门为了继承而设计的类,如果你未能提供香味良好的受保护的clone方法,它的子类就不可能实现Cloneable方法。
第12条:考虑实现Comparable接口
compareTo方法是Comparable接口唯一的方法,它与Object类的通用方法有很大的相似性。compareTo方法不但允许进行简单的等同性比较,而且允许执行顺序比较。类实现了Comparable接口,就表明它的实例具有内在的排序关系。对于实现了Comparable接口的对象数组进行排序:Arrays.sort(a)
一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进项协作。事实上,java的所有公共值类都实现了这个接口。假设你正在编写一个值类,它具有非常明显的内在排序关系,那么你就应该坚决考虑实现这个接口。
compareTo的通用公约:
将这个对象与指定的对象进行比较,当对象
- 小于指定对象时,返回负整数;
- 等于指定对象时,返回0;
- 大于指定对象时,返回正整数;
- 无法与指定对象进行比较,抛出
ClassCastException
与equals类似,compareTo同样有自反型、传递性、对称性
编写compareTo方法与编写equals方法非常相似,但也有一些区别:
- 不用类型检查与转换,compareTo方法的参数是静态类型,不是Object
- 当参数为null时,应该抛出NullPointException