clone () so với phương thức tạo bản sao so với phương thức nhà máy?


81

Tôi đã tìm kiếm nhanh trên google về cách triển khai clone () trong Java và tìm thấy: http://www.javapractices.com/topic/TopicAction.do?Id=71

Nó có nhận xét sau:

copy constructor và các phương thức static factory cung cấp một giải pháp thay thế cho clone và dễ thực hiện hơn nhiều.

Tất cả những gì tôi muốn làm là tạo một bản sao sâu. Việc thực hiện clone () có vẻ rất có ý nghĩa, nhưng bài viết được google xếp hạng cao này khiến tôi hơi e ngại.

Đây là những vấn đề mà tôi nhận thấy:

Các hàm tạo sao chép không hoạt động với Generics.

Đây là một số mã giả sẽ không biên dịch.

public class MyClass<T>{
   ..
   public void copyData(T data){
       T copy=new T(data);//This isn't going to work.    
   }
   ..
}

Mẫu 1: Sử dụng một hàm tạo sao chép trong một lớp chung.

Các phương thức gốc không có tên chuẩn.

Nó khá đẹp khi có một giao diện cho mã có thể sử dụng lại.

public class MyClass<T>{
    ..
    public void copyData(T data){
        T copy=data.clone();//Throws an exception if the input was not cloneable
    }
    ..
}

Mẫu 2: Sử dụng clone () trong một lớp chung.

Tôi nhận thấy rằng bản sao không phải là một phương thức tĩnh, nhưng vẫn không cần thiết phải tạo bản sao sâu của tất cả các trường được bảo vệ? Khi thực hiện clone (), nỗ lực bổ sung để ném các ngoại lệ trong các lớp con không thể sao chép có vẻ nhỏ đối với tôi.

Tui bỏ lỡ điều gì vậy? Mọi thông tin chi tiết sẽ được đánh giá cao.


Câu trả lời:


52

Về cơ bản, clone bị hỏng . Không có gì sẽ hoạt động với thuốc chung một cách dễ dàng. Nếu bạn có một cái gì đó như thế này (rút gọn để hiểu rõ hơn):

public class SomeClass<T extends Copyable> {


    public T copy(T object) {
        return (T) object.copy();
    }
}

interface Copyable {
    Copyable copy();
}

Sau đó, với một cảnh báo trình biên dịch, bạn có thể hoàn thành công việc. Bởi vì các generic bị xóa trong thời gian chạy, một thứ gì đó thực hiện một bản sao sẽ có một cảnh báo trình biên dịch tạo ra diễn viên trong đó. Nó không thể tránh khỏi trong trường hợp này. . Nó có thể tránh được trong một số trường hợp (cảm ơn, kb304) nhưng không phải trong tất cả. Hãy xem xét trường hợp bạn phải hỗ trợ một lớp con hoặc một lớp không xác định triển khai giao diện (chẳng hạn như bạn đang lặp qua một tập hợp các bản sao không nhất thiết phải tạo ra cùng một lớp).


2
Nó cũng có thể làm mà không có cảnh báo trình biên dịch.
akarnokd

@ kd304, nếu ý bạn là bạn nhấn cảnh báo trình biên dịch, đúng, nhưng thực sự không khác. Nếu bạn có nghĩa là bạn có thể tránh hoàn toàn nhu cầu cảnh báo, vui lòng giải thích.
Yishai

@Yishai: Hãy xem câu trả lời của tôi. Và tôi không đề cập đến cuộc chiến trấn áp.
akarnokd

Từ chối vì Copyable<T>câu trả lời của kd304 có ý nghĩa hơn nhiều khi sử dụng.
Simon Forsberg 13/1213

25

Ngoài ra còn có mẫu Builder. Xem Java hiệu quả để biết chi tiết.

Tôi không hiểu đánh giá của bạn. Trong một hàm tạo bản sao, bạn hoàn toàn biết về kiểu, tại sao lại cần phải sử dụng generic?

public class C {
   public int value;
   public C() { }
   public C(C other) {
     value = other.value;
   }
}

Có một câu hỏi tương tự gần đây ở đây .

public class G<T> {
   public T value;
   public G() { }
   public G(G<? extends T> other) {
     value = other.value;
   }
}

Một mẫu có thể chạy được:

public class GenTest {
    public interface Copyable<T> {
        T copy();
    }
    public static <T extends Copyable<T>> T copy(T object) {
        return object.copy();
    }
    public static class G<T> implements Copyable<G<T>> {
        public T value;
        public G() {
        }
        public G(G<? extends T> other) {
            value = other.value;
        }
        @Override
        public G<T> copy() {
            return new G<T>(this);
        }
    }
    public static void main(String[] args) {
        G<Integer> g = new G<Integer>();
        g.value = 1;
        G<Integer> f = g.copy();
        g.value = 2;
        G<Integer> h = copy(g);
        g.value = 3;
        System.out.printf("f: %s%n", f.value);
        System.out.printf("g: %s%n", g.value);
        System.out.printf("h: %s%n", h.value);
    }
}

