Effective-Java_读书笔记06-07

76 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

10 Always override toString 建议重写toString方法

默认toString方法是由三部分组成: 类名 + @ + 无符号16进制哈希值. 这样其实并没有反馈什么有效信息, 所以java规范建议开发人员每次都要重写toString方法, 返回结果包括对象中属性信息, 并且易于阅读.

toString方法工作一般不需要关注, 因为通常toString方法就是用于日志打印, 平常开发中一般使用lombok注解自动生成toString方法, 或者使用工具类将对象直接转成json打印. 所以本小节作者介绍的内容可以不需要太关注.

11 Override clone judiciously 谨慎重写clone方法

先上结论

  1. 不用clone方法就不要去重写clone方法.
  2. 如果有clone对象的场景, 优先考虑使用构造方法或者静态工厂方式.
  3. 那我就是要用jdk提供的clone方法呢? 那我们继续看...

使用方式

clone方法是Object提供的一个native方法, 注释中强调了, 使用clone方法必须先实现Cloneable接口, 否则会抛出CloneNotSupportedException. Cloneable是一个标记接口, 不包含任何方法.

实现接口后可以不重写clone方法, 此时默认的逻辑是将对象的每一个属性进行复制操作. 这里就涉及到一个概念: 浅拷贝和深拷贝. 所谓的浅拷贝就是默认的实现方式, 将对象中的每一个属性的值进行复制, 如果属性是基础类型, 那复制没有问题. 但是如果是一个可变的引用类型这里其实复制的是对象的引用, 也就是说修改复制对象, 原对象的属性也会更改. 代码演示如下.

public static void main(String[] args) {

    ClassA a = new ClassA(1, "str");
    ClassB source = new ClassB(1, "123", a);

    try {
        ClassB cloneB = sourceB.clone();
        ClassC cloneC = sourceC.clone();
        System.out.println("修改前");
        System.out.println("clone B = " + cloneB);
        a.setNum(123);
        sourceB.num = 222;
        sourceB.str = "333";

        System.out.println("修改后");
        System.out.println("clone B = " + cloneB);
        System.out.println("source B = " + sourceB);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 打印结果
修改前
clone B = ClassB{num=1, str='123', classA=ClassA{num=1, str='str'}}
修改后
clone B = ClassB{num=1, str='123', classA=ClassA{num=123, str='str'}}
source B = ClassB{num=222, str='333', classA=ClassA{num=123, str='str'}}

ClassB包含一个ClassA的属性, 复制之后对原对象属性, 打印结果发现:

  1. int作为基础类型, num修改不影响clone对象属性.
  2. String类型虽然是引用类型, 但是是不可变类型, 当执行source.str = "333"的时候会重新构建一个新的String对象, 所以clone对象中的属性并不会受到影响.
  3. 属性a修改后clone对象中的属性也会修改.

为了解决浅拷贝的问题, 就引出了深拷贝. 所谓的深拷贝就是针对上述有问题的点进行处理. 重写clone方法如下:

@Override
protected ClassC clone() throws CloneNotSupportedException {
    ClassC clone = (ClassC) super.clone();
    clone.classA = (ClassA) classA.clone();
    return clone;
}
// 按照刚才的代码逻辑处理ClassC, 打印结果如下
修改前
clone C = ClassB{num=1, str='123', classA=ClassA{num=1, str='str'}}
修改后
clone C = ClassB{num=1, str='123', classA=ClassA{num=1, str='str'}}
source C = ClassB{num=1, str='123', classA=ClassA{num=123, str='str'}}

从结果可知, 重写后的clone方法解决浅拷贝的问题. 这里有个小细节, clone()方法返回正常是返回Object类型, 但是这里返回的类型是ClassC, jdk1.5之后允许重写方法返回值可以为被覆盖方法返回值的子类. 通常也建议大家这么做, 避免使用方拿到结果还要做一次类型转换. 但是这带来一个问题, java语法中正常是不允许父类转换为子类的, 只能当父类指向子类引用才可以完成转换, 比如:

Father father = new Son();
Son son = (Son)father;

所以为了可以返回正确的类型, 必须要保证所有的每一个父类的clone方法都使用了super.clone(), 而不是通过构造器创建的对象, 就像一个调用链一样, 一层一层的调用父类的clone方法, 最终调用了Object的clone方法. 如果中间出现构造器构造对象, 就会造成类型转换异常.

除了这个问题外, 还要注意一点, 如果ClassC中的ClassA属性是一个final的话这里就没办法进行赋值操作了.

这里就引出我们推荐的复制方法

推荐的复制方式

拷贝构造器, 修改后代码如下:

public ClassC(ClassC classC) {
    this.num = classC.num;
    this.str = classC.str;
    this.classA = new ClassA(classC.classA);
}

使用这种方式就可以避免类型转换和final字段的问题.

什么时候使用clone()方法?

既然推荐使用拷贝构造器方式, 那clone()方法还有使用的必要么? 有一种说法是clone方法的效率高于构造器, 测试了下确实对于复杂对象来说, clone构造会比使用构造器构造对象要快不少. 测试代码如下:

public static void testPerformance(){
    SimpleDateFormat sdf  = new SimpleDateFormat("yyyy-MM-dd");
    long start = System.currentTimeMillis();
    for(int i = 0; i < 1000000; i++){
        SimpleDateFormat localSdf = (SimpleDateFormat)sdf.clone();
    }
    System.out.println("Cloning : " + (System.currentTimeMillis() - start) + " ms");

    start = System.currentTimeMillis();
    for(int i = 0; i < 1000000; i++){
        Object localSdf = new SimpleDateFormat("yyyy-MM-dd");
    }
    System.out.println("Creating : " + (System.currentTimeMillis() - start) + " ms");

}
//
Cloning : 333 ms
Creating : 821 ms

总结

总的来说, 没有使用场景不要去重写clone方法, 如果要复制的话优先考虑复制构造器的方式, 对于复杂的对象, 出于性能考虑可以选择使用clone方式. 无论使用哪种方式, 需要注意浅拷贝可能带来的影响.

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情