clone详解

想要复制一个对象的所有属性,简单的方式就是通过Cloneable接口,你还可以使用工具类BeanUtils的copyProperties方法,还可以使用拷贝工厂或者使用序列化。这篇文章主要介绍怎么使用Cloneable来完成对象的拷贝。
Cloneable接口的作用是表明这个对象允许克隆。如果一个类实现了Cloneable,那么Object的clone方法就返回该对象的逐域拷贝;没有实现Cloneable,调用Object的clone方法将会抛出CloneNotSupportedException异常。

一个简单的例子

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class People implements Cloneable {
    private Long id;
    private String code;
    private String name;
    private Integer age;

    @Override
    public People clone() {
        People people;
        try {
            people = (People) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            throw new AssertionError();//can't happen
        }
        return people;
    }

    public static void main(String[] args) {
        People.PeopleBuilder builder = People.builder();
        People people = builder.id(12L)
                .name("mbtlami")
                .code("2018")
                .age(100)
                .build();
        People clone = people.clone();//it's the clone object
        System.out.println(people);
    }
}

但是如果对象的域中包含有可变的对象,使用以上方式只能简单的将可变对象的引用地址复制给克隆出来的对象。这样不管是原对象还是克隆对象,有一方修改了这个可变对象内的属性值,另一方同样会体现出来,这就导致了严重的后果。

在Person类中新增加一个引用对象“宠物”,修改people的宠物名称为cat,clone出来的对象的宠物名称也变为了cat:

private Pet pet;

@Data
public class Pet{
    private String name;
}

public static void main(String[] args) {
    Pet dog = new Pet("dog");
    People.PeopleBuilder builder = People.builder();
    People people = builder.id(12L)
            .name("mbtlami")
            .code("2018")
            .age(100)
            .pet(dog)
            .build();
    People clone = people.clone();//it's the clone object
    System.out.println(people);

    people.getPet().setName("cat");//modify Pet name
    System.out.println(clone);//print cat
}

如何保证clone方法正常工作呢?必须进行深克隆。有一个需要注意的地方:clone和引用可变对象的final域是不兼容的,给Pet属性增加final修饰符将不能正常工作。

浅克隆(ShallowClone)和深克隆(DeepClone)都是克隆对象,他们的主要区别在于是否支持引用类型的成员变量的复制。那么怎么进行深克隆呢?让Pet实现Cloneable并重写clone方法,还需要修改People的clone方法。

@Data
public class Pet implements Cloneable {
    private String name;

    public Pet(String name) {
        this.name = name;
    }

    @Override
    public Pet clone() {
        try {
            return (Pet) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            throw new AssertionError();
        }
    }
}

class People {
...
    @Override
    public People clone() {
        People people;
        try {
            people = (People) super.clone();
            if (pet != null) {
                people.pet = pet.clone();
            }
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            throw new AssertionError();//can't happen
        }
        return people;
    }
}

people.getPet().setName("cat");
System.out.println(clone);
//People{id=12, code='2018', name='mbtlami', age=25, isMarried=null, pet=Pet(name=dog)}

如果People内定义了一个散列桶数组,实现了轻量级的单向链表,仅仅只是递归克隆对象,虽然被克隆的对象有它自己的散列桶,但是数组引用的链表还是和原对象一样的,这样也会导致问题。

class Person{
...
    private Node<String, Object>[] table;

    static class Node<K, V> {
        final int hash;
        final K key;
        V value;
        Node<K, V> next;

        Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        ...
    }

    @Override
    public People clone() {
        People people;
        try {
            people = (People) super.clone();
            if (pet != null) {
                people.pet = pet.clone();
            }
            if (table != null) {
                people.table = table.clone();
            }
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            throw new AssertionError();//can't happen
        }
        return people;
    }
}
People.Node<String, Object>[] nodes = new People.Node[10];
People.Node<String, String> node12 = new People.Node<>("mbtlami12".hashCode(), "mbtlami12", "mbtlami12", null);
People.Node<String, String> node11 = new People.Node<>("mbtlami11".hashCode(), "mbtlami11", "mbtlami11", node12);
nodes[0] = new People.Node("mbtlami1".hashCode(), "mbtlami1", "mbtlami1", node11);
People.PeopleBuilder builder = People.builder();
People people = builder.id(12L)
        .name("mbtlami")
        .code("2018")
        .pet(dog)
        .age(25)
        .table(nodes)
        .build();
People clone = people.clone();
People.Node<String, Object> stringObjectNode = clone.getTable()[0];
System.out.println(stringObjectNode);
People.Node<String, Object>[] table = people.getTable();
table[0].next = null;
System.out.println(stringObjectNode);
//Node{hash=1192532577, key=mbtlami1, value=mbtlami1, next=Node{hash=-1686195728, key=mbtlami11, value=mbtlami11, next=Node{hash=-1686195727, key=mbtlami11, value=mbtlami11, next=null}}}
//Node{hash=1192532577, key=mbtlami1, value=mbtlami1, next=null}

深克隆也需要注意拷贝每一个散列桶的非空链表,在Node中定义deepClone方法:

Node<K, V> deepClone() {
    return new Node<K, V>(hash, key, value, next == null ? null : next.deepClone());
}

这样就没问题了吗?请注意递归克隆在链表比较长的话,很容易导致栈溢出,可以使用迭代遍历代替递归。

Node<K, V> deepCopy() {
    Node<K, V> node = new Node<K, V>(hash, key, value, next);
    for (Node p = node;p.next!=null;p = p.next) {
        p.next = new Node(p.next.hash, p.next.key, p.next.value, p.next.next);
    }
    return node;
}

这样相对完整的People对象的clone方法是这样的:

    @Override
    public People clone() {
        People people;
        try {
            people = (People) super.clone();
            if (pet != null) {
                people.pet = pet.clone();
            }
            if (table != null) {
                /*people.table = table.clone();*/
                people.table = new Node[table.length];
                for (int i = 0; i < people.table.length; i++) {
                    if (table[i] != null) {
                        /*people.table[i] = table[i].deepClone();*/
                        people.table[i] = table[i].deepCopy();
                    }
                }
            }
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            throw new AssertionError();//can't happen
        }
        return people;
    }

完整的例子请查看链接

此外,还有需要注意的地方,如果是为了继承而设计的类需要覆盖clone方法,clone方法需要和Object的clone行为保持一致:声明为protected,抛出异常,并且该父类不应该实现Cloneable接口,这样子类就可以自主选者是否实现Cloneable接口。如果类为线程安全的也需要注意clone方法的同步实现。

序列化方式:People、Pet、Person.Node需要实现Serializable接口。序列化对象也有很多需要注意的地方,详细请查看我的后续文章,这里只给出一个简单的例子。

public static Object cloneObject(Object obj) throws IOException, ClassNotFoundException{
    ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
    ObjectOutputStream out = new ObjectOutputStream(byteOut);
    out.writeObject(obj);
    ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
    ObjectInputStream in =new ObjectInputStream(byteIn);
    return in.readObject();
}

总结:所有实现Cloneable接口的类都应该用一个公有的方法覆盖clone,此方法首先应该调用supper.clone(),然后依次copy引用类型的域,并返回一个自身类型的对象而不是Object类型(方便调用,不由客户端来强制类型转换),并忽略受检异常。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值