1
+1 đây là cách đơn giản nhất nhưng hiệu quả để triển khai một hàm tạo bản sao
dfa

Lưu ý rằng MyClass ở trên là một chung, StackOverflow nuốt <T>.
User1

Đã sửa định dạng câu hỏi của bạn. Sử dụng bốn dấu cách trên mỗi dòng thay vì các thẻ trước + mã.
akarnokd 09/07/09

Tôi nhận được một lỗi vào phương pháp bản sao của G - "phải ghi đè lên một phương pháp siêu đẳng"
Carl Pritchett

3
(Tôi biết điều này là cũ, nhưng để hậu thế) @CarlPritchett: lỗi này sẽ hiển thị trong Java 1.5 trở xuống. Đánh dấu các phương thức giao diện là @ Ghi đè được phép bắt đầu trong Java 1.6.
Carrotman 42,

10

Java không có các hàm tạo sao chép giống như C ++.

Bạn có thể có một phương thức khởi tạo lấy một đối tượng cùng kiểu làm đối số, nhưng ít lớp hỗ trợ điều này. (ít hơn số hỗ trợ sao chép có thể)

Đối với một bản sao chung, tôi có một phương thức trợ giúp tạo ra một phiên bản mới của một lớp và sao chép các trường từ bản gốc (một bản sao cạn) bằng cách sử dụng phản xạ (thực sự giống như phản xạ nhưng nhanh hơn)

Đối với một bản sao sâu, một cách tiếp cận đơn giản là tuần tự hóa đối tượng và hủy tuần tự hóa nó.

BTW: Đề xuất của tôi là sử dụng các đối tượng bất biến, sau đó bạn sẽ không cần sao chép chúng. ;)


Bạn có thể vui lòng giải thích tại sao tôi không cần nhân bản nếu tôi sử dụng các đối tượng bất biến không? Trong ứng dụng của tôi, tôi cần có một đối tượng khác có dữ liệu chính xác như đối tượng hiện có. Vì vậy, tôi cần phải sao chép nó bằng cách nào đó
Zhenya

2
@Ievgen nếu bạn cần hai tham chiếu đến cùng một dữ liệu, bạn có thể sao chép tham chiếu. Bạn chỉ cần sao chép nội dung của một đối tượng nếu nó có thể thay đổi (nhưng đối với đối tượng bất biến bạn biết điều đó sẽ không)
Peter Lawrey

Có lẽ tôi chỉ không hiểu lợi ích của các đối tượng bất biến. Giả sử tôi có một thực thể JPA / hibernate và tôi cần tạo một thực thể JPA khác dựa trên thực thể hiện có, nhưng tôi cần thay đổi id của thực thể mới. Làm thế nào tôi sẽ làm điều đó với các đối tượng bất biến?
Zhenya

@Ievgen theo định nghĩa, bạn không thể thay đổi các đối tượng bất biến. Stringví dụ là một đối tượng không thể thay đổi và bạn có thể truyền một Chuỗi xung quanh mà không cần phải sao chép nó.
Peter Lawrey

6

Tôi nghĩ rằng câu trả lời của Yishai có thể được cải thiện để chúng tôi không có cảnh báo với mã sau:

public class SomeClass<T extends Copyable<T>> {

    public T copy(T object) {
        return object.copy();
    }
}

interface Copyable<T> {
    T copy();
}

Theo cách này, một lớp cần triển khai giao diện Copyable phải như thế này:

public class MyClass implements Copyable<MyClass> {

    @Override
    public MyClass copy() {
        // copy implementation
        ...
    }

}

2
Hầu hết. Giao diện Copyable nên được khai báo là:, là giao diện interface Copyable<T extends Copyable>gần nhất với kiểu self mà bạn có thể mã hóa trong Java.
Lặp lại

Tất nhiên là bạn muốn nói interface Copyable<T extends Copyable<T>>, phải không? ;)
Adowrath

3

