메소드로 전달할 수 있는 익명 함수를 단순화한 것이다.
- 익명
보통의 메소드와 달리 이름이 없으므로 익명이라 표현한다.
구현해야하는 코드에 대한 걱정이 줄어든다.- 함수
메소드처럼 특정 클래스에 종속되지 않으므로 함수라고 부른다.
하지만 메소드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.- 전달
람다 표현식을 메소드 인수로 전달하거나 변수로 저장할 수 있다.- 간결성
익명 클래스처럼 자질구레한 코드를 구현할 필요가 없다.
(파라미터 리스트) -> 람다 바디
- 파라미터 리스트
- 화살표
파라미터 리스트와 람다 바디를 구분한다.- 람다 바디
람다의 반환값에 해당하는 표현식이다.
(String s) -> s.length()
String 형식의 파라미터 하나를 가지며 int를 반환한다.
람다 표현식에는 return이 함축되어 있으므로 return문을 명시하지 않아도 된다.
(Pencil pencil) -> pencil.getLength() > 25
Pencil 형식의 파라미터 하나를 가지며 boolean을 반환한다.
(int x, int y) -> {
System.out.println("Result");
System.out.println(x + y);
}
int 형식의 파라미터 두개를 가지며 반환 값이 없다.
() -> 42
파라미터가 없으며 int 42를 반환한다.
(Pencil p1, Pencil p2) -> p1.getLength().compareTo(p2.getLength())
Pencil 형식의 파라미터 두개를 가지며 int를 반환한다.
(Integer i) -> return 4 + 1
return은 흐름제어문이다. return을 사용한다면 {}를 사용해서 감싸주어야한다.
(String s) -> {"hello"}
"hello"는 구문이 아니라 표현식이다. 때문에 (String s) -> "hello"로 작성되어야 올바른 람다 표현식이다.
- boolean 표현식
(List<String> list) -> list.isEmpty()- 객체 생성
() -> new Pencil(COLOR.YELLOW, 30)- 객체에서 소비
(Pencil p) -> { System.out.println(p); }- 두 값을 조합
(int x, int y) -> x + y- 두 객체 비교
(Pencil p1, Pencil p2) -> p1.getLength().compareTo(p2.getLength())
함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.
함수형 인터페이스는 오직 하나의 추상 메소드를 지정하는 인터페이스다.
자바 API의 함수형 인터페이스로 Comparator, Runnable 등이 있다.
함수형 인터페이스의 추상 메소드 시그니처는 람다 표현식의 시그니처를 가리킨다.
람다 표현식의 시그니처를 서술하는 메소드를 함수 디스크립터라고 부른다.
메소드의 시그니처란, 반환 값과 예외를 제외한 나머지(메소드 명, 파라미터 개수, 파라미터 타입, 순서)를 의미한다.
함수형 인터페이스의 추상 메소드 시그니처란, 예외와 메소드 명을 제외한 나머지(반환 값, 파라미터~)를 의미한다.
예를 들어 Runnable 인터페이스의 유일한 추상 메소드 run은 인수와 반환 값이 없으므로 Runnable 인터페이스는 인수와 반환 값이 없는 시그니처로 생각할 수 있다.
자바 API를 살펴보면 함수형 인터페이스에 @FunctionalInterface 어노테이션이 추가되어 있다.
@FunctionalInterface는 함수형 인터페이스임을 가르키는 어노테이션이다.
해당 어노테이션이 선언된 인터페이스가 실제 함수형 인터페이스가 아닌 경우 컴파일 에러가 발생한다.
람다와 동작 파라미터화로 유연하고 간결한 코드를 구현하는 데 도움을 주는 실용적인 예제를 살펴볼 것이다.
자원 처리에 사용하는 순환 패턴은 자원을 열고, 처리한 후, 자원을 닫는 순서로 이루어진다. 설정과 정리 과정은 대부분 비슷하다.
즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다.
아래와 같은 코드를 실행 어라운드 패턴이라고 부른다.
public String processFile() throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("src/file/data.txt"))){
return br.readLine();
}
}
실제 필요한 작업을 수행하는 행은 return br.readLine()이다.
실습을 위해 파일을 생성한다.
hello
world
processFile();
출력 결과
hello
기존의 설정, 정리 과정을 재사용하고 processFile 메소드만 다른 동작을 수행하도록 명령할 수 있다며 좋을 것이다.
바로 processFile의 동작을 파라미터화하는 것이다.
processFile((BufferedReader br) -> br.readLine() + br.readLine());
위 코드는 BufferedReader를 인자로 받아 BufferedReader에서 두 행을 출력해 String을 반환하는 코드이다.
이제 함수형 인터페이스를 만들어보자.
BufferedReader -> String 그리고 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스여야한다.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader br) throws IOException;
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("src/file/data.txt"))){
return p.process(br);
}
}
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
출력 결과
helloworld
test라는 추상메소드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 boolean 값을 반환한다.
public <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for(T t : list){
if(p.test(t)){
result.add(t);
}
}
return result;
}
List<String> list = Arrays.asList("hello", "world", "", "");
List<String> filterList = filter(list, (String s) -> !s.isEmpty());
System.out.println(Arrays.toString(filterList.toArray()));
출력 결과
[hello, world]
제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메소드를 정의한다.
T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고싶을 때 Consumer 인터페이스를 사용할 수 있다.
예를 들어 Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메소드를 정의할 때 Consumer를 활용할 수 있다.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c){
for(T t : list){
c.accept(t);
}
}
forEach(Arrays.asList(1, 2, 3), (Integer i) -> System.out.println(++i));
결과 출력
2
3
4
제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메소드 apply를 정의한다.
입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.
String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 변환하는 map 메소드를 정의해보자.
@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 t : list){
result.add(f.apply(t));
}
return result;
}
List<Integer> list = map(Arrays.asList("one", "two", "three"), (String s) -> s.length());
System.out.println(list.toString());
결과 출력
[3, 3, 5]
지금까지 살펴본 함수형 인터페이스는 모두 제네릭 형식이다. 제네릭의 내부 구현상 제네릭 파라미터에는 참조형만 사용할 수 있다. 자바에서는 기본형을 참조형으로 변환하는 박싱이라는 기능을 제공한다. 반대로 참조형을 기본형으로 변환하는 언박싱 기능도 있다. 또한 박싱과 언박싱이 자동으로 이루어지는 오토박싱 기능도 제공한다.
예를 들어 int가 Integer로 박싱되는 예제를 살펴보겠다.
List<Integer> list = new ArrayList<>();
for(int i=0; i<100; i++){
list.add(i);
}
하지만 이런 변환 과정에는 비용이 소모된다. 박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장된다. 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.
자바8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.
예를 들어 아래 예제를 살펴보겠다.
public interface IntPredicate{
boolean test(int i);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(100); // 참(박싱 없음)
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 0;
oddNumbers.test(100); // 거짓(박싱)
첫 번째 함수는 100이라는 값을 박싱하지 않지만, 두 번째 함수에서는 100이라는 값을 Integer 객체로 박싱한다.
일반적으로 특정 형식을 입력으로 받는 함수형 인터페이스의 이름 앞에 DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction 처럼 형식이 붙는다. Function 인터페이스는 ToIntFunction<T>, IntToDoubleFunction 등의 다양한 출력 형식 파라미터를 제공한다.
- boolean 표현식
(List<String> list) -> list.isEmpty()
=>Predicate<List<String>>- 객체 생성
() -> new Pencil(COLOR.YELLOW, 30)
=>Supplier<Pencil>- 객체에서 소비
(Pencil p) -> { System.out.println(p); }
=>Consumer<Pencil>- 객체에서 선택/추출
(String s) -> s.length()
=>Function<String, Integer>
orToIntFunction<String>- 두 값을 조합
(int x, int y) -> x + y
=>IntBinaryOperator- 두 객체 비교
(Pencil p1, Pencil p2) -> p1.getLength().compareTo(p2.getLength())
=>Comparator<Pencil>
orBiFunction<Pencil, Pencil, Integer>
orToIntBiFunction<Pencil, Pencil>
함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉, 예외를 던지는 람다를 만드려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try/catch 블록으로 감싸야한다.
이 글은 모던 인 자바 액션 책을 실습하며 참고하여 작성한 글입니다.