[자바 8-11] 람다 뿌시기

코린이서현이·2024년 12월 9일
0

Java

목록 보기
49/50
post-thumbnail

이번 글에서는 람다를 쓰는 방법, 람다로 함수형 인터페이스 사용, 람다 표현식의 형식 검사 과정을 알아볼 것이다.!

이론적인 이야기만 계속 하게 될 것 같아서 미리 집중력을 채우시길 .. 🙏

람다는 아예 새로운 무언가는 아니다.
이전에 불필요했던 여러 코드를 조금 더 깔끔하고, 간결하게 표현할 수 있는 표현 방법이다.
결국 람다로 할 수 있는 것은 람다 없이도 할 수 있다.

람다를 쓰는 방법부터 알아보자!

람다는 파라미터 리스트와 화살표, 람다바디로 이루어져있다.

(Apple a1, Apple a2) ->  a1.getWeight().compareTo(a2.getWeight());
람다 파라미터        화살표  람다 바디

또한 람다바디 유형에 따라 쓰는 방식이 달라진다.

(1) 람다바디가 표현식일 때

(람다파라미터) -> 표현식 // 람다 표현식에는 retrun 이 함축되어있어서 사용하지 않아도 된다. 

(String s) -> s.length()
() -> 56
(int a) -> a > 140

(2) 람다바디가 구문일 때

(람다파라미터) -> {구문;}

(String s) -> {retrun s.length();}
(int a) -> {
		System.put.printin(a);
        retrun a; 
    }

람다의 특징에 대해서 알아보자

  • 익명 : 다른 메서드와 다르게 이름이 없다.
  • 함수 : 람다는 특정 클래스에 종속되지 않기 때문에 메서드가 아닌 함수라는 표현을 쓴다. 하지만 파라미터 리스트, 바디, 반환 형식, 예외 리스트를 가질 수 있다.
  • 전달 : 람다 표현식을 메서드의 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성 : 익명 클래스와 비교시 간단하다.

간결성과 익명에 대해서는 전 글을 읽어본다면 이해가 잘 될거라고 생각한다.

일급 시민(값)의 개념, 그리고 일급 시민인 람다

일급 시민이란 인수로, 변수로 전달 할 수 있는 값을 뜻한다.
전달할 수 없는 값은 이급시민이다.
intInteger 클래스의 인스턴스는 매개변수로 전달할 수 있다. 그러나, Integer클래스 자체를 매개변수로 전달할 수는 없다. 따라서 기본값, 인스턴스는 일급시민, 클래스는 이급시민이다.

1급 시민의 조건 🫡
1. 변수나 데이터에 할당할 수 있어야 한다.
2. 객체의 인자로 넘길 수 있어야한다.
3. 객체의 리턴값으로 리턴할 수있어야한다.

그런데 위 람다의 특징에서 람다는 메서드의 인수로, 변수로도 저장할 수 있다고 했다.
결국 람다는 일급시민이라는 뜻이다!

(1) 메서드의 인수로 사용되는 람다

int result1 = Calculator.operate(5, x -> x * x);

//함수형 인터페이스
interface Processor {
    int process(int x);
}

//함수형 인터페이스를 인수로 받는 메서드(이후 람다로 받음) 
class Calculator {
    public static int operate(int x, Processor processor) {
        return processor.process(x);
    }
}

(2) 변수로 저장되는 람다

Runnable runnable = () -> System.out.println("Hello");
//이때 변수의 타임은 무조건 함수형 인터페이스여야한다.

이렇게 람다는 전달할 수 있는 값인 일급시민에 해당된다.
그렇다면? 람다가 일급시민이 되었을 때 유용성은 무엇일까?

람다가 일급시민이 된 것의 유용성

  1. 함수를 변수에 저장하고 전달할 수 있어 유연한 설계가 가능해졌다.
  2. 함수를 변수에 저장하고 여러곳에서 재사용할 수 있게 되었다
    3. 동작 파라미터화 코드를 더 간결하게 전달할 수 있게 되었다.

람다 활용하기

람다 표현식으로 함수형 인터페이스의 인스턴스를 만들 수 있다.

함수형 인터페이스는 하나의 추상메서드만을 가지는 인터페이스다.
덜도말고 더도 말고 딱 하나만
여기서 기억해야할 점은 디폴트 메서드의 개수는 포함되지 않고 추상메서드의 개수만 하나여야 한다는 것이다.

그런데 함수형 인터페이스에만 쓰일 수 있는 이유는 무엇일까?
자바의 복잡성을 낮추고, 함수형 인터페이스에 이미 익숙해져있다는 점, 기존 자바의 타입 시스템을 유지할 수 있다는 점 때문이라고 한다!

(1) 함수형 인터페이스의 선언
추상메서드가 하나인 함수형 인터페이스를 선언한다.

interface ApplePredicate {
    boolean test(Apple apple);    // 사과를 선별하는 추상메서드
}

(2) 함수형 인터페이스의 사용 메서드

