Bagaimana cara mengganti metode klon dengan benar?

114

Saya perlu menerapkan klon dalam di salah satu objek saya yang tidak memiliki superclass.

Apa cara terbaik untuk menangani kotak centang yang CloneNotSupportedExceptiondilemparkan oleh superclass (yang mana Object)?

Seorang rekan kerja menyarankan saya untuk menanganinya dengan cara berikut:

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

    // Deep clone member fields here

    return foo;
}

Ini sepertinya solusi yang baik bagi saya, tetapi saya ingin menyampaikannya ke komunitas StackOverflow untuk melihat apakah ada wawasan lain yang dapat saya sertakan. Terima kasih!

Cuga
sumber
6
Jika Anda tahu bahwa kelas induk mengimplementasikan Cloneablemaka melempar AssertionErrordaripada sekadar polos Errorsedikit lebih ekspresif.
Andrew Duffy
Sunting: Saya lebih suka tidak menggunakan clone (), tetapi proyek ini sudah didasarkan padanya dan tidak akan ada gunanya merefaktor ulang semua referensi pada saat ini.
Cuga
Lebih baik gigit peluru sekarang daripada nanti (yah, kecuali jika Anda baru saja akan mencapai produksi).
Tom Hawtin - tackline
Kami memiliki satu setengah bulan tersisa pada jadwal sampai ini selesai. Kami tidak bisa membenarkan waktu.
Cuga
Saya pikir solusi ini sempurna. Dan jangan merasa buruk tentang menggunakan klon. Jika Anda memiliki kelas yang digunakan sebagai objek transportasi dan hanya berisi primitif dan tidak dapat diubah seperti String dan Enums, semua selain klon akan membuang-buang waktu karena alasan dogmatis. Hanya perlu diingat apa yang dilakukan klon dan apa yang tidak! (tidak ada kloning yang dalam)
TomWolk

Jawaban:

125

Apakah Anda benar-benar harus menggunakan clone? Kebanyakan orang setuju bahwa Java clonerusak.

Josh Bloch tentang Desain - Salin Pembuat versus Kloning

Jika Anda telah membaca item tentang kloning dalam buku saya, terutama jika Anda membaca yang tersirat, Anda akan tahu bahwa menurut saya clonesangat rusak. [...] Sayang sekali yang Cloneablerusak, tapi itu terjadi.

Anda dapat membaca lebih banyak diskusi tentang topik ini dalam bukunya Effective Java 2nd Edition, Butir 11: Mengabaikan clonedengan bijaksana . Sebagai gantinya, dia merekomendasikan untuk menggunakan pembuat salinan atau pabrik penyalinan.

Dia melanjutkan untuk menulis halaman halaman tentang bagaimana, jika Anda merasa harus, Anda harus menerapkan clone. Tapi dia menutup dengan ini:

Apakah semua kerumitan ini benar-benar diperlukan? Jarang. Jika Anda memperluas kelas yang mengimplementasikan Cloneable, Anda tidak punya banyak pilihan selain menerapkan clonemetode berperilaku baik . Jika tidak, Anda lebih baik menyediakan cara alternatif untuk menyalin objek, atau tidak menyediakan kemampuan .

Penekanannya adalah miliknya, bukan milikku.


Karena Anda telah menjelaskan bahwa Anda tidak punya banyak pilihan selain menerapkan clone, inilah yang dapat Anda lakukan dalam kasus ini: pastikan itu MyObject extends java.lang.Object implements java.lang.Cloneable. Jika demikian, maka Anda dapat menjamin bahwa Anda TIDAK PERNAH menangkap CloneNotSupportedException. Melempar AssertionErrorseperti yang disarankan beberapa tampaknya masuk akal, tetapi Anda juga dapat menambahkan komentar yang menjelaskan mengapa blok tangkap tidak akan pernah dimasukkan dalam kasus khusus ini .


Atau, seperti yang juga disarankan orang lain, Anda mungkin dapat menerapkan clonetanpa menelepon super.clone.

