Java Tip 76 : 전체 복사 기술의 대안

객체의 딥 카피를 구현하는 것은 학습 경험이 될 수 있습니다. 당신은 그것을 원하지 않는다는 것을 알게됩니다! 문제의 개체가 다른 복잡한 개체를 참조하고 다른 개체를 참조하는 경우이 작업은 실제로 벅찰 수 있습니다. 전통적으로, 객체의 각 클래스는 Cloneable인터페이스 를 구현하기 위해 개별적으로 검사하고 편집해야하며 clone(), 자신과 포함 된 객체의 전체 복사본을 만들기 위해 메서드를 재정의해야 합니다. 이 기사에서는 시간이 많이 걸리는 기존의 딥 카피 대신 사용할 수있는 간단한 기술을 설명합니다.

딥 카피의 개념

깊은 복사 가 무엇인지 이해하기 위해 먼저 얕은 복사의 개념을 살펴 보겠습니다.

이전 JavaWorld 기사 "트랩을 피하고 java.lang.Object의 메소드를 올바르게 재정의하는 방법"에서 Mark Roulo는 객체를 복제하는 방법과 딥 복사 대신 얕은 복사를 달성하는 방법을 설명합니다. 여기서 간단히 요약하면, 포함 된 개체없이 개체를 복사 할 때 단순 복사가 발생합니다. 도 1은 객체를 설명하기 위해 obj1, 즉 두 개의 개체를 포함 containedObj1하고 containedObj2.

에서 단순 복사가 수행 obj1되면 그림 2와 같이 복사되지만 포함 된 객체는 복사되지 않습니다.

전체 복사는 개체가 참조하는 개체와 함께 복사 될 때 발생합니다. 그림 3은 전체 obj1복사가 수행 된 후를 보여줍니다 . 있다뿐만 아니라 obj1복사,하지만 그 안에 포함 된 개체가 아니라 복사 한.

이러한 포함 된 개체 중 하나에 개체가 포함되어 있으면 전체 그래프가 순회 및 복사 될 때까지 전체 복사에서 해당 개체도 복사됩니다. 각 개체는 해당 clone()메서드 를 통해 자체 복제를 담당합니다 . clone()에서 상속 된 기본 메서드 Object는 개체의 얕은 복사본을 만듭니다. 전체 복사를 수행하려면 포함 된 모든 개체의 clone()메서드 를 명시 적으로 호출하고 포함 된 개체의 메서드를 차례로 호출하는 추가 논리를 추가해야합니다 clone(). 이를 수정하는 것은 어렵고 시간이 많이 소요될 수 있으며 거의 ​​재미가 없습니다. 상황을 더욱 복잡하게 만들기 위해 객체를 직접 수정할 수없고 해당 clone()메서드가 얕은 복사본을 생성하면 클래스를 확장해야합니다.clone()메서드를 재정의하고이 새 클래스는 이전 클래스 대신 사용됩니다. (예를 들어, Vector은 ( 는) 딥 복사에 필요한 로직을 포함하지 않습니다.) 런타임까지 객체를 딥 복사 할 것인지 얕은 복사 할 것인지에 대한 질문을 연기하는 코드를 작성하려면 훨씬 더 복잡한 상태. 이 경우 각 객체에 대해 두 개의 복사 함수가 있어야합니다. 하나는 깊은 복사 용이고 다른 하나는 얕은 복사 용입니다. 마지막으로 딥 복사중인 객체에 다른 객체에 대한 여러 참조가 포함되어 있더라도 후자의 객체는 한 번만 복사해야합니다. 이것은 객체의 확산을 방지하고 순환 참조가 무한 루프를 생성하는 특별한 상황을 방지합니다.

직렬화

