Hyun's Wonderwall

[Java] 이펙티브 자바 - 6장(2/2), 7장(1/2) (아이템 37~45) 본문

Study/Java

[Java] 이펙티브 자바 - 6장(2/2), 7장(1/2) (아이템 37~45)

Hyun_! 2025. 8. 16. 19:11

6장 열거 타입과 애너테이션 (2/2)

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

EnumMap: 열거 타입을 키로 사용하도록 설계한 아주 빠른 맵 구현체

- 내부에서 배열을 사용해 빠름..

- EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰. 런타임 제네릭 타입 정보를 제공.

Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);
        for (LifeCycle lc : LifeCycle.values()) {
            plantsByLifeCycle.put(lc, new HashSet<>()); // 빈 Set 초기화
        }
        for (Plant p : garden) {
            plantsByLifeCycle.get(p.lifeCycle).add(p);
        }
        System.out.println(plantsByLifeCycle);

(모든 enum 키가 항상 존재. garden에 없더라도 출력.)

 

스트림을 사용해 맵을 관리하면 코드를 더 줄일 수 있다.

- 동작에 차이가 존재: EnumMap은 열거 타입의 모든 상수를 키로 만들지만, 스트림 버전에서는 실제 데이터가 있는 상수만 키로 만든다.

System.out.println(Arrays.stream(garden)
                  .collect(Collectors.groupingBy(p -> p.lifeCycle, 
                      () -> new EnumMap<>(LifeCycle.class),toSet())));

(즉, 등장한 lifeCycle만 키로 출력)

 

배열의 인덱스를 얻기 위해 ordinal을 사용하지 말고, EnumMap을 사용하자!


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

열거 타입은 거의 모든 상황에서 '타입 안전 열거 패턴'(typesafe enum pattern)보다 우수하다. 확장할 수 없다는 점만이 따지자면 단점이나, 사실 열거 타입의 쓰임 상황에서는 대개 확장하는 것이 좋지 않기 때문에 그렇게 설계된 것.

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

- 클라이언트는 이 인터페이스를 구현해 자신만의 열거 타입을 만들 수 있다.

- API가 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 확장한 열거 타입의 인스턴스로 대체해 사용할 수 있다.


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

애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다. 자바가 제공하는 애너테이션 타입들을 잘 사용하자.


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

@Override는 메서드 선언에만 달 수 있으며, 이 애너테이션이 달렸다는 것은 상위 타입의 메서드를 재정의했음을 뜻한다.

- 안 달면 파라미터가 다른 경우 다중정의(오버로딩)으로 인식되는 버그가 발생한다.(컴파일러가 찾아내준다)

- 애너테이션을 통해 오버로딩이 아니라 오버라이딩한 것이라고 명시해야 한다.

- 상위 클래스의 메서드를 재정의하는 모든 메서드에 @Override 애너테이션을 달자.

- 구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때는 달지 않아도 되긴 하다.(구현하지 않은 추상 메서드가 남아 있다면 컴파일러가 그 사실을 바로 알림. 달아도 문제는 x.)

 

@Override 는 인터페이스 메서드를 재정의 할 때도 사용할 수 있다. 디폴트 메서드를 지원하기 시작해서.

추상 클래스나 인터페이스에서는 상위 클래스나 상위 인터페이스 메서드를 재정의하는 모든 메서드에 @Override를 다는 것이 좋다.


=> 가독성 + IDE 경고 등 종합해보면 재정의 메서드에는 다 @Override 붙이는 게 좋은 듯


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

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

- ex. Serializable: 자신을 구현한 클래스의 인스턴스는 직렬화할 수 있다고 알려준다. (ObjectOutputStream을 통해 쓸 수 있다고)

 

새로 등장한 '마커 애너테이션'보다 마커 인터페이스가 나은 점 2가지

1. 마커 인터페이스는 이를 구현한 클래스들을 구분하는 타입으로 쓸 수 있으나, 마커 애너테이션은 X

2. 마커 인터페이스는 적용 대상을 더 정밀하게 지정할 수 있다.

 

반대로 마커 애너테이션이 나은 점: 거대한 애너테이션 시스템의 지원을 받는다.

 

각자의 쓰임이 있다.

- 새로 추가하는 메서드 없이 단지 타입 정의가 목적이라면 -> 마크 인터페이스

- 클래스나 인터페이스 외의 프로그램 요소에 마킹해야 하거나, 애너테이션을 적극 활용하는 프레임워크의 일부로 그 마커를 편입시키고자 한다면 -> 마커 애너테이션


7장 람다와 스트림

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


아이템 42. 익명 클래스보다는 람다를 사용하라

람다식(lambda expression): 함수형 인터페이스 인스턴스 생성 가능. 작은 함수 객체를 아주 쉽게 표현할 수 있게 됨.

- 람다에서의 this는 바깥 인스턴스를 가리킨다.

 

타입을 명시해야 코드가 더 명확할 때만을 제외하고 람다의 모드는 매개변수 타입은 생략하자.

Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

 

람다 자리에 비교자 생성 메서드를 사용하면 이 코드를 더 간결하게 만들 수 있다.

Collections.sort(words, comparingInt(String::length));

 

더 나아가 List 인터페이스의 sort 메서드를 이용하면 더욱 짧아진다.

words.sort(comparingInt(String::length)));

 

