티스토리 뷰

728x90
반응형

람다 표현식


💡 람다란 무엇인가?

  •  람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있습니다. 람다 표현식은 이름은 없지만 매개변수,
    구현부, 반환 타입, 예외 종류를 가질 수 있습니다.

 

특징 

  • 익명 - 보통의 메서드와 달리 이름이 없으므로 익명이라 표현합니다. 이름이 없으므로 네이밍 걱정이 줄어들 수 있습니다.
  • 함수 - 람다는 메서드처럼 클래스에 종속되지 않으므로 함수라고 부릅니다. 
  • 전달 - 람다 표현식을 메서드 인수로 전달하거나 변수에 저장할 수 있습니다.
  • 간결성 - 익명 클래스처럼 구구절절하게 코드를 구현할 필요가 없습니다.

 

💡 예제 

  • 아래 예제코드를 보면 익명 클래스를 사용하면 구구절절한 코드가 있지만 람다를 사용하게 되면 간결한 방식으로 처리할 수 있습니다.
public static void main(String[] args) {

    List<String> stringList = new ArrayList<>(List.of("Hello", "Hi", "Bye"));
        
    // 익명 클래스를 사용하여 정렬
    stringList.sort(new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return Integer.compare(o1.length(), o2.length());
        }
    });

    // 람다 표현식을 사용하여 정렬
    stringList.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));
        
    // 람다 및 메서드 참조를 사용하여 정렬
    stringList.sort(Comparator.comparing(String::length));
}

 

💡 람다 표현식의 구성

stringList.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));

// (s1, s2) : 람다 파라미터
// -> : 파라미터와 바디를 구분시켜주는 화살표
// Integer.compare(s1.length(), s2.length()) : 람다 바디

 

💡 람다 표현식의 문법

  • () -> {  } : 파라미터가 없으면 void를 반환하는 람다 표현식입니다.
  • () -> "KDG" : 파라미터가 없으며 문자열을 반환하는 표현식입니다.
  • () -> { return "KDG" } : 파라미터가 없으며 명시적으로 return문을 사용해서 문자열을 반환하는 표현식입니다.

 

💡 어디에, 어떻게 람다를 사용할까?

  • 람다는 함수형 인터페이스라는 문맥에서 사용할 수 있습니다. 함수형 인터페이스란 오직 하나의 추상 메서드만을 가지는 인터페이스를 의미합니다.
  • java.util.function 패키지에는 자바에서 제공하는 함수형 인터페이스가 정의되어 있습니다. 또한 @FunctionalInterace 어노테이션은 해당 클래스가 함수형 인터페이스임을 가리키고 있으며 실제로 함수형 인터페이스가 아닌 경우 컴파일 에러가 발생하게 됩니다.
@FunctionalInterface
public interface Comparator<T> {
 
    int compare(T o1, T o2);
}

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}

 

이렇게 함수형 인터페이스에서 추상 메서드의 구현을 람다 표현식으로 표현한 후 전달을 할 수 있습니다. 그래서 람다 표현식 자체를 인터페이스의 구현체로 취급하고 전달할 수 있습니다.

💡 함수 디스크립터

  • 함수 디스크립터란? 람다 표현식의 시그니처를 서술하는 메서드를 일컫습니다. 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 말합니다.
  • 람다 표현식은 변수에 할당하거나, 함수형 인터페이스를 인수로 받는 메서드로 전달할 수 있으며 함수형 인터페이스의 추상 메서드와 같은 시그니처를 갖습니다.
  • 한 개의 void 메서드 호출은 중괄호를 사용할 필요가 없습니다. 자바 언어 명세에서는 void를 반환하는 메서드 호출과 관련한 특별한 규칙을 정하고 있기 때문입니다.
public static void main(String[] args) {
    
    process(() -> System.out.println("Lambda Expression"));
    process(() -> { System.out.println("Lambda Expression"); });
    process(() -> {
        System.out.println("Lambda Expression !");
        System.out.println("Lambda Expression !!");
    });
}

public static void process(Runnable r) {
    r.run();
}

 

💡 실행 어라운드 패턴

  • 자원 처리에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에 자원을 닫는 순서로 이루어집니다. 이렇게 실제 자원을 처리하는 단계는 여는 단계와 닫는 단계의 중간에 놓이게 되는데 이와 같은 형식의 코드를 실행 어라운드 패턴이라고 합니다.

