Java对象拷贝

2年前 (2022) 程序员胖胖胖虎阿
250 0 0

上篇《Java创建对象方式》介绍了对象创建的5中方式,本篇介绍下Java对象拷贝方式:浅拷贝深拷贝

在介绍拷贝之前,先说下Java基本复制方法 =

基本复制方法

public class CopyObject {
    public static void main(String[] args){
        Score score1 = new Score("math", "100");

        // 基本复制方法
        Score score2 = score1;
        System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
        System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());

        change(score2);
        System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
        System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
    }

    public static void change(Score score){
        score.grade = "90";
    }
}

输出:
score1=Score{course='math', grade='100'},hashCode=1625635731
score2=Score{course='math', grade='100'},hashCode=1625635731
score1=Score{course='math', grade='90'},hashCode=1625635731
score2=Score{course='math', grade='90'},hashCode=1625635731

Java中只有值传递。= 复制的是对象引用,并没有在堆空间中重新新建。对原先对象属性进行修改,复制对象的属性也随之修改。(引用指向同一块堆内存,理论上就是同一个内存空间。

浅拷贝、深拷贝概念

Java语言中,数据类型 分为 基本数据类型引用类型

  • 基本数据类型(值类型)
    • 字符类型 char
    • 布尔类型 boolean
    • 数值类型 byte、short、int、long、float、double
  • 引用类型
    • 类、接口、数组、枚举等

浅拷贝深拷贝的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

浅拷贝

clone方法

Object类的clone方法,访问限定符为protected

protected native Object clone() throws CloneNotSupportedException;

这是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。

因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。要想对一个对象进行复制,就需要对clone方法覆盖

修改基本类型

  1. 类需要实现Clonenable接口,该接口为标记接口。不实现的话在调用clone方法会抛出CloneNotSupportedException异常
  2. 重写clone()方法,访问修饰符设为public(方便类对象调用)。
public class Score implements Cloneable {
    public String course;
    public String grade;

    public Score() {
    }

    public Score(String course, String grade) {
        this.course = course;
        this.grade = grade;
    }

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

    @Override
    public String toString() {
        return "Score{" + "course='" + course + '\'' + ", grade='" + grade + '\'' + '}';
    }
}

调用:

public class CopyObject {
    public static void main(String[] args) throws CloneNotSupportedException {
        Score score1 = new Score("math", "100");

        // clone方法
        Score score2 = (Score) score1.clone();
        System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
        System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());

        change(score2);
        System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
        System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
    }

    public static void change(Score score){
        // 修改基本类型
        score.grade = "90";
    }
}

输出:
score1=Score{course='math', grade='100'},hashCode=1625635731
score2=Score{course='math', grade='100'},hashCode=1580066828
score1=Score{course='math', grade='100'},hashCode=1625635731
score2=Score{course='math', grade='90'},hashCode=1580066828

结果看出,原对象与新对象是两个不同的对象,拷贝出来的新对象与原对象内容一致。
而且,新对象的 基本类型String类型 的改变不会影响到原始对象。

如果改变引用类型呢?

修改引用类型

新增级别类Level:

public class Level {
    public String classLevel;
    public String gradeLevel;

    public Level(String classLevel, String gradeLevel) {
        this.classLevel = classLevel;
        this.gradeLevel = gradeLevel;
    }

    @Override
    public String toString() {
        return "Level{" +"classLevel='" + classLevel + '\'' +", gradeLevel='" + gradeLevel + '\'' +'}';
    }
}

类Score 属性增加类Level对象:

public class Score implements Cloneable {
    public String course;
    public String grade;
    public Level level;

    public Score() {
    }
    
    public Score(String course, String grade, Level level) {
        this.course = course;
        this.grade = grade;
        this.level = level;
    }
    
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Score{" +"course='" + course + '\'' +", grade='" + grade + '\'' +", level=" + level +'}';
    }
}

调用:

public class CopyObject {
    public static void main(String[] args) throws CloneNotSupportedException {
        Score score1 = new Score("math", "95", new Level("5","30"));

        // clone方法
        Score score2 = (Score) score1.clone();
        System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
        System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());

        change(score2);
        System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
        System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
    }

    public static void change(Score score){
        // 修改基本类型
        score.grade = "90";
        
        // 修改引用类型
        score.level.classLevel = "10";
    }
}

