자바 8부터 도입된 람다식은 코드의 간결성과 가독성을 크게 향상시켜주는 새로운 문법이다.
기존엔 단일 메서드를 구현하기 위해 익명 클래스를 자주 사용했지만 이 경우에 코드가 장황해지고 복잡해질 수 있었다.
Oracle 공식 문서 튜토리얼에 따르면 추상 메서드가 하나뿐인 익명 클래스의 구현은 문법이 번거롭고 명확하지 않을 수 있다며 동작을 메서드 인자로 전달하기 위해 람다식을 도입했다고 설명한다.
람다식의 필요성
람다식 도입 전엔 이벤트 처리나 컬렉션 정렬 등에서 주로 익명 클래스가 사용됐다.
하지만 하나의 인터페이스에 추상 메서드가 단 하나일 때조차 익명 클래스를 작성해야 하므로 코드가 길어지고 복잡해질 수 있다.
자바스크립트, 스칼라 등 다른 언어에서 사용되던 함수형 프로그래밍 기법이 인기를 끌면서 자바 진영에서도 블록을 일급 객체처럼 다루길 원했다.
자바의 람다식 도입은 코드의 간결성을 높이고 함수형 프로그래밍의 장점을 누릴 수 있도록 하기 위함이다.
람다식 기본 문법
람다식의 기본 형태를 보자
(매개변수 목록) -> { 실행 코드 }
매개변수 부분과 화살표(->), 그리고 본문 부분으로 이루어져 있다.
예를 들어 매개변수가 없고 반환값도 없는 경우 () -> { ... } 처럼 작성한다.
스레드를 생성할 때 Runnable 인터페이스를 구현하는 람다식을 구현해보자.
익명 클래스를 사용하면 이렇게 된다.
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello Lambda!");
}
};
r.run();
Runnable은 추상 메서드가 하나뿐이므로 함수형 인터페이스이다. 매개변수도 없고 실행 코드도 한 줄이다.
Runnable r = () -> System.out.println("Hello Lambda!");
r.run();
람다식에서 매개변수는 타입을 생략할 수 있고 단일 매개변수일 때는 괄호도 생략 가능하다.
예를 들어 x -> x * x는 하나의 매개변수 x를 받아 제곱을 반환하는 람다식이며 타입 선언 없이 작성했다.
화살표 오른 쪽의 본문이 한 문자 표현식일 땐 중괄호와 return을 생략할 수 있고 다중 문장이 필요하면 {}로 감싸고 return을 사용한다.
대상 타입이 이미 결정되어 있으면 컴파일러가 매개변수 타입을 추론하므로 파라미터 타입 표기도 일반적으로 생략한다.
List<String> names = Arrays.asList("Bob", "Alice", "Charlie");
names.sort((a, b) -> a.compareTo(b)); // Comparator<String>을 대체하는 람다식
이 코드에서 (a,b)의 타입은 String으로 추론되므로 명시하지 않아도 된다.
람다식 문법의 주요 표인트를 보자
- 매개변수 : 타입 생략 가능. 매개변수가 하나이고 타입도 추론 가능하면 괄호 생략함(x -> ...). 매개변수가 없으면 ().
- 화살표 연산자( ->) : 좌측 매개변수 리스트와 우측 본문 구분
- 본문 : 표현식 한 줄이면 {}와 return 없이 쓸 수 있고 여러 문장이면 { ... } 로 묶고 필요할 경우 return 사용
- 반환형: 람다식은 자체적으로 반환형을 선언하지 않으며 호출되는 함수형 인터페이스의 추상 메서드 시그니처에서 반환형을 결정한다. 단일 표현식 람다의 결과는 자동으로 반환된다.
Function<Integer, Integer> square1 = (Integer x) -> x * x; // 매개변수 타입 명시
Function<Integer, Integer> square2 = x -> x * x; // 타입 생략, 괄호 생략
Comparator<String> comp = (s1, s2) -> s1.compareTo(s2); // 두 매개변수
Supplier<Double> randomValue = () -> Math.random(); // 매개변수 없음
함수형 인터페이스와 람다식
람다식은 반드시 함수형 인터페이스를 대상으로 사용된다.
함수형 인터페이스란 하나의 추상 메서드만을 가진 인터페이스로, 자바 컴파일러는 이걸 기준으로 람다식이 구현할 메서드를 결정한다.
Java 8 문서에선 Runnable이나 Comparator<T>, Callable<T> 등 자바 8에서 함수형 인터페이스라 칭하는 것들은 단 하나의 메서드만 구현하면 되는 인터페이스라고 정의한다.
즉, 람다식은 함수형 인터페이스가 요구하는 추상 메서드를 대신 구현하는 익명 객체를 생성한다고 생각하면 된다.
@FunctionalInterface
public interface Action {
void execute(String name);
}
이런 함수형 인터페이스가 있을 때 람다식 name -> System.out.println(name)은 Action 인터페이스의 execute 메서드를 구현하는 익명 객체를 생성하는 것과 같은 효과를 낸다.
자바 8부터는 java.util.function 패키지에 Consumer, Function, Predicate, Supplier 등 기본적인 함수형 인터페이스가 다수 제공된다.
이런 인터페이스를 활용하면 별도의 인터페이스 정의 없이 람다식을 바로 이용할 수 있다.
주요 함수형 인터페이스
자바8에서 자주 사용되는 대표적인 함수형 인터페이스들을 알아보자.
consumer<T>
- void accept(T t) 메서드를 가지며 T 타입의 입력을 받아서 처리를 수행하고 결과를 반환하지 않는다. 주로 컬렉션 요소를 처리할 때 사용한다. -> “하나의 입력을 받아 결과를 반환하지 않는 연산”
- Consumer<String> printer = s -> System.out.println(s);
Supplier<T>
- T get() 메서드로 결과를 공급하는 인터페이스이다. 입력 없이 호출되어 T 타입의 값을 반환한다. 결과를 제공하는 기능을 가지며 주로 값 생성이나 Lazy 연산에 사용된다.
- 예: Supplier<Integer> rand = () -> (int)(Math.random()*100);
Function<T,R>
- R apply(T t) 메서드를 가지며 T 타입 입력을 받아 R 타입 결과를 돌려주는 함수이다. "하나의 인자를 받아 결과를 생성하는 함수"라고 정의되어 있다.
- Function<String, Integer> toInt = s -> Integer.parseInt(s); 와 같이 문자열을 정수로 변환하는 데 활용할 수 있다.
Predicate<T>
- boolean test(T t) 메서드를 가지며 T 타입 입력에 대해 참/거짓을 판별하는 불린 조건 함수이다. 하나의 인자를 받아 불리언 값을 반환하는 조건문이라고 설명된다,
- 리스트에서 특정 조건의 요소를 골라낼 때 자주 사용한다(ex. 필터링)
- Predicate<Integer> isEven = n -> (n % 2 == 0);
람다식 예제
1. 컬렉션 처리: forEach() + Consumer
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
//람다식 사용 전
//names.forEach(new Consumer<String>() {
// @Override
// public void accept(String name) {
// System.out.println("이름: " + name);
// }
//});
names.forEach(name -> {
System.out.println("이름: " + name);
});
2. 정렬 처리 - 함수형 인터페이스 Comparator를 대체
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
//람다식 사용 전
//names.sort(new Comparator<String>() {
// @Override
// public int compare(String s1, String s2) {
// return s1.compareTo(s2);
// }
//});
names.sort((s1, s2) -> s1.compareTo(s2));
names.sort(String::compareTo);
3. 조건 필터링 - Predicate 활용
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
//List<Integer> evens = numbers.stream()
// .filter(new Predicate<Integer>() {
// @Override
// public boolean test(Integer number) {
// return number % 2 == 0;
// }
// })
// .collect(Collectors.toList());
//System.out.println(evens);
// 짝수만 출력
numbers.stream()
.filter(n -> n % 2 == 0) // Predicate<Integer>
.forEach(System.out::println);
4. Function을 사용한 값 가공
List<String> words = Arrays.asList("hello", "java", "lambda");
//List<Integer> lengths = words.stream()
// .map(new Function<String, Integer>() {
// @Override
// public Integer apply(String word) {
// return word.length();
// }
// })
// .collect(Collectors.toList());
//System.out.println(lengths);
// 문자열 길이 출력
words.stream()
.map(word -> word.length()) // Function<String, Integer>
.forEach(System.out::println);
5. 값 생성 - Supplier 사용
//Supplier<Double> randomSupplier = new Supplier<Double>() {
// @Override
// public Double get() {
// return Math.random();
// }
//};
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println("랜덤값: " + randomSupplier.get());
실제 코드를 먼저 쳐보고 람다식으로 어떻게 바꿀지 최대한 생각하면서 코드를 짜보자
'Language > Java' 카테고리의 다른 글
[Java] 제네릭을 왜 쓰는 걸까? (1) | 2025.05.07 |
---|---|
[Java] Map을 왜 사용할까? (0) | 2025.03.17 |
[Java] 빌더 패턴과 스프링에서의 활용 (2) | 2025.01.08 |
[Java] toString을 왜 쓸까? (0) | 2025.01.07 |
[Java] 예외처리 코드 연습 (0) | 2025.01.05 |