聊聊对象浅拷贝和深拷贝

1. 前言

Java中的数据类型分为基本数据类型(值类型)和引用数据类型,基本数据类型包括byte、short、int、long、float、double、boolean、char等简单数据类型,引用类型包括:类、接口、数组等复杂类型。

根据数据类型的不同,在进行数据值拷贝的适合,如果是基本数据类型,复制的是属性值,如果是引用数据类型,比如对象,复制的内容可能是属性对应的内存引用地址。

因此,在Java中对于引用数据类型,也分为浅拷贝(浅克隆)深拷贝(深克隆) ,区别如下:

  • 浅拷贝:将原对象或原数组的引用直接赋给新对象或新数组,新对象只是原对象的一个引用,也就是说不管新对象还是原对象都是引用同一个对象
  • 深拷贝:创建一个新的对象或数组,将原对象的各项属性的值拷贝过来,是”值”而不是”引用”,两者对象是不一样的。

2. 案例实践

2.1 浅拷贝

首先新建两个对象,其中User关联Account对象,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Account {

private BigDecimal money;

public BigDecimal getMoney() {
return money;
}

public void setMoney(BigDecimal money) {
this.money = money;
}

@Override
public String toString() {
return "Account{" +
"money=" + money +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class User {
private Long userId;

private Account account;

public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}

public Account getAccount() {
return account;
}

public void setAccount(Account account) {
this.account = account;
}

@Override
public String toString() {
return "User{" +
"userId=" + userId +
", account=" + account +
'}';
}
}

使用spring BeanUtils工具进行对象属性赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(1000));

User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);

//进行对象属性拷贝
User targetUser = new User();
BeanUtils.copyProperties(sourceUser, targetUser);
System.out.println("修改嵌套对象属性前的结果: "+targetUser.toString());

sourceAccount.setMoney(BigDecimal.valueOf(2000));

System.out.println("修改嵌套对象属性后的结果: "+targetUser.toString());
}

输出结果如下:

1
2
修改嵌套对象属性前的结果: User{userId=1, account=Account{money=1000}}
修改嵌套对象属性后的结果: User{userId=1, account=Account{money=2000}}

从结果上可以看出:当修改原始的嵌套对象Account的属性值时,目标对象的Account对象的值也跟着变化。

很显然,这与我们预想的对象属性拷贝是相违背的。面对这种情况,我们可以把对象Account单独拉出来,进行依次属性值拷贝,然后再进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(1000));

User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);

//1. 先进行嵌套对象赋值
Account targetAccount = new Account();
BeanUtils.copyProperties(sourceAccount, targetAccount);
User targetUser = new User();
targetUser.setAccount(targetAccount);
System.out.println("修改嵌套对象属性前的结果: "+targetUser.toString());
sourceAccount.setMoney(BigDecimal.valueOf(2000));
System.out.println("修改嵌套对象属性后的结果: "+targetUser.toString());
}

这样即使Account对象数据发生变化,也不会改变目标对象的数据,现在的情况是User只有一个嵌套对象,如果有很多个呢?这种方式就不可取了,那么就要用到深拷贝。

2.2 深拷贝

2.2.1 通过写入文件方式

Java深拷贝有两种实现方式, 第一种是通过将对象序列化到临时文件,然后再通过反序列化方式,从临时文件中读取数据,具体操作如下:

首先所有的类,必须要实现Serializable接口,推荐显示定义序列化ID

1
2
3
4
5
6
7
public class User implements Serializable {
//.....省略,同上
}

public class Account implements Serializable {
//.....省略,同上
}

