본문 바로가기

Java

[Effective Java 7장] 람다

Effective Java 7장 들어가기 전

사실 Java를 아직도 잘 모르지만, Java 8을 기점으로 큰 변화가 있었다는 건 많이 들었다.

그 대표 격이 람다와 스트림인데, 이게 무엇이며 왜 등장하게 되었는지 먼저 살펴보기로 했다. 

 

Java는 객체 지향 프로그래밍에 쓰이는 대표적인 언어이며, Object가 일급 시민(First-class citizen)의 역할을 한다.

 

일급 시민이란?

  • 인자로 전달이 가능하다
  • 반환 값으로 사용할 수 있다.
  • 변수나 데이터 구조 안에 담을 수 있다

위 조건으로 보았을 때 Java의 method는 일급 시민이 될 수 없다.
하지만 함수가 일급 시민 역할을 하는 함수형 프로그래밍이 대세로 떠오르면서 Java에도 변화가 찾아왔다.
모던 자바라고 불리는 Java8에서는 함수를 일급 객체로 사용할 수 있게 된 것이다.

 

함수형 프로그래밍의 장점

더보기
  • side-effect가 없다. (상태 값을 가지지 않아, 변경을 일으키지 않는다)
  • 코드를 읽기 쉽다.
  • 테스트에 용이하다.
  • 특정 메서드의 작업 내용이나 결과를 여러 차수로 고도화할 수 있다. (고차 함수)

그렇다면 Java는 어떤 방법을 통해서 함수형 프로그래밍을 지원할 수 있게 되었을까?
그게 바로 람다와 스트림이다.

 


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

함수 객체

추상 메서드를 하나만 담은 인터페이스의 인스턴스 

 

함수 객체를 만드는 수단

  • 익명 클래스
  • 람다식

[코드 42-1]익명 클래스로 만든 함수 객체

Collections.sort(word, new Comparator<String>() {
	public int compare(String s1, String s2) {
    	return Integer.compare(s1.length(), s2.length());
    }
});

sort 메서드는 무엇을 정렬할 것인지와 그걸 어떤 방식으로 정렬할 것인지를 인자로 받고 있다.

word라고 하는 String list가 "What"이며 Comparator 를 상속받아 오버라이딩한 익명 클래스가 "How"에 대한 부분이다.

 

단점

너무 코드가 길고 복잡해서 함수형 프로그래밍에 적합하지 않다.

 

Java 8에서는

추상 메서드가 하나짜리인 Comparator 같은 인터페이스를 함수형 인터페이스라고 부르며,

함수형 인터페이스의 인스턴스는 람다식으로 표현할 수 있다.

 

[코드 42-2] 람다식으로 만든 함수 객체

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

비교자 생성 메서드나 List인터페이스에 추가된 sort 메서드를 이용하면 더 간결하게 쓰는 것도 가능

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

[코드 42-4] 람다를 인스턴스 필드에 저장해 상수별 동작을 구현한 열거 타입

public enum Operation {
	PLUS("+", (x, y) -> x+y),
    MINUS("-", (x, y) -> x-y),
    TIMES("*", (x, y) -> x*y),
    DIVIDE("/", (x, y) -> x/y),
    
    private final String symbol;
    private final DoubleBinaryOperator op;
    
    Operation(String symbol, DoublieBinaryOperation op) {
    	this.symbol = symbol;
        this.op = op;
    }
    
    @Override public String to String() { return symbol; }
    
    public double apply(double e, double y) {
    	return op.applyAsDouble(x, y);
   	}
}

DoubleBinaryOperator

더보기
  • java.util.function 패키지가 제공하는 다양한 함수 인터페이스 중 하나
  • Double 타입 인수 2개를 받아 Double 타입 결과를 반환

결론

Java 8에서는 작은 함수 객체를 구현하는데 더 적합한 람다가 도입되었다.

익명 클래스는 함수형 인터페이스가 아닌 타입의 인스턴스를 만들 때만 사용하라. 

 


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

item 42에서 익명클래스보다 간결한 람다를 사용하라고 말했는데, 43에 와서는 람다보다 메서드 참조를 사용하라고 한다. 그 이유는 바로 간결함 때문이다. 

 

