자바의 람다식과 함수형 프로그래밍
1. 함수형 프로그래밍이란?
1.1 함수형 프로그래밍의 정의
- 함수형 프로그래밍(Functional Programming): 순수 함수를 조합하고 공유 상태, 변경 가능한 데이터 및 부작용을 피하여 소프트웨어를 구축하는 프로그래밍 패러다임
- 명령형 프로그래밍(무엇을 어떻게 할지)과 대조적으로 선언형 프로그래밍(무엇을 할지)에 중점
1.2 함수형 프로그래밍의 핵심 개념
- 순수 함수(Pure Functions): 동일한 입력에 항상 동일한 출력을 반환하며, 부작용이 없는 함수
- 불변성(Immutability): 한번 생성된 데이터는 변경되지 않음
- 일급 객체로서의 함수(First-Class Functions): 함수를 변수에 할당하거나 다른 함수에 전달할 수 있음
- 고차 함수(Higher-Order Functions): 함수를 인자로 받거나 함수를 반환하는 함수
1.3 함수형 프로그래밍의 장점
- 코드의 가독성과 유지보수성 향상
- 병렬 처리 용이
- 테스트 및 디버깅 용이
- 버그 감소
2. 자바와 함수형 프로그래밍
2.1 Java 8 이전의 한계
- 객체 지향 언어로 설계됨
- 익명 내부 클래스를 사용한 간접적인 방식으로만 함수형 스타일 구현 가능
- 코드가 장황하고 가독성이 떨어짐
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
2.2 Java 8의 함수형 프로그래밍 지원
- 람다식(Lambda Expressions)
- 함수형 인터페이스(Functional Interfaces)
- 스트림 API(Stream API)
- 메소드 참조(Method References)
- 디폴트 메소드(Default Methods)
3. 람다식(Lambda Expressions)
3.1 람다식의 정의
- 익명 함수(메소드)를 간결하게 표현하는 방식
- 함수형 프로그래밍을 자바에서 구현하기 위한 핵심 기능
3.2 람다식의 기본 문법
Runnable r = () -> { System.out.println("Hello World"); };
Consumer<String> c = s -> { System.out.println(s); };
Consumer<String> c = s -> System.out.println(s);
Comparator<String> comp = (s1, s2) -> s1.compareTo(s2);
3.3 타입 추론
- 컴파일러가 문맥을 통해 람다식의 매개변수 타입을 추론
- 명시적인 타입 선언 생략 가능
Comparator<String> comp = (String s1, String s2) -> s1.compareTo(s2);
Comparator<String> comp = (s1, s2) -> s1.compareTo(s2);
4. 함수형 인터페이스(Functional Interfaces)
4.1 함수형 인터페이스의 정의
- 단 하나의 추상 메소드만을 가진 인터페이스
@FunctionalInterface 어노테이션으로 명시 (선택적)
- 람다식은 함수형 인터페이스의 구현체로 사용됨
@FunctionalInterface
interface MyFunction {
void apply(String s);
}
4.2 자바 표준 함수형 인터페이스
java.util.function 패키지에 정의된 표준 함수형 인터페이스들
| 인터페이스 | 메소드 | 설명 |
|---|
| Function<T,R> | R apply(T t) | T 타입을 받아 R 타입을 반환 |
| Predicate | boolean test(T t) | T 타입을 받아 boolean 반환 |
| Consumer | void accept(T t) | T 타입을 받아 소비만 함 (반환 없음) |
| Supplier | T get() | 인자 없이 T 타입 값 제공 |
| BiFunction<T,U,R> | R apply(T t, U u) | 두 인자를 받아 결과 반환 |
Function<String, Integer> strLength = s -> s.length();
Predicate<String> isEmpty = s -> s.isEmpty();
Consumer<String> printer = s -> System.out.println(s);
Supplier<String> getString = () -> "Hello";
5. 람다식의 활용
5.1 컬렉션 처리
List<String> names = Arrays.asList("John", "Jane", "Adam", "Mary");
Collections.sort(names, (a, b) -> a.compareTo(b));
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.toList());
5.2 이벤트 처리
button.addActionListener(e -> System.out.println("Button clicked"));
5.3 스트림 API와의 조합
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.reduce(0, Integer::sum);
6. 메소드 참조(Method References)
6.1 메소드 참조의 개념
- 기존에 정의된 메소드를 람다식 대신 사용하는 방법
- 코드를 더 간결하게 만들어줌
6.2 메소드 참조의 유형
Function<String, Integer> parser = Integer::parseInt;
String str = "Hello";
Supplier<Integer> lengthSupplier = str::length;
Function<String, Integer> lengthFunc = String::length;
Supplier<ArrayList<String>> listSupplier = ArrayList::new;
7. 실용적인 예제
7.1 사용자 필터링 예제
class User {
private String name;
private int age;
}
List<User> users = Arrays.asList(
new User("Alice", 25),
new User("Bob", 30),
new User("Charlie", 22)
);
List<String> adultNames = users.stream()
.filter(user -> user.getAge() >= 25)
.map(User::getName)
.collect(Collectors.toList());
7.2 맵-리듀스 패턴 예제
List<String> words = Arrays.asList("Java", "Lambda", "Functional", "Programming");
String longest = words.stream()
.reduce("", (word1, word2) ->
word1.length() > word2.length() ? word1 : word2);
8. 람다식의 제약사항 및 주의점
8.1 변수 캡처(Variable Capture)
- 람다식에서 외부 변수 참조 시 해당 변수는 effectively final이어야 함
- 로컬 변수는 변경 불가, 인스턴스/정적 변수는 변경 가능
int num = 10;
Runnable r = () -> System.out.println(num);
8.2 this 키워드
- 람다식에서 this는 람다를 포함하는 클래스를 가리킴 (익명 클래스와 다름)
public class ThisExample {
private int value = 10;
public void doSomething() {
Runnable anonymous = new Runnable() {
@Override
public void run() {
System.out.println(this);
}
};
Runnable lambda = () -> System.out.println(this);
}
}
9. 함수형 프로그래밍과 객체 지향 프로그래밍의 조화
9.1 두 패러다임의 조화
- 객체 지향: 데이터와 그 동작을 캡슐화
- 함수형: 데이터 변환에 초점
- 자바에서는 두 패러다임을 혼합하여 사용 가능
9.2 실용적인 접근법
- 상태 관리가 필요한 부분: 객체 지향 접근법
- 데이터 처리 로직: 함수형 접근법
10. 결론
10.1 람다식의 의의
- 자바 언어의 중요한 발전
- 코드 간결성 및 가독성 향상
- 병렬 처리 용이성
10.2 함수형 프로그래밍의 미래
- 반응형 프로그래밍(Reactive Programming)과의 연계
- 자바의 함수형 기능 지속적 확장 (Java 9 이후의 개선사항)
10.3 권장 사항
- 명료성을 위해 적절한 곳에 람다식 사용
- 복잡한 로직은 별도 메소드로 분리
- 표준 함수형 인터페이스 우선 사용