ch6 열거 타입과 애너테이션 - LenKIM/everyone-is-effective-java-study GitHub Wiki

6장 열거 타입과 애너테이션

특수한 목적의 참조 타입 두가지

  • 클래스의 일종인 열거 타입(enum; 열거형)
  • 인터페이스의 일종인 애너테이션

잘 쓰는 방법은?

아이템 34. int 상수 대신 열거 타입을 사용하라

  • 정수 열거 패턴은 비추천, 열거 타입은 추천

    public static final int APPLE_FUJI = 0;
    public static final int APPLE_PIPPIN = 1;
    public static final int APPLE_GRANNY_SMITH = 2;
    
    public static final int ORANGE_NAVEL = 0;
    public static final int ORANGE_TEMPLE = 1;
    public static final int ORANGE_BLOOD = 2;

정수 열거 패턴의 단점은?

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

더군다나, 동등연산자(==) 로 비교하더라도 컴파일러는 아무런 경고 메세지를 출력하지 않는다. 자바는 정수 열거 패턴을 위한 별도 이름공간(namespace)을 지원하지 않기 때문에 어쩔 수 없이 접두어를 써서 이름 충돌을 방지. 그러나 이는 깨지기 쉽다.

이런 단점을 극복하기 위한 수단으로 열거 타입(enum type)

public enum Apple {PUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}

자바 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개

열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final. 따라서 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함을 보장

  • 열거 타입은 컴파일타임 타일 안전성을 보장.
  • 완전한 형태의 클래스라서 단순한 정숫값인 다른 언어의 열거 타입보다 휠씬 강력
  • 각자의 이름공간이 주어짐
  • 싱글턴이 보장
  • Object메서드들과 Comparable, Serializable을 미리 구현해놓음
  • 메서드나 필드를 추가할 수 있고, 인터페이스를 구현하게 할 수도 있다.
// 코드 34-3 데이터와 메서드를 갖는 열거 타입 (211쪽)
public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    EARTH  (5.975e+24, 6.378e6),
    MARS   (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);

    private final double mass;           // 질량(단위: 킬로그램)
    private final double radius;         // 반지름(단위: 미터)
    private final double surfaceGravity; // 표면중력(단위: m / s^2)

    // 중력상수(단위: m^3 / kg s^2)
    private static final double G = 6.67300E-11;

    // 생성자
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double mass()           { return mass; }
    public double radius()         { return radius; }
    public double surfaceGravity() { return surfaceGravity; }

    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;  // F = ma
    }
}

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

: 열거 타입은 근본적으로 불편이라 모든 필드는 final이어야 한다. 필드는 public으로 선언해도 되지만, private 으로 두고 별도의 public 접근자 메서드를 두는 게 낫다.

package effectivejava.chapter6.item34;

// 어떤 객체의 지구에서의 무게를 입력받아 여덟 행성에서의 무게를 출력한다. (212쪽)
public class WeightTable {
   public static void main(String[] args) {
      double earthWeight = Double.parseDouble(args[0]);
      double mass = earthWeight / Planet.EARTH.surfaceGravity();
      for (Planet p : Planet.values())
         System.out.printf("%s에서의 무게는 %f이다.%n",
                           p, p.surfaceWeight(mass));
   }
}

1. 열거 타입에서 상수를 제거하면?

  • 제거한 상수를 참조하지 않는 코드에서는 아무 영향이 없다.

열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private이나 package-private 메서드로 구현. 이렇게 구현된 열거 타입 상수는 자신을 선언한 클래스 혹은 패키지에서만 사용할 수 있는 기능을 담게 한다.

2. 상수마다 동작이 달라져야 하는 상황이 있을 수 있다, 이를 구현하는 방식으로 먼저 switch문을 이용해 상수의 값에 따라 분기하는 방법을 보자.

public 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 문을 추가. 혹시라도 깜박한다면, 컴파일은 되지만 새로 추가한 연산을 수행하려 할 때 "알수 없는 연산"이라는 런타임오류를 내며 프로그램 종료.

... 아 정리했는데, 부분이 날라감...

아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

ordinal? : 해당 메서드는 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 메서드.

나쁜 예.

