6장 들어가기 : 열거 타입과 Annotation

• 자바에는 특수한 목적의 참조 타입이 두가지 있다.

    ◦  클래스의 일종인 열거 타입 ( Enum )

    ◦  인터페이스의 일종인 Annotation

• 6장에서 이 타입들을 올바르게 사용하는 방법을 알아보자!


정수 열거 패턴 (int enum pattern)

•열거 타입이란?

    ◦  일정 개수의 상수 값을 정의한 다음 그 외의 값은 허용하지 않는 타입

    ◦  열거 타입으로 만들기 좋은 개념들

          -  사계절 (봄-여름-가을-겨울)

          -  요일 (월-화-수-목-금-토-일)

          -  카드 게임의 카드 종류

 

•열거 타입이 등장하기 전, 정수 열거 패턴

    ◦  자바에서 열거 타입을 지원하기 전에는 아래처럼 정수 상수를 선언해서 사용하곤 했다.

// 정수 열거 패턴(int enum pattern)이라 불리기도 한다.
public static final int MINT_FLAVOR = 3;
public static final int MINT_MILK = 4;

public static final int CHOCOLATE_FLAVOR = 1;
public static final int CHOCOLATE_MILK = 2;

    ◦  민트용 상수의 이름은 모두 MINT_ 로 시작하고 초콜릿용 상수는 모두 CHOCOLATE_ 으로 시작한다.

          -  MINT 도 FLAVOR와 MILK 를 상수 이름으로 사용하고 싶고,
             CHCOLATE 도 FLAVOR와 MILK 를 상수 이름으로 사용하고 싶은 경우 위 처럼 접두어를 이용해 충돌을 방지해줘야 한다.

 

정수 열거 패턴의 단점

    ◦  타입 안전을 보장할 방법이 없으며 표현력도 좋지 않다.

          -  예) 초콜렛을 사용해야 할 상황에서 실수로 민트를 사용해도 컴파일러는 아무런 경고를 출력하지 않는다.

// 아 민트초코 싫다고!
int doubleChoco = MINT_FLAVOR + CHOCOLATE_FLAVOR

    ◦  정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다.

          -  깨지기 쉽다 = 어느 하나를 수정하면 다른 곳에서도 수정을 해줘야 한다.

          -  평범한 상수를 나열한 것 뿐이라 컴파일하면 그 값이 클래스 파일에 그대로 새겨진다.

    ◦  즉 상수의 값이 변경되면 다시 컴파일해줘야 한다. 그렇지 않으면 프로그램이 수정 전의 값으로 실행됨

    ◦  값을 출력하거나 로그를 찍을 때 변수명은 확인할 수 없으니, 단지 숫자로만 출력되는 값의 의미를 파악하기 어렵다.

    ◦  같은 정수 열거 패턴 그룹에 총 몇개의 상수가 있는지 알기 위해선 일일이 세어줘야 한다.

 

문자열 열거 패턴 (String Enum Pattern)

    ◦  정수 대신 문자열 상수를 사용하는 변형 패턴이다.

    ◦  상수의 의미를 출력할 수 있다는 점은 좋지만,
         문자열 상수의 이름 대신 문자열 값을 그대로 하드코딩하게 만들기 때문에 좋은 방법이 아니다.

    ◦  하드코딩한 문자열에 오타가 있어도 컴파일러는 확인할 길이 없으니 런타임 버그가 생긴다.

    ◦  문자열 비교에 따른 성능 저하가 발생한다.


Java 열거 타입(Enum)의 등장

• Java 열거 타입

    ◦  자바는 정수/문자열 열거 패턴의 단점을 말끔히 해결해주는 대안으로 열거 타입을 제시했다.

    ◦  public enum Mint { FLAVOR, MILK } public enum Chocolate { FLAVOR, MILK }

    ◦  자바의 열거 타입은 완전한 형태의 클래스이기 때문에 단순히 정수값이기만 한 다른 언어의 열거 타입보다 훨씬 강력하다.

 

• Java 열거 타입 구현 원리

    ◦  열거 타입 자체는 클래스이다.

    ◦  상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.

    ◦  밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final 이다.

    ◦  외부에서 열거 타입 인스턴스를 직접 생성하거나 확장할 수 없으므로, 만들어진 인스턴스는 단 하나만 존재함이 보장된다(싱글톤).

 

• 열거 타입의 장점

    ◦  열거 타입은 컴파일타임 타입 안전성을 제공한다.

          -  mintTest 메소드의 인수는 Mint의 두가지 값중 하나임이 확실하며, 다른 타입의 값을 넘기려 하면 컴파일 오류가 난다.

    ◦  열거 타입에는 각자의 이름공간이 있어서 이름이 같은 상수도 공존할 수 있다.

    ◦  새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일 할 필요가 없다.

          -  공개되는 것은 오직 필드의 이름 뿐이라 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문

    ◦  .toString() 메소드를 사용하면 상수 이름을 출력할 수 있다.

 

• 열거 타입의 추가적인 기능과 특징

    ◦  열거 타입에는 메서드나 필드를 추가할 수 있고, 열거 타입이 인터페이스를 구현하게 할 수도 있다.

          -  언제 열거 타입에서 메서드나 필드를 추가하여 사용할까?
              → 각 상수와 연관된 로직 또는 데이터까지 열거 타입 클래스 안에서 다루고 싶을 때

          -  예) Mint ICECREAM과 MILK를 함께 구매 했을때의 가격을 알고 싶다.

          -  열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.

    ◦  열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드인 values를 제공한다.

          -  예) 민트맛 음식 다 주세요

    ◦  열거 타입은 근본적으로 불변이라 모든 필드는 final 이어야 한다.

 

