技术标签: 序列化 浅克隆 Java基础 深克隆 浅拷贝 深拷贝
当需要把一个对象的值赋值给另一个对象时,使用new和赋值语句或者set注入都是可以的,但是,这会花费大量开销去做,效率低,并且还会产生冗余代码。克隆对象可以将对象直接复制,被复制的对象保留原对象的所有属性值
Java语言提供的Cloneable接口和Serializable接口都是空接口
,也称为标识接口
,标识接口中没有任何方法
,作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。
实现Cloneable接口( 该接口为标记接口,不含任何方法)
并重写Object类中的clone()方法
实现Serializable接口
,通过对象序列化
和反序列化
实现克隆,可以实现真正的深度克隆
。
浅克隆和深克隆的主要区别在于是否支持引用类型
的成员变量
的复制
浅克隆
:只复制基本类型的数据,引用类型的数据只复制了引用的地址,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。
也就是说在浅克隆中,如果原对象的成员变量是值类型,将复制一份给克隆对象;如果原对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原对象和克隆对象的引用类型成员变量指向相同的内存地址。
在Java中,通过重写clone()方法
实现浅克隆。
深克隆
:是在引用类型的类中也重写了clone(),是clone的嵌套,并且在clone方法中又对没有克隆的引用类型做差异化复制,克隆后的对象与原对象之间完全不会影响,但是属性值完全相同。使用clone实现的深克隆实际上是在浅克隆中嵌套了浅克隆
在深克隆中,无论原对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原对象的所有引用对象也复制一份给克隆对象。也就是说,除了对象本身被复制外,对象所包含的所有引用/非引用成员变量也将复制。
在Java中,如果需要实现深克隆,可以通过重写clone()方法或序列化(Serialization)
实现
序列化/反序列化
:如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。通过序列化
将对象写到IO流中,再从流里将其反序列化
读出来,可以实现深克隆
。 需要注意的是列化的对象必须实现Serializable接口
,否则无法实现序列化操作。
对象序列化后写入流中,不存在引用的概念,再从流中读取,生成新的对象,新对象和原对象之间也是完全互不影响的。
基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定
,可以检查出要克隆的对象是否支持序列化,这种方案明显优于重写clone方法克隆对象。让问题在编译的时候暴露出来,而不是在运行时暴露出来
被复制的类需要实现Clonenable接口, 该接口为标记接口(不含任何方法)
覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。
将得到的复制对象返回
需要注意:Object的clone()
方法是在java平台层实现的native
方法,具有开销小,速度快的特点。而且,原始的Object方法是被protected修饰的,在这里需要修改为public,如果不这么做,浅克隆时没有问题,深克隆就会遇到权限不够的问题。
java继承还有个原则,就是子类覆写父类方法,访问修饰符权限不能低于父类。
例:创建Student对象
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor//生成所有参数的有参构造方法
@ToString
public class Student implements Cloneable{
private int studentId;
private String studentName;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
创建Teacher对象
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* @Description TODO 教师类
*/
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor //生成所有参数的有参构造方法
@ToString//生成ToString()方法
public class Teacher implements Cloneable {
private Integer id;
private String name;
private Double money;
private Student student;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
/**
* 测试浅克隆
*
* @param args
* @throws CloneNotSupportedException
*/
public static void main(String[] args) throws CloneNotSupportedException {
Student xiaoMing = new Student(2, "小明同学");
Teacher zhangShan = new Teacher(365, "张三老师", 1000.25, xiaoMing);
//克隆对象
Teacher zhangShanClone = (Teacher) zhangShan.clone();
System.out.println("zhangShan=>"+zhangShan);
System.out.println("zhangShanClone=>"+zhangShanClone);
System.out.println("-----------------------");
//1.打印内存地址发现不一致,说明调用clone()会创建一个新的对象
//com.demo.clone.Teacher@4fca772d
//com.demo.clone.Teacher@9807454
//2.注释掉Role类的@ToString()观察成员变量student,发现student内存地址指定同一位置
//Teacher(id=365, name=张三老师, money=1000.25, student=com.demo.clone.Student@4fca772d)
//Teacher(id=365, name=张三老师, money=1000.25, student=com.demo.clone.Student@4fca772d)
//3.修改克隆对象的student属性值,然后放开Role的@ToString()注释观察成员变量student,发现克隆对象改变了student属性值,导致原对象属性值也改变了
Student xiaoMingClone = zhangShanClone.getStudent(); //获取克隆对象student属性,并修改属性值
xiaoMingClone.setStudentName("克隆小明同学");
System.out.println("zhangShan=>"+zhangShan);
System.out.println("zhangShanClone=>"+zhangShanClone);
//Teacher(id=365, name=张三老师, money=1000.25, student=com.demo.clone.Student@4fca772d)
//Teacher(id=365, name=张三老师, money=1000.25, student=com.demo.clone.Student@4fca772d)
}
}
不注释Student类ToString()执行结果:
注释Student类ToString()执行结果:
可以看出浅克隆只复制基本类型的数据
,引用类型的数据只复制了引用的地址
,引用的对象并没有复制,在新的对象中修改引用类型的数据会影响原对象中的引用。
通过上面的Teacher
例子可以看到,浅拷贝会带来数据安全方面的隐患,例如我只是想修改了克隆对象 zhangShanClone
的 成员变量student
,但是原对象
Teacher实例zhangShan
的 成员变量studen
也被修改了,因为它们都是指向的同一个地址。
前面两步跟浅克隆差不多后面一步不同
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* @Description TODO 用户
* @Author JianPeng OuYang
* @Date 2020/1/7 14:26
* @Version v1.0
*/
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor //生成所有参数的有参构造方法
@ToString//生成ToString()方法
public class User implements Cloneable {
private String userName;
private String password;
private Role role;
@Override
public Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
user.setRole((Role) user.getRole().clone());//调用Role的clone
return user;
}
/**
* 深克隆
* 是在引用类型的类中也实现了clone,是clone的嵌套,并且在clone方法中又对没有clone方法的引用类型又做差异化复制,克隆后的对象与原对象之间完全不会影响,但是内容完全相同。
*
* @param args
* @throws CloneNotSupportedException
*/
public static void main(String[] args) throws CloneNotSupportedException {
Role role = new Role(1, "超级管理员");
User zhangShan = new User("admin", "123456", role);
//克隆用户张三
User zhangShanClone = (User) zhangShan.clone();
zhangShanClone.setUserName("test");
//获取李四的角色类型,修改角色类型为普通用户
Role zhangShanRole = zhangShanClone.getRole();
zhangShanRole.setRoleId(3);
zhangShanRole.setRoleName("普通用户");
System.out.println(zhangShan);
System.out.println(zhangShanClone);
System.out.println("----------------------");
//User对象克隆内存地址不同,说明clone后是会创建一个新的对象
//com.demo.clone.User@4fca772d
//com.demo.clone.User@9807454
}
}
不注释Role类ToString()执行结果:
注释Role类ToString()执行结果:
可以看出深拷贝在拷贝时开辟了一个新的内存空间保存引用类型成员变量,从而实现真正内容上的拷贝。
对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝
。深拷贝相比于浅拷贝速度较慢并且花销较大。
如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦,需要每个对象都实现 Cloneable 并重写 clone() 方法,这时我们可以用序列化
来解决多层克隆
问题 ,将对象序列化,转换为二进制码,然后再反序列化成对象,最后赋值。从而实现克隆
自定义方法或者重写clone()
方法,使用IO流
实现序列化逻辑
对象输出流ObjectOutputStream
将对象写入到字节数组输出流ByteArrayOutputStream
中字节数组输入流ByteArrayInputStream
中,然后使用对象输入流ObjectInputStream
读取写入对象Serializable接口
,并定义serialVersionUID
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.*;
/**
* @Description TODO 父亲类
* @Author JianPeng OuYang
* @Date 2020/1/8 10:42
* @Version v1.0
*/
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor//生成所有参数的有参构造方法
@ToString//生成ToString()方法
public class Father implements Serializable {
private static final long serialVersionUID = -3442179150194293202L;
private Integer fatherId;
private String fatherName;
private Son son;
public static void main(String[] args) {
Son son = new Son(11, "儿子");
Father father = new Father(1, "爸爸", son);
//克隆Father对象
Father fatherClone = father.clone();
//修改克隆对象成员变量的sonId
Son sonClone = fatherClone.getSon();
sonClone.setSonId(111111);
System.out.println("father=>" + father);
System.out.println("fatherClone=>" + fatherClone);
}
@Override
public Father clone() {
//用于序列化对象IO流
//字节数组输出流
ByteArrayOutputStream bos=null;
//对象输出流
ObjectOutputStream oos = null;
//用于饭序列化对象IO流
//字节数组输入流
ByteArrayInputStream bis = null;
//对象输入流
ObjectInputStream ois = null;
try {
//-------序列化对象
bos = new ByteArrayOutputStream();
//对象输出流=》将对象通过对象输出流写入字节数组输出流
oos = new ObjectOutputStream(bos);
//将当前对象写入对象流中
oos.writeObject(this);
//--------反序列化对象
//读取字节输出流返回的字节数组
bis = new ByteArrayInputStream(bos.toByteArray());
//解析字节输入流写入的字节
ois = new ObjectInputStream(bis);
//-------读取对象
Father father = (Father) ois.readObject();
return father;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
close(bis,bos);
close(ois,oos);
}
return null;
}
public void close(InputStream inputStream, OutputStream outputStream) {
try {
if (outputStream != null) {
outputStream.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.*;
/**
* @Description TODO 儿子类
*/
@Getter //编译时生成所有get方法
@Setter //编译时生成所有set方法
@AllArgsConstructor//生成所有参数的有参构造方法
@ToString//生成ToString()方法
public class Son implements Serializable {
private static final long serialVersionUID = -86552601995157672L;
private Integer sonId;
private String sonName;
}
Java提供了序列化的能力,我们可以先将原对象进行序列化,再反序列化生成克隆对象。但是,使用序列化的前提是克隆的类(包括其成员变量)需要实现Serializable
接口。Apache Commons Lang包对Java序列化进行了封装,我们可以直接使用它。
导入依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.1</version>
</dependency>
/**
* 用户
*/
public class User implements Serializable {
private String name;
private Address address;
// constructors, getters and setters 省略
}
/**
* 地址
*/
public class Address implements Serializable {
private String city;
private String country;
// constructors, getters and setters 省略
}
@Test
public void serializableCopy() {
Address address = new Address("杭州", "中国");
User user = new User("大山", address);
// 使用Apache Commons Lang序列化进行深拷贝
User copyUser = (User) SerializationUtils.clone(user);
// 修改源对象的值
user.getAddress().setCity("深圳");
// 检查两个对象的值不同
assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());
}
优点:
- 可用性强,新增成员变量不需要修改clone()方法
缺点:
- 底层实现复杂,需要引入Apache Commons Lang第三方JAR包
- 拷贝类(包括其成员变量)需要实现Serializable接口
- 序列化与反序列化存在一定的系统开销
Gson
可以将对象序列化成JSON,也可以将JSON反序列化成对象,所以我们可以用它进行深拷贝。导入依赖
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3</version>
</dependency>
测试用例
//沿用 Apache Commons Lang的API序列化 的 User类和Address类
@Test
public void gsonCopy() {
Address address = new Address("杭州", "中国");
User user = new User("大山", address);
// 使用Gson序列化进行深拷贝
Gson gson = new Gson();
User copyUser = gson.fromJson(gson.toJson(user), User.class);
// 修改源对象的值
user.getAddress().setCity("深圳");
// 检查两个对象的值不同
assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());
}
优点:
- 可用性强,不需要实现任何接口或方法,也不需要修改clone()方法
缺点:
- 底层实现复杂,要导入Gson第三方Jar包
2.序列化与反序列化存在一定的系统开现额外接口和方法销
Jackson与Gson相似,也是一种JSON 和Java对象相互转换的 Java序列化/反序列化库,明显不同的地方是拷贝的类(包括其成员变量)需要有默认的无参构造函数
导入依赖
<!-- Jackson依赖库 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<!--2.8.8-->
<version>${dependency.version.jackson}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${dependency.version.jackson}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${dependency.version.jackson}</version>
</dependency>
<!--jackson-core——核心包(必须),提供基于“流模式”解析的API。-->
<!--jackson-databind——数据绑定包(可选),提供基于“对象绑定”和“树模型”相关API。-->
<!--jackson-annotations——注解包(可选),提供注解功能。-->
/**
* 地址
*/
public class Address {
private String city;
private String country;
public Address() {
}
//getters and setters省略
}
/**
* 用户
*/
public class User {
private String name;
private Address address;
public User() {
}
//getters and setters省略
}
测试用例
@Test
public void jacksonCopy() throws IOException {
Address address = new Address("杭州", "中国");
User user = new User("大山", address);
// 使用Jackson序列化进行深拷贝
ObjectMapper objectMapper = new ObjectMapper();
User copyUser = objectMapper.readValue(objectMapper.writeValueAsString(user), User.class);
// 修改源对象的值
user.getAddress().setCity("深圳");
// 检查两个对象的值不同
assertNotSame(user.getAddress().getCity(), copyUser.getAddress().getCity());
}
优点:
- 可用性强,不需要实现任何接口或方法,也不需要修改clone()方法
缺点:
- 底层实现复杂,要导入JackJson第三方Jar包
- 序列化与反序列化存在一定的系统开销
- 拷贝类(包括其成员变量)需要有默认无参构造方法
fastjson 是阿里巴巴的开源JSON解析库,JSON 和Java对象相互转换的 Java序列化/反序列化库
导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
public class UserGroup {
private String name;
List<User> userList = new ArrayList<>();
//getters and setters省略
}
public class User {
private String name;
private String age;
public User(String name, String age) {
super();
this.name = name;
this.age = age;
}
//getters and setters省略
}
测试案例
@Test
public void testFastJson(String[] args) {
// 构建用户geust
User guestUser = new User("guest","18");
// 构建用户root
User rootUser = new User("root","28");
// 构建用户组对象
UserGroup group = new UserGroup();
group.setName("admin");
group.getUserList().add(guestUser);
group.getUserList().add(rootUser);
// 用户组对象转JSON串
String jsonString = JSON.toJSONString(group);
System.out.println("jsonString:" + jsonString);
// JSON串转用户组对象
UserGroup group2 = JSON.parseObject(jsonString, UserGroup.class);
System.out.println("group2:" + group2);
// 构建用户对象数组
User[] users = new User[2];
users[0] = guestUser;
users[1] = rootUser;
// 用户对象数组转JSON串
String jsonString2 = JSON.toJSONString(users);
System.out.println("jsonString2:" + jsonString2);
// JSON串转用户对象列表
List<User> users2 = JSON.parseArray(jsonString2, User.class);
System.out.println("users2:" + users2);
}
浅克隆:只是增加了一个指针指向已存在的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深克隆 增加了一个指针并且申请了一个新的内存存放复制的对象,使这个增加的指针指向这个新的内存内存地址,
具体用那种克隆方式因实际情况而定,每种方法都有他的优缺点,适用情况也不同.
基本数据类型
就使用浅克隆即可,单个引用对象
(即类中只要一层引用关系)如上面的数组之类的,那么简单的深克隆即可,多层克隆
则使用序列化方式, 但是使用序列化的话相比较前面两种是比较消耗内存的。每次引入本地图片,css,js时路径经常因为引入错误报错,也是很醉,整理下以 / 为起始,表示从根目录开始解析; 以 ./ 为起始,表示从当前目录开始解析; 以 ../ 为起始,表示从上级目录开始解析; 如图:在a1.html里分别从不同文件下引入图片的路径写法..._从根目录下引用文件
Ubuntu虚拟机配置静态ip系统:、Ubuntu18.10配置静态ip的设置需要修改的文件第一步:vi /etc/network/interfaces内容 interfaces(5) file used by ifup(8) and ifdown(8) auto lo iface lo inet loopback auto ens33 ifac..._如何设置静态ip,以及 ssh 工具远程登录文字叙述
套接字(Socket)起初来源于UNIX,是加利福尼亚大学Berkeley分校开发的UNIX操作系统下的网络通信接口。随着UNIX操作系统的广泛使用,Socket亦当之无愧的成为了最流行的网络通信程序接口之一。90年代初期,由SunMicrosystems,JSB.CO,FTPSoftware,Microdyne和Microsoft等几家公司联合制定了一套Windows下套接字编程的规范,称为Wi_套接字名称的由来
首先long double是C99引入的,但是如何printf格式化一个long double的数据的呢?scanf一个double数据,是%lf,printf一个float或者double都是%f。但是输出一个long double是什么格式呢?这个时候,我们需要c标准:7 The length modifiers and their meanings are: 第七节,长度修饰符及其..._c语言输出long double
叶子猪小编为您带来千羽解读之关于断恨刀新改,一起来看看吧!倩女幽魂新职业断恨刀客3月31日维护有所改动,下面为大家带来千羽大神细致解读,希望对转职断恨刀或有意尝试断恨刀的小伙伴有所帮助!-- 系统 --1.新职业分支断恨刀客调整:1.1 修改了霜刃挥技能使用后自身霜刃挥状态中反伤比例显示不对的问题。调整了霜刃挥反伤伤害的计算,伤害提升。同时霜刃挥状态修改为只能对玩家目标使用,对怪物无效。1.2 ...
intellij idea 常用快捷键快速开发 Ctrl + alt +B 查找接口的实现类 Ctrl+U 转到父类 Ctrl+I 实现方法 Ctrl+F12 可以显示当前文件的结构 查看该类有哪些方法 Ctrl+N 可以快速打开类 Ctrl+S...
这是专门针对ie的hack写法“\9″ 在IE6/IE7/IE8/IE9/IE10下生效“\0” 在 IE8/IE9/IE10下生效“\9\0” 在IE9/IE10下生效_ie10生效的css hack
1.背景RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue)的开源实现。2.应用场景2.1异步处理场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种1.串行的方式;2.并行的方式 (1)串行方式:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是_rabbitmq 业务场景
<!DOCTYPE html><html> <head> <meta charset="utf-8"> <title></title> </head> <body> </body> <script> // 1.准备数据 var cityInfo = [..._javascript二级联动
前言:什么是漫游?漫游行为:简单来说,就是设备从一个AP,连接到另一个AP。IP地址不需要重新申请。整个过程需要尽可能快的进行,否则对于用户而言,就会发现网络出现卡顿。而为了安全,网络的认证过程已经变得十分耗时(例如802.1X认证)。所以为了避免漫游时出现重新认证,开发出了OKC,以及802.11R协议进行补充。避免漫游时进行完整的认证过程。 使用OKC,802.11R等..._802.11r
StringUtil.javapackage com.lh.bean;public class StringUtil { // 需要计算长度的字符串 private String str; // 字符串的实际长度 private int strLength; public String getStr() { return str; } public void setSt_字符串的实际长度
文章来源:王琦的个人博客-数据同步之rsync | 第3篇:sersync+rsync ,互联网打杂,喜欢多语言编程,记录一些知识碎片,分享一些心得。前一篇写了inotify+rsync进行实时同步,但是还有些不足。只能记录下被监听的目录发生了变化,没有记录具体到某个文件和目录。rsync在同步时候,每次都是对整个目录同步,文件多时比较耗时。sersync是基于inotify开发的。可以具体到某一个文件或者目录的名字,只同步发生变化的文件或目录,因此效率会比前者高很多。sersync下载._sersync + rsync 爬虫