람다 표현식 - Lambda Expression

상윤·2024년 9월 26일
1

BackEnd

목록 보기
10/11

람다 표현식이란?
기본적으로 익명 함수(anonymous function)를 말합니다.
람다는 메서드를 단순화하고 재사용성을 높이는데 도움을 줍니다.
특히, 람다는 기존에 인터페이스 구현을 위해 필요한 장황한 코드를 생략하고, 단일 메서드를 직접 정의할 수 있게 합니다.

람다가 필요한 이유
java 8 이전엔 인터페이스의 구현체는 보통 익명 클래스를 사용하여 작성하였습니다. 익명클래스는 코드가 길어질수록 중복되는 코드가 늘고, 가독성이 떨어집니다. 이런 문제를 해결하기 위해 람다가 등장했습니다.

//8버전 이전의  Runnable 
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, Lambda!");
    }
};

//lambda 
Runnable runnable = () -> System.out.println("Hello, Lambda!");

이처럼 람다 표현식은 가독성을 높이고, 간결한 코드를 제공합니다.

람다의 기본 문법

//매개변수     ->  실행될 블록
(parameters) -> expression
(parameters) -> { statements; }

//++자바 컴파일러는 매개변수의 타입을 추론할 수 있기 때문에, 타입을 생략할 수도 있습니다.
//++ 블록이 여러줄이라면 중괄호를 사용해야 합니다.


//ex 두 수를 더하는 람다 
(a, b) -> a + b

//짝수를 출력하는 forEach를 사용한 람다 예제
public class LambdaExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

        numbers.forEach(n -> {
            if (n % 2 == 0) {
                System.out.println(n);
            }
        });
    }
}

위와 같이 람다 표현식은 코드의 목적을 더 직접적으로 표현해줍니다.
메서드 이름, 반환 타입, 매개변수 타입 등을 반복적으로 작성하는 것을 추상화하여 코드의 의도를 직관적으로 보여줍니다.

자바의 함수형 인터페이스(Function Interfaces)란?

함수형 인터페이스는 단 하나의 추상메서드만을 가지는 인터페이스를 뜻합니다.
이 인터페이스는 람다 표현식을 사용하여 메서드를 구현할 수 있습니다. 자바에선 이를 나타내기 위해 @FunctionalInterface 어노테이션을 사용할 수 있으며, 이는 명시적으로 함수형 인터페이스임을 나타내는데 유용합니다.


@FunctionalInterface
public interface MyFunctionalInterface {
    void doSomething();
}

//람다로 구현
MyFunctionalInterface myFunc = () -> System.out.println("Doing something!");
myFunc.doSomething();

@FunctionalInterface 어노테이션
@FunctionalInterface는 컴파일러가 해당 인터페이스가 함수형 인터페이스임을 보장하도록 도와줍니다. 즉, 두 개 이상의 추상 메서드가 있을 경우 컴파일 에러가 발생합니다. 그러나 이 어노테이션을 생략해도 함수형 인터페이스는 여전히 람다로 사용할 수 있습니다.

자바의 대표적인 함수형 인터페이스
자바는 함수형 인터페이스를 제공한다. 이는 java.util.function 패키지에 정의되어 있다.

Supplier
입력 없이 값을 반환하는 함수형 인터페이스
메서드: T get()

Supplier<String> supplier = () -> "Hello, Supplier!";
System.out.println(supplier.get());  // 출력: Hello, Supplier!

주로 데이터를 지연 로딩하거나 특정 값이 필요할 때 사용하는 패턴에서 유용합니다.

Supplier<String> defaultSupplier = () -> "Default Value";
String result = Optional.ofNullable(null).orElseGet(defaultSupplier);
System.out.println(result);  // 출력: Default Value

Consumer
입력을 받아 처리하고 결과를 반환하지 않는 함수형 인터페이스
메서드 : void accept(T t)

Consumer<String> consumer = message -> System.out.println("Message: " + message);
consumer.accept("Hello, Consumer!");  // 출력: Message: Hello, Consumer!

주로 특정 입력을 처리하는 작업을 정의할 때 사용됩니다.

List<String> messages = Arrays.asList("Hello", "Lambda", "Consumer");
messages.forEach(printer);

//출력
//Message: Hello
//Message: Lambda
//Message: Consumer

//내부로직
for (String message : messages) {
    printer.accept(message);
}
  • forEach가 내부적으로 각 요소에 대해 Consumer의 accept 메서드를 자동으로 호출합니다.

