文章目录
Object
clone()
protected Object clone()
throws CloneNotSupportedException
返回值: 新的对象
使用场景: 当需要从某个已知对象A创建出另一个与A相同状态的对象B,并且对B的修改不会影响到A的状态时
clone()方法的约定
- 按照惯例都是通过调用
super.clone()
获取到复制的对象,因为最终都要调用Object类的clone(). - 按照惯例,此方法返回的对象应该独立于此对象,为了实现这种独立性,在返回
super.clone()
返回的对象之前,可能需要修改该对象的一个或多个字段。这意味着深克隆需要对引用对象的指向都有所改变。如果一个类只包含基础字段或对不可变对象的引用,那么通常情况下,super.clone返回的对象中没有字段需要修改。 - Cloneable接口作为一个混合接口,表明实现了该接口的对象允许克隆,但这个接口没有定义clone(),所以无法约束子类实现clone().虽然没有定义clone(),但它影响Object.clone()的行为,如果一个类实现了Cloneable,调用Object的clone()会返回该对象的逐域拷贝,否则抛出CloneNotSupportedException。
- 所有数组都被认为实现了接口Cloneable,并且数组类型T[]的clone方法的返回类型是T[],其中T是任何引用或基础类型。
- 无论是浅克隆还是深克隆,克隆出的基础数据应该都是一致的,区别在于引用对象的克隆与不克隆。
JDK文档中对clone()方法的约定:
- x.clone() != x [克隆对象与原对象不是同一个对象]
- x.clone().getClass() == x.getClass() [克隆的是同一类型的对象]
- x.clone().equals(x) == true [如果x.equals()方法定义恰当的话]
一般来说前两条是肯定的,但第三条建议遵守
使用clone()的规则
“如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone()而得到的对象”,这是使用clone()方法的规则,如果不遵守这条规则,在clone()方法中调用了构造器,那么就会得到错误的类,但不会有编译或者执行异常。
由此,我们可以看出调用super.clone()最终会调用Object类的clone方法,前提是子类的所有超类都遵循了调用super.clone()
的规则,否则无法实施。
实现Cloneable接口的类和其所有超类都要执行super.clone()
使clone()生效,这时无需调用构造器就可以创建对象。然而有些特殊的类可以调用构造器,比如final类,它没有子类,所以在clone()中调用构造器创建是合理的选择。
public class Test {
public static void main(String[] args) {
People p = new People();
System.out.println(p.clone().getClass());
System.out.println(p.getClass());
}
}
final class People{
public People clone(){
return new People();
}
}
浅克隆
克隆出来的对象的所有变量含有与原来的对象相同的值,而对其他对象的引用都指向原来的对象。也就是说,浅克隆仅仅克隆所考虑的对象。Object的clone就是"shallow copy"。如果类的每个域都是基本类型的值,或者是指向不可变对象的引用,那么调用Object.clone()就能得到正确的对象。
/**
*如果每个域都是基本类型,或者指向不可变对象的引用
*那么这个类只需要声明实现Cloneable接口,提供公有的clone()方法
*/
class ShallowCopy implements Cloneable
{
private String name;
private int no;
public ShallowCopy(String name,int no) {
this.name = name;
this.no = no;
}
/*只需调用super.clone()就能得到正确的行为*/
public ShallowCopy clone() {
try
{
return (ShallowCopy)super.clone();
}
catch (CloneNotSupportedException e)
{
throw new AssertionError();
}
}
}
通常情况下,我们已经得到了正确的对象,但是如果类里面包含代表序列号或者唯一ID的域,或者创建时间的域,还需要对这些域进行修正。
深克隆
深克隆把引用域所指向的对象也克隆一遍。
public class Test1 {
public static void main(String[] args) {
Children children = new Children("张三");
People people = new People(children);
System.out.println(people);
People clone = people.clone();
clone.getChildren().name= "李四";
System.out.println(people);
System.out.println(clone);
}
}
class People implements Cloneable{
private Children children;
public People(Children children){
this.children = children;
}
@Override
public People clone(){
try {
return (People) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new AssertionError();
}
}
@Override
public String toString() {
return "People{" +
"children=" + children.name +
'}';
}
public Children getChildren() {
return children;
}
}
class Children {
String name;
public Children(String name){
this.name = name;
}
}
输出:
People{children=张三}
People{children=李四}
People{children=李四}
Person类中的Children是引用对象,调用Object.clone()进行浅克隆后,克隆出来的children还是指向了原对象。
为了使Person的clone()正确工作,可以修改为:
public class Test1 {
public static void main(String[] args) {
Children children = new Children("张三");
People people = new People(children);
System.out.println(people);
People clone = people.clone();
clone.getChildren().name= "李四";
System.out.println(people);
System.out.println(clone);
}
}
class People implements Cloneable{
private Children children;
public People(Children children){
this.children = children;
}
@Override
public People clone(){
try {
People result = (People) super.clone();
// children 引用对象也调用super.clone
result.children = children.clone();
return result;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new AssertionError();
}
}
@Override
public String toString() {
return "People{" +
"children=" + children.name +
'}';
}
public Children getChildren() {
return children;
}
}
class Children implements Cloneable{ // Children实现接口
String name;
public Children(String name){
this.name = name;
}
//重写clone()
@Override
public Children clone() {
try {
return (Children) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
throw new AssertionError();
}
}
}
以上可以解决,但如果People中Children是final类,这个解决方案就不可行了,因为不能针对final修饰的类重复赋值,所以: clone架构与引用可变对象的final域的正确用法是不可兼容的!
抛开final域问题,递归调用clone()会不会存在问题?
import java.util.Arrays;
/**
*内部实现了单向链表
*buckets里的每个元素保存一个单向链表
*
*/
class NMap implements Cloneable {
private Entry[] buckets;
public NMap(int size) {
buckets = new Entry[size];
for(int i = 0; i < size; i++)
buckets[i] = new Entry("k1","v1",new Entry("nk1", "nv1", null));
}
public Entry[] getBuckets() {
return buckets;
}
static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key,Object value,Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
public void setNext(Entry next) {
this.next = next;
}
public String toString() {
String result = key + ":" + value + " ";
if(next != null)
result += next.toString();
return result;
}
}
public NMap clone(){
try
{
NMap result = (NMap)super.clone();
//数组被视为实现了Cloneable接口
result.buckets = buckets.clone();
return result;
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
public static void main(String[] args) {
NMap map = new NMap(5);
System.out.println(Arrays.toString(map.getBuckets()));
NMap clone = map.clone();
Entry entry = new Entry("ck1","cv1",new Entry("cnk1","cnv1",null));
for(Entry ent : clone.getBuckets())
ent.setNext(entry);
System.out.println(Arrays.toString(map.getBuckets()));
}
}
输出:
[k1:v1 nk1:nv1 , k1:v1 nk1:nv1 , k1:v1 nk1:nv1 , k1:v1 nk1:nv1 , k1:v1 nk1:nv1 ]
[k1:v1 ck1:cv1 cnk1:cnv1 , k1:v1 ck1:cv1 cnk1:cnv1 , k1:v1 ck1:cv1 cnk1:cnv1 , k1:v1 ck1:cv1 cnk1:cnv1 , k1:v1 ck1:cv1 cnk1:cnv1 ]
由输出可见,虽然克隆对象克隆了自己的buckets,但buckets中引用的链表与原始对象是一个,修改克隆对象数组中的链表,原对象数组的对象也随之改变了。这时可以在super.clone()后加一个 “深度拷贝”的方法。
import java.util.Arrays;
/**
*内部实现了单向链表
*buckets里的每个元素保存一个单向链表
*
*/
class NMap implements Cloneable {
private Entry[] buckets;
public NMap(int size) {
buckets = new Entry[size];
for(int i = 0; i < size; i++)
buckets[i] = new Entry("k1","v1",new Entry("nk1", "nv1", null));
}
public Entry[] getBuckets() {
return buckets;
}
static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key,Object value,Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
public void setNext(Entry next) {
this.next = next;
}
public String toString() {
String result = key + ":" + value + " ";
if(next != null)
result += next.toString();
return result;
}
// 深度拷贝,也就是迭代循环去生成引用
public Entry deepEntry() {
Entry result = new Entry(key,value,next);
for(Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key,p.next.value,p.next.next);
return result;
}
}
public NMap clone(){
try
{
NMap result = (NMap)super.clone();
result.buckets = buckets.clone();
// 深度拷贝
for(int i = 0; i < buckets.length; i++)
if(buckets[i] != null)
result.buckets[i] = buckets[i].deepEntry();
return result;
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
public static void main(String[] args) {
NMap map = new NMap(5);
System.out.println(Arrays.toString(map.getBuckets()));
NMap clone = map.clone();
Entry entry = new Entry("ck1","cv1",new Entry("cnk1","cnv1",null));
for(Entry ent : clone.getBuckets())
ent.setNext(entry);
System.out.println(Arrays.toString(map.getBuckets()));
System.out.println(Arrays.toString(clone.getBuckets()));
}
}
总结:
-
Cloneable接口是一个失败的接口,它没有提供clone()方法,却影响了Object.clone()克隆的行为:如果类没有实现Cloneable接口,调用super.clone()方法会得到CloneNotSupportedException。
-
所有实现了Cloneable接口的类都应该提供一个公有的方法覆盖clone(),此公有方法首先调用super.clone(),然后修正域,此公有方法一般不应该声明抛出CloneNotSupportedException。
-
如果为了继承而设计的类不应该实现Cloneable接口,这样可以使子类具有实现或者不实现Cloneable接口的自由,就仿佛它们直接扩展了Object一样。父类没有实现Cloneable接口,也没有覆盖clone(),子类如果实现了Cloneable,在覆盖的clone()中调用super.clone()是可以得到正确对象的。
据说很多专家级程序猿从来都不使用clone()方法。个人感觉容易有坑!
hashCode()
@HotSpotIntrinsicCandidate
public native int hashCode();
hashCode()方法是一个本地native方法,返回的是对象引用中存储的对象的内存地址
hashCode的通用协议是:
- 在Java应用程序的执行过程中,每当对同一对象多次调用hashCode方法时,只要不修改对象上的equals比较中使用的信息,hashCode方法就必须始终返回相同的整数。从一个应用程序的一次执行到同一应用程序的另一次执行,这个整数不需要保持一致。
- 如果根据equals(Object)方法,两个对象相等,那么对这两个对象中的每一个调用hashCode方法必须产生相同的整数结果。
- 如果根据equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的每一个调用hashCode方法必须产生不同的整数结果,这是不需要的。然而,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。
equals
Object
对象中的equals源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
在Object类源码(如下所示)中,其底层是使用了“==”来实现,也就是说通过比较两个对象的内存地址是否相同判断是否是同一个对象。hashCode()和equals()是一致的,都是用来比较内存地址,这也是为什么修改equals()也要修改hashCode().
在实际应用中,更多情况下我们只判断两个对象的某些属性值相同则认为这两个对象是同一个对象,所以需要重写equals(),此时如果两个对象指向内存地址或者两个对象的一些字段值相同,则为同一个对象。
注意:
如果子类重写了 equals() 方法,就需要重写hashCode()方法
比如 String 类就重写了 equals() 方法,同时也重写了 hashCode() 方法,都是针对String的value进行比较。
public class Simple {
public static void main(String[] args) {
EqualsA a = new EqualsA();
EqualsA a1 = new EqualsA();
// 不同对象,内存地址不同,返回false
System.out.println(a.equals(a1));
// 不同对象,但指向的都是一个内存地址,返回true
EqualsA a2 = a1;
System.out.println(a2.equals(a1));
String s1 = new String();
String s2 = new String();
// String类重写了equals(),只比较value是否相等,返回true
System.out.println(s1.equals(s2));
}
}
class EqualsA{
String name;
}
为什么重写equals()方法一定要重写hashCode()?
// 重写了equals(),没有重写hashCode()
public class OverrideEquals {
public static void main(String[] args) {
EqualsA1 equalsA1 = new EqualsA1();
equalsA1.id = "1";
EqualsA1 equalsA2 = new EqualsA1();
equalsA2.id = "2";
System.out.println(equalsA1);
System.out.println(equalsA2);
System.out.println(equalsA1.equals(equalsA2));
System.out.println(equalsA1.hashCode());
System.out.println(equalsA2.hashCode());
}
}
class EqualsA1{
String name;
String id;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass()!= o.getClass()) return false;
EqualsA1 that = (EqualsA1) o;
return Objects.equals(name, that.name);
}
@Override
public String toString() {
return "EqualsA1{" +
"name='" + name + '\'' +
", id='" + id + '\'' +
'}';
}
}
输出:
EqualsA1{name='null', id='1'}
EqualsA1{name='null', id='2'}
true
1627960023
357863579
以上代码重写了equals(),没有重写hashCode(),运行没有任何异常,输出可见,两个对象是相同的,但hashCode值不同
重写equals()一定要重写hashCode(),可以在HashSet和Map使用时来看下原因:
因为Map的特点是无序,key不能重复,在Map集合中,判断key相等标准是:两个key通过equals()方法比较返回true,如果加入两个相同的对象,则违反了Map的特点
public static void main(String[] args) {
Map<EqualsA1,EqualsA1> map = new HashMap<>();
EqualsA1 a1 = new EqualsA1();
a1.id = "1";
EqualsA1 a2 = new EqualsA1();
a2.id = "2";
map.put(a1,a1);
map.put(a2,a2);
// map不允许有重复的值,因为map无序,通过key的值来查找
System.out.println(map.get(a1).equals(map.get(a2)));
System.out.println(map.get(a2));
}
输出:
true
EqualsA1{name='null', id='2'}
对于对象集合的判重,如果一个集合含有100个对象实例,仅仅使用equals()方法的话,那么对于一个对象判重就需要比较4950次,随着集合规模的增大,时间开销是很大的。但是同时使用哈希表的话,就能快速定位到对象的大概存储位置,并且在定位到大概存储位置后,后续比较过程中,如果两个对象的hashCode不相同,也不再需要调用equals(),从而大大减少了equals()比较次数。所以从程序实现原理上来讲的话,既需要equals()方法,也需要hashCode()方法。那么既然重写了equals(),那么也要重写hashCode()方法,以保证两者之间的配合关系。
toString()
public String toString()
toString()
对于对象,返回的是组合而成的字符串:
getClass().getName() + '@' + Integer.toHexString(hashCode())