[모던 자바 인 액션] Chapter 3 람다 표현식

OhJuYeong·2025년 12월 17일

모던 자바 인 액션

목록 보기
1/9
post-thumbnail

3.1 람다란 무엇인가?

람다 표현식

  • 메서드로 전달할 수 있는 익명 함수를 단순화한 것
  • 이름은 없지만 파라미터 리스트, 바디, 반환형식, 발생할 수 있는 예외 리스트는 가질 수 있음

특징

  • 익명
    • 보통의 메서드와 달리 이름이 없으므로 익명이라고 표현
  • 함수
    • 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부름
    • 하지만 메서드처럼 파라미터 리스트, 비디, 반환 형식, 가능한 예외 리스트 포함
  • 전달
    • 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 잇음
  • 간결성
    • 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요없음

람다 표현식

(Apple a1, Apple a2) → a.getWeight().compareTo(a2.getWeight());

-람다 파라미터 - - 화살표- -람다 바디-

  • 파라미터 리스트
    • Comparator 의 compare 메서드 파라미터
  • 화살표
    • 람다 파라미터 리스트와 바디 구분
  • 람다 바디
    • 두 사과의 무게를 비교 , 람다의 반환값에 해당하는 표현식
 //String 형식의 파라미터를 가지며 int를 반환 람다 표현식은 return이 함축 되어있어 안써도됨
(String s) -> s.length() 
//Apple 형식의 파라미너 하나를 가지며 boolean을 반환
(Apple a) -> a.getWeight() > 150
//int 형식의 파리미터 두개를 가지며 리턴값은 없다 , 여러행을 포함해 쓸 수 있다.
(int x, int y) -> { System.out.println("Result:" ); System.out.println(x+y);}
//파라미터가 없으며 int 42 반환
() -> 42
//Apple 형식의 파라미터 두개를 가지며 int를 반환
(Apple a1, Apple a2) → a.getWeight().compareTo(a2.getWeight());

람다 문법

  1. () → {} 파리미터가 없고 void를 반환 pulbic void run() {}처럼 바디가 없는 메서드
  2. () → “Raoul” 파라미터가 없고 문자열을 반환
  3. () → { return “Mario”;} 파라미터가 없고 문자열을 반환하는 표현식(명시적으로 return 문사용)
  4. (Integer i ) → { return “Alan”+i;} return은 흐름 제어문 {} 가 있어야함
  5. (String s) → “Iron Man” 구문이 아니라 표현식으로 {} 가 있으면 안됨

예제

  1. 불리언 표현식 (List list) → list.isEmpty()
  2. 객체 생성 () → new Apple(10)
  3. 객체에서 소비 (Apple a) → { System.out.println(a.getWeight());}
  4. 객체에서 선택 /추출 (String s) → s.length()
  5. 두 값을 조합 (int a, int b) → a* b
  6. 두 객체 비교 (Apple a1. Apple a2) → a1.getWeight().compareTo(a2.getWeight())

3.2 어디에 어떻게 람다를 사용

3.2.1 함수형 인터페이스

Predicate 가 함수형 인터페이스 , 오직 하나의 추상 메서드만 지정하기 때문

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

함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스

추가로 Comparator, Runnable 등이 있음

전체 표현식을 함수형 인터페이스의 인스턴스로 취급 가능

Runnable r1 = () -> System.out.println("Hello Wolrd 1"); // ㅏㄹㅁ다 사용
Runnable 41 = new Runnable(){ // 익명 클래스 사용
	public void run(){
	 System,out.println("Hello Wolrd 2");
	}
};

public static void process(Runnable r){
 r.run();
}
process(r1); //Hello Wolrd 1 출력
process(r2); //Hello Wolrd 2 출력
process(() -> System.out.println("Hello world 3")); // 직접 전달된 람다 표현식 

3.2.2 함수 디스크립터

함수형 인터페이스의 추상 메서드 시그니처 는 람다 표현식의 시그니처를 가르킴

람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 부름

ex) Runnable 인터페이스의 유일한추상 메서드 run은 인수와 반환값이 없으므로 인수와 반환값이 없는 시그니처로 생각

() → void // 파라미터 리스트가 없고 void를 반환

(Apple, Apple) → int 는 두개의 인수를 받아 int를 반환

함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있을까 → 언어를 더 복잡하게 만들지 않기 위해

3.3 람다 호라용: 실행 어라운드 패턴

