관리 메뉴

개발그래머

[자바스터디 15주차] 람다식 본문

Java

[자바스터디 15주차] 람다식

임요환 2023. 7. 24. 16:39

목표

  • 자바의 람다식에 대해 학습하세요

학습할 것 (필수)

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

람다식 사용법

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// (parameter) -> expression
numbers.forEach(number -> System.out.println(number));
  • parameter : 람다 함수의 매개변수 목록을 정의함
  • -> : 람다식의 구분자
  • expression : 람다 함수의 본문

람다식

  • Java8에 도입된 람다식은 함수형 프로그래밍 개념을 Java에 도입한 중요한 기능임
  • 익명 함수를 만들어 코드를 더 간결하고 유연하게 작성할 수 있게 해 주며 컬렉션 처리, 쓰레드 처리, GUI 이벤트 처리 등 다양한 상황에서 사용됨
  • 장점
    • 코드 간결성: 불필요한 코드의 중복을 줄이고 가독성을 향상시킴
    • 익명 함수 지원: 메서드를 정의하지 않고도 함수의 로직을 전달할 수 있음
    • 병렬 처리 간소화: 병렬 처리를 더 쉽게 수행할 수 있도록 도와줌

람다가 나온 이유

  • Java의 기존 버전에서는 익명 내부 클래스를 사용해 함수를 전달할 수 있었지만, 코드가 불필요하게 복잡하고 길어지는 문제가 있었음
  • 멀티코어 CPU의 보편화로 인해 병렬 처리가 중요해지면서 더 유연하고 간결한 방식으로 함수를 전달하고 다루는 것이 필요해짐

함수형 인터페이스

public class FunctionalInterfaceExample {
    public static void main(String[] args) {
        binaryOperatorExample();

        myFunctionExample(6, 3);
    }

    private static void binaryOperatorExample() {
        // java.util.function에 정의된 함수형 인터페이스 사용
        BinaryOperator<Integer> add = (a, b) -> a + b;
        int result = add.apply(3, 5);
        System.out.println(result);
    }

    private static void myFunctionExample(int x, int y) {
        // 커스텀 함수형 인터페이스 사용
        MyFunction add = (a, b) -> a + b;
        MyFunction minus = (a, b) -> a - b;

        int result1 = add.calculate(x, y);
        int result2 = minus.calculate(x, y);

        System.out.println("x + y : " + result1);
        System.out.println("x - y " + result2);
    }
}

// 커스텀 함수형 인터페이스 정의
@FunctionalInterface
public interface MyFunction {
    int calculate(int a, int b);
}
  • 하나의 추상 메서드만을 가지고 있는 인터페이스
  • 람다식은 이 추상 메서드를 구현하는 코드임
  • java.util.fucntion 패키지에 다양한 함수 인터페이스들이 정의되어 있음

@FunctionalInterface

  • Java8부터 도입된 애노테이션
  • 함수형 인터페이스를 선언할 때 사용하는 메타애노테이션
  • 이 애노테이션을 사용하면 컴파일러가 해당 인터페이스가 함수형 인터페이스임을 검사하고, 하나의 추상 메서드만을 가지고 있는지 확인하게 됨
  • 검증: 이 애노테이션을 사용하면 인터페이스가 함수형 인터페이스의 조건을 만족하는지 컴파일러가 검증함
  • 문서화: @FunctionalInterface 애노테이션은 코드의 문서화에도 도움을 주며 해당 인터페이스가 함수형 인터페이스임을 명시적으로 표현 가능

주요 함수형 인터페이스

List<String> names = List.of("Lim", "Yo", "Hwan");
Consumer<String> printName = (name) -> System.out.println(name); // name -> System.out.println(name)
names.forEach(printName);
  • Consumer<T>: T 타입의 인자를 받아서 처리하는 함수 인터페이스
Supplier<String> getCurrentTime = () -> java.time.LocalTime.now().toString();
String currentTime = getCurrentTime.get();
System.out.println("현재 시간 : " + currentTime);
  • Supplier<T>: 값을 반환하지만 인자를 받지 않는 함수 인터페이스
Function<String, Integer> parseToInt = (s) -> Integer.parseInt(s); // s -> Integer.parseInt(s), Integer::parseInt
int result = parseToInt.apply("42");
System.out.println("Parsed integer: " + result);
  • Function<T, R>: T 타입을 받아서 R 타입을 반환하는 함수 인터페이스
