Làm thế nào để ghi đè đúng phương thức nhân bản?


114

Tôi cần triển khai một bản sao sâu trong một trong những đối tượng không có lớp cha.

Cách tốt nhất để xử lý kiểm tra được CloneNotSupportedExceptionném bởi lớp cha (đó là Object) là gì?

Một đồng nghiệp khuyên tôi nên xử lý theo cách sau:

@Override
public MyObject clone()
{
    MyObject foo;
    try
    {
        foo = (MyObject) super.clone();
    }
    catch (CloneNotSupportedException e)
    {
        throw new Error();
    }

    // Deep clone member fields here

    return foo;
}

Đây có vẻ là một giải pháp tốt đối với tôi, nhưng tôi muốn đưa nó ra cộng đồng StackOverflow để xem liệu tôi có thể đưa vào bất kỳ thông tin chi tiết nào khác không. Cảm ơn!


6
Nếu bạn biết rằng lớp cha thực hiện Cloneablethì việc ném một AssertionErrorthay vì chỉ đơn thuần Errorlà một cách biểu đạt hơn một chút.
Andrew Duffy

Chỉnh sửa: Tôi không muốn sử dụng clone (), nhưng dự án đã dựa trên nó và nó sẽ không đáng để cấu trúc lại tất cả các tham chiếu vào thời điểm này.
Cuga

Tốt hơn nên cắn viên đạn ngay bây giờ hơn là sau này (tốt, trừ khi bạn sắp đạt sản lượng).
Tom Hawtin - tackline

Chúng tôi còn một tháng rưỡi theo lịch trình cho đến khi việc này hoàn thành. Chúng tôi không thể biện minh cho thời gian.
Cuga

Tôi nghĩ rằng giải pháp này là hoàn hảo. Và đừng cảm thấy tồi tệ khi sử dụng bản sao. Nếu bạn có một lớp được sử dụng như một đối tượng truyền tải và nó chỉ chứa các primitves và bất biến như String và Enums, mọi thứ khác ngoài bản sao sẽ hoàn toàn lãng phí thời gian vì những lý do giáo điều. Chỉ cần ghi nhớ những gì nhân bản làm và những gì không! (không nhân bản sâu)
TomWolk

Câu trả lời:


125

Bạn có nhất thiết phải sử dụng clonekhông? Hầu hết mọi người đồng ý rằng Java clonebị hỏng.

Josh Bloch về thiết kế - Copy Constructor so với Cloning

Nếu bạn đã đọc mục về nhân bản trong cuốn sách của tôi, đặc biệt là nếu bạn đọc giữa các dòng, bạn sẽ biết rằng tôi nghĩ rằng tôi rất suy clonesụp. [...] Thật xấu hổ khi Cloneablebị phá vỡ, nhưng nó sẽ xảy ra.

Bạn có thể đọc thêm thảo luận về chủ đề này trong cuốn sách Hiệu quả Java lần 2, Mục 11: Ghi đè clonemột cách thận trọng . Thay vào đó, ông khuyến nghị nên sử dụng một công cụ tạo bản sao hoặc nhà máy sao chép.

Anh ấy tiếp tục viết các trang về cách thức, nếu bạn cảm thấy phải thực hiện, bạn nên thực hiện clone. Nhưng anh ấy đã kết thúc với điều này:

Tất cả những điều phức tạp này có thực sự cần thiết? Ít khi. Nếu bạn mở rộng một lớp thực thi Cloneable, bạn có ít lựa chọn ngoài việc triển khai một clonephương thức hoạt động tốt . Nếu không, bạn nên cung cấp các phương tiện sao chép đối tượng thay thế, hoặc đơn giản là không cung cấp khả năng .

Điểm nhấn là của anh ấy, không phải của tôi.


Vì bạn đã nói rõ rằng bạn không có lựa chọn nào khác ngoài việc triển khai clone, đây là những gì bạn có thể làm trong trường hợp này: hãy đảm bảo điều đó MyObject extends java.lang.Object implements java.lang.Cloneable. Nếu đúng như vậy, thì bạn có thể đảm bảo rằng bạn sẽ KHÔNG BAO GIỜ mắc phải a CloneNotSupportedException. Việc ném AssertionErrornhư một số người đã đề xuất có vẻ hợp lý, nhưng bạn cũng có thể thêm nhận xét giải thích lý do tại sao khối bắt sẽ không bao giờ được nhập trong trường hợp cụ thể này .


Ngoài ra, như những người khác cũng đã đề xuất, bạn có thể thực hiện clonemà không cần gọi super.clone.


1
Thật không may, dự án đã được viết xung quanh bằng cách sử dụng phương pháp nhân bản, nếu không tôi sẽ hoàn toàn không sử dụng nó. Tôi hoàn toàn đồng ý với bạn rằng việc triển khai bản sao của Java là fakakta.
Cuga

