자바 8 - lambda(람다) 더 알아보기

들어가기

지난 포스트에서는 람다(lambda)가 왜 사용되면 편리한지 또 어떻게 사용될 수 있을지에 대해 아주 간단하게 알아보았습니다.
이번 포스트에서는 람다(lambda)를 다시 한 번 정리하고 조금 더 심화된 내용을 알아보려고 합니다.

람다란 무엇인가

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 간단하게 표현한 것입니다. 람다 표현식은 익명 함수와 비슷하게 이름은 없지만, 파라미터, 바디, 반환 형식, 예외 리스트를 가질 수 있습니다.
즉, 보통의 메서드와는 달리 이름이 없으므로 익명이라 표현하고 특정 메서드에 종속되지 않은 독립적인 것이므로 메서드가 아닌 함수라고 부릅니다.
하지만 람다가 기술적으로 자바8 이전의 자바로 할 수 없었던 일을 제공하는 것은 아닙니다. 다만 람다는 동작 파라미터 형식의 코드를 더 쉽게 구현하고 보기 쉬운 코드를 작성하는데 큰 도움이 됩니다.

1
2
3
4
5
Comparator<Apple> byWeight = new Comparator<apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}

위와 같은 코드를 람다를 이용하면 훨씬 간단한 코드로 작성할 수 있습니다.

1
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

즉, compare 메서드의 바디를 직접 전달하는 것처럼 코드를 전달할 수 있으면 이전 코드에 비해 훨씬 가독성 또한 높아졌습니다.
확인한 것처럼 람다 표현식의 기본 문법은 간단합니다.

1
2
3
4
5
6
7
8
9
10
// 유효
() -> {}
// 유효
() -> "Raoul"
// 유효
() -> {return "Lee";}
// 유효하지 않음
(Integer i) -> return "Alan" + i;
// 유효하지 않음
(String s) -> {"Iron Man"}

(parameters) -> expression 또는 (parameters) -> {statement;} 형식으로 람다 표현식을 작성할 수 있습니다.

어디에, 어떻게 람다를 사용할까

함수형 인터페이스

함수형 인터페이스는 오직 하나의 추상 메서드를 지정하는 인터페이스 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// java.util.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}

// java.lang.Runnable
public interface Runnable {
void run();
}

// java.util.concurrent.Callable
public interface Callable<V> {
V call();
}

과연 이런 함수형 인터페이스로 무엇을 할 수 있을까요? 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있습니다.
또, 함수형 인터페이스보다는 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있습니다.

다음과 같은 코드는 모두 올바른 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 람다 사용
Runnable r1 = () -> System.out.println("Hello");
// 익명 클래스 사용
Runnable r2 = new Runnable() {
public void run() {
System.out.println("Hello 2");
}
}

public static void process(Runnable r) {
r.run();
}

process(r1);
process(r2);
process(() -> System.out.println("Hello 3"));

함수 디스크립터

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부릅니다. 예를 들어 Runnable 인터페이스의 유일한 추상 메서드 run은 인수와 반환값이 없으므로 Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각할 수 있습니다.
즉, Runnable 인터페이스의 run은 () -> void과 같이 표현될 수 있습니다. 그리고 이에 대응하는 람다의 함수 디스크립터 역시 () -> void로 표현되어야 합니다.

함수형 인터페이스 사용

Predicate

java.util.function.Predicate< T > 인터페이스는 test라는 추상메서드를 정의하며 test는 제네릭 형식의 t의 객체를 인수로 받아 Boolean을 반환합니다. 즉, 따로 Predicate를 만들 필요없이 필요에 따라 바로 Predicate 인터페이스를 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 @FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s : list) {
if(p.test(s)) {
results.add(s);
}
}
return results;
}

Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfString, nonEmptyStringPredicate)

Consumer

java.util.function.Consumer< T > 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의합니다. T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}

public static <T> void forEach(List<T> list, Consumer<T> c) {
for(T i : list) {
c.accept(i);
}
}

List<Integer> i = map(Arrays.asList(1, 2, 3, 4, 5), (Integer i) -> System.out.println(i));

Function

java.util.function.Function< T, R > 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 apply라는 추상 메서드를 정의하며 이를 활용하여 입력을 출력으로 매핑하는 형식으로 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}

public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> = result = new ArrayList<>();
for(T s : list) {
result.add(f.apply(s));
}
return result;
}

// 4, 2, 3
List<Integer> i = map(Array.asList("test", "dy", "lee"), (String s) -> s.length());

기본형 특화

자바에는 기본형과 참조형이 존재하지만 제네릭 파라미터에는 참조형만 사용할 수 있습니다. 이에 따라 자바에서는 기본형을 참조형으로 변환할 수 있는 기능을 제공하고 이 기능을 박싱이라고 부릅니다.
또 이와 반대로 참조형을 기본형으로 변환하는 작업을 언박싱이라고 부릅니다. 더 나아가 자바에서는 박싱과 언박싱이 자동으로 이루어지는 오토 박싱이라는 기능도 제공합니다.

1
2
3
4
5
List<Integer> list = new ArrayList<>();

for(int i = 300; i < 400; i ++) {
list.add(i);
}

예시와 같은 코드는 동작하는데는 무리가 없지만 기본형이 참조형으로 변환하는 과정에서 어쩔수 없는 비용이 소모됩니다. 박싱한 값은 기본형을 감싸는 래퍼이며 힙에 저장됩니다. 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요합니다.
자바 8에서는 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공합니다. 예를 들어 아래 예제에서 IntPredicate는 1000이라는 값을 박싱하지 않지만, Predicate< Integer >는 1000이라는 값을 Integer로 박싱합니다.

1
2
3
4
5
6
7
8
9
10
11
public interface IntPredicate {
boolean test(int t);
}

// 박싱 없음
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);

// 박싱
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);

마치며

자바 8에서 중요한 요소인 람다의 정리를 마치겠습니다. 질문은 언제든지 자유롭게 달아주시면 찾아서라도 답 달아보도록 노력할게요~

Share