Predicate<Integer> isGreaterThanZero = (i) -> i > 0;
boolean result1 = isGreaterThanZero.test(10);
boolean result2 = isGreaterThanZero.test(-1);
System.out.println("10은 0보다 큰가? " + result1);
System.out.println("-1은 0보다 큰가? " + result2);
  • Predicate<T>: T 타입의 인자를 받아서 boolean 값을 반환하는 함수 인터페이스

Variable Capture(변수 포획)

람다 표현식은 외부 범위에 있는 변수를 캡처할 수 있습니다. 
람다 표현식 내부에서 캡처된 변수의 값을 변경할 수 없지만, 값을 읽거나 사용할 수 있습니다. 
이를 람다 표현식이 해당 변수를 "포획"한다고 표현합니다.
  • 람다 표현식에서 외부 범위에 있는 변수를 사용하는 것
  • 람다 표현식의 스코프와 람다 표현식이 변수를 어떻게 캡처하는지에 대한 개념을 알아야 함
  • Java8 이전에는 익명 내부 클래스에서 외부 변수를 사용할 때 해당 변수는 명시적으로 final로 선언되어야 했음 -> 해당 변수가 람다나 익명 클래스 내부에서 변경되지 않음을 보장하기 위함
  • Java8부터는 람다 표현식에서 변수를 캡처할 때 더 유연한 방식을 제공하며 람다 표현식을 사용할 때 코드의 가독성과 유연성을 향상시킴
  • 변수 캡처에는 을 캡처하는 것과 참조를 캡처하는 것 두 가지 유형이 있을 수 있으며 람다 표현식의 내부 구현 방식에 따라 달라짐
  • 람다 표현식에서 변수를 캡처하는 것은 매우 강력한 기능이며 함수형 프로그래밍 패러다임을 구현하거나 다양한 컨텍스트에서 유용하게 활용 가능함

람다 표현식에서 변수를 캡처하는 방법

// Effective final 변수 캡처
int num1 = 10; // final이 붙어있지않지만 람다식 안에서 사실상 final로 취급됨
Consumer<Integer> effectiveFinalConsumer = (n) -> {
    // num1은 effective final로 취급됨 (값 변경하지 않음)
    // num1 += n; 컴파일 에러 발생
    System.out.println("Effective final consumer: " + (num1 + n));
};
effectiveFinalConsumer.accept(5);
  • Effective final 변수: 람다 표현식 내부에서 값이 변경되지 않는 변수를 캡처하는 것, 이 변수는 명시적으로 final로 선언되지 않아도 상수로 취급됨

메소드, 생성자 레퍼런스

  • Java8 이후에 도입된 편리한 기능이며 람다 표현식을 간단하게 표현할 수 있는 방법을 제공하고 특정 메소드나 생성자를 람다 대신 사용 가능함
  • 코드의 가독성을 향상시키고 코드를 더 간결하게 작성 가능하도록 함
  • 컬렉션과 같은 자주 사용되는 구조에서 레퍼런스를 적극적으로 활용하면 좋음

메소드 레퍼런스

public static void main(String[] args) {
    String[] names = {"lim", "yo", "hwan"};

    // 기본적으로 Comparator 사용하는 방법
    Arrays.sort(names, new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return o1.compareTo(o2);
        }
    });

    // 람다식
    Arrays.sort(names, (o1, o2) -> o1.compareTo(o2));

    // 메서드 레퍼런스
    // (this, o2) -> this.compareTo(o2) 이러한 형식으로 람다식이 매핑이 되었다고 생각하면 됨
    Arrays.sort(names, String::compareTo);
}
  • 람다 표현식으로 단순히 메소드 호출만 하는 경우에 사용
  • :: 연산자를 사용하여 표현하고 메소드를 직접 호출하는 대신 람다 표현식 내에서 해당 메소드를 참조하는 방식을 사용함

생성자 레퍼런스

public static void main(String[] args) {
    List<String> names = List.of("Lim", "Yo", "Hwan");
    // 람다식
    names.forEach(name -> new User(name));

    // 생성자 레퍼런스
    names.forEach(User::new);
}
static class User {
    String name;

