百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

面试官问:什么是浅拷贝和深拷贝?(浅拷贝与深拷贝区别)

wxin55 2024-11-17 02:44 8 浏览 0 评论

前言

平时我们从数据库查询出 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 会有问题,谨慎使用

四、总结

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

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

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

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




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

关注公众号:臻大虾 分享java后端技术干货,每天进步一点点

相关推荐

ES6中 Promise的使用场景?(es6promise用法例子)

一、介绍Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码doSomething(f...

JavaScript 对 Promise 并发的处理方法

Promise对象代表一个未来的值,它有三种状态:pending待定,这是Promise的初始状态,它可能成功,也可能失败,前途未卜fulfilled已完成,这是一种成功的状态,此时可以获取...

Promise的九大方法(promise的实例方法)

1、promise.resolv静态方法Promise.resolve(value)可以认为是newPromise方法的语法糖,比如Promise.resolve(42)可以认为是以下代码的语...

360前端一面~面试题解析(360前端开发面试题)

1.组件库按需加载怎么做的,具体打包配了什么-按需加载实现:借助打包工具(如Webpack的require.context或ES模块动态导入),在使用组件时才引入对应的代码。例如在V...

前端面试-Promise 的 finally 怎么实现的?如何在工作中使用?

Promise的finally方法是一个非常有用的工具,它无论Promise是成功(fulfilled)还是失败(rejected)都会执行,且不改变Promise的最终结果。它的实现原...

最简单手写Promise,30行代码理解Promise核心原理和发布订阅模式

看了全网手写Promise的,大部分对于新手还是比较难理解的,其中几个比较难的点:状态还未改变时通过发布订阅模式去收集事件实例化的时候通过调用构造函数里传出来的方法去修改类里面的状态,这个叫Re...

前端分享-Promise可以中途取消啦(promise可以取消吗)

传统Promise就像一台需要手动组装的设备,每次使用都要重新接线。而Promise.withResolvers的出现,相当于给开发者发了一个智能遥控器,可以随时随地控制异步操作。它解决了三大...

手写 Promise(手写输入法 中文)

前言都2020年了,Promise大家肯定都在用了,但是估计很多人对其原理还是一知半解,今天就让我们一起实现一个符合PromiseA+规范的Promise。附PromiseA+规范地址...

什么是 Promise.allSettled()!新手老手都要会?

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的pr...

前端面试-关于Promise解析与高频面试题示范

Promise是啥,直接上图:Promise就是处理异步函数的API,它可以包裹一个异步函数,在异步函数完成时抛出完成状态,让代码结束远古时无限回掉的窘境。配合async/await语法糖,可...

宇宙厂:为什么前端离不开 Promise.withResolvers() ?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发。1.为什么需要Promise.with...

Promise 新增了一个超实用的 API!

在JavaScript的世界里,Promise一直是处理异步操作的神器。而现在,随着ES2025的发布,Promise又迎来了一个超实用的新成员——Promise.try()!这个新方法简...

一次搞懂 Promise 异步处理(promise 异步顺序执行)

PromisePromise就像这个词的表面意识一样,表示一种承诺、许诺,会在后面给出一个结果,成功或者失败。现在已经成为了主流的异步编程的操作方式,写进了标准里面。状态Promise有且仅有...

Promise 核心机制详解(promise机制的实现原理)

一、Promise的核心状态机Promise本质上是一个状态机,其行为由内部状态严格管控。每个Promise实例在创建时处于Pending(等待)状态,此时异步操作尚未完成。当异步操作成功...

javascript——Promise(js实现promise)

1.PromiseES6开始支持,Promise对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。简单说就是处理异步请求的。之所以叫Promise,就是我承诺,如果成功则怎么处理,失败怎...

取消回复欢迎 发表评论: