Written by
Poogle
on
on
Modern Java in Action - Ch.3
참고: 책 - Modern Java in Action
Ch 3. 람다 표현식
- 자주 바뀌는 요구사항에 효과적으로 대응
- 람다 표현식: 메서드로 전달할 수 있는 익명 함수를 단순화한 것
람다의 특징
- 익명: 보통의 메서드와 달리 이름이 없으므로 익명이라 표현, 구현해야 할 코드에 대한 걱정거리가 줄어들음
- 함수: 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수라고 부름. 하지만 메서드처럼 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함
- 전달: 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있음
- 간결성: 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요가 없음
람다 문법 - 예제를 통해 알아보기
() -> {} //(O) 파라미터 X, void 반환
() -> "Poogle" // (O) 파라미터 X, 문자열 반환
() -> {return "Poogle"} // (O) 파라미터 X 명시적으로 문자열 반환
(Integer i) -> return "Poogle" + i; // (X)
(Integer i) -> {return "Poogle" + i;} // (O)
(String s) -> {"Iron Man";} //(X)
(String s) -> {return "Iron Man";} //(O)
(String s) -> "Iron Man" //(O)
process(() -> System.out.println("This is Awsome")); //(O)
execute(() -> {});
public void execute(Runnable r) {
r.run();
} //(O)
public Callable<String> fetch() {
return () -> "Tricky ;-)";
} //(O)
Predicate<Apple> p = (Apple a) -> get.weight(); //(X)
- Q. 왜 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있을까?
- -> 더 복잡하게 만들지 않는 현재 방법을 선택
람다 활용 - 실행 어라운드 패턴
- 기존 메서드
public static String processFileBefore() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
- 함수형 인터페이스 생성
import java.io.BufferedReader;
import java.io.IOException;
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader br) throws IOException;
}
- 함수형 인터페이스를 인자로 받는 메서드 생성
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
- 여러 줄 출력할 때도 사용할 수 있도록 활용
public static void main(String[] args) throws IOException {
String oneLine = processFile(BufferedReader::readLine);
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
}
Predicate
- test: 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환
@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)) {
results.add(t);
}
}
return results;
}
List<String> listOfStrings = new ArrayList<>();
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
- accept: 제네릭 형식 T 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> c) {
for (T t : list) {
c.accept(t);
}
}
forEach(
Arrays.asList(1, 2, 3, 4, 5),
(Integer i) -> System.out.println(i)
);
Function
- apply: 입력과 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스 활용
@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;
}
List<Integer> l = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length()
);
형식 검사, 형식 추론, 제약
- 람다가 사용되는 콘텍스트를 이용해서 람다의 형식 추론 가능
- 대상 형식을 이용해서 람다 표현식을 특정 콘텍스트에 사용할 수 있는지 확인할 수 있음
- 대상 형식 특징 때문에 같은 람다 표현식이더라도 호환되는 추상 메서드를 가진 다른 함수형 인터페이스로 사용될 수 있음
- 하나의 람다 표현식을 다양한 함수형 인터페이스에 사용할 수 있음
지역 변수 사용
- 람다 표현식에서는 익명함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있음 -> 람다 캡처링
- 자유 변수의 제약
- 지역 변수는 명시적으로 final로 선언되어 있어야 함
- 실질적으로 final로 선언된 변수와 똑같이 사용되어야 함
제약 이유는?
- 인스턴스 변수 -> 힙
- 지역 변수 -> 스택
- 람다에서 지역 변수에 바로 접근할 수 있다는 가정 하에 람다가 스레드에서 실행 -> 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있음
- ⏩ 자바 구현에서는 원래 변수에 접근을 허용 X -> 자유 지역 변수의 복사본을 제공
- ⏩ 복사본의 값이 바뀌지 않아야 함 -> 지역 변수에는 한 번만 값을 할당해야 함
메서드 참조
- 메서드 참조는 특정 메서드만을 호출하는 람다의 축약형
- ex. 람다 says “이 메서드명을 직접 호출해” -> 메서드를 어떻게 호출해야 하는지 설명을 참조하기보다는 메서드명을 직접 참조하는 것이 편리
- 기존 메서드 구현으로 람다 표현식을 만들 수 있음, 명시적으로 메서드명을 참조함으로써 가독성을 높일 수 있음
정적 메서드 참조
- ex.
Integer::parseInt
- ex.
//람다
ToIntFunction<String> stringToInt = (String s) -> Integer.parseInt(s);
//메서드 참조
Function<String, Integer> stringToInteger = Integer::parseInt;
다양한 형식의 인스턴스 메서드 참조
- ex.
String::length
- ex.
//람다
BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element);
//메서드 참조
BiPredicate<List<String>, String> contains = List::contains;
기존 객체의 인스턴스 메서드 참조
- ex. Trasaction 객체를 할당받은 expensiveTransaction 지역변수, Transaction 객체에 getValue가 있을 때 ->
expensiveTransaction::getValue
- ex.
//람다
Predicate<String> startsWithNumebr = (String string) -> this.startsWithNumber(string);
//메서드 참조
Predicate<String> startsWithNumber = this::startsWithNumber;
생성자 참조
ClassName::new
- new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있음
- 정적 메서드의 참조를 만드는 방법과 비슷
람다 표현식의 조합
- 단순한 람다 표현식을 조합해서 더 복잡한 람다 표현식 만들기
- 람다 표현식을 조합해도 코드 자체가 문제를 잘 설명함
Comparator 조합
ex. 사과 무게를 내림차순 정렬, 같은 무게의 사과라면 국가별로 정렬
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
Predicate 조합
ex. 빨간색이 아닌 사과
Predicate<Apple> notRedApple = redApple().negate;
ex. 빨간색이면서 무거운 사과
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
ex. 빨간색이면서 무거운 사과 또는 그냥 녹색 사과
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));