• 열거 타입과 private/package-private 메서드

    ◦  열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private 메서드 또는 package-private 메서드로 구현하는 것이 좋다.

    ◦  이렇게 구현된 기능은 해당 열거 타입 상수를 선언한 클래스/패키지에서만 사용할 수 있다.

    ◦  즉 일반 클래스와 마찬가지로 기능을 외부에 노출해야할 합당한 이유가 없다면

         private/package-private으로 선언하라 (Item15 : 클래스와 멤버의 접근 권한을 최소화하라)

 

• 열거 타입과 클래스 레벨

    ◦  널리 쓰이는 열거 타입은 top-level 클래스로 만들고, 특정 클래스에서만 사용 되는 열거 타입은 해당 클래스의 멤버 클래스로 만들자.

    ◦  이러한 규칙을 지향함으로써 다양한 API가 더 일관된 모습을 갖출 수 있게 되었다!


열거 타입 사용의 확장

• 상수마다 적용되는 비즈니스 로직이 달라져야 하는 상황이 있을수도 있다.

    ◦  예) 계산기의 연산 종류를 열거 타입으로 선언하는 경우

 

• switch-case 문을 사용하는 경우

enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    
    public double apply(double x, double y) {
        switch(this) {
            case PLUS: return x + y;
            case MINUS: return x - y;
            case TIMES: return x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("알 수 없는 연산: " + this);
    }
}

    ◦  동작은 하지만 가독성이 좋지 않을 뿐더러 깨지기 쉬운 코드이다.

          -  새로운 상수가 추가되면 새로운 case 문을 추가해야 한다.

          -  새로운 case 문을 추가하는것을 깜빡하고 프로그램을 실행시키는 경우, 컴파일은 되지만 새로 추가한 연산을 수행하려 하면

              AssertionError가 발생할 것이다.

 

• 상수별 메서드 구현 방법 : 추상 메서드를 사용하는 경우

public enum Operation {
  PLUS {public double apply(double x, double y) {return x + y;}},
  MINUS {public double apply(double x, double y) {return x - y;}},
	TIMES {public double apply(double x, double y) {return x * y;}},
  DIVIDE {public double apply(double x, double y) {return x / y;}};

  public abstract double apply(double x, double y);
}

    ◦  하나의 추상 메서드를 선언하고, 각 상수마다 자신에 맞게 해당 메서드를 재정의하여 사용하는 방법

    ◦  메서드 재정의와 상수 선언이 나란히 붙어 있으니 새로운 상수를 추가할 때 메서드 재정의를 깜빡하기 어려울 뿐더러,

         추상 메서드이므로 재정의 하지 않으면 컴파일 오류가 난다.

    ◦  상수별 메서드 구현을 상수별 데이터와 결합할 수도 있다.

public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) {return x + y;}
    },
    MINUS("-") {
        public double apply(double x, double y) {return x - y;}
    },
    TIMES("*") {
        public double apply(double x, double y) {return x * y;}
    },
    DIVIDE("/") {
        public double apply(double x, double y) {return x * y;}
    };

		private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    @Override
    public String toString() { return symbol; }
    public abstract double apply(double x, double y);

          -  toString을 재정의하여 상수값 이름 대신 연산 기호를 반환하도록 만들어 주었다.

          -  계산식을 출력하는 것이 훨씬 간편해졌다!

          -  단, 열거 타입의 toString 메서드를 재정의하려거든, toString 이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는

              fromString 메서드도 함께 제공하는것을 고려해보자.

private static final Map<String, Operation> stringToEnum =
		Stream.of(values()).collect(Collectors.toMap(Object::toString, e -> e));

// 지정된 문자열에 해당하는 Operation이 존재하면 반환
public static Optional<Operation> fromString(String symbol) {
    return Optional.ofNullable(stringToEnum.get(symbol));
}

    ◦  상수별 메서드 구현에서 열거 타입을 다룰 때 주의할 점

          -  열거 타입의 static 필드 중 열거 타입 생성자에서 접근할 수 있는 것은 상수 변수 뿐이다.

          -  열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화되기 전 이기 때문

          -  열거 타입 생성자에서 자기 자신 뿐만 아니라 같은 열거 타입 내부의 다른 상수에도 접근할 수 없다.

    ◦  fromString 을 사용할 때 주의할 점

          -  fromString 의 반환 타입은 Optional<Operation>이다.

          -  주어진 문자열이 가리키는 연산이 존재하지 않을 수 있음을 알리기 위함

    ◦  상수별 메서드 구현의 단점

          -  열거 타입 상수끼리 코드를 공유하기 어렵다.

          -  즉, 동일한 연산을 하지만 모양만 다른 상수가 있다고 할 때, 하나의 상수에서 먼저 정의한 연산을 가져다가 사용할 수 없다.

             (동일한 연산을 수행하는 코드를 두 번 작성해 주어야 함)(재사용성 BAD)

enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

	private static final int MINS_PER_SHIFT = 8 * 60; // 하루 8시간

	int pay(int minutesWorked, int payRate) {
    int basePay = minutesWorked * payRate;
		int overtimePay;

		switch(this) {
			case SATURDAY:
			case SUNDAY: // 주말
				overtimePay = basePay / 2;
				break;
			default: // 주중
				if (minutesWorked <= MINS_PER_SHIFT) overtimePay = 0;
				else overtimePay = (minutesWorked-MINS_PER_SHIFT) * payRate / 2;
		}

		return basePay + overtimePay;

	}
}

          -  ‘공휴일’ 을 나타내는 상수를 열거 타입에 추가하려면, 해당 값을 처리하는 case 문도 추가해야 한다.

 

• 전략 열거 타입 패턴을 사용하는 방법

    ◦  위 상황처럼 열거 타입 상수 일부가 같은 동작을 공유할 때 사용하기 좋은 방법이다.

    ◦  새로운 상수를 추가할 때, 해당 상수가 취할 동작인 전략을 선택할 수 있게 하는 것

