이번에는 자바의 람다식(Lambda Expression)에 대해 정리해보려고 한다.
자바스크립트에서 화살표 함수를 자주 사용해서 그런지 꽤 친숙한 느낌이었다.
람다 표현식은 함수형 프로그래밍을 가능하게 만들어주는 중요한 문법이라고 하는데, 간단하게 람다식에 대해서 정리해보자 👀
람다식은 함수를 하나의 식(expression)으로 표현한 것이다.
메서드의 이름과 반환값이 없어지므로 익명 함수라고도 한다.
기존의 자바가 객체지향 언어의 특성만을 가졌다면, 람다식의 등장으로 함수형 프로그래밍이 가능해졌다.
왜 람다식이 필요한지 간단한 예제를 통해 살펴보자!
Sample Code
interface Calculator { int calculate(int x, int y); } class Addition implements Calculator { @Override public int calculate(int x, int y) { return x + y; } }
// 사용 예시 Calculator calc = new Addition(); System.out.println(calc.calculate(5, 3));
위 코드에서는 calculate 메서드를 사용하기 위해 해당 타입을 갖는 인스턴스를 생성하고 호출하고 있다.
하지만, 이것도 길다고 느껴지는 것 같다. 위 코드를 람다식으로 변경해보자
(이정도면 개발자는 코드 줄이기에 광적으로 집착하는거 아닌가싶다)
같은 기능을 람다식으로 표현하면 다음과 같다.
Sample Code
Calculator calc = (x, y) -> x + y; System.out.println(calc.calculate(5, 3));
Calculator 타입을 갖는 calc 변수를 생성하고, 뒤에 람다식을 사용해서 함수를 정의하고 있다.
하지만, 필자는 이 코드를 처음봤을 때 변수 선언 뒤에 어떻게 표현식이 올 수 있는지 이해가되지 않았다.
이후에 강사님께서 말씀해주시는 내용을 들어보니 그 이유는 다음과 같았다.
Notice
Calculator 인터페이스와 같이 내부 메서드가 하나만 있다면,
뒤에 정의한 함수는 해당 메서드의 구현부가 된다.
즉, 위에서 정의한 (x, y) -> x + y;
가 calculate의 구현부가 된 것이다.
(이 부분은 뒤에서 @FunctionalInterface
에서 다룰예정이다.)
람다식을 구성하는 요소는 매개변수
, 화살표
, 함수 Body
이렇게 크게 3가지로 나눌 수 있을 것 같다.
우선 예시 코드를 한번 살펴보자
Sample Code
// 1. 매개변수가 하나일 때 parameter -> expression // 2. 매개변수가 여러 개일 때 (parameter1, parameter2) -> expression // 3. 함수 body가 여러 줄일 때 (parameter1, parameter2) -> { statements; return value; }
위 코드처럼 매개변수를 받는 부분과 이를 사용해서 함수를 정의하는 부분으로 나눌 수 있다.
사실 람다식이 JavaScript의 화살표 함수와 크게 다를 부분은 없다.
함수 Body에서 바로 return이 필요하다면 대괄호를 생략할 수 있다는 것, 그리고 대괄호를 사용하면 return이 필요하다는 것 등등 모두 비슷하다.
람다식은 앞으로 자주 사용하게 될테니, 지금은 람다식이 어떤 구성으로 되어있는지만 파악하도록 하자
람다식을 사용하기 위해서는 함수형 인터페이스라는 개념을 알아야 한다.
( 1. 람다식이란?
에서 예제코드를 보여주며 잠깐 넘어간 부분이다.)
여기서 함수형 인터페이스는 단 하나의 추상 메서드만 가지는 인터페이스를 의미한다.
우선 함수형 인터페이스를 사용한 예제를 살펴보자
Sample Code
@FunctionalInterface interface Printable { void print(String message); }
// 사용 Printable printer = message -> System.out.println(message); printer.print("Hello Lambda!");
이전에 살펴봤던 Calculator 인터페이스와 같이 Printable 인터페이스를 새로 정의했다.
여기서 Calculator와 Printable의 차이점은 @FunctionalInterface
어노테이션의 여부이다.
해당 어노테이션이 추가된 Printable은 내부에 메서드를 오직 하나만 정의할 수 있다.
반면, 이런 어노테이션이 없는 Calculator는 내부에 메서드를 여러개 정의할 수 있게된다.
이렇게 되면 기존과 같이 사용할 수 없게된다.
예제 코드를 한번 살펴보자
Sample Code
interface Calculator { int calculate(int x, int y); int print(String s); }
Calculator calc = (x, y) -> x + y; // 오류 발생
여기서 오류가 발생하는 이유는 선언한 람다식이 어느 메서드의 구현인지 특정할 수 없기 때문이다.
함수형 인터페이스 어노테이션의 개념을 이해하면 쉽다!
자바에서는 자주 사용되는 형태의 메서드들을 함수형 인터페이스로 미리 정의해두었다.
(각각의 인터페이스는 그 용도가 매우 명확하다.)
다음 예제를 통해 각 인터페이스의 특징과 활용법을 살펴보자
함수형 인터페이스 예제
// 1. Runnable - 매개변수도 없고 반환값도 없음 Runnable task = () -> System.out.println("실행"); task.run(); // "실행" 출력
// 2. Consumer<T> - 값을 받아서 소비만 하고 반환하지 않음 Consumer<String> printer = str -> System.out.println("입력값: " + str); printer.accept("Hello"); // "입력값: Hello" 출력 // forEach 등 스트림 작업에서 각 요소를 처리할 때 자주 사용
// 3. Supplier<T> - 매개변수 없이 값만 제공 Supplier<LocalDateTime> now = () -> LocalDateTime.now(); System.out.println(now.get()); // 현재 시간 출력
// 4. Function<T, R> - 입력값을 다른 값으로 변환 Function<String, Integer> wordCounter = str -> str.length(); int length = wordCounter.apply("Lambda"); // 6 반환 // map 연산 등에서 데이터 변환 시 활용
// 5. Predicate<T> - 조건식 표현 Predicate<String> isEmpty = str -> str.trim().length() == 0; boolean result = isEmpty.test(" "); // true 반환 // filter 작업이나 데이터 검증에 활용
이러한 함수형 인터페이스들은 주로 스트림 API와 함께 사용되어 데이터 처리를 간결하고 명확하게 만들어준다.
위 코드에서 확인할 수 있는 특징은 각 함수형 인터페이스마다 정의된 메서드 명이 다르다는 것이다.
Predicate에서는 test
를 사용하고, Function에서는 apply
를 사용한다.
이처럼 각 함수형 인터페이스마다 메서드명은 다르니 용도를 명확히 구분해서 사용할 수 있어야 한다.
이러한 함수형 인터페이스는 다음과 같이 스트림과 조합하여 자주 사용된다.
(스트림은 다음에 정리할 예정이다)
Sample Code 1
List<String> names = Arrays.asList("김자바", "이파이썬", "", "박코틀린"); names.stream() .filter(str -> !str.isEmpty()) // Predicate .map(str -> str.length()) // Function .forEach(len -> System.out.println(len)); // Consumer
이 예제에 달린 주석을 보면 해당 람다식에 왜 이런 주석이 달렸는지 이해가 안될 수 있다.
위에서 사용된 filter, map, forEach의 매개변수로 전달되는 람다식은 주석으로 달린 함수형 인터페이스를 받는다.
실제 구현을 살펴보면 다음 사진과 같다.
filter, map, forEach 구현부분
따라서 위 코드를 풀어서 작성해보면 다음과 같다.
Sample Code 2
List<String> names = Arrays.asList("김자바", "이파이썬", "", "박코틀린"); Predicate<String> isStringEmpty = str -> !str.isEmpty(); Function<String, Integer> findLength = str -> str.length(); Consumer<Integer> println = len -> System.out.println(len); names.stream() .filter(isStringEmpty) .map(findLength) .forEach(println);
각 함수형 인터페이스를 정의해서 넣어줘도 동일하게 동작하는 것을 확인할 수 있다!
실제로 개발할 때는 이런 함수형 인터페이스를 의식하면서 코딩하지는 않는다.
필자도 처음 스트림을 사용할 때는 그냥 필요한대로 람다식을 작성했고, 나중에 코드를 들여다보니 이런 함수형 인터페이스들이 사용되고 있었다.
이전에 살펴본 코드는 그저 "비어있지 않은 문자열을 골라서, 길이로 변환하고, 그걸 출력하자"라는 생각으로 작성한 코드일 뿐이다.
하지만 내부적으로는 각각 Predicate, Function, Consumer라는 함수형 인터페이스가 사용되고 있다.
이처럼 각각의 함수형 인터페이스는 특정 용도에 맞게 최적화되어 있지만, 실제 개발할 때는 그저 자연스럽게 사용하게 된다.
함수형 인터페이스가 이런 식으로 사용될 수 있다는 것만 기억해두자
이번 포스팅에서는 자바의 람다식과 함수형 인터페이스에 대해서 알아봤다.
람다식은 코드를 간결하게 만들어주는 것은 물론이고, 함수형 프로그래밍의 장점을 자바에서도 활용할 수 있게 해주는 강력한 기능이다.
특히 스트림 API와 함께 사용하면 컬렉션을 다루는 코드를 훨씬 더 깔끔하게 작성할 수 있다
자바스크립트의 화살표 함수처럼 자연스럽게 사용할 수 있으면 좋겠다 👊