자원 처리에 사용하는 순환패턴: 자원 열기 → 처리 → 자원 닫기

설정과 정리 과정은 대부분 비슷

→ 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 가짐

초기화/준비코드→ 작업 → 정리/ 마무리 코드

→ 이걸 실행 어라운드 패턴 이라고 부름

예제, 자바 8에서 새로 추가된 trh-with-resources 구문, 자원을 명시적으로 닫을 필요가 없어 간결

public String porcessFile() throws IOException {
 try( BufferedReader br = new BufferedReader(new FileReader("data.txt")))}
  return br.readLing();
}
}  

3.3.1 1단계 : 동작 파라미터화를 기억하라

혅내 코드는 파일에서 한번에 한줄말 읽을 수 있음

한번데 두줄을 읽거나 가장 자주 사용되는 단어를 반환하려면?

→ 기존의 설정, 정리과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령할 수 있으면 좋음

→ 동작을 파라미터화화 하는 것

processFile 메서드가BufferedReader를 이용해 다른 동작을 수해할 수 있도록 processFile 메서드로 동작을 전달

람다를 이용해 동작 전달

한번에 두행을 읽게 하려면?

String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

3.3.2 2단계: 함수형 인터페이스를 이용해서 동작 전달

함수형 인터페이스 자리에 람다를 사용할 수 있음

BufferedReader → String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어함

@FunctinalInterface
public interface BufferedReaderProcessor{
 String process(BufferedReader b) throws IOException;
 }
 
 public String processFile(BufferedReaderProcessor p) thorws IOException{
 ...
 }

3.3.3 3단게: 동작 실행

람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으면 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리

public String porcessFile(BufferedReaderProcessor p ) throws IOException {
 try( BufferedReader br = new BufferedReader(new FileReader("data.txt")))}
  return p.process(br);
}
}  

3.3.4 4단계: 람다 전달

이제 람다를 이용해 다양한 동작을 메서드로 전달 가능

String oneLine = processFile((BufferedReader br) -> br.readLine());

String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine()); 

3.4 함수형 인터페이스 사용

함수형 인터페이스는 오직 하나의 추상 메서드를 지정

함수형 인터페이스의 추상 메서드는 람다 표현식의 시그니처를 묘사함

  • 함수 디스크립터

다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요

3.4.1 Predicate

java.util.function.Predicate 인터페이스

  • test라는 추상메서드를 정의 , test는 제너릭 형식 T의 객체를 인수로 받아 불리언을 반환
  • 정의할 필요없이 바로 사용할 수 있음

T 형식의 객체를 사용하는 불리언 표현식이 필요한 상황에서 Predicate 인터페이스를 사용할 수 있음

ex) String 객체를 인수로 받는 람다를 정의

@FunctionalInterface
public interface Predicate<T> {
 boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p){
 List<T> results = new ArrayList<>();
 for(T t: list){
  if(p.test(t)){
   result.add(t);
  }
 }
 return results;
}
Predicate<String nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

Predicate 인터페이스의 자바독 명세를 보면 and나 or 같은 메서드도 있음을 알 수 있음

3.4.2 Consumer

java.util.function.Consumer 인터페이스는 제너릭 형식 T 객체를 받아서 void를 반환하는 accpet라는 추상 메서드를 정의

T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있음

ex) Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach메서드를 정의할 때 Consumer를 활용할 수 있음

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

public <T> void forEach(List<T> list, Consumer<T> c){
 for(T t:list){
  c.accpet(t);
 }
}
forEach( Arrays.asList(1,2,3,4,5),(Integer i ) -> System.out.println(i)
);

3.4.3 Function

java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의

입력을 출력으로 매핑하는 랃마를 정의할 때 Function인터페이스를 활용

ex) String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer리스트로 변환하는 map메서드를 정의하는 예제

@FunctionalInterface
public interface Function<T,R>{
 R apply(T t);
}
public <T,R> List<R> map(List<T> list,Function<T,R> f){
 List<R> result = new ArrayList<>();
 for(T t:list){
  result.add(f.apply(t));
 }
 return result;
}

//[7,2,6]
List<Integer> l = map(Arrays.asList("lambds","in","action"),(String s) -> s.length()
);

기본형 특화

Predicate, Consumer,Function<T,R>

자바의 모든 형식은 참조형 아니면 기본형에 해당

제네릭 파라미터에는 참조형만 사용할 수 있음 제네릭의 내부 구현 때문에 어쩔 수 없는 일