    public User(String name) {
        this.name = name;
    }
}
  • 객체를 생성하는 생성자를 대신 호출하는 데 사용
  • :: 연산자를 사용하여 생성자를 참조하고 람다 표현식에서 객체 생성을 간단하게 나타냄

백기선님 팁

  • 람다식은 익명내부클래스가 아님 -> 구현 자체가 다름
  • 익명내부클래스는 컴파일 시 뒤에 $가 붙는 클래스 파일이 새로 생김 -> 람다식은 생기지 않음
  • 람다식은 INVOKEDYNAMIC(indy)으로 구현되어 있음
  • 내부적으로 자바의 버전이 올라갈 때 INVOKEDYNAMIC으로 구현되어 있으면 하위호환성을 유지하면서 개선할 수 있는 여지가 있음
  • 익명내부클래스와 다르기 때문에 scope에서 오는 차이도 가지고 있음
  • 자바 API에서 제공하는 함수형 인터페이스
    • Function = 트랜스포머(변신 로봇) = 값을 변환하기 때문에 = 가장 범용적으로 사용되는 것
    • Consumer = 스파르탄(Spartan) = 모든 걸 빼앗고 아무것도 내주지 마라 = void 리턴
    • Predicate = 판사 = 참과 거짓으로 판단하기 때문에 = boolean 리턴
    • Suppliers = 게으른 공급자 = 입력값이 존재하지 않는데 내가 원하는 것을 미리 준비하기 때문에 = 파라미터가 없음
  • 메서드 레퍼런스
public class MethodReferenceExample {
    public static void main(String[] args) {
        String[] names = {"lim", "yo", "hwan"};

        // 기본적으로 Comparator 사용하는 방법
        Arrays.sort(names, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.compareTo(o2);
            }
        });

        // 람다식
        Arrays.sort(names, (o1, o2) -> o1.compareTo(o2));

        // 메서드 레퍼런스 
        // (this, o2) -> this.compareTo(o2) 이러한 형식으로 람다식이 매핑이 되었다고 생각하면 됨
        Arrays.sort(names, String::compareTo);
    }
}
  • 쉐도잉 = 기존변수가 익명내부클래스에서 가려지게 됨 하지만 람다는 scope이 같기 때문에 기존변수를 또 선언할 수 없음
public class ShadowingExample {
    public static void main(String[] args) {
        ShadowingExample shadowingExample = new ShadowingExample();
        shadowingExample.testShadowing();
    }
    private void testShadowing() {
        int baseNumber = 10;

        // 1. 로컬 클래스
        // testShadowing 내의 새로운 Scope임
        class LocalClass {
            void printBaseNumber() {
                int baseNumber = 11;
                System.out.println(baseNumber);
            }
        }

        // 2. 익명 클래스
        // testShadowing 내의 새로운 Scope임
        Consumer<Integer> consumer = new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                int baseNumber = 11;
                System.out.println(baseNumber);
            }
        };

        // 3. 람다
        // 람다는 scope이 같으므로 같은 이름의 변수를 선언할 수 없음
        IntConsumer intConsumer = (i) -> {
//            int baseNumber = 11; // Variable 'baseNumber' is already defined in the scope
            System.out.println(i + baseNumber);
        };

        intConsumer.accept(10);
    }

}
  • 람다식의 타입과 형변환
  • Java8에서 람다식을 지원하게 된 계기는 함수형 프로그래밍이 가능하도록 하기 위해서였음
  • 함수자체가 first class(일급객체)가 되어야 함 -> 무언가가 first class라는 것은 변수에 할당할 수 있고 파라미터로 메서드에 전달할 수 있고 return으로 받을 수 있어야 함
  • Java8 부터 함수형프로그래밍이 가능하다는 것은 함수를 변수에 선언하거나 함수를 메서드에 전달하거나 함수를 리턴받을 수 있음
  • 지연연산 = 람다를 써서 비용이 비싼 연산이나 비용이 비싼 객체 생성 작업을 뒤쪽으로 미룰 수 있음, 자바스크립트의 콜백처럼 쓰는 느낌이 있음, 필요한 순간에 function을 호출함
  • 자바8 병렬처리, 스트림과 람다는 관련되어 있음
  • LambdaMethodFactory를 활용하여 Reflection 대체하기