Java에서 개체의 전체 복사본을 만드는 방법

1. 소개

Java에서 객체를 복사하려는 경우 고려해야 할 두 가지 가능성이 있습니다. 얕은 복사와 깊은 복사입니다.

단순 복사는 필드 값만 복사 할 때의 접근 방식이므로 복사가 원본 객체에 종속 될 수 있습니다. 전체 복사 방식에서는 트리의 모든 개체가 전체적으로 복사되었는지 확인하므로 복사본이 변경 될 수있는 이전의 기존 개체에 종속되지 않습니다.

이 기사에서는이 두 가지 접근 방식을 비교하고 딥 카피를 구현하는 네 가지 방법을 배웁니다.

2. Maven 설정

세 가지 Maven 종속성 (Gson, Jackson 및 Apache Commons Lang)을 사용하여 딥 복사를 수행하는 다양한 방법을 테스트합니다.

이러한 종속성을 pom.xml에 추가해 보겠습니다 .

 com.google.code.gson gson 2.8.2   commons-lang commons-lang 2.6   com.fasterxml.jackson.core jackson-databind 2.9.3 

Gson, Jackson 및 Apache Commons Lang의 최신 버전은 Maven Central에서 찾을 수 있습니다.

3. 모델

Java 객체를 복사하는 다른 방법을 비교하려면 작업 할 두 개의 클래스가 필요합니다.

class Address { private String street; private String city; private String country; // standard constructors, getters and setters }
class User { private String firstName; private String lastName; private Address address; // standard constructors, getters and setters }

4. 얕은 복사

얕은 복사본은 한 개체에서 다른 개체로 필드 값만 복사 하는 것입니다.

@Test public void whenShallowCopying_thenObjectsShouldNotBeSame() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User shallowCopy = new User( pm.getFirstName(), pm.getLastName(), pm.getAddress()); assertThat(shallowCopy) .isNotSameAs(pm); }

이 경우 pm! = shallowCopy , 이는 서로 다른 객체 임을 의미 하지만 문제는 원래 address ' 속성을 변경 하면 shallowCopy 의 주소 에도 영향을 미친다는 것 입니다.

Address 가 불변 이면 신경 쓰지 않을 것이지만, 그렇지 않습니다.

@Test public void whenModifyingOriginalObject_ThenCopyShouldChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User shallowCopy = new User( pm.getFirstName(), pm.getLastName(), pm.getAddress()); address.setCountry("Great Britain"); assertThat(shallowCopy.getAddress().getCountry()) .isEqualTo(pm.getAddress().getCountry()); }

5. 딥 카피

딥 카피는이 문제를 해결하는 대안입니다. 그것의 장점은 객체 그래프 에서 최소한 각 변경 가능한 객체가 재귀 적으로 복사 된다는 것입니다 .

사본은 이전에 생성 된 변경 가능한 객체에 종속되지 않기 때문에 얕은 사본에서 본 것처럼 우연히 수정되지 않습니다.

다음 섹션에서는 몇 가지 딥 카피 구현을 보여주고이 장점을 보여줄 것입니다.

5.1. 생성자 복사

구현할 첫 번째 구현은 복사 생성자를 기반으로합니다.

public Address(Address that) { this(that.getStreet(), that.getCity(), that.getCountry()); }
public User(User that) { this(that.getFirstName(), that.getLastName(), new Address(that.getAddress())); }

위의 딥 카피 구현에서는 String 이 변경 불가능한 클래스 이기 때문에 복사 생성자에 새로운 String 을 생성하지 않았습니다 .

결과적으로 실수로 수정할 수 없습니다. 이것이 작동하는지 봅시다 :

@Test public void whenModifyingOriginalObject_thenCopyShouldNotChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User deepCopy = new User(pm); address.setCountry("Great Britain"); assertNotEquals( pm.getAddress().getCountry(), deepCopy.getAddress().getCountry()); }

5.2. 복제 가능한 인터페이스

두 번째 구현은 Object 에서 상속 된 복제 메서드를 기반으로합니다 . 보호되지만 public 으로 재정의해야합니다 .

