likes
comments
collection
share

Java: 避免使用 Cloneable 接口,如何实现深拷贝?

作者站长头像
站长
· 阅读数 1

背景

在 Java 中,Cloneable接口默认的 clone方法提供的是浅拷贝。要实现深拷贝,需要在 clone 方法中手动实现所有可变成员变量的复制,确保复制的是对象的内容,而不是引用。这个过程可能非常繁琐,尤其是对象结构复杂时。并且当对象中的字段经常发生变化时,clone 方法需要不断更新以匹配当前的字段,这导致维护的困难和出错的风险增加。

在本文中,我们将探索多种利用序列化技术来实现对象深拷贝的方法。考虑到每种方法都适用于特定场合,可以根据自己的实际需要来挑选最佳的解决方案。通过序列化实现深拷贝的原理就是先将对象序列化成字符串或字节流,在反序列化成原类型的对象,达到深层次的对象复制的目的。

这些方法可以简单的分为两种类型,一种是拷贝的对象需要实现 Serializable 接口的:

  • Spring 框架提供的序列化方法
  • Apache Commons 中的序列化方法
  • 通过 Java 的 IO 流序列化

此外,对于一些无法实现 Serializable 接口的对象,通过使用各种 JSON 处理库实现深拷贝也是一个有效途径。这些库采用自身的序列化机制,与 Java 的 Serializable 接口并不挂钩。文章中将展示四种流行的 JSON 库(Fastjson, Jsoniter, Jackson, Gson) 的使用示例,便于根据项目需求选择相应的工具进行深拷贝的实现。

1. Spring Framework SerializationUtils 实现深拷贝

需要对象实现 Serializable 接口。

org.springframework.util.SerializationUtils 是 Spring 框架提供的一个工具类,用于对 Java 对象进行序列化和反序列化。Spring 项目无需导入第三方依赖就可以使用。

Spring 6.0 版本中,SerializationUtils 类提供了 clone() 方法可以直接完成一个对象的深拷贝。在其他版本,也可以使用 SerializationUtils 类提供的序列化和反序列化方法实现深拷贝。

测试类:

@Data
public class Person {
    private String name;
    private Integer age;
    private String sex;

    public Person(String name, Integer age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}
@Data
public class Organize {
    private String id;
    private String name;
    private List<Person> member;

    public Organize(String id, String name, List<Person> member)
    {
        this.id = id;
        this.name = name;
        this.member = member;
    }
}

深拷贝测试:

public static void main(String[] args) throws Exception {
    Person person1 = new Person("aaa", 18, "男");
    Person person2 = new Person("bbb", 19, "女");
    Organize organize = new Organize("org", "orgName", List.of(person1, person2));

    long start = System.currentTimeMillis();
    // 深拷贝 organize 对象
    // Spring 6.0
    Organize copyOrganize = SerializationUtils.clone(organize);
    // Spring 6.0 以下版本:
    //Organize copyOrganize = (Organize) SerializationUtils.deserialize(serialize(organize));
    System.out.println("耗时:"+(System.currentTimeMillis() - start));

    copyOrganize.getMember().get(0).setName("ccc");
    copyOrganize.getMember().get(0).setAge(20);
    copyOrganize.getMember().get(1).setName("dddd");
    copyOrganize.setName("orgName111111");

    System.out.println("origin: "+organize);
    System.out.println("copy: "+copyOrganize);
}

耗时:21

origin: Organize(id=org, name=orgName, member=[Person(name=aaa, age=18, sex=男), Person(name=bbb, age=19, sex=女)])

copy: Organize(id=org, name=orgName111111, member=[Person(name=ccc, age=20, sex=男), Person(name=dddd, age=19, sex=女)])

2. Apache Commons Lang 库的深拷贝工具

需要对象实现 Serializable 接口。

Apache Commons Lang 是一个提供了大量基础工具类和方法的开源库,以帮助开发者处理一些常见的编程任务,例如字符串操作、数字处理、系统属性、反射、并发性、对象创建、序列化等。

其中的 SerializationUtils.clone 方法的内部实现是通过将对象序列化成字节流(byte[]),然后再从该字节流反序列化回来创建对象的副本。这种方式的深拷贝确保了所有通过序列化过程创建的对象都是全新的实例,从而防止了对原始实例中任何嵌套引用对象的共享。

导入依赖:

<!-- org.apache.commons-lang3 -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.14.0</version>
</dependency>

测试类:

@Data
public class Person implements Serializable {
    private static final long serialVersionUID = 3148380222986005471L;
    private String name;
    private Integer age;
    private String sex;