5
Nếu một lớp và tất cả các lớp cha của nó gọi super.clone()bên trong các phương thức nhân bản của chúng, thì một lớp con nói chung sẽ chỉ phải ghi đè clone()nếu nó thêm các trường mới mà nội dung của chúng cần được sao chép . Nếu bất kỳ lớp cha nào sử dụng newthay vì sử dụng super.clone(), thì tất cả các lớp con phải ghi đè clone()cho dù chúng có thêm bất kỳ trường mới nào hay không.
supercat

56

Đôi khi, việc triển khai một hàm tạo bản sao đơn giản hơn:

public MyObject (MyObject toClone) {
}

Nó giúp bạn tiết kiệm rắc rối khi xử lý CloneNotSupportedException, hoạt động với finalcác trường và bạn không phải lo lắng về kiểu trả về.


11

Cách mã của bạn hoạt động khá gần với cách viết "chuẩn". Tuy nhiên, tôi sẽ ném một AssertionErrorquả bóng vào tầm bắt. Nó báo hiệu rằng dòng đó sẽ không bao giờ đạt được.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}

Xem câu trả lời của @polygenelubricants để biết lý do tại sao đây có thể là một ý tưởng tồi.
Karl Richter

@KarlRichter Tôi đã đọc câu trả lời của họ. Nó không nói rằng đó là một ý tưởng tồi, nhưng Cloneablenói chung là một ý tưởng hỏng, như đã giải thích trong Java Hiệu quả. OP đã bày tỏ rằng họ phải sử dụng Cloneable. Vì vậy, tôi không biết làm cách nào khác mà câu trả lời của tôi có thể được cải thiện, ngoại trừ việc xóa nó ngay lập tức.
Chris Jester-Young

9

Có hai trường hợp CloneNotSupportedExceptionsẽ được ném:

  1. Lớp đang được nhân bản không được thực hiện Cloneable(giả sử rằng việc nhân bản thực sự cuối cùng định Objectnghĩa thành phương thức nhân bản của). Nếu lớp bạn đang viết phương thức này trong các triển khaiCloneable , điều này sẽ không bao giờ xảy ra (vì bất kỳ lớp con nào sẽ kế thừa nó một cách thích hợp).
  2. Ngoại lệ được thực hiện một cách rõ ràng - đây là cách được khuyến nghị để ngăn chặn khả năng sao chép trong một lớp con khi lớp cha đó Cloneable.

Trường hợp thứ hai không thể xảy ra trong lớp của bạn (vì bạn đang gọi trực tiếp phương thức của lớp cha trong trykhối, ngay cả khi được gọi từ cách gọi lớp con super.clone()) và trường hợp trước thì không xảy ra vì lớp của bạn rõ ràng nên thực thi Cloneable.

Về cơ bản, bạn nên ghi lại lỗi cho chắc chắn, nhưng trong trường hợp cụ thể này, nó sẽ chỉ xảy ra nếu bạn làm sai định nghĩa lớp của mình. Vì vậy, hãy coi nó như một phiên bản đã kiểm tra của NullPointerException(hoặc tương tự) - nó sẽ không bao giờ bị ném nếu mã của bạn hoạt động.


Trong các tình huống khác, bạn sẽ cần phải chuẩn bị cho tình huống này - không có gì đảm bảo rằng một đối tượng nhất định có thể được sao chép, vì vậy khi bắt được ngoại lệ, bạn nên thực hiện hành động thích hợp tùy thuộc vào điều kiện này (tiếp tục với đối tượng hiện có, thực hiện một chiến lược nhân bản thay thế ví dụ: serialize-deserialize, ném một IllegalParameterExceptionnếu phương thức của bạn yêu cầu tham số bằng cách sao chép, v.v.).

Chỉnh sửa : Mặc dù về tổng thể, tôi nên chỉ ra rằng có, clone()thực sự rất khó triển khai chính xác và người gọi khó biết liệu giá trị trả về có như họ muốn hay không, gấp đôi khi bạn xem xét nhân bản sâu và nông. Tốt hơn là bạn nên tránh toàn bộ sự việc và sử dụng một cơ chế khác.


Nếu một đối tượng hiển thị một phương pháp nhân bản công khai, thì bất kỳ đối tượng dẫn xuất nào không hỗ trợ nó sẽ vi phạm Nguyên tắc thay thế Liskov. Nếu một phương thức nhân bản được bảo vệ, tôi sẽ nghĩ rằng tốt hơn nên phủ bóng nó bằng một thứ gì đó khác hơn là một phương thức trả về kiểu thích hợp, để ngăn một lớp con thậm chí cố gắng gọi super.clone().
supercat

5

Sử dụng tuần tự hóa để tạo bản sao sâu. Đây không phải là giải pháp nhanh nhất nhưng nó không phụ thuộc vào loại.


Đây là một giải pháp tuyệt vời và rõ ràng khi tôi đã sử dụng GSON.
Jacob Tabak

