面试官问:什么是浅拷贝和深拷贝?

前言

平时我们从数据库查询出 po 对象,要返回给前端时,会有另一个对象 vo,此时我们需要将 po 的值复制给 vo,如果是你,你会怎么做呢?

有时我们除了复制之外,还要求 po 参数值的改变不能影响到 vo,也就是 po 和 vo 是两个独立的个体,此时我们又需要怎么做呢?

带着这些疑问,我们一起来看下今天所要讲解的关于对象复制的知识点。

一、什么是浅拷贝和深拷贝

浅拷贝

  • 对于基本数据类型的成员变量,浅拷贝直接进行值传递,也就是将属性值复制了一份给新的成员变量

  • 对于引用数据类型的成员变量,比如成员变量是数组、某个类的对象等,浅拷贝就是引用的传递,也就是将成员变量的引用(内存地址)复制了一份给新的成员变量,他们指向的是同一个事例。在一个对象修改成员变量的值,会影响到另一个对象中成员变量的值。

深拷贝

  • 对于基本数据类型,深拷贝复制所有基本数据类型的成员变量的值

  • 对于引用数据类型的成员变量,深拷贝申请新的存储空间,并复制该引用对象所引用的对象,也就是将整个对象复制下来。所以在一个对象修改成员变量的值,不会影响到另一个对象成员变量的值。

二、浅拷贝

1、clone

  • 实现 Cloneable

  • 重写 clone()方法,并声明为 public

  • 调用 super.clone()

copydemo

@Data
public class CopyDemo implements Cloneable{
    private int age;
    private User user;

