[Java 심화] 람다, 함수형 인터페이스, 메서드 참조 (Lambda expression, Method Reference)

Kyung Jae, Cheong·2024년 10월 11일
0
post-thumbnail

람다, 함수형 인터페이스, 메서드 참조

0. 개요

  • 이번 포스팅에서는 Java 8부터 도입된 함수형 인터페이스, 람다 표현식, 그리고 메서드 참조에 대해 알아보겠습니다.
    • 이들은 Java에서 함수형 프로그래밍을 가능하게 하는 핵심 요소로, 코드의 간결성과 가독성을 크게 향상시켜 줍니다.
    • 문법적 설탕에 해당하는 요소들이라서 다른 방식으로도 충분히 구현 가능하지만, 코드의 간결성과 가독성을 크게 향상시키기 위해 주로 쓰이는 문법들입니다.
  • 람다 표현식
    • 함수형 인터페이스의 구현을 더 간단하게 표현할 수 있는 방법입니다.
    • 익명 클래스를 대체하여 가독성 높은 코드를 작성할 수 있도록 도와주며, 다양한 상황에서 효율적으로 사용할 수 있습니다.
  • 함수형 인터페이스
    • 하나의 추상 메서드를 가진 인터페이스로, Java에서 람다 표현식의 기반이 되는 요소입니다.
    • 함수형 인터페이스의 개념과 함께, 자바에서 기본으로 제공하는 함수형 인터페이스들에 대해서 다룹니다.
  • 메서드 참조
    • 람다 표현식을 더욱 간단하게 표현하는 방식입니다.
    • 메서드를 직접 참조하여 더 직관적이고 간결한 코드를 작성할 수 있습니다.

1. 람다 표현식 (Lambda Expression)

  • 람다 표현식은 Java 8부터 도입된 기능으로, 함수형 프로그래밍을 가능하게 하며, 코드의 간결성을 크게 향상시킵니다.
  • 람다 표현식은 익명 클래스를 대체할 수 있으며, 함수형 인터페이스와 함께 사용됩니다.

1.1 람다 표현식의 정의

  • 람다 표현식은 메서드처럼 보이지만, 익명 함수라고 할 수 있습니다.
    • 이를 통해 메서드를 단순히 인수로 전달하거나, 메서드처럼 사용할 수 있습니다.
  • 람다 표현식의 기본 문법은 아래와 같습니다.
(parameters) -> expression
(parameters) -> { statements }
  • 람다 표현식은 매개변수 목록, 화살표 (->) 그리고 메서드 본문으로 구성됩니다.

1.1.1 기본 예제

// 두 정수를 더하는 람다 표현식
(int a, int b) -> a + b
  • 위의 예제에서 (int a, int b)는 매개변수 목록이고, a + b는 메서드 본문으로, 두 정수를 더하는 기능을 제공합니다.
    • 이때 람다 표현식은 함수형 인터페이스에 종속되어 사용됩니다.
    • 함수형 인터페이스는 아래에서 자세히 다룹니다.

1.1.2 파라미터와 리턴 타입 생략 가능한 경우

  • Java는 타입 추론을 통해, 람다 표현식에서 파라미터의 타입을 생략할 수 있습니다.
  • 또한, 람다 표현식이 하나의 문장만을 포함하는 경우, 중괄호와 return 키워드도 생략 가능합니다.
// 파라미터 타입 생략
(a, b) -> a + b

// 문장이 한 줄일 경우 중괄호 생략
s -> System.out.println(s);

1.2 람다 표현식과 함수형 인터페이스

  • 람다 표현식은 함수형 인터페이스를 구현할 때 사용됩니다.
    • 함수형 인터페이스는 오직 하나의 추상 메서드를 가지며, 그 메서드의 구현을 람다 표현식으로 할 수 있습니다.
  • 다음은 Runnable 인터페이스를 람다로 구현한 예시입니다.
Runnable runnable = () -> System.out.println("Running");
runnable.run();  // 출력: Running
  • Runnable 인터페이스는 run()이라는 하나의 메서드를 가지므로, 이를 람다 표현식으로 구현할 수 있습니다.

