JAVA文集程序员首页投稿(暂停使用,暂停投稿)

Effective Java 读书笔记(2)

2017-08-31  本文已影响85人  Lin_Shao

Object 通用方法

Object是一个具体类,但是设计它主要是为了扩展,其所有的非final方法(equals、hashCode、toString、clone、finalize)都有明确的通用约定,我们可以在遵守通用约定的前提下override这些方法。

注:不遵守通用约定(general contract)复写Object类的非final方法,可以会导致依赖于这些约定的类(hashMap之类的)无法正常工作。


第8条:覆盖equals时请遵守通用约定

Object的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引用值

实现高质量equals的诀窍:

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、优化比较顺序:优先比较最有可能不一致的域、比较开销最低的域。不比较不属于对象逻辑状态(值)的域,或者冗余域(除非其能代表大量关键域,比较其能节省大量时间)

最后的告诫:


第9条:覆盖equals时总要覆盖hashCode

在Object类中,hashCode方法是一个native方法,返回值与对象的存储地址相关,计算方法由jvm决定。

hashCode通用公约(JavaSE6):

注: HashTable、HashMap、HashSet等散列集合类的散列算法实现往往依赖于hashCode。假设我们覆盖了equals方法,但没有覆盖hashCode,那么在我们理解中equals的对象,对于散列集合类而言,可能是不相等。

高质量的hashCode方法的原则:

编写好质量的hashCode的诀窍:

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方法的通用公约:
创建和返回该对象的一个拷贝。这个拷贝的精确含义由该对象的类决定。一般有以下含义:

拷贝对象往往会导致创建它的类的一个新实例,但它同时要求拷贝类内部的数据结构。这个过程没有调用构造器。

覆盖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的通用公约:
将这个对象与指定的对象进行比较,当对象

与equals类似,compareTo同样有自反型传递性对称性
编写compareTo方法与编写equals方法非常相似,但也有一些区别:

上一篇 下一篇

猜你喜欢

热点阅读