enum PayrollDay {
    MONDAY(WEEKDAY),
    TUESDAY(WEEKDAY),
    WEDNESDAY(WEEKDAY),
    THURSDAY(WEEKDAY),
    FRIDAY(WEEKDAY),
    SATURDAY(WEEKEND),
    SUNDAY(WEEKEND);

  private final PayType payType;

  PayrollDay(PayType payType) { this.payType = payType; }

  int pay(int minutesWorked, int payRate) {
      return payType.pay(minutesWorked, payRate);
  }

  enum PayType {
      WEEKDAY {
          int overtimePay(int minsWorked, int payRate) {
              return minsWorked <= MINS_PER_SHIFT ? 0 :
                      (minsWorked - MINS_PER_SHIFT) * payRate / 2;
          }
      },

      WEEKEND {
          int overtimePay(int minsWorked, int payRate) {
              return minsWorked * payRate / 2;
          }
      };

      abstract int overtimePay(int mins, int payRate);
      private static final int MINS_PER_SHIFT = 8 * 60;

      int pay(int minsWorked, int payRate) {
          int basePay = minsWorked * payRate;
          return basePay + overtimePay(minsWorked, payRate);
      }
  }

  public static void main(String[] args) {
      for (PayrollDay day : values())
          System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
  }
}

          -  휴일근무수당 계산을 private 중첩 열거 타입(PayType)으로 옮기고, PayrollDay 열거 타입의 생성자에서 이중 적당한 것을

             선택한다.

          -  그러면 PayrollDay 열거타입은 휴일근무수당 계산을 해당 전략 열거 타입에 위임하여 switch 문이나 상수별 메서드 구현이

              필요 없게 된다.

          -  이 패턴은 switch 문보다 복잡하지만 더 안전하고 유연하다.

    ◦  switch 문을 잘 활용하는 방법

          -  switch 문은 열거 타입의 상수별 동작을 구현하는데에는 적합하지 않지만 기존 열거 타입에 상수별 동작을 혼합해 넣을 때,

             혹은 가끔 쓰이지만 열거 타입 안에 포함시킬만큼 유용하지는 않은 경우 등에는 좋은 선택이 될 수 있다.


열거 타입을 언제 사용해야 할까?

• 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 열거 타입으로 구현하자.

    ◦  예) 태양계 행성, 요일, 체스 말 등

 

• 허용하는 값들을 컴파일 타임에 이미 알고 있을 때에도 사용할 수 있다.

    ◦  예) 메뉴 아이템, 연산 코드, 명령줄 플래그 등

 

• 열거 타입에 정의된 상수 개수가 고정 불변일 필요는 없다.

    ◦  열거 타입은 나중에 상수가 추가되어도 바이너리 수준에서 호환되도록 설계되었기 때문

  •  

제네릭 타입의 클래스를 생성하는 것

• Java JDK에서 제공되는 제네릭 타입의 클래스 메소드를 사용하는 것은 크게 어렵지 않다.

• 그럼 직접 제네릭 타입의 클래스를 선언하는 것은 어떨까?

• 이번 아이템과 함께해줄 코드를 소개합니다! (아이템 7에서 다룬 Object 기반 스택 코드)

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size);
    }
}

• 제네릭 타입을 사용하지 않고 단순히 Object 라는 최상위 클래스의 범용성을 이용해 구현했다.

• 스택 POP시 Object 타입의 객체를 반환하기 때문에 객체를 꺼낸 후 형변환을 시켜줘야한다.

    ◦ 형변환 과정에서 런타임 오류가 발생할 수 있음

    ◦ 제네릭 타입으로 구현해주는 것이 안전하다.


일반 클래스를 제네릭 클래스로 만드는 법

• 먼저 클래스 선언에 타입 매개변수를 추가해줘야 한다.

    ◦ 스택이 담을 원소의 타입을 하나 추가해주자.

        - 타입 이름으로는 보통 E를 사용한다.

• 그 다음 코드에 쓰인 Object 자료형을 적절한 타입 매개변수 자료형으로 바꿔주어야 한다.

    ◦ Object → E

public class Stack {
    private Object[] elements;
    ...
    public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; }
		...
}
public class Stack<E> {
    private E[] elements;
    ...
    public Stack() { elements = new E[DEFAULT_INITIAL_CAPACITY]; } // 컴파일 에러 발생
		...
}

• 이 때 E 같이 실체화가 불가능한 타입으로는 배열을 만들 수 없어 오류가 발생한다.

 

해결방법 1 : 제네릭 배열 생성을 금지하는 제약을 우회하는 방법

• Object 배열을 생성한 다음 제네릭 배열로 형변환하면 오류가 경고로 바뀐다.

    ◦ 컴파일러는 이 프로그램의 타입 안전성을 증명할 방법이 없음 : 직접 확인해줘야 한다.

        - 현재 코드의 배열 elements는 private 필드에 저장되므로 외부에 반환되는 일이 없다.

        - 즉 push 메소드를 통해 배열에 저장되는 원소의 타입은 항상 E 이므로 이 비검사 형변환은 확실히 안전하다.

    ◦ 비검사 형변환이 안전함을 직접 증명했다면 @SuppressWarnings 어노테이션으로 해당 경고를 숨기면 된다.

        - 현재 예시에서는 생성자가 비검사 배열 생성 말고는 하는 일이 없으므로 생성자 전체에서 경고를 숨겨도 된다.

• 이렇게 어노테이션을 사용하면 오류나 경고 없이 컴파일되고, ClassCastException 발생 걱정도 없다!

// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 타입 안정성을 보장하지만 Object 배열을 생성한 다음 제네릭 배열로 형변환한 것이기 때문에
// 이 배열의 런타임 타입은 E[]가 아닌 Object[] 이다.