Dưới đây là một số khuyết điểm do nhiều nhà phát triển không sử dụng Object.clone()

  1. Việc sử dụng Object.clone()method đòi hỏi chúng ta phải thêm rất nhiều cú pháp vào mã của chúng ta như Cloneablegiao diện triển khai , xác định clone()phương thức và xử lý CloneNotSupportedExceptionvà cuối cùng gọi đến Object.clone()và ép kiểu đối tượng của chúng ta.
  2. Cloneableinterface thiếu clone()phương thức, nó là một giao diện đánh dấu và không có bất kỳ phương thức nào trong đó, và chúng ta vẫn cần triển khai nó chỉ để nói với JVM rằng chúng ta có thể thực hiện clone()trên đối tượng của mình.
  3. Object.clone()được bảo vệ vì vậy chúng tôi phải cung cấp clone()cuộc gọi riêng và gián tiếp Object.clone()từ nó.
  4. Chúng tôi không có bất kỳ quyền kiểm soát nào đối với việc xây dựng đối tượng vì Object.clone()không gọi bất kỳ hàm tạo nào.
  5. Nếu chúng ta đang viết clone()phương thức trong một lớp con, ví dụ: Personthì tất cả các lớp cha của nó phải định nghĩa clone()phương thức trong chúng hoặc kế thừa nó từ một lớp cha khác, nếu không super.clone()chuỗi sẽ bị lỗi.
  6. Object.clone()chỉ hỗ trợ bản sao nông, vì vậy các trường tham chiếu của đối tượng mới được nhân bản của chúng ta sẽ vẫn giữ các đối tượng mà các trường của đối tượng ban đầu của chúng ta đang giữ. Để khắc phục điều này, chúng ta cần triển khai clone()trong mọi lớp mà lớp của chúng ta đang nắm giữ tham chiếu và sau đó sao chép chúng riêng biệt trong clone()phương thức của chúng ta như trong ví dụ dưới đây.
  7. Chúng ta không thể thao tác các trường cuối cùng Object.clone()vì các trường cuối cùng chỉ có thể được thay đổi thông qua các hàm tạo. Trong trường hợp của chúng ta, nếu chúng ta muốn mọi Personđối tượng là duy nhất theo id, chúng ta sẽ nhận được đối tượng trùng lặp nếu chúng ta sử dụng Object.clone()Object.clone()sẽ không gọi hàm tạo và idkhông thể sửa đổi trường cuối cùng từ đó Person.clone().

Các trình tạo bản sao tốt hơn là Object.clone()vì chúng

  1. Không bắt buộc chúng tôi phải triển khai bất kỳ giao diện nào hoặc ném bất kỳ ngoại lệ nào nhưng chúng tôi chắc chắn có thể làm điều đó nếu nó được yêu cầu.
  2. Không yêu cầu bất kỳ phôi nào.
  3. Không yêu cầu chúng tôi phụ thuộc vào một cơ chế tạo đối tượng không xác định.
  4. Không yêu cầu lớp cha tuân theo bất kỳ hợp đồng hoặc thực hiện bất kỳ điều gì.
  5. Cho phép chúng tôi sửa đổi các trường cuối cùng.
  6. Cho phép chúng ta có toàn quyền kiểm soát việc tạo đối tượng, chúng ta có thể viết logic khởi tạo của mình trong đó.

Đọc thêm về sao chép Java - Copy Constructor so với Cloning


1

Thông thường, clone () hoạt động song song với một hàm tạo bản sao được bảo vệ. Điều này được thực hiện vì clone (), không giống như một phương thức khởi tạo, có thể là ảo.

Trong một cơ thể lớp cho Xuất phát từ một Cơ sở siêu cấp chúng ta có

class Derived extends Base {
}

Vì vậy, đơn giản nhất, bạn sẽ thêm vào này một hàm tạo bản sao ảo với clone (). (Trong C ++, Joshi đề xuất sao chép làm phương thức tạo bản sao ảo.)

protected Derived() {
    super();
}

protected Object clone() throws CloneNotSupportedException {
    return new Derived();
}

Sẽ phức tạp hơn nếu bạn muốn gọi super.clone () theo khuyến nghị và bạn phải thêm các thành viên này vào lớp, bạn có thể thử cách này

final String name;
Address address;

/// This protected copy constructor - only constructs the object from super-class and
/// sets the final in the object for the derived class.
protected Derived(Base base, String name) {
   super(base);
   this.name = name;
}

protected Object clone() throws CloneNotSupportedException {
    Derived that = new Derived(super.clone(), this.name);
    that.address = (Address) this.address.clone();
}

Bây giờ, nếu một cuộc hành quyết, bạn có

Base base = (Base) new Derived("name");

và sau đó bạn đã làm

Base clone = (Base) base.clone();

điều này sẽ gọi, clone () trong lớp Derived (ở trên), điều này sẽ gọi super.clone () - có thể được triển khai hoặc không, nhưng bạn nên gọi nó. Sau đó, việc triển khai sẽ chuyển đầu ra của super.clone () tới một phương thức khởi tạo sao chép được bảo vệ lấy một Cơ sở và bạn chuyển bất kỳ thành viên cuối cùng nào cho nó.

Sau đó, phương thức tạo bản sao đó sẽ gọi phương thức tạo bản sao của lớp siêu cấp (nếu bạn biết rằng nó có một) và thiết lập các giá trị cuối cùng.

