Coding 공부/Java

[Java] lambda, 내장형 함수 인터페이스(Consumer, Supplier, Function, Operator), 람다식에서 지역 변수, 메소드 참조(Method Reference)

CBJH 2024. 3. 27. 14:38
728x90
반응형

1. 람다(lambda)

 

  • 함수를 즉시성 있고 간결하게 표현하는 방법이다.
  • 함수 이름이 없으므로 다시 호출해서 사용할 수 없다.

  1.1 람다식의 기본 구조

람다식은 (매개변수) -> { 실행 코드 }의 형식을 가집니다. 매개변수의 타입을 명시적으로 적어주지 않아도 되며, 실행 코드가 한 줄일 경우 중괄호 {}도 생략할 수 있습니다.

  • 기본 형태: (parameters) -> expression
  • 또는 (parameters) -> { statements; }

  1.2 람다식의 장점

  1. 코드의 간결성: 불필요한 코드를 줄여주어, 더 읽기 쉽고 유지보수하기 좋은 코드를 만들 수 있습니다.
  2. 함수형 프로그래밍의 접근: 불변성(Immutability), 부작용(Side-effects) 최소화 등 함수형 프로그래밍의 여러 장점을 활용할 수 있습니다.
  3. 병렬 처리 용이: 자바 8부터는 스트림 API와 람다식을 함께 사용하여 컬렉션을 매우 효율적으로 병렬 처리할 수 있습니다.

  1.3 람다식 사용 시 주의점

  • 가독성 저하: 너무 복잡하거나 긴 람다식은 오히려 가독성을 떨어뜨릴 수 있습니다. 이런 경우 별도의 메소드로 분리하는 것이 좋습니다.
  • this와 super의 의미: 람다식 내부에서 this 키워드는 람다식을 실행하는 인스턴스를 가리킵니다. 람다식은 익명 클래스의 인스턴스로 간주되지 않기 때문입니다.
  • 직렬화에 주의: 람다식은 직렬화할 수 있지만, 직렬화와 관련된 다양한 문제로 인해 가능한 한 피하는 것이 좋습니다.

 

람다식 쓰기 전
람다식으로 표현

 

  1.4 람다식의 인터페이스

자바 8에서는 람다식과 함께 사용되는 여러 가지 내장 함수형 인터페이스를 제공합니다. 이 인터페이스들은 java.util.function 패키지에 포함되어 있으며, 개발자가 흔히 마주치는 함수적 요구 사항을 추상화한 것입니다. 여기서는 그 중에서도 주요하게 사용되는 Consumer, Supplier, Function, Operator에 대해 자세히 설명하겠습니다.

1. Consumer

Consumer 인터페이스는 매개변수를 하나 받고, 반환 값은 없습니다(void 반환). 주로 매개변수로 전달된 객체를 소비하는 데 사용됩니다. 예를 들어, 어떤 객체를 기반으로 출력을 하거나, 계산을 수행하고 그 결과를 외부에 저장하는 것과 같은 작업에 사용할 수 있습니다.

  • 기본 메서드: void accept(T t)
Consumer<String> consumer = x -> System.out.println(x);
consumer.accept("Hello, Consumer!");

2. Supplier

Supplier 인터페이스는 매개변수가 없고, 반환 값이 있습니다. 호출될 때마다 특정 타입의 결과를 반환하는 데 사용됩니다. 객체의 생성, 또는 공급에 주로 사용됩니다.

  • 기본 메서드: T get()
Supplier<Double> randomValue = () -> Math.random();
System.out.println(randomValue.get());

 

3. Function

Function 인터페이스는 하나의 매개변수를 받고, 결과를 반환합니다. 이 인터페이스는 주로 입력을 출력으로 매핑하는 변환 작업에 사용됩니다.

  • 기본 메서드: R apply(T t)
Function<String, Integer> func = x -> x.length();
int length = func.apply("Hello, Function!");
System.out.println(length);

 

 

4. Operator

Operator 인터페이스는 Function 인터페이스의 특수한 형태로, 입력과 출력이 같은 타입일 때 사용됩니다. 주로 동일한 타입의 연산을 수행할 때 사용되며, BinaryOperator와 UnaryOperator의 두 가지로 나뉩니다.

  • BinaryOperator: 두 개의 매개변수를 받아 동일한 타입의 결과를 반환합니다.
    • 기본 메서드: T apply(T t1, T t2)
