Effective java 总结 - 对于所有对象都通用的方法
Object 是一个具体的类,它的所有的非final方法: equals,hashcode, toString, clone, finalize 被设计成需要覆盖的,因此有自己的通用约定,覆盖的时候需要遵循这些约定。
第10条 覆盖equals时请遵守通用约定
不用覆盖equals方法的情况
- 类的每个实例本质上都是唯一的, 代表活动实体而不是值的类:Thread
- 类没有必要提供“逻辑相等”的测试功能, 例如 java.util.regex.Pattern可以覆盖equals
- 超类已经覆盖了equals, 超类的行为对这个类也是合适的,eg: set,list,map
- 类是私有的,或者是包级私有的,可以确定它的equals方法永远不会被调用
- 特例,实例受控确保,每个值至多存在一个对象的类,eg: 枚举类型
需要覆盖equals的情况
- 类具有自己特有的“逻辑相等”,且超类没有覆盖equals,通常属于值类,且可用作map的key, 或者set的元素
覆盖equals方法的通用约定: 等价关系
- 自反性: x.equals(x) 一定为TRUE,x非null
- 对称性: x.equals(y)为TRUE时, y.equals(x)也为TRUE
- 传递性: x.equals(y)为TRUE,y.equals(z)为True,则x.equals(z)为TRUE
- 一致性:x.equals(y) 被多次调用的返回值一致
- 对于任何非null的引用值x, x.equals(null) 必须返回false
结论
-
一旦违反了equals约定,当其他对象面对你的对象时,这些对象的行为将不可控
-
无法在扩展实例化类的同时,既增加新的值组件,同时又保留equals约定
-
抽象类的子类中增加新的值组件不违反equals约定,只要不可能直接创建超类的实例即可
-
权宜之计:复合优先于继承
-
不要使equals方法依赖不可靠的资源
-
equals方法必须使用instanceof操作符来检查参数的类型是否正确,不可以用getclass替代(子类问题)
java平台类库反面案例
- java.sql.Timestamp 对java.sql.Date进行了扩展(nanosenonds)域,Timestamp中的equals方法违反了对称性,因此不要混用这两个类型对象
- java.net.URL 的equals方法依赖于对URL中主机IP地址的比较,有可能IP地址发生了改变,导致违反约定
高质量equals方法诀窍
-
使用 == 操作符检查:参数是否为这个对象的引用
-
使用instanceof操作符检查:参数是否为正确的类型
-
把参数转换为正确的类型
-
对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配
- 类型是接口,通过接口方法访问参数的域
- 类型是类,直接访问参数的域
- 非float,double类型基本域, 直接用==操作符比较
- float域,可以使用静态的Float.compare(float, float)方法
- double域,使用Double.compare(double, double)
- 数组域,可以使用Array.equals
- 有些对象引用域含有null,可以使用Objects.equals(Object, Object)
-
域的比较顺序可能会影响equals方法的性能,最可能失败的先比较
一些告诫
-
覆盖equals时总要覆盖hashcode
-
不要企图让equals过 于智能
-
不要将equals声明中的Object对象替换为其他的类型
-
不要轻易覆盖equals,除非迫不得已
eg:
public final class PhoneNumber{
private final short areaCode, preFix, lineNum;
public PhoneNumber(int areaCode, int preFix, int lineNum){
this.areaCode = areaCode;
this.preFix = preFix;
this.lineNum = lineNum;
}
@Override public boolean equals(Object o){
if(o == this) return true;
if(!(o instanceof PhoneNumber)) return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.areaCode == areaCode && pn.preFix == preFix && pn.lineNum == lineNum;
}
}
第11条 覆盖equals时总要覆盖hashCode
在每个覆盖了equals方法的类中,都必须覆盖hashCode方法,否则导致该类无法结合所有基于散列的集合一起正常工作,包括hashMap 和 hashSet
hashCode通用约定
- 应用程序执行期间,equals方法所用到的信息没有改变,则多次调用该对象的hashCode始终返回相同的值
- 两个对象根据equals方法比较相等,则两个对象调用hashCode方法也必须相等
- 两个对象根据equals方法比较不相等,则不一定要求两个对象调用hashCode方法也必须不相等
- 没有覆盖hashCode,则会违反第二条约定
// 若没有覆盖hashCode
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(1, 2, 3), "hahaha");
// get 方法返回null
m.get(new PhoneNumber(1, 2, 3)) // 两个相等的实例具有不同的散列码
// 简单覆盖hashCode,合法但不应被使用
@Override public int hashCode(){
return 42;
}
构造好的散列码
尽量为不相等的对象产生不同的散列码
- 声明一个变量result,初始化为对象中第一个关键域的散、\i列码 c
- 对象中每一个关键域都执行以下操作:
- 计算int类型的散列码 c
- 类型为基本类型: 计算 Type.hashCode(f)
- 类型为对象引用:若该类的equals方式是递归调用的,则为这个域递归调用hashCode;需要复杂的则计算一个范式得到hashCode,值为null则返回0
- 类型为数组: Arrays.hashCode()
- result = 31 * result + c 为啥是31: 31 * i == (i <<5) - i
- 返回result
@Override public int hashCode(){
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(preFix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
// Objects 类有个静态方法 hash(), 在不太注重性能的情况下可以使用
@Override public int hashCode(){
return Objects.hash(areaCode, preFix, lineNum);
}
// 优化,缓存hashCode的值,延迟初始化
privat int hcode;
@Override public int hashCode(){
int result = hcode;
if (result == 0){
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(preFix);
result = 31 * result + Short.hashCode(lineNum);
hcode = result;
}
return result;
}
注意
- 不要试图从散列码计算中排除掉一个对象的关键域来提高性能
- 不要对hashCode方法的返回值做出具体的规定,客户端无法依赖它;这样可以为修改提供灵活性
第12条 始终要覆盖toString
Object默认提供了toString的实现:类的名称+@+散列码的无符号16进制表示法,eg: PhoneNumber@163b91
toString约定建议所有的子类都覆盖toString方法
提供好的toString可以是类用起来舒适,易于调试
println, printf, +, assert 会自动调用toString方法
实际应用中,toString方法应该返回对象中所有值得关注的信息
对于值类,建议指定返回值的格式,BigInteger,BigDecimal等大多数基本类型包装类采用这种做法
静态工具类和枚举类型编写toString是无意义的,java已经提供了完美的方法
大多数集合实现的toString方法都是继承自抽象的集合类
自动生成的toString方法远远优于继承自Object的方法
toString方法应该以美观的格式返回一个关于对象的简洁、有用的描述
第13条 谨慎地覆盖clone
Cloneable接口的目的是作为对象的一个 mixin(混合类型)接口,表明这样的对象允许克隆,有几个缺陷:
- 缺少一个clone方法,Object的clone方法是受保护的
- 可以借助反射调用clone()方法,但反射调用可能失败
- 接口没有包含任何方法
用处:决定了Object中受保护的clone方法实现的行为:一个类实现了Cloneable, Object的clone方法返回该对象的逐域拷贝,或者抛出CloneNotSupportedException
实现Cloneable接口的类是为了提供一个功能适当的,共有的clone方法
clone 无需调用构造器就可以创建对象
clone方法的通用约定
- x.clone() != x true
- x.clone().getclass() == x.getclass() true
- x.clone().equals(x) true
情况1:类中声明的域包含基本类型的值,或者包含指向不可变对象的引用
// PhoneNumber 需实现Cloneable接口
@Overrive public PhoneNumber clone(){
try {
return (PhoneNumber)super.clone(); // java 支持协变返回类型
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
不可变的类永远不应该提供clone方法
情况2:对象的包含的域应用了可变的对象
public class Stack{
private int size;
private Object[] elements;
private static final int DEFAULT_INITIAL_CAPACITY;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(0 == size) throw new EmptyStackError();
return elements[--size]; // 内存泄漏
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1)
}
}
}
实际上,clone方法就是另一个构造器;必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。在elements数组中递归调用clone:
@Overrive public Stack clone(){
try{
Stack result = (Stack)super.clone();
result.elements = elements.clone();
return results;
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
如果elements域是final,则无效,clone方法禁止给final域赋值,Cloneable架构与引用可变对象的final域的正常用法是不相兼容的。
情况3 散列表的内部数据包含一个散列桶数组,每个散列桶都指向键值对链表的第一项
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 next){
this.key = key;
this.value = value;
this.next = next;
}
// 递归
Entry deepCopy(){
return new Entry(key, value, next == null ? null : next.deepCopy());
}
// or 迭代
Entry deepCopy2(){
Entry resutl = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next){
p.next = new Entry(p.next.key, p.next.value, p.next.next);
}
return result;
}
}
@Override public HashTable clone(){
try{
HashTable result = (HashTable)super.clone();
result.buckets = new Entry[buckets.length];
for(int i = 0; i < buckets.length; i++){
result.buckets[i] = buckets[i].deepCopy();
}
return result;
}catch(CloneNotSupportedException e){
throw new AssertionError();
}
}
}
公有的clone方法应该省略throws声明
clone总结
所有实现了Cloneable接口的类都应该覆盖clone方法,并且是公有的方法,返回类型为类的本身。该方法应该先调用super.clone()方法,然后修正任何需要修正的域。一般情况下,意味着需要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用。如果类只包含基本类型的域,或者指向不可变对象的引用,一般没有域需要修正。
对象拷贝的更好的方法是提供一个拷贝构造器或者拷贝工厂
public Yum(Yum yum){}
public static Yum newInstance(Yum yum){}
对比clone的优势:
- 不依赖与某一种很有风险的,语言之外的对象创建机制
- 不要求遵守制定好文档的规范
- 不会与final域的正常使用发生冲突
- 不会抛出不必要的受检异常
- 不需要类型转换
劣势:
- 数组的复制最好的是clone方法
基于接口的拷贝构造器和拷贝工厂,允许客户选择拷贝的实现类型,而不是一定要是原有类型,eg:
HashSet s;
TreeSet result = new TreeSet<>(s);
第14条 考虑实现Comparable接口
compateTo方法 是Comparable中唯一的方法,允许进行简单的等同性比较,且允许执行顺序比较。
一旦实现了Comparable接口,可以和许多泛型算法及依赖该接口的集合实现进行协作
类实现了Comparable接口,表明它的实例具有内在的排序关系
java类库中的值类,所有的枚举类型都实现了 Comparable 接口
public i terfase Comparable {
int compateTo(T t);
}
为实现Comparable接口的对象数组进行排序
Arrays.sort(a);
对存储在集合中的Comparable对象进行排序
public class WordList{
public static void main(String[] args){
TreeSet<String> ts = new TreeSet<>();
Collections.assAll(ts, args);
System.out.println(ts);
}
}
CompareTo的通用约定
- sgn(x.compateTo(y)) == -sgn(y.compateTo(x)) signum函数根据表达式的正负值,零值返回 1,-1, 0
- x.compateTo(y) > 0 && y.compateTo(z)>0 得到 x.compateTo(z) > 0
- x.compateTo(y) == 0 得到 x.compateTo(z) == y.compateTo(z)
- 强烈建议 ( x.compateTo(y) ==0 ) == ( x.equals(y) )
约定解释
-
compateTo不能跨越不同的类型的对象进行比较,否则抛出ClassCastException
-
违反compateTo约定的类也会破坏其依赖比较关系的类,eg: TreeSet, TreeMap, Collections, Arrays。
-
最后一条未遵守,如果一个有序集合包含了该类的元素,这个集合就无法遵守(Collections, map, set)的通用约定(接口的通用约定按照 equals, 有序结合使用compareTo进行等同性测试)
案例
BigDecimal类的compareTo和equals方法不一致。
equals 方法比较不相等:
创建一个HashSet实例,添加new BigDecimal(“1.0”), new BigDecimal(“1.00”),则集合中有两个实例
compateTo方法比较相等:
创建一个TreeSet实例,添加new BigDecimal(“1.0”), new BigDecimal(“1.00”),集合中只有一个实例
CompareTo 特点 与 equals的区别
-
Comparable 接口是参数化的,且Comparable方法是静态的类型,无需参数检查和类型转换
-
CompareTo 方法中域的比较是顺序的比较,而不是等同性的比较
-
比较对象的引用域可以通过递归调用CompareTo 方法来实现
-
若果一个域没有实现Comparable , 可以用显示的Comparator来代替,或者编写自己的比较器 or 利用现有的比较器
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString>{ public int comparaTo(CaseInsensitiveString cis){ return String.CASE_INSENSITIVE_ORDER.compare(s, cis); } }
Java所有的装箱基本类都增加了静态的compare方法。
一个类中有多个域,按照一定的顺序来比较是很关键的:
public int compareTo(PhoneNumber pn){
int result = Short.compare(areaCode, pn.areaCode);
if (0 == result) {
result = Short.compare(preFix, pn.preFix);
if(0 == result){
result = Short.compare(lineNum, pn.lineNum);
}
}
return result;
}
java8中, Comparator接口配置了一组比较器构造方法
private static final Comparator<PhoneNum> COMPARATOR =
comparingInt((PhoneNum pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.preFix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNum pn) {
return COMPARATOR.compare(this, pn);
}
comparingInt 这是一个静态方法,带有一个 键提取器函数,将一个对象引用映射到一个类型为Int的键上,返回一个对实例进行排序的比较器。comparingInt 带有一个lambda 从PhoneNum提取域areaCode,并返回一个按areaCode进行排序的 Comparator。
thenComparingInt,这是Comparator的一个实例方法,同样带有Int 键提取器函数,可以随意叠加多个thenComparingInt调用,且不一定要传入键提取器函数的参数类型,java的类型推导十分智能。
compareTo和compare方法偶尔依赖两个值的比较
反面案例
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2){
return o1.hashCode() - o2.hashCode(); // 容易造成整数溢出
}
}
使用静态方法compare:
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2){
return Integer.compare(o1.hashCode(), o2.hashCode());
}
}
比较器构造方法:
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
当在compareTo方法的实现中比较域值时,避免使用 < or > 操作符,应该是装箱基本类型的类中使用静态的compare方法,或者在Comparator接口中使用比较器构造方法。