哪个更好的选择:克隆或复制构造函数?

这就是我开始撰写本文的方式。 我已经读过很多次这样的声明: “当对象引用可变的最终字段时,克隆变得很困难。” 每次我在Google上搜索它时,都要了解它的确切含义,并且在此过程中也忘了它。 因此以为我会在此撰写博客,以便将其作为我的直接参考。

克隆对象(我可以从我的研究生课程的OOP课程中回想起)正在创建对象的类似副本,该副本基本上应符合以下规则:

  1. x.clone()!= x
  2. x.clone()。getClass()== x.getClass()
  3. x.clone()。equals(x)

请注意,在所有情况下都必须始终满足条件(1)。 尽管条件(2)和(3)并非绝对要求,但最好以这样的方式设计克隆方法,使其保持良好状态。 在继续讨论之前,这是Object类中clone方法的方法签名:

protected native Object clone() throws CloneNotSupportedException;

因此,正如您注意到protected修饰符一样,我们不可能直接在任何对象上调用clone()方法。 我们必须重写此方法作为公共方法,并在我们的类中为其提供实现才能访问它。 如果不需要特定的实现,我们可以返回super.clone()。 由于在Java 5之后可以进行协变返回,因此我们可以修改clone的返回值以返回类的对象。 因此,如果我们正在编写员工类,则这是clone()方法的方式:

@Override
public Employee clone() throws CloneNotSupportedException {return (Employee) super.clone();
}

但是请注意,Object类中的clone方法检查我们的类是否实现Cloneable接口。 如果未实现,则抛出CloneNotSupportedException。 否则,它将创建一个新副本。 但是请注意,克隆方法从不调用构造函数来创建对象的副本。 因此,如果您想通过增加构造函数内部的静态计数器来跟踪为类创建的实例数量,则此方法将不会起作用,因为永远不会调用构造函数。 相反,clone方法从对象内存中逐实例复制实例属性,然后将其返回给调用方。 因此,如果类必须提供一个用于克隆它的选项而不导致CloneNotSupportedException,则必须实现该类的标记接口Cloneable。 但是请注意,调用clone()的代码应处理此异常。 否则会导致编译器错误。 是的,这是一个痛点,对此受到批评。

现在让我们举一个例子: 案例(1)

public class Employee implements Cloneable{private String name;private String identifier;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getIdentifier() {return identifier;}public void setIdentifier(String identifier) {this.identifier = identifier;}@Overridepublic Employee clone() throws CloneNotSupportedException {return (Employee)super.clone();}public void print() {System.out.println(Objects.toStringHelper(this).add("name:", name).add("id:", identifier).toString());}public static void main(String[] args) throws CloneNotSupportedException {Employee employee1 = new Employee();employee1.setName("Ram");employee1.setIdentifier("1");System.out.println("1: "+employee1);employee1.print();Employee employee2 = employee1.clone();System.out.println("2: "+employee2);employee2.print();}
}

这是此的输出:

1: com.pramati.test.Employee@19821f
Employee{name:=Ram, id:=1}
2: com.pramati.test.Employee@de6ced
Employee{name:=Ram, id:=1}

从上面的示例可以看出,clone()方法创建了一个新Employee,其值是从现有对象中复制的。 这很简单,并且可以正常工作,因为Employee类中没有对象引用。 让我们这样修改类: 案例(2):

