浅拷贝

首先创建两个类,方便理解浅拷贝

@Data
class Student implements Cloneable{
//年龄和名字是基本属性
private int age;
private String name;
//书包是引用属性
private Bag bag; public Student(int age, String name, Bag bag) {
this.age = age;
this.name = name;
this.bag = bag;
} @Override
public String toString() {
return "age=" + age + ", name='" + name + ", bag=" + bag;
} @Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
class Bag {
private String color;
private int price; public Bag(String color, int price) {
this.color = color;
this.price = price;
} @Override
public String toString() {
return "color='" + color + ", price=" + price;
}
}

Cloneable 接口只是一个标记接口(没属性和方法):

public interface Cloneable {
}

标记接口的作用其实很简单,用来表示某个功能在执行的时候是合法的。

如果不实现Cloneable接口直接重写并调用clone()方法,会抛出 CloneNotSupportedException 异常。

测试类

class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
Student student1 = new Student(18, "张三", new Bag("红",100));
Student student2 = (Student) student1.clone(); System.out.println("浅拷贝后:");
System.out.println("student1:" + student1);
System.out.println("student2:" + student2); //修改非引用类型属性name
student2.setName("李四"); //修改引用类型属性bag
Bag bag = student2.getBag();
bag.setColor("蓝");
bag.setPrice(200); System.out.println("修改了 student2 的 name 和 bag 后:");
System.out.println("student1:" + student1);
System.out.println("student2:" + student2); }
} //打印结果
浅拷贝后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='张三, bag=color='红, price=100
修改了 student2 的 name 和 bag 后:
student1:age=18, name='张三, bag=color='蓝, price=200
student2:age=18, name='李四, bag=color='蓝, price=200

可以看得出,浅拷贝后:

修改了student2的非引用类型属性name,student1的name并不会跟着改变

但修改了student2的引用类型属性bag,student1的bag跟着改变了

说明浅拷贝克隆的对象中,引用类型的字段指向的是同一个,当改变任何一个对象,另外一个对象也会随之改变。

深拷贝

深拷贝和浅拷贝不同的,深拷贝中的引用类型字段也会克隆一份,当改变任何一个对象,另外一个对象不会随之改变。

例子

@Data
class Bag implements Cloneable {
private String color;
private int price; public Bag(String color, int price) {
this.color = color;
this.price = price;
} @Override
public String toString() {
return "color='" + color + ", price=" + price;
} @Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

注意,此时的 Bag 类和浅拷贝时不同,重写了 clone() 方法,并实现了 Cloneable 接口。为的就是深拷贝的时候也能够克隆该字段。

@Data
class Student implements Cloneable{
//年龄和名字是基本属性
private int age;
private String name;
//书包是引用属性
private Bag bag; public Student(int age, String name, Bag bag) {
this.age = age;
this.name = name;
this.bag = bag;
} @Override
public String toString() {
return "age=" + age + ", name='" + name + ", bag=" + bag;
} @Override
protected Object clone() throws CloneNotSupportedException {
Student s = (Student) super.clone();
s.setBag((Bag) s.getBag().clone());
return s;
} }

注意,此时 Student 类也与之前的不同,clone() 方法当中,不再只调用 Object 的 clone() 方法对 Student 进行克隆了,还对 Bag 也进行了克隆。

来看测试类

class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
Student student1 = new Student(18, "张三", new Bag("红",100));
Student student2 = (Student) student1.clone(); System.out.println("深拷贝后:");
System.out.println("student1:" + student1);
System.out.println("student2:" + student2); //修改非引用类型属性name
student2.setName("李四"); //修改引用类型属性bag
Bag bag = student2.getBag();
bag.setColor("蓝");
bag.setPrice(200); System.out.println("修改了 student2 的 name 和 bag 后:");
System.out.println("student1:" + student1);
System.out.println("student2:" + student2);
}
} //这个测试类和之前的浅拷贝的测试类一样,但运行结果是不同的。
深拷贝后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='张三, bag=color='红, price=100
修改了 student2 的 name 和 bag 后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='李四, bag=color='蓝, price=200

