본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 13 - clone 재정의는 주의해서 진행하라 - 완벽 공략

by 개발인생 2022. 10. 28.
반응형

아이템 13 - clone 재정의는 주의해서 진행하라 - 완벽 공략

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

UncheckedException

우리는 보통 UncheckedException 을 선호햔다.

public class MyException extends RuntimeException {
}

UncheckedException 은 RuntimeException 을 상속 받은 Exception 이다.

public class MyException extends Error {
}

Error 를 상속받은 클래스들 역시 UncheckedException 이라고 부를 수 있다.

RuntimeExceptionError 를 확장한 클래스들은 UncheckedException 이다.

우리는 왜 UncheckedException 을 선호할까?

public class MyApp {

   public void hello(String name)  {
      throw new MyException();
   }

   public static void main(String[] args) {
      MyApp myApp = new MyApp();
      myApp.hello("junhyun");
   }
}

UncheckedException 던지는 코드는 작성이 쉽다.

호출을 하더라도 사용하는 쪽에서 조치를 할 필요가 없다.

만약 CheckedException 이라면

public class MyException extends Exception {
}
public class MyApp {

   public void hello(String name)  {
      throw new MyException();
   }

   public static void main(String[] args) {
      MyApp myApp = new MyApp();
      try {
         myApp.hello("푸틴");
      } catch (MyException e) {
         e.printStackTrace();
      }
   }
}

해당 코드를 호출하는 쪽에서 try-catch 블럭을 사용하거나

   public void hello(String name) throws MyException {
      throw new MyException();
   }

throws 를 사용해 에러를 던져야한다.

만약 throws 를 하게되면 해당 코드를 받는 쪽에서 다시한번 처리를 해주어야한다.

이러한 번거로움 떄문에 UncheckedException 을 선호하게 된다.

하지만 이러한 이유만으로 UncheckedException 사용을 결정하는 건 잘못된 선택이다.

절대 편리하다는 이유만으로 UncheckedException 을 사용하면 안된다.

CheckedExceptiontry-catchthrows 가 있는 이유는

CheckedException 자체만으로 API 이기 떄문이다.

클라이언트에게 해당 메서드 호출 시 CheckedException 이 발생할 수 있음을 알려준다.

public class MyApp {

   /**
    *
    * @param name
    * @throws MyException
    */
   public void hello(String name) throws MyException {
      if (name.equals("푸틴")) {
         throw new MyException();
      }

      System.out.println("hello");
   }

   public static void main(String[] args) {
      MyApp myApp = new MyApp();
      try {
         myApp.hello("푸틴");
      } catch (MyException e) {
         e.printStackTrace();
      }
   }
}

위의 코드는 잘못된 값이 들어왔을 떄 예외를 던지게 된다.

잘못된 값이 들어오면 코드를 사용하는 쪽에서 대응을 할 수 있다.

무언가 복구를 하는 시도를 할 수 있도록 API 를 사용하는 쪽에 정보를 제공한다.

이떄 CheckedException문서화 가 되어 있어야한다.

public class MyApp {

   /**
    *
    * @param name
    * @throws MyException
    */
   public void hello(String name) throws MyException, NullPointerException {
      if (name.equals("푸틴")) {
         throw new MyException();
      }

      System.out.println("hello");
   }

   public static void main(String[] args) {
      MyApp myApp = new MyApp();
      try {
         myApp.hello("푸틴");
      } catch (MyException e) {
         e.printStackTrace();
      }
   }
}

위의 코드와 같이 UncheckedException 계열의 예외 역시 try-catchthrows 를 사용할 수 있다.

하지만 UncheckedException 은 복구할 수 있는 방법이 없기 때문에 try-catchthrows강제 하지 않는다.

UncheckedException 은 언제 어디서든 발생할 수 있기 때문에 try-catchthrows 를 사용하면

비효율적인 동시에 프로그램의 명확도를 떨어뜨린다.

클라이언트 코드에서 해당 예외를 복구할 수 있는 방법 이 있다면 CheckedException 을 사용하도록 하자.


TreeSet

TreeSet 은 AbstractSet 확장해 만든 정렬된 컬렉션이다.

정렬된 컬렉션이란 데이터를 넣을때부터 정렬이 된 상태로 들어간다는 걸 뜻한다.