Function<T, R>
입력을 받아 변환한 값을 반환하는 함수형 인터페이스
메서드: R apply(T t)

Function<Integer, String> function = number -> "Number is " + number;
System.out.println(function.apply(5));  // 출력: Number is 5

입력값을 처리하여 새로운 값을 반환해야 할 때 유용합니다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<String> result = numbers.stream()                             
							.map(numberToString)                       
							.collect(Collectors.toList());
System.out.println(result);  // 출력: [Number: 1, Number: 2, Number: 3, Number: 4, Number: 5]

-----------------------------
//연속적인 변환 작업
Function<Integer, Integer> multiplyByTwo = x -> x * 2;
Function<Integer, Integer> addTen = x -> x + 10;

Function<Integer, Integer> combinedFunction = multiplyByTwo.andThen(addTen);

System.out.println(combinedFunction.apply(5));  // 출력: 20
------------------------------
//조건에 따른 변환 작업
Function<Integer, String> evenToString = number -> {
    if (number % 2 == 0) {
        return "Even number: " + number;
    } else {
        return "Odd number";
    }
};

System.out.println(evenToString.apply(4));  // 출력: Even number: 4
System.out.println(evenToString.apply(7));  // 출력: Odd number

Predicate
입력을 받아 논리적 테스트를 한 후, boolean 값을 반환하는 함수형 인터페이스
메서드: boolean test(T t)

Predicate<Integer> predicate = number -> number > 0;
System.out.println(predicate.test(10));  // 출력: true

필터링 조건을 정의할 때 사용됩니다.

List<Integer> numbers = Arrays.asList(-2, -1, 0, 1, 2);
List<Integer> positiveNumbers = numbers.stream()
                                       .filter(isPositive)
                                       .collect(Collectors.toList());
System.out.println(positiveNumbers);  // 출력: [1, 2]

커스텀 함수형 인터페이스
내장 함수형 인터페이스를 사용하지 않고, 사용자 정의 함수형 인터페이스를 정의할 수 있습니다.

@FunctionalInterface
public 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
    }
}

Function 인터페이스의 장점

  • 코드 간결화: Function 인터페이스를 사용하면 간단한 변환 작업을 훨씬 간결하게 표현할 수 있습니다. 복잡한 조건이나 여러 단계의 변환도 쉽게 처리할 수 있습니다.

  • 재사용성: Function은 추상화된 변환 작업을 정의할 수 있기 때문에, 다양한 상황에서 재사용할 수 있습니다.

  • 조합 가능성: Function은 andThen()과 같은 메서드를 통해 연속적인 변환 작업을 조합할 수 있어 복잡한 로직도 쉽게 관리할 수 있습니다.

    예제: 데이터 처리 파이프라인

Function<String, String> toUpperCase = String::toUpperCase;
Function<String, String> addPrefix = s -> "User: " + s;

List<String> users = Arrays.asList("alice", "bob", "charlie");

List<String> result = users.stream()
                           .map(toUpperCase.andThen(addPrefix))
                           .collect(Collectors.toList());

System.out.println(result);  
// 출력: [User: ALICE, User: BOB, User: CHARLIE]

람다 표현식과 메서드 참조 (Method Reference)

때로는 람다 표현식도 불필요하게 길어질 수 있습니다. 메서드 참조는 이러한 경우에 더욱 간결하고 직관적인 코드를 작성할 수 있도록 도와줍니다.

메서드 참조란?
기존의 메서드를 간단하게 참조하여 람다 표현식을 대체할 수 있는 기능입니다. 즉, 람다 표현식에서 이미 정의된 메서드나 생성자를 사용하는 경우, 메서드 참조로 바꾸어 더 간결한 코드를 작성할 수 있습니다.

//람다 표현식 
Function<String, Integer> strToIntLambda = s -> Integer.parseInt(s);
System.out.println(strToIntLambda.apply("123"));  // 출력: 123
//메서드 참조
Function<String, Integer> strToIntMethodRef = Integer::parseInt;
System.out.println(strToIntMethodRef.apply("123"));  // 출력: 123

정적 메서드 참조
• 클래스 이름과 정적 메서드를 참조하는 형태입니다.
• 형식: ClassName::staticMethod

Function<String, Integer> strToInt = Integer::parseInt;
System.out.println(strToInt.apply("123"));  // 출력: 123

---------
//정적 메서드를 참조하여 변환 작업을 수행할 수 있습니다.
Function<Double, Long> roundFunction = Math::round;
System.out.println(roundFunction.apply(4.7));  // 출력: 5