    public Person(String name, Integer age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}
@Data
public class Organize implements Serializable {
    private static final long serialVersionUID = 2414543438700324722L;
    private String id;
    private String name;
    private List<Person> member;

    public Organize(String id, String name, List<Person> member)
    {
        this.id = id;
        this.name = name;
        this.member = member;
    }
}

深拷贝测试:

public static void main(String[] args) throws Exception {
    Person person1 = new Person("aaa", 18, "男");
    Person person2 = new Person("bbb", 19, "女");
    Organize organize = new Organize("org", "orgName", List.of(person1, person2));

    long start = System.currentTimeMillis();
    // 深拷贝 organize 对象
    Organize copyOrganize = SerializationUtils.clone(organize);
    System.out.println("耗时:"+(System.currentTimeMillis() - start));

    copyOrganize.getMember().get(0).setName("ccc");
    copyOrganize.getMember().get(0).setAge(20);
    copyOrganize.getMember().get(1).setName("dddd");
    copyOrganize.setName("orgName111111");

    System.out.println("origin: "+organize);
    System.out.println("copy: "+copyOrganize);
}

耗时:33

origin: Organize(id=org, name=orgName, member=[Person(name=aaa, age=18, sex=男), Person(name=bbb, age=19, sex=女)])

copy: Organize(id=org, name=orgName111111, member=[Person(name=ccc, age=20, sex=男), Person(name=dddd, age=19, sex=女)])

3. JSON 序列化实现深拷贝

JSON 序列化和反序列化不要求对象实现 Serializable 接口,因为 JSON 库(如 Gson, Jackson, Fastjson 等)使用自己的序列化机制来将对象转换为JSON字符串,以及将 JSON 字符串转换为对象,而与 Java 原生的序列化机制(即通过 Serializable 接口)无关。

测试类:

@Data
public class Person {
    private String name;
    private Integer age;
    private String sex;

    public Person(String name, Integer age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}
@Data
public class Organize {
    private String id;
    private String name;
    private List<Person> member;