public class AppleFilter {
    public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate predicate) {
        List<Apple> result = new ArrayList<>();
        for (Apple apple : inventory) {
            if (predicate.test(apple)) {
                result.add(apple);
            }
        }
        return result;
    }
}

(3) 람다 표현식으로 동작 전달(람다 전달)

List<Apple> heavyApples = AppleFilter.filterApples(inventory,
    (Apple apple) -> apple.getWeight() > 150);

람다 표현식의 형식 검사 과정

컴파일러는 어떻게 람다 표현식을 보고, 정확한 구현인지 아닌지를 판단할 수 있을까? 바로 람다가 사용되는 컨텍스트를 통해 람다의 형식을 추혼한다.

함수 디스크립터는 함수형 인터페이스의 추상 메서드가 받는 입력값과 반환하는 값의 형태를 화살표를 사용해 표현한 것이다.
String을 받아서 Integer를 반환하는 메서드의 함수 디스크립터는 (String) -> Integer로 표현된다.

이런 함수 디스크립터를 알아야하는 이유는 함수 디스크립터에 따라 람다 표현식의 형식이 결정되기 때문이다. 이렇게 결정되는 람다 표현식의 형식은 대상 형식이라고 불린다.

컴파일러가 람다의 형식 검사를 하는 과정을 알아보자

(1) 람다가 사용된 콘텍스트, 즉 해당 메서드의 함수형 인터페이스를 확인한다.
(2) 함수형 인터페이스의 추상메서드의 함수 디스크립터를 묘사한다.
(3) 람다 표현식과 함수 디스크립터가 일치하는지 검사한다.

형식 추론을 통한, 파라미터 타입 생략

람다 파라미터의 타입을 생략할 수 있게 된다. 왜냐면, 컴파일 과정 중에서 함수 디스크립터를 통해 람다의 파라미터 형식을 알고 있기 때문이다. 선택적으로 생략할 수 있다.

//생략 버전
List<Apple> heavyApples = AppleFilter.filterApples(inventory,
    apple -> apple.getWeight() > 150);

람다가 자주 쓰이는 함수형 인터페이스

존재를 알고 넘어가자!

(1) Predicate<T> : T -> boolean
조건식을 판단할때 사용한다.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

// 사용 예시
Predicate<String> isEmpty = str -> str.isEmpty();
list.removeIf(isEmpty);

(2) Consumer<T> : T->void
값을 받아서 처리만 할때

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

// 사용 예시
Consumer<String> printer = s -> System.out.println(s);
list.forEach(printer);

(3) Function<T,R> : T -> R
값을 받아서 다른 값으로 변환할때

@FunctionalInterface
public interface Function<T,R> {
    R apply(T t);
}

// 사용 예시
Function<String, Integer> toInt = str -> Integer.parseInt(str);
list.stream().map(toInt);

(4) Supplier<T> : () -> T
파라미터는 없고, 값을 공급할 때

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

// 사용 예시
Supplier<String> supplier = () -> "Hello";
String str = supplier.get();

메서드 참조

람다를 더 간단하게 표현하는 메서드 참조 표현도 있지만, 실제로 쓰이는 예시들을 알고넘어가면 충분할 것 같다.

메서드 참조란 가독성을 높이기 위해 메서드 명을 직접 참조하는 것이다.
아래 대표적인 용법에 대해서 살펴보자

(1) 타입 변환

strings.stream().mapToInt(Integer::parseInt);
numbers.stream().mapToDouble(Double::valueOf);

//스트림없는 버전으로 보고 싶다면
Function<String, Double> toDouble = Double::valueOf;

(2) 컬렉션으로 변환

List<String> list = stream.collect(Collectors::toList);
// 자바 16이상인 경우에는 아래가 더 간결하다.
List<String> list = stream.toList();     

(3) 생성자 참조

stream.map(Student::new)

마무리하면서..!

개발 공부를 하다보면 어떤게 중요한 지, 어떤게 덜 중요한지 파악하는 능력이 필요한데 그런건 참 어려운 것 같다 ㅎ..

그래도 이것만은 기억하자!!라고 한다면!?

람다 표현식으로 함수형 인터페이스의 추상메서드를 정의해, (구현 클레스를 만들고, 인스턴스화 하는 과정없이도) 인터페이스의 인스턴스를 만들 수 있다!

또 그렇기 때문에 람다표현식은 함수형 인터페이스를 받는 메서드의 파라미터로 쓰인다!!

결과적으로 람다 표현식은 동적 파라미터화를 지원하게 된다.

이론적인 이야기를 어쩔 수 없이 계속 하게 되었다...
블로그를 운영하면서 이론 60 : 실제 40의 비율을 지금부터라도 유지하기로 했기때문에 
당장 코드 짜러 가야한다 ㅎ_ㅎ
profile
24년도까지 프로젝트 두개를 마치고 25년에는 개발 팀장을 할 수 있는 실력이 되자!

0개의 댓글