Java 람다 표현식 마스터하기: 기초부터 Stream API까지
Java 람다 표현식 마스터하기
명확한 예제와 함께 Java의 람다 표현식, 함수형 인터페이스, 제네릭, Stream API를 탐구합니다.
Java 8은 언어의 표현력을 향상시키기 위해 람다 표현식을 도입하여 개발자가 더 간결하고 읽기 쉬운 코드를 작성할 수 있게 했습니다.
람다에 들어가기 전에 람다 구현의 근간인 함수형 인터페이스를 이해하는 것이 필수적입니다.
함수형 인터페이스란?
함수형 인터페이스는 정확히 하나의 추상 메서드만을 포함하는 Java 인터페이스입니다. 이 단일 메서드가 인터페이스의 의도된 목적을 정의합니다.
java.lang의 Runnable 인터페이스는 run() 메서드만 노출하는 고전적인 예입니다.
예제 1: 함수형 인터페이스 정의
import java.lang.FunctionalInterface;
@FunctionalInterface
public interface MyInterface {
// 단일 추상 메서드
double getValue();
}
여기서 MyInterface는 @FunctionalInterface로 주석 처리되어 컴파일러가 함수형 인터페이스 계약을 강제하여 여러 추상 메서드의 우발적 추가를 방지합니다. 이 주석은 선택 사항이지만 명확성과 안전성을 위해 강력히 권장됩니다.
Java 7에서 함수형 인터페이스는 단일 추상 메서드(SAM)라고 불리며 일반적으로 익명 클래스로 구현되었습니다.
예제 2: 익명 클래스로 SAM 구현
public class FunctionInterfaceTest {
public static void main(String[] args) {
// Runnable 함수형 인터페이스를 구현하는 익명 클래스
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("I just implemented the Runnable Functional Interface.");
}
}).start();
}
}
출력:
I just implemented the Runnable Functional Interface.
익명 클래스는 전통적 구현보다 보일러플레이트를 줄여주지만 문법은 여전히 장황합니다. 람다 표현식은 함수형 인터페이스를 인수로 전달할 때 명시적 메서드 이름이 필요 없게 합니다.
람다 표현식 소개
람다 표현식은 기본적으로 함수형 인터페이스의 추상 메서드에 대한 간결한 구현을 제공하는 익명 메서드입니다. 독립적으로 실행되지 않으며 대신 인터페이스 메서드의 동작을 제공합니다.
Java에서 람다 표현식 정의 방법
일반적인 문법은 다음과 같습니다:
(parameter list) -> lambda body
화살표 연산자(->)는 매개변수 목록과 바디를 분리합니다. 아래는 구체적 예제들입니다.
Pi를 반환하는 메서드가 있다고 가정해 봅시다:
double getPiValue() {
return 3.1415;
}
람다 사용 시:
() -> 3.1415
매개변수가 없으면 화살표 왼쪽은 비어 있고, 오른쪽은 값을 반환하는 표현식 바디입니다.
람다 바디의 유형
Java 람다 바디는 두 가지 형태가 있습니다:
1. 표현식 바디 - 단일 표현식을 포함합니다.
() -> System.out.println("Lambdas are great");
2. 블록 바디 - 중괄호로 캡슐화되어 여러 문장과 선택적 return을 허용합니다.
() -> {
double pi = 3.1415;
return pi;
};
참고: 표현식 바디는 명시적 return이 필요 없지만, 값을 반환하는 블록 바디에는 필요합니다.
예제 3: 실전 람다 표현식
람다 표현식을 사용하여 Pi를 반환하는 Java 프로그램을 작성해 봅시다.
import java.lang.FunctionalInterface;
@FunctionalInterface
interface MyInterface {
double getPiValue();
}
public class Main {
public static void main(String[] args) {
MyInterface ref = () -> 3.1415;
System.out.println("Value of Pi = " + ref.getPiValue());
}
}
실행 결과:
Value of Pi = 3.1415
핵심 포인트:
MyInterface는 단일 추상 메서드를 정의합니다.
MyInterface에 대한 참조가 선언되지만 직접 인스턴스화되지 않습니다.
- 람다 표현식이
getPiValue()의 구현을 제공합니다.
ref.getPiValue() 호출이 람다 바디를 트리거합니다.
매개변수가 있는 람다 표현식
메서드처럼 람다는 매개변수를 받을 수 있습니다. 예:
(n) -> (n % 2) == 0
이 람다는 입력 정수가 짝수인지 확인합니다.
예제 4: 매개변수화된 람다로 문자열 뒤집기
@FunctionalInterface
interface MyInterface {
String reverse(String s);
}
public class Main {
public static void main(String[] args) {
MyInterface ref = (str) -> {
String result = "";
for (int i = str.length() - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
};
System.out.println("Lambda reversed = " + ref.reverse("Lambda"));
}
}
실행 결과:
Lambda reversed = adbmaL
제네릭 함수형 인터페이스
함수형 인터페이스는 제네릭일 수 있어 모든 데이터 타입에서 작동할 수 있습니다. 예를 들어:
@FunctionalInterface
interface MyInterface<T> {
T func(T t);
}
예제 5: 문자열과 정수를 위한 제네릭 람다
public class Main {
public static void main(String[] args) {
// 문자열 뒤집기
MyInterface<String> reverse = (str) -> {
String result = "";
for (int i = str.length() - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
};
System.out.println("Lambda reversed = " + reverse.func("Lambda"));
// 정수 팩토리얼
MyInterface<Integer> factorial = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
};
System.out.println("factorial of 5 = " + factorial.func(5));
}
}
실행 결과:
Lambda reversed = adbmaL
factorial of 5 = 120
람다 표현식과 Stream API
JDK 8에 도입된 java.util.stream 패키지는 컬렉션에서 필터링, 매핑, 리듀싱, 정렬 등 함수형 스타일 연산을 수행할 수 있게 합니다.
각각 국가 이름으로 접두사가 붙은 장소 이름 리스트를 고려해 보세요. 스트림과 람다를 사용하면 데이터를 효율적으로 추출하고 조작할 수 있습니다.
예제 6: 람다로 스트림 처리
import java.util.ArrayList;
import java.util.List;
public class StreamMain {
static List<String> places = new ArrayList<>();
public static List<String> getPlaces() {
places.add("Nepa, Kathmandu");
places.add("Nepa, Pokhara");
places.add("India, Delhi");
places.add("USA, New York");
places.add("Africa, Nigeria");
return places;
}
public static void main(String[] args) {
List<String> myPlaces = getPlaces();
System.out.println("Places from Nepal:");
myPlaces.stream()
.filter(p -> p.startsWith("Nepa"))
.map(p -> p.toUpperCase())
.sorted()
.forEach(p -> System.out.println(p));
}
}
실행 결과:
Places from Nepal:
NEPA, KATHMANDU
NEPA, POKHARA
여기서 스트림 연산 체인(filter, map, sorted, forEach)은 각각 람다를 받아 Java 8 이전 솔루션에 비해 보일러플레이트를 크게 줄여줍니다.