`
jaesonchen
  • 浏览: 313165 次
  • 来自: ...
社区版块
存档分类
最新评论

探究Java中的克隆

 
阅读更多

本文将尝试介绍一些关于Java中的克隆和一些深入的问题,希望可以帮助大家更好地了解克隆。

Java中的赋值

在Java中,赋值是很常用的,一个简单的赋值如下

1
2
3
4
5
6
7
//原始类型
int a = 1;
int b = a;
//引用类型
String[] weekdays = new String[5];
String[] gongzuori = weekdays;//仅拷贝引用

在上述代码中。

  • 如果是原始数据类型,赋值传递的为真实的值
  • 如果是引用数据类型,赋值传递的为对象的引用,而不是对象。

了解了数据类型和引用类型的这个区别,便于我们了解clone。

Clone

在Java中,clone是将已有对象在内存中复制出另一个与之相同的对象的过程。java中的克隆为逐域复制。

在Java中想要支持clone方法,需要首先实现Cloneable接口

Cloneable其实是有点奇怪的,它不同与我们常用到的接口,它内部不包含任何方法,它仅仅是一个标记接口。

其源码如下

1
2
public interface Cloneable {
}

关于cloneable,需要注意的

  • 如果想要支持clone,就需要实现Cloneable 接口
  • 如果没有实现Cloneable接口的调用clone方法,会抛出CloneNotSupportedException异常。

然后是重写clone方法,并修改成public访问级别

1
2
3
4
5
6
7
8
9
10
static class CloneableImp implements Cloneable {
  public int count;
  public Child child;
  @Override
  public Object clone() throws CloneNotSupportedException {
      return super.clone();
  }
}

调用clone方法复制对象

1
2
3
4
5
6
7
8
9
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)。

关于浅拷贝,你该了解的

  • 使用默认的clone方法
  • 对于原始数据域进行值拷贝
  • 对于引用类型仅拷贝引用
  • 执行快,效率高
  • 不能做到数据的100%分离。
  • 如果一个对象只包含原始数据域或者不可变对象域,推荐使用浅拷贝。

关于无法做到数据分离,我们可以使用这段代码验证

1
2
3
4
5
6
7
8
9
10
11
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 得到的结果是

1
main imp1.child.name=Bob

原因是浅拷贝并没有做到数据的100%分离,imp1和imp2共享同一个Child对象,所以一个修改会影响到另一个。

深拷贝

深拷贝可以解决数据100%分离的问题。只需要对上面代码进行一些修改即可。

  1. Child实现Cloneable接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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方法。

1
2
3
4
5
6
7
8
9
10
11
12
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%隔离。

关于深拷贝的一些特点

  • 需要重写clone方法,不仅仅只调用父类的方法,还需调用属性的clone方法
  • 做到了原对象与克隆对象之间100%数据分离
  • 如果是对象存在引用类型的属性,建议使用深拷贝
  • 深拷贝比浅拷贝要更加耗时,效率更低

为什么使用克隆

很重要并且常见的常见就是:某个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

使用复制构造器也可以实现对象的拷贝。

  • 复制构造器也是构造器的一种
  • 只接受一个参数,参数类型为当前的类
  • 目的是生成一个与参数相同的新对象

复制构造器相比clone方法的优势是简单,易于实现。
一段使用了复制构造器的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
  }
}

注意,上面的代码实现为浅拷贝,如果想要实现深拷贝,参考如下代码

1
2
3
4
5
6
7
8
//copy constructor
public Car(Car car) {
  Wheel wheel = new Wheel();
  wheel.brand = car.wheel.brand;
  this.wheel = wheel;
  this.manufacturer = car.manufacturer;
}

为了更加便捷,我们还可以为上述类增加一个静态的方法

1
2
3
public static Car newInstance(Car car) {
  return new Car(car);
}

使用Serializable实现深拷贝

其实,使用序列化也可以实现对象的深拷贝。简略代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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接口

1
2
3
4
5
6
7
8
9
10
11
12
13
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 + "]";
  }
}

使用示例兼测试代码

1
2
3
4
5
6
7
8
9
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值,即使用序列化可以实现对象的深拷贝。

分享到:
评论

相关推荐

    很好的英语研究题材论文

    具体来说,作者使用了NiCad克隆检测器来测量开源软件系统中的Python脚本语言的克隆特性,并将其结果与之前对C、C#和Java等传统编程语言的研究结果进行了比较。 ### 三、实验设计 #### 实验工具:NiCad克隆检测器 ...

    javac源码和运行说明文件.zip

    总之,这个压缩包提供了一个难得的机会,让我们能够深入探究Java编程语言的底层实现。通过学习javac源码,不仅可以提升对Java语言的理解,还能增强对编译原理的认识,对于软件开发者来说是一笔宝贵的财富。在实际...

    javajdk1.8源码-yang-java-source:jdk1.8源码学习

    而源码分析是提升编程技能的关键步骤,它允许开发者探究Java库函数背后的实现逻辑,从而更好地理解和优化自己的代码。 【描述】中的"java jdk1.8源码 yang-java-source jdk1.8源码学习"强调了这次学习的重点在于JDK...

    commons-beanutils-1.8.2-src官方源文件,是你学习beanutils工具的必备资料

    在这个版本中,我们可以深入到源代码层面,探究BeanUtils是如何实现对Java Bean属性的便捷访问、复制和转换的。 1. **Java Bean**:Java Bean是一种符合特定规范的Java类,通常具有无参构造函数、getter和setter...

    用SVN下载编译Spring3.2.4源码导入eclipse

    在这个场景中,Spring是一个著名的Java企业级应用框架,版本3.2.4是一个特定的稳定发行版。SVN(Subversion)是一种版本控制系统,用于跟踪文件和目录的修改。Eclipse是广泛使用的Java IDE,支持各种项目的开发和...

    游戏,合成大西瓜.zip

    解压后的文件“daxigua-master”可能是一个Git仓库的克隆,通常包含项目的所有源代码文件、配置文件、资源文件等。在这样的项目结构中,我们可以找到以下组成部分: 1. **源代码**:主要位于src目录下,通常分为...

    pendex:Pendex 应用程序

    【 Pendex 应用程序详解】 彭德克斯(Pendex)是一款基于Java技术的应用程序,它提供了一种高效、灵活的方式来处理特定的...如果你对Java编程和企业级应用开发感兴趣,深入探究Pendex的源代码将是一个极好的学习机会。

    android源码

    右键点击项目,选择`Properties`,然后在弹出的窗口中选择`Java Build Path` -> `Source`标签页。点击`Add Folder...`,将SDK中的源码目录添加进来。 3. **配置adt插件**:确保你已经安装了Android Developer Tools...

    用于从 Google Authenticator 的 QR 码中提取命令行工具

    在压缩包子文件的文件名称列表中,"google-authenticator-extractor-master"表明这是一个Git仓库的克隆,可能是GitHub或其他版本控制系统的一个分支,通常包含了源代码、文档、示例和构建脚本等资源。用户可以通过...

    myTest:这是一个测试存储库

    深入探究,Java测试可能会涉及JUnit(一个流行的Java单元测试框架)、Mockito(用于模拟对象以隔离测试)或者Spring Boot Test(对于Spring框架的测试支持)。开发者可能还会使用持续集成工具如Jenkins或Travis CI,...

    turnt-octo-batman:这是我使用网络共享内容进行学习的测试库

    由于压缩包子文件的文件名称列表只给出了“turnt-octo-batman-master”,这通常表示这是从Git仓库中克隆或下载的主分支。在Git版本控制系统中,"master"分支代表了项目的主线开发。因此,这个压缩包可能包含了项目的...

    tomcat 源码 学习

    通常,源码位于Git仓库中,你可以使用Git命令行工具或者图形化客户端进行克隆。例如: ``` git clone https://github.com/apache/tomcat.git ``` **导入源码** 下载完成后,你可以使用IDE(如IntelliJ IDEA或...

    bookchallenge-源码.rar

    《书挑战》项目源码分析 “bookchallenge-源码.rar”是一个压缩包,其中包含了名为“bookchallenge-源码.zip”的源代码...如果你希望深入学习某个特定方面,建议解压源码并根据文件结构、注释和代码逻辑进一步探究。

    spbuik:圣彼得堡从2013年开始的5年内PEC组成数据

    该数据集覆盖了5年的连续时间跨度,意味着可以进行趋势分析,探究PEC在这些年间的动态变化。这将帮助我们理解教育政策的演变、教育资源的分配模式以及教育成果的长期影响。 【标签】"Java"暗示了这些数据可能与一个...

    proyectoProgramacionTema5

    深入探究这个项目,我们可以学习到如何组织Java项目结构,使用版本控制工具,理解项目构建流程(如Maven或Gradle),以及如何编写和执行单元测试。此外,代码实现的具体内容将提供关于特定Java编程技术的实际应用...

    HackTheAnvil:HackTheAnvil 的东西

    在深入探究这个项目之前,用户需要先克隆或下载这个“HackTheAnvil-master”压缩包,然后在本地环境中设置和运行。首先,他们需要安装Node.js环境,接着在项目根目录下运行`npm install`来安装所有必要的依赖。一旦...

    gradle-training

    通过学习和实践,你不仅可以理解Gradle的基本用法,还能深入探究其高级特性和自定义能力,这对于提升项目管理和开发效率至关重要。无论你是初学者还是经验丰富的开发者,都能从中受益,让构建工作变得更加轻松和高效...

    android_research

    在“android_research”中,可能涉及的是对这些层次的探究,如自定义系统服务、修改权限管理或优化性能。 **Android应用开发流程** 开发Android应用通常包括以下几个步骤:设计界面、编写业务逻辑、集成API、处理...

    JabRef-2.6-src.tar.gz_jabref

    JabRef是一款强大的开源文献管理软件,主要设计用于Linux/Unix操作系统环境...对于科研人员来说,掌握JabRef的使用可以大大提高文献管理的效率,而对于开发者来说,探究其源代码则是一次学习和贡献开源软件的良好机会。

Global site tag (gtag.js) - Google Analytics