자바 Cloneable

|

자바에서는 객체 클로닝을 위해 Clone() 메서드를 제공한다.

여기서 객체 클로닝이란

원본 객체의 필드값과 동일한 값을 가지는 새로운 객체를 생성을 뜻한다. (deep cloning)

deep cloning에 대한 설명은 필요한 부분에서 다시 하겠다.

방법

  1. cloneable 인터페이스를 implement 한다.

  2. Object 클래스에 존재하는 clone() 메서드를 overide 한다.

    컨벤션에 따라 접근 제어자를 ‘public’으로 오버라이딩 할 것

  3. 클래스가 clone() 메서드를 지원하지 않을 경우를 나타내기 위한 CloneNotSupportedException 처리

바로 예시를 보자.

(편의를 위해 모든 클래스는 static 클래스로 생성하였다)

1) Clone 대상이 되는 클래스


   public static class CloneClass implements Cloneable {
        private Person person;
        private String str = "String";
        private int anInt = 0;

        public CloneClass(Person person){this.person = person;}

        public String getStr() {
            return str;
        }

        public void setStr(String str) {
            this.str = str;
        }

        public int getAnInt() {
            return anInt;
        }

        public void setAnInt(int anInt) {
            this.anInt = anInt;
        }

        public Person getPerson() {
            return person;
        }

        public void setPerson(Person person){
            this.person = person;
        }

        @Override
        public CloneClass clone() throws CloneNotSupportedException {
            return (CloneClass) super.clone();
        }

        @Override
        public String toString() {
            return "CloneClass{" +
                    "person=" + person +
                    ", str='" + str + '\'' +
                    ", anInt=" + anInt +
                    '}';
        }
    }

2) CloneClass의 프로퍼티 Person 클래스


   public static class Person {
        private String name;
        private String address;

        public Person(String name, String address)
        {
            this.name = name;
            this.address = address;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", address='" + address + '\'' +
                    '}';
        }
    }


3) 실행을 위한 main 메서드


 public static void main(String[] args) throws CloneNotSupportedException {

        CloneClass original = new CloneClass(new Person("jayjays","서울"));
        CloneClass cloned = original.clone();

   			//CloneClass{person=Person{name='jayjays', address='서울'}, str='String', anInt=0}
        System.out.println(original.toString());
   			//CloneClass{person=Person{namE='jayjays', address='서울'}, str='String', anInt=0}
        System.out.println(cloned.toString());

    }

여기서 만약 cloned의 프로퍼티 Person을 변경한다면 결과는 어떻게 될까 ?


    cloned.getPerson().setName("changed");

		System.out.println(original.toString());
		//CloneClass{person=Person{name='changed', address='서울'}, str='String', anInt=0}
    System.out.println(cloned.toString());
		//CloneClass{person=Person{name='changed', address='서울'}, str='String', anInt=0}

cloned의 프로퍼티 하나만 변경했을 뿐인데 기존 객체인 original에도 변경이 일어났다.

무슨 일이 벌어진 걸까?

여기서 얕은 복제(shallow cloning)와 깊은 복제(deep cloning) 개념이 나온다.

얕은 복제란 값(value)가 복제된 것이 아니라 주소 값(reference)이 복제된 것을 의미한다.

따라서 해당 프로퍼티를 수정하면 해당 프로퍼티를 참조하는 모든 객체들에게 영향을 미친다.

  • ​ cloned.getPerson().setName(“changed”); // shallow clone

반면 깊은 복제이란 값이 복제된 것을 의미한다.

객체를 새로 생성하여 할당하므로 cloned 객체와 original 객체는 서로 독립적이다.

CloneClass 객체는 깊은 복제가 발생하였으나 (변수 original 과 cloned hashcode() 비교 )

CloneClass의 프로퍼티 Person은 얕은 복제가 일어났다.


  		System.out.println(original.hashCode()); // 1836019240
      System.out.println(cloned.hashCode()); 	 // 325040804

      System.out.println(original.getPerson().hashCode()); // 1173230247
      System.out.println(clone.getPerson().hashCode()); 	 // 1173230247

즉, 우리가 Clone() 한 객체 자체 (CloneClass) 는 deep cloning 되었으나,

그 객체의 프로퍼티 Person은 shallow cloning 된 것이다.

참조형 객체를 프로퍼티로 가지는 객체들은 위와 같이 프로퍼티가 shallow cloning 되는 부분을 처리하지 않으면

예상치 못한 문제에 부딪힐 수 있다.

이에 따른 해결 방법은 아래와 같다.

