EffectiveJava第三章:对于所有对象都通用的方法

注意:重写equals、hashCode、clone、toString等方法都可以直接在IDE右键-->Generate...中自动生成

 

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

1)类的每个实例本质上是唯一的

2)类没有必要提供“逻辑相等”的测试功能 

3)超类已经覆盖了equals,超类的行为对于这个类也是合适的

4)类是私有的,或者是包级私有的,可以确定它的equals方法永远不会被调用

equals方法的通用约定:自反性、对称性、传递性、一致性、非空性(对于任何非null的引用值x,x.equals(null)必须返回false),覆盖equals方法的步骤

1、使用==操作符检查“参数是否为这个对象的引用”。如果是,返回true,这是性能优化,如果比较操作很昂贵就值得这么做

2、使用instanceof操作符检查“参数是否为正确的类型”。如果不是,返回false。一般所谓“正确的类型”是指equals方法所在的那个类

3、把参数转换成正确的类型。因为转换之前进行过instance测试,所以确保会成功

4、对于该类中的每个“关键”(significant)域,检查参数中的域是否与该对象中对应的域相匹配。如果这些测试全部成功,则返回true;否则返回false。如果第2步中的类型是个接口,就必须通过接口方法访问参数中的域;如果该类型是个类,也许就能够直接访问参数中的域,这取决于它们的可访问性

public class PhoneNumber {

    private final short areaCode,prefix,lineNum;
    public PhoneNumber(int areaCode,int prefix,int lineNum){
        this.areaCode=rangeCheck(areaCode,999,"area code");
        this.prefix=rangeCheck(prefix,999,"prefix");
        this.lineNum=rangeCheck(lineNum,999,"line num");
    }

    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;
    }
}

注意:

1、覆盖equals方法的时候,也要覆盖hashcode方法

2、不要企图让equals方法过于智能。一般简单的测试域中的值是否相等就好

3、不要将equals声明中的object对象替换为其他的类型

    /**
      *这里的public boolean equals(Object o)换成了MyClass,无法正常工作
      *原因:这个方法没有覆盖(override)Object.equals,它的参数应该是Object类型
      *相反,它重载(overload)了Object.equals
      */    
    @Override
    public boolean equals(MyClass 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;
    }

 

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

在每个覆盖了equals方法的类中,都必须覆盖hashCode方法。如果不这样做,就会违反hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括HashMap和HashSet。

  1. 因没有覆盖hashCode而违反的关键约定是第二条:相等的对象必须具有相等的散列码
  2. 不要试图从散列码计算中排除掉一个对象的关键域来提高性能
  3. 不要对hashCode方法的返回值做出具体的规定,因为客户端无法理所当然的依赖它,这样可以为修改提供灵活性

覆盖hashCode方法的步骤:

  1. 声明一个int变量并命名为result,将它初始化为对象中第一个关键域的散列码c,如步骤2.A所示(第10条:关键域是指影响equals比较的域)
  2. 对象中剩下的每一个关键域f都完成以下步骤
  1. 为该域计算int类型的散列码c:
  1. 如果该域是基本类型,则计算Type.hashCode(f),这里的Type是装箱基本类型的类,与f的类型相对应
  2. 如果该域是一个对象引用,并且该类的equals方法通过递归的调用equals的方式来比较这个域,则同样为这个域递归的调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,通常是0)
  3. 如果该域是一个数组,则要把每一个元素当作单独的域里处理。即递归的调用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.B中的做法把这些散列值组合起来。如果数组域中没有重要的元素,可以使用一个常量,但最好不要用0.如果数组域中的所有元素都很重要,可以使用Arrays.hashCode方法
  1. 按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中

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

result = 31 * result + c;

3)返回result

public class PhoneNumber {

    private final short areaCode,prefix,lineNum;
    public PhoneNumber(int areaCode,int prefix,int lineNum){
        this.areaCode=rangeCheck(areaCode,999,"area code");
        this.prefix=rangeCheck(prefix,999,"prefix");
        this.lineNum=rangeCheck(lineNum,999,"line num");
    }

    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;
    }

    /**
     * 常用hashCode方法
     * @return
     */
    @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():可以传递任意个参数,并为它们返回一个散列码
     * 速度略慢,因为它们会引发数组的创建,以便传入数目可变的参数,如果参数中有基本类型,还需要装箱和拆箱。
     * 不注重性能的时候可以用这个方法
     * @return
     */
    @Override
    public int hashCode(){
        return Objects.hash(lineNum,prefix,areaCode);
    }

    
}

第12条:始终要覆盖toString

重写了toString方法,最好注释toString输出的格式,调用toString也不用System.out.println(entity.toString);直接System.out.println(entity);因为它会自动调用toString方法。

  1. 提供好的toString实现可以使类用起来更加舒适,使用了这个类的系统也更易于调试
  2. 在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息
  3. 无论是否指明格式,都应该在文档注释中表明重写toString的意图,并为toString返回值中包含的所有信息提供一种可以通过编程访问之的途径
    @Override
    public String toString() {

        return "PhoneNumber{" +
                "areaCode=" + areaCode +
                ", prefix=" + prefix +
                ", lineNum=" + lineNum +
                '}';
    }

第13条:谨慎的覆盖clone

实现Cloneable接口的类是为了提供一个功能适当的公有的clone方法,不可变的类永远都不应该提供clone方法。步骤:

