杰瑞科技汇

Java中clone()调用时,为何需实现Cloneable接口?

目录

  1. clone() 的基本概念
  2. 如何调用 clone()
    • 步骤 1:让类实现 Cloneable 接口
    • 步骤 2:重写 clone() 方法
    • 步骤 3:处理异常
    • 完整代码示例
  3. clone() 的工作原理:浅拷贝 vs. 深拷贝
    • 浅拷贝
    • 深拷贝
    • 代码示例对比
  4. clone() 的严重缺点和为什么应该避免它
  5. 推荐的替代方案
    • 拷贝构造器
    • 静态工厂方法
    • 序列化/反序列化

clone() 的基本概念

clone()Object 类中的一个受保护的方法,它的设计初衷是创建并返回一个对象的新副本,新副本的初始状态与原始对象相同。

Java中clone()调用时,为何需实现Cloneable接口?-图1
(图片来源网络,侵删)

关键点:

  • 受保护方法:你不能直接调用任何对象的 clone()myObject.clone() 是不合法的,会编译报错,只有在定义该类的包内,或者该类的子类中才能直接调用。
  • Cloneable 接口:虽然 Object 类定义了 clone() 方法,但它并不直接实现拷贝逻辑,Java 规定,如果一个类想要支持 Object.clone(),它必须实现 Cloneable 接口,这个接口是一个标记接口,它里面没有任何方法,实现它只是告诉 JVM:“这个类可以被安全地拷贝”。
  • CloneNotSupportedException:如果一个类没有实现 Cloneable 接口,那么当调用其 clone() 方法时,JVM 会抛出 CloneNotSupportedException 异常。

如何调用 clone()

要正确地使用 clone(),你需要遵循以下三个步骤。

步骤 1:让类实现 Cloneable 接口

这是最基本的先决条件。

public class MyClass implements Cloneable {
    // ... 类内容
}

步骤 2:重写 clone() 方法

为了能从类的外部调用 clone(),你需要将其重写为 public 方法,最佳实践是让该方法返回当前类的类型,而不是通用的 Object 类型,这被称为协变返回类型

Java中clone()调用时,为何需实现Cloneable接口?-图2
(图片来源网络,侵删)
@Override
public MyClass clone() {
    // ... 拷贝逻辑
}

步骤 3:处理异常

Object.clone() 方法会抛出 CloneNotSupportedException,所以你的重写方法也需要处理这个异常(要么 throws,要么 try-catch)。

完整代码示例

下面是一个简单的 Person 类,它实现了 clone()

import java.util.Arrays;
// 1. 实现 Cloneable 接口
class Person implements Cloneable {
    private String name;
    private int age;
    private int[] scores; // 一个可变对象
    public Person(String name, int age, int[] scores) {
        this.name = name;
        this.age = age;
        // 重要:这里进行数组拷贝,而不是直接赋值引用
        this.scores = Arrays.copyOf(scores, scores.length);
    }
    // 2. 重写 clone() 方法为 public
    @Override
    public Person clone() {
        Person clonedPerson = null;
        try {
            // 3. 调用父类的 clone() 方法,它会进行浅拷贝
            clonedPerson = (Person) super.clone();
            // 如果需要对可变成员进行深拷贝,在这里手动处理
            clonedPerson.scores = Arrays.copyOf(this.scores, this.scores.length);
        } catch (CloneNotSupportedException e) {
            // 因为 Person 实现了 Cloneable,所以这个异常理论上不会发生
            throw new AssertionError(e);
        }
        return clonedPerson;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", scores=" + Arrays.toString(scores) +
                '}';
    }
}
public class Main {
    public static void main(String[] args) {
        int[] scores = {100, 90, 80};
        Person original = new Person("Alice", 30, scores);
        // 调用 clone() 方法
        Person cloned = original.clone();
        System.out.println("Original: " + original);
        System.out.println("Cloned:   " + cloned);
        // 修改克隆对象中的数组
        cloned.scores[0] = 50;
        System.out.println("\nAfter modifying cloned's scores:");
        System.out.println("Original: " + original); // 原对象的数组也被修改了!
        System.out.println("Cloned:   " + cloned);
    }
}

注意:上面的例子中,我们手动处理了 int[] 的拷贝,以确保这是一个深拷贝,关于浅拷贝和深拷贝,我们接下来会详细解释。