public enum Ensemble {
  SOLO, DUET, TRIO, QUARTET, QUNITET, SEXTET, SEPTET, OCTET, NONET, DECTET;
  
  public int numberOfMusicians() {return orinal() + 1;}
}

상수 선언 순서를 바꾸는 순간 numberOfMusicians가 오동작하며, 이미 사용 중인 정수와 값이 같은 상수는 추가할 방법이 없다.

// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입 (222쪽)
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int numberOfMusicians;
    Ensemble(int size) { this.numberOfMusicians = size; }
    public int numberOfMusicians() { return numberOfMusicians; }
}

이렇게 해결 한다.

아이템 36. 비트 필드 대신 EnumSet을 사용하라.

열거한 값들이 주로(단독이 아닌) 집합으로 사용될 경우, 예전에는 각 상수에 서로 다른 2의 거듭제곱 값을 할당한 정수 열거 패턴을 사용...? 응? 진짜?

public class Text {
  public static final int STYPE_BOLD = 1 << 0;
  public static final int STYPE_ITALIC = 1 << 1;
  public static final int STYPE_UNDERLINE = 1 << 2;
  public static final int STYPE_STRIKETHROUGH = 1 << 3;
  
	public void applyStyles(int styles){ ... }
}

비트 필드를 사용하면 비트별 연산을 사용해 합집합과 교집합 같은 집합 연산을 효율적으로 수행할 수 있지만, 비트 필드는 정수 열거 상수의 단점을 그대로 지니며, 추가로 해석하기가 휠씬 어렵다는 단점을 가지고 있다.

EnumSet 클래스는 열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현. Set 인터페이스를 완벽히 구현하며, 타입 안전하고, 다른 어떤 Set 구현체와도 함께 사용가능함. 하지만 EnumSet의 내부는 비트 백터로 구현.

완소가 총 64개 이하라면, 즉 대부분의 경우에 EnumSet 전체를 long 변수 하나로 표현하며 비트 필드에 비견되는 성능.

public class Text {
  public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
  
  //어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
  public void applyStyles(Set<Style> styles) { ... }
}

applyStyles 메서드에 EnumSet 인스턴스를 건네는 클라이언트 코드다. EnumSet은 집합 생성  당양한 기능의 정적 팩터리를 제공하는데, 다음 코드에서는 그중 of 메서드 사용.

text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

열거할 수 있는 타입을 한데 모아 집합 형태로 사용한다고 해도 비트 필드를 사용할 이유는 없다.

아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라.

package effectivejava.chapter6.item37;
import java.util.*;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toSet;

// EnumMap을 사용해 열거 타입에 데이터를 연관시키기 (226-228쪽)

// 식물을 아주 단순하게 표현한 클래스 (226쪽)
class Plant {
    enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }

    final String name;
    final LifeCycle lifeCycle;

    Plant(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }

    @Override public String toString() {
        return name;
    }

    public static void main(String[] args) {
        Plant[] garden = {
            new Plant("바질",    LifeCycle.ANNUAL),
            new Plant("캐러웨이", LifeCycle.BIENNIAL),
            new Plant("딜",      LifeCycle.ANNUAL),
            new Plant("라벤더",   LifeCycle.PERENNIAL),
            new Plant("파슬리",   LifeCycle.BIENNIAL),
            new Plant("로즈마리", LifeCycle.PERENNIAL)
        };
// 코드 37-1 ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것! (226쪽)
Set<Plant>[] plantsByLifeCycleArr =
  (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycleArr.length; i++)
  plantsByLifeCycleArr[i] = new HashSet<>();
for (Plant p : garden)
  plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p);
// 결과 출력
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
  System.out.printf("%s: %s%n",
                    Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
}
  • 위 코드에서 배열은 제네릭과 호환되지 않고, 비검사 형변환을 수행해야 하고 깔끔히 컴파일 되지 않을 것.

  • 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다. 가장 심각한 문제는 정확한 정숫값을 사용한다는 것을 직접 보증해야 한다.

  • 정수는 열거 타임과 달리 타입 안전하지 않기 때문, 잘못된 값을 사용하면 잘못된 동작을 묵묵히 수행하거나( 운이 좋다면 ) ArrayIndexOutofBoundsException을 던짐.