자바에서는 기본형으로 참조형으로 변환하는 기능 제공

→ 이 기능을 박싱, 반대 동작을 언박싱

박싱, 언박싱 자동으로 이루어지는 오토박싱

이런 변환 과정을 비용이 소모됨

박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장됨

→ 박싱한 값은 메모리를 더 소미하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요

자바 8에서는 기본형 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공

하지만 이런 변환 과정은 비용이 소모

자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 함수형 인터페이스 제공

public interface IntPredicate{
 boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i%2 ==0;
evenNumbers.test(1000); //참(박싱없음)
Predicate<Integer> oddNumbers = (Integer i) -> i%2 !=0;
oddNumberse.test(1000); //거짓(박싱)

3.5 형식 검사, 형식 추론, 제약

람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있진 않음

3.4.1 형식 검사

람다가 사용되는 콘텍스트를 이용해 람다의 형식을 추론할 수 있음

어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부름

List<Apple> heavierThan150g = 
filter(inventory, (Apple apple) -> apple.getWeight() > 150);

다음 순서로 형식 확인 과정 진행

  1. filter 메서드의 선언을 확인
  2. filter 메서드는 두 번째 파라미터로 Predicate 형식을 기대
  3. Predicate 은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스
  4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사
  5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야함

람다 표현식이 예외를 던질 수 있다면 추상 메서드도 같이 예외를 던질 수 있도록 throws로 선언

3.5.2 같은 람다, 다른 함수형 이넡페이스

대상 형식 이라는 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용 될 수 있음

ex) Callable 과 PrivilegedAction 인터페이스는 인수를 받지 않고 제네릭 형식 T를 반환하는 함수를 정의

Callable<Integer> c = () -> 42; 
PrivilegedAction<Integer> p = () -> 42;

둘다 유효

3.5.3 형식 추론

자바 컴파일러는 대상 형식을 이용해서 함수 디스크립터를 알수 있으므로 컴파일러는 람다 시그니처도 추론 가능

→ 컴파일러는 람다 표현식의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략 가능

아래처럼 람다 파라미터 형식을 추론할 수 있음

List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor());
//파라미터 apple에 형식을 명시적으로 지정하지 않음

상황에 따라 명시적으로 형식을 포함하는게 좋을 수도 있고 배제하는 것이 가독성을 향상시킬때도 음

정해진 규칙은 없기 때문에 스스로 향상 시킬 수 있는 코드를 결정

3.5.4 지역변수 사용

람다 표현식에서 익명 함수가 하는 것 처럼 자유 변수를 활용할 수 있음

→ 람다 캡처링

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

자유 변수에도 약간의 제약이 있음

람다는 인스턴스 변수와 정적 변수를 자유롭게 캡쳐할 수 있음

→ 지역변수는 명시적으로 final로 선언 되어있어야하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야함

즉 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처 가능

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337; //에러 람다에서 참고하는 지역 변수는 final로 선언되건 final 취급

지역 변수 제약

제약이 필요한 이유

내부적으로 인스턴스 변수와 지역 변수는 태생부터 다름

인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치

람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행되면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접글하려할 수 있음

자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공

→ 복사본의 값이 바뀌지 않아야하므로 지역 변수에는 한번만 값을 할당해야한다는 제약이 생김

이 제약 때문에 외부 변수를 변화시키는 일반적인 명령형 프로그램 패턴에 제동 걸 수 있음

3.6 메서드 참조

메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달 할 수 있음

→ 람다 표현식보다 메서드 ㅊ암조를 사용하는 것이 더 가독성 좋을 때가 있음

inventory.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

inventory.sort(comparing(Apple::getWeight)//메서드 참조와 java.util.Comparator.comparing 활용한 코드
//(Apple a) → a.getWeight() 축약

3.6.1 요약

메서드 참조

  • 특정 메서드만을 호출하는 람다의 축약형
  • 예) 람다가 이 메서드를 직접 호출해 라고 명령 시 메서드를 어떻게 호출해야 하는지 설명을 참조하기 보다 메서드명을 직접 참조하는 것이 편리
  • 메서드 참조 이용하면 기존 메서드 구현으로 람다 표현식을 만들 수 있음
  • 가독성 향상
  • 메서드명 앞에 구분자(::)를 붙임
  • 괄호는 필요 없음

ex)