BinaryOperator<Integer> operator = (a, b) -> a + b;
int result = operator.apply(5, 3);
System.out.println(result);

 

  • UnaryOperator: 한 개의 매개변수를 받아 동일한 타입의 결과를 반환합니다.
    • 기본 메서드: T apply(T t)
UnaryOperator<Integer> square = x -> x * x;
System.out.println(square.apply(5));

 

  • UnaryOperator

UnaryOperator<T>는 Function<T, T>의 자식 인터페이스입니다. 이는 입력과 결과가 같은 타입일 때 사용됩니다. 단 하나의 매개변수를 받아 같은 타입의 값을 반환합니다.

주요 메서드

  • apply(T t): 주어진 인자에 대해 연산을 수행하고 결과를 반환합니다.

예시

    public static void main(String[] args) {
        UnaryOperator<Integer> squareOperator = x -> x * x;
        System.out.println(squareOperator.apply(5)); // 출력: 25
    }

이 예제에서 UnaryOperator는 정수를 받아 그 제곱을 반환합니다.

 

 

  • BinaryOperator

BinaryOperator<T>는 BiFunction<T, T, T>의 자식 인터페이스입니다. 두 개의 매개변수(동일한 타입)를 받아 처리한 후, 결과로 같은 타입의 객체를 반환합니다. 주로 두 객체를 결합하거나 비교하는 연산에 사용됩니다.

주요 메서드

  • apply(T t1, T t2): 두 매개변수에 대해 연산을 수행하고 결과를 반환합니다.

예시

  public static void main(String[] args) {
        BinaryOperator<Integer> sumOperator = (a, b) -> a + b;
        System.out.println(sumOperator.apply(5, 3)); // 출력: 8
    }

이 예제에서 BinaryOperator는 두 정수를 받아 그 합을 반환합니다.

Operator들은 주로 수학적 연산이나 단순 결합, 비교 연산 등에 사용됩니다. UnaryOperator와 BinaryOperator 모두 자바의 함수형 프로그래밍 모델을 간결하고 효율적으로 만드는 데 도움을 줍니다.

 

 

 

  • IntUnaryOperator

단일 정수형 인자를 받아서 연산 후 정수형 결과를 반환하는 연산에 사용됩니다. 이 인터페이스는 주로 람다 표현식이나 메서드 참조와 함께 사용됩니다.

compose

  • compose 메서드는 현재 IntUnaryOperator에 앞서 실행할 IntUnaryOperator를 결합합니다.
  • 즉, f.compose(g)는 먼저 g를 실행하고, 그 결과를 f에 적용합니다.
  • 메서드 시그니처: default IntUnaryOperator compose(IntUnaryOperator before)

andThen

  • andThen 메서드는 현재 IntUnaryOperator에 이어서 실행할 IntUnaryOperator를 결합합니다.
  • 즉, f.andThen(g)는 먼저 f를 실행하고, 그 결과를 g에 적용합니다.
  • 메서드 시그니처: default IntUnaryOperator andThen(IntUnaryOperator after)

IntUnaryOperator 예제

    public static void main(String[] args) {
        IntUnaryOperator operator = x -> x * 2; // 입력된 값에 2를 곱함
        IntUnaryOperator beforeOperator = x -> x + 3; // 입력된 값에 3을 더함
        
        // compose 사용 예: 먼저 beforeOperator를 적용하고, 그 결과에 operator를 적용
        int resultCompose = operator.compose(beforeOperator).applyAsInt(5); // (5 + 3) * 2
        System.out.println("Result of compose: " + resultCompose); // 출력: 16
        
        // andThen 사용 예: 먼저 operator를 적용하고, 그 결과에 beforeOperator를 적용
        int resultAndThen = operator.andThen(beforeOperator).applyAsInt(5); // (5 * 2) + 3
        System.out.println("Result of andThen: " + resultAndThen); // 출력: 13
    }

 

 

  • IntBinaryOperator

IntBinaryOperator 인터페이스 역시 java.util.function 패키지에 정의되어 있으며, 두 개의 정수형 인자를 받아서 연산 후 정수형 결과를 반환하는 연산에 사용됩니다.

IntBinaryOperator 예제

    public static void main(String[] args) {
        IntBinaryOperator operator = (a, b) -> a + b; // 두 입력값을 더함

        int result = operator.applyAsInt(5, 3); // 5 + 3
        System.out.println("Result: " + result); // 출력: 8
    }

