CV程序员都要掌握的对象拷贝

1,299 阅读7分钟

前言

CV程序员:熟练使用ctrl+c和ctrl+v或者command+c和command+v的程序员,CV程序员分两种,第一种见多识广,游走在各种搜索引擎、博客以及项目中,搜寻着自己想要的代码,第一种是把自己的代码从一个类复制到另一个类。

我觉得如果是作为一个刚起步的程序员来说,CV别人的代码挺好的,因为至少他具备了搜索以及自己解决问题的能力。这种能力和学习方式是非常重要的,因为很多问题都需要自己去解决一遍才会慢慢积累经验,向更高的水平进阶,但是要注意的是懂得回顾和反思,虽然已经把代码CV过来了,但是事后需要思考代码为什么要这么写,而不是解决了就丢一旁。日积跬步,终有一天你的代码也会被别人用来CV。而如果是CV自己的代码就要注意了,因为你在写着冗余的代码,需要思考为什么有这么多重复的代码,是否可以做一些抽象设计或者运用一些设计模式来避免代码的冗余。

今天要讲的是CV程序员都要掌握的对象拷贝,主要是从以下三个方面开始讲解:

克隆什么?

怎么克隆?

为什么要克隆?

深拷贝和浅拷贝区别是什么?

克隆什么?

要讲克隆,先弄清楚第一个问题:克隆什么东西?这还用回答吗,当然是克隆对象。但是到底克隆的是什么类型的对象?

我们知道在 Java中基本数据类型和引用数据类型之分,基本数据类型与引用数据类型最本质的区别是:

  • 基本数据类型直接包含值,在被创建时,在栈上给其划分一块内存,将数值直接存储在栈上。
  • 引用数据类型被创建时,首先要在栈上给其引用(句柄)分配一块内存,而对象的具体信息都存储在堆内存上,然后由栈上面的引用指向堆中对象的地址,可以理解为栈上面保存的是堆中对象的地址。

而我们所说的克隆,可以分这两种类型的对象来讨论。

偷偷引入一个知识点:

装箱:将基本数据类型转换为引用数据类型。比如int转化为Integer。

拆箱:将引用数据类型转换为基本数据类型。比如Integer转化为int。

怎么克隆?

弄清楚了要克隆什么,那么我们来看看怎么克隆。还是分为克隆基本类型对象和引用类型对象:

基本类型对象克隆

下面是一个基本类型的克隆示例:

public static void main(String[] args) {
    int a = 1;
    // 用"="克隆
    int b = a;
    System.out.println("a=" + a);
    System.out.println("b=" + b);
    // 测试是否修改了a的值
    b = 2;
    System.out.println("修改了b的值后,a=" + a);
    System.out.println("修改了b的值后,b=" + b);

}

运行结果

对于基本类型来说,直接用“=”就能完成克隆,并且这样克隆出来的对象是完全独立于原来的对象的。

引用类型对象

引用类型对象的克隆有两种方法

  • 用”=“进行克隆
  • 用Object类的clone方法进行克隆

下面先定义一个类:

public class School implements Cloneable {
    /**
     * 校名
     */
    String name;
    /**
     * 校长
     */
    String president;

    public School(String name, String president) {
        this.name = name;
        this.president = president;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPresident(String president) {
        this.president = president;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "School:[name=" + name + "," +
                "president=" + president + "]";
    }
}

用”=“进行克隆

示例

public static void main(String[] args) throws CloneNotSupportedException {
    School school = new School("家里蹲", "crazyhzm");
    // 用"="克隆
    School schoolDuplicate = school;
    System.out.println("school的hashcode=" + school.hashCode());
    System.out.println("schoolDuplicate的hashcode=" + schoolDuplicate.hashCode());
    System.out.println("school的值=" + school.toString());
    System.out.println("schoolDuplicate的值=" + schoolDuplicate.toString());
    // 修改 schoolDuplicate的值
    schoolDuplicate.setPresident("加点代码调调味");
    System.out.println("修改schoolDuplicate的值后,school的值=" + school.toString());
    System.out.println("修改schoolDuplicate的值后,schoolDuplicate的值=" + schoolDuplicate.toString());
}

运行结果

从运行结果看,用“=”进行克隆,其实只是克隆了原对象的内存地址,schoolDuplicate和school对象都指向了同一块内存区域,

用Object类的Clone方法进行克隆

示例

public static void main(String[] args) throws CloneNotSupportedException {
    School school = new School("家里蹲", "crazyhzm");
    // 用"clone"克隆
    School schoolDuplicateForClone = (School) school.clone();
    System.out.println("school的hashcode=" + school.hashCode());
    System.out.println("schoolDuplicateForClone的hashcode=" + schoolDuplicateForClone.hashCode());
    System.out.println("school的值=" + school.toString());
    System.out.println("schoolDuplicateForClone的值=" + schoolDuplicateForClone.toString());

    // 修改 schoolDuplicateForClone的值
    schoolDuplicateForClone.setPresident("点个赞");
    System.out.println("修改schoolDuplicateForClone的值后,school的值=" + school.toString());
    System.out.println("修改schoolDuplicateForClone的值后,schoolDuplicateForClone的值=" + schoolDuplicateForClone.toString());

}

运行结果

为什么要克隆?

了解了克隆什么以及怎么克隆后,我们到底为什么要使用克隆?

从JDK1.0版本开始,Java语言就提供了克隆机制。要知道我们为什么要克隆,先来了解一下克隆这种行为有什么方法替代,对于CV程序员来说,如果不能 复制黏贴,那就知道自己把代码一行行自己敲上去。那么对于对象的克隆来说,无非就是自己new一个新的对象,然后将其中的属性一个一个赋值过去,如果遇到引用数据类型,还需要继续new,继续赋值。当想要创建的新对象的属性值与原来的对象的属性值保持一致时,这种手动new,再一个一个赋值的行为就看似有点繁琐了,这种时候我们就可以用到克隆。从上面怎么克隆可以看到会出现两种不一样的结果,在克隆中,需要搞清楚什么是深拷贝和浅拷贝。

深拷贝和浅拷贝区别是什么?

在这里姑且把克隆改为拷贝,它存在深拷贝和浅拷贝,我们知道一个对象的属性中可能存在着另一个对象的引用,而这个对象是引用数据类型的。而浅克隆后的对象中非基本对象和原对象指向同一块内存,因此对这些非基本对象的修改会同时更改克隆前后的对象。深克隆可以实现完全的克隆,可以用反射的方式或序列化的方式实现。具体是怎么样的,我修改一下上述的示例来介绍:

浅拷贝

下面还是School类的定义,但是新增了一个Grade属性。

public class School implements Cloneable {
    /**
     * 校名
     */
    String name;
    /**
     * 校长
     */
    String president;

