栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Effective Java读书笔记---三、对于所有对象都通用的方法

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Effective Java读书笔记---三、对于所有对象都通用的方法

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

10.覆盖 equals 时请遵守通用约定

不需覆盖:

  • 类的每个实例本质上都是唯一的
  • 类没有必要提供“逻辑相等”( logical equality )的测试功能
  • 超 类 已经覆盖 了 equals , 超类的行为对于这个 类也是合适的
  • 类是私有的,或者是包级私有的 ,可以确定它的 equals 方法永远不会被调用 。

equals实现等价关系,其属性:

  • 自反性
  • 对称性
  • 一致性
  • 对于任何非null的引用值x,x.equals(null)必须返回false

实现高质量equals方法:

  • 使用==操作符检查“参数是否为这个对象的引用”
  • 使用 i nstanceof 操作符检查“参数是否为正确的类型” 。
  • 把参数转换成正确的类型
  • 对于该类中的每个“关键”( significant )域,检查参数中的域是否与该对象中对应的域相匹配
  • 在编写完 equals 方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的 ?

eg:

public final class PhoneNumber {
    
    private final short areaCode, prefix, lineNum;

    public PhoneNumber(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }
    
    private static short rangeCheck(int val, int max, String arg){
        
        if(val < 0 || val > max){
            throw new IllegalArgumentException(arg + ":" + val);
        }
        return (short)val;
    }
    
    @Override
    public boolean equals(Object o){
        
        if(o == this){
            return true;
        }
        if(!(o instanceof PhoneNumber)){
            return false;
        }
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNum == lineNum
                && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }   
}

告诫:

  • 覆盖 equals 时总要覆盖 hashCode
  • 不要企图让 equals 方法过于 智能
  • 不要将 equals 声 明 中 的 Object 对象替换为其他的类型
11.覆盖 equals 时总要覆盖 hashCode

Object 规范

  • 在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用, hashCode 方法都必须始终返回同一个值 。在一个应用程序与另一个程序的执行过程中,执行 hashCode 方法所返回的值可以不一致 。
  • 如果两个对象根据 equals(Object )方法比较是相等的,那么调用这两个对象中的 hashCode 方法都必须产生同样的整数结果 。
  • 如果两个对象根据 equals(Object )方法比较是不相等的,那么调用这两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果 。 但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表( hashtable )的性能 。

解决方法:

  1. 声明一个 int 变量并命名为 result ,将它初始化为对象中第一个关键域的散列码c 。

  2. 对象中剩下的每一个关键域 f 都完成以下步骤:
    a . 为该域计算 int 类型的散列码 C:
    I . 如果该域是基本类型,则计算 Type. hashCode ( f ),这里的 Type 是装箱基本类型的类,与 f 的类型相对应 。
    II .如果该域是一个对象引用,并且该类的 equals 方法通过递归地调用 equals的方式来比较这个域,则同样为这个域递归地调用 hashCode 。 如果需要更复杂的比较,则为这个域计算一个“范式”,然后针对这个范式调用 hashCode 。 如果这个域的值为 null , 则 返回0(或者其他某个常数,但通常是 0)

III. 如果该域是一个数组,则要把每一个元素当作单独的域来处理 。 也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤 2.b中的做法把这些散列值组合起来 。 如果数组域中没有重要的元素,可以使用一个常量,但最好不要用 0。 如果数组域中的所有元素都很重要,可以使用Arrays.hashCode方法

b. 按照下面公式,把2.a中计算得到的散列码c合并到result中

result = 31 * result + c;
3 返回 result 。

之所以选择 31 ,是因为它是一个奇素数 。 如果乘数是偶数,并且乘法、溢出的话,信息就会丢失,因为与 2 相乘等价于移位运算 。 使用素数的好处并不很明显,但是习惯上都使用素数来计算散列结果 。 3 1 有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能 : 31 * i = = ( i < < 5 ) - i. 现代的虚拟机可以自动完成这种优化 。

@Override
    public int hashCode(){
        int result = Short.hashCode(areaCode);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        return result;
    }

Object方法:

    @Override
    public int hashCode() {
        return Objects.hash(areaCode, prefix, lineNum);
    }

如果一个类是不可变的,并且计算散列码的开销也比较大 , 就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码 。

如果你觉得这种类型的大多数对象会被用作散列键( hash keys ),就应该在创建实例的时候计算散列码 。 否则,可以选择“延迟初始化”( lazi ly initialize )散列码,即一直到 hashCode 被第一次调用的时候才初始化

12.始终要覆盖toString

建议所有的子类都覆盖这个方法

eg

@Override
    public String toString(){
        return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
    }
13.谨慎地覆盖clone

