Java深拷贝与浅拷贝

11 篇文章 0 订阅
7 篇文章 0 订阅

一、深拷贝与浅拷贝

  • 浅拷贝只会复制对象中基本数据类型数据和引用数据的对象内存地址,不会递归的复制引用对象、以及引用对象的引用对象…即浅拷贝得到的对象跟原始对象共享数据。
  • 深拷贝不仅会复制索引,还会复制数据本身,得到的是一份完完全全的独立对象。所以深拷贝比浅拷贝来说更加厚实,更加耗内存空间。

二、实现方式

1、使用Object中的clone方法

使用Object中的clone方法,必须实现Cloneable接口才可以调用clone()方法,否则抛出CloneNotSupportedException异常,深拷贝也需要实现Cloneable接口,同时其成员变量为引用类型的也需要实现Cloneable接口,然后重写clone方法。Cloneable接口是一个空接口,记作他是一个标记接口,实现Cloneable接口的类被标记为可以被clone的类。

代码示例:

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Long
 * @Date 2023/9/9 11:32
 */
public class People implements Cloneable {

    private String name;

    private String age;

    private Info info; // 是一个对象,也必须继续拷贝

    /**
     * 深拷贝
     *
     * @return Info
     */
    public People deepClone() {
        People people;
        try {
            people = (People) super.clone();
            people.info = this.info.deepClone();
        } catch (CloneNotSupportedException exception) {
            people = null;
            System.out.println("clone Info error: " + exception);
        }

        return people;
    }

}

class Info implements Cloneable {
    private String address;

    private List<String> familyMember = new ArrayList<>();

    /**
     * 深拷贝
     *
     * @return Info
     */
    public Info deepClone() {
        Info info;
        try {
            info = (Info) super.clone();
            info.familyMember = new ArrayList<>();
            info.familyMember.addAll(this.familyMember);
        } catch (CloneNotSupportedException exception) {
            info = null;
            System.out.println("clone Info error: " + exception);
        }
        return info;
    }
}

如上述代码示例,若People的属性不止三个,这时如果想得到一份数据一样但是完全独立的对象,则可以使用深拷贝的方式,相当于复制对象。且People中存在Info引用对象,则Info也得继续实现拷贝,才是真正实现的深拷贝,否则复制出来的Info是指向同一个内存对象,即为浅拷贝。

2、通过对象序列化 (实现Serializable接口)

使用序列化的方式来复制对象 对象需要继承Serializable接口。先将对象序列化,然后再反序列化成新的对象。
Serializable 接口是一个标记接口,不用实现任何方法,仅用于标识可序列化的语义。
反序列化实现 Serilaziable 接口的类并不会调用构造方法。反序列的对象是由 JVM(java虚拟机) 以存储的二进制位为基础来构造,不通过构造方法生成。

  • Serialization(序列化):将 java 对象以一连串的字节序列保存在本地磁盘中的过程,也可以说是保存 java 对象状态的过程。序列化可以将数据永久保存在磁盘上(通常保存在文件中),避免程序运行结束后对象从内存中消失,字节序列也方便在网络中传输。
  • Deserialization(反序列化):将保存在磁盘文件中的 java 字节序列重新转换成 java 对象称为反序列化。

对象是序列化的作用:

  • 1)对象持久化: 把对象的字节序列保存在磁盘或数据库中,一般是存在文件里,这使得对象的状态可以在程序重启或跨网络传输后得以恢复,方便数据的存储和传输。
  • 2)网络传输对象:在分布式系统中,对象的序列化与反序列化可以实现对象在网络中的传输,将对象转换成字节流后,可以通过网络将字节流发送给其他节点,接收方再将字节流反序列化为对象进行处理。
  • 3)共享对象状态: 在多线程或分布式环境中,对象的状态共享。可能引发并发访问问题,通过将对象序列化,可以将对象的状态进行共享,从而实现对象状态的一致性和同步。

序列化步骤:
步骤一:创建一个 ObjectOutputStream 输出流;
步骤二:调用 ObjectOutputStream 对象的 writeObject() 输出可序列化对象。

反序列化步骤:
步骤一:创建一个 ObjectInputStream 输入流;
步骤二:调用 ObjectInputStream 对象的 readObject() 得到序列化的对象。

代码示例:

import java.io.Serializable;
public class User implements Serializable {

  private String name;
  private Address2 address;

  public User(String name, Address2 address) {
    this.name = name;
    this.address = address;
  }

  public String getName() {
    return name;
  }

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

  public Address2 getAddress() {
    return address;
  }

  public void setAddress(Address2 address) {
    this.address = address;
  }

  /**
     * 深拷贝
     *
     * @return Object
     */
  public User deepClone() throws Exception
  {
    // 序列化
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);

    oos.writeObject(this);

    // 反序列化
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);

    return (User) ois.readObject();
  }
}

class Address2 implements Serializable {
  private String city;
  private String country;

  public Address2(String city, String country) {
    this.city = city;
    this.country = country;
  }

  public String getCity() {
    return city;
  }

  public void setCity(String city) {
    this.city = city;
  }

  public String getCountry() {
    return country;
  }

  public void setCountry(String country) {
    this.country = country;
  }

  @Override
  public String toString() {
    return "Address2{" +
        "city='" + city + '\'' +
        ", country='" + country + '\'' +
        '}';
  }
}