3

Bạn có thể triển khai các trình tạo bản sao được bảo vệ như sau:

/* This is a protected copy constructor for exclusive use by .clone() */
protected MyObject(MyObject that) {
    this.myFirstMember = that.getMyFirstMember(); //To clone primitive data
    this.mySecondMember = that.getMySecondMember().clone(); //To clone complex objects
    // etc
}

public MyObject clone() {
    return new MyObject(this);
}

3
Điều này không hoạt động hầu hết thời gian. Bạn không thể gọi clone()đối tượng được trả về getMySecondMember()trừ khi nó có một public clonephương thức.
polygenelubricants 24/02/10

Rõ ràng, bạn cần triển khai mẫu này trên mọi đối tượng mà bạn muốn để có thể nhân bản hoặc tìm cách khác để nhân bản sâu từng thành viên.
Dolph

Vâng, đó là một yêu cầu cần thiết.
polygenelubricants

1

Vì hầu hết các câu trả lời ở đây đều hợp lệ, tôi cần phải nói rằng giải pháp của bạn cũng chính là cách các nhà phát triển Java API thực tế thực hiện. (Josh Bloch hoặc Neal Gafter)

Đây là một trích xuất từ ​​openJDK, lớp ArrayList:

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

Như bạn đã nhận thấy và những người khác đã đề cập, CloneNotSupportedExceptionhầu như không có cơ hội bị ném nếu bạn tuyên bố rằng bạn triển khaiCloneable giao diện.

Cũng thế, không cần ghi đè phương thức nếu bạn không thực hiện bất kỳ điều gì mới trong phương thức được ghi đè. Bạn chỉ cần ghi đè nó khi bạn cần thực hiện thêm các thao tác trên đối tượng hoặc bạn cần đặt nó ở chế độ công khai.

Cuối cùng, tốt nhất vẫn là tránh nó và sử dụng một số cách khác.


0
public class MyObject implements Cloneable, Serializable{   

    @Override
    @SuppressWarnings(value = "unchecked")
    protected MyObject clone(){
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            ByteArrayOutputStream bOs = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bOs);
            oos.writeObject(this);
            ois = new ObjectInputStream(new ByteArrayInputStream(bOs.toByteArray()));
            return  (MyObject)ois.readObject();

        } catch (Exception e) {
            //Some seriouse error :< //
            return null;
        }finally {
            if (oos != null)
                try {
                    oos.close();
                } catch (IOException e) {

                }
            if (ois != null)
                try {
                    ois.close();
                } catch (IOException e) {

                }
        }
    }
}

Vì vậy, bạn vừa ghi nó vào hệ thống tệp và đã đọc lại đối tượng. Được rồi, đây có phải là phương pháp tốt nhất để xử lý sao chép không? Bất cứ ai từ cộng đồng SO có thể nhận xét về cách tiếp cận này? Tôi đoán điều này ràng buộc một cách không cần thiết giữa Nhân bản và Tuần tự hóa - hai khái niệm hoàn toàn khác nhau. Tôi sẽ chờ xem những người khác nói gì về điều này.
Saurabh Patil

2
Nó không nằm trong hệ thống tệp, nó nằm trong bộ nhớ chính (ByteArrayOutputStream). đối với các đối tượng lồng nhau sâu, giải pháp của nó hoạt động tốt. Đặc biệt nếu bạn không cần nó thường xuyên, ví dụ như trong bài kiểm tra đơn vị. nơi hiệu suất không phải là mục tiêu chính.
AlexWien

0

Chỉ vì việc triển khai Cloneable của java bị hỏng không có nghĩa là bạn không thể tạo một bản sao của riêng mình.

Nếu mục đích thực sự của OP là tạo một bản sao sâu, tôi nghĩ rằng có thể tạo một giao diện như thế này:

public interface Cloneable<T> {
    public T getClone();
}

sau đó sử dụng phương thức khởi tạo nguyên mẫu đã đề cập trước đó để triển khai nó:

public class AClass implements Cloneable<AClass> {
    private int value;
    public AClass(int value) {
        this.vaue = value;
    }

    protected AClass(AClass p) {
        this(p.getValue());
    }

    public int getValue() {
        return value;
    }

    public AClass getClone() {
         return new AClass(this);
    }
}

và một lớp khác có trường đối tượng AClass:

public class BClass implements Cloneable<BClass> {
    private int value;
    private AClass a;

    public BClass(int value, AClass a) {
         this.value = value;
         this.a = a;
    }

    protected BClass(BClass p) {
        this(p.getValue(), p.getA().getClone());
    }

    public int getValue() {
        return value;
    }

    public AClass getA() {
        return a;
    }

    public BClass getClone() {
         return new BClass(this);
    }
}

Bằng cách này, bạn có thể dễ dàng sao chép sâu một đối tượng của lớp BClass mà không cần đến @SuppressWarnings hoặc mã phô trương khác.

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.