개발/[스터디] 이펙티브 자바

[이펙티브 자바 스터디] 아이템 41-46

Dahee Joy Cha 2022. 1. 23. 15:09

아이템 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이 있다.