poligenelubricants
sumber
1
Sayangnya, proyek ini sudah ditulis menggunakan metode klon, jika tidak saya sama sekali tidak akan menggunakannya. Saya sepenuhnya setuju dengan Anda bahwa implementasi klon Java adalah fakakta.
Cuga
5
Jika sebuah kelas dan semua kelas supernya memanggil super.clone()dalam metode klonnya, sebuah subkelas umumnya hanya perlu mengganti clone()jika ia menambahkan bidang baru yang isinya perlu di-clone. Jika ada superclass menggunakan newdaripada super.clone(), maka semua subclass harus mengesampingkan clone()apakah atau tidak mereka menambahkan bidang baru.
supercat
56

Terkadang lebih mudah menerapkan konstruktor salinan:

public MyObject (MyObject toClone) {
}

Ini menghemat masalah penanganan CloneNotSupportedException, bekerja dengan finalbidang dan Anda tidak perlu khawatir tentang jenis yang akan dikembalikan.

Aaron Digulla
sumber
11

Cara kerja kode Anda sangat mirip dengan cara "kanonis" untuk menulisnya. Tapi aku akan melempar AssertionErrortangkapan. Ini menandakan bahwa garis itu tidak boleh dicapai.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
Chris Jester-Young
sumber
Lihat jawaban @polygenelubricants tentang mengapa ini mungkin ide yang buruk.
Karl Richter
@KarlRich Saya telah membaca jawaban mereka. Ia tidak mengatakan itu ide yang buruk, selain itu Cloneablepada umumnya adalah ide yang rusak, seperti yang dijelaskan dalam Java Efektif. OP sudah menyatakan bahwa mereka harus menggunakan Cloneable. Jadi saya tidak tahu bagaimana lagi jawaban saya bisa diperbaiki, kecuali mungkin langsung menghapusnya.
Chris Jester-Young
9

Ada dua kasus di mana surat CloneNotSupportedExceptionwasiat dilemparkan:

  1. Kelas yang sedang diklon tidak diimplementasikan Cloneable(dengan asumsi bahwa kloning sebenarnya pada akhirnya menolak Objectmetode kloning). Jika kelas tempat Anda menulis metode ini diimplementasikan Cloneable, ini tidak akan pernah terjadi (karena sub-kelas apa pun akan mewarisinya dengan tepat).
  2. Pengecualian secara eksplisit dilemparkan oleh implementasi - ini adalah cara yang direkomendasikan untuk mencegah klonabilitas dalam subclass saat superclass tersebut Cloneable.

Kasus terakhir tidak dapat terjadi di kelas Anda (karena Anda secara langsung memanggil metode superclass di tryblok, bahkan jika dipanggil dari pemanggilan subclass super.clone()) dan kasus sebelumnya tidak boleh karena kelas Anda harus diimplementasikan dengan jelas Cloneable.

Pada dasarnya, Anda harus mencatat kesalahan dengan pasti, tetapi dalam contoh khusus ini, itu hanya akan terjadi jika Anda mengacaukan definisi kelas Anda. Jadi perlakukan itu seperti versi yang dicentang NullPointerException(atau serupa) - itu tidak akan pernah ditampilkan jika kode Anda berfungsi.


Dalam situasi lain, Anda perlu bersiap untuk kemungkinan ini - tidak ada jaminan bahwa objek tertentu dapat digandakan, jadi saat menangkap pengecualian, Anda harus mengambil tindakan yang sesuai tergantung pada kondisi ini (lanjutkan dengan objek yang ada, ambil strategi kloning alternatif misalnya serialize-deserialize, throw an IllegalParameterExceptionif your method memerlukan parameter by cloneable, dll.).

Sunting : Meskipun secara keseluruhan saya harus menunjukkan bahwa ya, clone()sangat sulit untuk diterapkan dengan benar dan sulit bagi penelepon untuk mengetahui apakah nilai yang dikembalikan akan menjadi apa yang mereka inginkan, dua kali lipat ketika Anda mempertimbangkan klon dalam vs dangkal. Seringkali lebih baik hanya menghindari semuanya dan menggunakan mekanisme lain.