엘리먼트를 추가한 순서는 중요하지 않고 기본적으로 오름차순 으로 정렬한다.

데이터를 넣을 때 엘리먼트가 지닌 자연적인 순서(natural order) 를 사용해 정렬한다.

자바에서 제공하는 기본적인 타입들에는 자연적인 순서(natural order) 가 구현되어있다.

만약 우리가 생성한 클래스같은 경우에는 우리가 자연적인 순서(natural order) 를 정의해주어야한다.

자연적인 순서(natural order) 정의는 Comparable 인터페이스를 통해 구현한다.

TreeSet 은 Thread-safety 하지 않으므로 다중 스레드 환경에서 정렬된 컬렉션을 사용하고 싶다면

Collections 를 통해 Thread-safety 한 Set 을 얻어오면 된다.

public class TreeSetExample {

   public static void main(String[] args) {
        TreeSet<Integer> numbers = new TreeSet<>();
        numbers.add(10);
        numbers.add(4);
        numbers.add(6);

      for (PhoneNumber number : numbers) {
         System.out.println(number);
      }
   }
}

엘리먼트를 추가한 순서와 상관없이 엘리먼트들이 정렬되어 있는 걸 확인할 수 있다.

public class TreeSetExample {

   public static void main(String[] args) {
      TreeSet<PhoneNumber> numbers = new TreeSet<>();
      phoneNumbers.add(new PhoneNumber(123, 456, 780));
      phoneNumbers.add(new PhoneNumber(123, 456, 7890));
      phoneNumbers.add(new PhoneNumber(123, 456, 789));

      for (PhoneNumber number : numbers) {
         System.out.println(number);
      }
   }
}

우리가 생성한 클래스의 인스턴스를 TreeSet 에 넣으면 에러가 발생한다.

자연적인 순서(natural order) 가 없기 때문이다.

TreeSet 에 값을 넣으면 내부적으로 Comparable 타입으로 형변환을 해 자연적인 순서(natural order) 를 알아내려한다.

우리가 생성한 클래스에는 Comparable 이 구현되어 있지 않기 때문에 에러가 발생한다.

우리가 생성한 클래스에 Comparable 를 구현하거나

public class TreeSetExample {

   public static void main(String[] args) {
      TreeSet<PhoneNumber> numbers = new TreeSet<>(Comparator.comparingInt(PhoneNumber::hashCode));
      Set<PhoneNumber> phoneNumbers = Collections.synchronizedSet(numbers);
      phoneNumbers.add(new PhoneNumber(123, 456, 780));
      phoneNumbers.add(new PhoneNumber(123, 456, 7890));
      phoneNumbers.add(new PhoneNumber(123, 456, 789));

      for (PhoneNumber number : numbers) {
         System.out.println(number);
      }
   }
}

TreeSet 생성시 자연적인 순서(natural order) 를 넘겨주는 방법이 있다.

TreeSet<PhoneNumber> numbers = new TreeSet<>(Comparator.comparingInt(PhoneNumber::hashCode));

Comparator.comparingInt() 를 통해 자연적인 순서(natural order) 를 넘겨주는 방법이다.

public class TreeSetExample {

   public static void main(String[] args) {
      TreeSet<PhoneNumber> numbers = new TreeSet<>(Comparator.comparingInt(PhoneNumber::hashCode));
      Set<PhoneNumber> phoneNumbers = Collections.synchronizedSet(numbers);

      phoneNumbers.add(new PhoneNumber(123, 456, 780));
      phoneNumbers.add(new PhoneNumber(123, 456, 7890));
      phoneNumbers.add(new PhoneNumber(123, 456, 789));

      for (PhoneNumber number : numbers) {
         System.out.println(number);
      }
   }
}
Set<PhoneNumber> phoneNumbers = Collections.synchronizedSet(numbers);

Collections 에 있는 synchronizedSet 을 통해 Thread-safety 한 Set 을 얻을 수 있다.


HashSet 은 내부적으로 이진 검색 트리 중 레드 블랙 트리 를 사용한다.

데이터를 넣거나 꺼낼 때 O(logN) 만큼의 시간이 걸린다.

TreeSet 은 내부적으로 TreeMap 을 사용한다.

TreeMap정렬된 Map 이다.

반응형

댓글