1.3 람다 표현식의 다양한 형태

  • 람다 표현식은 다양한 형태로 사용될 수 있으며, 코드를 간결하게 작성하는 데 큰 역할을 합니다.

1.3.1 매개변수가 없는 람다 표현식

  • 매개변수가 없는 경우, 빈 괄호 ()를 사용합니다.
// 매개변수 없는 람다
() -> System.out.println("No parameters");

1.3.2 하나의 매개변수를 가진 람다 표현식

  • 하나의 매개변수를 가질 경우, 괄호를 생략할 수 있습니다.
// 괄호 생략 가능
s -> System.out.println(s);

1.3.3 여러 매개변수를 가진 람다 표현식

  • 매개변수가 여러 개일 경우, 괄호로 묶어 사용해야 합니다.
// 여러 매개변수
(a, b) -> System.out.println(a + b);

1.3.4 블록을 사용하는 람다 표현식

  • 람다 표현식이 여러 문장을 포함하는 경우, 중괄호 {}를 사용해야 합니다.
// 여러 문장을 포함하는 람다 표현식
(int a, int b) -> {
    int result = a + b;
    System.out.println(result);
    return result;
};

1.4 람다 표현식의 장점

  • 코드의 간결성: 익명 클래스에 비해 훨씬 짧고 직관적인 코드를 작성할 수 있습니다.
  • 가독성 향상: 복잡한 메서드 호출을 간단하게 표현할 수 있습니다. (다만 복잡한 코드는 가독성이 오히려 떨어질 수 있습니다)
  • 병렬 프로그래밍에 유리: Stream API와 결합하여 병렬 처리를 간단하게 구현할 수 있습니다.

1.5 람다 표현식과 익명 클래스 비교

  • 람다 표현식은 익명 클래스와 비슷한 역할을 하지만, 몇 가지 차이가 있습니다.
    • 타입 추론: 람다 표현식은 컴파일러가 타입을 추론하여 코드를 간결하게 작성할 수 있지만, 익명 클래스는 명시적으로 타입을 선언해야 합니다.
    • 간결성: 익명 클래스에 비해 코드가 매우 간결합니다.
    • this 참조: 람다 표현식 내에서의 this는 람다를 감싸고 있는 외부 객체를 가리키지만, 익명 클래스에서는 익명 클래스 자신을 가리킵니다.

예시

// 익명 클래스
Runnable runnable1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Running");
    }
};

// 람다 표현식
Runnable runnable2 = () -> System.out.println("Running");
  • 익명 클래스를 사용할 때는 new 키워드를 사용해 객체를 생성하고 메서드를 오버라이드해야 하지만, 람다 표현식은 단순한 화살표(->) 구문으로 동일한 작업을 수행할 수 있습니다.

2. 함수형 인터페이스 (Functional Interface)

  • 함수형 인터페이스(Functional Interface)는 단 하나의 추상 메서드를 가진 인터페이스입니다.
  • Java 8부터 도입된 이러한 인터페이스는 주로 람다 표현식과 함께 사용되며, Java에서 함수형 프로그래밍을 가능하게 해주는 중요한 요소입니다.

2.1 함수형 인터페이스의 정의

  • 함수형 인터페이스는 단 하나의 추상 메서드만을 가져야 합니다.
    • 이러한 특성 덕분에, 함수형 인터페이스는 바로 람다 표현식으로 구현할 수 있습니다.
  • Java에서는 @FunctionalInterface Annotation을 사용하여 정의할 수 있습니다.
    • 이는 함수형 인터페이스임을 명시적으로 나타내기 위한 Annotation입니다.
    • 이 Annotation은 인터페이스가 단일 추상 메서드만을 가지도록 강제하는 역할을 합니다.
    • 이 Annotation은 선택 사항이지만, 함수형 인터페이스임을 컴파일러가 검증하도록 도와줍니다.

예시

@FunctionalInterface
interface MyFunctionalInterface {
    void myMethod();
}
  • 위의 코드에서 MyFunctionalInterface는 단 하나의 추상 메서드인 myMethod()를 가지고 있기 때문에 함수형 인터페이스입니다.
    • 여기에 @FunctionalInterface 애너테이션을 붙이면, 추가적인 추상 메서드가 선언되었을 때 컴파일 오류가 발생하게 됩니다.