@SuppressWarnings("unckecked")
public Stack() {
	elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

 

 

해결방법 2 : elements 필드의 타입을 E[...] 에서 Objects[...] 로 바꾸는 방법

• 이렇게 하면 배열이 반환한 원소를 E로 형변환했을 때 오류 대신 경고가 뜬다.

• E는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다.

    ◦ 즉 이번에도 개발자가 직접 안정성을 확인하고 경고를 숨겨주는 것이 좋다.

• pop 메소드 전체에서 경고를 숨기지 말고, 비검사 형변환을 수행하는 할당문에서만 숨겨보자(by Item27)

// 비검사 경고를 적절히 숨긴다.
public E pop() {
	if (size == 0) throw new EmptyStackException();
	
	// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
	@SuppressWarnings("unckecked") E result = (E)elements[--size];

	elements[size] = null; // 다 쓴 참조 해제
	return result;
}

 

 

두가지 방법의 비교

• 첫번째 방법이 가독성이 더 좋다.

    ◦ 배열의 타임을 E[]로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필할 수 있고 코드도 더 짧다.

    ◦ 첫번째 방식에서는 형변환을 배열 생성 시 단 한번만 해주면 되지만, 두번째 방법에서는 배열에서 원소를 읽을 때마다 해줘야 한다.

    ◦ 현업에서는 첫번째 방식이 더 많이 사용된다.

    ◦ 그러나 Heap Pollution(힙 오염)을 일으킨다.

        - 배열의 런타임 타입이 컴파일 타입과 다르기 때문

        - 힙 오염을 피하고자 두번째 방법이 채택되는 경우가 많다.


제네릭 타입과 배열 vs 리스트

• 지금까지 설명된 Stack 예시는 계속 배열 사용중 → Item 28 : 배열보다는 리스트를 우선하라 와 모순되어 보일 수 있다.

• 제네릭 타입의 경우 리스트를 사용하는것이 항상 가능하지도 않고, 항상 더 좋다고 이야기 할 수도 없다.

    ◦ Java가 리스트를 기본 타입으로 제공하지 않기 때문에 ArrayList 같은 제네릭 타입도 결국 기본 타입인 배열을 사용하여

      구현해야 한다.

    ◦ HashMap 같은 제네릭 타입은 성능을 높일 목적으로 배열을 사용하기도 한다.

 

• 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않는다.

    ◦ Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등 어떤 참조 타임으로도 Stack을 만들 수 있다.

    ◦ 단 기본 타입은 사용할 수 없다. Stack<int>, Stack<double>을 만들려고 하면 컴파일 오류가 난다.

    ◦ 이는 자바 제네릭 타입 시스템의 근본적인 문제이지만, 박싱된 기본 타입(Integer, Double)을 사용해 우회할 수 있다.

 

• 타입 매개변수에 제약을 두는 제네릭 타입도 있다.

    ◦ 예) java.util.concurrent.DelayQueue

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

        - 타입 매개변수 목록인 <E extends Delayed>는 java.util.concurrent.Delayed의 하위 타입만 받는다는 뜻이다.

        - 이렇게 ehlaus DelayQueue 자신과 DelayQueue를 사용하는 클래스는 DelayQueue의 원소에서 형변환없이 곧바로

           Delyed 클래스의 메소드를 호출할 수 있다.

        - ClassCastException 발생 걱정은 하지 않아도 된다.

    ◦ 이러한 타입 매개변수 E 를 한정적 타입 매개변수(bounded type parameter)라고 한다.

    ◦ 모든 타입은 자기 자신의 하위 타입이므로 DelayQueue<Delayed>로 사용할 수 있다.

중첩 클래스

• 다른 클래스 안에 정의된 클래스.

• 중첩 클래스는 해당 클래스를 감싸고 있는 클래스 안에서만 사용될 수 있으며 그 외의 클래스에서 사용되기 위해서는

    Top Level 클래스로 만들어야 한다.

• 중첩클래스의 종류

    ◦  정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스

    ◦  정적 멤버 클래스 빼고 모두 inner class에 해당한다.

• 각각의 중첩 클래스를 언제, 왜 사용해야 하는지 살펴보자.


정적 멤버 클래스

• 다른 클래스 내부에서 선언되고, 바깥 클래스의 private 멤버에 접근할 수 있다.

public class OuterClass {
	private String name;
	static class StaticMemberClass {
	    void hello() {
	        OuterClass outerClass = new OuterClass();
	        outerClass.name = "홍길동";
	  }
	}
}

• 위 특징을 제외하면 일반 클래스와 거의 동일하다.

• 일반적인 정적 필드와 동일한 접근 규칙을 갖는다

    ◦  예 : private 으로 선언하면 바깥 클래스에서만 해당 클래스에 접근할 수 있다.


정적 멤버 클래스의 사용 예시

• 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로 사용될 수 있다.

    ◦  예 : 계산기의 연산 종류를 나타내는 열거 타입 객체 Operation이 있다고 생각해보자.

    ◦  Operation Enum은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 한다.

    ◦  그래야 Calculator 클래스 객체가 Calculator.Operation.PLUS 나 Calculator.Operation.MINUS 등의 형태로

       필요한 연산을 참조할 수 있다.

public class Calculator {
   public enum Operation {
      PLUS("+", (x, y) -> x + y),
      MINUS("-", (x, y) -> x - y);
      ...
  }
}

비정적 멤버 클래스

• 정적 멤버 클래스와 비교했을 때 코드 상의 차이는 static 이 붙어 있느냐 없느냐 뿐이지만 의미적으로 비교해보면 꽤 큰 차이가 있다.


비정적 멤버 클래스의 특징

• 비정적 멤버 클래스 객체는 바깥 클래스 객체와 암묵적으로 연결된다.

    ◦  비정적 멤버 클래스 객체의 메서드에서 정규화된 this(클래스명.this)를 이용해 바깥 클래스 객체의 메서드를 호출하거나

       바깥 클래스 객체의 참조를 가져올 수 있다.

class OuterClass {
  int x = 10;

  // 비정적 멤버 클래스
  public class InnerClass {
    int x = 100;
    public void run() { System.out.println(OuterClass.this.x, this.x); }
  }