Cloneable接口的目的是作为对象的一个mixn接口,表明这样的对象允许克隆,它的主要缺陷是少一个clone方法,而Object的clone方法是受保护的。

Cloneable接口并没有包含任何方法,作用为决定了Object中受保护的clone方法实现的行为,如果一个类实现类Cloneable,Object的clone方法就返回该对象的逐域看吧,否则就会抛出CloneNotSupportedException异常。

拷贝的含义

  • 对于任何对象x:x.clone != x -------->true
  • x.clone.getClass() == x.getClass() -------> true
  • x.clone.equals(x) -----> true (不是绝对的要求)

不可变的类永远不应该提供clone方法

clone 方法就是另一个构造器; 必须确保它不会伤害到原始的对象, 并确保正确地创建被克隆对象中的
约束条件 。

公有的 clone 方法应该省略 throws 声明 ,因为不会抛出受检异常的方法使用起来更加轻松 。

所有实现了 Cloneable 接口的类都应该覆盖 clone 方法,并且是公有的方法,它的返回类型为类本身 。 该方法应该先调用 super.clone 方法,然后修正任何需要修正的域 。 一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用 。 虽然,这些内部拷贝操作往往可以通过
递归地调用 clo口e 来完成,但这通常并不是最佳方法 。 如果该类只包含基本类型的域,或者指向不可变对象的引用,那么多半的情况是没有域需要修正 。

eg

@Getter
@Setter
public final class PhoneNumber implements Cloneable {

    private final short areaCode, prefix, lineNum;
    private Entry[] buckets;
    private List lists;
    private Object[] elements;
    private Person person;


    private class Entry{
        final Object key;
        Object value;
        Entry next;

        public 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());
//        }
        //可以用迭代代替递归
        Entry deepCopy(){
            Entry result = 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;
        }
    }


    public PhoneNumber(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }

    private static short rangeCheck(int val, int max, String arg){

        if(val < 0 || val > max){
            throw new IllegalArgumentException(arg + ":" + val);
        }
        return (short)val;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PhoneNumber that = (PhoneNumber) o;
        return areaCode == that.areaCode &&
                prefix == that.prefix &&
                lineNum == that.lineNum;
    }

    @Override
    public String toString(){
        return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
    }

    @Override
    public PhoneNumber clone(){
        try {
            PhoneNumber phoneNumber = (PhoneNumber)super.clone();
            //对object[] 的clone
            phoneNumber.elements = elements.clone();
            //对键值对 clone
            phoneNumber.buckets = new Entry[buckets.length];
            for (int i = 0; i < buckets.length; i++) {
                if(buckets[i] != null){
                    phoneNumber.buckets[i] = buckets[i].deepCopy();
                }
            }
            return phoneNumber;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

}

对象拷贝的更好的办法是提供一个拷贝构造器( copyconstructor)或拷贝工厂( copy factory ) 。

和JAVA开发者对话

Bill Venners: 在你的书中,你建议使用复制构造函数而不是实现Cloneable和编写clone。你能详细说明吗?

Josh Bloch:如果你已经阅读了我的书中关于克隆的章节,特别是如果你看得仔细的话,你就会知道我认为克隆已经完全坏掉的东西。有一些设计缺陷,其中最大的一个是 Cloneable 接口没有 clone方法。这意味着它根本不起作用:实现了 Cloneable 接口并不说明你可以用它做什么。相反,它说明了内部可能做些什么。它说如果通过super.clone 反复调用它最终调用 Object 的 clone 方法,这个方法将返回原始的属性副本。

但它没有说明你可以用一个实现 Cloneable 接口的对象做什么,这意味着你不能做多态 clone 操作。如果我有一个 Cloneable 数组,你会认为我可以运行该数组并克隆每个元素以制作数组的深层副本,但不能。你不能强制转换对象为 Cloneable 接口并调用 clone 方法,因为 Cloneable 没有public clone 方法,Object 类也没有。如果您尝试强制转换 Cloneable 并调用该 clone 方法,编译器会说您正在尝试在对象上调用受保护的clone方法。

事实的真相是,您不通过实施 Cloneable 和提供 clone 除复制能力之外的公共方法为您的客户提供任何能力。如果您提供具有不同名称的copy操作, 怎么也不次于去实现 Cloneable 接口。这基本上就是你用复制构造函数做的事情。复制构造方法有几个优点,我在本书中有讨论。一个很大的优点是可以使副本具有与原始副本不同的实现。例如,您可以将一个 linkedList 复制到 ArrayList。

Object 的 clone 方法是非常棘手的。它基于属性复制,而且是“超语言”。它创建一个对象而不调用构造函数。无法保证它保留构造函数建立的不变量。多年来,在Sun内外存在许多错误,这源于这样一个事实,即如果你只是super.clone 反复调用链直到你克隆了一个对象,那么你就拥有了一个浅层的对象副本。克隆通常与正在克隆的对象共享状态。如果该状态是可变的,则您没有两个独立的对象。如果您修改一个,另一个也会更改。突然之间,你会得到随机行为。

我使用的东西很少实现 Cloneable。我经常提供实现类的 clone 公共方法,仅是因为人们期望有。我没有抽象类实现 Cloneable,也没有接口扩展它,因为我不会将实现的负担 Cloneable 放在扩展(或实现)抽象类(或接口)的所有类上。这是一个真正的负担,几乎没有什么好处。

Doug Lea 走得更远。他告诉我 clone 除了复制数组之外他不再使用了。您应该使用 clone 复制数组,因为这通常是最快的方法。但 Doug 的类根本就不再实施 Cloneable了。他放弃了。而且我认为这并非不合理。

这是一个耻辱, Cloneable 接口坏掉了,但它发生了。最初的 Java API在紧迫的期限内完成,以满足市场窗口收紧的需求。最初的 Java 团队做了不可思议的工作,但并非所有的 API 都是完美的。 Cloneable 是一个弱点,我认为人们应该意识到它的局限性。
————————————————
版权声明:本文为CSDN博主「weixin_39624700」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_39624700/article/details/114058196
public class Parents {
    
    private int id;
    private Child child;

    public Parents(int id, Child child) {
        this.id = id;
        this.child = child;
    }
    
    //实现对象的复制
    public Parents(Parents parents){
        id = parents.id;
        child = new Child(parents.child);
    }
}


public class Child {

    public String name;

    public Child(String name){
        this.name = name;
    }

    //实现对象的复制
    public Child(Child child){
        name = child.name;
    }
}


14.考虑实现Comparable接口
  • compare To 方法并没有在 Object 类中声明 。
  • 它是 Comparable 接口中唯一的方法 。
  • compare To 方法不但允许进行简单的等同性比较,而且允许执行顺序比较,
  • 它与 Object 的 equals 方法具有相似的特征,它还是个泛型( generic ) 。

Comparable接口

public interface Comparable {
     public int compareTo(T o);
}

compareTo约定:

当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数 。 如果由 于指定对象的类型而无法与该对象进行比较,则抛出 ClassCastException 异常 。

自反性,传递性,对称性

依赖于比较关系的类包括有序集合类 TreeSet 和 TreeMap ,以及工具类 Collections 和 Arrays ,它们内部包含有搜索和排序算法 。

public class CaseInsensitiveString implements Comparable {

    private final String s;

    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }

  public int compareTo(CaseInsensitiveString cis){
        
        return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
  }
}