2.2 함수형 인터페이스와 람다 표현식의 관계

  • 함수형 인터페이스는 람다 표현식의 기초가 됩니다.
    • 함수형 인터페이스는 람다 표현식을 통해 구현할 추상 메서드를 정의합니다.
MyFunctionalInterface example = () -> System.out.println("Hello, Lambda!");
example.myMethod();  // 출력: Hello, Lambda!
  • 이 예제에서 함수형 인터페이스 MyFunctionalInterface를 람다 표현식으로 구현했습니다.
    • myMethod() 메서드를 실행하면 Hello, Lambda!가 출력됩니다.
    • 익명 클래스와 달리, 람다 표현식은 매우 간결하고 직관적인 형태로 코드를 작성할 수 있습니다.
  • 물론 익명클래스로도 동일하게 정의하여 사용할 수 있습니다.

2.3 자바에서 제공하는 주요 함수형 인터페이스

  • Java 8부터 자바 표준 라이브러리에는 다양한 기본 함수형 인터페이스가 포함되어 있습니다.
  • 이들은 모두 @FunctionalInterface로 정의되어 있으며, 여러 상황에서 유용하게 사용할 수 있습니다.

2.3.1 Runnable

  • 인자를 받지 않고 반환값도 없는 run() 메서드를 가지고 있습니다.
  • java.lang 패키지에 소속되어 있어서 별도의 import 없이 바로 사용 가능합니다.
  • 주로 쓰레드 작업에서 사용됩니다.
Runnable runnable = () -> System.out.println("Running");
runnable.run();  // 출력: Running

2.3.2 Supplier<T>

  • 인자를 받지 않고, T 타입의 값을 반환하는 get() 메서드를 가지고 있습니다.
  • java.util.function 패키지에 소속되어 있습니다.
  • 데이터를 생성하거나 제공하는 용도로 자주 사용됩니다.
Supplier<String> supplier = () -> "Supplied Value";
System.out.println(supplier.get());  // 출력: Supplied Value

2.3.3 Consumer<T>

  • 인자를 받고, 반환값이 없는 accept() 메서드를 가지고 있습니다. (void)
  • java.util.function 패키지에 소속되어 있습니다.
  • 주어진 데이터를 소비하는 작업(예: 출력, 저장 등)에 사용됩니다.
Consumer<String> consumer = (message) -> System.out.println(message);
consumer.accept("Hello, Consumer!");  // 출력: Hello, Consumer!

2.3.4 Function<T, R>

  • T 타입의 인자를 받고, R 타입의 결과를 반환하는 apply() 메서드를 가지고 있습니다.
  • java.util.function 패키지에 소속되어 있습니다.
  • 주로 데이터를 변환하는 작업에 사용됩니다.
Function<Integer, String> function = (number) -> "Number: " + number;
System.out.println(function.apply(5));  // 출력: Number: 5

2.3.5 Predicate<T>

  • T 타입의 인자를 받고, boolean 값을 반환하는 test() 메서드를 가지고 있습니다.
  • java.util.function 패키지에 소속되어 있습니다.
  • 주어진 조건에 맞는지 여부를 판별할 때 사용됩니다.
Predicate<Integer> isPositive = (num) -> num > 0;
System.out.println(isPositive.test(10));  // 출력: true
System.out.println(isPositive.test(-10)); // 출력: false

2.3.6 그외 함수형 인터페이스

  • UnaryOperator<T>
    • Function<T,T>를 상속 받는 인터페이스입니다.
    • 타입 T 인자를 받아서 동일한 T 타입을 반환합니다.
  • BiConsumer<T,U>
    • 타입 T와 타입 U 두개의 인자를 받아서 반환값 없이 소비합니다.
  • BiFunction<T,U,R>
    • 타입 T와 타입 U 두개의 인자를 받아서 타입 R을 반환합니다.
  • Bipredicate<T,U>
    • 타입 T와 타입 U 두개의 인자를 받아서 boolean 값을 반환합니다.
  • BinaryOperator<T>
    • BiFunction<T,T,T>를 상속 받는 인터페이스입니다.
    • 타입 T 인자 두개를 받아서 동일한 T 타입을 반환합니다.

