วิธีการแทนที่วิธีการโคลนอย่างถูกต้อง?


114

ฉันจำเป็นต้องใช้การโคลนระดับลึกในวัตถุของฉันซึ่งไม่มีชั้นยอด

วิธีใดที่ดีที่สุดในการจัดการกับการตรวจสอบที่CloneNotSupportedExceptionถูกโยนโดยซูเปอร์คลาส (ซึ่งคือObject)?

เพื่อนร่วมงานแนะนำให้ฉันจัดการด้วยวิธีต่อไปนี้:

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

    // Deep clone member fields here

    return foo;
}

นี่ดูเหมือนจะเป็นวิธีแก้ปัญหาที่ดีสำหรับฉัน แต่ฉันต้องการเผยแพร่ให้ชุมชน StackOverflow ดูว่ามีข้อมูลเชิงลึกอื่น ๆ ที่ฉันสามารถรวมไว้ได้หรือไม่ ขอบคุณ!


6
ถ้าคุณรู้ว่าคลาสพาเรนต์ใช้การCloneableโยนAssertionErrorมากกว่าธรรมดาErrorก็เป็นการแสดงออกมากกว่า
Andrew Duffy

แก้ไข: ฉันไม่ต้องการใช้ clone () แต่โครงการมีพื้นฐานอยู่แล้วและจะไม่คุ้มค่าที่จะอ้างอิงการอ้างอิงทั้งหมด ณ จุดนี้
Cuga

ดีกว่าที่จะกัดกระสุนตอนนี้มากกว่าในภายหลัง (เว้นแต่คุณกำลังจะเข้าสู่การผลิต)
Tom Hawtin - แท็

เรามีเวลาเหลืออีกหนึ่งเดือนครึ่งสำหรับกำหนดการนี้จนกว่าจะเสร็จสมบูรณ์ เราไม่สามารถปรับเวลาได้
Cuga

ฉันคิดว่าโซลูชันนี้สมบูรณ์แบบ และอย่ารู้สึกแย่กับการใช้โคลน หากคุณมีคลาสที่ใช้เป็นอ็อบเจ็กต์การขนส่งและมีเพียงไพรไมท์และไม่เปลี่ยนรูปเช่น String และ Enums ทุกอย่างนอกเหนือจากการโคลนจะเป็นการเสียเวลาโดยสิ้นเชิงด้วยเหตุผลที่ไม่เชื่อ เพียงจำไว้ว่าโคลนทำอะไรและไม่ได้อะไร! (ไม่มีการโคลนลึก)
TomWolk

คำตอบ:


125

ต้องใช้อย่างแน่นอนclone? คนส่วนใหญ่ยอมรับว่า Java'scloneเสีย

Josh Bloch เกี่ยวกับการออกแบบ - ตัวสร้างการคัดลอกเทียบกับการโคลน

ถ้าคุณเคยอ่านเนื้อหาเกี่ยวกับการโคลนนิ่งในหนังสือของฉันโดยเฉพาะอย่างยิ่งถ้าคุณอ่านระหว่างบรรทัดคุณจะรู้ว่าฉันคิดว่าcloneมันพังมาก [... ] มันน่าเสียดายที่Cloneableเสีย แต่มันเกิดขึ้น

คุณสามารถอ่านอภิปรายเพิ่มเติมในหัวข้อในหนังสือของเขาที่มีประสิทธิภาพ Java ฉบับที่ 2 ข้อที่ 11: แทนที่cloneรอบคอบ เขาแนะนำให้ใช้ตัวสร้างสำเนาหรือโรงงานคัดลอกแทน

cloneเขาเดินไปที่หน้าเขียนหน้าเกี่ยวกับวิธีการถ้าคุณรู้สึกว่าคุณต้องคุณควรดำเนินการ แต่เขาปิดด้วยสิ่งนี้:

ความซับซ้อนทั้งหมดนี้จำเป็นจริงหรือ? นาน ๆ ครั้ง. หากคุณขยายคลาสที่ใช้Cloneableได้คุณมีทางเลือกน้อยมากนอกจากใช้cloneวิธีการที่มีพฤติกรรมดี มิฉะนั้นคุณจะดีกว่าให้หมายถึงทางเลือกของวัตถุการคัดลอกหรือเพียงแค่ไม่ให้ความสามารถในการ

สิ่งที่เน้นคือเขาไม่ใช่ของฉัน


เนื่องจากคุณทำให้มันชัดเจนว่าคุณมีทางเลือกน้อย แต่จะใช้ที่นี่เป็นสิ่งที่คุณสามารถทำได้ในกรณีนี้ให้แน่ใจว่าclone MyObject extends java.lang.Object implements java.lang.Cloneableหากเป็นเช่นนั้นคุณสามารถรับประกันได้ว่าคุณจะไม่จับไฟล์CloneNotSupportedException. การขว้างปาAssertionErrorตามที่บางคนแนะนำดูเหมือนจะสมเหตุสมผล แต่คุณยังสามารถเพิ่มความคิดเห็นที่อธิบายว่าเหตุใดจึงไม่ป้อนบล็อกการจับในกรณีนี้


อีกวิธีหนึ่งคือเป็นคนอื่นได้ชี้ให้เห็นนอกจากนี้คุณอาจจะสามารถดำเนินการโดยไม่ต้องโทรclonesuper.clone


1
น่าเสียดายที่โครงการนี้ถูกเขียนขึ้นโดยใช้วิธีการโคลนมิฉะนั้นฉันจะไม่ใช้มันอย่างแน่นอน ฉันเห็นด้วยอย่างยิ่งกับคุณว่าการใช้งานการโคลนของ Java นั้นเป็นเรื่องที่ผิดปกติ
Cuga

5
หากคลาสและซูเปอร์คลาสทั้งหมดเรียกใช้super.clone()ภายในเมธอดการโคลนโดยทั่วไปคลาสย่อยจะต้องแทนที่clone()หากเพิ่มฟิลด์ใหม่ซึ่งเนื้อหาจะต้องถูกโคลน หากซูเปอร์คลาสใดใช้newมากกว่าsuper.clone()คลาสย่อยทั้งหมดจะต้องแทนที่clone()ไม่ว่าจะเพิ่มฟิลด์ใหม่หรือไม่
supercat

56

บางครั้งการใช้ตัวสร้างสำเนาก็ง่ายกว่า:

public MyObject (MyObject toClone) {
}

ช่วยให้คุณประหยัดปัญหาในการจัดการCloneNotSupportedExceptionทำงานกับfinalฟิลด์และคุณไม่ต้องกังวลเกี่ยวกับประเภทที่จะส่งคืน


11

วิธีการทำงานของโค้ดของคุณค่อนข้างใกล้เคียงกับวิธีเขียนแบบ "บัญญัติ" ฉันจะโยนAssertionErrorเข้าไปในการจับแม้ว่า มันส่งสัญญาณว่าไม่ควรไปถึงเส้นนั้น

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

ดูคำตอบของ @polygenelubricants ว่าทำไมถึงเป็นความคิดที่ไม่ดี
Karl Richter

@KarlRichter ฉันอ่านคำตอบของพวกเขาแล้ว ไม่ได้บอกว่ามันเป็นความคิดที่ไม่ดีนอกเหนือจากนั้นCloneableโดยทั่วไปแล้วเป็นความคิดที่ไม่ดีดังที่อธิบายไว้ใน Effective Java Cloneableสหกรณ์แสดงแล้วว่าพวกเขาต้องใช้ ดังนั้นฉันจึงไม่รู้ว่าคำตอบของฉันจะปรับปรุงได้อย่างไรยกเว้นบางทีอาจจะลบออกทันที
Chris Jester-Young

9