또한 마커 인터페이스 인 Cloneable 을 클래스에 추가 하여 클래스가 실제로 복제 가능함을 나타냅니다.

Address 클래스에 clone () 메서드를 추가해 보겠습니다 .

@Override public Object clone() { try { return (Address) super.clone(); } catch (CloneNotSupportedException e) { return new Address(this.street, this.getCity(), this.getCountry()); } }

이제 User 클래스에 대해 clone () 을 구현해 보겠습니다 .

@Override public Object clone() { User user = null; try { user = (User) super.clone(); } catch (CloneNotSupportedException e) { user = new User( this.getFirstName(), this.getLastName(), this.getAddress()); } user.address = (Address) this.address.clone(); return user; }

참고는 것을 super.clone에 () 를 호출하면 개체의 단순 복사본을 반환하지만 결과가 정확 그래서 우리는 수동으로 변경 가능한 필드의 전체 복사본을 설정합니다 :

@Test public void whenModifyingOriginalObject_thenCloneCopyShouldNotChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User deepCopy = (User) pm.clone(); address.setCountry("Great Britain"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }

6. 외부 라이브러리

위의 예제는 쉽게 보이지만 추가 생성자를 추가하거나 clone 메서드를 재정의 할 수없는 경우 솔루션으로 적용되지 않는 경우가 있습니다 .

이것은 우리가 코드를 소유하지 않았거나 객체 그래프가 너무 복잡해서 추가 생성자를 작성하거나 객체 그래프의 모든 클래스에 복제 메소드를 구현하는 데 집중한다면 프로젝트를 제 시간에 완료하지 못할 때 발생할 수 있습니다.

그럼 뭐야? 이 경우 외부 라이브러리를 사용할 수 있습니다. 깊은 복사본을 얻기 위해 객체를 직렬화 한 다음 새 객체로 역 직렬화 할 수 있습니다.

몇 가지 예를 살펴 보겠습니다.

6.1. Apache Commons Lang

Apache Commons Lang has SerializationUtils#clone, which performs a deep copy when all classes in the object graph implement the Serializable interface.

If the method encounters a class that isn't serializable, it'll fail and throw an unchecked SerializationException.

Because of that, we need to add the Serializable interface to our classes:

@Test public void whenModifyingOriginalObject_thenCommonsCloneShouldNotChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); User deepCopy = (User) SerializationUtils.clone(pm); address.setCountry("Great Britain"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }

6.2. JSON Serialization With Gson

The other way to serialize is to use JSON serialization. Gson is a library that's used for converting objects into JSON and vice versa.

Unlike Apache Commons Lang, GSON does not need the Serializable interface to make the conversions.

Let's have a quick look at an example:

@Test public void whenModifyingOriginalObject_thenGsonCloneShouldNotChange() { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); Gson gson = new Gson(); User deepCopy = gson.fromJson(gson.toJson(pm), User.class); address.setCountry("Great Britain"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }

6.3. Jackson을 사용한 JSON 직렬화

Jackson은 JSON 직렬화를 지원하는 또 다른 라이브러리입니다. 이 구현은 Gson을 사용하는 구현과 매우 유사하지만 기본 생성자를 클래스에 추가해야합니다 .

예를 보겠습니다.

@Test public void whenModifyingOriginalObject_thenJacksonCopyShouldNotChange() throws IOException { Address address = new Address("Downing St 10", "London", "England"); User pm = new User("Prime", "Minister", address); ObjectMapper objectMapper = new ObjectMapper(); User deepCopy = objectMapper .readValue(objectMapper.writeValueAsString(pm), User.class); address.setCountry("Great Britain"); assertThat(deepCopy.getAddress().getCountry()) .isNotEqualTo(pm.getAddress().getCountry()); }

7. 결론

딥 카피를 만들 때 어떤 구현을 사용해야합니까? 최종 결정은 종종 복사 할 클래스와 개체 그래프의 클래스 소유 여부에 따라 달라집니다.

항상 그렇듯이이 자습서의 전체 코드 샘플은 GitHub에서 찾을 수 있습니다.