    @Override
    public CopyDemo clone() {
        try {
            CopyDemo clone = (CopyDemo) super.clone();
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

user

@Data
public class User {
    private String name;
}

使用

@Service
public class CopyServiceDemo {

    public static void main(String[] args) {
        CopyDemo source=new CopyDemo();
        source.setAge(10);

        User user=new User();
        user.setName("user-旧名字");
        source.setUser(user);

        CopyDemo target = source.clone();
        System.out.println("改变之前");
        System.out.println("source:"+source.getAge());
        System.out.println("target:"+target.getAge());
        System.out.println("source-user:"+source.getUser().getName());
        System.out.println("target-user:"+target.getUser().getName());

        source.setAge(20);
        user.setName("user-新名字");

        System.out.println("改变之后");
        System.out.println("source:"+source.getAge());
        System.out.println("target:"+target.getAge());
        System.out.println("source-user:"+source.getUser().getName());
        System.out.println("target-user:"+target.getUser().getName());

    }
}

结果

改变之前
source:10
target:10
source-user:user-旧名字
target-user:user-旧名字
改变之后
source:20
target:10
source-user:user-新名字
target-user:user-新名字

从以上结果可以看出

  • 修改 source 的 age,并不会影响到拷贝之后的 target 的 age

  • 修改 source 的 user 的 name,会影响到拷贝之后的 targe 的 user 的 name,因为 target 的 user 跟 source 的 user 所指向的是同一个 user 实例。

2、Apache BeanUtils(不推荐)

Apache BeanUtils 属于比较古老的工具类,由于存在性能问题,阿里巴巴手册明确禁止使用该工具类

性能差的原因是:力求做得完美, 在代码中增加了非常多的校验、兼容、日志打印等代码,过度的包装导致性能下降严重。

图片

3、Spring BeanUtils

Spring BeanUtils 和上面所提到的 apche 的很像,但是在效率上比 apache 的更高

Spring BeanUtils 的 copyProperties() 方法,第一个是源对象,第二个是目标对象。和 Apache BeanUtils 正好相反,要注意避免踩坑。

import org.springframework.beans.BeanUtils;

CopyDemo target=new CopyDemo();
BeanUtils.copyProperties(source, target);

4、Spring BeanCopier

Spring 还为我们提供了一种基于 Cglib 的浅拷贝方式 BeanCopier,引入 spring-core 依赖包后即可使用,它被认为是取代 BeanUtils 的存在。

以下是自己封装的工具类:

import org.springframework.cglib.beans.BeanCopier;

public static <T> T copyByClass(Object src, Class<T> clazz) {
  BeanCopier copier = BeanCopier.create(src.getClass(), clazz, false);
  T to = newInstance(clazz);
  copier.copy(src, to, null);
  return to;
}

public static <T> T newInstance(Class<?> clazz) {
  try {
      return (T) clazz.newInstance();
    } catch (InstantiationException e) {
        throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}

public static void copyByObj(Object src, Object dist) {
  BeanCopier copier = BeanCopier
    .create(src.getClass(), dist.getClass(), false);
  copier.copy(src, dist, null);
 }

使用

CopyDemo target = copyByClass(source, CopyDemo.class);

三、深拷贝

1、构造方法-new

手动 new 新的对象,一个属性一个属性的 set 过去,属性多的话,这样非常麻烦

    public static void main(String[] args) {
        CopyDemo source= new CopyDemo();
        source.setAge(10);

        User user=new User();
        user.setName("user-旧名字");
        source.setUser(user);

        CopyDemo target=new CopyDemo();
        target.setAge(source.getAge());
        User targetUser=new User();
        targetUser.setName(source.getUser().getName());
        target.setUser(targetUser);

        System.out.println("改变之前");
        System.out.println("source:"+source.getAge());
        System.out.println("target:"+target.getAge());
        System.out.println("source-user:"+source.getUser().getName());
        System.out.println("target-user:"+target.getUser().getName());

        source.setAge(20);
        user.setName("user-新名字");

        System.out.println("改变之后");
        System.out.println("source:"+source.getAge());
        System.out.println("target:"+target.getAge());
        System.out.println("source-user:"+source.getUser().getName());
        System.out.println("target-user:"+target.getUser().getName());

    }

改变之前
source:10
target:10
source-user:user-旧名字
target-user:user-旧名字
改变之后
source:20
target:10
source-user:user-新名字
target-user:user-旧名字

Process finished with exit code 0

2、重载 clone()方法

  • 拷贝的对象中还包含其他对象的话,包含的对象也需要重写 clone 方法

  • super.clone()其实是浅拷贝,所以在重写 CopyDemo 类的 clone()方法时,user 对象需要调用 user.clone()重新赋值

CopyDemo

@Data
public class CopyDemo implements Cloneable{
    private int age;
    private User user;

    @Override
    public CopyDemo clone() {
        try {
            CopyDemo copyDemo = (CopyDemo) super.clone();
            copyDemo.setUser(this.user.clone());

            return copyDemo;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

User

@Data
public class User implements Cloneable{
    private String name;

    @Override
    public User clone() {
        try {
            User clone = (User) super.clone();
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

使用

CopyDemo target = source.clone();

3、Apache Commons Lang 序列化方式

Java 提供了序列化的能力,我们可以先将源对象进行序列化,再反序列化生成拷贝对象。但是,使用序列化的前提是拷贝的类(包括其成员变量)需要实现 Serializable 接口。Apache Commons Lang 包对 Java 序列化进行了封装:SerializationUtils,我们可以直接使用它。

@Data
public class CopyDemo implements Serializable {
    private static final long serialVersionUID = -9820808986091860L;
    private int age;
    private User user;
}

@Data
public class User implements Serializable {
    private static final long serialVersionUID = 1900781036567192607L;
    private String name;

}

使用

import org.apache.commons.lang3.SerializationUtils;

CopyDemo target = SerializationUtils.clone(source);

4、json 转化方式

利用 json 将对象转为 json,在将 json 转为对象,本质上是反射

//对象
String jsonString = JSON.toJSONString(source);
CopyDemo target = JSON.parseObject(jsonString, CopyDemo.class);

//集合
List<CopyDemo> sourceList=Lists.newArrayList();
String jsonString = JSON.toJSONString(sourceList);
List<CopyDemo> targetList = JSON.parseArray(json,CopyDemo.class);

5、Orika

orika 是深拷贝,但是遇到多层签到数组,clone 会有问题,谨慎使用

四、总结

如果对象中只有基本数据类型或者引用数据类型不会改动,则可以使用浅拷贝

如果存在引用数据类型且会改动,则可以使用深拷贝

具体使用拷贝中的哪个方法,需要具体情况具体分析,比如性能考虑、便捷考虑、依赖引入的考虑等等。

今天只是列出了一些常用的方法,还有其他的拷贝方法,可以自行搜索,多学习,多实践。


我是臻大虾,你的支持是对我不断创作的极大鼓励,咱们下期见。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335