diff --git "a/docs/\354\225\204\354\235\264\355\205\234 37. ordinal \354\235\270\353\215\261\354\213\261 \353\214\200\354\213\240 EnumMap\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 37. ordinal \354\235\270\353\215\261\354\213\261 \353\214\200\354\213\240 EnumMap\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..79dfada --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 37. ordinal \354\235\270\353\215\261\354\213\261 \353\214\200\354\213\240 EnumMap\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,227 @@ +# 아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라 + +```java +// 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; + } +} +``` + +```java +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[] plantsByLifeCycleArr = (Set[]) new Set[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", LifeCycle.values()[i], plantsByLifeCycleArr[i]); + } +} +``` + +- ordinal() 값을 배열의 인덱스로 사용하는 경우 다음과 같은 **문제점**이 있다. + - 배열은 제네릭과 호환되지 않아(아이템 28) 비검사 형변환을 수행해야한다. + - 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야한다. + - 잘못된 인덱스를 사용하는 경우 ArrayIndexOutOfBoundsException이 발생할 수 있다. + +```java +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-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다. (227쪽) + Map> plantsByLifeCycle = new EnumMap<>(LifeCycle.class); + for (LifeCycle lc : LifeCycle.values()) { + plantsByLifeCycle.put(lc, new HashSet<>()); + } + for (Plant p : garden) { + plantsByLifeCycle.get(p.lifeCycle).add(p); + } + System.out.println(plantsByLifeCycle); +} +``` + +- EnumMap은 열거 타입을 키로 사용하도록 설계된 아주 빠른 Map 구현체이다. +- 배열을 사용했을 때보다 다음과 같은 개선점이 있다. + - 코드가 더 짧고 안전하고 성능도 비등하다. + - 안전하지 않은 형변환을 사용하지 않는다. + - 배열 인덱스를 계산하는 과정에서 오류가 발생할 가능성이 없다. +- EnumMap의 성능이 ordinal을 사용한 배열과 비등한 이유는 내부에서 배열을 사용하기 때문이다. + +```java +public class EnumMap, V> extends AbstractMap implements java.io.Serializable, Cloneable { + /** + * The {@code Class} object for the enum type of all the keys of this map. + * + * @serial + */ + private final Class keyType; + + /** + * All of the values comprising K. (Cached for performance.) + */ + private transient K[] keyUniverse; + + /** + * Array representation of this map. The ith element is the value + * to which universe[i] is currently mapped, or null if it isn't + * mapped to anything, or NULL if it's mapped to null. + */ + private transient Object[] vals; + + /** + * The number of mappings in this map. + */ + private transient int size = 0; + + /** + * Creates an empty enum map with the specified key type. + * + * @param keyType the class object of the key type for this enum map + * @throws NullPointerException if {@code keyType} is null + */ + public EnumMap(Class keyType) { + this.keyType = keyType; + keyUniverse = getKeyUniverse(keyType); + vals = new Object[keyUniverse.length]; + } + ... +} +``` + +- EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다(아이템 33) + +```java +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-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()))); +} +``` + +- 코드 37-3의 경우 EnumMap이 아닌 고유한 맵 구현체를 사용했기 때문에 EnumMap의 이점이 사라진다. +- 코드 37-4와 같이 맵 구현체를 지정하는 것이 가능하다. +- EnumMap만 사용했을때와 다르게 스트림을 사용한 경우, 데이터가 존재하는 경우에만 생성하는 차이점이 있다. + +```java +public enum BadPhase { + SOLID, LIQUID, GAS; + + public enum Transition { + MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; + + private static final Transition[][] TRANSITIONS = { + {null, MELT, SUBLIME}, + {FREEZE, null, BOIL}, + {DEPOSIT, CONDENSE, null}, + }; + + public static Transition from(BadPhase from, BadPhase to) { + return TRANSITIONS[from.ordinal()][to.ordinal()]; + } + } +} +``` + +- 컴파일러는 ordinal과 배열 인덱스의 관계를 알 수 없다. +- BadPhase.Transition 열거 타입을 수정하면서 TRANSITIONS 필드에 정의된 내용을 수정하지 않으면 런타임 오류가 발생 할 것이다. + - IndexOutOfBoundsException, NPE 등이 발생할 수 있다. + +```java +// 코드 37-6 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다. (229-231쪽) +public enum Phase { + SOLID, LIQUID, GAS; + + public enum Transition { + MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), + BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), + SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID); + +// // 코드 37-7 EnumMap 버전에 새로운 상태 추가하기 (231쪽) +// SOLID, LIQUID, GAS, PLASMA; +// public enum Transition { +// MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), +// BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), +// SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID), +// IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS); + + private final Phase from; + private final Phase to; + + Transition(Phase from, Phase to) { + this.from = from; + this.to = to; + } + + // 상전이 맵을 초기화한다. + private static final Map> + m = Stream.of(values()).collect(groupingBy(t -> t.from, + () -> new EnumMap<>(Phase.class), + toMap(t -> t.to, t -> t, + (x, y) -> y, () -> new EnumMap<>(Phase.class)))); + + public static Transition from(Phase from, Phase to) { + return m.get(from).get(to); + } + } + + // 간단한 데모 프로그램 - 깔끔하지 못한 표를 출력한다. + public static void main(String[] args) { + for (Phase src : Phase.values()) { + for (Phase dst : Phase.values()) { + Transition transition = Transition.from(src, dst); + if (transition != null) + System.out.printf("%s에서 %s로 : %s %n", src, dst, transition); + } + } + } +} +``` + +- 배열을 사용하기 보단 EnumMap을 활용하는 것이 좋다. \ No newline at end of file diff --git "a/docs/\354\225\204\354\235\264\355\205\234 38. \355\231\225\354\236\245\355\225\240 \354\210\230 \354\236\210\353\212\224 \354\227\264\352\261\260 \355\203\200\354\236\205\354\235\264 \355\225\204\354\232\224\355\225\230\353\251\264 \354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 38. \355\231\225\354\236\245\355\225\240 \354\210\230 \354\236\210\353\212\224 \354\227\264\352\261\260 \355\203\200\354\236\205\354\235\264 \355\225\204\354\232\224\355\225\230\353\251\264 \354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..005a41d --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 38. \355\231\225\354\236\245\355\225\240 \354\210\230 \354\236\210\353\212\224 \354\227\264\352\261\260 \355\203\200\354\236\205\354\235\264 \355\225\204\354\232\224\355\225\230\353\251\264 \354\235\270\355\204\260\355\216\230\354\235\264\354\212\244\353\245\274 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,202 @@ +# 아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 + +```java +// 타입 안전 열거 패턴 +public class TypesafeOperation { + private final String type; + private TypesafeOperation(String type) { + this.type = type; + } + + public String toString() { + return type; + } +} + +public static final TypesafeOperation PLUS = new TypesafeOperation("+"); +public static final TypesafeOperation MINUS = new TypesafeOperation("-"); +public static final TypesafeOperation TIMES = new TypesafeOperation("*"); +public static final TypesafeOperation DIVIDE = new TypesafeOperation("/"); +``` + +열거 타입은 거의 모든 상황에서 **타입 안전 열거 패턴(typesafe enum pattern)**보다 우수하다. 단, 타입 안전 열거 패턴은 확장할 수 있으나 열거 타입은 그럴 수 없다.(열거 타입은 상속이 불가하다는 의미로 이해) + +열거 타입은 확장하는 것은 좋지 않다. + +- 확장한 타입의 원소는 기반 타입의 원소로 취급하지만 반대는 성립하지 않는다. +- 기반 타입과 확장된 타입들의 원소 모두를 순회할 방법이 마땅치 않다. +- API가 제공하는 기본 연산 외에 사용자 확장 연산을 추가할 수 있도록 열어줘야 할 수 도 있다. + +```java +// 코드 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; + } +} + +// 코드 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 & Operation> void test(Class 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)); + } + } +} +``` + +- 열거 타입의 임의의 인터페이스를 구현하는 것으로 위 문제를 해결할 수 있다. +- test 메서드에 class 리터럴은 한정적 타입 토큰(아이템 33)역할을 한다. +- opEnumType의 타입( & Operation>)의 경우 Class 객체가 열거 타입인 동시에 Operation의 하위 타입이어야 한다는 의미 + +```java +// 컬렉션 인스턴스를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예 (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 opSet, double x, double y) { + for (Operation op : opSet) { + System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)); + } +} +``` + +- 위 코드는 Class 객체 대신 한정적 와일드카드 타입(아이템 31)인 Collection을 넘기는 방법이다. +- 특정 연산에서 EnumSet과 EnumMap을 사용하지 못한다. + +인터페이스를 이용해 확장 가능한 열거 타입을 흉내 내는 방식에 한 가지 문제가 있다. + +- 열거 타입끼리 구현을 상속할 수 없다. + +이에 대한 대안으로 인터페이스에 **디폴트 메소드(아이템 20)**을 추가하는 방안이 있긴하다. + +자바 라이브러리 중 위 내용을 적용한 예시가 있다. java.nio.file.LinkOption 열거타입은 CopyOption과 OpenOption 인터페이스를 구현했으니 참고 + +```java +package java.nio.file; + +/** + * An object that configures how to open or create a file. + * + *

