[6주차] 37-44

Joshua Bloch 저 이복연 역 「Effective Java 3rd Edition, 2018」를 읽고 정리하였습니다.

37 ordinal 인덱싱 대신 EnumMap을 사용하라

아래 코드 문제

  • 배열은 제너릭과 호환되지 않음. (비검사형변환 필요) <>[]

  • 배열은 각 인덱스 의미를 모르니 출력결과에 직접 레이블을 달아야 한다. printf 부분.

  • 정확한 정숫값을 사용한다는 보장을 해야된다. 정수는 열거타입과 달리 안전하지 않기때문이다.

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toSet;
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("Basil",    LifeCycle.ANNUAL),
                new Plant("Carroway", LifeCycle.BIENNIAL),
                new Plant("Dill",     LifeCycle.ANNUAL),
                new Plant("Lavendar", LifeCycle.PERENNIAL),
                new Plant("Parsley",  LifeCycle.BIENNIAL),
                new Plant("Rosemary", LifeCycle.PERENNIAL)
        };

        // 나쁜 예: 배열 인덱스에 ordinal() 사용하지 x
        Set<Plant>[] plantsByLifeCycleArr =
                (Set<Plant>[]) new Set[LifeCycle.values().length];
        for (int i = 0; i < plantsByLifeCycleArr.length; i++)
            plantsByLifeCycleArr[i] = new HashSet<>();
        for (Plant p : garden){
            // p.lifeCycle.ordinal() -> ANNUAL:0, PERENNIAL:1, BIENNIAL:2
            plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p);            
        }
        for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
            //Set에 저장된 해시셋 묶음별로 출력
            //출력결과에 레이블을 단다
            System.out.printf("%s: %s%n",
                    LifeCycle.values()[i], plantsByLifeCycleArr[i]); 
        }
}

위같은 Set 대신 열거 타입을 키로 사용하도록 설계한 아주빠른 Map 구현체 EnumMap을 사용하자.

장점을 보면,

  • 안전하지 않은 형변환 쓰지 않고, (EnumMap 따로 변환 x)

  • 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력에서 직접 레이블 달 필요 x (plantsByLifeCycle만 출력하면 key-value 같이 출력된다.)

  • 배열 인덱스 계산 과정 오류날 가능성 zero

요약하면, EnumMap은 내부에서 배열을 사용하기에 내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.

내생각 - EnumMap은 Enum 타입의 값을 키로 담고 처리하기에 좋은 자료구조 인거 같다.

스트림으로 더 간단히 만들면 아래와 같다.

스트림을 사용하면 원래 EnumMap과는 약간 다르게 동작 스트림은 생애주기에 속하는 식물이 없으면 중첩맵을 만들지 않는다.

용어 정리

고체 -> 액체 Melting 액체 -> 가스 Evaporation 고체 -> 가스 Sublimation 액체 -> 고체 Freezing 가스 -> 액체 Condensation 가스 -> 고체 Deposition

아래 두 열거 타입 값들을 매핑하느라 ordinal을 두번이나 쓴 배열들의 배열을 보자. 전과 마찬가지로 컴파일러는 ordinal과 배열 인덱스의 관계를 알 도리가 없다. ArrayIndexOutOfBound나 NullPointerException 발생 가능성 있다.

EnumMap 사용하자. 중첩 EnumMap으로 enum 쌍과 데이터를 연결했다.

  • groupBy는 transition을 이전 상태 기준으로 묶는다.

  • toMap에서는 이후상태를 transition에 대응 시키는 EnumMap을 생성한다.

  • 두번째 수집기 (x, y) -> y 는 실제로 쓰이진 않는다.

상태 PLASMA를 추가하고 transition에 IONIZE와 DEIONIZE를 추가 할 경우도 마찬가지로. ordinal을 사용한것에 비해 EnumMap을 사용하는게 더 코드 수정도 쉽다.

요약

배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니 대신 EnumMap을 사용하자.

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

핵심 정리

열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거타입을 함께 사용해 같은 효과를 낼 수 있다.

타입 안전 열거패턴은 확장할 수 있으나 열거타입은 그럴 수 없다. (타입 안전 열거패턴: 열거한 값들 그대로 가져온 다음 값을 추가하여 다른 목적으로 쓸 수있다.)

열거타입을 확장하려는 건 좋지 않은 생각이다. 드물지만 확장된 열거타입 어울리는 곳이 있다. -> 연산 코드 (책코드, Operation-BasicOperation, ExtendedOperation)

enum이 인터페이스 구현했다. BasicOperation은 확장 x, Operation은 확장 o