실행 어라운드 패턴

  • 아래 코드는 try-with-resourcs를 사용하여 자동적으로 close 해주고 있습니다. 그렇기 때문에 실행 어라운드 패턴과 비슷한것을 알 수 있습니다. 지금은 파일의 한줄씩 읽고 있지만 두줄, 세줄씩 읽을려면 각 상황에 따른 메서드를 구현하거나 함수형 인터페이스를 사용할 수 있습니다.
public static String readFile() throws IOException {
    try(BufferedReader br = new BufferedReader(new FileReader("text.txt"))) {
        return br.readLine(); // 작업 코드
    }
}

 

함수형 인터페이스를 사용하여 해결

@FunctionalInterface
public interface BufferedReaderProcessor {

    String process(BufferedReader br) throws IOException;
}

public static String readFile(BufferedReaderProcessor b) throws IOException {
    try(BufferedReader br = new BufferedReader(new FileReader("text.txt"))) {
        return b.process(br);
    }
}

// 사용
readFile((BufferedReader br) -> br.readLine() + br.readLine());

 

💡 함수형 인터페이스 사용

  • 위에서 언급한거처럼 함수형 인터페이스란 오직 하나의 추상 메서드를 가지는 인터페이스입니다. 함수형 인터페이스의 추상 메서드는 람다 표현식의 시그니처를 묘사하며, 함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라 합니다.

 

Predicate<T>

  • java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 boolean을 반환합니다.
  • Predicate<T> 자세히 보기

 

Consumer<T>

  • java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의하고 있습니다. T 객체를 받아 소비의 목적으로 사용하는거 같습니다.
  • Consumer<T> 자세히 보기

 

Function<T, R>

  • java.util.function.Frunction<T, R> 인터페이스는 제네릭 형식의 T를 인수로 받아 제네릭 형식 R 객체를 반환하는 apply라는 추상 메서드를 정의하고 있습니다.
  • Function<T, R> 자세히 보기

 

기본형 특화 함수형 인터페이스

  • 자바에서는 기본형을 참조형으로 변환하는 기능을 제공하고 있습니다. 이 기능을 박싱이라 하며, 참조형을 기본형으로 반환하는 동작을 언박싱이라 합니다.
  • 박싱<->언박싱의 변환 과정은 비용이 소모되는데, 박싱한 값은 기본형을 감싸는 래퍼 클래스이며 힙 영역에 저장이 되고, 그렇기 때문에 박싱된 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 발생하게 됩니다.

  • 그렇기 때문에 자바 8에서는 기본형을 입출력으로 사용하는 상황에서는 오토박싱의 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공하고 있습니다. (IntPredicate, LongPredicate 등)
  • IntPredicate 자세히 보기

 

💡 형식 검사, 형식 추론, 제약

  • 람다 표현식은 자신이 어떤 함수형 인터페이스를 구현하였는지에 대한 정보를 명시적으로 노출하지 않습니다. 
  • 람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있습니다.

 

형식 검사

  • 람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있습니다. 어떤 콘텍스트(람다가 전달될 메서드 파라미터나 람다가 할당되는 변수)에서 기대되는 람다 표현식의 형식을 대상 형식이라 합니다.
  • 대상 형식이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있습니다.
  • 추가적을 람다 표현식이 예외를 던질 수 있다면 추상 메서드도 같은 예외를 던질 수 있도록 throws로 선언해야 합니다.
public static void main(String[] args) throws IOException {

    List<Apple> appleList = List.of(new Apple(Color.RED, 250), new Apple(Color.GREEN, 200));
    List<Apple> redAppleList = filter(appleList, (Apple apple) -> apple.getColor() == Color.RED);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();

    for (T t : list) {
        if (p.test(t)) {
            result.add(t);
        }
    }
    return result;
}

// 1. filter 메서드를 확인합니다.
// 2. filter 메서드의 두번째 인자는 Predicate<T>이며 T는 Apple로 대치됩니다.
// 3. Predicate는 boolean을 반환하는 함수형 인터페이스입니다.
// 4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사합니다.

 

형식 추론

  • 자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해 람다 표현식과 관련된 함수형 인터페이스를 추론합니다 . 즉 대상 형식을 사용해 함수 디스크립터를 알 수 있으므로 컴파일러는 람다의 시그니처도 추론할 수 있습니다. 
  • 결과적으로 컴파일러는 람다 표현식의 파라미터에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있습니다.