class Test {
    public static void main(String[] args) {
        try {
            // 创建一个 ObjectOutputStream 输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\person.txt"));
            // 第一次序列化 person 对象
            Person person = new Person("涛涛", 21);
            oos.writeObject(person);
            // 第二次序列化 person
            oos.writeObject(person);

            // 创建一个 ObjectInputStream 输入流
            ObjectInputStream ios = new ObjectInputStream(new FileInputStream("D:\\person.txt"));
            // 依次反序列化出 p1、p2
            Person p1 = (Person) ios.readObject();
            Person p2 = (Person) ios.readObject();
            
            // 判断 p1、p2 是否是同一对象
            int i1 = System.identityHashCode(p1);
            int i2 = System.identityHashCode(p2);
            System.out.println("i1 = " + i1);
            System.out.println("i2 = " + i2);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

>>> 输出结果:
i1 = 1349393271
i2 = 1349393271

serializable 接口的注意事项:

  • 如果实现 Serializable 接口的类有父类,则父类也必须可以序列化,若父类没有实现序列化接口,则父类必须有无参构造函数,否则会抛异常 java.io.InvalidClassException。因为在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。
  • 序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。即static属性不会被序列化
  • 某些属性不需要序列化。使用 transient 关键字选择不需要序列化的字段。
  • 同一对象多次序列化,并不会得到多个二进制流,既不会反序列化为多个对象。而是只有第一次序列化为二进制流,以后都只是保存序列化版本号,且按自上而下的顺序依次保存。(反序列化时的顺序与序列化时的顺序一致)
  • java 序列化提供了一个 “private static final long serialVersionUID” 的序列化版本号,只要版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。所以序列化一个类时最好指定一个序列化版本号,方便项目升级。JVM 首先会通过类名来区分 Java 类,类名不同,则不是同一个类。当类名相同时,JVM 就会通过序列化版本号来区分 Java 类,如果序列化版本号相同就为同一个类,序列化版本号不同就为不同的类。在序列化一个对象时,如果没有指定序列化版本号,后期对该类的源码进行修改并重新编译后,可能会导致修改前后的序列化版本号不一致,因为 JVM 会提供一个新的序列化版本号给该类对象,此时再用以往的反序列化代码去反序列化该类的对象,会导致反序列化使用的class的版本号与序列化时使用的不一致,就会抛出异常 java.io.InvalidClassException。
  • 在Java中,如果一个实现了Serializable接口的类没有指定serialVersionUID,那么JVM在序列化时,会根据属性自动生成一个serialVersionUID,然后和属性一起序列化,再进行网络传输或者持久化。在反序列化时,JVM会再根据属性生成一个新版本的serialVersionUID,然后再用这个新版本和serialVersionUID和序列化时生成的旧版本的serialVersionUID进行计较。如果二者一样就可以序列化成功。反之,报错。
  • 如果我们显示指定了serialVersionUID,JVM在序列化和反序列化时,就会使用我们指定的serialVersionUID。这样我们就可以确保在反序列化时,serialVersionUID和之前的相同。

三、原型设计模式

深拷贝与浅拷贝就是原型设计模式的两种实现方式。

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同,在这种情况下可以利用对已有对象(原型)进行复制(拷贝)的方式来创建新对象,以达到节省创建时间的目的,这种基于原型来创建对象的方式就叫做原形设计模式,简称原形模式。

实际上创建对象包含的申请内存、给成员变量赋值这一过程本身并不会花费太多时间,或者说对于大部分业务系统来说,这些时间完全是可以忽略的,应用一个复杂的模型只得到一点点的性能提升,就是所谓的过渡设计、得不偿失。
但是如果对象中的数据需要经过复杂的计算才能得到,(比如排序、计算哈希值),或者需要从PC网络,数据库,文件系统等非常慢速的io中读取,这种情况下就可以使用原型模式从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候都重复执行这些耗时的操作。

  • 31
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是用于描述对象拷贝的概念。 浅拷贝是指创建一个新对象,新对象的属性值和原对象的属性值相同,但是对于引用类型的属性,新旧对象将共享同一个引用。也就是说,新旧对象的引用类型属性指向同一个内存地址。 深拷贝则是在拷贝对象时,不仅会复制对象本身,还会递归复制对象的引用类型属性,使得新对象和原对象的引用类型属性指向不同的内存地址。 Java 中的默认拷贝行为是浅拷贝。可以通过以下两种方式实现深拷贝: 1. 实现 `Cloneable` 接口并重写 `clone()` 方法:这是一种较为简单的实现深拷贝的方式。在需要进行深拷贝的类中,实现 `Cloneable` 接口,并重写 `clone()` 方法,在 `clone()` 方法中递归复制引用类型的属性。 ```java class MyClass implements Cloneable { private int number; private MyObject myObject; // constructors, getters, setters @Override protected Object clone() throws CloneNotSupportedException { MyClass cloned = (MyClass) super.clone(); cloned.myObject = (MyObject) myObject.clone(); // 实现 MyObject 的 clone() 方法 return cloned; } } ``` 通过调用 `clone()` 方法来创建一个新的深拷贝对象:`MyClass clonedObject = (MyClass) originalObject.clone();` 2. 使用序列化和反序列化:这是另一种实现深拷贝的方式。通过将对象序列化为字节流,然后再将字节流反序列化为新的对象,可以实现深拷贝。这种方式需要确保对象及其引用类型属性都是可序列化的。 ```java import java.io.*; class MyClass implements Serializable { // class definition } ``` ```java // 实现深拷贝的方法 public static <T extends Serializable> T deepCopy(T object) throws IOException, ClassNotFoundException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(object); ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); return (T) objectInputStream.readObject(); } ``` 通过调用 `deepCopy()` 方法来创建一个新的深拷贝对象:`MyClass clonedObject = deepCopy(originalObject);` 需要注意的是,为了实现深拷贝,引用类型的类也需要实现 `Cloneable` 接口并重写 `clone()` 方法,或者是可序列化的。 希望这些信息对你有所帮助。如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值