// 코드 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다. (227쪽)
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
  new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
  plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
  plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
// 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다! (228쪽)
System.out.println(Arrays.stream(garden)
                   .collect(groupingBy(p -> p.lifeCycle)));
// 코드 37-4 스트림을 사용한 코드 2 - EnumMap을 이용해 데이터와 열거 타입을 매핑했다. (228쪽)
System.out.println(Arrays.stream(garden)
      .collect(groupingBy(p -> p.lifeCycle,() -> new EnumMap<(LifeCycle.class), toSet())));

아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라.

타입 열거 패턴? 확장 가능

그럼에도 불구하고, 확장할 수 있는 열거 타입에 잘 적용되는 부분이 있다.
바로, 연산 코드 , 각 원소는 특정 기계가 수행하는 연산을 뜻함.

// 코드 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다. (232쪽)
public interface Operation {
    double apply(double x, double y);
}
// 코드 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다. - 기본 구현 (233쪽)
public enum BasicOperation implements 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;

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

    @Override public String toString() {
        return symbol;
    }
}
import java.util.*;

// 코드 38-2 확장 가능 열거 타입 (233-235쪽)
public enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x % y;
        }
    };
    private final String symbol;
    
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override public String toString() {
        return symbol;
    }

//    // 열거 타입의 Class 객체를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예 (234쪽)
//    public static void main(String[] args) {
//        double x = Double.parseDouble(args[0]);
//        double y = Double.parseDouble(args[1]);
//        test(ExtendedOperation.class, x, y);
//    }
//    private static <T extends Enum<T> & Operation> void test(
//            Class<T> opEnumType, double x, double y) {
//        for (Operation op : opEnumType.getEnumConstants())
//            System.out.printf("%f %s %f = %f%n",
//                    x, op, y, op.apply(x, y));
//    }

    // 컬렉션 인스턴스를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예 (235쪽)
    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        test(Arrays.asList(ExtendedOperation.values()), x, y);
    }
    private static void test(Collection<? extends Operation> opSet,
                             double x, double y) {
        for (Operation op : opSet)
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }
}

이렇게 되면 확장에 열려 있는가? 장점은 무엇이라 생각되는가?

  • 먼저 추상 메서드를 선언할 필요가 없다.
  • <T extends Enum<T> & Operation> 의 의미는 Class 객체가 열거 타입인 동시에 Operatopn의 하위 타입.
  • 열거 타입이어야 원소를 순회할 수 있고, Operation이어야 원소를 뜻하는 연산을 수행.

이런 방식에도 문제는 바로 열거 타입끼리 구현을 상속할 수 없다는 점에 있다.

아무 상태에서도 의존하지 않는 경우에는 디폴트 구현을 이용해 인터페이스에 추가하는 방법, 반면 Operation 예는 연산 기호를 저장하고 찾는 로직이 BasicOperation과 ExtendedOperation 모두가 들어가야 한다.

위 코드와 비슷한 패턴으로 구현 부분은 java.nio.file.LinkOption 열거 타입은 CopyOption, OpenOption 이다.

package java.nio.file;

/**
 * Defines the options as to how symbolic links are handled.
 *
 * @since 1.7
 */

public enum LinkOption implements OpenOption, CopyOption {
    /**
     * Do not follow symbolic links.
     *
     * @see Files#getFileAttributeView(Path,Class,LinkOption[])
     * @see Files#copy
     * @see SecureDirectoryStream#newByteChannel
     */
    NOFOLLOW_LINKS;
}
package java.nio.file;

/**
 * An object that configures how to copy or move a file.
 *
 * <p> Objects of this type may be used with the {@link
 * Files#copy(Path,Path,CopyOption[]) Files.copy(Path,Path,CopyOption...)},
 * {@link Files#copy(java.io.InputStream,Path,CopyOption[])
 * Files.copy(InputStream,Path,CopyOption...)} and {@link Files#move
 * Files.move(Path,Path,CopyOption...)} methods to configure how a file is
 * copied or moved.
 *
 * <p> The {@link StandardCopyOption} enumeration type defines the
 * <i>standard</i> options.
 *
 * @since 1.7
 */

public interface CopyOption {
}