多个关键域,从最关键的域开始,产生非0结果,比较操作结束

@Getter
@Setter
public final class PhoneNumbers implements Cloneable, Comparable {

    private final short areaCode, prefix, lineNum;

    public PhoneNumbers(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PhoneNumbers that = (PhoneNumbers) o;
        return areaCode == that.areaCode &&
                prefix == that.prefix &&
                lineNum == that.lineNum;
    }

    @Override
    public String toString(){
        return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
    }


    @Override
    public int compareTo(PhoneNumbers ph) {
        int result = Short.compare(areaCode, ph.areaCode);
        if(result == 0){
            result = Short.compare(prefix, ph.prefix);
            if(result == 0){
                result = Short.compare(lineNum, ph.lineNum);
            }
        }
        return result;
    }
}

java8 比较器构造方法 —> 简洁,但付出性能成本

@Getter
@Setter
public final class PhoneNumbers implements Cloneable, Comparable {

    private static final Comparator COMPARATOR = comparingInt((PhoneNumbers pn) ->pn.areaCode)
            .thenComparingInt(pn -> pn.prefix)
            .thenComparingInt(pn -> pn.lineNum);
    private final short areaCode, prefix, lineNum;

    public PhoneNumbers(short areaCode, short prefix, short lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PhoneNumbers that = (PhoneNumbers) o;
        return areaCode == that.areaCode &&
                prefix == that.prefix &&
                lineNum == that.lineNum;
    }

    @Override
    public String toString(){
        
        return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
    }

    @Override
    public int compareTo(PhoneNumbers ph) {
        
        return COMPARATOR.compare(this, ph);
    }
}

  • 每当实现一个对排序敏感的类时,都应该让这个类实现 Comparable 接口,以便其实例可以轻松地被分类 、 搜索,以及用在基于比较的集合中 。
  • 每当在 compareTo 方法的实现中比较域值时,都要避免使用 < 和>操作符,而应该在装箱基本类型
    的类中使用静态的 compare 方法,或者在 Comparator 接口中使用比较器构造方法 。
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/292701.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号