  // 생성자 사용 방법 주목
  public static void main(String[] args) {
    OuterClass outer = new OuterClass();
    OuterClass.InnerClass inner = outer.new InnerClass();
    inner.run(); // 10, 100 출력
  }
}

• 중첩 클래스의 객체가 바깥 클래스의 객체와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들자.

    ◦  비정적 멤버 클래스는 바깥 클래스 객체 없이는 생성할 수 없기 때문

• 변수명이 겹치지 않으면 this 사용하지 않고 바로 참조하는 것도 가능

public class OuterClass{
	int outerfield;
	void outerMethod(){ System.out.println(outerfield); }

	// 비정적 멤버 클래스
	public class InnerClass {
		void innerMethod() {
			outerfield = 3;
			outerMethod();
		}
	}
}

• 비정적 멤버 클래스의 객체와 바깥 클래스 객체 사이의 이러한 암묵적인 관계는 멤버 클래스가 객체화될 때 확립되며 그 이후에는

    변경할 수 없다.

    ◦  이 관계는 보통 바깥 클래스 객체의 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지지만,

         드물게는 직접 바깥클래스.new 내부클래스(args) 를 호출해 수동으로 만들기도 한다.

    ◦  이 관계 정보는 비정적 멤버 클래스 객체 안에 저장되어 메모리 공간을 차지하며, 생성 시간도 소모한다.


비정적 멤버 클래스의 사용 예시

• 비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다.

    ◦  어댑터 : 어떤 클래스의 객체를 감싸 마치 다른 클래스의 객체처럼 보이게 하는 것

          -  예) Map 인터페이스 : 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용한다.

          -  예) Set, List 같은 컬렉션 인터페이스 : 자신의 반복자를 구현할 때 비정적 멤버 클래스를 사용한다.

public class MySet<E> extends AbstractSet<E> {
	...
	@Override public Iterator<E> iterator() { return new MyIterator(); }
	private class MyIteratpr implements Iterator<E> { ... }
	...
}

비정적 멤버 클래스의 단점

• 멤버 클래스에서 바깥 클래스 객체에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

• static을 생략하면 바깥 객체로의 숨은 외부 참조를 갖게 된다.

    ◦  이 참조를 저장하는데 시간과 공간이 소모된다.

    ◦  GC가 바깥 클래스의 객체를 수거하지 못하는 메모리 누수 문제가 생길수도 있다.

    ◦  참조가 눈에 보이지 않으니 문제 원인을 찾기가 어렵다.


private 정적 멤버 클래스

• private 정적 멤버 클래스는 주로 바깥 클래스가 표현하는 객체의 한 부분을 나타낼 때 사용한다.

• 예) 키와 값을 매핑시키는 Map 객체

    ◦  많은 Map 구현체는 각각의 키-값 쌍을 표현하는 Entry 객체를 가지고 있다.

    ◦  모든 Entry가 Map과 연관되어 있지만 Entry의 메서드들(getKey, getValue, setValue 등)은 맵을 직접 사용하지는 않는다.

    ◦  즉 Entry를 비정적 멤버 클래스로 표현하는 것은 낭비이고, private 정적 멤버 클래스가 가장 알맞다.

    ◦  Entry를 선언할 때 static을 빼먹어도 Map은 여전히 동작하지만 모든 Entry가 바깥 Map으로의 참조를 갖게 되어 공간과 시간을

         낭비할 것이다.

• 멤버 클래스가 공개된 클래스의 public이나 protected 멤버라면 static이냐 아니냐의 여부는 2배로 중요해진다.

    멤버 클래스 역시 외부에 공개되므로, 개발 도중 static이 붙으면 하위 호환성이 깨질 수 있다.


익명 클래스

• 이름이 없는 클래스. 멤버 클래스와 달리 쓰이는 시점에 선언과 객체 생성이 동시에 이루어진다.

    ◦  익명 클래스는 바깥 클래스의 멤버가 아니다.

    ◦  왜냐하면 사용되는 시점에 인스턴스화 되고, 코드상의 어떤 위치에서든 만들 수 있기 때문이다.

• 비정적인 문맥에서 사용될 때만 바깥 클래스의 객체를 참조할 수 있다.

• 정적 문맥에서 사용될 때는 static 변수 이외의 static 필드는 가질 수 없다.

    ◦  상수 표현을 위해 초기화된 final 기본 타입과 문자열 필드만 가질 수 있다.


익명 클래스의 단점

• 선언한 지점에서만 인스턴스를 만들 수 있다.

• instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.

• 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하면서 다른 클래스를 상속받을 수 없다.

• 익명 클래스를 사용하는 외부 클래스는 해당 익명 클래스가 상속받은 상위 클래스 외에는 호출할 수 없다.

• 표현식 중간에 등장하므로 짧지 않으면(10줄 이하) 가독성이 떨어진다.


익명 클래스의 사용 예시

• 자바가 람다를 지원하기 전에는 즉석에서 작은 함수 객체나 처리객체(Process Object)를 만드는데 익명 클래스를 주로 사용했다.

• 현재는 람다가 그 역할을 대신하고 있다.

• 정적 팩토리 메서드를 구현할 때 사용될 수 있다.

static List<Integer> intArrayAsList(int[] a) {

	Objects.requireNonNull(a);
	return new AbstractList<>() {
		@Override
		public Interger get(int i) { return a[i]; }
	
		@Override
		public Integer set(int i, Integer val) {
			int oldVal = a[i];
			a[i] = val;
			return oldVal;
		}

		@Override
		public int size() { return a.length; }
	}
}

 


지역 클래스

• 네가지 중첩 클래스 중 가장 드물게 사용된다.

• 지역변수를 선언할 수 있는 곳이면 어디서는 선언할 수 있고 유효 범위도 지역변수와 동일하다.