มีสองกรณีที่CloneNotSupportedExceptionจะถูกโยน:

  1. คลาสที่กำลังโคลนจะไม่ถูกนำไปใช้Cloneable(สมมติว่าในที่สุดการโคลนนิ่งจริงจะเปลี่ยนไปObjectใช้วิธีการโคลนของ) หากคลาสที่คุณกำลังเขียนวิธีนี้โดยใช้วิธีCloneableนี้จะไม่เกิดขึ้น (เนื่องจากคลาสย่อยใด ๆ จะสืบทอดอย่างเหมาะสม)
  2. ยกเว้นในกรณีที่จะถูกโยนทิ้งอย่างชัดเจนโดยการดำเนินงาน - นี้เป็นวิธีที่แนะนำเพื่อป้องกันไม่ให้ clonability ใน subclass เมื่อ superclass Cloneableคือ

กรณีหลังไม่สามารถเกิดขึ้นในชั้นเรียนของคุณ (ตามที่คุณกำลังโดยตรงเรียก superclass' วิธีการในtryบล็อกแม้ว่าเรียกจากโทร subclass super.clone()) Cloneableและอดีตไม่ควรตั้งแต่ระดับของคุณอย่างชัดเจนควรใช้

โดยพื้นฐานแล้วคุณควรบันทึกข้อผิดพลาดอย่างแน่นอน แต่ในกรณีนี้จะเกิดขึ้นก็ต่อเมื่อคุณทำผิดนิยามของคลาส ดังนั้นให้ปฏิบัติเหมือนกับเวอร์ชันที่ตรวจสอบแล้วของNullPointerException(หรือคล้ายกัน) - จะไม่ถูกโยนทิ้งหากรหัสของคุณใช้งานได้


ในสถานการณ์อื่น ๆ คุณจะต้องเตรียมพร้อมสำหรับเหตุการณ์ที่อาจเกิดขึ้นนี้ - ไม่มีการรับประกันว่าอ็อบเจ็กต์ที่ระบุนั้นสามารถโคลนนิ่งได้ดังนั้นเมื่อตรวจพบข้อยกเว้นคุณควรดำเนินการที่เหมาะสมโดยขึ้นอยู่กับเงื่อนไขนี้ (ดำเนินการต่อกับอ็อบเจ็กต์ที่มีอยู่ใช้กลยุทธ์การโคลนทางเลือก เช่น serialize-deserialize ให้โยนIllegalParameterExceptionถ้าวิธีการของคุณต้องการพารามิเตอร์โดยการโคลนเป็นต้น)

แก้ไข : แม้ว่าโดยรวมแล้วฉันควรชี้ให้เห็นว่าใช่ แต่clone()เป็นเรื่องยากที่จะนำไปใช้อย่างถูกต้องและยากสำหรับผู้โทรที่จะรู้ว่าค่าที่ส่งคืนจะเป็นสิ่งที่พวกเขาต้องการหรือไม่ดังนั้นเมื่อคุณพิจารณาโคลนลึกและตื้น มักจะดีกว่าเพียงแค่หลีกเลี่ยงเรื่องทั้งหมดและใช้กลไกอื่น


หากอ็อบเจ็กต์เปิดเผยวิธีการโคลนนิ่งสาธารณะอ็อบเจ็กต์ที่ได้รับใด ๆ ที่ไม่รองรับจะละเมิดหลักการการแทนที่ Liskov หากวิธีการโคลนได้รับการคุ้มครองฉันคิดว่ามันจะดีกว่าที่จะเงากับสิ่งอื่นนอกเหนือจากวิธีการที่กลับมาชนิดที่เหมาะสมเพื่อป้องกันไม่ให้ subclass super.clone()จากได้พยายามที่จะเรียกร้อง
supercat

5

ใช้การทำให้เป็นอนุกรมเพื่อทำสำเนาแบบลึก นี่ไม่ใช่วิธีแก้ปัญหาที่เร็วที่สุด แต่ไม่ได้ขึ้นอยู่กับประเภท


นี่เป็นวิธีแก้ปัญหาที่ยอดเยี่ยมและชัดเจนเมื่อพิจารณาว่าฉันใช้ GSON อยู่แล้ว
Jacob Tabak

3

คุณสามารถใช้ตัวสร้างสำเนาที่มีการป้องกันได้ดังนี้:

/* 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
ไม่ได้ผลเกือบตลอดเวลา คุณไม่สามารถเรียกclone()วัตถุที่ส่งคืนได้getMySecondMember()เว้นแต่จะมีpublic cloneวิธีการ
polygenelubricants

เห็นได้ชัดว่าคุณต้องใช้รูปแบบนี้กับทุกออบเจ็กต์ที่คุณต้องการโคลนหรือหาวิธีอื่นในการโคลนสมาชิกแต่ละคนอย่างลึกซึ้ง
Dolph

ใช่นั่นเป็นข้อกำหนดที่จำเป็น
polygenelubricants

1

คำตอบส่วนใหญ่ที่นี่ถูกต้องฉันต้องบอกว่าโซลูชันของคุณเป็นวิธีที่นักพัฒนา Java API จริงทำด้วย (ไม่ว่าจะเป็น Josh Bloch หรือ Neal Gafter)

นี่คือสารสกัดจากคลาส openJDK, 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);
    }
}

ดังที่คุณสังเกตเห็นและคนอื่น ๆ กล่าวถึงCloneNotSupportedExceptionแทบไม่มีโอกาสถูกโยนทิ้งหากคุณประกาศว่าคุณใช้Cloneableอินเทอร์เฟซ

นอกจากนี้ไม่จำเป็นต้องแทนที่เมธอดหากคุณไม่ได้ทำอะไรใหม่ในเมธอดที่ถูกแทนที่ คุณจะต้องลบล้างเมื่อคุณต้องการดำเนินการเพิ่มเติมกับวัตถุหรือคุณต้องทำให้เป็นสาธารณะ

ในที่สุดก็ยังดีที่สุดที่จะหลีกเลี่ยงและใช้วิธีอื่น


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

                }
        }
    }
}

คุณเพิ่งเขียนลงในระบบไฟล์และอ่านวัตถุกลับมา โอเคนี่เป็นวิธีที่ดีที่สุดในการจัดการกับโคลนหรือไม่? ทุกคนจากชุมชน SO สามารถแสดงความคิดเห็นเกี่ยวกับแนวทางนี้ได้หรือไม่? ฉันเดาว่าสิ่งนี้เกี่ยวข้องกับการโคลนและการทำให้เป็นอนุกรมโดยไม่จำเป็น - สองแนวคิดที่แตกต่างกันอย่างสิ้นเชิง ฉันจะรอดูว่าคนอื่นจะพูดถึงเรื่องนี้อย่างไร
Saurabh Patil

2
ไม่ได้อยู่ในระบบไฟล์ แต่อยู่ในหน่วยความจำหลัก (ByteArrayOutputStream) สำหรับอ็อบเจ็กต์ที่ซ้อนกันอย่างลึกซึ้งเป็นวิธีการแก้ปัญหาที่ใช้ได้ดี โดยเฉพาะอย่างยิ่งถ้าคุณไม่จำเป็นต้องใช้บ่อยๆเช่นในการทดสอบหน่วย โดยที่ประสิทธิภาพไม่ใช่เป้าหมายหลัก
AlexWien

0

เพียงเพราะการใช้งาน Cloneable ของ java ไม่ได้หมายความว่าคุณไม่สามารถสร้างขึ้นมาเองได้

หากวัตถุประสงค์ที่แท้จริงของ OP คือการสร้างโคลนลึกฉันคิดว่าเป็นไปได้ที่จะสร้างอินเทอร์เฟซเช่นนี้:

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

จากนั้นใช้ตัวสร้างต้นแบบที่กล่าวถึงก่อนที่จะใช้งาน:

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

และคลาสอื่นที่มีฟิลด์อ็อบเจ็กต์ 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);
    }
}

ด้วยวิธีนี้คุณสามารถโคลนวัตถุของคลาส BClass ได้อย่างง่ายดายโดยไม่ต้องใช้ @SuppressWarnings หรือรหัสลูกเล่นอื่น ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.