자바 공부를 하다가 보면 가끔 이런 식으로 쓰인 부분들이 있다.
List<String> filtered = stream
.filter(name -> name.startsWith("A")) // 여기같이 표현
.map(String::toUpperCase) // 이거는 '메서드 참조'이다
name -> name.startsWith("A")
이게 뭐지?
이건 람다식 혹은 람다라고 하는 코드 작성법이다.
자바의 람다식(lambda expression)은 익명 함수(이름이 없는 함수)를 간결하게 표현하는 방식으로, 주로 함수형 인터페이스를 구현할 때 사용된다. Java 8부터 도입되었으며, 코드의 가독성을 높이고 간결하게 작성할 수 있는 장점이 있다.
기본 문법은 다음과 같다 :
(매개변수) -> { 실행문 }
// 매개변수 하나, 실행문 하나
x -> System.out.println(x)
// 매개변수 여러 개, 반환값 있는 경우
(x, y) -> x + y
// 실행문 여러 줄
(x, y) -> {
int result = x + y;
return result;
}
실제로도 보면 작성도 쉽고 읽기도 쉽다.
장점과 단점에 대해 좀 더 자세하게 알아보자.
람다식의 장점
1. 코드가 간결해짐
- 익명 클래스보다 훨씬 짧고 읽기 쉬움.
// 전통적인 방식
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Hello!");
}
};
// 람다식
Runnable r2 = () -> System.out.println("Hello!");
2. 가독성이 좋아짐
- 함수의 목적이 명확하고, 코드 흐름이 더 직관적.
- 특히 Stream API와 함께 쓰면 굉장히 깔끔한 코드가 가능.
List<String> list = Arrays.asList("Helldiver", "Stratagem", "Bug");
list.stream()
.filter(s -> s.startsWith("H"))
.forEach(System.out::println);
3. 병렬 처리 등 함수형 프로그래밍에 적합
- Stream API와 함께 사용하면 병렬 작업도 쉽게 적용할 수 있다.
list.parallelStream().forEach(System.out::println);
4. 콜백 구현이 쉬움
- 이벤트 처리, 비동기 작업 등에 콜백을 깔끔하게 구현 가능.
람다식의 단점
1. 복잡한 로직에는 부적합
- 여러 줄로 작성할 경우 오히려 가독성이 떨어짐.
(x, y) -> {
if (x > y) return x;
else return y;
}
복잡하면 그냥 일반 메서드나 익명 클래스로 하는 게 더 명확할 수도 있음.
2. 디버깅이 어려움
- 익명 함수이기 때문에 디버거에서 이름이 없어 추적하기 어려움.
- 예외 처리 위치도 찾기 힘들 수 있음.
3. 오용 시 가독성 하락
- 지나치게 중첩된 람다식은 오히려 코드 이해를 방해함.
list.stream()
.filter(x -> x.length() > 3)
.map(x -> x.toUpperCase())
.sorted((a, b) -> a.compareTo(b))
.forEach(System.out::println);
// 너무 길어지면 눈 아픔...
4. 익명 클래스와의 기능 차이
- 익명 클래스는 다중 메서드를 구현하거나 상태를 가질 수 있지만, 람다식은 그럴 수 없음
익명 클래스는 내부에서 여러 메서드를 정의할 수 있지만
람다식은 오직 하나의 추상 메서드만 표현 가능
이런 람다식이지만 이걸 활용하기 위해서는 함수형 인터페이스를 써야한다는 전제가 있다.
함수형 인터페이스란?
- 자바에서 람다식은 익명 구현 객체를 생성하는 문법이다.
- 이때 어떤 인터페이스를 구현하는 객체가 생성되는데, 추상 메서드가 하나만 있어야 어떤 메서드를 구현할지 애매하지 않기 때문에 그 조건이 필요함.
@FunctionalInterface
interface MyFunction {
void run();
}
@FunctionalInterface 어노테이션은 선택 사항이지만, 붙이면 컴파일러가 실수로 두 개 이상의 추상 메서드를 만들지 않도록 도와준다.
// 컴파일 에러 발생!
@FunctionalInterface
interface InvalidInterface {
void method1();
void method2(); // 에러!
}
1. 함수형 인터페이스의 특징
- 단 하나의 추상 메서드만 존재
- default, static 메서드는 여러 개 있어도 무관 (이것들은 추상 메서드가 아님)
- @FunctionalInterface는 선택이지만, 붙이는 걸 권장
2. 예시: 직접 만든 함수형 인터페이스와 람다식 사용
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
public class Test {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println(add.calculate(3, 4)); // 7
System.out.println(multiply.calculate(3, 4)); // 12
}
}
3. 자바에서 제공하는 주요 함수형 인터페이스
Java 8부터 java.util.function 패키지에서 많이 제공하고, 이걸 기반으로 스트림 API, 람다식 등에서 사용함.
인터페이스 | 추상 메서드 | 설명 |
Function<T, R> | R apply(T t) | T를 받아 R로 변환 |
Consumer<T> | void accept(T t) | T를 받아 소비 (반환 없음) |
Supplier<T> | T get() | T를 반환 (입력 없음) |
Predicate<T> | boolean test(T t) | T를 받아 조건 검사 (boolean 반환) |
UnaryOperator<T> | T apply(T t) | T를 받아 T를 반환 (Function의 특수 형태) |
BinaryOperator<T> | T apply(T t1, T t2) | T 둘을 받아 T를 반환 |
3-1. 예시:
1. Predicate<T>
- T를 받아 조건 검사 후 boolean 반환
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isPositive = x -> x > 0;
System.out.println(isPositive.test(10)); // true
System.out.println(isPositive.test(-5)); // false
}
}
2. Supplier <T> — "공급자"
- 아무것도 받지 않고, T를 리턴하는 함수형 인터페이스
- 메서드: T get()
import java.util.function.Supplier;
public class SupplierExample {
public static void main(String[] args) {
Supplier<String> stringSupplier = () -> "HELLDIVERS 2 - MISSION READY!";
System.out.println(stringSupplier.get());
}
}
3. Consumer <T> — "소비자"
- T를 받아서 소비만 하고, 리턴값은 없음
- 메서드: void accept(T t)
import java.util.function.Consumer;
public class ConsumerExample {
public static void main(String[] args) {
Consumer<String> printStratagem = stratagem -> System.out.println("Deploying: " + stratagem);
printStratagem.accept("Eagle Airstrike");
}
}
4. Function <T, R> — "변환자"
- T를 받아서 R로 변환
- 메서드: R apply(T t)
import java.util.function.Function;
public class FunctionExample {
public static void main(String[] args) {
Function<String, Integer> stringLength = str -> str.length();
System.out.println("Length: " + stringLength.apply("Super Earth"));
}
}
개인적으로 람다식을 잘 사용을 안 해서 아직도 어색하다.
나중에 시간이 되면 람다식 연습도 해봐야겠다.
'공부 > 자바(Java)' 카테고리의 다른 글
Java) 자바 SOLID 원칙 (객체 지향 설계의 5가지 원칙) (1) | 2025.04.21 |
---|---|
Java) Java의 객체지향 프로그래밍(OOP)이란? (1) | 2025.04.21 |
Java) Stream API란? (0) | 2025.04.06 |
Java) Set이란? (세트 간단 설명, 사용법, 예제) (0) | 2025.04.06 |
Java) Arrays VS Collections | ( Arrays와 Collections의 간단 설명, 사용법, 예제) (0) | 2025.04.05 |