• 다른 중첩 클래스들과의 공통점

    ◦  멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있다.

    ◦  익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 클래스 객체를 참조할 수 있으며,

         정적 필드는 가질 수 없고 가독성을 위해 짧게 작성해야 한다.

클래스의 상속을 허용하면 문제가 발생할 수 있다.

기존 클래스(A) 내부에서 먼저 정의 및 호출되는 함수(F)가 재정의 가능한 함수일 때, 다른 클래스(B)가 A 클래스를 상속한 다음 F 함수를 재정의(F’)하여 사용할 수 있는데 모종의 이유로 클래스 A 에서 F 함수를 수정하면 클래스 B 의 F’ 함수에서 문제가 생길 수 있다.

이러한 문제가 왜 발생하는지 좀 더 자세히 알아보고, 어떻게 방지할 수 있는지 알아보자.


주석을 잘 남겨라

• 외부에서 상속용 클래스의 메서드를 재정의하면 어떤 일이 일어나는지 정확히 정리하여 문서로 남겨라

• 상속용 클래스는 재정의 가능한 메서드들을 내부에서 어떻게 이용하는지 문서로 남겨야 한다.

• 클래스 내부에서 재정의 가능한 메서드를 호출하고 있다면, 그 사실과 어떤 순서로 호출하는지,

    각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 문서로 남겨라

    ◦  재정의 가능 메서드 : final이 아닌 모든 public, protected 메서드

• 아니 그냥 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨라!

    ◦  백그라운드 스레드나 정적 초기화 과정에서도 호출이 일어날 수 있음

• 메서드에서 종종 Implementation Requirements로 시작하는 주석을 발견할 수 있는데, 이는 메서드의 내부 동작 방식에 대한 설명을

    적어놓은 주석이다.

    ◦  Command Line에서 -tag “implSpec:a:Implementation Requirements:” 명령어를 입력해 @implSpec 어노테이션을 새롭게

         지정할 수 있는데, 이 어노테이션을 주석에 붙이면 Implementation Requirements 라는 문구가 자동으로 생성된다.

         (자바독의 커스텀 태그 기능)

    ◦  자바 개발팀에서 내부적으로 사용하는 규약

    ◦  Annotation 이름을 반드시 @implSpec으로 해야할 필요는 없지만, 언젠가 표준 태그로 정의될지도 모르니

         이왕이면 자바 개발팀과 같은 방식으로 사용하는 것을 추천한다. (by 옮긴이)

→ java.util.AbstractCollection의 remove 메서드

• @implSpec은 이 클래스를 상속하여 메서드를 재정의했을 때 나타날 효과를 상세히 설명하고 있다.

    ◦  java.util.AbstractCollection의 remove 메서드는 내부적으로 iterator의 remove 메서드를 사용하고 있다.

    ◦  위 설명에 의하면 iterator 인터페이스를 구현한 구현체에 remove 메소드가 구현되어 있지 않은 경우

        UnsupportedOperationException이 발생한다.

• iterator 메서드를 재정의하면 java.util.AbstractCollection의 remove 메서드의 동작에 영향을 준다.

    ◦  iterator 메서드를 재정의하면 반드시 remove 메소드도 함께 구현해 주어야 java.util.AbstractCollection의 remove 메서드를

         오류 없이 사용할 수 있다.

• 이처럼 클래스를 안전하게 상속할 수 있도록 하려면 클래스의 내부 구현 방식을 설명으로 남겨야 한다.

    ◦  @implSpec 어노테이션을 적극 활용하라


클래스의 Hook 을 잘 선별하여 protected 메서드 형태로 공개하라

• 효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 내부 동작 과정에 끼어들 수 있는 훅(hook)을

    protected 메서드(재정의 가능한)로 제공하는것이 좋다.

• removeRange 메서드는 특정 리스트 또는 부분 리스트의 clear 메서드에서 사용된다고 나와있다.

• 또한 해당 메서드를 재정의하면 특정 리스트 또는 부분 리스트의 clear 메서드를 고성능으로 만들 수 있다고 명시되어있다.

• removeRange 메서드를 protected 접근 제어자로 제공한 이유는 단지 하위 클래스의 clear 메서드를 고성능으로 만들기 쉽게

    하기 위해서이다.

 

• 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지는 어떻게 결정할까?

    ◦  심사숙고해서 잘 예측해본 다음 실제 하위 클래스를 만들어 실험해보는것이 최선

          -  상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는것이 유일한 방법이다.

          -  한 3개 정도 만들어 보는 것이 적당하다. (이 중 하나 이상은 제 3자가 작성해볼 것)

          -  상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

• protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 한 적어야 하나 너무 적게 노출해서 상속으로 얻는 이점마저

    없애지 않도록 주의해야 한다.

• 클래스를 확장해야 할 명확한 이유가 떠오르지 않는다면 상속을 금지하는 편이 낫다.


상속을 허용하는 클래스가 지켜야 할 제약

• 상속을 허용하는 클래스의 생성자는 클래스 내부의 재정의 가능 메서드를 호출하면 안된다.

    ◦  프로그램 오작동의 원인이 됨

    ◦  상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 호출되기 때문

    ◦  하위 클래스에서 재정의된 메서드가 하위 클래스의 생성자보다 먼저 호출된다.

    ◦  재정의된 메서드가 하위 클래스의 생성자에서 초기화되는 값에 의존한다면 의도대로 동작하지 않을 것

public class Super {
    public Super() { overrideMe(); }
    public void overrideMe() { ... }
}
public final class Sub extends Super {
    private final Instant instant;
    Sub() { instant = Instant.now(); }

    @Override
    public void overrideMe() { System.out.println(instant); } 
}

Sub sub = new Sub();  // null 출력
sub.overrideMe();     // instant 출력

• 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화 하기도 전에 overrideMe 함수를 호출하기 때문에

    이러한 문제가 발생한다.

