JavaEasy#60
Java 8의 람다 표현식과 함수형 인터페이스를 설명해주세요.
#Java#람다#함수형#Java8
힌트
익명 클래스의 간결한 표현, @FunctionalInterface를 생각해보세요.
정답 및 해설
Java 8의 람다 표현식과 함수형 인터페이스를 설명해주세요.
Java 8에서 도입된 람다 표현식은 익명 함수를 간결하게 표현하는 문법으로, 함수형 프로그래밍 패러다임을 Java에 도입했습니다. 함수형 인터페이스와 결합하여 코드의 가독성과 유연성을 크게 향상시킵니다. 특히 컬렉션 처리, 이벤트 핸들링, 비동기 처리 등에서 코드를 획기적으로 단순화할 수 있습니다.
람다 표현식(Lambda Expression)
기본 문법
(매개변수) -> { 본문 }
- 매개변수가 하나일 경우 괄호 생략 가능
- 본문이 단일 표현식이면 중괄호 및
return생략 가능
// 기존 익명 클래스 방식
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// 람다 표현식으로 변환
Runnable r2 = () -> System.out.println("Hello");
// 매개변수 있는 경우
Comparator<String> comp1 = (s1, s2) -> s1.compareTo(s2);
// 여러 줄 본문
Comparator<String> comp2 = (s1, s2) -> {
System.out.println("comparing...");
return s1.compareTo(s2);
};
람다의 특징
- 간결성: 불필요한 보일러플레이트 코드 제거
- 지연 실행: 람다는 필요 시점에 실행
- 변수 캡처: 외부 지역 변수를 참조할 수 있으나 effectively final이어야 함
// effectively final 변수 캡처
String prefix = "Hello";
// prefix = "Hi"; // 이렇게 변경하면 컴파일 에러
Consumer<String> greeter = name -> System.out.println(prefix + ", " + name);
greeter.accept("World"); // Hello, World
함수형 인터페이스(Functional Interface)
추상 메서드가 정확히 하나만 있는 인터페이스입니다. @FunctionalInterface 애노테이션으로 명시적으로 선언하며, 이 애노테이션이 있으면 컴파일러가 조건을 검증합니다.
@FunctionalInterface
public interface MyFunction {
int apply(int a, int b);
// 추상 메서드 2개 이상이면 컴파일 에러
// default 메서드와 static 메서드는 허용
default void log() {
System.out.println("logging...");
}
}
// 사용
MyFunction add = (a, b) -> a + b;
System.out.println(add.apply(3, 5)); // 8
주요 내장 함수형 인터페이스
Java 8은 java.util.function 패키지에 다양한 표준 함수형 인터페이스를 제공합니다.
Function<T, R>
입력값 T를 받아 결과 R을 반환합니다.
Function<String, Integer> strToLen = s -> s.length();
System.out.println(strToLen.apply("Hello")); // 5
// andThen: 함수 합성 (f -> g 순서)
Function<Integer, String> intToStr = i -> "Length: " + i;
Function<String, String> combined = strToLen.andThen(intToStr);
System.out.println(combined.apply("Hello")); // "Length: 5"
// compose: 반대 순서로 합성 (g -> f 순서)
Function<Integer, Integer> doubleIt = x -> x * 2;
Function<Integer, Integer> addThree = x -> x + 3;
Function<Integer, Integer> doubleThenAdd = addThree.compose(doubleIt);
System.out.println(doubleThenAdd.apply(5)); // (5*2)+3 = 13
Predicate<T>
T를 받아 boolean을 반환합니다. 조건 검사에 사용합니다.
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isLong = s -> s.length() > 5;
// and, or, negate 조합
Predicate<String> isNotEmpty = isEmpty.negate();
Predicate<String> isNotEmptyAndLong = isNotEmpty.and(isLong);
List<String> words = Arrays.asList("", "Hi", "Hello World", "Java");
words.stream()
.filter(isNotEmptyAndLong)
.forEach(System.out::println); // "Hello World"
Consumer<T>
T를 받아 소비(처리)하고 반환값이 없습니다.
Consumer<String> printer = System.out::println;
Consumer<String> upperPrinter = s -> System.out.println(s.toUpperCase());
// andThen으로 체이닝
Consumer<String> bothPrint = printer.andThen(upperPrinter);
bothPrint.accept("hello");
// hello
// HELLO
Supplier<T>
매개변수 없이 T를 반환합니다. 지연 초기화 등에 유용합니다.
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> newList = listSupplier.get();
// 지연 초기화 패턴
Supplier<HeavyObject> lazyObj = () -> new HeavyObject(); // 실제 생성은 get() 호출 시
BiFunction<T, U, R>
두 개의 입력 T, U를 받아 R을 반환합니다.
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("ab", 3)); // "ababab"
기타 주요 인터페이스 정리
| 인터페이스 | 메서드 시그니처 | 설명 |
|---|---|---|
UnaryOperator<T> | T apply(T t) | 같은 타입 입출력 (Function 특수화) |
BinaryOperator<T> | T apply(T t1, T t2) | 같은 타입 두 입력 (BiFunction 특수화) |
IntFunction<R> | R apply(int value) | int 전용 Function (오토박싱 방지) |
ToIntFunction<T> | int applyAsInt(T t) | int 반환 Function |
IntUnaryOperator | int applyAsInt(int t) | int → int |
메서드 참조(Method Reference)
람다 표현식을 더 간결하게 표현하는 방법입니다. ClassName::methodName 형식을 사용합니다.
네 가지 유형
// 1. 정적 메서드 참조: ClassName::staticMethod
Function<String, Integer> parser = Integer::parseInt;
// 동일: s -> Integer.parseInt(s)
// 2. 인스턴스 메서드 참조 (특정 인스턴스): instance::method
String str = "Hello";
Supplier<String> upper = str::toUpperCase;
// 동일: () -> str.toUpperCase()
// 3. 인스턴스 메서드 참조 (임의 인스턴스): ClassName::instanceMethod
Function<String, String> toUpper = String::toUpperCase;
// 동일: s -> s.toUpperCase()
// 4. 생성자 참조: ClassName::new
Supplier<ArrayList<String>> listFactory = ArrayList::new;
Function<String, StringBuilder> sbFactory = StringBuilder::new;
// 동일: s -> new StringBuilder(s)
실전 활용 예시
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// 정렬
names.sort(String::compareTo);
// 출력
names.forEach(System.out::println);
// 변환
List<Integer> lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
람다와 스트림 API 활용
람다 표현식은 Stream API와 결합할 때 진가를 발휘합니다.
List<Employee> employees = getEmployees();
// 나이 30 이상인 직원의 이름을 알파벳 순으로 출력
employees.stream()
.filter(e -> e.getAge() >= 30) // Predicate
.sorted(Comparator.comparing(Employee::getName)) // Function
.map(Employee::getName) // Function
.forEach(System.out::println); // Consumer
// 평균 급여 계산
OptionalDouble avgSalary = employees.stream()
.mapToDouble(Employee::getSalary)
.average();
클로저와 변수 캡처 주의사항
// 잘못된 예: 람다 내부에서 변경되는 변수 참조 불가
List<Integer> numbers = Arrays.asList(1, 2, 3);
int sum = 0;
// numbers.forEach(n -> sum += n); // 컴파일 에러! sum은 effectively final이 아님
// 올바른 예: 스트림의 reduce 활용
int total = numbers.stream().mapToInt(Integer::intValue).sum();
// 또는 AtomicInteger 활용
AtomicInteger atomicSum = new AtomicInteger(0);
numbers.forEach(atomicSum::addAndGet);
커스텀 함수형 인터페이스 예시
// 체크 예외를 처리하는 함수형 인터페이스
@FunctionalInterface
public interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> f) {
return t -> {
try {
return f.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
}
// 사용
List<String> urls = Arrays.asList("http://example.com");
List<String> contents = urls.stream()
.map(ThrowingFunction.wrap(url -> fetchContent(url)))
.collect(Collectors.toList());
정리
| 구분 | 내용 |
|---|---|
| 람다 문법 | (매개변수) -> { 본문 } |
| 함수형 인터페이스 | 추상 메서드 하나만 있는 인터페이스, @FunctionalInterface 권장 |
Function<T,R> | T 입력 → R 반환, apply() |
Predicate<T> | T 입력 → boolean 반환, test() |
Consumer<T> | T 입력 → 반환 없음, accept() |
Supplier<T> | 입력 없음 → T 반환, get() |
BiFunction<T,U,R> | T, U 입력 → R 반환, apply() |
| 메서드 참조 | ClassName::method 형식으로 람다 간결화 |
| 변수 캡처 | 외부 변수는 effectively final이어야 함 |
| 주요 활용 | Stream API, 이벤트 핸들러, 콜백, 전략 패턴 |