[Modern Java] 람다 표현식

람다 표현식

람다 표현식은 익명 함수를 단순화 한 것이다. 따라서 익명 함수와 같이 익명, 함수, 전달과 같은 동일한 장점이 있고 그에 더해 간결성까지 포함한다.

람다 표현식은 다음과 같이 세 부분으로 나누어 진다.

(int value1, int value2) -> value1 + value2

  • (int value1, int value2) 파라미터
  • -> 파라미터와 바디의 구분자
  • value1 + value2 바디

또한 여러 줄의 바디를 위해 {}에 바디를 감쌀 수 있다.

1
2
3
4
(int value1, int value2) -> {
System.out.println("Value1 : " + value1 + " Value2 : " + value2);
return value1 + value2;
}

@FunctionalInterface

Java에 함수형 인터페이스를 의미하기 위한 어노테이션이다. 이 어노테이션을 통해 함수형 인터페이스라는 의미를 명시할 수 있다. 또한 @FunctionalInterFace 어노테이션을 붙혔더라도 실제로 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다.

함수형 인터페이스

java.util.function 패키지에는 자주 사용하는 함수형 인터페이스들을 미리 만들어 놓았다.

  • Predicate\ : T 클래스를 받아 boolean으로 리턴
  • Consumer\ : T 클래스를 받아 void로 리턴
  • Function\ : T 클래스를 받아 R 클래스로 리턴
  • supplier\ : 입력값 없이 T 클래스로 리턴
  • UnaryOperator\ : T 클래스를 받아 T 클래스로 리턴
  • BinaryOperator\ : T 클래스를 2개 받아 T 클래스로 리턴
  • BiPredicate\ : L R 클래스를 받아 boolean으로 리턴
  • BiConsumer\ : T U 클래스를 받아 void로 리턴
  • BiFunction\ : T U 클래스를 받아 R 클래스로 리턴

위에 나열된 함수형 인터페이스들 말고도 원시타입들을 위해 지원하는 함수형 인터페이스들도 존재한다. IntPredicate IntConsumer IntFunction 같은 함수형 인터페이스들은 자바에서 원시타입을 레퍼런스 타입으로 변환하면서 생기는 박싱 비용을 절감하기 위해 위에 나열한 함수형 인터페이스들 대신에 사용할 수 있다.

람다의 동작

형식 검사

람다는 형식 검사를 통해서 서술된 형식이 적합한지를 판단한다.

1
2
List<String> strings = Arrays.asList("a", "ab", "abc");
strings.stream().filter(string -> string.length() > 1).collect(Collectors.toList());

위와 같은 코드가 있을때 다음과 같은 순서로 검사한다.

  1. filter 메소드의 선언을 확인
  2. filter 메소드의 파라미터는 Predicate<? super T> 라는 것을 확인
  3. Predicate<? super T>는 T 클래스를 받아 boolean으로 리턴하는 함수형 인터페이스임을 인지
  4. Predicate<? super T>의 추상 메소드를 묘사
  5. filter로 전달되는 인수가 Predicate<? super T>의 추상 메소드를 묘사할 수 있는지 확인

형식 추론

람다는 형식 추론을 통해 서술된 형식을 더 단순화 할 수 있게 돕는다.

1
2
3
4
5
List<String> strings = Arrays.asList("a", "ab", "abc");
//형식을 추론함
strings.stream().filter((string) -> string.length() > 1).collect(Collectors.toList());
//형식을 추론하지 않음
strings.stream().filter((String string) -> string.length() > 1).collect(Collectors.toList());

지역 변수 사용

람다 내에서는 자유 변수를 사용할 수 있는데 이를 람다 캡처링이라 한다. 하지만 람다 캡처링에 사용되는 자유 변수에는 변수가 변하지 않는다는 명시적 혹은 암시적 전제조건이 붙어야 한다. 람다는 람다가 수행되는 도중에 할당된 지역 변수가 멀티스레드의 상황 등 에서 해제되는것을 방지하기 위해 지역 변수의 복사본을 제공하는데 이에 따라 복사본의 값이 바뀌지 않아야 하므로 한 번만 값을 할당해야하는 제약이 생긴다.

1
2
3
final String str = "ABC";	//명시적으로 변수가 변하지 않음
List<String> strings = Arrays.asList("a", "ab", "abc");
strings.stream().map(string -> string + str).collect(Collectors.toList());
1
2
3
String str = "ABC";	//암시적으로 변수가 변하지 않음
List<String> strings = Arrays.asList("a", "ab", "abc");
strings.stream().map(string -> string + str).collect(Collectors.toList());
1
2
3
4
String str = "ABC";
List<String> strings = Arrays.asList("a", "ab", "abc");
strings.stream().map(string -> string + str).collect(Collectors.toList());
str += "D"; //변수가 변하므로 람다에서 에러 발생

메소드 참조

메소드 참조

메소드 참조를 통해서 람다를 전달함으로써 가독성을 높힐 수 있다. 컴파일러는 람다 표현식의 형식을 검사하던 방법과 유사한 과정으로 메소드 참조가 주어진 함수형 인터페이스와 호환이 되는지 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyString {
private String string;

public MyString(String string) {
this.string = string;
}

public MyString(Integer integer) {
this.string = integer.toString();
}

public String getString() {
return this.string;
}
}
1
2
3
4
5
6
7
List<MyString> myStrings = Arrays.asList(
new MyString("a"),
new MyString("ab"),
new MyString("abc")
);

List<String> strings = myStrings.stream().map(MyString::getString).collect(Collectors.toList());

생성자 참조

1
2
3
List<Integer> integers = Arrays.asList(123, 456, 789);

List<MyString> strings = integers.stream().map(MyString::new).collect(Collectors.toList());

함수형 인터페이스 조합

java.until.function에는 자주 사용되는 함수형 인터페이스들이 존재한다. 또한 이런 함수형 인터페이스의 메소드를 통해 다양한 조합을 만들 수 있다. 어떤 메소드들이 있는지는 공식문서를 통해 확인이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
Set<Integer> set = new HashSet<>();
set.add(null);
set.add(1);
set.add(2);
set.add(3);
set.add(4);

//filter를 두번 사용하게 됨
set.stream().filter(Objects::nonNull).filter(num -> num > 2).collect(Collectors.toList()).forEach(System.out::println);

//predicate의 and를 사용해 조합
Predicate<Integer> nonNull = Objects::nonNull;
set.stream().filter(nonNull.and(num -> num > 2)).collect(Collectors.toList()).forEach(System.out::println);
Author: Song Hayoung
Link: https://songhayoung.github.io/2021/03/29/Languages/Modern%20Java/chapter3/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.