유랑하는 나그네의 갱생 기록

だけど素敵な明日を願っている -HANABI, Mr.children-

Study/Java

[Java] Lambda 훑어보기

Madirony 2024. 8. 25. 22:33
728x90
Lambda

Lambda Expressions?

 람다 표현식은 Java 8에서 도입되었습니다. 함수형 프로그래밍 스타일을 Java에 도입하면서 코드를 간결, 명확하게 작성할 수 있도록 한 것이죠. 익명 함수(anonymous function)를 정의하는 방법으로 하나의 메서드를 간단하게 표현할 수 있습니다.
 
 한 가지 예시를 들어 보여드리겠습니다.
 

// 기존 익명 클래스 사용 방식
Comparator<Integer> comparator = new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
};

 기존에는 익명 클래스를 사용하여 Comparator 인터페이스를 구현했지만, 람다식을 활용하면 다음과 같이 간결한 코드가 나오게 됩니다.
 

// 람다 표현식 사용 방식
Comparator<Integer> comparatorLambda = (o1, o2) -> o1.compareTo(o2);

 


람다 표현식 구조와 활용 예시

 람다 표현식의 기본 구조는 다음과 같습니다.

(parameters) -> { statements; }

 

// e.g.
(int num) -> { System.out.println(num); }

코드를 좀 더 간결하게 줄여볼까요?
 

(num) -> { System.out.println(num); }

런타임 시, 매개변수의 자료형을 자동 인식하므로 타입 생략이 가능합니다.
 

num -> System.out.println(num);

심지어 매개변수가 1개면 소괄호()를, 단일 실행문이면 중괄호{}도 생략 가능합니다.
 

Runnable runnable = () -> System.out.println("gd");

매개변수가 없다면? 반드시 빈괄호를 명시해주어야 합니다.
 

(a, b) -> a + b

 리턴 값만 있다면 return을 생략해도 됩니다.
 

List<String> list = Arrays.asList("apple", "banana", "orange");

// 람다 표현식을 사용한 정렬
list.sort((s1, s2) -> s1.compareTo(s2));

이런 식으로도 람다를 활용할 수 있어 컬렉션 객체를 다루기도 훨씬 수월합니다.
이게 끝이 아닙니다. 메서드 참조(Method References) 방식을 사용한다면 코드를 더 줄일 수 있습니다.
 

List<String> list = Arrays.asList("apple", "banana", "orange");

// 기존 람다 표현식
list.forEach(s -> System.out.println(s));

// 메서드 참조 사용
list.forEach(System.out::println);

메서드 참조(method references)는 람다 표현식이 단 하나의 메서드만을 호출하는 경우, 해당 람다 표현식에서 불필요한 매개변수를 제거하고 사용할 수 있도록 합니다.
 


유의사항 :: @FunctionalInterface

@FunctionalInterface

 람다식이 할당되는 인터페이스를 람다식의 타겟 타입이라고 합니다. 유의할 점은 "타겟 타입의 abstract 메서드는 하나만 있어야 한다." 입니다. abstract 메서드가 2개 이상이면 익명 내부 클래스를 사용해야 합니다. 그래서 저 @FunctionalInterface 어노테이션이 있는 겁니다. 컴파일러가 하나의 abstract method만 있음을 확인하도록 합니다.
 

유의사항 :: this

 람다식은 내부에서 this 키워드를 쓴다면? 외부 클래스의 인스턴스를 참조합니다. 외부 클래스의 멤버 변수는 접근제한자의 제약 없이 사용할 수 있습니다. 하지만 외부 클래스의 로컬 변수는 오로지 읽기만 가능합니다.
 


함수형 인터페이스(Functional Interfaces)

 자 그러면 람다 표현식을 사용할 때 함수형 인터페이스가 필요하다고 얘기했었습니다. 그러면 기본적으로 제공되는 인터페이스 몇 가지만 알아볼까요?

 

Predicate<T>: 매개변수를 받아 boolean을 반환

// e.g.
Predicate<String> isEmpty = (String s) -> s.isEmpty();
        System.out.println(isEmpty.test(""));

 
Function<T, R>: 매개변수를 받아 다른 타입의 결과를 반환
Consumer<T>: 매개변수를 받아 사용하고, 반환값 없음
Supplier<T>: 매개변수를 받지 않고, 결과를 반환
BiFunction<T, U, R>: 두 개의 매개변수를 받아 연산을 수행하고, 결과를 반환
 
Runnable : 매개변수도 반환값도 없는 함수형 인터페이스, 주로 별도의 스레드에서 실행될 코드를 정의할 때 사용

 

Runnable Interface

 

// e.g.

Runnable runnable = () -> System.out.println("gd");

//Thread 생성 및 시작
Thread thread = new Thread(runnable);
thread.start();

 


장단점?

 람다를 쓰면? 코드가 간결해지고, 가독성도 좋아지고.. 함수형 프로그래밍 스타일을 Java에서 사용할 수 있다는 점을 장점이라 말할 수 있겠습니다. 심지어 스트림과 함께 활용하면 코드의 중복도 줄이고 병렬 처리 역시 쉽게 구현할 수 있습니다.
 

// e.g.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 람다 표현식을 사용하여 이름의 길이가 3인 이름을 필터링
names.stream()
     .filter(name -> name.length() == 3)
     .forEach(System.out::println);

 
 
 하지만 람다 표현식은 코드가 간결한 만큼 디버깅이 어려울 수도 있습니다. 예를 들어, 람다 표현식 내에서 발생하는 예외는 스택 트레이스에서 익명 함수의 위치나 원인을 명확히 파악하기 어려운 경우가 있습니다. 특히, 람다 표현식은 디버거에서 명확하게 식별되지 않는 경우도 있어, 문제가 발생한 위치를 찾는 데 시간이 더 걸릴 수 있습니다.
 
 무작정 좋다고 사용하는 것보다는 뭐든 적재적소에 사용하는 것이 좋은 것 같습니다. 다음 글에서는 스트림을 알아보겠습니다.

728x90