• overrideMe 메서드에서 instant 객체의 메서드를 호출하려 한다면 상위 클래스의 생성자가 overrideMe 메서드를 호출할 때

    NullPointerException을 던지게 될 것이다.

    ◦  위 코드에서는 println이 null 입력을 처리해주기 때문에 예외는 발생하지 않음


상속용 클래스의 설계와 Cloneable/Serializable 인터페이스

• 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다.

    ◦  clone 과 readObject 메서드는 생성자와 비슷한 효과를 내기 때문 : 새로운 객체를 생성한다.

    ◦  즉 상속용 클래스에서 Cloneable 또는 Serializable을 구현하게 된다면 이들을 구현할 때 따르는 제약도 생성자와 비슷하다.

    ◦  clone 과 readObject 메서드 모두 직/간접적으로 재정의 가능한 메서드를 호출해서는 안된다.


상속용으로 설계되지 않은 일반적인 구체 클래스

• 상속용으로 설계하지 않은 클래스는 상속을 금지하라

• 상속용으로 설계되지 않은 일반적인 구체 클래스는 final도 아니고 상속용으로 설계/문서화되지 않았다.

    ◦  그대로 두면 위험하다. / 누가 상속해버릴지 몰라... 누가 줄줄이 도미노 문제 만들어놓을지 몰라...

• 클래스의 상속을 금지시키는 두가지 방법

    ◦  클래스를 final로 선언하는 방법

    ◦  모든 생성자를 private 또는 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법

          -  클래스 내부에서 다양한 하위 클래스를 만들어 쓰는 등의 유연한 작업을 가능하게 해준다.

• 상속을 금지해도 괜찮은 것인가?

    ◦  클래스가 특정 인터페이스를 구현한 클래스라면 상속을 금지해도 개발하는데 문제가 없다.

          -  예) Set / List / Map

    ◦  단 구체 클래스가 인터페이스를 구현하지 않았는데 상속을 금지해 버리면 상당히 불편해질 수 있다.

    ◦  이런 경우에는 클래스 내부에서 재정의 가능 메서드를 사용하지 않게 만들고, 해당 사실을 문서로 남기자.

          -  메서드를 재정의해도 다른 메서드의 동작에 영향을 주지 않게 만들기 위해


클래스 동작을 유지하면서, 재정의 가능 메서드를 사용한 부분을 제거하는 법

public class Super {
    public Super() { overrideMe(); }
    public void overrideMe() { ... }
}
public class Super {
    public Super() { overrideMe(); }
    public void overrideMe() { ... }
    private void helperMethod() { ... }
}
public class Super {
    public Super() { helperMethod(); }
    public void overrideMe() { helperMethod(); }
    private void helperMethod() { ... }
}

• 새로운 private helperMethod(도우미 메서드)를 생성한다.

• 각각의 재정의 가능 메서드에 작성된 코드를 helperMethod로 옮긴다.

• 기존 재정의 가능 메서드가 이 helperMethod를 호출하도록 수정한다.

• 기존 재정의 가능 메서드를 호출하는 다른 코드 역시 이 helperMethod를 호출하도록 수정한다.

Comparable 인터페이스의 유일무이한 메서드, compareTo

• comapreTo = 값을 비교하는 method

A.compareTo(B)
A < B : 음수 반환 ( sgn(A.compareTo(B)) = -1 )
A = B : 0 반환 ( sgn(A.compareTo(B)) = 0 )
A > B : 양수 반환 ( sgn(A.compareTo(B)) = -1 )

• compareTo는 Object의 메서드가 아닌, Comparable 인터페이스의 메서드이다.

public class ClassName implements Comparable<ClassName> { ... }
public class Main {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        System.out.println(a.compareTo(b)); // -1
    }
}
public class Main {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        System.out.println(a.compareTo(b)); // error: int cannot be dereferenced
    }
}

• equals 메서드처럼 값이 같은지 비교하는 기능 뿐만 아니라, 값의 대소비교가 가능하다.

• 값의 순서를 매기는데 유용하게 사용된다 : 검색/정렬 메서드, 자동 정렬 Collection 등에서 사용됨

public PriorityQueue(Collection<? extends E> c) {
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        initElementsFromCollection(ss);
    }
    else if (c instanceof PriorityQueue<?>) {
        PriorityQueue<? extends E> pq = (PriorityQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        initFromPriorityQueue(pq);
    }
    else {
        this.comparator = null;
        initFromCollection(c);
    }
}

• Generic Type 으로 다양한 객체 타입에서 사용할 수 있다.

public class Main {
    public static void main(String[] args) {
        Integer iA = 1; Integer iB = 2;
        Float fA = 1F; Float fB = 2F;
        Double dA = 1.0; Double dB = 2.0;
        
        System.out.println(iA.compareTo(iB)); // -1
        System.out.println(fA.compareTo(fB)); // -1
        System.out.println(dA.compareTo(dB)); // -1
    }
}

• 직접 작성한 클래스에서 Comparable을 구현하면 해당 인터페이스를 구현하는 다양한 제네릭 알고리즘과
    Collection을 사용할 수 있다는 장점이 있다.

    ◦  Java 라이브러리의 모든 값 클래스(Integer, Float, Double 등)와 열거 타입이 Comparable을 구현한다.

    ◦  알파벳, 숫자, 날짜 등 순서가 명확한 값 클래스를 작성할 때는 꼭 Comparable 인터페이스를 구현하자.

public final class Integer extends Number
        implements Comparable<Integer>, Constable, ConstantDesc { ... }
public abstract class Enum<E> extends Enum<E>>
        implements Constable, Comparable<E>, Serializable { ... }

• 정렬된 컬렉션인 TreeSet, TreeMap 과 검색/정렬 기능을 제공하는 유틸리티 클래스 Collections, Arrays 역시
   Comparable을 활용한다.


compareTo 메서드의 세가지 규약