1998 년 1 월, JavaWorld 는 "고정 건조 된 JavaBeans를 사용하여 'Nescafé'방식으로 수행하십시오."라는 직렬화에 대한 기사로 Mark Johnson 의 JavaBeans 칼럼을 시작했습니다 . 요약하면 직렬화는 객체 그래프 (단일 객체의 퇴화 케이스 포함)를 동일한 객체 그래프로 되돌릴 수있는 바이트 배열로 변환하는 기능입니다. 객체는 경우 또는 조상 구현 중 하나 직렬화이라고합니다 java.io.Serializablejava.io.Externalizable. 직렬화 가능한 개체는 개체의 writeObject()메서드 에 전달하여 직렬화 할 수 있습니다 ObjectOutputStream. 이것은 객체의 기본 데이터 유형, 배열, 문자열 및 기타 객체 참조를 작성합니다. 그만큼writeObject()그런 다음 참조 된 개체에서 메서드를 호출하여 직렬화합니다. 또한, 이러한 객체의 각각이 자신의 참조와 객체 직렬화; 이 프로세스는 전체 그래프가 순회되고 직렬화 될 때까지 계속됩니다. 익숙한 것 같습니까? 이 기능을 사용하여 전체 복사를 수행 할 수 있습니다.

직렬화를 사용한 전체 복사

직렬화를 사용하여 전체 복사를 만드는 단계는 다음과 같습니다.

  1. 객체 그래프의 모든 클래스가 직렬화 가능한지 확인합니다.

  2. 입력 및 출력 스트림을 만듭니다.

  3. 입력 및 출력 스트림을 사용하여 개체 입력 및 개체 출력 스트림을 만듭니다.

  4. 복사 할 개체를 개체 출력 스트림에 전달합니다.

  5. 개체 입력 스트림에서 새 개체를 읽고 보낸 개체의 클래스로 다시 캐스팅합니다.

ObjectCloner2 ~ 5 단계를 구현 하는 클래스를 작성했습니다 . "A"로 표시된 라인은 온라인 B 라인 ByteArrayOutputStream을 만드는 데 사용되는 a 를 설정합니다 ObjectOutputStream. 라인 C는 마법이 이루어지는 곳입니다. 이 writeObject()메서드는 객체의 그래프를 재귀 적으로 탐색하고 바이트 형식으로 새 객체를 생성 한 다음 ByteArrayOutputStream. 라인 D는 전체 개체가 전송되었는지 확인합니다. 그런 다음 E 행의 코드 ByteArrayInputStream는를 만들고 ByteArrayOutputStream. 라인 F 는 라인 E ObjectInputStream에서 ByteArrayInputStream생성 된를 사용하여 인스턴스화 하고 객체는 역 직렬화되고 라인 G의 호출 메서드로 반환됩니다. 코드는 다음과 같습니다.