// 형식 추론을 하지 않음
Comparator<Apple> appleList = (Apple a1, Apple a2) -> Integer.compare(a1.getWeight(), a2.getWeight());

// 형식 추론 사용
Comparator<Apple> appleList = (a1, a2) -> Integer.compare(a1.getWeight(), a2.getWeight());

 

지역 변수 사용

  • 람다 표현식에서는 익명 함수가 하는 것처럼 자유변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있습니다.
  • 이와 같은 동작을 람다 캡처링이라 합니다.
  • 하지만 제약 사항이 존재합니다.
  • 제약 사항 - 지역 변수는 명시적으로 final로 선언되어 있거나, final로 선언된 변수와 똑같이 사용되어야 합니다. 즉 불변성을 보장

 

자유 변수에 불변성이 보장되어야 하는 이유

  • 내부적으로 인스턴스 변수와 지역 변수는 애초에 다름니다. 인스턴스는 힙영역에 저장되지만 지역 변수는 스택 영역에 저장됩니다.
  • 람다가 참조한 값이 다른 스레드에 의해 값이 변경되는 경우 실행되던 기존 람다에는 변경된 값을 반영할 방법이 없고, 예기치 않는 결과를 도출할 수 있습니다.
  • 결국 동시성 문제가 발생할 수 있습니다.
  • 이러한 문제가 있기 때문에 람다도 원래의 변수에 접근하는게 아닌 자유 지역 변수의 복사본을 떠서 사용합니다. 결국 복사본의 값이 변경되지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생깁니다.
  • https://cobbybb.tistory.com/19
  • https://docs.oracle.com/javase/specs/jls/se10/html/jls-15.html#jls-15.27.2

 

💡 메서드 참조

  • 메서드 참조를 사용하면 가독성을 높일 수 있습니다.
List<String> stringList = new ArrayList<>(List.of("Hello", "Hi", "Bye"));

// 람다 표현식을 사용하여 정렬
stringList.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));

// 람다 및 메서드 참조를 사용하여 정렬
stringList.sort(Comparator.comparing(String::length));

 

💡 생성자 참조

@Getter
@AllArgsConstructor
public class AppleDTO {

    private Color color;
    private int weight;
}

@Getter
@AllArgsConstructor
public class Apple {

    private Color color;
    private int weight;

    public Apple(AppleDTO dto) { // DTO 매개변수로 생성자 생성
        this.color = dto.getColor();
        this.weight = dto.getWeight();
    }
}

public static void main(String[] args) {

    AppleDTO appleDTO = new AppleDTO(Color.GREEN, 150);
    
    // 형식 추론 사용
    Function<AppleDTO, Apple> appleFunctionV1 = Apple::new;
    
    // 형식 추론 사용하지 않음
    Function<AppleDTO, Apple> appleFunctionV2 = (AppleDTO dto) -> new Apple(dto);
}

 

💡 람다 표현식의 단점 - 이펙티브 자바(아이템 42)

  • 람다는 이름이 없고 문서화도 할 수 없기 때문에 코드 자체로 동작이 명확하지 않고 모호하다면 람다식을 사용해서는 안됩니다. 즉 람다의 장점인 간결함을 유지하지 못한다면 수수깨끼가 되므로 주의가 필요합니다.
  • 람다에서는 this 키워드는 바깥 인스턴스를 가리킵니다. 반명 익명 클래스에서의 this는 자기자신을 가리킵니다. 그래서 함수 객체가 자기 자신을 참조해야 한다면 반드시 익명 클래스를 사용해야 합니다.

 

✔️ 정리

  • 람다 표현식은 익명 함수의 일종입니다. 이름은 없지만 매개변수, 구현부, 반환 형식을 가지며 필요시 예외를 던질 수 있습니다.
  • 람다 표현식으로 인해 간결한 코드를 작성할 수 있습니다.
  • 함수형 인터페이스란 오직 하나만의 추상 메서드를 가지는 인터페이스입니다.
  • 람다 표현식을 이용해서 함수형 인터페이스의 추상 메서드를 즉석으로 구현할 수 있으며, 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급됩니다.

 

 

 

 

 

 

728x90
반응형