不只是 student1 和 student2 是不同的对象,它们中的 bag 也是不同的对象。所以,改变了 student2 中的 bag 并不会影响到 student1。

不过,通过 clone() 方法实现的深拷贝比较笨重,因为要将所有的引用类型都重写 clone() 方法。

更好的方法是利用序列化

序列化

序列化是将对象写入流中,而反序列化是将对象从流中读取出来。写入流中的对象就是对原始对象的拷贝。需要注意的是,每个要序列化的类都要实现 Serializable 接口,该接口和 Cloneable 接口类似,都是标记型接口。

来看例子

@Data
class Bag implements Serializable {
private String color;
private int price; public Bag(String color, int price) {
this.color = color;
this.price = price;
} @Override
public String toString() {
return "color='" + color + ", price=" + price;
}
}

Bag 需要实现 Serializable 接口

@Data
class Student implements Serializable {
//年龄和名字是基本属性
private int age;
private String name;
//书包是引用属性
private Bag bag; public Student(int age, String name, Bag bag) {
this.age = age;
this.name = name;
this.bag = bag;
} @Override
public String toString() {
return "age=" + age + ", name='" + name + ", bag=" + bag;
} //使用序列化拷贝
public Object serializeClone() throws IOException, ClassNotFoundException {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); // 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject();
} }

Student 类也需要实现 Serializable 接口,并且在该类中,增加了一个 serializeClone() 的方法,利用 OutputStream 进行序列化,InputStream 进行反序列化,这样就实现了深拷贝。

来看示例

class TestClone {
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
Student student1 = new Student(18, "张三", new Bag("红",100));
Student student2 = (Student) student1.serializeClone(); System.out.println("浅拷贝后:");
System.out.println("student1:" + student1);
System.out.println("student2:" + student2); //修改非引用类型属性name
student2.setName("李四"); //修改引用类型属性bag
Bag bag = student2.getBag();
bag.setColor("蓝");
bag.setPrice(200); System.out.println("修改了 student2 的 name 和 bag 后:");
System.out.println("student1:" + student1);
System.out.println("student2:" + student2); }
} //与之前测试类不同的是,调用了 serializeClone() 方法。
浅拷贝后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='张三, bag=color='红, price=100
修改了 student2 的 name 和 bag 后:
student1:age=18, name='张三, bag=color='红, price=100
student2:age=18, name='李四, bag=color='蓝, price=200

测试结果和之前用 clone() 方法实现的深拷贝一样。

clone() 方法同时是一个本地(native)方法,它的具体实现会交给 HotSpot 虚拟机,那就意味着虚拟机在运行该方法的时候,会将其替换为更高效的 C/C++ 代码,进而调用操作系统去完成对象的克隆工作。

需要注意,由于是序列化涉及到输入流和输出流的读写,在性能上要比 HotSpot 虚拟机实现的 clone() 方法差很多。

