深拷贝与潜拷贝
一、概述
浅拷贝:
浅拷贝只是拷贝了源对象的地址,所以源对象的值发生变化时,拷贝对象的值也会发生变化。
深拷贝:
深拷贝则是拷贝了源对象的所有值,所以即使源对象的值发生变化时,拷贝对象的值也不会改变
二、使用方法
1. 潜拷贝
1.1 spring BeanUtils(Apache BeanUtils)

Source source = getSource(); Source target = new Source(); BeanUtils.copyProperties(source, target);
spring的BeanuUtils和Apache BeanUtils原理都类似,都是利用反射获取了对象的字段,逐个赋值,性能方面其实也是比较好了,虽然利用了反射,但是内部缓存了反射的结果,后面在复制的时候可以直接取缓存的结果。反射的性能损耗在获取Class信息那一块,在调用的开销和普通调用的类似,Jvm也会使用Jit进行优化。
2. 深拷贝
2.1 构造函数
我们可以通过在调用构造函数进行深拷贝,形参如果是基本类型和字符串则直接赋值,如果是对象则重新new一个。

package com.kmair.member.service.sym.impl; import com.kmair.member.common.exception.BusinessException; import com.kmair.member.common.jpa.predicate.logic.Or; import com.kmair.member.entity.po.sym.CabinGradePo; import com.kmair.member.entity.po.sym.CompanyRulePo; import com.kmair.member.common.jpa.predicate.Cnd; import com.kmair.member.common.jpa.predicate.logic.And; import com.kmair.member.entity.query.sym.CabinGradeQuery; import com.kmair.member.entity.query.sym.CompanyRuleQuery; import com.kmair.member.service.sym.ICompanyRuleService; import com.kmair.member.repository.sym.ICompanyRuleRepository; import lombok.val; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.util.StringUtils; /** * @Description: (企业会员规则维护 服务层实现) * @author doublening * @Date 2020年2月11日 下午06时30分45秒 */ @Service @Transactional(readOnly = true,rollbackFor = RuntimeException.class) public class CompanyRuleServiceImpl implements ICompanyRuleService{ @Autowired private ICompanyRuleRepository companyRuleRepository; @Override @Transactional(readOnly = false,rollbackFor = RuntimeException.class) public CompanyRulePo save(CompanyRulePo companyRulePo){ return companyRuleRepository.save(companyRulePo); } @Override public CompanyRulePo getById(Integer id){ return companyRuleRepository.getOne(id); } @Override public List<CompanyRulePo> list(CompanyRuleQuery query){ return companyRuleRepository.findAll(new ListQueryCnd(query),query.jpaSort()); } @Override public Page<CompanyRulePo> listWithPage(CompanyRuleQuery query){ return companyRuleRepository.findAll(new ListQueryCnd(query),query.jpaPageRequest()); } @Override @Transactional(readOnly = false,rollbackFor = RuntimeException.class) public void delete(Integer[] ids) { if(ObjectUtils.isEmpty(ids)){ return; } List<CompanyRulePo> pos = Arrays.stream(ids) .map(id -> companyRuleRepository.getOne(id)) .collect(Collectors.toList()); companyRuleRepository.deleteAll(pos); } @Override @Transactional(readOnly = false,rollbackFor = RuntimeException.class) public void updateStatus(Integer id, CompanyRulePo.RuleStatus status) { if (StringUtils.isEmpty(id)) { return; } CompanyRulePo rule = this.getById(id); rule.setStatus(status); // 验证数据 this.validateRule(rule); CompanyRulePo companyRulePo = new CompanyRulePo(); companyRulePo.setId(id); companyRulePo.setStatus(status); companyRuleRepository.updateInclude(companyRulePo, "status"); } /** * 启用数据状态时,若奖励类型为单价奖励,则需要判断以下规则: * 1、若舱位交叉,则航班起止时间不允许交叉 * 2、舱位不交叉则航班时间允许交叉 * @param rule */ private void validateRule(CompanyRulePo rule) { // 判断是否是单价奖励 if (!Objects.equals(rule.getAwardType().getCode(), CompanyRulePo.AwardType.UNIT_AWARD.getCode())) { return; } // 判断是否是启用 | 待启用 if (Objects.equals(rule.getStatus().getCode(), CompanyRulePo.RuleStatus.DISABLED.getCode())) { return; } String cabins = rule.getCabin(); if (cabins == null) { return; } String[] cabinArr = cabins.split(","); CompanyRuleQuery query = new CompanyRuleQuery(); query.setFlightDateCrossRange(new Date[]{rule.getFlightStartDate(), rule.getFlightEndDate()}); query.setAwardType(CompanyRulePo.AwardType.UNIT_AWARD); query.setStatus(CompanyRulePo.RuleStatus.ENABLED); // 编辑时需要排除自身 query.setExcludeId(rule.getId()); for (String cabin : cabinArr) { query.setCabin(cabin); List<CompanyRulePo> list = this.list(query); if (!CollectionUtils.isEmpty(list)) { throw new BusinessException("舱位和航班时间存在交叉!"); } } } /** * 集合查询条件 */ private static class ListQueryCnd implements Specification<CompanyRulePo>{ private final CompanyRuleQuery q; ListQueryCnd(CompanyRuleQuery q){ if(q == null){ q = new CompanyRuleQuery(); } this.q = q; } @Override public Predicate toPredicate(Root<CompanyRulePo> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { And<CompanyRulePo> and = Cnd.and(root, criteriaBuilder); // 排除id if (q.getExcludeId() != null) { and.notEquals("id", q.getExcludeId()); } // 舱位 if (StringUtils.hasText(q.getCabin())) { and.likeIgnoreCase("cabin", q.getCabin()); } // 创建人 if (StringUtils.hasText(q.getCreateUser())) { and.equals("createUser", q.getCreateUser()); } // 创建时间 if (q.getCreateDate() != null) { and.equals("createDate", q.getCreateDate()); } // 航班起始时间 if (q.getFlightStartDate() != null) { and.gte("flightStartDate", q.getFlightStartDate()); } // 航班结束时间 if (q.getFlightEndDate() != null) { and.lte("flightEndDate", q.getFlightEndDate()); } // 航班日期交叉范围 if (q.getFlightDateCrossRange() != null) { Or<CompanyRulePo> or = and.cnd().or(); or.between("flightStartDate", q.getFlightDateCrossRange()[0], q.getFlightDateCrossRange()[1]); or.between("flightEndDate", q.getFlightDateCrossRange()[0], q.getFlightDateCrossRange()[1]); and.add(or.endOr()); } // 奖励类型 if (q.getAwardType() != null) { and.equals("awardType", q.getAwardType()); } // 数据状态 if (q.getStatus() != null) { and.equals("status", q.getStatus()); } return and.endAnd(); } } }
2.2 重载clone()方法
Object父类有个clone()的拷贝方法,不过它是protected类型的,我们需要重写它并修改为public类型。除此之外,子类还需要实现Cloneable接口来告诉JVM这个类是可以拷贝的。

package copy; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import static org.junit.Assert.assertNotSame; /** * 深拷贝: * 方法二 重载clone()方法 * @author zls * @date 2020/2/13 */ public class CopyTest3 { public static void main(String[] args) throws CloneNotSupportedException { Address1 address = new Address1("杭州", "中国"); User1 user = new User1("黄梦莹", address); // 调用clone()方法进行深拷贝 User1 copyUser = user.clone(); // 修改源对象的值 user.getAddress1().setCity("苏州"); // 检查两个对象的值不同 assertNotSame(user.getAddress1().getCity(), copyUser.getAddress1().getCity()); } } /** * 用户 */ @Data @AllArgsConstructor @NoArgsConstructor class User1 implements Cloneable { private String name; private Address1 address1; // constructors, getters and setters /** * 需要注意的是,super.clone()其实是浅拷贝,所以在重写User1类的clone()方法时, * address对象需要调用address.clone()重新赋值。 */ @Override public User1 clone() throws CloneNotSupportedException { User1 user1 = (User1) super.clone(); user1.setAddress1(this.address1.clone()); return user1; } } /** * 地址 * 实现Cloneable接口,使其支持深拷贝。 */ @Data @AllArgsConstructor @NoArgsConstructor class Address1 implements Cloneable { private String city; private String country; @Override public Address1 clone() throws CloneNotSupportedException { return (Address1) super.clone(); } }
2.3 Apache Commons Lang序列化
Java提供了序列化的能力,我们可以先将源对象进行序列化,再反序列化生成拷贝对象。但是,使用序列化的前提是拷贝的类(包括其成员变量)需要实现Serializable接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。

package copy; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.lang3.SerializationUtils; import java.io.Serializable; import static org.junit.Assert.assertNotSame; /** * 深拷贝: * 方法三 Apache Commons Lang序列化 * @author zls * @date 2020/2/13 */ public class CopyTest4 { public static void main(String[] args) { Address2 address = new Address2("杭州", "中国"); User2 user = new User2("大山", address); // 使用Apache Commons Lang序列化进行深拷贝 User2 copyUser = (User2) SerializationUtils.clone(user); // 修改源对象的值 user.getAddress().setCity("苏州"); // 检查两个对象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); } } /** * 用户 * 实现Serializable接口,使其支持序列化。 */ @Data @AllArgsConstructor @NoArgsConstructor class User2 implements Serializable { private String name; private Address2 address; } /** * 地址 */ @Data @AllArgsConstructor @NoArgsConstructor class Address2 implements Serializable { private String city; private String country; }
2.4 Gson序列化
Gson可以将对象序列化成JSON,也可以将JSON反序列化成对象,所以我们可以用它进行深拷贝。

package copy; import com.google.gson.Gson; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import static org.junit.Assert.assertNotSame; /** * 深拷贝: * 方法四 Gson序列化 * @author zls * @date 2020/2/13 */ public class CopyTest5 { public static void main(String[] args) { Address3 address = new Address3("杭州", "中国"); User3 user = new User3("大山", address); // 使用Gson序列化进行深拷贝 Gson gson = new Gson(); User3 copyUser = gson.fromJson(gson.toJson(user), User3.class); // 修改源对象的值 user.getAddress().setCity("苏州"); // 检查两个对象的值不同 assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity()); } } /** * 用户 * 实现Serializable接口,使其支持序列化。 */ @Data @AllArgsConstructor @NoArgsConstructor class User3 implements Serializable { private String name; private Address3 address; } /** * 地址 */ @Data @AllArgsConstructor @NoArgsConstructor class Address3 implements Serializable { private String city; private String country; }
2.5 通过流的方式序列化
深拷贝:

package copy; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import javax.jnlp.PersistenceService; import java.io.*; import java.util.ArrayList; import java.util.List; /** * 首先简单介绍一下序列化是什么:把对象转换为字节序列的过程称为对象的序列化,反之将字节序列恢复为对象的过程称为对象的反序列化。 * JDK类库中的序列化API:java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。 * java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。 * * 只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口, * 实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。 * * 对象序列化包括如下步骤: * 1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流; * 2) 通过对象输出流的writeObject()方法写对象。 * 对象反序列化的步骤如下: * 1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流; * 2) 通过对象输入流的readObject()方法读取对象。 * * 参考:https://www.jianshu.com/p/5a31266c6adc * @author zls * @date 2020/2/7 */ public class CopyTest { public static void main(String[] args) throws IOException, ClassNotFoundException { List<Person> srcList = new ArrayList<>(); Person p = new Person(20, "李一桐"); Person p2 = new Person(21, "黄梦莹"); Person p3 = new Person(22, "刘亦菲"); srcList.add(p); srcList.add(p2); srcList.add(p3); List<Person> destList = deepCopy(srcList); printList(destList); srcList.get(0).setName("李沁"); printList(destList); // 可以看出在对srcList进行修改后,拷贝得到的destList没有发生改变 } public static <Person> void printList(List<Person> List) { System.out.println(" ---------begin----------- "); for (Person p : List) { System.out.println(p); } System.out.println(" ---------end------------- "); } /** * 利用序列化实现深拷贝,可以看到序列化和反序列化的过程与上文过程相同,不同的地方是,该程序里面用的并非文件输出(输入)流, * 而是字节数组(ByteArray)输出(输入流),文件输出(输入)流会将转化的字符序列存储在文件中,而字节数组输出(输入)流则 * 是将其保存在一个字节数组的临时变量中,仅占用内存空间,用后会自动清除。 * 这里面使用的是List的泛型类,目的就是能够使已经实现Serializable接口的Person类能够被函数所调用然后进行拷贝。 * <p> * 而实现深度拷贝的原理是什么呢?首先已经知道浅拷贝的原因是两个开辟的空间同时指向了同一个顺序表而导致对其中一个进行操作时, * 另一个也会受到影响。而当把对象序列化并存储后,再将其反序列化(反序列化返回的是一个对象),这时候反序列化得到的对象的存储 * 位置已经与原对象不同了,也就是在反序列化后产生了两个一毛一样的对象,但它们并不是同一个。这时候将用于拷贝的空间用新的对象 * 赋值即可实现深度拷贝。 * * @param src * @param <T> * @return * @throws IOException * @throws ClassNotFoundException */ public static <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException { ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(byteOut); out.writeObject(src); ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray()); ObjectInputStream in = new ObjectInputStream(byteIn); List<T> dest = (List<T>) in.readObject(); return dest; } } @Data @AllArgsConstructor @NoArgsConstructor class Person implements Serializable { private int age; private String name; }
三、使用示例
1、org.springframework.beans.BeanUtils
父类的属性拷贝
@Data class A { private String name; } @Data class B extends A {}
public static void main(String[] args) { A a = new A(); a.setName("刘亦菲"); B b = new B(); BeanUtils.copyProperties(a, b); System.out.println("b.getName() = " + b.getName()); //b.getName() = 刘亦菲 }
或者 效果也是一样的
@Data class B extends A { private String name; }
参考:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现