열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다. 이렇게 하면 클라이언트는 이 인터페이스를 구현해 자시만의 열거 타입(혹은 다른 타입)을 만들 수 있다. 그리고 API가(기본 열거 타입을 직접 명시하지 않고) 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용할 수 있다.

아이템 39. 명명 패턴보다 애너테이션을 사용하라.

명명 패턴의 단점은 무엇인가?

  • 정확한 단어가 아니면, 실패했어도 성공으로 착각할 수 있다.
  • 올바른 프로그램 요소에서만 사용될 수 있다고 보증 할 방법이 없다.
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다는 것. 예외를 던지는 테스트 이름 작성의 경우도 마찬가지.

이런 문제를 해결하는 방법으로 애노테이션이 활용될 수 있음.

/**
 * 테스트 메서드임을 선언하는 애너테이션이다.
 * 매개변수 없는 정적 메서드 전용이다.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
// 코드 39-2 마커 애너테이션을 사용한 프로그램 예 (239쪽)
public class Sample {
    @Test
    public static void m1() { }        // 성공해야 한다.
    public static void m2() { }
    @Test public static void m3() {    // 실패해야 한다.
        throw new RuntimeException("실패");
    }
    public static void m4() { }  // 테스트가 아니다.
    @Test public void m5() { }   // 잘못 사용한 예: 정적 메서드가 아니다.
    public static void m6() { }
    @Test public static void m7() {    // 실패해야 한다.
        throw new RuntimeException("실패");
    }
    public static void m8() { }
}
// 코드 39-3 마커 애너테이션을 처리하는 프로그램 (239-240쪽)
import java.lang.reflect.*;

public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " 실패: " + exc);
                } catch (Exception exc) {
                    System.out.println("잘못 사용한 @Test: " + m);
                }
            }
        }
        System.out.printf("성공: %d, 실패: %d%n",
                passed, tests - passed);
    }
}

이번에는 특정 예외를 던져야만 성공하는 테스트를 지원하도록 해보자.

// 코드 39-4 매개변수 하나를 받는 애너테이션 타입 (240-241쪽)
import java.lang.annotation.*;

/**
 * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}
import effectivejava.chapter6.item39.markerannotation.Test;
import java.lang.reflect.*;

// 마커 애너테이션과 매개변수 하나짜리 애너태이션을 처리하는 프로그램 (241-242쪽)
public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
                } catch (InvocationTargetException wrappedEx) {
                    Throwable exc = wrappedEx.getCause();
                    Class<? extends Throwable> excType =
                            m.getAnnotation(ExceptionTest.class).value();
                    if (excType.isInstance(exc)) {
                        passed++;
                    } else {
                        System.out.printf(
                                "테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n",
                                m, excType.getName(), exc);
                    }
                } catch (Exception exc) {
                    System.out.println("잘못 사용한 @ExceptionTest: " + m);
                }
            }
        }

        System.out.printf("성공: %d, 실패: %d%n",
                passed, tests - passed);
    }
}

예외 케이스 던지는 경우에는 Class<? extends Throwable> 이것이 중요.

이번에는 단일 아닌 배열을 받는 경우

import java.lang.annotation.*;

// 코드 39-6 배열 매개변수를 받는 애너테이션 타입 (242쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}
package effectivejava.chapter6.item39.annotationwitharrayparameter;
import effectivejava.chapter6.item39.markerannotation.Test;

import java.lang.reflect.*;

// 마커 애너테이션과 배열 매개변수를 받는 애너테이션을 처리하는 프로그램 (243쪽)
public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            ...

            // 배열 매개변수를 받는 애너테이션을 처리하는 코드 (243쪽)
            if (m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
                } catch (Throwable wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    int oldPassed = passed;
                    Class<? extends Throwable>[] excTypes =
                            m.getAnnotation(ExceptionTest.class).value();
                    for (Class<? extends Throwable> excType : excTypes) {
                        if (excType.isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPassed)
                        System.out.printf("테스트 %s 실패: %s %n", m, exc);
                }
            }
        }
        System.out.printf("성공: %d, 실패: %d%n",
                passed, tests - passed);
    }
}

자바8에서는 여러 값을 받는 애너테이션을 다른 방식으로도 만들 수 있다. 배열 매개변수를 사용하는 대신 애너테이션에 @Repeatable 메타애너테이션을 다는 방식.

import java.lang.annotation.*;

// 코드 39-8 반복 가능한 애너테이션 타입 (243-244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}
import java.lang.annotation.*;

// 코드 39-8 반복 가능한 애너테이션 타입 (243-244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

// 코드 39-10 반복 가능 애너테이션 다루기 (244-245쪽)
if (m.isAnnotationPresent(ExceptionTest.class)
    || m.isAnnotationPresent(ExceptionTestContainer.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
  } catch (Throwable wrappedExc) {
    Throwable exc = wrappedExc.getCause();
    int oldPassed = passed;
    ExceptionTest[] excTests =
      m.getAnnotationsByType(ExceptionTest.class);
    for (ExceptionTest excTest : excTests) {
      if (excTest.value().isInstance(exc)) {
        passed++;
        break;
      }
    }
    if (passed == oldPassed)
      System.out.printf("테스트 %s 실패: %s %n", m, exc);
  }
}
  • 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.
  • 자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들을 사용해야 한다.

아이템 40. @Override 애너테이션을 일관되게 사용하라.

@Override 의 힘?

import java.util.*;

// 코드 40-1 영어 알파벳 2개로 구성된 문자열(바이그램)을 표현하는 클래스 - 버그를 찾아보자. (246쪽)
public class Bigram {
    private final char first;
    private final char second;

    public Bigram(char first, char second) {
        this.first  = first;
        this.second = second;
    }

    public boolean equals(Bigram b) {
        return b.first == first && b.second == second;
    }

    public int hashCode() {
        return 31 * first + second;
    }

    public static void main(String[] args) {
        Set<Bigram> s = new HashSet<>();
        for (int i = 0; i < 10; i++)
            for (char ch = 'a'; ch <= 'z'; ch++)
                s.add(new Bigram(ch, ch));
        System.out.println(s.size());
    }
}

위 코드에서 equals에 Override가 없는 것이 어떤 문제를 일으킬까?

마지막 print는 에서는 260이 찍히게 될 것.

Override가 없으면, Obejct의 객체가 아닌, Bigram의 식별성을 확인하는 다중정의(oveloading)를 해버린것과 같다. 따라서, 같은 소문자를 소유한 바이그램 10개 각각이 서로 다른 객체로 인식되고, 결국 260을 출력한 것.

명확하게는

@Override public boolean equals(Object o) {
        if (!(o instanceof Bigram2))
            return false;
        Bigram2 b = (Bigram2) o;
        return b.first == first && b.second == second;
    }

위와 같이 되어야 한다.

아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라.

마커 인터페이스란? 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스(marker interface)라 한다.

정확하게 마커 인터페이스마커 애노테이션은 다른 것이다.

확실하게 이해될 수 있는 예시로 Serializable 인터페이스가 좋다. 자신을 구현한 클래스의 인스턴스는 ObjectOutputStream을 통해 쓸(write)수 있다고, 즉 직렬화(serialization) 할 수 있다고 알려준다.

마커 인터페이스는 두 가지 면에서 마커 애노테이션 보다 좋다.

  1. 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 인터페이스는 그렇지 않다. 마커 인터페이스는 어엿한 타입이기 때문에, 마커 애너테이션을 사용했다면 런타임에야 발견될 오류를 컴파일타임에 잡을 수 있다.

  2. 적용 대상을 더 정밀하게 지정할 수 있다는 것. 예를 들어, 적용 대상(@target)을 ElementType.TYPE 으로 선언한 애너테이션은 모든 타입(클래스, 인터페이스, 열거 타입, 애너테이션)에 달 수 있다. 즉 부착할 수 있는 타입을 더 세밀하게 제한하지는 못한다는 뜻.

    그런데, 특정 인터페이스를 구현한 클래스에만 적용하고 싶은 마커가 있을 경우, 그냥 마킹하고 싶은 클래스에서만 그 인터페이스를 구현(인터페이스라면 확장)하면 된다.

이번에는 반대로마커 애너테이션이 마커 인터페이스보다 나은 점으로는 거대한 애너테이션 시스템의 지원을 받는다는 점을 들 수 있다.

⚠️ **GitHub.com Fallback** ⚠️