아래 추가된 작성한 연산은 Operation인터페이스 사용하는 곳 어디에서도 쓰일 수 있다. apply가 인터페이스 에선되어 있으니 열거타입에 따로 추상메서드로 선언하지 않아도 된다.

개별 인스턴스 수준에서 뿐만 아니라 타입 수준에서도, 기본 열거 타입 대신 확장된 열거타입을 넘겨 확장된 열거타입의 원소 모두를 사용할게 할 수도 있다.

& Operation> 열거 타입이어야 원소를 순회할 수 있고 Operation이어야 원소가 뜻하는 연산을 수행할 수 있다.

두번째 대안. Class 객체 대신 한정적 와일드 카드 사용.

test가 더 유연해졌다. 여러 구현 타입의 연산을 조합해 호출할 수 있게 되었다.

인터페이스를 이용해 확장 가능한 열거타입을 흉내내어 봤다.

자바 라이브러리에서는 nio.file.LinkOption 열거타입이 CopyOption과 OpenOption 인터페이스를 구현했다.

39 명명패턴보다 애너테이션 사용 - 발

명명방식 문제점

1 tsetSafetyOverride -> 오타 때문에 JUnit3은 무시 -> 실패, 통과 여부 모름. 2 메서드가 아닌 클래스 TestSafetyMechanisms은 JUnit에서 관심이 없다. 3 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다. 뭔말?

JUnit4에서 모든 문제를 해결 -> 애너테이션이 등장

애너테이션 동작 방식을 보여주고자 직접 제작한 작은 테스트 프레임워크 사용할 것이다.

Test 애너테이션을 정의해보자.

메타 애너테이션 - 애너테이션의 애너테이션

스택오버플로우에 나오면 설명하기 - 주석: 매개변수 없는 정적 메서드 전용

The comment before the Test annotation declaration says, “Use only on parameterless static methods.” It would be nice if the compiler could enforce this, but it can’t, unless you write an annotation processor to do so. For more on this topic, see the documentation for javax.annotation.processing.

이제 @Test 애너테이션을 적용해보자. 특징, marker 애너테이션 = 아무 매개변수 없이 단순히 대상에 마킹한다는 뜻 Test 이름에 오타나 메서드 선언 외의 프로그램 요소에 달면 컴파일러 오류를 내준다.

문제 - 여기서 성공, 실패, 잘못 사용한 경우를 뽑아내시오.

현재 사용된 @Test는 Sample 클래스에 의미적으로 직접 영향을 주진 않고 애너태이션에 관심있는 프로그램에게 추가 정보를 제공할 뿐이다.

그 이유는 아래를 보자.

  • isAnnotationPresent -> 실행할 메서드를 찾는다.

  • 테스트 메서드 예외 던질 경우 -> InvocationTargetException으로 감싸서 다시 던져준다.

  • getCause - InvocationTargetException로 부터 원래 예외에 담긴 실패정보를 추출해 출력해준다.

InvocationTargetException 이외 예외 -> 인스턴스 메서드, 매개변수가 있는 메서드, 호출할 수 없는 메서드에 달았을 경우 생김. 위 Boom, Crash.

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

Throwable을 확장한 클래스의 객체를 던지는 즉, 모든 예외(와 오류) 타입을 수용한다. ( = 한정적 타입토큰 )

ArithmeticExceptionarrow-up-right devided by zero

메인에 추가할 사항

m1, m2, m3 결과를 보자

해당 예외의 클래스 파일이 컴파일타임에는 존재했으나 런타임에 존재하지 않을 경우 TypeNotPresentException이 발생할 것이다.

복수개 예외 매개변수로 사용시. 애너테이션.

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

주의할점은,

  • @Repeatable 반환하는 컨테이너 애너테이션 하나더 정의 후 @Repeatable에 이 컨테이너 애너테이션의 클래스 객체를 매개변수로 전달한다.

  • 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.

반복 가능 애너테이션을 여러번 단 다음 isAnnotationPresent로 반복 가능 애너테이션이 달렸는지 검사한다면 "그렇지 않다"라고 알려준다 (컨테이너때문). 그결과 애너테이션을 여러번 단 메서드들을 모두 무시하고 지나친다. 같은 이유 isAnnotationPresent로 컨테이너 애너테이션이 달렸는지 검사한다면 반복 가능 애너테이션을 한번만 단 메서드를 무시하고 지나간다. 그래서 달려있는 수와 상관없이 모두 검사하려면 둘을 따로따로 확인해야 한다.