    Grade grade;

    public School(String name, String president) {
        this.name = name;
        this.president = president;
    }

    public School(String name, String president, Grade grade) {
        this.name = name;
        this.president = president;
        this.grade = grade;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPresident(String president) {
        this.president = president;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                ", president='" + president + '\'' +
                ", grade=" + grade.toString() +
                '}';
    }
}

Grade类定义:

public class Grade {
    /**
     * 年级名称
     */
    String name;

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

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Grade{" +
                "name='" + name + '\'' +
                '}';
    }
}

示例代码

public static void main(String[] args) throws CloneNotSupportedException {
    Grade grade = new Grade("一年级");
    School school = new School("家里蹲", "crazyhzm", grade);
    // 用"clone"克隆
    School schoolDuplicateForClone = (School) school.clone();
    System.out.println("school的hashcode=" + school.hashCode());
    System.out.println("school.grade的hashcode=" + school.grade.hashCode());
    System.out.println("schoolDuplicateForClone的hashcode=" + schoolDuplicateForClone.hashCode());
    System.out.println("schoolDuplicateForClone.grade的hashcode=" + schoolDuplicateForClone.grade.hashCode());
    System.out.println("school的值=" + school.toString());
    System.out.println("schoolDuplicateForClone的值=" + schoolDuplicateForClone.toString());

    // 修改 schoolDuplicateForClone的值
    schoolDuplicateForClone.setPresident("点个赞");
    schoolDuplicateForClone.grade.setName("二年级");
    System.out.println("修改schoolDuplicateForClone的值后,school的值=" + school.toString());
    System.out.println("修改schoolDuplicateForClone的值后,schoolDuplicateForClone的值=" + schoolDuplicateForClone.toString());

}

运行结果

从运行结果可以看到,School的拷贝对象修改了年级名称后,被拷贝对象的值也被修改了,并且可以从他们的hashcode也能看出来,在拷贝的时候都指向了同一个内存区域,这就是所谓的浅拷贝。

深拷贝

深拷贝我们稍微修改一下上述的School以及Grade代码

school类

修改了clone方法。

public class School implements Cloneable {
    /**
     * 校名
     */
    String name;
    /**
     * 校长
     */
    String president;

    Grade grade;

    public School(String name, String president) {
        this.name = name;
        this.president = president;
    }

    public School(String name, String president, Grade grade) {
        this.name = name;
        this.president = president;
        this.grade = grade;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPresident(String president) {
        this.president = president;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        School school = (School) super.clone();
        school.grade = (Grade) grade.clone();
        return school;
    }

    @Override
    public String toString() {
        return "School{" +
                "name='" + name + '\'' +
                ", president='" + president + '\'' +
                ", grade=" + grade.toString() +
                '}';
    }
}

Grade类

Grade实现了Cloneable接口。

public class Grade implements Cloneable{
    /**
     * 年级名称
     */
    String name;

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

    public void setName(String name) {
        this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Grade{" +
                "name='" + name + '\'' +
                '}';
    }
}

还是执行上述示例代码,运行后结果为:

可以看到school中grade属性也被拷贝,修改年级名称后,原对象的数据并没有随之改变,这就是深拷贝。

内推区域

点击【加点代码调调味】个人主页的最新沸点,有内推信息,欢迎咨询。

送福利区域

扫描下方二维码关注公众号【加点代码调调味】 点击菜单栏获取免费49篇的《Dubbo源码解析》系列文章