1、类实现Cloneable接口

2、如果类中属性只有基本类型(如int/String/long等),直接在方法中return (entity)super.clone(),注意处理异常CloneNotSupportedException

    /**
     * 自动生成的clone方法是这样的,我们可以把它修改
     * 1)protected --> public
     * 2)返回值Object --> 对应的entity
     * 3)异常不要抛出throws --> catch
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }


    @Override
    public PhoneNumber clone(){
        try {
            return (PhoneNumber) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }

3、如果对象中包含的域引用了可变的对象(有属性是数组),就需要在数组中递归的调用clone方法进行拷贝

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

public class Stack {

    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT = 16;

    @Override
    public Stack clone(){
        Stack result = null;
        try {
            result = (Stack) super.clone();
            // 递归的拷贝每一个值
            result.elements = elements.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return result;
    }
}

4、如果对象中包含有链表结构,那就需要进行深度拷贝,代码clone要具体到链表中每一个对象

// 必须实现Cloneable接口
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(){
            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;
        }
    }

    /**
     * clone方法
     */
    @Override
    public HashTable clone() throws CloneNotSupportedException {
        // 基本属性直接super.clone
        HashTable result = (HashTable) super.clone();
        // 为特殊属性链表分配适当的拷贝空间
        result.buckets = new Entry[buckets.length];
        // 遍历,对每一个非空散列桶进行深度拷贝
        for (int i = 0; i < buckets.length; i++) {
            if (buckets[i]!=null){
              result.buckets[i] = buckets[i].deepCopy();
            }
        }
        return result;
    }
}

建议使用:对象拷贝的更好的办法是提供一个拷贝构造器或拷贝工厂。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类,

    /**
     * 拷贝构造器
     * @param hashTable
     */
    public HashTable(HashTable hashTable){
        this.buckets = hashTable.buckets;
    }

拷贝工厂

public static HashTable newInstance(HashTable hashTable){
     // 加了static关键字就成了拷贝工厂了   
}

第14条:考虑实现Comparable接口

对一个List<Entity>中的实体类某个或者某几个字段进行分组,然后再对实体类的另外的属性进行排序,并重新赋值

public class Test{

    public void test(List<Entity> entities) {

        /**
         * 确保要比较的字段不为null之后可以直接使用lambda
         */
        /*entities = Comparator.comparingInt(entity->fetchGroupKey(entity))
                .thenComparingInt(entity->entity.getTwoThing());*/

        //-----不使用lambda进行分组排序的方法-------------------
        /**
         * OneThing、TwoThing字段都是String类型,ThreeDate是时间类型
         *
         * 1、对List<Entity>里的OneThing、ThreeDate属性相等的字段(唯一key)进行分组
         * 2、然后根据twoThing属性排序(从小到大)
         */
        entities.sort(this::compare);

        /**
         * 3、对根据OneThing、ThreeDate分好组的实体类的TwoThing重新赋值
         */
        int count = 1;
        String preKey = null;

        for (int i = 0; i < entities.size(); i++) {
            Entity entity = entities.get(i);
            if (Objects.equals(preKey, fetchGroupKey(entity))) {
                // count对相同key、已排序的组计数
                count++;
            } else {
                count = 1;
            }

            // 已经分组排序了,所以可以使用preKey作为比较计数的判断条件,一个preKey遍历一组
            preKey = fetchGroupKey(entity);
            /**
             * 对TwoThing字段赋值规则:数量count不足两位(每组的实体类数量少于10),前面补0,并将其转为String类型
             * 如果多于10,就正常赋值
             * 值得形式如:01 02 03...10 11 12...
             */
            entity.setTwoThing(String.format("%02d", count));
        }
    }

    private int compare(Entity o1, Entity o2) {
        // 以o1.getOneThing()为比较条件,进行分组
        int is = fetchGroupKey(o1).compareTo(fetchGroupKey(o2));
        // 这两个实体类不是同一组的,直接返回,继续比较下面两组
        if (is != 0) {
            return is;
        } else {
            // 这两个实体类是同一组的,进行排序

            // 以TwoThing字段的大小作为分组后的排序
            String o1Number = o1.getTwoThing();
            String o2Number = o2.getTwoThing();
            // 对字段进行判空,防止为null时候比较产生异常
            if (null == o1Number && null != o2Number) {
                return -1;
            }

            if (null != o1Number && null == o2Number) {
                return 1;
            }

            if (null == o1Number && null == o2Number) {
                return 0;
            }
            return Integer.parseInt(o1.getTwoThing()) - Integer.parseInt(o2.getTwoThing());
        }
    }

    /**
     * 对该实体类的某几个字段组装唯一key,作为分组条件
     */
    private String fetchGroupKey(Entity entity) {
        // 这里用getTime()是因为效率稍微比toString()高一些
        return entity.getOneThing() + "-" + (null == entity.getDate() ? "" : entity.getThreeDate().getTime());
    }
}

使用Lambda表达式进行分组排序,记得使用Java的静态导入(static import)设施,通过静态比较强构造方法的简单的名称就可以对它们进行引用。简单点说就是使用static final关键字修饰

        /**
         * 确保要比较的字段不为null之后可以直接使用lambda
         */
        private static final List<Entity> entities = 
            Comparator.comparingInt(entity->fetchGroupKey(entity))
                .thenComparingInt(entity->entity.getTwoThing());

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值