2.4 사용자 정의 함수형 인터페이스

  • 필요에 따라 사용자 정의 함수형 인터페이스를 만들 수도 있습니다.
  • 함수형 인터페이스는 특정 작업을 더 명확하게 표현하거나, 커스텀한 로직을 위한 추상화를 제공하는 데 유용합니다.
@FunctionalInterface
interface Adder {
    int add(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Adder adder = (a, b) -> a + b;
        System.out.println(adder.add(10, 20));  // 출력: 30
    }
}
  • 이 코드에서 Adder라는 함수형 인터페이스를 정의하고, 이를 람다 표현식으로 구현하여 두 정수를 더하는 작업을 수행합니다.

2.5 디폴트 메서드와 정적 메서드

  • Java 8 이후부터 인터페이스에 디폴트 메서드와 정적 메서드를 선언할 수 있습니다.
  • 디폴트 메서드는 기본 구현을 제공할 수 있으며, 정적 메서드는 객체 생성 없이 호출할 수 있습니다.
    • 함수형 인터페이스의 정의에서 디폴트 메서드와 정적 메서드는 추상 메서드로 간주되지 않으므로 한개의 메서드만 포함해야한다는 함수형 인터페이스의 규칙을 위반하지 않습니다.
@FunctionalInterface
interface Calculator {
    int calculate(int x, int y);
    
    default void printMessage() {
        System.out.println("This is a default method.");
    }

    static void staticMethod() {
        System.out.println("This is a static method.");
    }
}
  • 위 예제에서 Calculator는 함수형 인터페이스이지만, 디폴트 메서드와 정적 메서드를 포함하고 있어도 문제가 되지 않습니다.

3. 메서드 참조 (Method Reference)

  • 메서드 참조(Method Reference)는 람다 표현식을 더욱 간결하게 표현할 수 있는 방식입니다.
    • 람다 표현식이 단순히 메서드 호출을 수행하는 경우, 메서드 참조를 사용하여 코드를 간단하고 읽기 쉽게 만들 수 있습니다.
  • 메서드 참조는 :: 연산자를 사용하여 클래스 또는 객체의 메서드를 직접 참조합니다.

3.1 메서드 참조의 기본 형태

  • 메서드 참조는 주로 다음과 같은 네 가지 형태로 나뉩니다.
    • 정적 메서드 참조
    • 인스턴스 메서드 참조 (객체::인스턴스 메서드)
    • 특정 객체의 인스턴스 메서드 참조
    • 생성자 참조 (Class::new)

3.1.1 정적 메서드 참조

  • 클래스의 정적(static) 메서드를 참조하는 방법입니다.
  • 형식: 클래스::메서드명
Function<Integer, String> intToString = String::valueOf;
System.out.println(intToString.apply(123));  // 출력: "123"
  • 위 예제에서는 String 클래스의 valueOf 정적 메서드를 참조하여, 정수를 문자열로 변환하는 작업을 수행하고 있습니다.

3.1.2 인스턴스 메서드 참조

  • 특정 객체의 인스턴스 메서드를 참조하는 방법입니다.
  • 형식: 객체::메서드명
String str = "Hello, World!";
Supplier<Integer> stringLength = str::length;
System.out.println(stringLength.get());  // 출력: 13
  • 이 예제에서는 문자열 객체 strlength 메서드를 참조하여 문자열의 길이를 반환하는 메서드 참조를 사용하고 있습니다.

3.1.3 임의 객체의 인스턴스 메서드 참조

  • 특정 클래스의 인스턴스 메서드를 참조할 때, 그 객체는 람다 표현식의 매개변수에서 전달됩니다.
  • 형식: 클래스::메서드명
Function<String, Integer> stringLength = String::length;
System.out.println(stringLength.apply("Java"));  // 출력: 4
  • 이 예제에서는 String 클래스의 length() 메서드를 참조하여, 매개변수로 전달된 문자열의 길이를 반환합니다.

