[이펙티브 자바 스터디] 아이템 41-46
아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용해라.
마커인터페이스(marker interface)
- 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스. 아무 메서드도 없다.
- 예. Seria
마커 인터페이스가 마커 애너테이션 보다 나은점.
- 컴파일 타임에 타입 불일치 체크 가능
: 마커 인터페이스는 구현 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있다
cf. 마커 에너테이션은 불가능.
- 마커 인터페이스는 적용 가능 대상을 세밀하게 제한 가능하다
: 마킹하고 싶은 클래스에만 인터페이스 구현하면 됨.
cf. 애너테이션은 모든 타입에 달 수 있다. (클래스, 인터페이스, 열거타입 등)
TODO: 마커 인터페이스, 마커 애너테이션 구현 후 비교하는 코드 추가
마커 애너테이션이 마커 인터페이스보다 나은 점
애너테이션 시스템의 지원을 받는다. 따라서 애너테이션을 적극 활용하는 프레임워크에서는 마커 애너테이션이 나을 것이다.
정리: 언제 마커 애너테이션 vs 마커 인터페이스를 각각 사용할까?
- 마커 애너테이션
: 클래스와 인터페이스 외의 프로그램 요소에 마킹해야 할 경우
: 애너테이션을 활발히 사용하는 프레임워크에서 사용하는 마커일 경우.
- 마커 인터페이스
: 마커인터페이스 구현체를 매개변수로 받는 메서드가 구현될 예정일때. (컴파일 타임에 오류 잡을 수 있다.)
아이템 42. 익명 클래스보다는 람다를 사용하라
함수 객체(function object)
- 정의 : 추상 메서드를 하나만 담은 인터페이스의 인스턴스
- 목적: 특정 함수/동작을 나타내는데 사용
함수 객체 구현법
- 익명 클래스를 사용
- 예제 코드
// Comparator: 정렬을 담당하는 추상 클래스
// comare:문자열을 정렬하는 구체적인 전략. 익명 클래스로 구현
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
익명 클래스의 단점
- 코드가 너무 길다
- 매개변수와 반환값의 타입 정의 때문에 코드의 가독성 저하
예) words는 String임을 선언부에서 확인 가능. 하지만 String s1, String s2로 별도 타입 재선언 필요.
Comparator<String>의 반환 타입이 int라는 것을 선언해야함.
람다( =람다식 =lambda expression)의 등장
- 작은 함수 객체를 구현하는 데 좋다
- 함수형 프로그래밍에 큰 도움이 됨.
// A. 람다 적용
Collections.sort(words, (s1,s2) -> Integer.compare(s1.length(), s2.length()));
// B. (A)의 코드를 생성자 메서드를 사용하여 간결화
Collections.sort(words, comparingInt(String::length));
// c. (B)의 코드를 List 인터페이스의 sort 메서드 사용하여 간소화
words.sort(comparingInt(String::length));
이럴 때 람다 쓰지 말자.
1. 람다 코드 자체로 동작이 명확하지 않거나, 코드 줄 수가 길어진다면
: 코드 줄 수는 한 줄이 best, 최대 3줄을 넘지 말자.
2. 열거타입의 상수별로 람다 사용시, 인스턴스 필드/메서드를 사용해야한다면 (순수함수 특징에 어긋난다.)
람다로 대체할 수 없는 곳
1. 추상 클래스의 인스턴스를 만들 때
2. 추상 메서드가 여러개인 인터페이스의 인스턴스를 만들 때
3. 함수 객체가 자신을 참조해야할 때
-> TODO: 1~3 예제 추가
아이템 43. 람다보다는 메서드 참조를 사용하라
메서드 참조(method reference)
람다 함수 내의 매개변수를 코드에서 대체. 간결화
// 예.
// 람다
map.merge(key, 1, (count, incr) -> count + incr);
// 메서드 참조
map.merge(key, 1, Integer::sum);
람다가 메서드 참조보다 나은 경우
1. 매개변수명이 코드의 이해를 도울 때
2. 람다가 메서드 참조보다 간결할 경우 (예. 메서드와 람다가 같은 클래스에 있을 때)
인스턴스 메서드를 참조하는 유형
- 정적
- 한정적
- 비한정적
- 클래스 생성자
- 배열생성자
질문: 한정적 참조와 비한정적 참조의 개념이 잘 이해 가지 않는다.
결론
코드가 간결하고 명료해진다면 메스드 참조 사용하자. 그렇지 않다면 람다 유지하자.
아이템 44. 표준 함수형 인터페이스를 사용하라.
표준 함수형 인터페이스
- 필요한 용도에 맞는게 있다면 직접 구현하지 말고 표준 함수형 인터페이스를 활용하자.
- java.util.function 패키지에 다양한 용도의 표준 함수형 인터페이스 존재
- 기본 인터페이스 6가지
- UnaryOperator <T> : 반환값과 인수의 타입 동일. 인수 하나
- BinaryOperator<T> : 반환값과 인수의 타입 동일. 인수 둘
- Predicate<T> : 인수 하나 받아 boolean 반환
- Function<T,R> : 인수와 반환값의 타입이 다르다.
- Supplier<T> : 인수를 받지 않고 값 반환
- Consumer<T> : 인수 하나 받고 반환 값 없음.
표준 함수형 인터페이스 사용시 주의 사항
- 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어 사용하지는 말자
: 계산량이 많을 때는 성능이 처참히 느려진다.
별도의 전용 함수형 인터페이스
- 아래의 경우 중 하나를 만족 한다면 별도의 전용 함수형 인터페이스를 정의하자 TODO: 각 경우에 대한 예시 코드 적기
(예시. Comparator<T>)
1. 자주 쓰이고, 이름 자체가 용도를 명확히 설명
2. 반드시 따라야하는 규약이 있다.
3. 유용한 디폴트 메서드 제공 가능
- 직접 만든 함수형 인터페이스에서는 항상 @FunctionalInterface 애너테이션을 사용하자.
1. 인터페이스가 람다용으로 설계됨을 타 개발자에게 인지 시킴
2. 해당 인터페이스는 추상 메서드를 하나만 가지고 있어야 함을 알림.
3. 유지 보수 과정에서 누군가 추가로 메서드를 추가하는 것을 막아줌.
함수형 인터페이스를 API에서 사용할 때 주의 사항
서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드를 다중 정의하지 말자.
예) ExecutorService의 submit 메서드는 Callable<T>와 Runnable을 받는 것을 다중 정의 함.
아이템 45. 스트림은 주의해서 사용해라
스트림 API 등장 배경
다량의 데이터 처리 작업을 돕고자 자바 8에 추가.
스트림 API 핵심 개념
스트림(stream)
- 데이터 원소의 유한 혹은 무한 시퀀스
- 원소는 객체 참조나 기본 타입 (int, long, double)
스트림 파이프 라인(stream pipeline)
- 스트림 내 원소들로 수행할 수 있는 연산 단계
- 소스 스트림 - 중간 연산 - 종단 연산 으로 이뤄진다.
중간연산(Intermediate operation)
- 스트림을 특정 방식으로 변환
- 예시) (1) 각 원소에 함수 적용/ (2) 필터링
종단 연산(terminal operation)
- 중간 연산이 내놓은 스트림에 최후의 연산 적용
- 스트림 파이프라인은 지연 평가(Lazy Evaluation) 된다. (= 종단 연산에 쓰이는 데이터 원소만 계산에 쓰인다. )
-> TODO: Lazy Evaluation 정의 명확히 하기.
스트림 사용시 주의사항
- 프로그램의 가독성이 낮아질 경우는 피하자.
(예제: 코드 45-1 ~ 코드 45-3)
- IDE가 stream을 권하더라도, 기존 코드의 가독성이 높아질 때만 for문에서 stream으로 변경 하자.
스트림의 가독성 높이는 법
- 람다 매개변수의 이름을 주의해서 정하자. (예. 코드 45-3의 word)
- 세부 구현 사항은 별도의 도우밍 메서드로 분리하고, 적절한 네이밍을 부여하자.
(예. 코드 45-3의 alphabetize는 단어를 알파벳 단위로 분리한다.)
스트림에서 지원 불가능한 기능
- 람다 내부에서는 final 변수만 읽을 수 있고, 지역 변수를 수정할 수 없다.
- return, break, continue 사용 불가능하다.
아이템 46. 스트림에서는 부작용 없는 함수를 사용하자.
Stream에서 부작용 방지하는 방법
- Stream pipeLine에서 각 변환 단계는 이전 단계의 결과를 받아 처리하는 순수함수여야 한다.
* 순수함수: 오직 입력만이 결과에 영향을 주는 함수
- forEach는 계산에 사용하지 말자. forEach는 스트림에서 계산된 결과값을 보여주는데만 사용하자.
수집기(Collector)
- 스트림 원소를 손쉽게 컬렉션으로 모을 수 있다.
- toList(), toSet(), toCollection(collectionFactory)
- TODO: java.util.stream.Collectors 레퍼런스 내에서 관련 메서드 예시 블로그에 추가
수집기의 내부 메서드
- toMap, groupingBy, partioningBy...
결론
- 스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
- forEach는 계산값 출력에만 사용하자. 계산 자체에는 사용하지 말자.
- 주요 수집기는 toList, toSet, toMap, groupingBy, joining이 있다.