public class PayPackDetails{private double basicSalary = 500000d;private double incentive = 50000d;public double getSalary() {return getBasicSalary()+getIncentive();}public double getBasicSalary() {return basicSalary;}public double getIncentive() {return incentive;}public void setBasicSalary(double basicSalary) {this.basicSalary = basicSalary;}public void setIncentive(double incentive) {this.incentive = incentive;}
}public class Employee implements Cloneable {private String name;private String identifier;private PayPackDetails packDetails;public Employee(String name, String identifier, PayPackDetails packDetails) {this.name = name;this.identifier = identifier;this.packDetails = packDetails;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getIdentifier() {return identifier;}public void setIdentifier(String identifier) {this.identifier = identifier;}public PayPackDetails getPackDetails() {return packDetails;}@Overridepublic Employee clone() throws CloneNotSupportedException {return (Employee)super.clone();}public void print() {System.out.println(Objects.toStringHelper(this).add("name:", name).add("id:", identifier).add("package:", packDetails.getSalary()).toString());}public static void main(String[] args) throws CloneNotSupportedException {Employee employee1 = new Employee("Ram","1",new PayPackDetails());System.out.println("1: "+employee1);employee1.print();Employee employee2 = employee1.clone();System.out.println("2: "+employee2);employee2.print();}
}

运行main方法时,我们将得到以下结果:

1: com.pramati.clone.Employee@addbf1
Employee{name:=Ram, id:=1, package:=550000.0}
2: com.pramati.clone.Employee@de6ced
Employee{name:=Ram, id:=1, package:=550000.0}

这可以。 现在说,我们像这样修改了我们的主要方法: 案例(3):

public static void main(String[] args) throws CloneNotSupportedException {Employee employee1 = new Employee("Ram","1",new PayPackDetails());Employee employee2 = employee1.clone();employee2.setName("Krish"); employee2.setIdentifier("2");employee2.getPackDetails().setBasicSalary(700000d);employee1.print();employee2.print();
}

现在您认为employee1的薪水是多少? 随着我们增加了克隆员工的薪水,我们自然希望为他增加薪水。 但是这里出乎意料的转折是,employee1的薪水也增加了。 这是输出或此:

Employee{name:=Ram, id:=1, package:=750000.0}
Employee{name:=Krish, id:=2, package:=750000.0}

请注意,当我们克隆对象时,不会调用构造函数。 宁愿对原始对象的地址位置中存在的所有成员变量进行逐域复制。 现在,当有对象引用时,该引用将被复制,而不是原始对象。 因此,原始对象和克隆对象都指向同一成员对象。 因此,对一个对象所做的更改将自动对另一对象可见。 那么如何解决这个问题呢?

最简单的解决方案是也为PayPackDetails实现克隆方法,并从Employee的克隆方法中调用它。 情况(4):

@Override
public Employee clone() throws CloneNotSupportedException {Employee employee = (Employee)super.clone();employee.packDetails = packDetails.clone();return employee;
}

现在运行main()方法,它将按预期给出正确的结果:

Employee{name:=Ram, id:=1, package:=550000.0}
Employee{name:=Krish, id:=2, package:=750000.0}

但是,如果PayPackDetails由其他对象引用组成,则我们也必须重写该对象的克隆方法,并在PayPackDetails内部调用其克隆方法。 同样,当我们在PayPackDetails中组成新对象时,除了在新组成的对象中实现clone()方法外,我们还必须在PayPackDetails中修改clone方法。 组合对象类还应该实现Cloneable接口。 与往常一样,我们还必须处理CloneNotSupportedException。

现在考虑将PayPackDetails声明为final的另一种情况,这将使情况更加糟糕: 情况(5):

public class Employee implements Cloneable {private String name;private String identifier;private final PayPackDetails packDetails;// -- Rest of the methods
}

由于该字段被声明为final,因此无法在clone方法中为其分配新值,因为它被声明为final。 那么如何处理呢? 解决方案如下:使用复制构造函数并从克隆中返回新实例。

public class Employee implements Cloneable {private String name;private String identifier;private final PayPackDetails packDetails;public Employee(String name, String identifier, PayPackDetails packDetails) {this.name = name;this.identifier = identifier;this.packDetails = packDetails;}protected Employee(Employee emp) throws CloneNotSupportedException{name = emp.name;identifier = emp.identifier;packDetails = emp.packDetails.clone();}@Overridepublic Employee clone() throws CloneNotSupportedException {return new Employee(this);}public void print() {System.out.println(Objects.toStringHelper(this).add("name:", name).add("id:", identifier).add("package:", packDetails.getSalary()).toString());}
}

请注意,复制构造函数访问修饰符受到保护。 现在问题来了:为什么我们也不能将复制构造函数用于PayPackDetails而不是克隆方法? 答案是:是的,我们可以使用它。 情况(6):

public class PayPackDetails {private double basicSalary = 500000d;private double incentive = 50000d;public PayPackDetails(PayPackDetails details){basicSalary = details.getBasicSalary();incentive = details.getIncentive();}public static void main(String[] args) {Employee employee1 = new Employee("Ram","1",new PayPackDetails());employee1.print();Employee employee2 = new Employee(employee1);employee2.print();}
}
public class Employee {private String name;private String identifier;private final PayPackDetails packDetails;protected Employee(Employee emp) {name = emp.name;identifier = emp.identifier;packDetails = new PayPackDetails(emp.packDetails);}// .. Other methods}

到目前为止,这是最好的情况,这是此程序的输出:

Employee{name:=Ram, id:=1, package:=550000.0}
Employee{name:=Ram, id:=1, package:=550000.0}

实际上,这是最好的方法,因为它解决了有缺陷的克隆方法的许多问题:

1.没有一个类必须实现标记接口Cloneable
2.由于不需要克隆,因此无需捕获CloneNotSupportedException
3.由于不需要克隆,因此无需在调用super.clone()时对对象进行类型转换。

但是问题来了:假设您有一个PayPackDetails的子类。 案例(7):

public class AdvancedPayPackDetails extends PayPackDetails {private double flexiblePayPercent = 10d;public AdvancedPayPackDetails(AdvancedPayPackDetails details) {super(details);flexiblePayPercent = details.getFlexiblePayPercentage();}@Overridepublic double getSalary() {return super.getSalary()+(getBasicSalary()*getFlexiblePayPercentage()/100);}public double getFlexiblePayPercentage() {return flexiblePayPercent;}public void setFlexiblePayPercent(double flexiblePayPercent) {this.flexiblePayPercent = flexiblePayPercent;}public static void main(String[] args) throws CloneNotSupportedException {Employee employee1 = new Employee("Ram","1",new AdvancedPayPackDetails());employee1.print();Employee employee2 = employee1.clone();employee2.print();}}

现在运行main方法,它将为我们提供输出:

Employee{name:=Ram, id:=1, package:=600000.0}
Employee{name:=Ram, id:=1, package:=550000.0}

原因很明显。 Employee的副本构造函数不知道创建的这个新类(AdvancedPayPackDetails)。 实际上,我们可以修改Employee构造函数以包括对PayPackDetails的instanceOf检查,但这不是正确的方法。 相反,最好返回到我们先前的解决方案,在最终字段的情况下使用复制构造函数,并对具有继承层次结构的类使用克隆方法(案例(5)的解决方案)。

结论:正如我们在本文中一直看到的那样,以正确的方式实现克隆方法非常复杂。 因此最好尽量远离克隆。 只要复制对象没有任何继承层次结构,最好使用复制构造函数。

参考: 哪个更好的选择:克隆或复制构造函数? 来自我们的JCG合作伙伴 Prasanth Gullapalli,位于prasanthnath博客上。

翻译自: https://www.javacodegeeks.com/2014/01/which-is-better-option-cloning-or-copy-constructors.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/364784.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Fiddler教程--简介

1、开发环境host配置自己修改系统的host来回挺麻烦的 2、前后的接口调试 3、线上bugfix 4、性能分析和优化 5.等等... 工作原理 一个代理服务器地址改为 127.0.0.1:8888流模式边走边返回缓冲模式http请求完成所有的数据之后,才返回 界面功能介绍 1.工具栏 从下图红色…

java map是有序的吗_Java:如何初始化和填充最终的静态有序Map?

我在Java中有一个词干算法,它需要一个静态的最终HashMap< String,String>预先填写了大约30 000条记录.我需要地图按照插入的顺序保存记录(我得到一个提示,我可以使用LinkedHashMap&#xff1f;).我以为我可以在Java类文件中手动插入值,因为这是在RAM中加载它们的最快方法…

跟面向对象卯上了,看看ES6的“类”

上回我们说到ES5的面向对象&#xff0c;以及被大家公认的最佳的寄生组合式继承。时代在进步&#xff0c;在ES6中对于面向对象这个大boss理所应当地进行了一次大改&#xff0c;从原先那种比较长的写法转变为“小清新”写法。我们一起来看一下。 在ES6中是有类这个概念&#xff0…

js 变量作用域

例子 <script>var a "heh"function findLove(){console.log(a);function findforyou(){var a "you";console.log(a);}function findother(){console.log(a)}findforyou();findother();}findLove(); </script> 输出 heh you heh 例子 <scri…

Jin Ge Jin Qu hao UVA - 12563 01背包

题目&#xff1a;题目链接 思路&#xff1a;由于t最大值其实只有180 * 50 678&#xff0c;可以直接当成01背包来做&#xff0c;需要考虑的量有两个&#xff0c;时间和歌曲数&#xff0c;其中歌曲优先级大于时间&#xff0c;于是我们将歌曲数作为背包收益&#xff0c;用时间作为…

Java 8中的5个功能将改变您的编码方式

Java 8在JVM和语言级别都包含了一些非常令人兴奋的功能。 虽然最初为该发行版设想的某些功能已扩大范围或已推出到第9版&#xff0c;但实际上有数十个新功能。 许多新添加的内容在编译器&#xff0c;JVM或帮助系统级别都进行了后台改进。 这样&#xff0c;虽然我们可能会从中受…

Java相关资料分享(视频+电子书籍)

关注微信公众号【Java典籍】&#xff0c;获取百度网盘提取码 ▼微信扫一扫下图↓↓↓二维码关注 转载于:https://www.cnblogs.com/bingyimeiling/p/10279049.html

vue项目 一行js代码搞定点击图片放大缩小

一行js代码搞定xue项目需要点击图片放大缩小&#xff0c;其实主要用的是用到了vue:class的动态切换&#xff0c;内容比较简单。一开始我把维护的需求想得太复杂了&#xff0c;和测试小姐姐聊了一下才反应过来。 两个月不到跟了四个项目&#xff0c;现在是维护改bug阶段&#x…

指针系统学习8-小结

1.有关指针的数据类型的小结 2.指针运算小结 一、指针变量加&#xff08;减&#xff09;一个整数,会指向上&#xff08;下&#xff09;1&#xff08;i&#xff09;个元素  例如&#xff1a;&#xff50;&#xff0b;&#xff0b;、&#xff50;&#xff0d;&#xff0d;、&am…

java项目中外接扫描仪无法使用_java – 扫描仪行不可用错误

我用两种不同的方法从两个不同的扫描仪对象调用Scanner.nextLine()方法.有时当我从第二种方法调用Scanner.nextLine()时,它会给我一个“行不可用”错误.可能是什么问题&#xff1f;import java.util.Scanner;public class TicTacToe {private final String COMPUTER "com…

Spring集成–使用RMI通道适配器

1.引言 本文介绍了如何使用Spring Integration RMI通道适配器通过RMI发送和接收消息。 它由以下部分组成&#xff1a; 实施服务&#xff1a;第一部分着重于创建和公开服务。 实现客户端&#xff1a;显示如何使用MessagingTemplate类调用服务。 抽象SI逻辑&#xff1a;最后&a…

mybatis 直接执行sql 【我】

Connection conn getConnection();// Connection conn this.ss.getConnection(); 返回Connection对象 try { String sql "UPDATE PARTY SET PARTY_NAME 测试0000 WHERE PARTY_ID 0;UPDATE PARTY SET PARTY_NAME 测试1111 WH…

jquery点击非div区域隐藏div

点击非div区域隐藏div&#xff0c;如图&#xff0c;点击圆的头像&#xff08;.person-msg&#xff09;弹出白色底框(.person-centre)。点击圆头像以外的区域隐藏白色底框 html代码 <div class"per_c"><div class"person-msg pull-right"><i…

几种常用的函数

range()函数: range(起始位置,终止位置,步长)  next()函数: __next__是迭代器方法变量.__next__() 带双下火线的魔术发那个方法 一般情况下不直接使用next(变量)带双下划线的所有方法都可能和内置函数有千丝万缕的联系iter()函数: 可迭代的迭代器 可迭代的.__iter__()迭代器…

java sql server连接字符串_关于Java:SQL Server的等效jdbc连接字符串

我目前正在使用以下连接字符串连接到数据库(该数据库与ServerIP在同一服务器上)&#xff1a;String constr "Data SourceServerIP,1433;Network LibraryDBMSSOCN;InitialCatalogdbName;User IDdbUserID;PassworddbUserPassword";在asp.net中使用时&#xff0c;此连接…

上传图片或文件 方法一

最近在巩固一些知识点&#xff0c;回过头看之前做过的项目&#xff0c;所以就在这里总结一下 话不多说&#xff0c;直接看源码 前端 publish-menu.jsp 1 <form action"PublishMenu" method"post" enctype"multipart/form-data" > 2 3 …

如何在Ubuntu上轻松安装Oracle Java

Ubuntu上的开发人员习惯于至少看到两种Java风格。 OpenJDK是Java运行时和编译器的开源构建。 Oracle JDK以此为基础&#xff0c;但是增加了一些封闭源组件。 从理论上讲&#xff0c;OpenJDK是Java 7的官方参考实现 &#xff0c;并且完全可以满足您的所有需求。 在实践中&#…

今天发现新大陆:haml和Emmet

其实一开始小渣渣我只是想接触一下&#xff08;css预处理器&#xff09;sass&#xff0c;可是突然冒出一个haml。 原文是酱紫的。 Sass 是采用 Ruby 语言编写的一款 CSS 预处理语言&#xff0c;它诞生于2007年&#xff0c;是最大的成熟的 CSS 预处理语言。最初它是为了配合 H…

Docker系列(五):.Net Core实现k8s健康探测机制

k8s通过liveness来探测微服务的存活性&#xff0c;判断什么时候该重启容器实现自愈。比如访问 Web 服务器时显示 500 内部错误&#xff0c;可能是系统超载&#xff0c;也可能是资源死锁&#xff0c;此时 httpd 进程并没有异常退出&#xff0c;在这种情况下重启容器可能是最直接…

编写java程序计算梯形面积_学习练习 java面向对象梯形面积

package com.hanqi;public class Ladder {double ShangDi;double XiaDi;double Gao;double MianJi;Ladder(double ShangDi, double XiaDi, double Gao){//使用参数来初始化属性//this 代表当前类this.ShangDi ShangDi;this.XiaDi XiaDi;this.Gao Gao;}//方法的命名&#xff…