(Apple apple) -> apple.getWeight() => Apple::getWeight
() ->  Thread.currentThread().dumpStack() => Thread.currentThread()::dumpStack
(str,i) -> str.substring(i) => String::substring
(String s) -> System.out.println(s) => System.out::println
(String s) -> this.isValidName(s) => this::isValidName
//뭐야 책을 굉장히 보기 어렵게 해놨다;;;;

메서드 참조 만드는 법

  1. 정적 메서드 참조
    1. Integer의 parseInt Integer::parseInt
  2. 다양한 형식의 인스턴스 메서드 참조
    1. String의 length 메서드는 String::length로 표현
  3. 기존 객체의 인스턴스 메서드 참조
    1. Transcation객체를 할당 받은 expensvieTranscation지역 변수가 있고
    2. Transaction객체에는 getValue가 있다면 expensiveTransaction::getValue 로 표현

컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷한 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환하는지 확인

→ 메서드 참조는 콘텍스트의 형식과 일치해야함

3.6.2 생성자 참조

ClassName::new 처럼 클래스명과 new 키워드를 이용해 기존 생성자의 참조를 만들 수 있음

ex) 인수가 없는 생성자 즉 Supplierd의 () → Apple 과 같은 시그니처를 갖는 생성자가 있다고 가정

Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get(); //Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있음
//아래와 같은 코드
Supplier<Apple> c1 = () -> new Apple(); //람다 표현식은 디폴트 생성자를 가진 Apple을 만듦
Apple a1 = c1.get(); //Supplier의 get 메서드를 호출해서 새로운 Apple 객체를 만들 수 있음

Apple(Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스와 같음

Function<Integer,Apple> c2= Apple::new;
Apple a2 = c2.apply(110);

Function<Integer,Apple> c2= (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);

인스턴스화 하지 않고도 생성자에 접근할 수 있는 기능을 다양한 상황에 응용 가능

3.7 람다, 메서드 참조 활용하기

3.7.1 1단계 : 코드 전달

sort 메서드에 정렬 전략을 전달 할 수 있나?

void sort(Comparator<? super E> c)

Comparator 객체를 인수로 받아 두 사과를 비교

객체 안에 동작을 포함시키는 방식으로 다양한 전략을 전달

sort의 동작은 파라미터화 됨

→ sort에 전달된 정렬 전략 에 따라 sort의 동작 달라짐

public class AppleComparator implements Comparator<Apple>{
 public int compare(Apple a1, Apple a2)
{ return a1.getWeight().compareTo(a2.getWeight());
}
inventory sort(new AppleComparator());

3.7.2 2단계: 익명클래스 사용

inventory.sort(newComparator<Apple>(){
 public int compare(Apple a1, Apple a2)
{ return a1.getWeight().compareTo(a2.getWeight());
});

3.7.3 3단계: 람다 표현식 사용

inventory.sort((Apple a1, Apple a2) -> a1.getWeigth().compareTo(a2.getWeight()));
//형식 추론을 통해 아래와 같이 더 줄일 수 있음
inventory.sort((a1,a2) -> a1.getWeight().compareTo(a2.getWeight()));

가독성 더 향상

Comparator 는 comparable 키를 추출해서 Comparator 객체로 만드는 Function 함수를 인수로 받는 정적 메서드 comparing 포함

Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight())));

3.7.4 4단계: 메서드 참조 사용

메서드 참조 이용하면 더 깔끔하고 간소하게 전달가능

inventory.srot(comparing(Apple::getWeight));

3.8 랃마 표현식을 조립할 수 있는 유용한 메서드

3.8.1 Comparator 조합

Comparator c = Comparator.comparing(Apple::getWeight);

역정렬

inventory.srot(comparing(Apple::getWeight).reversed());

Comperator 연결

무게가 같은 두 사과가 존재할때 어떤 사과부터 나열"?

→ 비교 결과를 더 다듬을 수 있는 두번째 Comparator를 만들 수 있음

inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));

3.8.2 Predicate 조합

Predicate 인퍼테이스는 복잡한 프레디케이트를 만들 수 있도록 netgate, and, or 세가지 메서드 제공

Predicate<Apple> notRedApple = redApple.netgate();

Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);

3.8.3 Function 조합

Function 인터페이스는 Function 인스턴스를 반환하는 andThen,compose 두가지 디폴트 메서드를 제공

andThen 메서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환

compose 메서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공

3.9 비슷한 수학 개념

수학을 좋아하지 않으니 건너 뛰겠다.

profile
기록하는 개발자

0개의 댓글