自学内容网 自学内容网

Effective Java学习笔记--Object的非final方法重写(Clone与toString)

目录

Clone方法的重写

Object中默认的clone方法是如何实现的

Cloneable接口

如何实现拷贝方法

重写clone方法

拷贝构造器与静态工厂方法

toString方法的重写

Object中默认的toString方法是如何实现的

如何实现toString方法


这一篇文章继续总结Objects另外两个重要的非final方法,Clone和toString。

Clone方法的重写

Clone是Object的一个受保护方法,也就是说如果某个类不显式定义Clone方法,默认其他类是不能调用它默认从Object中继承的clone方法的(因为protected方法一般情况下只有两种途径可以访问,一是直接子类,一是同一包内其他类,如果某个类没有显式的重写Clone方法,那么Java编译器只能去寻找Object的protected方法,这就不是直接子类的调用,并且调用类与Object不在同一个包内,因此调用会失败)。所以,如果要在某个类中实现拷贝的功能一定要重写这个方法。

Object中默认的clone方法是如何实现的

书中作者介绍Object中的clone方法通过一种在Java语言标准以外的协议,使得不使用构造器也可以新建一个对象。同时,Object默认的clone方法只拷贝对象引用,属于浅拷贝。

但是,我们要知道这个clone方法新建的对象和默认构造器新建的对象有本质的区别,clone新建的对象只新建了引用,而默认构造器是新建了实例的,从这个意义上来说,clone方法是一个基于已构建实例的轻量化构造方法,而构造器由于构建了实例所以相对来说偏重,那么通过重写clone方法来实现深拷贝的意义有多大确实值得思考(因为构造器就可以实现),这也是为什么作者在这一章末尾推荐了拷贝构造器和静态工厂方法的原因。(个人想法,欢迎批评)

Cloneable接口

Clone方法与其他Object的非final方法不太一样,单纯的重写clone方法在运行的时候会直接返回CloneNotSupportedException,因为Object的clone方法默认只能处理Object类的拷贝,所以这里目标类需要额外实现一个Cloneable接口,这个接口没有任何的方法,但是可以赋予目标类一个可以处理自身类型拷贝的功能。

如何实现拷贝方法

书中总共介绍了三种方式,一是重写clone方法,二是构建一个拷贝构造器,三是静态工厂方法,下面依次介绍:

重写clone方法

  1. 方法特性的调整:将clone方法实现为public;返回值设置为相应类自身(这里借助了Java支持的协变反应类型,即方法签名中返回值可以设置为所覆盖方法的子类);实现Cloneable接口。
  2. clone方法首先继承Object的clone方法(ClassA a = super.clone()):由于我们已经实现了Cloneable接口,这里Object的clone方法返回的就是对应的类实例,但是实现的方式依然是默认的浅拷贝(仅拷贝了所有值组件的引用);同时super.clone()的调用需要包含在try-catch语句中,因为该方法会抛出CloneNotSupportedException。
  3. (可选)如果想要实现深拷贝,那就需要将拷贝对象相应的值组件替换成新建的实例,这就需要对每一个值组件分别使用clone方法。

​​​​​​​

这里要重点说一下对于集合的拷贝,Java自带集合类(ArrayList、LinkedList等)的clone方法都是Object默认的浅拷贝,如果相关集合内的元素是可变对象时,一旦拷贝引用或者原引用指向的实例发生了改变,另一方也会同步改变,造成严重的后果。所以作者鼓励要重写集合的clone方法。但由于Java自带的集合都是final类型不可被继承,因此我们可以通过复合的方式来实现clone方法的重写:

public class MyArrayList<T> implements Cloneable{
    private ArrayList<T> list;

    public MyArrayList() {
        list = new ArrayList<>();
    }

    public void add(T t){
        list.add(t);
    }

    public T get(int index){
        return list.get(index);
    }

    public void clear(){
        list.clear();
    }

    public int size(){
        return list.size();
    }

    @Override
    public MyArrayList<T> clone(){
        try {
            MyArrayList<T> clonelist = (MyArrayList<T>) super.clone();
            clonelist.list = new ArrayList<T>();
            for(T t : list){
                clonelist.add(t);
                
            }
            return clonelist;
        }catch(CloneNotSupportedException e){
            throw new AssertionError();
        }
    }
}

下面说一下clone方法重写的注意事项:

  • clone方法是被禁止给final域赋新值的,所以为了使得类成为可克隆的,有必要将类中的final域调整成可变对象。
  • 完全用于继承而设计的类(比如抽象类)不应该实现Cloneable接口,同时要将clone方法给显式禁用。
  • Object的clone方法默认是线程不安全的,所以如果涉及多线程需要Synchronized关键字保证线程安全。
  • clone方法重写的实现中不应该出现可以被子类覆盖的方法,否则一旦子类继承了父类的clone方法,同时重写了相应的可覆盖方法可能会导致不可预知的错误。

拷贝构造器与静态工厂方法

拷贝构造器就是通过类构造器的方法去实现对象的拷贝功能:

    public MyArrayList(MyArrayList<T> mylist){
        this.list = new ArrayList<T>();
        for(T t : mylist.list){
            this.list.add(t);
        }
    }

静态工厂方法也好理解:

public static MyArrayList<T> newInstance(MyArrayList<T> mylist){
    
    ...

}

对于除集合以外的对象,拷贝构造器和静态工厂方法都比clone方法要更优,原因如下:

  • 不必依赖于语言之外的对象创建机制,使得程序更可控
  • 不会与final域的使用产生冲突,可以通过新建final对象的方式解决
  • 不会抛出不必要的受检异常:CloneNotSupportedException
  • 不需要进行类型转换,super.clone()返回的是Object对象

拷贝构造器和静态工厂方法甚至可以提供额外接口实现的功能,比如将HashSet: s拷贝成TreeSet,只要在构造器中实现new TreeSet<>(s)就行,相当于将HashSet实现了SortedSet接口。

toString方法的重写

toString方法是对象的一个识别信息,它和hashcode不一样,hashcode是给程序看的,而toString是给人看的,因此需要包括对象中所有值得关注的信息。toString方法会被Printf、assert调用,所以如果设置不好,在日志中就比较难以发现问题。

Object中默认的toString方法是如何实现的

它包含一个类的名称、一个@符号,接着是散列码的无符号十六进制表示法,例如:PhoneNumber@163b91。因为Object默认的实现对于用户并不友好,所以建议所有子类都覆盖这个方法,让用户能够掌握对象的关键信息。

如何实现toString方法

有几个规范:

  • 应该返回对象所有值得关注的信息
  • 建议指定String的返回格式,同时在类的注解中明确表明自己的意图
public String toString(){
    return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
  • 要为所有返回值中包含的信息提供编程访问的途径,而不要让用户尝试将toString返回的内容进行解析返回相关信息

原文地址:https://blog.csdn.net/weixin_50353846/article/details/140363893

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!