따라서 반복 가능 애너테이션을 다루기 위한 코드는 다음과 같다

마지막 페이지 글 다시 읽자ㅎㅎ

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

일관되게 사용하면 버그를 줄인다. 아래 코드에서 버그를 찾아보자.

equals 매개변수 타입을 Object로 했어야 했다. 오버라이딩이 아닌 오버로딩(다중정의)를 하였다. == 식별성만 확인한다.

버그가 없게 잘 재정의 해보자.

핵심 정리

재정의한 모든 메서드에 @Override 애너테이션을 의식적으로 달면 실 수 했을때 컴파일러가 바로 알려줄 것이다. 예외는 한가지 뿐이다. 구체 클래스에서 상위클래스의 추상메서들을 재정의한 경우엔 이 애너테이션을 달지 않아도 되나.

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

마커인터페이스: 아무 메서드도 담지 않고, 단지 자신을 구현한 클래스가 특정 속성을 가짐을 표시해주는 인터페이스. 예, Serializable

마커 인터페이스와 마커 애너테이션 차이를 알아야 한다.

마커 인터페이스

  • 사용하는 이유 중 하난 컴파일 타임 오류 검출이 가능하다.

  • 마커 인터페이스를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있다.

  • 적용대상을 세밀하게 지정가능. 반대로, 애너테이션은 @Target ElementType.Type과 같이 모든타입(클래스, 인터페이스, 열거타입, 애너테이션)에 달 수 있어 세밀하게 제한하지 못한다.

마커 애너테이션

  • 거대한 애너테이션 시스템을 지원받는 장점이 있다. 프레임워크.

핵심정리

마커인터페이스와 마커애너테이션은 각자 쓰임이 있다. 새로 추가하는 메서드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택하자. 클래스나 인터페이스 외의 프로그램 요소에 마킹해야 하거나, 애너테이션을 적극 활용하는 프레임워크의 일부로 그 마커를 편입시키고자 한다면 마커 애너테이션이 올바른 선택이다. 적용대상이 ElementType.TYPE인 마커 애너테이션을 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션으로 구현하는게 옳은지, 혹은 마커 인터페이스가 낫지 않을지 곰곰이 생각해보자.

7장 람다와 스트림

자바 8부터 함수형 인터페이스, 람다, 메서드 참조 개념 추가되어 햄수 객체 더 쉽게 만들수 있게 되었다. 이와 함께 스트림 API까지 추가되어 데이터 원소의 시퀀스 처리를 라이브러리 차원에서 지원하기 시작했다.

42 익명 클래스보다 람다를 사용하라

1997년 JDK 1.1 시절 함수객체를 만드는 주요수단은 익명클래스였다. 전략패턴 비슷. 코드가 길어지면 익명클래스... 함수형 프로그래밍 적합하지가 않다!

자바 8에 와서 추상 메서드 하나짜리 인터페이스는 특별한 의미를 인정받아 특별한 대우를 받게 되었다. 지금은 함수형 인터페이스로 부르는 이 인터페이스들의 인스턴스를 람다식을 사용해 만들 수 있게 되었다.

람다식은 컴파일러 추론으로 처리된다. 타입을 결정하지 못할때는 명시해야 코드가 더 명확해진다. 그 외에는 모든 매개변수 타입은 생략하자.

item29,30 제너릭에서 컴파일러의 타입추론에 대해 보았다. 컴파일러는 타입정보 추론 대부분을 제너릭에서 얻기 때문에, 람다와 함께 쓸때 더 중요해진다.

item 34 코드를 람다로 바꿔보자.

apply 메서드의 동작이 상수마다 달라야 해서 상수별 몸체를 사용해 각 상수에서 apply 메서드를 재정의하었고, 상수별 클래스 몸체를 구현하는 방식보다는 열거타입에 인스턴스 필드를 두는 편이 낫다고 했다.

깔끔해졌다.

다만 명심해야될 것은, 람다는 이름이 없고 문서화도 못한다. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다. 길어도 3줄 안으로.

람다의 제한을 보자.

람다는 함수형 인터페이스에서만 쓰인다. 예컨데 추상클래스의 인스턴스를 만들때 람다를 쓸 수 없으니, 익명 클래스를 써야한다. 추상메서드가 여러 개인 인터페이스의 인스턴스를 만들 때도 마찬가지다.

또한, 람다에서 this 키워드는 바깥 인스턴스를 가리킨다 (=자신을 참조할 수 없다.) 그래서 함수객체가 자신을 참조해야 한다면 반드시 익명클래스를 써야 한다.