clone() 的工作原理:浅拷贝 vs. 深拷贝

Object.clone() 方法默认执行的是浅拷贝

Java中clone()调用时,为何需实现Cloneable接口?-图3
(图片来源网络,侵删)

浅拷贝

  • 定义:创建一个新的对象,新对象的基本类型成员变量与原始对象的值相同,对于引用类型成员变量,新对象和原始对象共享同一个引用,指向堆内存中的同一个对象。
  • 后果:如果你修改了克隆对象中的某个可变引用类型成员(如 ArrayList, int[], 自定义对象),这个修改会反映到原始对象上,因为它们指向的是同一个东西。

浅拷贝示例:

class Address {
    String city;
    Address(String city) { this.city = city; }
}
class Employee implements Cloneable {
    String name;
    Address address; // 引用类型
    Employee(String name, Address address) {
        this.name = name;
        this.address = address;
    }
    @Override
    public Employee clone() {
        try {
            return (Employee) super.clone(); // 默认浅拷贝
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}
public class ShallowCopyDemo {
    public static void main(String[] args) {
        Address address = new Address("New York");
        Employee original = new Employee("John", address);
        Employee cloned = original.clone();
        System.out.println("Original Employee's City: " + original.address.city); // New York
        System.out.println("Cloned Employee's City:   " + cloned.address.city);     // New York
        // 修改克隆对象的地址
        cloned.address.city = "Boston";
        System.out.println("\nAfter modifying cloned's address:");
        System.out.println("Original Employee's City: " + original.address.city); // Boston! 原对象也被影响了!
        System.out.println("Cloned Employee's City:   " + cloned.address.city);     // Boston
    }
}

深拷贝

  • 定义:创建一个新的对象,新对象的所有成员变量(包括引用类型指向的对象)都会被递归地创建一份全新的副本,新对象和原始对象完全独立,互不影响。
  • 如何实现
    1. clone() 方法中,对于每一个可变的引用类型成员,手动创建一个副本并赋值给克隆对象。
    2. 让每个内部的可变类也实现 Cloneable 接口,并在外层类的 clone() 方法中递归调用它们的 clone() 方法。

深拷贝示例 (基于上面的浅拷贝代码修改):

class Address implements Cloneable {
    String city;
    Address(String city) { this.city = city; }
    @Override
    public Address clone() {
        try {
            return (Address) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}
class Employee implements Cloneable {
    String name;
    Address address;
    Employee(String name, Address address) {
        this.name = name;
        this.address = address;
    }
    // 实现深拷贝
    @Override
    public Employee clone() {
        Employee cloned = null;
        try {
            cloned = (Employee) super.clone(); // 浅拷贝基本类型和引用
            // 对引用类型成员进行深拷贝
            cloned.address = this.address.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
        return cloned;
    }
}
public class DeepCopyDemo {
    public static void main(String[] args) {
        Address address = new Address("New York");
        Employee original = new Employee("John", address);
        Employee cloned = original.clone();
        System.out.println("Original Employee's City: " + original.address.city); // New York
        System.out.println("Cloned Employee's City:   " + cloned.address.city);     // New York
        // 修改克隆对象的地址
        cloned.address.city = "Boston";
        System.out.println("\nAfter modifying cloned's address:");
        System.out.println("Original Employee's City: " + original.address.city); // New York! 原对象不受影响
        System.out.println("Cloned Employee's City:   " + cloned.address.city);     // Boston
    }
}

clone() 的严重缺点和为什么应该避免它

尽管 clone() 存在,但 Joshua Bloch 在其经典著作《Effective Java》中明确指出:通常情况下,最好避免使用 clone() 方法

原因如下:

  1. 接口与实现混淆clone() 是一个 Object 类的方法,但它却与具体的类实现紧密相关,这违反了面向对象设计的“接口与实现分离”原则,调用 clone() 时,你实际上是在调用一个受保护的、与具体实现细节相关的方法。
  2. 浅拷贝的陷阱:如前所述,Object.clone() 默认是浅拷贝,开发者很容易忘记处理引用类型成员,从而在无意中导致程序 bug(一个对象的修改影响另一个)。
  3. 构造函数被绕过clone() 的过程是:创建一个新对象,然后逐个字段地复制原始对象的值。它不会调用被克隆对象的构造函数,这会带来一些问题:
    • 如果构造函数中做了一些不可重入的初始化逻辑(比如启动一个线程、建立网络连接),这些逻辑在 clone() 时会被跳过,可能导致克隆对象处于不一致的状态。
    • 对于“final”字段,如果它们不是基本类型或不可变对象,clone() 可能无法正确复制它们(尽管 Java 允许在 clone() 方法中修改 final 字段,但这很危险且不常见)。
  4. 方法签名不清晰clone() 返回 Object 类型,强制要求每次调用后都要进行强制类型转换,这很繁琐且容易出错。
  5. 文档约定不明确Cloneable 接口没有指定 clone() 方法的行为,它只表示“可以被克隆”,但如何克隆、拷贝的深度(深/浅)完全由类的实现者决定,这增加了使用者的理解成本。

推荐的替代方案

既然 clone() 有这么多问题,我们应该用什么来替代呢?以下是几种更安全、更清晰的方式。

拷贝构造器

提供一个构造函数,其参数是同类型的另一个对象,这个构造函数负责创建新对象并复制所有成员。

class Person {
    private String name;
    // ... 其他字段
    // 原始构造函数
    public Person(String name) { ... }
    // 拷贝构造器
    public Person(Person other) {
        this.name = other.name; // 基本类型直接复制
        // 如果有引用类型,需要在这里手动进行深拷贝
        // this.address = new Address(other.address);
    }
}
// 使用
Person original = new Person("Alice");
Person copied = new Person(original); // 非常清晰明了

优点

  • 非常直观,符合 Java 构造对象的习惯。
  • 不需要实现任何特殊接口或重写方法。
  • 没有隐藏的规则,代码可读性高。

静态工厂方法

创建一个静态方法,接收一个对象作为参数,并返回该对象的一个副本。

class Person {
    // ... 同上
    // 静态工厂方法
    public static Person copyOf(Person other) {
        Person p = new Person();
        p.name = other.name;
        // ... 深拷贝逻辑
        return p;
    }
}
// 使用
Person original = new Person("Alice");
Person copied = Person.copyOf(original);

或者更简洁的写法(利用 Java 16 的 record 特性,record 会自动生成 equals, hashCode, toString,并且推荐使用拷贝构造器):

record Point(int x, int y) {}
// Point p1 = new Point(1, 2);
// Point p2 = new Point(p1); // 编译器自动生成的拷贝构造器

优点

  • 比拷贝构造器更灵活,可以给方法起一个更清晰的名字(如 copyOf, newInstanceFrom)。
  • 可以返回原类型的子类,这是构造器做不到的。

序列化/反序列化

将对象序列化为字节流,然后再从字节流反序列化出一个新的对象,这种方式天然就是深拷贝。

import java.io.*;
public class DeepCopyUtils {
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T deepCopy(T object) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(object);
            oos.flush();
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (T) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException("Failed to deep copy object", e);
        }
    }
}
// 使用
Person original = new Person("Alice");
Person copied = DeepCopyUtils.deepCopy(original);

优点

  • 实现简单,可以处理非常复杂的对象图,无需手动编写深拷贝逻辑。
  • 保证是真正的深拷贝。

缺点

  • 性能开销大:涉及 I/O 操作,比拷贝构造器慢得多。
  • 类必须实现 Serializable 接口
  • 不安全:会拷贝所有字段,包括那些你不希望被拷贝的(如缓存、数据库连接句柄等),如果对象图中有不可序列化的对象,会直接抛出异常。

特性 clone() 拷贝构造器 静态工厂方法 序列化/反序列化
简洁性 差(需实现接口、重写、处理异常)
安全性 差(浅拷贝陷阱、绕过构造函数) 优(显式、可控) 优(显式、可控) 中(可能拷贝不该拷贝的字段)
性能
可读性 差(与实现耦合)
适用场景 几乎不推荐 首选方案 灵活场景,如返回子类 对象图复杂,且类都 Serializable

最终建议

在 99% 的情况下,请避免使用 clone(),优先选择拷贝构造器静态工厂方法来创建对象的副本,它们更安全、更清晰,也更符合 Java 的设计哲学,只有在需要处理极其复杂的对象图,并且性能不是关键瓶颈时,才考虑使用序列化/反序列化的方式。

分享:
扫描分享到社交APP
上一篇
下一篇