A.compareTo(B)
A < B : 음수 반환 ( sgn(A.compareTo(B)) = -1 )
A = B : 0 반환 ( sgn(A.compareTo(B)) = 0 )
A > B : 양수 반환 ( sgn(A.compareTo(B)) = -1 )

• sgn(x.compareTo(y)) == -sgn(y.compareTo(x))

    ◦  모든 x, y에 대해 x > y 이면 y < x 이다.

    ◦  한쪽에서 예외가 발생하면 다른 쪽에서도 응당 예외가 발생해야 함

 

• x.compareTo(y) > 0 && y.compareTo(z) > 0 이면 x.compareTo(z)

    ◦  모든 x, y, z 에 대해 x > y 이고 y > z 이면 x > z 이다

 

• x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))

    ◦  x = y 이면, 모든 z에 대해 x > z , x < z 의 참/거짓 여부는 y > z , y < z 의 참/거짓 여부와 동일

 

• (권장) x.compareTo(y) == 0 이면 x.equals(y) == 0 이다.

    ◦  Comparable을 구현하지만 위 사항을 지키지 않는 클래스는 반드시 그 사실을 명시해줘야 한다.

    ◦  주의! 이 클래스의 순서는 equals 메서드와 일관되지 않다.

 

• 위 규약들을 어기는 것이 특정한 오류를 발생시키는 것은 아니지만, 크고 작은 문제들이 발생할 수 있다.

• 정렬된 컬렉션들은 값이 같은지를 비교할 때 equals 대신 compareTo를 사용하기 때문

public class Main {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("1.0");
        BigDecimal b = new BigDecimal("1.00");

        System.out.println(a.compareTo(b)); // 0
        System.out.println(a.equals(b)); // false

				HashSet<Object> A = new HashSet<>(); // 동치 비교에 equals 사용
        TreeSet<Object> B = new TreeSet<>(); // 동치 비교에 compareTo 사용

        A.add(a); A.add(b);
        B.add(a); B.add(b);

        System.out.println(A.size()); // 2
        System.out.println(B.size()); // 1
    }
}

compareTo 메서드의 작성 요령

• 타입이 다른 객체를 신경쓰지 않아도 됨 : 대부분 ClassCastException을 던지는 식으로 구현한다.

    ◦  일반적으로 객체간의 비교는 객체들이 구현한 공통 인터페이스를 매개로 이루어지기 때문

• 기존 클래스를 확장한 구체 클래스에서 새로운 값 Component를 추가하면 compareTo 규약을 위반하는 상황이 발생할 수 있으니

    주의하자.

    ◦  만약 새로운 값 Component를 추가하고 싶다면, 새로운 클래스를 만들어서 이 클래스에 원래 클래스의 인스턴스를 가리키는

         필드를 두자. 그 다음 내부 인스턴스를 반환하는 메서드르 제공하면 된다.

    ◦  이렇게 하면 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해넣을 수 있으며 필요에따라 바깥 클래스의 인스턴스를 원래

         클래스의 인스턴스로 다룰수도 있다.

• Comparable은 제네릭 인터페이스 이므로 compareTo 메서드의 parameter 타입은 컴파일 타임에 정해진다.

    즉 입력 인수의 타입을 확인하거나 형변환할 필요가 없음

    ◦  parameter 타입이 잘못되었다면 컴파일 자체가 되지 않는다.

    ◦  null 이 인수로 들어오면 NullPointerException이 Throw 되어야 한다.

• compareTo 메서드는 값이 동일한지를 비교하는데 보다는 값의 대소와 순서를 비교하는데 주로 사용된다.

    ◦  클래스에 핵심 필드가 여러개라면 가장 핵심적인 필드부터 비교해나가자.

public int compareTo(PhoneNumber pn) {
    int result = Short.compare(areaCode, pn.areaCode); // 가장 중요한 필드

    if (result == 0) {
        result = Short.compare(prefix, pn.prefix); // 두번째로 중요한 필드

				// 세번째로 중요한 필드
        if (result == 0) result = Short.compare(lineNum, pn.lineNum);
    }
    return result;
}

• 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출하면 된다.


Java 제공 비교자의 활용

@Override
public int compare(A,B){
    if (A > B) { return 1; }
		else if (A < B) { return -1; }
		else { return 0; } 
}

• Comparable을 구현하지 않은 필드나, 표준이 아닌 순서로 비교해야 한다면 Java에서 제공하는 비교자(Comparator)를 사용할 수 있다.

• 객체 참조 필드가 하나인 비교자 : compare

• Comparator 인터페이스를 구현할 때 작성해야하는 메서드로 compare()에 2개의 인자를 넘겨 내부 구현에 따라 int 결과 값을 리턴한다.


관계연산자 >, < 사용에 주의하라

• compareTo 메서드에서 관계 연산자 >, <를 사용하는 방식은 거추장스럽고 오류를 유발하기 쉽다.

• Java7 부터 라이브러리의 모든 값 클래스(Integer, Float, Double 등)가 Comparable을 구현하고 있으므로,

    해당 기능을 사용하는것이 권장된다.


Java의 비교자 생성 메서드와 객체 참조용 비교자 생성 메서드

• 비교자 생성 메서드를 사용하면 method chaining 방식으로 비교자를 생성할 수 있다.

private static final Comparator<PhoneNumber> COMPARATOR =
        Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode)
                .thenComparingInt(pn -> pn.prefix)
                .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}

• comparing 객체 참조를 키에 맵핑하는 키 추출 함수를 인수로 받아 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드

    ◦  입력 인수의 타입을 명시해줘야 한다. Java의 타입 추론 능력이 해당 타입을 인식한 만큼 강력하지 않으므로

• thenComparing 키 추출자 함수를 입력받아 다시 비교자를 반환하는 인스턴스 메서드

    ◦  원하는 만큼 연달아 호출할 수 있다.

    ◦  입력 인수의 타입을 명시해줄 필요가 없다. 자바의 타입 추론 능력이 이정도는 추론 가능하므로