그리고 익명클래스나 람다 둘다 직렬화하는 일은 극히 삼가해야 한다. 가령 Comparator 처럼 직렬화 해야하는 함수객체가 있다면 private 정적 중첩클래스의 인스턴스를 사용하자.

핵심 정리

버전 8이 되며 작은 함수 객체를 구현하는데 적합한 람다가 도입되었다. 익명 클래스는 (함수형 인터페이스가 아닌) 타입의 인스턴스를 만들 때만 사용하라. 람다는 작은 함수객체를 아주 쉽게 표현할 수 있어 함수 프로그래밍의 지평을 열었다.

43 람다보다는 메서드 참조를 사용하라

람다보다 더 간결한 식이 있다니, 메서드 참조.

두번째 방식은 메서드 참조로 자바 8부터 Integer 클래스는 람다와 기능이 같은 정적 메서드 sum을 제공하기 시작했다.

람다가 더 좋을 때도 있다.

Function.identity()도 x->x를 직접사용하는 편이 낫다.

메서드 참조유형에 5가지가 있다.

인스턴스 메서드를 참조하는 유형에 두가지

1 수신객체를 특정하는 한정적 인스턴스 메서드 참조

2 특정하지 않는 비한정적 메서드 참조

메서드 참조

람다

Static

Integer::parseInt

str -> Integer.parseInt(str)

Bound

Instant.now()::isAfter

Instant then = Instant.now(); t -> then.isAfter(t)

Unbound

String::toLowerCase

str -> str.toLowerCase()

Class Constructor

TreeMap::new

() -> new TreeMap

Array Constructor

int[]::new

len -> new int[len

람다로는 불가능하나 메서드 참조로 가능한 유일한 예가 있다. 제너릭 함수 타입.

44 표준 함수형 인터페이스를 사용하라

핵심정리

입력값과 반환값에 함수형 인터페이스 타입을 활용하라. 보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용한느 것이 가장 좋은 선택이다. 단, 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 잇음을 잊지 말자.

람다를 지원하며 API를 작성하는 방식도 크게 바뀌었다. 템플릿 메서드 패턴 매력이 줄었다. 대신 같은 효과의 함수객체를 받는 정적 팩터리나 생성자를 제공한다.

함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다.

궁금 - removeEldestEntry를 재정의 하면 캐시로 사용할 수 있다는 말이 무슨뜻?

removeEldestEntry는 size()를 호출해 맵 안의 원소 수를 알아내는데, 인스턴스 메서드라 가능하다. 하지만 생정자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다. 팩터리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문이다.

불필요한 이유: 이미 자바에 같은 모양 인터페이스 있다. java.util.function 내부에.

LinkedHashMap 예에서 만든 EldestEntryRemovalFunction 대신 BiPredicate<Map<K,V>, Map.Entry<K,V>> 사용.

java.util.function 기본 인터페이스 6개 (총개수는 43개. 모두 참조 타입용.)

인터페이스

함수 시그니처

UnaryOperator

T apply(T t)

String::toLowerCase

BinaryOperator

T apply(T t1, T t2)

BigInteger::add

Predicate

boolean test(T t)

Collection::isEmpty

Function

R apply(T t)

Arrays::asList

Supplier

T get()

Instant::now

Consumer

void accept(T t)

System.out::println

Function: 인수와 반환 타입이 다르다. R T UnaryOperator: 인수와 같은 타입 반환

long을 받아 int 반환: LontToIntFunction 입력을 매개변수화 할 경우: ToLongFunction 인수를 2개씩 받는 변형: BiFunction

기본 함수형 인터페이스에 박싱된 기본타입을 넣어 사용하지는 말자. 무슨말?

Comparator와 ToBiFunction는 구조적으로 동일하다. Comparator를 독자적으로 사용하는 이유는 네이밍이 그 용도를 제대로 알려주고, 구현하는 쪽에서 지켜야할 규약을 잘 담고있고 그리고 비교자들을 반환하고 조합해주는 유용한 디폴트 메서드를 가졌다.

그럼 함수형 인터페이스 구현해야될 상황은?

  • 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.

  • 반드시 따라야하는 규약이 있다.

  • 유용한 디폴트 메서드를 제공할 수 있다

@FunctionInterface 애너테이션 목적

  • 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.

  • 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일 되게 해준다.

  • 그 결과 유지보수 과정에서 누군가 실수로 메서드 추가를 막아준다.

주의할점

서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다. (무슨말?) 클라이언트에게 모호함을 준다. 예, ExceutorService의 submit 메서드는 Callable과 Runnable을 받는 것을 다중정의했다.

Last updated