특정 객체의 인스턴스 메서드 참조
• 특정 객체의 메서드를 참조하는 형태입니다.
• 형식: instance::method

String message = "Hello, World!";
Supplier<Integer> lengthSupplier = message::length;
System.out.println(lengthSupplier.get());  // 출력: 13

-------------
//특정 객체의 인스턴스 메서드를 참조하여 작업을 수행할 수 있습니다.
String str = "Java";
Supplier<Integer> lengthSupplier = str::length;
System.out.println(lengthSupplier.get());  // 출력: 4

클래스의 임의 객체의 인스턴스 메서드 참조
• 클래스의 임의의 객체의 메서드를 참조하는 형태입니다.
• 형식: ClassName::method

Function<String, String> toUpperCase = String::toUpperCase;
System.out.println(toUpperCase.apply("hello"));  // 출력: HELLO

생성자 참조
• 생성자를 참조하여 객체를 생성하는 형태입니다.
• 형식: ClassName::new

Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();
System.out.println(list);  // 출력: []

--생성자를 참조하여 새로운 객체를 생성할 수 있습니다.

list.add("Hello");
System.out.println(list);  // 출력: [Hello]

람다와 함수형 인터페이스의 실용적인 예제

  1. 리스트 필터링
    자바의 Predicate 인터페이스와 람다 표현식을 활용하여 리스트에서 특정 조건을 만족하는 요소를 쉽게 필터링할 수 있습니다.

//Predicate<String>을 사용하여 이름이 “A”로 시작하는 요소만 필터링
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

Predicate<String> startsWithA = name -> name.startsWith("A");

List<String> filteredNames = names.stream()
                                  .filter(startsWithA)
                                  .collect(Collectors.toList());

System.out.println(filteredNames);  // 출력: [Alice]
  1. 데이터 변환
    Function 인터페이스와 map() 메서드를 사용하여 리스트의 데이터를 변환할 수 있습니다.
//리스트의 문자열을 모두 대문자로 변환

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

List<String> upperCaseNames = names.stream()
                                   .map(String::toUpperCase)
                                   .collect(Collectors.toList());

System.out.println(upperCaseNames);  // 출력: [ALICE, BOB, CHARLIE]

3. 데이터 누적 처리 (Reduce)
BinaryOperator 인터페이스와 reduce() 메서드를 사용하면 리스트의 데이터를 누적 처리할 수 있습니다.

//리스트의 숫자들의 합을 구하는 코드
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int sum = numbers.stream()
                 .reduce(0, (a, b) -> a + b);

System.out.println(sum);  // 출력: 15

4. 데이터 그룹화
Collectors.groupingBy()와 람다 표현식을 결합하여 리스트의 데이터를 특정 기준에 따라 그룹화할 수 있습니다.

//문자열의 길이를 기준으로 리스트의 문자열들을 그룹화하는 코드
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

Map<Integer, List<String>> groupedByLength = names.stream()
                                                  .collect(Collectors.groupingBy(String::length));

System.out.println(groupedByLength);
// 출력: {3=[Bob], 5=[Alice, David], 7=[Charlie]}

5. 조건부 데이터 처리
Predicate를 사용하여 데이터가 조건을 만족할 때만 특정 작업을 수행할 수 있습니다.

//리스트에서 짝수만 출력하는 코드

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

Predicate<Integer> isEven = number -> number % 2 == 0;

numbers.stream()
       .filter(isEven)
       .forEach(System.out::println);
// 출력: 2, 4, 6

6. 복잡한 데이터 처리 파이프라인
람다 표현식과 함수형 인터페이스를 조합하여 복잡한 데이터 처리 파이프라인을 구축할 수도 있습니다.

//리스트에서 짝수만 필터링한 후, 각 숫자에 10을 더하고 결과를 출력하는 코드

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

numbers.stream()
       .filter(n -> n % 2 == 0)
       .map(n -> n + 10)
       .forEach(System.out::println);
// 출력: 12, 14, 16

7. 비동기 처리와 람다
자바 8의 CompletableFuture와 람다 표현식을 결합하면 비동기 처리도 간결하게 구현할 수 있습니다.

//비동기적으로 데이터를 처리한 후 그 결과를 받아 작업을 수행하는 코드

CompletableFuture.supplyAsync(() -> "Hello, Asynchronous World!")
                 .thenAccept(System.out::println);
// 출력: Hello, Asynchronous World!

0개의 댓글