Objects of this type are used by methods such as {@link + * Files#newOutputStream(Path,OpenOption[]) newOutputStream}, {@link + * Files#newByteChannel newByteChannel}, {@link + * java.nio.channels.FileChannel#open FileChannel.open}, and {@link + * java.nio.channels.AsynchronousFileChannel#open AsynchronousFileChannel.open} + * when opening or creating a file. + * + *

The {@link StandardOpenOption} enumeration type defines the + * standard options. + * + * @since 1.7 + */ + +public interface OpenOption { +} +``` + +```java +package java.nio.file; + +/** + * An object that configures how to copy or move a file. + * + *

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. + * + *

The {@link StandardCopyOption} enumeration type defines the + * standard options. + * + * @since 1.7 + */ + +public interface CopyOption { +} +``` + +```java +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; +} +``` \ No newline at end of file diff --git "a/docs/\354\225\204\354\235\264\355\205\234 39. \353\252\205\353\252\205 \355\214\250\355\204\264\353\263\264\353\213\244 \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" "b/docs/\354\225\204\354\235\264\355\205\234 39. \353\252\205\353\252\205 \355\214\250\355\204\264\353\263\264\353\213\244 \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" new file mode 100644 index 0000000..3ff7ed2 --- /dev/null +++ "b/docs/\354\225\204\354\235\264\355\205\234 39. \353\252\205\353\252\205 \355\214\250\355\204\264\353\263\264\353\213\244 \354\225\240\353\204\210\355\205\214\354\235\264\354\205\230\354\235\204 \354\202\254\354\232\251\355\225\230\353\235\274.md" @@ -0,0 +1,310 @@ +# 아이템 39. 명명 패턴보다 애너테이션을 사용하라 + +과거 프레임워크 등의 프로그램은 명명 패턴을 적용해왔다. 예컨데 테스트 프레임워크인 JUnit 3의 경우 테스트 메서드 이름을 test로 시작하게끔 했다. 그러나 명명 패턴은 여러 단점을 가진다. + +- 오타가 나면 안 된다. +- 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다. + - 만약 개발자가 TestMemberSave라는 클래스를 정의하고 클래스에 정의된 메서드들을 테스트 하길 기대한다고 하자. 그러나, JUnit은 클래스 이름에 관심이 없기 때문에 개발자가 의도하는 테스트를 수행할 수 없다. +- 프로그램 요소를 매개변수로 전달할 방법이 없다. + - 특정 예외를 던져야 성공하는 테스트의 경우, 예외 타입을 테스트 메서드에 매개변수로 전달해야하는 상황이다. 메서드 이름에 예외 이름을 지정하는 방법도 있지만 깨지기 쉽고 컴파일러가 예외인지 알 수 없는 문제가있다. + +애너테이션[JLS, 9.7]은 이 모든 문제를 해결해준다. + +```java +/** + * 테스트 메서드임을 선언하는 애너테이션이다. + * 매개변수 없는 정적 메서드 전용이다. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Test { +} +``` + +- @Retention, @Target과 같은 애너테이션에 다는 애너테이션은 메타애너테이션(meta-annotation)이라 한다. + - @Retention은 런타임에도 유지되어야 한다는 표시 + - @Target은 반드시 메서드 선언에만 사용돼야 한다는 표기 +- javax.annotation.processing API 문서 참고 + +```java +// 코드 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() { + } +} +``` + +- @Test와 같이 “아무 매개변수 없이 단순히 대상에 마킹(marking)한다”는 뜻에서 마커(marker) 애너테이션이라 한다. +- @Test 애너테이션이 클래스에 직접적인 영향을 주지는 않는다. + - 추가적인 정보를 제공할 뿐이다. + +```java +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +// 코드 39-3 마커 애너테이션을 처리하는 프로그램 (239-240쪽) +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); + } +} +``` + +- 해당 클래스는 명령줄로부터 완전 정규화된 클래스 이름을 받아, 그 클래스에서 @Test 애너테이션이 달린 메서드를 차례로 호출한다. + +```java +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// 코드 39-4 매개변수 하나를 받는 애너테이션 타입 (240-241쪽) +/** + * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ExceptionTest { + Class value(); +} +``` + +- 특정 예외를 처리할 수 있도록 하는 애너테이션 타입 정의 +- Class에서 와일드카드 타입은 “Throwable을 확장한 클래스의 Class 객체”라는 뜻이며, 따라서 모든 예외(와 오류) 타입을 다 수용한다. + - 한정적 타입 토큰(아이템 33)의 또 하나의 활용 사례다. + +```java +// 코드 39-5 매개변수 하나짜리 애너테이션을 사용한 프로그램 (241쪽) +public class Sample2 { + @ExceptionTest(ArithmeticException.class) + public static void m1() { // 성공해야 한다. + int i = 0; + i = i / i; + } + + @ExceptionTest(ArithmeticException.class) + public static void m2() { // 실패해야 한다. (다른 예외 발생) + int[] a = new int[0]; + int i = a[1]; + } + + @ExceptionTest(ArithmeticException.class) + public static void m3() { + } // 실패해야 한다. (예외가 발생하지 않음) +} +``` + +- 특정 예외를 처리하는 애너테이션 적용 예시 + +```java +import item39.markerannotation.Test; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +// 마커 애너테이션과 매개변수 하나짜리 애너태이션을 처리하는 프로그램 (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(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); + } + } + + if (m.isAnnotationPresent(ExceptionTest.class)) { + tests++; + try { + m.invoke(null); + System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m); + } catch (InvocationTargetException wrappedEx) { + Throwable exc = wrappedEx.getCause(); + Class 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); + } +} +``` + +```java +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// 코드 39-6 배열 매개변수를 받는 애너테이션 타입 (242쪽) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ExceptionTest { + Class[] value(); +} +``` + +- 예외를 여러 개 명시하고 그중 하나가 발생하면 성공하도록 배열로 선언 + +```java +import java.util.ArrayList; +import java.util.List; + +// 배열 매개변수를 받는 애너테이션을 사용하는 프로그램 (242-243쪽) +public class Sample3 { + + // 코드 39-7 배열 매개변수를 받는 애너테이션을 사용하는 코드 (242-243쪽) + @ExceptionTest({IndexOutOfBoundsException.class, + NullPointerException.class}) + public static void doublyBad() { // 성공해야 한다. + List list = new ArrayList<>(); + + // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나 + // NullPointerException을 던질 수 있다. + list.addAll(5, null); + } +} +``` + +```java +// 배열 매개변수를 받는 애너테이션을 처리하는 코드 (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[] excTypes = + m.getAnnotation(ExceptionTest.class).value(); + for (Class excType : excTypes) { + if (excType.isInstance(exc)) { + passed++; + break; + } + } + if (passed == oldPassed) + System.out.printf("테스트 %s 실패: %s %n", m, exc); + } +} +``` + +```java +import java.lang.annotation.*; + +// 코드 39-8 반복 가능한 애너테이션 타입 (243-244쪽) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Repeatable(ExceptionTestContainer.class) +public @interface ExceptionTest { + Class value(); +} +``` + +- 자바 8에서는 여러 개의 값을 받는 애너테이션을 다른 방식으로도 만들 수 있다. + - 배열 매개변수를 사용하는 대신 애너테이션에 @Repeatable 매타애너테이션을 다는 방식이다. + +```java +// 반복 가능한 애너테이션의 컨테이너 애너테이션 (244쪽) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ExceptionTestContainer { + ExceptionTest[] value(); +} +``` + +- 반복 가능 애너테이션을 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 ‘컨테이너’ 애너테이션 타입이 적용된다. + +```java +// 코드 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); + } +} +``` + +- 반복 가능 애너테이션을 처리하도록 수정한 코드 + +

\ No newline at end of file diff --git a/src/main/java/item37/BadPhase.java b/src/main/java/item37/BadPhase.java new file mode 100644 index 0000000..ba9fac4 --- /dev/null +++ b/src/main/java/item37/BadPhase.java @@ -0,0 +1,19 @@ +package item37; + +public enum BadPhase { + SOLID, LIQUID, GAS; + + public enum Transition { + MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; + + private static final Transition[][] TRANSITIONS = { + {null, MELT, SUBLIME}, + {FREEZE, null, BOIL}, + {DEPOSIT, CONDENSE, null}, + }; + + public static Transition from(BadPhase from, BadPhase to) { + return TRANSITIONS[from.ordinal()][to.ordinal()]; + } + } +} diff --git a/src/main/java/item37/Phase.java b/src/main/java/item37/Phase.java new file mode 100644 index 0000000..cedeaa3 --- /dev/null +++ b/src/main/java/item37/Phase.java @@ -0,0 +1,57 @@ +package item37; + +import java.util.EnumMap; +import java.util.Map; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toMap; + +// 코드 37-6 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다. (229-231쪽) +public enum Phase { + SOLID, LIQUID, GAS; + + public enum Transition { + MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), + BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), + SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID); + +// // 코드 37-7 EnumMap 버전에 새로운 상태 추가하기 (231쪽) +// SOLID, LIQUID, GAS, PLASMA; +// public enum Transition { +// MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), +// BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), +// SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID), +// IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS); + + private final Phase from; + private final Phase to; + + Transition(Phase from, Phase to) { + this.from = from; + this.to = to; + } + + // 상전이 맵을 초기화한다. + private static final Map> + m = Stream.of(values()).collect(groupingBy(t -> t.from, + () -> new EnumMap<>(Phase.class), + toMap(t -> t.to, t -> t, + (x, y) -> y, () -> new EnumMap<>(Phase.class)))); + + public static Transition from(Phase from, Phase to) { + return m.get(from).get(to); + } + } + + // 간단한 데모 프로그램 - 깔끔하지 못한 표를 출력한다. + public static void main(String[] args) { + for (Phase src : Phase.values()) { + for (Phase dst : Phase.values()) { + Transition transition = Transition.from(src, dst); + if (transition != null) + System.out.printf("%s에서 %s로 : %s %n", src, dst, transition); + } + } + } +} diff --git a/src/main/java/item37/Plant.java b/src/main/java/item37/Plant.java new file mode 100644 index 0000000..a0b11a6 --- /dev/null +++ b/src/main/java/item37/Plant.java @@ -0,0 +1,69 @@ +package 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[] plantsByLifeCycleArr = (Set[]) new Set[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", + LifeCycle.values()[i], plantsByLifeCycleArr[i]); + } + + // 코드 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다. (227쪽) + Map> plantsByLifeCycle = new EnumMap<>(LifeCycle.class); + for (LifeCycle lc : 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()))); + } +} diff --git a/src/main/java/item38/BasicOperation.java b/src/main/java/item38/BasicOperation.java new file mode 100644 index 0000000..66ed1e6 --- /dev/null +++ b/src/main/java/item38/BasicOperation.java @@ -0,0 +1,36 @@ +package item38; + +// 코드 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; + } +} diff --git a/src/main/java/item38/ExtendedOperation.java b/src/main/java/item38/ExtendedOperation.java new file mode 100644 index 0000000..801b907 --- /dev/null +++ b/src/main/java/item38/ExtendedOperation.java @@ -0,0 +1,53 @@ +package item38; + +import java.util.Arrays; +import java.util.Collection; + +// 코드 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 & Operation> void test(Class 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 opSet, double x, double y) { + for (Operation op : opSet) { + System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)); + } + } +} diff --git a/src/main/java/item38/Operation.java b/src/main/java/item38/Operation.java new file mode 100644 index 0000000..24b97c8 --- /dev/null +++ b/src/main/java/item38/Operation.java @@ -0,0 +1,6 @@ +package item38; + +// 코드 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다. (232쪽) +public interface Operation { + double apply(double x, double y); +} diff --git a/src/main/java/item39/annotationwitharrayparameter/ExceptionTest.java b/src/main/java/item39/annotationwitharrayparameter/ExceptionTest.java new file mode 100644 index 0000000..407b95c --- /dev/null +++ b/src/main/java/item39/annotationwitharrayparameter/ExceptionTest.java @@ -0,0 +1,13 @@ +package item39.annotationwitharrayparameter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// 코드 39-6 배열 매개변수를 받는 애너테이션 타입 (242쪽) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ExceptionTest { + Class[] value(); +} diff --git a/src/main/java/item39/annotationwitharrayparameter/RunTests.java b/src/main/java/item39/annotationwitharrayparameter/RunTests.java new file mode 100644 index 0000000..7de2010 --- /dev/null +++ b/src/main/java/item39/annotationwitharrayparameter/RunTests.java @@ -0,0 +1,53 @@ +package item39.annotationwitharrayparameter; + +import item39.markerannotation.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +// 마커 애너테이션과 배열 매개변수를 받는 애너테이션을 처리하는 프로그램 (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()) { + 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); + } + } + + // 배열 매개변수를 받는 애너테이션을 처리하는 코드 (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[] excTypes = + m.getAnnotation(ExceptionTest.class).value(); + for (Class 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); + } +} diff --git a/src/main/java/item39/annotationwitharrayparameter/Sample3.java b/src/main/java/item39/annotationwitharrayparameter/Sample3.java new file mode 100644 index 0000000..b6c2de8 --- /dev/null +++ b/src/main/java/item39/annotationwitharrayparameter/Sample3.java @@ -0,0 +1,35 @@ +package item39.annotationwitharrayparameter; + +import java.util.ArrayList; +import java.util.List; + +// 배열 매개변수를 받는 애너테이션을 사용하는 프로그램 (242-243쪽) +public class Sample3 { + // 이 변형은 원소 하나짜리 매개변수를 받는 애너테이션도 처리할 수 있다. (241쪽 Sample2와 같음) + @ExceptionTest(ArithmeticException.class) + public static void m1() { // 성공해야 한다. + int i = 0; + i = i / i; + } + + @ExceptionTest(ArithmeticException.class) + public static void m2() { // 실패해야 한다. (다른 예외 발생) + int[] a = new int[0]; + int i = a[1]; + } + + @ExceptionTest(ArithmeticException.class) + public static void m3() { + } // 실패해야 한다. (예외가 발생하지 않음) + + // 코드 39-7 배열 매개변수를 받는 애너테이션을 사용하는 코드 (242-243쪽) + @ExceptionTest({IndexOutOfBoundsException.class, + NullPointerException.class}) + public static void doublyBad() { // 성공해야 한다. + List list = new ArrayList<>(); + + // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나 + // NullPointerException을 던질 수 있다. + list.addAll(5, null); + } +} diff --git a/src/main/java/item39/annotationwithparameter/ExceptionTest.java b/src/main/java/item39/annotationwithparameter/ExceptionTest.java new file mode 100644 index 0000000..3b5ba9a --- /dev/null +++ b/src/main/java/item39/annotationwithparameter/ExceptionTest.java @@ -0,0 +1,16 @@ +package item39.annotationwithparameter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// 코드 39-4 매개변수 하나를 받는 애너테이션 타입 (240-241쪽) +/** + * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ExceptionTest { + Class value(); +} diff --git a/src/main/java/item39/annotationwithparameter/RunTests.java b/src/main/java/item39/annotationwithparameter/RunTests.java new file mode 100644 index 0000000..6146baf --- /dev/null +++ b/src/main/java/item39/annotationwithparameter/RunTests.java @@ -0,0 +1,51 @@ +package item39.annotationwithparameter; + +import item39.markerannotation.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +// 마커 애너테이션과 매개변수 하나짜리 애너태이션을 처리하는 프로그램 (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(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); + } + } + + if (m.isAnnotationPresent(ExceptionTest.class)) { + tests++; + try { + m.invoke(null); + System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m); + } catch (InvocationTargetException wrappedEx) { + Throwable exc = wrappedEx.getCause(); + Class 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); + } +} diff --git a/src/main/java/item39/annotationwithparameter/Sample2.java b/src/main/java/item39/annotationwithparameter/Sample2.java new file mode 100644 index 0000000..1d0682c --- /dev/null +++ b/src/main/java/item39/annotationwithparameter/Sample2.java @@ -0,0 +1,20 @@ +package item39.annotationwithparameter; + +// 코드 39-5 매개변수 하나짜리 애너테이션을 사용한 프로그램 (241쪽) +public class Sample2 { + @ExceptionTest(ArithmeticException.class) + public static void m1() { // 성공해야 한다. + int i = 0; + i = i / i; + } + + @ExceptionTest(ArithmeticException.class) + public static void m2() { // 실패해야 한다. (다른 예외 발생) + int[] a = new int[0]; + int i = a[1]; + } + + @ExceptionTest(ArithmeticException.class) + public static void m3() { + } // 실패해야 한다. (예외가 발생하지 않음) +} diff --git a/src/main/java/item39/markerannotation/RunTests.java b/src/main/java/item39/markerannotation/RunTests.java new file mode 100644 index 0000000..26e66dd --- /dev/null +++ b/src/main/java/item39/markerannotation/RunTests.java @@ -0,0 +1,28 @@ +package item39.markerannotation; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +// 코드 39-3 마커 애너테이션을 처리하는 프로그램 (239-240쪽) +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); + } +} diff --git a/src/main/java/item39/markerannotation/Sample.java b/src/main/java/item39/markerannotation/Sample.java new file mode 100644 index 0000000..ac80f6a --- /dev/null +++ b/src/main/java/item39/markerannotation/Sample.java @@ -0,0 +1,34 @@ +package item39.markerannotation; + +// 코드 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() { + } +} diff --git a/src/main/java/item39/markerannotation/Test.java b/src/main/java/item39/markerannotation/Test.java new file mode 100644 index 0000000..8ff8817 --- /dev/null +++ b/src/main/java/item39/markerannotation/Test.java @@ -0,0 +1,18 @@ +package item39.markerannotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// 코드 39-1 마커(marker) 애너테이션 타입 선언 (238쪽) + + +/** + * 테스트 메서드임을 선언하는 애너테이션이다. + * 매개변수 없는 정적 메서드 전용이다. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Test { +} diff --git a/src/main/java/item39/repeatableannotation/ExceptionTest.java b/src/main/java/item39/repeatableannotation/ExceptionTest.java new file mode 100644 index 0000000..38f1fe1 --- /dev/null +++ b/src/main/java/item39/repeatableannotation/ExceptionTest.java @@ -0,0 +1,11 @@ +package item39.repeatableannotation; + +import java.lang.annotation.*; + +// 코드 39-8 반복 가능한 애너테이션 타입 (243-244쪽) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Repeatable(ExceptionTestContainer.class) +public @interface ExceptionTest { + Class value(); +} diff --git a/src/main/java/item39/repeatableannotation/ExceptionTestContainer.java b/src/main/java/item39/repeatableannotation/ExceptionTestContainer.java new file mode 100644 index 0000000..d6d82cb --- /dev/null +++ b/src/main/java/item39/repeatableannotation/ExceptionTestContainer.java @@ -0,0 +1,13 @@ +package item39.repeatableannotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// 반복 가능한 애너테이션의 컨테이너 애너테이션 (244쪽) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ExceptionTestContainer { + ExceptionTest[] value(); +} diff --git a/src/main/java/item39/repeatableannotation/RunTests.java b/src/main/java/item39/repeatableannotation/RunTests.java new file mode 100644 index 0000000..d3def1d --- /dev/null +++ b/src/main/java/item39/repeatableannotation/RunTests.java @@ -0,0 +1,53 @@ +package item39.repeatableannotation; + +import item39.markerannotation.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +// 마커 애너테이션과 반복 가능 애너테이션을 처리하는 프로그램 (244-245쪽) +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); + } + } + + // 코드 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); + } + } + } + System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed); + } +} diff --git a/src/main/java/item39/repeatableannotation/Sample4.java b/src/main/java/item39/repeatableannotation/Sample4.java new file mode 100644 index 0000000..e707b5b --- /dev/null +++ b/src/main/java/item39/repeatableannotation/Sample4.java @@ -0,0 +1,33 @@ +package item39.repeatableannotation; + +import java.util.ArrayList; +import java.util.List; + +// 반복 가능한 애너테이션을 사용한 프로그램 (244쪽) +public class Sample4 { + @ExceptionTest(ArithmeticException.class) + public static void m1() { // 성공해야 한다. + int i = 0; + i = i / i; + } + + @ExceptionTest(ArithmeticException.class) + public static void m2() { // 실패해야 한다. (다른 예외 발생) + int[] a = new int[0]; + int i = a[1]; + } + + @ExceptionTest(ArithmeticException.class) + public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음) + + // 코드 39-9 반복 가능 애너테이션을 두 번 단 코드 (244쪽) + @ExceptionTest(IndexOutOfBoundsException.class) + @ExceptionTest(NullPointerException.class) + public static void doublyBad() { + List list = new ArrayList<>(); + + // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나 + // NullPointerException을 던질 수 있다. + list.addAll(5, null); + } +}