输出:
score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1625635731
score2=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1580066828
score1=Score{course='math', grade='95', level=Level{classLevel='10', gradeLevel='30'}},hashCode=1625635731
score2=Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}},hashCode=1580066828

新对象score2的修改影响到原始对象score1,说明引用类型的改变会影响到原始数据。进而说明clone()方法是浅拷贝,只是复制了level变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

深拷贝

那如何实现深拷贝呢?

有两个方法:

  1. 需要拷贝的引用类型也实现Cloneable接口、并覆写clone方法
  2. 利用序列化

1、需要拷贝的引用类型也实现Cloneable接口、并重写clone方法

Level实现Cloneable接口、重写clone方法:

public class Level implements Cloneable {
    ...
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    ...
}

Score增加引用对象的clone调用:

public class Score implements Cloneable {
    ...
    @Override
    public Object clone() throws CloneNotSupportedException {
        Score score = (Score) super.clone();  //浅复制

        Level level = (Level) this.level.clone(); //深度复制
        score.level = level;

        return score;
    }
    ...
}

调用结果:

score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1625635731
score2=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1580066828
score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=1625635731
score2=Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}},hashCode=1580066828

结果看出,对象score2的修改没有影响原对象score1。

多层引用类型拷贝问题

但是这种做法有个弊端,这里我们Score 类只有一个 Level 引用类型,而 Level 类没有,所以我们只用重写 Level 类的clone 方法。
但是如果 Level 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。

所以,可以考虑用序列化

2、序列化

序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中

通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深拷贝

需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。

类Score实现Serializable、新增serialClone()方法:

public class Score implements Serializable {
    public static final long serialVersionUID = 1L;
    
    public String course;
    public String grade;
    public Level level;

    public Score() { }

    public Score(String course, String grade, Level level) {
        this.course = course;
        this.grade = grade;
        this.level = level;
    }

    public Score serialClone() throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        oos.close();

        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        Score score = (Score) ois.readObject();
        ois.close();

        return score;
    }

    @Override
    public String toString() {
        return "Score{" +"course='" + course + '\'' +", grade='" + grade + '\'' +", level=" + level +'}';
    }
}

Level也必须实现Serializable,否则无法序列化:

public class Level implements Serializable {
    public static final long serialVersionUID = 1L;

    public String classLevel;
    public String gradeLevel;

    public Level(String classLevel, String gradeLevel) {
        this.classLevel = classLevel;
        this.gradeLevel = gradeLevel;
    }

    @Override
    public String toString() {
        return "Level{" +"classLevel='" + classLevel + '\'' +", gradeLevel='" + gradeLevel + '\'' +'}';
    }
}

调用:

public class CopyObject {
    public static void main(String[] args) throws CloneNotSupportedException {
        Score score1 = new Score("math", "95", new Level("5","30"));

        // 序列化
        Score score2 = null;
        try {
            score2 = (Score) score1.serialClone();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
        System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());

        change(score2);
        System.out.println("score1=" + score1 + ",hashCode="+score1.hashCode());
        System.out.println("score2=" + score2 + ",hashCode="+score2.hashCode());
    }

    public static void change(Score score){
        // 修改基本类型
        score.grade = "90";
        
        // 修改引用类型
        score.level.classLevel = "10";
    }
}

输出结果:

score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=723074861
score2=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=664223387
score1=Score{course='math', grade='95', level=Level{classLevel='5', gradeLevel='30'}},hashCode=723074861
score2=Score{course='math', grade='90', level=Level{classLevel='10', gradeLevel='30'}},hashCode=664223387

结果看出,对象score2的修改没有影响原对象score1。

小结

  1. 如果需要创建一个对象的副本,可以使用clone(浅拷贝),因为clone是一个native方法,底层实现,效率高
  2. 浅拷贝中,如果原型对象的成员变量是值类型,将复制一份给拷贝对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给拷贝对象,也就是说原型对象和拷贝对象的 引用类型 变量指向相同的内存地址
  3. 深拷贝中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给拷贝对象,深拷贝将原型对象的所有引用对象也复制一份给拷贝对象。
  4. 如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,可以用序列化的方式来实现对象的深拷贝。
版权声明:程序员胖胖胖虎阿 发表于 2022年9月22日 下午11:40。
转载请注明:Java对象拷贝 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...