Language/Java
[모던 자바 인 액션] Chap3. 람다 표현식
뚜sh뚜sh
2024. 1. 29. 16:43
람다 표현식
- 메서드로 전달할 수 있는 익명 함수를 단순화한 것
- 이름은 없지만, 파라미터 리스트, 바디, 반환 형식, 발생할 수 있는 예외 리스트는 가질 수 있음
- 람다의 특징
- 익명 : 보통의 메서드와 달리 이름이 없으므로 익명이라 표현함, 구현해야 할 코드에 대한 걱정거리가 줄어듬
- 함수 : 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부름, 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함함
- 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있음
- 간결성 : 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없음
※ 파라미터 리스트 (Parameter List)
- 메서드: 메서드는 0개 이상의 파라미터를 가질 수 있음, 각 파라미터는 타입과 이름을 가짐, 예: void method(int a, String b)
- 람다 표현식: 람다 표현식도 0개 이상의 파라미터를 가질 수 있음, 타입을 명시할 수도 있고, 생략할 수도 있음, 예: (a, b) -> {...} 또는 (int a, String b) -> {...}
※ 바디 (Body)
- 메서드: 메서드 바디는 중괄호 {}로 둘러싸여 있으며, 명령문(statement)과 표현식(expression)을 포함할 수 있음
- 람다 표현식: 람다 바디도 중괄호로 둘러싸일 수 있음, 단일 표현식은 중괄호 없이 사용될 수 있으며, 여러 명령문은 {}로 둘러싸여야 함
※ 반환 형식 (Return Type)
- 메서드: 메서드는 반환 형식을 명시적으로 선언해야 함, 예: int method(...) {...}
- 람다 표현식: 람다 표현식은 컴파일러가 문맥을 통해 반환 타입을 유추할 수 있으므로, 반환 형식을 명시적으로 작성하지 않음, 명령문이 한 줄이라면 return 문도 생략할 수 있음
※ 발생할 수 있는 예외 리스트 (List of Possible Exceptions)
- 메서드: 메서드는 발생할 수 있는 예외를 명시할 수 있음, 예: void method() throws IOException {...}
- 람다 표현식: 람다 표현식에서도 예외를 던질 수 있지만, 이를 명시적으로 선언하지는 않음, 대신 람다 표현식을 사용하는 함수형 인터페이스가 예외를 선언할 수 있음
Expression과 Statement의 차이
Expression (표현식)
- 표현식은 값으로 평가되는 코드 조각, 즉, 실행되면 단일 값으로 해석됨
- 표현식은 변수, 연산자, 함수 호출 등을 포함할 수 있으며, 이들의 조합으로 구성됨
- 예를 들어, 5 * 3, a + b, Math.max(10, 20) 등이 표현식임, 이들은 각각 15, a와 b의 합, 20으로 평가됨
- 표현식은 보통 다른 표현식의 일부로 사용되거나, statement의 일부로 사용됨
Statement (구문)
- statement는 완전한 실행 단위임, 프로그램에서 어떤 작업을 수행하는 코드의 단위로, CPU에게 실행할 명령을 나타냄
- statement는 표현식을 포함할 수 있지만, 값으로 평가되지는 않음
- statement의 예로는 변수 선언(int a = 5;), 조건문(if (a > 5) {...}), 반복문(for (int i = 0; i < 10; i++) {...}) 등이 있음
- 각 statement는 보통 세미콜론(;)으로 끝남
함수형 인터페이스
- 정확히 하나의 추상 메서드를 지정하는 인터페이스
- 예) 자바 API의 함수형 인터페이스로는 Comparator, Runnable 등이 있음
- 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스(기술적으로 따지면 함수형 인터페이스를 구현한 클래스의 인스턴스)로 취급할 수 있음
- @FunctionalInterface : 함수형 인터페이스임을 가리키는 어노테이션
함수 디스크립터
- 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킴
- 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부름
※ 시그니처
- 프로그래밍에서 메서드 또는 함수를 고유하게 식별하는 데 사용되는 특정 구성 요소를 말함
- 예) () -> void
실행 어라운드 패턴(Execute Around Pattern)
- 자원 처리(예를 들면 데이터베이스의 파일 처리)에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어짐
- 즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 가지는 코드를 실행 어라운드 패턴이라고 부름
Predicate
- java.util.function.Predicate<T> 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 boolean을 반환함
- 따로 정의할 필요 없이 바로 사용할 수 있다는 점이 특징
- T 형식의 객체를 사용하는 boolean 표현식이 필요한 상황에서 사용할 수 있음
Consumer
- java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의함
- T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있음
Function
- java.util.function.Function<T, R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply를 정의함
- 입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있음
기본형 특화
- 자바의 모든 형식은 참조형(Byte, Integer, Object, List) 아니면 기본형(int, double, byte char)에 해당함
- 하지만 제네릭 파라미터에는 참조형만 사용할 수 있음(제네릭 내부 구현 떄문)
- 자바에서는 기본형을 참조형으로 변환하는 기능을 제공(박싱)
- 박싱한 값은 기본형을 감싸는 래퍼이며 힙에 저장됨
※ 언박싱 : 참조형을 기본형으로 변환하는 동작
※ 오토박싱 : 프로그래머가 편리하게 코드를 구현할 수 있도록 박싱과 언박싱이 자동으로 이루어지는 기능
Parameter와 Argument의 차이
Parameter (파라미터)
- 파라미터는 함수나 메서드 정의에 나타나는 변수
- 함수나 메서드가 수행할 작업에 필요한 입력을 나타내며, 함수나 메서드의 '서명(signature)'의 일부임
- 함수나 메서드가 호출될 때, 파라미터는 특정 값을 받아들일 '공간' 또는 '슬롯'으로 생각할 수 있음
- 예: 자바에서 void myMethod(int a, String b)에서 a와 b는 파라미터임
Argument (아규먼트)
- 아규먼트는 함수나 메서드를 호출할 때 전달하는 실제 값
- 아규먼트는 호출된 함수나 메서드의 파라미터에 할당되거나, 그 함수 내에서 사용됨
- 즉, 아규먼트는 함수나 메서드 '호출' 시에 사용되는 구체적인 값임
- 예: myMethod(5, "Hello")에서 5와 "Hello"는 아규먼트임
형식 검사
- 람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있음
- 어떤 콘텍스트(예를 들면 람다가 전달될 메서드 파라미터나 람다가 할당되는 변수 등)에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부름
※ 다이아몬드 연산자
- 자바 7부터 도입된 "다이아몬드 연산자"(<>)는 제네릭 타입 추론을 단순화하는 문법
- 다이아몬드 연산자를 사용하면, 컴파일러가 컨텍스트를 기반으로 제네릭 타입의 파라미터를 자동으로 추론할 수 있음
- 이는 코드를 더 간결하고 읽기 쉽게 만들며, 타입 선언을 중복해서 작성하는 번거로움을 줄여줌
※ 특별한 void 호환 규칙
- void 반환 타입을 가진 람다 표현식과 관련된 특정 상황을 다룰 수 있음
- 이 규칙은 주로 함수형 인터페이스와 람다 표현식을 사용할 때 중요한 역할을 함
- 람다 표현식에서는 반환 타입이 void인 경우, 표현식의 바디(body)가 단일 문장(single statement)인 경우에는 그 문장이 반환 타입이 void이거나, 어떠한 값도 반환하지 않는 메서드 호출일 수 있음
람다 캡처링(capturing lambda)
- 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있음
- 이러한 동작을 람다 캡처링이라고 부름
int portNumber = 1337;
Runnable r = () => System.out.println(portNumber);
- 하지만 자유 변수에도 약간의 제약이 있음
- 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록)할 수 있음
- 하지만 그러려면 지역 변수는 명시적으로 final로 선언되어 있어야 하거나 실질적으로 final로 선언된 변수와 똑같이 사용되어야 함
- 즉, 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있음(참고 : 인스턴스 변수 캡처는 final 지역 변수 this를 캡처하는 것과 마찬가지다)
※ 인스턴스 변수
- 클래스의 각 객체(인스턴스)에 속한 변수를 말함
- 인스턴스 변수는 클래스 내에서 선언되지만, 클래스의 메서드, 생성자 또는 블록에 속하지 않는 변수임
- 인스턴스 변수는 객체가 생성될 때마다 생성되며, 각 객체에 대해 고유한 상태를 저장함
- 인스턴스 변수는 자동으로 기본값으로 초기화됨
※ 정적 변수(static variable) 또는 클래스 변수(class variable)
- 클래스의 모든 인스턴스 간에 공유되는 변수
- 정적 변수는 클래스 레벨에 선언되며, 'static' 키워드를 사용하여 정의됨
- 클래스의 모든 인스턴스가 동일한 변수를 공유하기 때문에, 메모리 사용이 효율적임
- 정적 변수는 클래스가 메모리에 로드될 때 한 번만 생성되며, 프로그램 실행이 끝날 때까지 유지됨
- 클래스의 모든 인스턴스는 정적 변수에 대한 동일한 값을 보고, 이 값을 변경할 수 있음
※ 지역 변수(Local Variable)
- 함수, 메서드 또는 블록 내에서 선언되고 사용되는 변수를 말함
- 지역 변수는 선언된 블록, 함수, 또는 메서드 내에서만 접근 가능함
- 해당 범위를 벗어나면, 지역 변수는 더 이상 접근할 수 없으며, 메모리에서 해제됨
- 지역 변수는 자동으로 기본값으로 초기화되지 않기 때문에 사용하기 전에 반드시 초기화해야 함
- 지역 변수는 스택 메모리에 할당됨
지역 변수의 제약
- 내부적으로 인스턴스 변수와 지역 변수는 태생부터 다름
- 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치함
- 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있음
- 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공함
- 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것임
클로저(Closure)
- 클로저(Closure)는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical Environment)과의 조합임
- 렉시컬 환경이란 함수가 생성될 당시의 변수 스코프를 의미함
- 즉, 클로저는 함수가 자신이 정의됐을 때의 환경을 '기억'하는 함수
- 원칙적으로 클로저란 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킴
- 예를 들어 클로저를 다른 함수의 인수로 전달할 수 있음
- 클로저는 클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수 있음
- 자바 8의 람다와 익명 클래스는 클로저와 비슷한 동작을 수행함
- 람다와 익명 클래스 모두 메서드의 인수로 전달될 수 있으며 자신의 외부 영역의 변수에 접근할 수 있지만 람다가 정의된 메서드의 지역 변수의 값은 바꿀 수 없음
메서드 참조
- 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있음
- 메서드명 앞에 구분자(::)를 붙이는 방식으로 메서드 참조를 활용할 수 있음
메서드 참조를 만드는 방법
1. 정적 메서드 참조
- 예를 들어 Integer의 parseInt 메서드는 Integer::parseInt로 표현할 수 있음
2. 다양한 형식의 인스턴스 메서드 참조
- 예를 들어 String의 length 메서드는 String::length로 표현할 수 있음
3. 기존 객체의 인스턴스 메서드 참조
- 예를 들어 Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue 메서드가 있다면, 이를 expensiveTransaction::getValue라고 표현할 수 있음
※ 정적 메서드
- 클래스의 인스턴스(객체)와는 독립적으로 클래스 자체에 속하는 메서드
- 자바에서 정적 메서드는 static 키워드를 사용하여 정의
- 클래스가 로드되는 시점에 메모리에 할당됨
- 정적 메서드는 객체를 생성하지 않고도 클래스 이름을 통해 직접 호출할 수 있음
- 정적 메서드 내에서는 인스턴스 변수나 인스턴스 메서드에 직접 접근할 수 없음
- 왜냐하면 정적 메서드는 객체가 아닌 클래스 레벨에 존재하기 때문
- 하지만, 해당 클래스의 정적 변수나 정적 메서드에는 접근할 수 있음
※ 클래스 레벨 (Class Level)
- 클래스 레벨의 요소는 클래스에 속하며, 모든 인스턴스(객체)에 공통적으로 적용됨
- 클래스 변수(정적 변수, static variables)와 클래스 메서드(정적 메서드, static methods)는 클래스 레벨에 속함
- 이들은 static 키워드를 사용하여 정의되며, 클래스 이름을 통해 직접 접근할 수 있음, 예를 들어, Math.PI 또는 Math.max()와 같이 사용할 수 있음
- 클래스 레벨의 요소는 해당 클래스의 모든 객체에서 공유됨, 따라서 한 곳에서 클래스 변수의 값을 변경하면, 해당 클래스의 모든 객체에 영향을 미침
- 클래스 레벨의 요소는 프로그램이 시작될 때 메모리에 로드되고, 프로그램이 종료될 때 해제됨
※ 객체 레벨 (Object Level)
- 객체 레벨의 요소는 특정 클래스의 인스턴스(객체)에 속함
- 인스턴스 변수(non-static variables)와 인스턴스 메서드(non-static methods)는 객체 레벨에 속함
- 이들은 객체를 생성할 때마다 각 객체에 대해 별도로 생성되며, 객체를 통해 접근함, 예를 들어, myObject.methodName() 또는 myObject.variableName과 같이 사용할 수 있습니다.
- 객체 레벨의 요소는 해당 객체에 고유하며, 다른 객체의 상태나 동작에 영향을 미치지 않음
- 객체 레벨의 요소는 객체가 생성될 때 메모리에 할당되고, 객체가 더 이상 사용되지 않을 때 가비지 컬렉터에 의해 메모리에서 해제됨
※ 헬퍼 메서드(helper method) 또는 유틸리티 메서드(utility method)
- 주로 작업을 수행하는 데 도움이 되는 보조적인 기능을 제공하는 메서드
- 이러한 메서드들은 코드 중복을 줄이고, 가독성을 높이며, 유지 보수를 용이하게 하기 위해 사용됨
- 헬퍼 메서드 특징
- 재사용성: 헬퍼 메서드는 일반적으로 재사용 가능하도록 설계됨, 즉, 다양한 곳에서 같은 기능이 필요할 때 해당 메서드를 호출함으로써 코드를 재사용할 수 있음
- 단일 책임: 헬퍼 메서드는 보통 한 가지 작업 또는 관련된 몇 가지 작업만 수행함, 이는 '단일 책임 원칙(Single Responsibility Principle)'을 따르는 것으로, 코드를 이해하고 유지 관리하기 쉽게 만듦
- 모듈성: 헬퍼 메서드를 사용하면 코드를 잘 정의된 작은 단위로 분리할 수 있어, 전체 코드의 모듈성이 향상됨
- 숨김성: 때로는 헬퍼 메서드를 private으로 선언하여 클래스 내부에서만 사용하도록 할 수 있음, 이렇게 하면 클래스의 내부 구현을 숨기고, 외부에서는 클래스의 주요 기능만 사용하도록 할 수 있음
- 명확성: 헬퍼 메서드는 자체적인 이름을 가지고 있어, 메서드의 이름만 보고도 어떤 기능을 수행하는지 이해하기 쉬움
생성자 참조
- ClassName::new처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있음
- 이것은 정적 메서드의 참조를 만드는 방법과 비슷함
1. Supplier의 () -> Apple과 같은 시그니처를 갖는 생성자가 있다고 가정하자(아래 두 코드는 같은 코드)
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();
2. Apple(Integer weight)라는 시그니처를 갖는 생성자는 Function 인터페이스의 시그니처와 같다(아래 두 코드는 같은 코드)
Function<Integer, Apple> c2 = Apple::new;
Apple c2 = c2.apply(100);
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(100);
3. Map으로 생성자와 문자열값을 관련시키고, String과 Integer가 주어졌을 때 다양한 무게를 갖는 여러 종류의 과일을 만드는 giveMeFruit라는 메서드를 만들 수 있음
1. Map 선언 및 초기화
static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
2. 정적 초기화 블록
static {
map.put("apple", Apple::new);
map.put("orange", Orange::new);
}
- 정적 초기화 블록(static {})은 클래스가 로드될 때 한 번 실행됨
- 이 블록에서 map에 두 개의 항목을 추가함
3. giveMeFruit 메서드
public static Fruit giveMeFruit(String fruit, Integer weight) {
return map.get(fruit.toLowerCase())
.apply(weight);
}
Comparator.comparing()
- 자바 8에서 도입된 메서드로, 특정 클래스의 객체를 비교하기 위한 Comparator 객체를 생성하는 데 사용됨
Comparator.comparing(Function<? super T,? extends U> keyExtractor)
- keyExtractor: 객체에서 특정 키를 추출하는 함수, 이 함수는 비교할 객체의 한 속성을 반환함
예시
import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));
Comparator 조합
역정렬
- 내림차순으로 정렬하고 싶을 때 다른 Comparator 인스턴스를 만들 필요가 없음
- 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를 제공하기 때문
inventory.sort(comparing(Apple::getWeight).reversed());
Comparator 연결
- 정렬 리스트에서 같은 값이 존재한다면 thenComparing 메서드로 두 번째 비교자를 만들 수 있음
- thenComparing은 함수를 인수로 받아 첫 번째 비교자를 이용해서 두 객체가 같다고 판단되면 두 번째 비교자에 객체를 전달함
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
Predicate 조합
- Predicate 인터페이스는 복잡한 프레디케이트를 만들 수 있도록 negate, and, or 세 가지 메서드를 제공함
- 예를 들어 '빨간색이 아닌 사과'처럼 특정 프레디케이트를 반전시킬 때 negate 메서드를 사용할 수 있음
Predicate<Apple> notRedApple = redApple.negate();
- 또한 and 메서드를 이용해서 빨간색이면서 무거운 사과를 선택하도록 두 람다를 조합할 수 있음
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
- 그뿐만 아니라 or을 이용해서 '빨간색이면서 무거운 사과 또는 그냥 녹색 사과' 등 다양한 조건을 만들 수 있음
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));