Java对象拷贝介绍


一、基本介绍

1. 介绍

Java 工程中,我们经常涉及对对象的创建与赋值,今天就详细介绍一下其背后的机制。

在开始之前我们要了解一个对象在 Java 中是如何进行存储的?对于 int、 byte 等等几类基本数据类型,Java 专门在堆栈中创立了一片常量池,当声明此类数据时对于 Java 内部而言相同的值仅存了一份,它们所指向的内存地址是一致。

如下示例中的两个赋值语句在 Java 常量池中只存储一个数值 10 ,然后将 ab 的引用都指向这个值。若后续 a 的值发生变化,则会在常量池中再存入新的数值,将 a 的引用指向这个新数值。

int a = 10;
int b = 10;

a = 20;

这样做的一大好处即能够大大的节省内存空间,无需为相同的数据分配不同的内存空间。

2. 对象存储

我们都知道当需要创建一个对象时可以通过 new 关键字创建对象,但是与 Java 基本类型不同是其会在内存中为每个对象分配一个单独的空间。

其中需要特别注意的是 String 类型,因为其不像基本类型都有其对应的封装类如 int 对应的 Integer 。因此对于 String 类型而言,当通过 = 赋值创建时作用效果等同于 int 等基本类型,但通过 new String() 创建时则会为其分配专门的内存空间。

如下面的示例中使用两种不同方式两个 String 对象,两个对象的内容虽然都一致,但因为 a 是直接赋值的所以则会存在常量池中,而 b 是通过 new 关键字创建所以会为其分配单独的内存空间,二者底层指向的内存地址因此是不同的,所以使用 == 进行引用比较时返回的结果将会是 false

String a = "test";
String b = new String("test");
// false
System.out.println(a == b);

二、对象引用

1. 介绍

在上面我们介绍了数据与对象的基本存储机制,下面再来了解一下对象引用赋值的相关原理。

Java 中通过 = 对对象进行复制时并不会创建一个新的对象,而是将新创建的对象的引用指向原对象的内存引用,如下示例中先创建了 user1 ,再通过=user1 赋值给 user2

User user1 = new User(1, "Alex");
User user2 = user1;

上述的代码在实际存储中效果如下,因此此时如果改变 user2 的值则 user1 会同步发生变化,在实际开发中要小心防止对象错误变更。

三、对象拷贝

1. 浅拷贝

通过上述的例子介绍了对象赋值存在的一些问题,但如果在开发中就是涉及到对象复制呢?根据上面的示例显然不能直接通过 = 直接赋值,有一个最简单的方式就是通过 new 重新创建一个对象。

如上述例子可以改造成如下:

User user1 = new User(1, "Alex");
User user2 = new User(1, "Alex");

但很显然这也有一个问题,但对象的属性过多时,这种方式将变得及其繁杂与低效。因此 Java 引入了 Cloneable 接口从而实现这一问题,被克隆对象只需实现 Cloneable 接口并重写 clone() 即可。

稍微改造一下 User 类为如下:

public class User implements Cloneable {
    private int id;
    private String name;

    public User(int i, String name) {
        this.id = id;
        this.name = name
    }

    @Override
    public User clone() {
        try {
            User user = (User) super.clone();
            return user;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

这时候即可直接调用 clone() 方法接口实现对象拷贝:

User user1 = new User(1, "Alex");
User user2 = user1.clone();
System.out.println("user1: " + user1);
System.out.println("user2: " + user2);

此时 user1user2 的内存存储示意图如下:

2. 深拷贝

所谓深拷贝为相对浅拷贝而言,当一个对象的属性仍为对象时,通过上述 clone() 方式只能实现非对象的子属性赋值,对于属性中的对象其仍会采用 = 的赋值方式。

假如此时有两个类 UserCity ,其中 User 属性包含 City 类。

public class User implements Cloneable {
    private int id;
    private String name;
    private City city;

    //... 略去其它代码
}

class City {
    String address;

    //... 略去其它代码
}

首先创建 user1 再通过 clone() 拷贝给 user2,但此时 user1user2City 对象其实指向的是同一个对象,若此时修改 user2City 对象 user1 的也会同步发生变化。

User user1 = new User(1, "Alex", new City("Beijing"));
User user2 = user1.clone();

因此若包含多层对象嵌套拷贝时每一级的对象类都需要实现 Cloneable 接口并重写 clone() ,同时重写 clone() 方法是也有所区别,如下是修改后的 User 类和 City 类。

public class User implements Cloneable {
    private int id;
    private String name;
    private City city;

    @Override
    public User clone() {
        try {
            User user = (User) super.clone();
            // 子对象属性仍需要通过拷贝赋值
            user.city = city.clone();
            return user;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

class City implements Cloneable{
    String address;

    //... 略去其它代码
}

3. 序列化拷贝

在深拷贝的例子中可以看到,当多层对象嵌套时使用 clone() 十分繁杂,因此我们还可以采用其它方式实现对象拷贝,即序列化拷贝。

通过 ByteArrayOutputStreamObjectOutputStreamIO 流将对象先进行序列化再实现转化拷贝,缺点显然易见这种方式将会产生额外的 IO 操作,因此需要根据实际情况进行选择。

public class User implements Serializable {
    public static User myClone(User user){
        User newUser = null;
        try (
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(bos);
        ) {
            out.writeObject(user);
            try (
                    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
                    ObjectInputStream ois = new ObjectInputStream(bis);
            ) {
                newUser = (User) ois.readObject();
                return newUser;
            } catch (IOException | ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录