    public Organize(String id, String name, List<Person> member)
    {
        this.id = id;
        this.name = name;
        this.member = member;
    }
}

Alibaba Fastjson

Fastjson 是一个 Java 语言编写的高性能功能完备的 JSON 库,由阿里巴巴公司开源。Fastjson 的亮点包括其快速的处理速度和丰富的特性,特别是涉及到序列化和反序列化操作时,可以轻松实现对象与 JSON 之间的转换,而无需编写冗长的代码处理 JSON 的解析或格式化。

Fastjson 分为两个版本: fastjson 和 fastjson2。早期的 fastjson 版本在安全性方面曾经遭遇一些问题,为了解决这些问题并提供更好的性能和功能, fastjson2 应运而生。fastjson2 是对 fastjson 的重构和升级版本。如果正在进行新的开发项目,推荐使用 fastjson2。Fastjson的开发团队已经明确表示,Fastjson2是未来的主要支持方向。

导入 fastjson 依赖:

<!-- fastjson2 -->
<dependency>
  <groupId>com.alibaba.fastjson2</groupId>
  <artifactId>fastjson2</artifactId>
  <version>2.0.48</version>
</dependency>

<!-- fastjson -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.83</version>
</dependency>

Fastjson序列化和反序列化方法:

// fastjson2
public static <T> T fastJsonDeepCopy(T object, Class<T> clazz) {
    // 序列化
    String jsonString = JSON.toJSONString(object);
    // 反序列化
    return JSON.parseObject(jsonString, clazz);
}

// fastjson
public static <T> T fastJsonDeepCopy(T object, Class<T> clazz) {
    // 序列化(调整SerializerFeature可以关闭某些行为以优化性能,比如跳过transient修饰的属性)
    String jsonString = JSON.toJSONString(object, SerializerFeature.SkipTransientField);
    // 反序列化
    return JSON.parseObject(jsonString, clazz);
}

深拷贝测试:

public static void main(String[] args) throws Exception {
    Person person1 = new Person("aaa", 18, "男");
    Person person2 = new Person("bbb", 19, "女");
    Organize organize = new Organize("org", "orgName", List.of(person1, person2));

    long start = System.currentTimeMillis();
    // 深拷贝 organize 对象
    Organize copyOrganize = DeepCopyUtils.fastJsonDeepCopy(organize, Organize.class);
    System.out.println("耗时:"+(System.currentTimeMillis() - start));

    copyOrganize.getMember().get(0).setName("ccc");
    copyOrganize.getMember().get(0).setAge(20);
    copyOrganize.getMember().get(1).setName("dddd");
    copyOrganize.setName("orgName111111");

    System.out.println("origin:"+organize);
    System.out.println("copy:"+copyOrganize);
}

耗时:172

origin: Organize(id=org, name=orgName, member=[Person(name=aaa, age=18, sex=男), Person(name=bbb, age=19, sex=女)])

copy: Organize(id=org, name=orgName111111, member=[Person(name=ccc, age=20, sex=男), Person(name=dddd, age=19, sex=女)])

Jsoniter (json-iterator)

Jsoniter 是一个高性能的 JSON 解析器和序列化库,开发团队宣称它的性能超越了许多同类库,包括 Gson 和 Jackson。Jsoniter 的设计目标之一是为了实现快速的 JSON 解析和序列化能力。

官方文档:jsoniter.com/index.cn.ht…

导入Jsoniter依赖:

<!-- com.jsoniter/jsoniter -->
<dependency>
  <groupId>com.jsoniter</groupId>
  <artifactId>jsoniter</artifactId>
  <version>0.9.23</version>
</dependency>

序列化和反序列化方法:

public static <T> T jsoniterDeepCopy(T object, Class<T> clazz) {
    // 序列化
    String jsonString = JsonStream.serialize(object);
    // 反序列化
    return JsonIterator.deserialize(jsonString, clazz);
}

深拷贝测试:

public static void main(String[] args) throws Exception {
    Person person1 = new Person("aaa", 18, "男");
    Person person2 = new Person("bbb", 19, "女");
    Organize organize = new Organize("org", "orgName", List.of(person1, person2));

    long start = System.currentTimeMillis();
    // 深拷贝 organize 对象
    Organize copyOrganize = jsoniterDeepCopy(organize, Organize.class);
    System.out.println("耗时:"+(System.currentTimeMillis() - start));

    copyOrganize.getMember().get(0).setName("ccc");
    copyOrganize.getMember().get(0).setAge(20);
    copyOrganize.getMember().get(1).setName("dddd");
    copyOrganize.setName("orgName111111");

    System.out.println("origin: "+organize);
    System.out.println("copy: "+copyOrganize);
}

耗时:85

origin: Organize(id=org, name=orgName, member=[Person(name=aaa, age=18, sex=男), Person(name=bbb, age=19, sex=女)])

copy: Organize(id=org, name=orgName111111, member=[Person(name=ccc, age=20, sex=男), Person(name=dddd, age=19, sex=女)])

Jackson

Jackson 是当前最流行的一个用于处理 JSON 数据格式的 Java 库。Jackson 的处理速度非常快,在 JSON 解析器中表现突出,能够快速处理大量数据。它由 FasterXML 社区维护,并被广泛用于各种 Java 应用程序中,尤其是在 Web 应用程序和微服务架构中。

需要注意的是,Jackson 是 Spring Boot 默认的 JSON 解析器,如果你的项目使用的框架是 Spring Boot ,无需重复引入依赖。

Jackson 依赖:

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.17.0</version>
</dependency>

序列化和反序列化方法:

// 重用同一个ObjectMapper实例以提高性能
public static ObjectMapper mapper = new ObjectMapper();
public static <T> T jacksonDeepCopy(T object, Class<T> clazz) throws JsonProcessingException {
    // 序列化
    String jsonString = mapper.writer().writeValueAsString(object);
    // 反序列化
    return mapper.readValue(jsonString, clazz);
}

深拷贝测试:

public static void main(String[] args) throws Exception {
    Person person1 = new Person("aaa", 18, "男");
    Person person2 = new Person("bbb", 19, "女");
    Organize organize = new Organize("org", "orgName", List.of(person1, person2));

    long start = System.currentTimeMillis();
    // 深拷贝 organize 对象
    Organize copyOrganize = jacksonDeepCopy(organize, Organize.class);
    System.out.println("耗时:"+(System.currentTimeMillis() - start));

    copyOrganize.getMember().get(0).setName("ccc");
    copyOrganize.getMember().get(0).setAge(20);
    copyOrganize.getMember().get(1).setName("dddd");
    copyOrganize.setName("orgName111111");

    System.out.println("origin: "+organize);
    System.out.println("copy: "+copyOrganize);
}

耗时:270

origin: Organize(id=org, name=orgName, member=[Person(name=aaa, age=18, sex=男), Person(name=bbb, age=19, sex=女)])

copy: Organize(id=org, name=orgName111111, member=[Person(name=ccc, age=20, sex=男), Person(name=dddd, age=19, sex=女)])

Gson

Gson 是由 Google 开发的一个 JSON 库,它支持简单且灵活的序列化方式。

导入 Gson 依赖:

<!-- com.google.code.gson/gson -->
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.10.1</version>
</dependency>

序列化和反序列化方法:

// 重用同一个Gson实例以提高性能
public static Gson gson = new Gson();
public static <T> T gsonDeepCopy(T object, Class<T> clazz) {
    // 序列化
    String jsonString = gson.toJson(object);
    // 反序列化
    return gson.fromJson(jsonString, clazz);
}

深拷贝测试:

public static void main(String[] args) throws Exception {
    Person person1 = new Person("aaa", 18, "男");
    Person person2 = new Person("bbb", 19, "女");
    Organize organize = new Organize("org", "orgName", List.of(person1, person2));

    long start = System.currentTimeMillis();
    // 深拷贝 organize 对象
    Organize copyOrganize = gsonDeepCopy(organize, Organize.class);
    System.out.println("耗时:"+(System.currentTimeMillis() - start));

    copyOrganize.getMember().get(0).setName("ccc");
    copyOrganize.getMember().get(0).setAge(20);
    copyOrganize.getMember().get(1).setName("dddd");
    copyOrganize.setName("orgName111111");

    System.out.println("origin: "+organize);
    System.out.println("copy: "+copyOrganize);
}

耗时:250

origin: Organize(id=org, name=orgName, member=[Person(name=aaa, age=18, sex=男), Person(name=bbb, age=19, sex=女)])

copy: Organize(id=org, name=orgName111111, member=[Person(name=ccc, age=20, sex=男), Person(name=dddd, age=19, sex=女)])

4. IO流序列化实现深拷贝

需要对象实现 Serializable 接口。

Java 原生的序列化和反序列化方式,将对象的状态转换为可以被存储或传输的字节流,再将字节流中的数据转换回对象,Java 序列化机制会重建对象,并为其分配内存。

需要注意的是,Java 原生序列化相对效率较低,可能并不是所有用途的最优选择,特别是对性能有严格要求的场合。如果是性能要求高的场景,推荐使用上面介绍的方法。

测试类:

@Data
public class Person implements Serializable {
    private static final long serialVersionUID = 3148380222986005471L;
    private String name;
    private Integer age;
    private String sex;