浅拷贝、深拷贝与序列化【初级Java必需理解的概念】的更多相关文章

  1. Java 轻松理解深拷贝与浅拷贝

    目录 前言 直接赋值 拷贝 浅拷贝 举例 原理 深拷贝 实现: Serializable 实现深拷贝 总结 前言 本文代码中有用到一些注解,主要是Lombok与junit用于简化代码. 主要是看到一堆 ...

  2. Java原型模式之浅拷贝-深拷贝

    一.是什么? 浅拷贝:对值类型的成员变量进行值的复制,对引用类型的成员变量仅仅复制引用,不复制引用的对象 深拷贝:对值类型的成员变量进行值的复制,对引用类型的成员变量也进行引用对象的复制 内部机制: ...

  3. Java深拷贝与序列化

    对基本类型的变量进行拷贝非常简单,直接赋值给另外一个对象即可: int b = 50; int a = b; // 基本类型赋值 对于引用类型的变量(例如 String),情况稍微复杂一些,因为直接等 ...

  4. 【Java面试】简单说一下你对序列化和反序列化的理解

    Hi,大家好,我是Mic 一个工作4年的粉丝,投了很多简历 好不容易接到一个互联网公司的面试邀约. 在面试第一轮就被干掉了,原因是对主流互联网技术理解太浅了. 其中就有一个这样的问题:"简单 ...

  5. Effective Java通俗理解(持续更新)

    这篇博客是Java经典书籍<Effective Java(第二版)>的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约 ...

  6. Effective Java通俗理解(上)

    这篇博客是Java经典书籍<Effective Java(第二版)>的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约 ...

  7. Effective Java通俗理解(下)

    Effective Java通俗理解(上) 第31条:用实例域代替序数 枚举类型有一个ordinal方法,它范围该常量的序数从0开始,不建议使用这个方法,因为这不能很好地对枚举进行维护,正确应该是利用 ...

  8. Java虚拟机理解-内存管理

    运行时数据区域 jdk 1.8之前与之后的内存模型有差异,方法区有变化(https://cloud.tencent.com/developer/article/1470519). java的内存数据区 ...

  9. [java] 深入理解内部类: inner-classes

    [java] 深入理解内部类: inner-classes // */ // ]]>   [java] 深入理解内部类: inner-classes Table of Contents 1 简介 ...

  10. Java初始化理解与总结 转载

    Java的初始化可以分为两个部分: (a)类的初始化 (b)对象的创建 一.类的初始化 1.1 概念介绍: 一个类(class)要被使用必须经过装载,连接,初始化这样的过程. 在装载阶段,类装载器会把 ...

随机推荐

  1. 表格存储 SQL 查询多元索引

    ​简介: 多元索引是表格存储产品中一个重要的功能,多元索引使用倒排索引技术为表格存储提供了非主键列上的快速检索功能,另外也提供了统计聚合功能.表格存储近期开放了SQL查询功能,SQL引擎默认从原始表格 ...

  2. dotnet 5 从 IL 层面分析协变返回类型新特性

    在 C# 9.0 里面添加的一个新特性是支持协变返回类型,也就说子类重写了基类的抽象或虚拟方法,可以在返回值里面返回协变的类型,也就是返回值的类型可以是继承原本子类返回值类型的子类.本文将来从 IL ...

  3. GitHub Action 新上线 WPF .NET Core 自动构建模板

    在很土豪的微软免费给大家提供 GitHub 的构建服务器受到了小伙伴们的一堆好评之后,微软最近推出了 WPF 的 .NET Core 版本的模板,可以快速上手 WPF 项目的自动构建,支持自动进行单元 ...

  4. CMDB开发(二)

    一.项目架构:目录规范 # 遵循软件开发架构目录规范 bin 启动文件 src 源文件(核心代码) config 配置文件 lib 公共方法 tests 测试文件 二.采集规范 # bin目录下新建s ...

  5. 如何将本地项目第一次同步到gitee远程

    一,Gitee账号的注册/登录 在gitee登录入口输入相关信息进行注册登录https://gitee.com/signup#lang=zh-CN 二,本地安装git客户端并配置用户信息 1.Git ...

  6. SpringBoot实现WebSocket发送接收消息 + Vue实现SocketJs接收发送消息

    SpringBoot实现WebSocket发送接收消息 + Vue实现SocketJs接收发送消息 参考: 1.https://www.mchweb.net/index.php/dev/887.htm ...

  7. 让.NET 8 支持 Windows Vista RTM

    众所周知,从 Windows 的每次更新又会新增大量 API,这使得兼容不同版本的 Windows 需要花费很大精力.导致现在大量开源项目已经不再兼容一些早期的 Windows 版本,比如 .NET ...

  8. 带你十天轻松搞定 Go 微服务系列全集+勘误

    官网手册: https://go-zero.dev/cn/ 文档说明: https://zhuanlan.zhihu.com/p/461604538 本地开发运行环境: https://github. ...

  9. 命令行创建vue项目

    vue -h vue create learn-vue ❯ Manually select features 选择下面的组件(空格为选中/取消)回车确认 使用 history mode 使用node ...

  10. ls的输出格式

    在Linux中,如果在一个目录下面执行ls -al命令,输出格式如下: ls -al总共输出7列,下面对每一列进行说明. 第一列表示这个文件的权限与类型,它总共有10位,每个位的作用如下图所示: 其中 ...