import java.io. *; import java.util. *; import java.awt. *; public class ObjectCloner {// 아무도 ObjectCloner 객체를 실수로 만들 수 없도록 private ObjectCloner () {} // 객체의 전체 복사본을 반환합니다. static public Object deepCopy (Object oldObj) throws Exception {ObjectOutputStream oos = null; ObjectInputStream ois = null; try {ByteArrayOutputStream bos = new ByteArrayOutputStream (); // A oos = new ObjectOutputStream (bos); // B // 객체 직렬화 및 전달 oos.writeObject (oldObj); // C oos.flush (); // D ByteArrayInputStream bin = new ByteArrayInputStream (bos.toByteArray ()); // E ois = new ObjectInputStream (bin); // F // 새 객체를 반환합니다. return ois.readObject (); // G} catch (Exception e) {System.out.println ( "Exception in ObjectCloner ="+ e); 던지기 (e); } 마지막으로 {oos.close (); ois.close (); }}}

에 대한 액세스 권한이있는 모든 개발자 ObjectCloner는이 코드를 실행하기 전에해야 할 일이 남아 있습니다. 개체 그래프의 모든 클래스가 직렬화 가능한지 확인해야합니다. 대부분의 경우이 작업은 이미 수행되어야합니다. 그렇지 않다면 소스 코드에 접근하는 것이 상대적으로 쉬워야합니다. JDK에있는 대부분의 클래스는 직렬화 가능합니다. 과 같이 플랫폼에 의존 FileDescriptor하는 것만 그렇지 않습니다. 또한 JavaBean과 호환되는 타사 공급 업체에서 가져온 모든 클래스는 정의상 직렬화 가능합니다. 물론 직렬화 가능한 클래스를 확장하면 새 클래스도 직렬화 가능합니다. 이러한 모든 직렬화 가능한 클래스가 떠 다니는 상황에서 직렬화해야하는 유일한 클래스는 자신의 것입니다. 이것은 각 클래스를 통과하고 덮어 쓰는 것과 비교할 때 케이크 조각입니다.clone() 깊은 복사를합니다.

객체의 그래프에 직렬화 할 수없는 클래스가 있는지 확인하는 쉬운 방법은 모두 직렬화 가능하다고 가정하고 ObjectClonerdeepCopy()메소드를 실행 하는 것입니다. 클래스를 직렬화 java.io.NotSerializableException할 수 없는 객체 가 있으면 어떤 클래스가 문제를 일으켰는지 알려주는 a 가 발생합니다.

빠른 구현 예가 아래에 나와 있습니다. 그것은 간단한 객체 생성 v1A는, Vector가 포함를 Point. 이 개체는 그 내용을 표시하기 위해 인쇄됩니다. 그런 다음 원본 객체 v1는 새 객체으로 복사되며,이 객체 는와 vNew동일한 값이 포함되어 있음을 보여주기 위해 인쇄됩니다 v1. 다음으로의 내용 v1이 변경되고 마지막으로 v1및 둘 다 vNew인쇄되어 값을 비교할 수 있습니다.

import java.util. *; import java.awt. *; public class Driver1 {static public void main (String [] args) {try {// 명령 줄에서 메소드 가져 오기 String meth; if ((args.length == 1) && ((args [0] .equals ( "deep")) || (args [0] .equals ( "shallow")))) {meth = args [0]; } else {System.out.println ( "사용법 : java Driver1 [deep, shallow]"); 반환; } // 원본 객체 생성 Vector v1 = new Vector (); 포인트 p1 = new Point (1,1); v1.addElement (p1); // 그것이 무엇인지 확인 System.out.println ( "Original ="+ v1); 벡터 vNew = null; if (meth.equals ( "deep")) {// 전체 복사 vNew = (Vector) (ObjectCloner.deepCopy (v1)); // A} else if (meth.equals ( "shallow")) {// 얕은 복사 vNew = (Vector) v1.clone (); // B} // 동일한 지 확인 System.out.println ( "New ="+ vNew);// 원본 객체의 내용 변경 p1.x = 2; p1.y = 2; // 이제 각 항목에 무엇이 있는지 확인 System.out.println ( "Original ="+ v1); System.out.println ( "New ="+ vNew); } catch (Exception e) {System.out.println ( "Exception in main ="+ e); }}}

전체 복사 (라인 A)를 호출하려면을 실행 java.exe Driver1 deep합니다. 전체 복사가 실행되면 다음과 같은 출력이 표시됩니다.

원본 = [java.awt.Point [x = 1, y = 1]] 신규 = [java.awt.Point [x = 1, y = 1]] 원본 = [java.awt.Point [x = 2, y = 2]] 신규 = [java.awt.Point [x = 1, y = 1]] 

이것은 원본 Point, p1이 변경 되었을 때 Point전체 그래프가 복사 되었기 때문에 전체 복사의 결과로 생성 된 새 항목 이 영향을받지 않았 음을 보여줍니다 . 비교를 위해를 실행하여 얕은 복사본 (라인 B)을 호출합니다 java.exe Driver1 shallow. 얕은 복사가 실행되면 다음과 같은 출력이 표시됩니다.

원본 = [java.awt.Point [x = 1, y = 1]] 신규 = [java.awt.Point [x = 1, y = 1]] 원본 = [java.awt.Point [x = 2, y = 2]] 신규 = [java.awt.Point [x = 2, y = 2]] 

This shows that when the original Point was changed, the new Point was changed as well. This is due to the fact that the shallow copy makes copies only of the references, and not of the objects to which they refer. This is a very simple example, but I think it illustrates the, um, point.

Implementation issues

Now that I've preached about all of the virtues of deep copy using serialization, let's look at some things to watch out for.

The first problematic case is a class that is not serializable and that cannot be edited. This could happen, for example, if you're using a third-party class that doesn't come with the source code. In this case you can extend it, make the extended class implement Serializable, add any (or all) necessary constructors that just call the associated superconstructor, and use this new class everywhere you did the old one (here is an example of this).

This may seem like a lot of work, but, unless the original class's clone() method implements deep copy, you will be doing something similar in order to override its clone() method anyway.

The next issue is the runtime speed of this technique. As you can imagine, creating a socket, serializing an object, passing it through the socket, and then deserializing it is slow compared to calling methods in existing objects. Here is some source code that measures the time it takes to do both deep copy methods (via serialization and clone()) on some simple classes, and produces benchmarks for different numbers of iterations. The results, shown in milliseconds, are in the table below:

Milliseconds to deep copy a simple class graph n times
Procedure\Iterations(n) 1000 10000 100000
clone 10 101 791
serialization 1832 11346 107725

As you can see, there is a large difference in performance. If the code you are writing is performance-critical, then you may have to bite the bullet and hand-code a deep copy. If you have a complex graph and are given one day to implement a deep copy, and the code will be run as a batch job at one in the morning on Sundays, then this technique gives you another option to consider.

Another issue is dealing with the case of a class whose objects' instances within a virtual machine must be controlled. This is a special case of the Singleton pattern, in which a class has only one object within a VM. As discussed above, when you serialize an object, you create a totally new object that will not be unique. To get around this default behavior you can use the readResolve() method to force the stream to return an appropriate object rather than the one that was serialized. In this particular case, the appropriate object is the same one that was serialized. Here is an example of how to implement the readResolve() method. You can find out more about readResolve() as well as other serialization details at Sun's Web site dedicated to the Java Object Serialization Specification (see Resources).

One last gotcha to watch out for is the case of transient variables. If a variable is marked as transient, then it will not be serialized, and therefore it and its graph will not be copied. Instead, the value of the transient variable in the new object will be the Java language defaults (null, false, and zero). There will be no compiletime or runtime errors, which can result in behavior that is hard to debug. Just being aware of this can save a lot of time.

The deep copy technique can save a programmer many hours of work but can cause the problems described above. As always, be sure to weigh the advantages and disadvantages before deciding which method to use.

Conclusion

복잡한 개체 그래프의 딥 카피를 구현하는 것은 어려운 작업 일 수 있습니다. 위에 표시된 기술 clone()은 그래프의 모든 개체에 대해 방법을 덮어 쓰는 기존 절차에 대한 간단한 대안 입니다.

Dave Miller는 컨설팅 회사 인 Javelin Technology의 선임 아키텍트로서 Java 및 인터넷 애플리케이션을 담당하고 있습니다. 그는 Hughes, IBM, Nortel 및 MCIWorldcom과 같은 회사에서 객체 지향 프로젝트에서 일했으며 지난 3 년 동안 Java로 독점적으로 작업했습니다.

이 주제에 대해 더 알아보기

  • Sun의 Java 웹 사이트에는 Java Object Serialization Specification 전용 섹션이 있습니다.

    //www.javasoft.com/products/jdk/1.2/docs/guide/serialization/spec/serialTOC.doc.html

이 이야기, "Java Tip 76 : 딥 카피 기술의 대안"은 원래 JavaWorld에 의해 출판되었습니다.