Java 함수형 프로그래밍 - Lambda와 Method Reference



Java 8부터 도입된 람다 표현식과 메서드 참조를 사용하면 간결하고 읽기 쉬운 코드를 작성할 수 있습니다.



언제 사용하나요?



  • 컬렉션 필터링, 매핑, 정렬

  • 이벤트 핸들러, 콜백 구현

  • 스트림 API와 함께 사용

  • 함수를 파라미터로 전달할 때



람다 표현식 기본 문법


// 기본 형태
(파라미터) -> { 실행문; }

// 파라미터 1개면 괄호 생략 가능
x -> x * 2

// 실행문 1개면 중괄호, return 생략 가능
(a, b) -> a + b

// 타입 추론 가능하면 타입 생략
(String s) -> s.length() // 명시
s -> s.length() // 생략


Before / After 비교


// Before - 익명 클래스
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});

// After - 람다
Collections.sort(list, (a, b) -> a.compareTo(b));

// 더 간단하게 - 메서드 참조
Collections.sort(list, String::compareTo);


함수형 인터페이스


// 하나의 추상 메서드를 가진 인터페이스
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b);
}

// 람다로 구현
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;

System.out.println(add.calculate(3, 5)); // 8
System.out.println(multiply.calculate(3, 5)); // 15


주요 내장 함수형 인터페이스









인터페이스메서드용도
Predicate<T>boolean test(T t)조건 검사
Function<T,R>R apply(T t)변환
Consumer<T>void accept(T t)소비 (반환값 없음)
Supplier<T>T get()생성 (입력 없음)
BiFunction<T,U,R>R apply(T t, U u)2개 입력 변환


Predicate - 조건 검사


Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<String> isLong = s -> s.length() > 10;

// 사용
isEmpty.test(""); // true
isLong.test("Hello World"); // true

// 조합
Predicate<String> isNotEmpty = isEmpty.negate();
Predicate<String> combined = isEmpty.or(isLong);

// 스트림에서
list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());


Function - 변환


Function<String, Integer> toLength = s -> s.length();
Function<Integer, Integer> square = n -> n * n;

// 사용
toLength.apply("Hello"); // 5

// 합성
Function<String, Integer> composed = toLength.andThen(square);
composed.apply("Hello"); // 25 (5의 제곱)

// 스트림에서
list.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());


Consumer - 소비


Consumer<String> printer = s -> System.out.println(s);
Consumer<String> logger = s -> log.info(s);

// 사용
printer.accept("Hello");

// 체이닝
Consumer<String> both = printer.andThen(logger);

// 스트림에서
list.forEach(s -> System.out.println(s));


Supplier - 생성


Supplier<LocalDateTime> now = () -> LocalDateTime.now();
Supplier<Double> random = () -> Math.random();
Supplier<User> userFactory = () -> new User();

// 사용
LocalDateTime time = now.get();

// 지연 실행에 유용
Optional.ofNullable(value)
.orElseGet(() -> createDefault());


메서드 참조 (Method Reference)


// 4가지 유형

// 1. 정적 메서드 참조
Function<String, Integer> parser = Integer::parseInt;
// = s -> Integer.parseInt(s)

// 2. 인스턴스 메서드 참조 (특정 객체)
String str = "Hello";
Supplier<Integer> lengthSupplier = str::length;
// = () -> str.length()

// 3. 인스턴스 메서드 참조 (임의 객체)
Function<String, Integer> length = String::length;
// = s -> s.length()

// 4. 생성자 참조
Supplier<List<String>> listFactory = ArrayList::new;
// = () -> new ArrayList<>()

Function<String, User> userFactory = User::new;
// = name -> new User(name)


스트림과 함께 사용


List<User> users = Arrays.asList(
new User("Kim", 30),
new User("Lee", 25),
new User("Park", 35)
);

// 필터링 + 매핑 + 수집
List<String> names = users.stream()
.filter(u -> u.getAge() >= 30) // Predicate
.map(User::getName) // Function
.sorted() // Comparator
.collect(Collectors.toList());

// 출력
users.forEach(System.out::println); // Consumer

// 조건 검사
boolean allAdult = users.stream()
.allMatch(u -> u.getAge() >= 18); // Predicate


실전 활용


// 정렬
users.sort(Comparator.comparing(User::getAge));
users.sort(Comparator.comparing(User::getName).reversed());

// Optional과 함께
Optional.ofNullable(user)
.map(User::getName)
.filter(name -> !name.isEmpty())
.orElse("Unknown");

// 그룹핑
Map<Integer, List<User>> byAge = users.stream()
.collect(Collectors.groupingBy(User::getAge));