[Modern Java In Action] ch3 - 람다 표현식
익명 클래스로 다양한 동작을 구현할 수 있었지만 코드가 깔끔하지는 않았다. 람다로 더 깔끔한 코드를 구현하고 전달하자.
함수형 인터페이스
Predicate<T>
, Consumer<T>
, Function<T, R>
, Supplier<T>
, UnaryOperator<T>
, BinaryOperator<T>
등의 함수형 인터페이스가 있다.
함수 디스크립터
(Apple, Apple) -> int
와 같은 람다 표현식의 시그니처를 함수 디스크립터라고 한다.
람다 활용 : 실행 어라운드 패턴
함수형 인터페이스의 사용
- Predicate
java.util.Predicate<T>
인터페이스의test
라는 추상 메서드는 제너릭 T를 받아 boolean을 반환한다.@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
```java public class PredicateExample { public static void main(String[] args) { Predicate
nonEmptyStringPredicate = (String s) -> !s.isEmpty(); List filter = filter(List.of("Hello", "", "World", "!", ""), nonEmptyStringPredicate); System.out.println(filter.size()); }
private static
2. Consumer
`java.util.Consumer<T>` 인터페이스의 `accept` 라는 추상 메서드는 제너릭 T를 받아 void를 반환한다.
```java
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public class ConsumerExample {
public static void main(String[] args) {
List<String> strings = List.of("Hello", "World", "!");
forEach(strings, (String s) -> System.out.println(s));
}
private static <T> void forEach(List<T> strings, Consumer<T> c) {
for (T s : strings) {
c.accept(s);
}
}
}
- Function
java.util.Function<T, R>
인터페이스의apply
라는 추상 메서드는 제너릭 T를 받아 제너릭 R을 반환한다. 입력을 출력으로 매핑하는 함수를 정의할 때 사용한다.public class FunctionExample { public static void main(String[] args) { List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length()); System.out.println(l); } private static <T, R> List<R> map(List<T> list, Function<T, R> o) { List<R> result = new ArrayList<>(); for (T t : list) { result.add(o.apply(t)); } return result; } }
기본형 특화
위 세개의 함수형 인터페이스 Predicate
, Consumer
, Function
은 기본형 특화 버전이 있다.
오토박싱의 과정은 비용이 소모되며 성능을 저하시킬 수 있다.
박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장된다.
따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정을 거치게 된다.
아래 예제에서 IntPredicate
는 박싱을 하지 않지만, Predicate<Integer>
는 박싱을 한다.
이런식으로 특정 형식을 입력으로 받는 함수형 인터페이스는 IntPredicate
, IntConsumer
, IntFunction<R>
등이 앞에 타입이 붙는다.
public class App {
public static void main(String[] args) {
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
System.out.println(evenNumbers.test(1000));
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
System.out.println(oddNumbers.test(1000));
}
}
public interface IntPredicate {
boolean test(int value);
}
자바 8의 함수형 인터페이스 종류
| Functional Interface | Function Descriptor | Examples |
| ——————– | ——————- | ——– |
| Predicate
참고
Function Descriptor | Functional Interface |
---|---|
T -> boolean |
Predicate<T> |
T -> void |
Consumer<T> |
T -> R |
Function<T, R> |
(int, int) -> int |
IntBinaryOperator |
() -> T |
Supplier<T> |
(T, U) -> R |
BiFunction<T, U, R> |
형식 검사, 형식 추론, 제약
형식 검사
어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 한다. 아래 코드를 통해 람다 표현식을 사용하면 실제 어떤 일이 일어나는지 보여준다.
List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
- filter 메서드의 선언
invetory, (Apple a) -> a.getWeight() > 150
을 확인 - filter 메서드는
(Apple a) -> a.getWeight() > 150
에 대해Apple
을 받아서boolean
을 반환하는Predicate<Apple>
형식으로 대체됨 Predicate<Apple>
은test
라는 추상 메서드 정의하는 함수형 인터페이스test()
메서드는Apple
을 받아서boolean
을 반환하는 함수 디스크립터를 묘사,boolean test(Apple a)
로 Apple을 인수로 받아 boolean을 반환하는 test 메서드로 대체됨
형식 검사2
() -> void
형식의 함수 디스크립터를 갖는 Runnable로 대상 형식을 바꿔서 문제를 해결할 수 있다.
// ERROR > Target type of a lambda conversion must be an interface
Object obj = () -> { System.out.println("Hello"); };
Runnable obj = () -> { System.out.println("Hello"); };
Object obj = (Runnable) () -> { System.out.println("Hello"); };
형식 추론
자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 이용해 람다 표현식과 관련된 함수형 인터페이스를 추론합니다.
지역 변수 사용, 람다 캡처링
람다 표현식에서는 익명 함수가 하는 것처럼 외부에 정의된 변수(자유변수)를 활용할 수 있고, 이를 람다 캡처링이라고 합니다.
람다 표현식에서 사용될 자유변수는 명시적으로 final
이거나 final
처럼 작동해야 합니다.
자바 8에서는 effectively final이라는 개념을 도입해 해당 변수가 변경되지 않았다고 컴파일러가 판단하면, 해당 변수를 final로 해석한다.
public class App {
public static void main(String[] args) {
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 1000; // ERROR
r.run();
}
}
왜? 람다는 지역변수에 제약을 걸었을까?
인스턴스 변수와 지역변수는 다른 메모리에 저장됩니다. 인스턴스 변수는 힙에 저장되고, 지역변수는 스택에 저장됩니다. 람다에서 지역변수에 바로 접근이 가능하며 둘이 각각 스레드에서 실행되고 있단 가정하에 변수를 할당한 스레드가 사라져서 할당이 해제가 되어도 람다를 실행하는 스레드에선 해당 변수에 접근하려고 할 수 있기 때문입니다. 중요한 점은, 지역 변수가 스택 메모리에 할당되며 스레드가 종료되면 해당 스택 메모리 영역도 해제된다는 것 그러나 람다 표현식에서 사용하는 지역 변수는 내부적으로 람다 객체에 복사되어 저장됩니다. 따라서, 변수를 할당한 스레드가 종료되어도 람다 표현식은 여전히 그 변수의 복사본에 접근할 수 있습니다.
메서드 참조
메서드 참조는 람다 표현식을 더 간결하게 만들 수 있는 방법입니다.
//람다 미사용
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
//람다 사용
inventory.sort(comparing((Apple::getWeight)));
명령어 | 메서드 참조 대신 함수 표현 |
---|---|
(Apple apple) -> apple.getWeight() | Apple::getWeight |
() -> | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substrㅍㅍing |
(String s) -> System.out.println(s) | System.out.println |
-> this.isValidName(s) | this::isValidName |
댓글남기기