Khi bạn quay lại phương thức clone (), bạn đặt bất kỳ thành viên nào không phải là cuối cùng.

Người đọc Astute sẽ nhận thấy rằng nếu bạn có một hàm tạo bản sao trong Cơ sở, nó sẽ được gọi bởi super.clone () - và sẽ được gọi lại khi bạn gọi hàm tạo siêu trong hàm tạo được bảo vệ, vì vậy bạn có thể đang gọi super copy-constructor hai lần. Hy vọng rằng, nếu nó đang khóa tài nguyên, nó sẽ biết điều đó.


0

Một mẫu có thể phù hợp với bạn là sao chép cấp đậu. Về cơ bản, bạn sử dụng một hàm tạo no-arg và gọi các bộ định tuyến khác nhau để cung cấp dữ liệu. Bạn thậm chí có thể sử dụng các thư viện thuộc tính bean khác nhau để thiết lập các thuộc tính tương đối dễ dàng. Điều này không giống như làm một bản sao () nhưng đối với nhiều mục đích thực tế, nó tốt.


0

Giao diện Cloneable bị hỏng, theo nghĩa là nó vô dụng nhưng bản sao hoạt động tốt và có thể dẫn đến hiệu suất tốt hơn cho các đối tượng lớn - 8 trường và hơn thế nữa, nhưng sau đó nó sẽ không phân tích thoát được. vì vậy nên sử dụng hàm tạo bản sao hầu hết thời gian. Sử dụng bản sao trên mảng nhanh hơn Arrays.copyOf vì độ dài được đảm bảo là như nhau.

thêm chi tiết tại đây https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html


0

Nếu một người không nhận thức được 100% tất cả những điều kỳ quặc clone(), thì tôi khuyên bạn nên tránh xa nó. Tôi sẽ không nói rằng clone()bị hỏng. Tôi sẽ nói: chỉ sử dụng nó khi bạn hoàn toàn chắc chắn rằng đó là lựa chọn tốt nhất của bạn. Một hàm tạo bản sao (hoặc phương thức gốc, điều đó không thực sự quan trọng theo tôi nghĩ) rất dễ viết (có thể dài dòng, nhưng dễ dàng), nó chỉ sao chép những gì bạn muốn sao chép và sao chép theo cách bạn muốn mọi thứ được sao chép. Bạn có thể cắt nó theo nhu cầu chính xác của bạn.

Thêm vào đó: thật dễ dàng để gỡ lỗi những gì xảy ra khi bạn gọi phương thức nhà máy / phương thức khởi tạo bản sao của mình.

clone()không tạo một bản sao "sâu" của đối tượng của bạn ra khỏi hộp, giả sử bạn muốn nói rằng không chỉ các tham chiếu (ví dụ: a Collection) được sao chép qua. Nhưng đọc thêm về sâu và nông tại đây: Bản sao sâu, bản sao nông, bản sao


-2

Những gì bạn đang thiếu là bản sao tạo ra các bản sao cạn theo mặc định và quy ước, và việc tạo bản sao sâu nói chung là không khả thi.

Vấn đề là bạn không thể thực sự tạo ra các bản sao sâu của đồ thị đối tượng tuần hoàn, mà không thể theo dõi những đối tượng nào đã được truy cập. clone () không cung cấp theo dõi như vậy (vì đó sẽ phải là một tham số cho .clone ()) và do đó chỉ tạo ra các bản sao nông.

Ngay cả khi đối tượng của riêng bạn gọi .clone cho tất cả các thành viên của nó, nó vẫn không phải là một bản sao sâu.


4
Có thể không khả thi khi thực hiện sao chép sâu clone () cho bất kỳ đối tượng tùy ý nào, nhưng trên thực tế, điều này có thể quản lý được đối với nhiều cấu trúc phân cấp đối tượng. Nó chỉ phụ thuộc vào loại đối tượng bạn có và biến thành viên của chúng là gì.
Ông Shiny và Mới 安 宇

Có ít sự mơ hồ hơn nhiều người khẳng định. Nếu một người có một SuperDuperList<T>bản sao hoặc một dẫn xuất của chúng, thì việc nhân bản nó sẽ tạo ra một bản sao mới cùng loại với bản sao đã được nhân bản; nó phải được tách ra khỏi bản gốc, nhưng nên tham chiếu đến cùng Tmột thứ, theo thứ tự, như bản gốc. Không có gì được thực hiện cho một trong hai danh sách từ thời điểm đó trở đi sẽ ảnh hưởng đến danh tính của các đối tượng được lưu trữ trong danh sách kia. Tôi biết không có "quy ước" hữu ích nào kêu gọi một bộ sưu tập chung để thể hiện bất kỳ hành vi nào khác.
mèo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.