1) Person 클래스 Cloneable 인터페이스 구현


	public static class Person implements Cloneable {
        private String name;
        private String address;

        public Person(String name, String address)
        {
            this.name = name;
            this.address = address;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

				//Cloneable 인터페이스 구현
        @Override
        public Person clone() throws CloneNotSupportedException {
            return (Person) super.clone();
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", address='" + address + '\'' +
                    '}';
        }
    }



2) CloneClass clone() 메서드 수정


  public static class CloneClass implements Cloneable {
        private Person person;
        private String str = "String";
        private int anInt = 0;

        public CloneClass(Person person){this.person = person;}

        public String getStr() {
            return str;
        }

        public void setStr(String str) {
            this.str = str;
        }

        public int getAnInt() {
            return anInt;
        }

        public void setAnInt(int anInt) {
            this.anInt = anInt;
        }

        public Person getPerson() {
            return person;
        }

        public void setPerson(Person person){
            this.person = person;
        }

				//person 객체 클론
        @Override
        public CloneClass clone() throws CloneNotSupportedException {
            CloneClass c = (CloneClass) super.clone();
            c.person = person.clone();
            return c;
        }

        @Override
        public String toString() {
            return "CloneClass{" +
                    "person=" + person +
                    ", str='" + str + '\'' +
                    ", anInt=" + anInt +
                    '}';
        }
    }

3) 결과 확인


 public static void main(String[] args) throws CloneNotSupportedException {

        CloneClass original = new CloneClass(new Person("jayjays","서울")); 
   // CloneClass{person=Person{name='jayjays', address='서울'}, str='String', anInt=0}
        CloneClass clone = original.clone(); 							   
   // CloneClass{person=Person{name='jayjays', address='서울'}, str='String', anInt=0}

        System.out.println(original.toString()); 
   // CloneClass{person=Person{name='jayjays', address='서울'}, str='String', anInt=0}
        System.out.println(clone.toString()); 	 
   // CloneClass{person=Person{name='jayjays', address='서울'}, str='String', anInt=0}

        clone.getPerson().setName("changed");

        System.out.println(original.hashCode());  // 1836019240
        System.out.println(clone.hashCode()); 	  // 325040804

        System.out.println(original.getPerson().hashCode()); // 1173230247
        System.out.println(clone.getPerson().hashCode());	 // 856419764

        System.out.println(original.toString());  
   // CloneClass{person=Person{name='jayjays', address='서울'}, str='String', anInt=0}
        System.out.println(clone.toString());	  
   // CloneClass{person=Person{name='changed', address='서울'}, str='String', anInt=0}

    }

참고로 primitive type, String type 은 deep cloning 되므로 신경 쓸 필요가 없다

자바 제네릭(Generic)

|

해당 사이트 를 번역하며 공부한 내용을 정리하였습니다.

자바 제네릭에 대해 알아보자.

목차
1. 소개
2. 제네릭의 필요성
3. 제네릭 타입
4. 제네릭 메서드
5. 와일드카드('?')의 사용
6. 타입 소거자 (Type Erasure)
7. 제네릭과 원시 데이터 타입
8. 결론
  1. 소개

자바에서 제네릭(Generic)은 버그를 줄이고, 타입에 대한 추상화 계층을 추가할 목적으로 jdk 1.5 버전부터 등장하였다.

  1. 제네릭의 필요성
List list = new LinkedList();
list.add(new Integer(1)); 
Integer i = list.iterator().next(); // 컴파일 에러

컴파일러는 list.iterator().next(); 해당 라인에서 어떤 데이터 타입을 리턴할지 모르기 때문에 컴파일 에러를 일으킨다. 따라서 다음과 같이 명시적인 캐스팅이 필요하다.

Integer i = (Integer) list.iterator.next();

해당 예제의 List 타입의 list는 리턴 타입이 항상 Integer라는 보장이 없다. 단지 Object 타입이라는 것만 명백하게 알 수 있다. 그러므로 목적에 맞게 사용하려면 항상 캐스팅이 필요하게 된다.

이 것은 꽤나 번거로운 일이고, 명시적 캐스팅으로 인해 런타임에 타입 관련 에러를 발생시킬 수 있게 된다.

만약 개발자가 사용할 특정 타입을 사용할 것이라는 의도를 표현할 수 있고 컴파일러가 이러한 타입의 정확성을 보장할 수 있다면?

그것이 제네릭의 핵심 아이디어다.

List<Integer> list = new LinkedList<>();

타입을 포함한 다이아몬드 연산자(‘<>’)로 우리는 list가 오직 Integer 타입의 리스트임을 알 수 있다.

작은 프로그램에서는 이것이 사소해 보일 수 있지만 규모가 커질 수록 다이아몬드 연산자가 프로그램의 가독성을 높일 수있다.

  1. 제네릭 타입

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말한다. ex) List, HashMap<Integer,String>... 제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 "<>" 부호가 붙고, 사이에 타입 파라미터가 위치한다. 아래 코드에서 타입 파라미터의 이름은 T이다.


public class className<T> { ... }
 
public interface interfaceName<T> { ... }

  1. 제네릭 메서드

제네릭 메서드는 다음과 같은 특징을 가진다. 1) 메서드 선언부에서 리턴 타입 앞에 타입 파라미터(type에 둘러쌓인 ‘<>’연산자)를 가진다. 2) 타입 파라미터들은 바운드 될 수 있다. (뒤에 설명할 예정)

public <T> List<T> fromArrayToList(T[] a) {   
    return Arrays.stream(a).collect(Collectors.toList());
}