对象序列化与反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static void main(String[] args) {
Account sourceAccount = new Account();
sourceAccount.setMoney(BigDecimal.valueOf(1000));

User sourceUser = new User();
sourceUser.setUserId(1L);
sourceUser.setAccount(sourceAccount);

//把对象写入文件中
try{
FileOutputStream fos = new FileOutputStream("temp.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(sourceUser);
oos.flush();
oos.close();
fos.close();
}catch (Exception ex){
ex.printStackTrace();
}
//从文件中读取对象
User targetUser = null;
try {
FileInputStream fis = new FileInputStream("temp.out");
ObjectInputStream ois = new ObjectInputStream(fis);
targetUser = (User) ois.readObject();
fis.close();
ois.close();
}catch (Exception ex){
ex.printStackTrace();
}
System.out.println("修改嵌套对象属性前的结果: "+targetUser.toString());
sourceAccount.setMoney(BigDecimal.valueOf(2000));
System.out.println("修改嵌套对象属性后的结果: "+targetUser.toString());
}

输出结果:

1
2
修改嵌套对象属性前的结果: User{userId=1, account=Account{money=1000}}
修改嵌套对象属性后的结果: User{userId=1, account=Account{money=1000}}

通过序列化和反序列化的方式,可以实现多层复杂度的对象数据拷贝。

因为涉及到需要将数据写入临时磁盘,性能可能会有所下降。

2.2.2 JSON序列化和反序列化

采用json序列化和反序列化的技术实现,同时性能也比将数据写入临时磁盘的方式要好很多。并且不需要显示实现序列化接口。

json序列化和反序列化的底层思想是:将对象序列化成字符串;然后再将字符串通过反序列化方式成对象。

  1. 引入相关的jackson
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.14.2</version>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.14.2</version>
    </dependency>
    <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.14.2</version>
    </dependency>
  1. 编写统一Json处理工具类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    /**
    * @author xiaoyuge
    */
    public class JsonUtils {
    private static final Logger log = LoggerFactory.getLogger(JsonUtils.class);

    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
    // 序列化时,将对象的所有字段全部列入
    objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
    // 允许没有引号的字段名
    objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
    // 自动给字段名加上引号
    objectMapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, true);
    // 时间默认以时间戳格式写,默认时间戳
    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
    // 忽略空bean转json的错误
    objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    // 设置时间转换所使用的默认时区
    objectMapper.setTimeZone(TimeZone.getDefault());


    // 反序列化时,忽略在json字符串中存在, 但在java对象中不存在对应属性的情况, 防止错误
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);

    //序列化/反序列化,自定义设置
    SimpleModule module = new SimpleModule();
    // 序列化成json时,将所有的long变成string
    module.addSerializer(Long.class, ToStringSerializer.instance);
    module.addSerializer(Long.TYPE, ToStringSerializer.instance);
    // 自定义参数配置注册
    objectMapper.registerModule(module);
    }

    /**
    * 对象序列化成字符串
    *
    * @param obj 对西那个
    * @param <T> 字符串
    * @return 序列化
    */
    public static <T> String objToStr(T obj) {
    if (null == obj) {
    return null;
    }

    try {
    return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
    } catch (Exception e) {
    log.warn("objToStr error: ", e);
    return null;
    }
    }

    /**
    * 字符串反序列化成对象
    *
    * @param str
    * @param clazz
    * @param <T>
    * @return
    */
    public static <T> T strToObj(String str, Class<T> clazz) {
    try {
    return clazz.equals(String.class) ? (T) str : objectMapper.readValue(str, clazz);
    } catch (Exception e) {
    log.warn("strToObj error: ", e);
    return null;
    }
    }

    /**
    * 字符串反序列化成对象(数组)
    *
    * @param str
    * @param typeReference
    * @param <T>
    * @return
    */
    public static <T> T strToObj(String str, TypeReference<T> typeReference) {
    try {
    return (T) (typeReference.getType().equals(String.class) ? str : objectMapper.readValue(str, typeReference));
    } catch (Exception e) {
    log.warn("strToObj error", e);
    return null;
    }
    }
    }
  2. 编写测试代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static void main(String[] args) {
    Account sourceAccount = new Account();
    sourceAccount.setMoney(BigDecimal.valueOf(1000));

    User sourceUser = new User();
    sourceUser.setUserId(1L);
    sourceUser.setAccount(sourceAccount);

    //json 序列化和反序列化
    User targetUser = JsonUtils.strToObj(JsonUtils.objToStr(sourceUser), User.class);
    System.out.println("修改嵌套对象属性前的结果: "+targetUser.toString());
    sourceAccount.setMoney(BigDecimal.valueOf(2000));
    System.out.println("修改嵌套对象属性后的结果: "+targetUser.toString());
    }
  1. 查看输出结果
    1
    2
    修改嵌套对象属性前的结果: User{userId=1, account=Account{money=1000}}
    修改嵌套对象属性后的结果: User{userId=1, account=Account{money=1000}}

3. 总结

  1. 浅拷贝下,原对象和目标对象,引用都是同一个对象,当被引用的对象数据发生变时,相关的引用者也会跟着一起改变
  1. 深拷贝下,原对象和目标对象数据时两个完全独立的存在,相互之间不受影响
  1. 如果对象需要深拷贝,推荐采用json序列化和反序列化的方式实现,相比通过文件写入的方式进行序列化和反序列化,操作简单且性能高。