[예시] 

1. map.merge(key, 1, (count, incr) -> count + incr);
2. map.merge(key, 1, Integer::sum);

Java 8에서는 모든 기본 타입의 박싱 타입 클래스는 정적 메서드 sum을 제공한다.

람다 대신 이 메서드의 참조를 전달하면 같은 결과를 더 보기 좋게 얻을 수 있다.

 

Q. 간결한게 좋다고는 하지만, 메서드 이름을 통해서 알 수 있는 정보도 있다. 이런 경우 메서드 참조보다 람다가 읽기 쉽고 유지보수 하기도 좋지 않을까?

A. 책에서는 이런 경우 람다로 작성할 코드를 메서드로 작성한 후, 그 메서드 참조를 사용하는 방법이 있다고 제시해 준다.

보통 람다보다 메서드 참조가 훨씬 간결하며, 메서드는 이름을 지어주거나 문서화를 할 수 있기 때문이다.

 

람다를 쓰는 게 더 간결한 경우

메서드와 람다가 같은 클래스에 있을 경우

1. service.execute(GoshThisClassNameIsHumongous::action);
2. service.exectue(() -> action());

메서드 참조 쪽이 더 짧지도 명확하지도 않다.

결론 

메서드 참조는 람다의 간단 명료한 대안이 될 수 있다.

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

 


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

java 8부터는 람다의 지원을 위해 java.util.function 패키지에 표준 함수형 인터페이스를 다양하게 제공하고 있다.

필요한 용도에 맞는게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라

 

표준 함수형 인터페이스를 사용해야 하는 이유

1. API가 다루는 개념의 수가 줄어들어 익히기 더 쉬워진다.

2. 유용한 디폴트 메서드를 많이 제공하므로 다른 코드와의 상호운용성도 크게 좋아지게 된다.

 

java.util.function 패키지

총 43개의 인터페이스가 담겨 있는데, 이 중 기본 인터페이스 6개만 기억하면 나머지를 충분히 유추할 수 있다.

 

Interface 함수 시그니처
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add
Predicate<T> boolean test(T t) Collection::isEmpty
Function<T> R apply(T t) Arrays::asList
Supplier<T> T get() Instant::now
Coㅜsumer<T> void accept(T t) System.out::println

Operator

  • 인수가 1개인 UnaryOperator, 인수가 2개인 BinaryOperator로 나뉨
  • 반환값과 인수의 타입이 같은 함수

Predicate

  • 인수 하나를 받아 boolean으로 반환하는 함수

Function

  • 인수와 반환 타입이 다른 함수

Supplier

  • 인수를 받지 않고 값을 반환 or 제공하는 함수

Consumer

  • 인수를 하나 받으며 반환값이 없는 인수를 소비하는 형태의 함수

표준 함수형 인터페이스 주의점

  • 대부분 기본 타입만 지원하는데, 기본 함수형 인터페이스에 박싱된 기본 타입을 넣어선 안된다
    • 계산량이 많을 경우 성능이 많이 느려질 수 있다.

 함수형 인터페이스를 직접 구현하는 경우

  • 자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.
  • 반드시 따라야 하는 규약이 있다.
  • 유용한 디폴트 메서드를 제공할 수 있다.

위 항목 중 하나 이상을 만족한다면 전용 함수형 인터페이스 구현을 고려해본다.

 

@FunctionallInterface

전용 함수형 인터페이스 구현시 사용하는 인터페이스

  • 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일 되게 해준다.
  • 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.

결론 

입력값과 반환값에 함수형 인터페이스 타입을 활용하라. 

보통은 java.util.function 패키지의 표준 함수형 인터페이스를 사용하는 것이 가장 좋은 선택이다.

단, 흔치는 않지만 직접 새로운 함수형 인터페이스를 만들어 쓰는 편이 나을 수도 있음을 잊지 말자.

'Java' 카테고리의 다른 글

[스트림 활용] 중간 연산  (0) 2021.05.18
[스트림 활용] 생성  (0) 2021.05.18