3.1.4 생성자 참조

  • 생성자를 참조할 때는 클래스명 뒤에 ::new를 붙여서 사용합니다.
  • 형식: 클래스명::new
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();  // 새로운 ArrayList 객체 생성
  • 위 예제에서는 ArrayList의 생성자를 참조하여 새로운 리스트를 생성하는 메서드 참조를 사용하고 있습니다.

3.2 메서드 참조와 람다 표현식의 차이

  • 메서드 참조는 람다 표현식과 같은 기능을 제공하지만, 코드를 더 간결하게 만들 수 있습니다.

람다 표현식

Function<String, Integer> stringLength = s -> s.length();
System.out.println(stringLength.apply("Java"));  // 출력: 4
  • 람다 표현식으로 작성된 코드가 단순히 메서드를 호출하는 것이라면, 메서드 참조로 대체할 수 있습니다.

메서드 참조

Function<String, Integer> stringLength = String::length;
System.out.println(stringLength.apply("Java"));  // 출력: 4
  • 코드 간결성: 람다 표현식보다 더 간단하게 메서드를 참조할 수 있어 코드를 간결하게 작성할 수 있습니다.
  • 가독성 향상: 람다 표현식이 길어질 경우 메서드 참조를 통해 코드의 가독성을 높일 수 있습니다.

3.3 메서드 참조 사용 시 주의점

  • 메서드 참조는 람다 표현식과 유사하지만, 모든 경우에 적합한 것은 아닙니다.
  • 람다 표현식이 더 직관적이거나 이해하기 쉬운 경우, 메서드 참조 대신 람다를 사용하는 것이 좋습니다.
    • 특히, 복잡한 로직을 포함하거나 여러 작업을 수행해야 하는 경우에는 메서드 참조보다는 람다 표현식이 더 적합할 수 있습니다.

3.4 메서드 참조의 활용 예시

문자열 리스트 정렬

List<String> names = Arrays.asList("Kim", "Lee", "Park");
names.sort(String::compareToIgnoreCase);
System.out.println(names);  // 출력: [Kim, Lee, Park]
  • String::compareToIgnoreCase 메서드 참조를 사용하여 리스트를 대소문자 구분 없이 정렬하는 예제입니다.

생성자 참조를 사용한 객체 생성

Function<String, Person> personCreator = Person::new;
Person person = personCreator.apply("Alice");
System.out.println(person.getName());  // 출력: Alice
  • Person 클래스의 생성자를 메서드 참조로 사용하여, 새로운 Person 객체를 생성하는 예제입니다.

마무리

  • 이번 포스팅에서는 Java 8에서 도입된 람다 표현식, 함수형 인터페이스, 그리고 메서드 참조에 대해 다뤘습니다.
    • 이들은 Java에서 함수형 프로그래밍을 가능하게 하며, 코드의 간결성, 가독성, 유지보수성을 크게 향상시키는 중요한 요소들입니다.
  • 람다 표현식은 함수형 인터페이스를 기반으로, 더 간결하고 직관적으로 코드를 작성할 수 있게 도와줍니다.
    • 익명 클래스의 대체로 사용되며, 코드의 복잡성을 줄여줍니다.
  • 함수형 인터페이스는 람다 표현식의 기반이 되는 요소로, Java에서 함수형 프로그래밍을 가능하게 합니다.
    • 표준 라이브러리에 다양한 기본 함수형 인터페이스가 포함되어 있어, 이를 통해 더 쉽게 람다 표현식을 활용할 수 있습니다.
  • 메서드 참조는 람다 표현식의 단순한 호출을 더욱 간결하게 표현할 수 있는 방식으로, 코드를 읽기 쉽고 유지보수하기 좋게 만들어줍니다.
  • 이번 포스팅에서 정리한 문법적 요소들은 모두 함수형 프로그래밍 패러다임의 일부이며, Java에서 더욱 직관적이고 강력한 코드를 작성하는 데 큰 도움을 줍니다.
  • 다음 포스팅에서는 Java Stream API와 같은 함수형 프로그래밍 기법을 깊이 있게 다루어 보겠습니다.
profile
일 때문에 포스팅은 잠시 쉬어요 ㅠ 바쁘다 바빠 모두들 화이팅! // Machine Learning (AI) Engineer & BackEnd Engineer (Entry)

0개의 댓글