(아이템34) 열거 타입 상수별 동작을 정의하기 위해서도, 함수 객체(람다)를 인스턴스 필드에 저장해 상수별 동작을 구현하면 더욱 간단해진다.

- 열거 타입 상수의 동작을 표현한 람다를 DoubleBinaryOperator 인터페이스 변수에 할당했다.

- DoubleBinaryOperator 함수 인터페이스는 double 타입 인수 2개를 받아 double 타입 결과를 돌려준다.

 

익명 클래스를 사용하던 자리들 대부분이 람다로 대체

 

람다는 이름이 없고 문서화도 못 한다. 코드 자체로 동작이 설명되지 않거나 코드 줄 수가 많아지는 경우는 사용하지 x.

- 열거 타입 생성자에 넘겨지는 인수들의 타입도 컴파일타임에 추론됨. 따라서 열거 타입 생성자 안의 람다는 열거 타입의 인스턴스 멤버에 접근할 수 없다(인스턴스는 런타임에 만들어지기 때문에)

 

But 람다를 사용하지 못하고 익명 클래스를 사용해야 할 때: (함수형 인터페이스가 아닌) 타입의 인스턴스를 만들 때

- 추상 클래스의 인스턴스를 만들 때

- 추상 메서드가 여러 개인 인터페이스의 인스턴스를 만들 때

- 자신을 참조해야 할 때 (익명 클래스의 this는 인스턴스 자신을 가리킴)

 

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

 


아이템 43. 람다보다는 메서드 참조를 사용하라

람다는 간결. 그런데 메서드 참조가 더 간결.

메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하자.

 

예제
Integer::parseInt

Instant.now()::isAfter

String::toLowerCase

TreeMap<K, V>::new

int[]::new

 

메서드 참조 표현식은 함수형 인터페이스를 위한 제네릭 함수 타입 구현 가능하지만, 람다식으로는 불가능하다.


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

java.util.function 패키지에 다양한 표준 함수형 인터페이스가 담겨있으므로, 필요한 용도에 맞는 게 있다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용해라. 

 

함수형 인터페이스 시그니처 ↔ 메서드 참조 매핑 예

- String::toLowerCase → UnaryOperator<String>에 매핑
- BigInteger::add → BinaryOperator<BigInteger> 에 매핑
- Collection::isEmpty → Predicate<Collection<?>> 에 매핑
- Arrays::asList → Function<T[], List<T>> 에 매핑

- Instant::now -> Supplier<Instant> 또는 Function<Clock, Instant>에 매핑
- System.out::println → Consumer<String> 에 매핑

 

함수형 인터페이스 대부분은 기본 타입만 지원하는데, 그렇다고 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자. 계산량이 많을 때는 성능이 처참히 느려질 수 있다.

 

직접 만들어 쓸 수도 있다. 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 애너테이션을 사용하라.


아이템 45. 스트림은 주의해서 사용하라

스트림 API 다량의 데이터 처리 작업을 돕고자 추가되었다.

- 플루언트 API. 파이프라인 하나를 구성하는 모든 호출을 연결해 하나의 표현식으로 완성할 수 있다. 파이프라인 여러 개를 표현식 하나로 만들 수도 있다.

 

스트림: 데이터 원소의 유한/무한 시퀀스.

스트림 파이프라인: 이 원소들로 수행하는 연산 단계를 표현하는 개념.

- 스트림 파이프라인은 소스 스트림에서 시작, 하나 이상의 중간 연산이 있을 수 있고, 종단 연산으로 끝난다.

- 중간 연산은 스트림을 변환한다.

 

스트림 파이프라인은 '지연 평가'되어, 평가되는 시점이 종단 연산 호출 때이다.

- 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다. -> 이로써 무한 스트림을 다룰 수 있다.

- 파이프라인은 순차적으로 수행된다.

 

스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다. 적절히 사용하자.

char값들을 처리할 때는 스트림을 삼가는 편이 낫다. (.chars()가 int 스트림을 반환해서 헷갈릴 수 있어서. .forEach(x-> System.out.println((char)x)); 하면 된다)

 

기존 코드를 스트림을 사용하도록 리팩터링하고 새 코드가 더 나아 보일 때만 반영하자.

반복문으로만 할 수 있는 일들도 있다.(람다로는 불가능)

- 범위 안의 지역변수 읽고 수정

- return, break, continue로 빠져나가거나 종료/건너뛰는 것, 메서드 선언에 명시된 검사 예외 던지는 것

 

스트림이 적절한 때

- 원소들의 시퀀스를 일관되게 변환, 필터링, 하나의 연산을 사용해 결합(합산, 연결, 최솟값 구하기))할 때.

- 원소들의 시퀀스를 컬렉션에 모을 때(공통된 속성을 기준으로).

- 원소들의 시퀀스에서 특정 조건에 맞는 원소를 찾을 때.

 

스트림으로 처리하기 어려운 일

- 각 단계에서의 값들에 동시 접근 (스트림은 한 값을 다른 값에 매핑하고 나면 원래의 값을 잃는 구조이다)

 

+ 스트림 중간 연산 flatMap: 평탄화를 한다. (스트림의 원소 각각을 하나의 스트림으로 매핑한 다음 그 스트림들을 다시 하나의 스트림으로 합침)

 

스트림/반복 둘다 더 적합한 때가 있고, 대체로 어느 쪽이 나은지 확연히 드러난다.

스트림과 반복 중 어느 쪽이 더 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하라.