Effective java 总结2 - 对于所有对象都通用的方法

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平台类库反面案例

  1. java.sql.Timestamp 对java.sql.Date进行了扩展(nanosenonds)域,Timestamp中的equals方法违反了对称性,因此不要混用这两个类型对象
  2. java.net.URL 的equals方法依赖于对URL中主机IP地址的比较,有可能IP地址发生了改变,导致违反约定

高质量equals方法诀窍

  1. 使用 == 操作符检查:参数是否为这个对象的引用

  2. 使用instanceof操作符检查:参数是否为正确的类型

  3. 把参数转换为正确的类型

  4. 对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配

    1. 类型是接口,通过接口方法访问参数的域
    2. 类型是类,直接访问参数的域
    3. 非float,double类型基本域, 直接用==操作符比较
    4. float域,可以使用静态的Float.compare(float, float)方法
    5. double域,使用Double.compare(double, double)
    6. 数组域,可以使用Array.equals
    7. 有些对象引用域含有null,可以使用Objects.equals(Object, Object)
  5. 域的比较顺序可能会影响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;
}

构造好的散列码

尽量为不相等的对象产生不同的散列码

  1. 声明一个变量result,初始化为对象中第一个关键域的散、\i列码 c
  2. 对象中每一个关键域都执行以下操作:
    • 计算int类型的散列码 c
    • 类型为基本类型: 计算 Type.hashCode(f)
    • 类型为对象引用:若该类的equals方式是递归调用的,则为这个域递归调用hashCode;需要复杂的则计算一个范式得到hashCode,值为null则返回0
    • 类型为数组: Arrays.hashCode()
    • result = 31 * result + c 为啥是31: 31 * i == (i <<5) - i
  3. 返回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) )

约定解释

  1. compateTo不能跨越不同的类型的对象进行比较,否则抛出ClassCastException

  2. 违反compateTo约定的类也会破坏其依赖比较关系的类,eg: TreeSet, TreeMap, Collections, Arrays。

  3. 最后一条未遵守,如果一个有序集合包含了该类的元素,这个集合就无法遵守(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接口中使用比较器构造方法。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值