이 예제에서는 두 정수를 받아서 그 합을 반환하는 간단한 IntBinaryOperator 람다 표현식을 정의하고 있습니다. IntBinaryOperator는 주로 두 개의 입력값에 대한 연산이 필요할 때 사용됩니다, 예를 들어, 리스트의 요소들을 합산하거나 최대값/최소값을 계산할 때 유용하게 사용될 수 있습니다.

 

  1.5 람다식에서 지역 변수 

 

람다식에서 지역 변수를 사용할 때, 그 변수는 변경되지 않아야 하며, 이를 "effectively final"이라고 합니다. 이는 변수가 final 키워드로 명시적으로 선언되지 않았더라도, 초기화된 후 그 값이 변경되지 않으면, 마치 final인 것처럼 취급됩니다. 이러한 규칙은 람다식이 실행될 때 변수의 안전한 캡처를 보장하기 위한 것으로, 메모리 해제와는 직접적인 관련이 없습니다. 따라서, 변수는 변경되지 않는 한 람다식 내에서 안전하게 사용될 수 있으며, 이는 람다식 내외부에서 변수의 일관된 값을 유지하기 위해 필요합니다.

 

  1.6 메소드 참조(Method Reference)

콜론 두 개(::)는 자바에서 메소드 참조(method reference)를 나타내는데 사용되는 문법입니다. 이 문법은 람다 표현식을 더 간결하게 표현할 수 있게 해줍니다. 메소드 참조는 특정 메소드만을 호출하는 람다 표현식을 대체하기 위해 사용됩니다. 크게 네 가지 유형이 있습니다:

  1. 정적 메소드 참조 (ClassName::methodName)
  2. 인스턴스 메소드 참조 (instanceReference::methodName)
  3. 특정 유형의 임의 객체에 대한 인스턴스 메소드 참조 (ClassName::methodName)
  4. 생성자 참조 (ClassName::new)

각각의 예제를 통해 자세히 알아보겠습니다.

    1) 정적 메소드 참조

IntBinaryOperator intOP = Math::max;
int r9 = intOP.applyAsInt(10, 20);
System.out.println(r9);

여기서 Math::max는 Math 클래스의 정적 메소드인 max 메소드를 참조합니다. 이 코드는 두 정수의 최대값을 반환하는 IntBinaryOperator 함수형 인터페이스의 인스턴스를 생성합니다. Math.max 메소드는 두 개의 int 타입 인자를 받고 int 타입의 결과를 반환하므로, IntBinaryOperator 인터페이스의 applyAsInt 메소드 시그니처와 일치합니다.

     2) 인스턴스 메소드 참조

String str = "Hello";
Predicate<String> predicate = str::startsWith;
boolean result = predicate.test("He");
System.out.println(result);

이 예제에서 str::startsWith는 str 객체의 startsWith 메소드를 참조합니다. 이는 주어진 문자열이 특정 접두사로 시작하는지 여부를 검사하는 Predicate<String> 함수형 인터페이스의 인스턴스를 생성합니다.

     3) 특정 유형의 임의 객체에 대한 인스턴스 메소드 참조

List<String> list = Arrays.asList("a", "b", "A", "B");
list.sort(String::compareToIgnoreCase);

이 코드는 String 객체의 compareToIgnoreCase 메소드를 참조하여 리스트의 문자열을 대소문자를 구분하지 않고 정렬합니다. 여기서 String::compareToIgnoreCase는 모든 String 객체에 적용될 수 있는 인스턴스 메소드 참조입니다.

     4) 생성자 참조

BiFunction<String, Integer, Member> bb = Member::new;
Member m1 = bb.apply("홍길수", 48);

Member::new는 Member 클래스의 생성자를 참조합니다. 여기서 사용된 문법은 두 개의 인자를 받는 Member 클래스의 생성자에 대한 참조를 생성합니다. 이는 BiFunction<String, Integer, Member> 인터페이스의 인스턴스를 생성하며, 이 인터페이스는 두 인자를 받고 결과를 반환하는 함수입니다. 이 경우, apply 메소드는 "홍길수"와 48을 인자로 받아 Member 객체를 생성하고 반환합니다.

메소드 참조를 사용함으로써 코드가 더 간결해지고 가독성이 향상됩니다. 또한, 메소드 참조는 람다 표현식과 마찬가지로 함수형 인터페이스의 인스턴스를 생성합니다.

 

2. 참고하면 좋을 사이트(자바 람다식과 함수형 인터페이스)

https://yozm.wishket.com/magazine/detail/2023/