1. 자바 람다
1.1 람다 문법
매개변수
- 메서드의 인수와 마찬가지로 쉼포로 구분한다.
- 컴파일러가 매개변수의 타입을 추론할 수 있는 경우 매개변수의 타입을 생략할 수 있다.
- 매개변수가 하나인 경우에는 괄호를 생략할 수 있지만 매개변수가 없거나 둘 이상인 경우 괄호를 사용해야 한다.
화살표
- 람다의 매개변수와 람다 바디를 구분하기 위해 사용
바디
- 동작을 정의하는 부분
- 중괄호로 둘러싸여 있고, 메서드 본문과 유사하게 작성된다.
- 단일 표현식인 경우 중괄호를 생략할 수 있다.
Function<String, String> sayHello = (String input) -> {return "hello, " + input;};
Function<String, String> sayHello = input -> {return "hello, " + input;};
Function<String, String> sayHello = (String input) -> "hello, " + input;
Function<String, String> sayHello = input -> "hello, " + input
1.2 함수형 인터페이스
일반적인 인터페이스는 제네릭 바운드, 상속 인터페이스, 인터페이스 바디로 구성
메서드 시그니처
- 반드시 구현되어야 하는 추상 메서드 시그니처가 포함된다.
Default 메서드
- 인터페이스에 기본적인 구현을 제공
- default 키워드로 정의되며, 본문을 가질 수 있다.
- 구현체가 default 메서드를 오버라이드할 수 있지만, 하지 않는 경우 인터페이스의 기본 구현이 사용된다.
Static 메서드
- 인터페이스의 메서드로서 클래스명으로 직접 호출이 가능하다.
- static 키워드로 정의되며 메서드 바디를 가질 수 있다.
- 인터페이스의 구현체와는 무관하게 사용될 수 있다.
기존 인터페이스를 수정하지 않고도 새로운 기능을 추가할 수 있다.
인터페이스에 유틸리티 메서드를 추가하여 사용자에게 편리한 기능을 제공할 수 있다.
Q. 함수형 인터페이스 조건
SAM(Single Abstract Method)
- 추상 메서드 한 개를 가진 인터페이스
- Predicate 인터페이스는 하나의 추상 메서드인 test()를 갖고 있으므로 SAM 조건을 만족한다.
- 함수형 인터페이스로, 주어진 인자에 대해 참인지 거짓인지를 평가하는 역할을 한다.
- 필터링, 조건 검사, 조건부 동작을 수행하고자 할 때 사용된다.
public static void main(String[] args) {
Predicate<Integer> isPositive = num -> num > 0;
// 조건 검사
System.out.println(isPositive.test(5)); // true
System.out.println(isPositive.test(-5)); // false
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Predicate<Integer> isEven = num -> num % 2 == 0;
numbers.stream()
// 필터링
.filter(isEven)
// 조건부 동작
.forEach(num -> System.out.println(num + " "));
}
@FunctionalInterface
자바 8에서 SAM 조건을 만족하기 위해 @FunctionalInterface 어노테이션을 도입.
- 이 어노테이션을 사용하면 컴파일러가 해당 인터페스가 함수형 인터페이스임을 확인하고, 추상 메서드의 개수가 맞는지 검사한다.
- 어노테이션이 적용된 인터페이스를 구현한 클래스가 다른 메서드를 추가할 경우, 컴파일 오류가 발생한다.
- 람다 표현식이나 메서드 참조를 사용하기 위해서는 인터페이스에 추가 메서드를 정의할 수 없다.
@FunctionalInterface를 명시함으로써 해당 인터페이스가 함수형 인터페이스임을 알려주고,
코드와 인터페이스의 의도를 명확히 표현할 수 있다.
1.3 람다와 외부변수
람다는 외부 상태에 영향을 주지 않는 순수 함수의 개념을 따르지만, 유연성을 위해 어느 정도의 불순성을 허용한다.
'캡처(capture)'를 통해 람다가 정의된 생성 스코프 내의 상수와 변수를 획득할 수 있다.
아래 예제는 람다식 외부에서 정의된 변수(theAnswer)를 람다에 캡처하고,
해당 람다식을 printAnswer라는 변수에 할당한다.
static void capture() {
int theAnswer = 43;
Runnable printAnswer = () -> System.out.println("the answer is " + theAnswer);
run(printAnswer);
}
static void run(Runnable r) {
r.run();
}
public static void main(String[] args) {
capture();
}
람다 표현식에서 사용되는 외부 변수는 final 또는 Effectively final 이어야 한다.
그 이유는 람다 표현식 내부에서 사용되는 외부 변수를 캡처할 때,
그 값을 람다 표현식 인스턴스 내에 저장하고 변경하지 못하도록 보장하기 위함이다.
만약 외부 변수가 람다 표현식에서 변경된다면 예상치 못한 동작이 발생할 수 있다.
int theAnswer = 43;
Runnable printAnswer = () -> System.out.println("the answer is " + theAnswer);
// 컴파일 에러
theAnswer = 24;
run(printAnswer);
// local variables referenced from a lambda expression must be final or effectively final
// 람다 바디에서는 값이 단 한 번만 할당되는 지역변수만을 캡처할 수 있다
이를 방지하지 위해 불필요한 캡처 사용은 피하는 것이 좋다.
Effectively final
변수가 선언된 후 그 값이 변경되지 않음을 의미한다.
명시적으로 final 키워드를 사용하지 않아도 코드상에서 그 값이 처음 할당된 후 변경되지 않을 때 사용한다.
캡처된 어떤 변수든 초기화된 이후에 값이 한 번도 변경되지 않았다면 Effectively final이라 할 수 있다.
변수가 Effectively final인지 확인하는 방법은 해당 변수를 명시적으로 final로 선언하는 것이다.
final 키워드를 추가한 후에도 컴파일이 가능하다면 해당 키워드 없이도 컴파일이 가능하다.
그렇다고 모든 변수를 final로 선언하지는 않는데, 그 이유는 컴파일러가 외부에서 참조되는 부분을 Effectively final로 처리해주기 때문이다.
모든 변수를 final로 선언하면 코드의 가독성만 떨어진다.
참조를 다시 final로 만들기
Effectively final이 아닐 수 있지만 참조를 람다 내에서 사용해야 한다면 다시 final로 만드는 트릭이 있다.
// 여전히 Effectively final
var nonEffectivelyFinal = 1000L;
// 변경시 해당 변수는 람디에서 사용할 수 없다.
nonEffectivelyFinal = 9000L;
// 새 변수를 선언하고 해당 변수를 초기화 후 변경하지 않으면, 참조를 다시 final로 만들 수 있다.
var finalAgain = nonEffectivelyFinal;
참조를 다시 final로 바꾸는 것은 임시 방편일 뿐이다.
코드를 리팩토링하거나 재설계하는 것이 더 나은 선택지가 될 것이다.
1.4 익명 클래스
이름 없는 클래스로, 클래스를 정의하고 동시에 인스턴스를 생성하는 방법이다.
주로 인터페이스나 추상 클래스의 인스턴스를 생성할 때 사용된다.
람다 표현식과 익명 클래스는 둘 다 클래스를 정의하고 인스턴스를 생성하는 방법을 제공하지만,
구현 방식과 사용법에서 차이가 있다.
타입 유추
- 람다 표현식은 컴파일러가 타입을 유추할 수 있기 때문에 보다 간결하다.
- 익명 클래스는 명시적인 타입 선언이 필요하다.
캡처 동작
- 둘 다 외부 변수를 캡처하여 사용할 수 있다.
- 익명 클래스에서는 캡처된 변수가 final 또는 Effectively final이어야 하지만,
- 람다 표현식에서는 final 키워드를 사용하지 않아도 된다.
인터페이스 요구사항
- 익명 클래스는 추상 메서드를 직접 구현하는 방식으로 사용된다.
- 람다 표현식은 인터페이스를 인스턴스화 하는 데 사용된다.
- 람다 표현식은 익명 클래스보다 더 간결하고 명확한 코드를 작성할 수 있게 해준다.
선언적 프로그래밍
- 람다 표현식은 코드가 무엇을 하는지에 대한 의도를 더 잘 나타낸다.
public class Example {
// 함수형 인터페이스
interface HelloWorld {
String sayHello(String name);
}
public static void main(String[] args) {
// 익명 클래스
HelloWorld helloWorld = new HelloWorld() {
// 명시적 타입 선언 필요
@Override
public String sayHello(String name) {
return "hello, " + name + "!";
}
};
System.out.println(helloWorld.sayHello("성현"));
// 람다 표현식
HelloWorld helloWorldLambda = name -> "hello, " + name + "!";
System.out.println(helloWorldLambda.sayHello("성현2"));
}
}
익명 클래스는 함수형 인터페이스가 아닌 타입의 인스턴스를 만들 때만 사용하는 것이 좋다.
2. 람다 활용
2.1 람다 생성
새 인스턴스를 생성하려면 변수의 타입이 정의되어야 한다.
Predicate<String> isNull = value -> value == null;
인수에 타입을 명시적으로 사용하는 경우에도 함수형 인터페이스 타입이 필요하다.
// 컴파일 실패
var isNull = (String value) -> value == null;
타입 호환성
- 두 람다가 동일한 SAM 시그니처를 공유한다해도 타입 호환성 문제는 발생한다.
interface LikePredicate {
boolean test(T value);
}
public static void main(String[] args) {
LikePredicate<String> isNull = value -> value == null;
// 컴파일 에러
Predicate<String> wontCompile = isNull;
}
// LikePredicate<java.lang.String> cannot be converted to
// java.util.function.Predicate<java.lang.String>
메서드 인수와 리턴 타입으로써 생성된 람다는 타입 호환성 문제가 발생하지 않는다.
Predicate<Integer> isGreaterThan(int value) {
return compareValue -> compareValue > value;
}
메서드 시그니처에서 직접 람다의 타입을 추론하므로 람다로 얻고자 하는 결과에 집중할 수 있다.
2.2 람다 호출
언어별 람다 호출 차이
// JavaScript
let helloWorldJs = name => "hello, " + name + "!";
helloWorldJs("성현");
// Java
Function<Integer, String> helloWorldJava = name -> "hello, " + name + "!";
helloWorldJava.apply("성현");
다른 언어처럼 간결하지 않을 수 있지만 자바의 하위 호환성이 지속된다는 이점이 있다.
하위 호환성
새로운 버전의 자바가 이전 버전의 코드와 호환되는 정도를 의미
코드의 안전성과 신뢰성을 유지한다.
2.3 메서드 참조
이중콜론(::) 을 사용하여 기존의 메서드를 참조하여 람다 표현식을 생성한다.
정적 메서드 참조 (static method reference)
- 일반적으로 ClassName::staticMethodName 모양으로 사용된다.
Function<Integer, String> asRef = Integer::toHexString;
바운드/한정적 비정적 메서드 참조 (bound non-static method reference)
- 이미 존재하는 객체의 비정적 메서드를 참조
- 람다 인수는 그 특정 객체의 메서드 참조의 인수로 전달된다.
// 바운드 비정적 메서드 참조
var now = LocalDate.now();
Predicate<LocalDate> isAfterNowAsRef = now::isAfter;
// 반환값 바인딩 - 중간 변수 필요없이 직접 :: 연산자와 함께 결합 가능
Predicate isAfterNowAsRef = LocalDate.now()::isAfter;
System.out.println(isAfterNowAsRef.test(LocalDate.now()));
언바운드/비한정적 비정적 메서드 참조 (unbound non-static method reference)
- ClassName::instanceMethodName 패턴을 따른다.
- ClassName은 참조된 인스턴스 메서드가 정의된 인스턴스 유형을 나타내며 람다 표현식의 첫번째 인수이다.
- 명시적으로 참조된 인스턴스가 아닌 들어오는(인자) 인스턴스에서 호출된다.
// 람다
Function<String, String> toLowerCaseLambda = str -> str.toLowerCase();
// 언바운드 비정적 메서드 참조
Function<String, String> toLowerCaseRef = String::toLowerCase;
생성자 참조 (constructor reference)
- ClassName::new 로 사용된다.
//람다
Function<String, String> newStringLambda = input -> new String(input);
// 생성자 참조
Function<String, String> newStringRef = String::new;
메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 사용하고,
그렇지 않을 때 람다를 사용하는 것이 좋다.
'함수형 프로그래밍' 카테고리의 다른 글
[ch.06] 스트림을 이용한 데이터 처리 (0) | 2024.06.06 |
---|---|
[ch.05] 레코드 (0) | 2024.06.01 |
[ch.04] 가변성 & 불변성 (0) | 2024.05.23 |
[ch.03] JDK 함수형 인터페이스 (0) | 2024.05.16 |
[ch.01] 함수형 프로그래밍 소개 (1) | 2024.05.02 |