Andrzej Doyle
sumber
Jika suatu objek mengekspos metode kloning publik, objek turunan apa pun yang tidak mendukungnya akan melanggar Prinsip Substitusi Liskov. Jika metode kloning dilindungi, saya akan berpikir bahwa akan lebih baik untuk membayangi dengan sesuatu selain metode yang mengembalikan tipe yang tepat, untuk mencegah subclass bahkan mencoba memanggil super.clone().
supercat
5

Gunakan serialisasi untuk membuat salinan dalam. Ini bukan solusi tercepat tetapi tidak tergantung pada tipenya.

Michał Ziober
sumber
Ini adalah solusi yang bagus dan jelas mengingat saya sudah menggunakan GSON.
Jacob Tabak
3

Anda dapat mengimplementasikan konstruktor salinan yang dilindungi seperti ini:

/* 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);
}
Dolph
sumber
3
Ini tidak selalu berhasil. Anda tidak dapat memanggil clone()objek yang dikembalikan getMySecondMember()kecuali jika objek tersebut memiliki public clonemetode.
poligenelubricants
Tentunya, Anda perlu menerapkan pola ini pada setiap objek yang ingin Anda klon, atau temukan cara lain untuk melakukan kloning mendalam setiap anggota.
Dolph
Ya, itu adalah persyaratan yang perlu.
poligenelubricants
1

Karena sebagian besar jawaban di sini valid, saya perlu memberi tahu bahwa solusi Anda juga bagaimana pengembang Java API yang sebenarnya melakukannya. (Entah Josh Bloch atau Neal Gafter)

Berikut adalah ekstrak dari openJDK, kelas 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);
    }
}

Seperti yang Anda perhatikan dan orang lain sebutkan, CloneNotSupportedExceptionhampir tidak ada peluang untuk dilempar jika Anda menyatakan bahwa Anda mengimplementasikan Cloneableantarmuka.

Selain itu, Anda tidak perlu mengganti metode jika Anda tidak melakukan sesuatu yang baru dalam metode yang diganti. Anda hanya perlu menggantinya saat Anda perlu melakukan operasi tambahan pada objek atau Anda perlu membuatnya menjadi publik.

Pada akhirnya, yang terbaik adalah menghindarinya dan melakukannya dengan cara lain.

Haggra
sumber
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) {

                }
        }
    }
}
Kvant_hv
sumber
Jadi Anda baru saja menulisnya ke sistem file dan telah membaca objeknya kembali. Oke, apakah ini metode terbaik untuk menangani klon? Adakah yang bisa dari komunitas SO mengomentari pendekatan ini? Saya kira ini tidak perlu mengikat Kloning dan Serialisasi - dua konsep yang sama sekali berbeda. Saya akan menunggu untuk melihat apa yang orang lain katakan tentang ini.
Saurabh Patil
2
Ini tidak masuk ke sistem file, itu di memori utama (ByteArrayOutputStream). untuk objek yang sangat bersarang, solusinya adalah yang berfungsi dengan baik. Apalagi jika Anda tidak terlalu membutuhkannya, misal dalam unit test. dimana kinerja bukanlah tujuan utama.
AlexWien
0

Hanya karena implementasi java dari Cloneable rusak, itu tidak berarti Anda tidak dapat membuatnya sendiri.

Jika tujuan OP sebenarnya adalah untuk membuat klon yang dalam, saya rasa dimungkinkan untuk membuat antarmuka seperti ini:

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

kemudian gunakan konstruktor prototipe yang disebutkan sebelumnya untuk mengimplementasikannya:

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);
    }
}

dan kelas lain dengan bidang objek 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);
    }
}

Dengan cara ini Anda dapat dengan mudah mengkloning objek kelas BClass dengan mudah tanpa perlu @SuppressWarnings atau kode menarik lainnya.

Francesco Rogo
sumber