위 예제에서 는 제네릭 타입으로 이 메서드를 다룰 것을 암시한다. (1) 위 예졔가 리턴 타입이 void인 경우에도 는 필요하다.

만약 위의 예제에서 제네릭 타입을 하나 이상 사용하려면 다음과 같이 추가해줘야 한다.

public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

오라클 권장사항으로 제네릭 타입을 나타낼때 대문자를 사용하고 좀 더 대표적인 문자를 사용하라는 것이다. T : type K : key V : value

4-1. Bounded Generics.

위에서 언급한 것처럼, 타입 파라미터들은 바운드 될 수 있다. 바운드 된다는 것은 “제한된다”라는 것을 의미한다. 우리는 메서드 파라미터 타입을 제한 할 수 있다.

예를 들어 우리는 파라미터 타입을 1) 특정 타입(ex.Number) + 특정 타입의 subclass(ex. Integer,Long …) 2) 특정 타입 + 특정 타입의 superclass 으로 제한 할 수 있다.

  • 1): upper bound 2): lower bound

upper bound한 제네릭 메서드를 예를 들어보면 다음과 같다.

public <T extends Number> List<T> fromArrayToList(T[] a) {
    ...
}

4-2. Multiple Bounds.

타입은 또한 여러개의 upper bound를 가질 수 있다.

<T extends Number & Comparable>

첫 번째 타입은 클래스이며, 두 번째 타입은 인터페이스여야만 한다. 그렇지 않으면 컴파일 에러가 발생한다.

  1. 와일드 카드 사용

자바에서 와일드카드는 ‘?’로 나타낼 수 있으며, unknown type으로 추론된다. 와일드카드는 제네릭과 함꼐 사용할 때 유용하며 파라미터로써 사용될 수 있다. 그러나 사용할때 주의할 사항이 있다.

자바에서 ‘Object’ 는 모든 클래스들의 supertype이다. 그러나 ‘Object’의 collection(ex. List)은 어느 collection(ex.List)의 superclass가 아니다.

List는 List의 supertype이 아니므로 List 변수에 List을 할당하는 것은 컴파일 에러를 일으킨다. 이 것은 하나의 컬렉션에 여러 타입들을 더해지는 것을 막는다.

public static void paintAllBuildings(List<Building> buildings) {
    buildings.forEach(Building::paint);
}

위와 같은 메서드가 존재한다고 하자. ‘Building’의 subtype인 ‘House’가 존재한다고 하면, 우리는 이 메서드를 House의 list로는 사용할 수 없다. 만약 우리가 이 메서드를 Building 타입과 모든 subtype들이 사용하기를 원한다면 바운드된 와일드 카드가 좋은 방법이 될 수 있다.

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}
  1. 타입 소거 (Type Erasure)

제네릭은 타입 안정성을 보장하고 런타임시 오버헤드를 유발하지 않는다. 컴파일 시점에 컴파일러는 제네릭에 ‘타입 소거’(type erasure)를 한다.

타입 소거는 모든 타입 파라미터를 지우고 1) 파라미터가 바운드 되었다면, 바운드된 타입으로 replace 2) 파라미터가 언바운드 되었다면, Object type으로 replace 한다.

예시 1) 타입이 bound 되었다면, 컴파일 시점에 바운드된 타입으로 replace 된다.

public <T extends Building> void genericMethod(T t) {
    ...
}

컴파일 후

public void genericMethod(Building t) {
    ...
}

2) 타입이 unbound 되었다면, 컴파일 시점에 Object로 replace 된다.

public <T> List<T> genericMethod(List<T> list) {
    return list.stream().collect(Collectors.toList());
}

컴파일 후

// for illustration
public List<Object> withErasure(List<Object> list) {
    return list.stream().collect(Collectors.toList());
}
 
// which in practice results in
public List withErasure(List list) {
    return list.stream().collect(Collectors.toList());
}
  1. 제네릭과 원시 데이터 타입

자바에서 제네릭의 한계는 원시 타입은 타입 파라미터가 될 수 없다는 것이다.

List<int> list = new ArrayList<>();
list.add(17);

왜 원시타입이 안돼는 지 살펴보자. 제네릭은 ‘compile-time feature’ 이다. 즉, 타입 파라미터는 지워지고 모든 제네릭 타입들은 Object를 상속받는다.

좀 더 쉽게 예시를 들어보자.

List<Integer> list = new ArrayList<>();
list.add(17);

list의 메서드 add를 살펴보자.

boolean add(E e);

이 메서드는 다음과 같이 컴파일 될 것이다.

boolean add(Object e);

그러므로 타입 파라미터들은 반드시 Object로 변환가능 해야 한다. 따라서 원시타입은 타입 파라미터로 사용할 수 없다.

  1. 결론

제네릭은 자바 언어에서 강력한 기능으로서, 개발자들의 작업을 쉽게 만들고 에러 가능성을 낮춰 준다. 제네릭은 또한 컴파일 시점에 타입 정확성을 높이며 무엇보다도 추가적인 오버헤드 없이 제네릭 알고리즘을 상속할 수 있게 해주는 유용한 기능이므로 잘 사용하자 !

끝.