深入理解Java中的克隆
作者:daisy 发布时间:2023-03-14 02:59:14
前言
Java克隆(Clone)是Java语言的特性之一,但在实际中应用比较少见。但有时候用克隆会更方便更有效率。
对于克隆(Clone),Java有一些限制:
1、被克隆的类必须自己实现Cloneable 接口,以指示 Object.clone()
方法可以合法地对该类实例进行按字段复制。Cloneable 接口实际上是个标识接口,没有任何接口方法。
2、实现Cloneable接口的类应该使用公共方法重写 Object.clone
(它是受保护的)。某个对象实现了此接口就克隆它是不可能的。即使 clone 方法是反射性调用的,也无法保证它将获得成功。
3、在Java.lang.Object
类中克隆方法是这么定义的:
protected Object clone()
throws CloneNotSupportedException
创建并返回此对象的一个副本。表明是一个受保护的方法,同一个包中可见。
按照惯例,返回的对象应该通过调用 super.clone
获得。
Java中的赋值
在Java中,赋值是很常用的,一个简单的赋值如下
//原始类型
int a = 1;
int b = a;
//引用类型
String[] weekdays = new String[5];
String[] gongzuori = weekdays;//仅拷贝引用
在上述代码中。
1、如果是原始数据类型,赋值传递的为真实的值
2、如果是引用数据类型,赋值传递的为对象的引用,而不是对象。
了解了数据类型和引用类型的这个区别,便于我们了解clone。
Clone
在Java中,clone是将已有对象在内存中复制出另一个与之相同的对象的过程。java中的克隆为逐域复制。
在Java中想要支持clone方法,需要首先实现Cloneable接口
Cloneable其实是有点奇怪的,它不同与我们常用到的接口,它内部不包含任何方法,它仅仅是一个标记接口。
其源码如下
public interface Cloneable {
}
关于cloneable,需要注意的
1、如果想要支持clone,就需要实现Cloneable 接口
2、如果没有实现Cloneable接口的调用clone方法,会抛出CloneNotSupportedException异常。
然后是重写clone方法,并修改成public访问级别
static class CloneableImp implements Cloneable {
public int count;
public Child child;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
调用clone方法复制对象
CloneableImp imp1 = new CloneableImp();
imp1.child = new Child("Andy");
try {
Object obj = imp1.clone();
CloneableImp imp2 = (CloneableImp)obj;
System.out.println("main imp2.child.name=" + imp2.child.name);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
浅拷贝
上面的代码实现的clone实际上是属于浅拷贝(Shallow Copy)。
关于浅拷贝,你该了解的
1、使用默认的clone方法
2、对于原始数据域进行值拷贝
3、对于引用类型仅拷贝引用
4、执行快,效率高
5、不能做到数据的100%分离。
6、如果一个对象只包含原始数据域或者不可变对象域,推荐使用浅拷贝。
关于无法做到数据分离,我们可以使用这段代码验证
CloneableImp imp1 = new CloneableImp();
imp1.child = new Child("Andy");
try {
Object obj = imp1.clone();
CloneableImp imp2 = (CloneableImp)obj;
imp2.child.name = "Bob";
System.out.println("main imp1.child.name=" + imp1.child.name);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
上述代码我们使用了imp1的clone方法克隆出imp2,然后修改 imp2.child.name
为 Bob,然后打印imp1.child.name
得到的结果是
main imp1.child.name=Bob
原因是浅拷贝并没有做到数据的100%分离,imp1和imp2共享同一个Child对象,所以一个修改会影响到另一个。
深拷贝
深拷贝可以解决数据100%分离的问题。只需要对上面代码进行一些修改即可。
1、Child实现Cloneable接口。
public class Child implements Cloneable{
public String name;
public Child(String name) {
this.name = name;
}
@Override
public String toString() {
return "Child [name=" + name + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
2.重写clone方法,调用数据域的clone方法。
static class CloneableImp implements Cloneable {
public int count;
public Child child;
@Override
public Object clone() throws CloneNotSupportedException {
CloneableImp obj = (CloneableImp)super.clone();
obj.child = (Child) child.clone();
return obj;
}
}
当我们再次修改imp2.child.name
就不会影响到imp1.child.name
的值了,因为imp1和imp2各自拥有自己的child对象,因为做到了数据的100%隔离。
关于深拷贝的一些特点
1、需要重写clone方法,不仅仅只调用父类的方法,还需调用属性的clone方法
2、做到了原对象与克隆对象之间100%数据分离
3、如果是对象存在引用类型的属性,建议使用深拷贝
4、深拷贝比浅拷贝要更加耗时,效率更低
为什么使用克隆
很重要并且常见的常见就是:某个API需要提供一个List集合,但是又不希望调用者的修改影响到自身的变化,因此需要克隆一份对象,以此达到数据隔离的目的。
应尽量避免clone
1.通常情况下,实现接口是为了表明类可以为它的客户做些什么,而Cloneable仅仅是一个标记接口,而且还改变了超类中的手保护的方法的行为,是接口的一种极端非典型的用法,不值得效仿。
2.Clone方法约定及其脆弱 clone方法的Javadoc描述有点暧昧模糊,如下为 Java SE8的约定
clone方法创建并返回该对象的一个拷贝。而拷贝的精确含义取决于该对象的类。一般的含义是,对于任何对象x,表达式
x.clone() != x 为 true x.clone().getClass() == x.getClass()
也返回true,但非必须 x.clone().equals(x)
也返回true,但也不是必须的
上面的第二个和第三个表达式很容易就返回false。因而唯一能保证永久为true的就是表达式一,即两个对象为独立的对象。
3.可变对象final域 在克隆方法中,如果我们需要对可变对象的final域也进行拷贝,由于final的限制,所以实际上是无法编译通过的。因此为了实现克隆,我们需要考虑舍去该可变对象域的final关键字。
4.线程安全 如果你决定用线程安全的类实现Cloneable接口,需要保证它的clone方法做好同步工作。默认的Object.clone
方法是没有做同步的。
总的来说,java中的clone方法实际上并不是完善的,建议尽量避免使用。如下是一些替代方案。
Copy constructors
使用复制构造器也可以实现对象的拷贝。
1、复制构造器也是构造器的一种
2、只接受一个参数,参数类型为当前的类
3、目的是生成一个与参数相同的新对象
4、复制构造器相比clone方法的优势是简单,易于实现。
一段使用了复制构造器的代码示例
public class Car {
Wheel wheel;
String manufacturer;
public Car(Wheel wheel, String manufacturer) {
this.wheel = wheel;
this.manufacturer = manufacturer;
}
//copy constructor
public Car(Car car) {
this(car.wheel, car.manufacturer);
}
public static class Wheel {
String brand;
}
}
注意,上面的代码实现为浅拷贝,如果想要实现深拷贝,参考如下代码
//copy constructor
public Car(Car car) {
Wheel wheel = new Wheel();
wheel.brand = car.wheel.brand;
this.wheel = wheel;
this.manufacturer = car.manufacturer;
}
为了更加便捷,我们还可以为上述类增加一个静态的方法
public static Car newInstance(Car car) {
return new Car(car);
}
使用Serializable实现深拷贝
其实,使用序列化也可以实现对象的深拷贝。简略代码如下
public class DeepCopyExample implements Serializable{
private static final long serialVersionUID = 6098694917984051357L;
public Child child;
public DeepCopyExample copy() {
DeepCopyExample copy = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
copy = (DeepCopyExample) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return copy;
}
}
其中,Child必须实现Serializable接口
public class Child implements Serializable{
private static final long serialVersionUID = 6832122780722711261L;
public String name = "";
public Child(String name) {
this.name = name;
}
@Override
public String toString() {
return "Child [name=" + name + "]";
}
}
使用示例兼测试代码
DeepCopyExample example = new DeepCopyExample();
example.child = new Child("Example");
DeepCopyExample copy = example.copy();
if (copy != null) {
copy.child.name = "Copied";
System.out.println("example.child=" + example.child + ";copy.child=" + copy.child);
}
//输出结果:example.child=Child [name=Example];copy.child=Child [name=Copied]
由输出结果来看,copy对象的child值修改不影响example对象的child值,即使用序列化可以实现对象的深拷贝。
总结
猜你喜欢
- C# using 三种使用方式介绍1.using指令。using + 命名空间名字,这样可以在程序中直接用命令空间中的类型,而不必指定类型的
- 本文实例讲述了C#实现判断当前操作用户管理角色的方法。分享给大家供大家参考。具体实现方法如下:/// <summary>///
- Solr我还是个菜鸟,写这一些文章只是记录一下最近一段时间学习Solr的心得。 Solr是什么? 最近我学Solr的时候,一直看到一句话,S
- 本文实例讲解的是如何画一个满满圆形水波纹loadingview,这类效果应用场景很多,比如内存占用百分比之类的,分享给大家供大家参考,具体内
- 本文实例讲述了Java实现的对称加密算法AES定义与用法。分享给大家供大家参考,具体如下:一 简介1、AES是目前使用最多的对称加密算法。2
- 1.指针指针就是地址(即一个数据)。2.指针变量2.1概念(1)指针变量是变量,是变量在内存中就会开辟空间,会有自己对应的的地址,有自己的变
- C#与Java相比较从整体上来看,c#和java及其相似,甚至超过了c#与c、c++的相似程度,下面是两种语言的比较。两者都能编译成跨平台,
- import java.util.concurrent.Semaphore;public class ThreeThread {
- 背景何为延迟队列?顾名思义,延迟队列就是进入该队列的消息会被延迟消费的队列。而一般的队列,消息一旦入队了之后就会被消费者马上消费。场景一:在
- 生成唯一值的方法很多,下面就不同环境下生成的唯一标识方法一一介绍,作为工作中的一次总结,有兴趣的可以自行测试:一、在 .NET 中生成1、直
- 罗马数字转整数罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
- 这篇文章主要介绍了Mybatis模糊查询及自动映射实现详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要
- LocalDateTime 是 Java 8 中日期时间 API 提供的一个类,在日期和时间的表示上提供了更加丰富和灵活的支持。LocalD
- 基于Java swing+MySQL实现学生信息管理系统:主要实现JDBC对学生信息进行增删改查,应付一般课设足矣,分享给大家。(由于篇幅原
- 在 C# 中,new 关键字可用作运算符、修饰符或约束。new 运算符 用于创建对象和调用构造函数。new 修饰符 用于向基类成员隐藏继承成
- 从一个Stream中过滤null值复习一个Stream 包含 null 数据的例子.Java8Examples.javapackage co
- java弱口令检测机制1. 设计要求应具备检测口令的长度和是否在指定字符集合内的能力。应具备检测口令字符逻辑相邻的能力,如aBc,abC等。
- 今天学习了Map集合的几种方法,尤其是遍历Map集合感觉尤为重要,所以发出来供大家学习和自己复习以用。众所周知Map集合里存储元素是以键值对
- 本文实例为大家分享了Android实现跟随手指画圆的具体代码,供大家参考,具体内容如下首先自己定义一个View子类:package com.
- 前言在Spring Boot中有一个注释@Async,可以帮助开发人员开发并发应用程序。但使用此功能非常棘手。在本博客中,我们将了解如何将此