    public Person(String name, Integer age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}
@Data
public class Organize implements Serializable {
    private static final long serialVersionUID = 2414543438700324722L;
    private String id;
    private String name;
    private List<Person> member;

    public Organize(String id, String name, List<Person> member)
    {
        this.id = id;
        this.name = name;
        this.member = member;
    }
}

IO流序列化深拷贝方法:

public static <T> T streamDeepCopy(T object) throws IOException, ClassNotFoundException{
    // 写入对象到流中
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(object);
    oos.close();
    // 从流中读取对象以创建深拷贝
    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bais);
    // 注:此处会抑制类型转换未经检查的警告
    @SuppressWarnings("unchecked")
    T deepCopy = (T) ois.readObject();
    ois.close();
    return deepCopy;
}

深拷贝测试:

public static void main(String[] args) throws Exception {
    Person person1 = new Person("aaa", 18, "男");
    Person person2 = new Person("bbb", 19, "女");
    Organize organize = new Organize("org", "orgName", List.of(person1, person2));

    long start = System.currentTimeMillis();
    // 深拷贝 organize 对象
    Organize streamDeepCopy = DeepCopyUtils.streamDeepCopy(organize);
    System.out.println("耗时:"+(System.currentTimeMillis() - start));

    streamDeepCopy.getMember().get(0).setName("ccc");
    streamDeepCopy.getMember().get(0).setAge(20);
    streamDeepCopy.getMember().get(1).setName("dddd");
    streamDeepCopy.setName("orgName111111");

    System.out.println(organize);
    System.out.println(streamDeepCopy);
}

耗时:268

Organize(id=org, name=orgName, member=[Person(name=aaa, age=18, sex=男), Person(name=bbb, age=19, sex=女)])

Organize(id=org, name=orgName111111, member=[Person(name=ccc, age=20, sex=男), Person(name=dddd, age=19, sex=女)])

总结

七种深拷贝方式全部测试完成,都能够实现对象的深层次复制,那么对比一下他们的各项指标吧。

类型耗时(ms)是否需要导入依赖是否需要实现Serializable接口
Spring SerializationUtils21Spring 项目不需要
Commons SerializationUtils33
Alibaba Fastjson172
Jsoniter85
Jackson270
Gson250
Java IO 流268

耗时是多次测试后选择的一个中间值,仅供参考

总结:

  • 如果对象实现了 Serializable 接口,推荐使用 Spring SerializationUtilsCommons SerializationUtils
  • 如果对象没有实现 Serializable 接口,可以直接使用项目中使用的 JSON 库,效率相差不大;如果追求高性能,推荐使用 Jsoniter 库。
  • 实测可以同时引入多个 JSON 库的依赖,不会冲突。