chapter 3. 람다 표현식
람다 표현식
더 깔끔한 코드로 동작 구현 및 전달하는 자바 8의 새로운 기능
람다 표현식은 익명 클래스처럼 이름이 없는 함수이면서 매서드를 인수로 전달할 수 있다.
3.1 람다란 무엇인가?
람다 표현식은 매서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 할 수 있다.
- 익명 : 보통의 메서드와 달리 이름이 없다.
- 함수 : 특정 클래스에 종속되지 않는다.
- 전달 : 메서드 인수로 전달, 변수로 저장 가능
- 간결성 : 익명 클래스처럼 코드 구현 필요 X
1) 기존 코드
Comparator<Apple> byWeight = new Comparator<Apple>() {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
};
2) 람다를 이용한 코드
Comparator<Apple> byRamdaWeight = (Apple a3, Apple a4)->a3.getWeight().compareTo(a4.getWeight());
- 파라미터 리스트
Comparator의 compare 메서드 파라미터 (사과 두개)
- 화살표
화살표(->)는 람다의 파라미터 리스트와 바디를 구분한다.
- 람다 바디
두 사과의 무게를 비교한다. 람다의 반환값에 해당하는 표현식이다.
예제 3-1 자바 8의 유효한 람다 표현식
// String 형식 파라미터, int 반환 (return 함축)
(String s) -> s.length()
// Apple 형식 파라미터, boolean 반환
(Apple a) -> a.getWeight() > 150
// int 파라미터 2개 리턴 (void 리턴) 람다 표현식은 여러 행의 문장을 포함할 수 있다.
(int x, int y) -> {
System.out.println("Result:");
System.out.println(x + y);
}
// int 42 반환
() -> 42
// Apple 형식 파라미터 2개, int 반환
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
3.2 어디에, 어떻게 람다를 사용할까?
예 : 필터메서드
List<Apple> greenApples = filter(inventory, (Apple a) -> GREEN.equals(a.getColor()));
3.2.1 함수형 인터페이스
2장에서 필터 메서드를 파라미터화하기 위해 사용했던 Predicate가 바로 함수형 인터페이스 이다. 함수형 인터페이스는 정확히 하나의 추상 메서드를 지정하는 인터페이스 로, Comparator, Runnable 등이 있다.
public interface Predicate<T> {
boolean test (T t);
}
public interface Comparator<T>{
int compare(T o1, T o2);
}
public interface Runnable{
void run();
}
public interface ActionListener extends EventListener{
void actionPerformed(ActionEvent e);
}
public interface Callable<V>{
V call() throws Exception;
}
public interface PrivilegedAction<T>{
T run();
}
인터페이스는 디폴트 매서드를 포함할 수 있다. 많은 디폴트 매서드가 있더라도 추상 매서드가 오직 하나면 함수형 인터페이스이다.
다음 예제는 Runnable이 오직 하나의 추상 메서드 run을 정의하는 함수형 인터페이스이다.
Runnable r1 = () -> System.out.println("Hello World 1"); //람다 사용
Runnable r2 = new Runnable() { //익명 클래스 사용
@Override
public void run() {
System.out.println("Hello World 2");
}
};
process(r1);
process(r2);
process(()->System.out.println("Hello World 3"));// 직접 전달된 람다 표현식으로 'Hello World 3'출력
3.2.2 함수 디스크립터
람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터 라고 한다. () -> void 표기는 파라미터 리스트가 없으며 void를 반환하는 함수를 의미한다.
람다와 메소드 호출
//정상적인 람다 표현식
process(()->System.out.println("This is awesome"));
위 코드에서는 중괄호를 사용하지 않았고 System.out.println은 void를 반환하므로 완벽한 표현식이 아닌 것처럼 보인다.
process(()->{System.out.println("This is awesome");});
결론적으로 이미 살펴본 것처럼 중괄호는 필요 없다. 한 개의 void 메소드 호출은 중괄호로 감쌀 필요가 없다.
퀴즈 3-3 어디에 람다를 사용할 수 있는가?
1) 람다 표현식 ()->{}의 시그니처는 ()->void며 Runnable의 추상 메서드 run의 시그니처와 일치하므로 유효한 람다 표현식이다.
execute(()->{});
public static void execute(Runnable r){
r.run();
}
2) fetch 매서드의 반환 형식은 Callable이다.
T를 String으로 대치했을 때 Callable 매서드의 시그니처는 ()->String이 된다.
public static Callable<String> fetch(){
return () -> "Tricky example ;-)";
}
3) 람다 표현식 (Apple a) -> a.getWeight()의 시그니처는 (Apple) -> Integer이므로 Predicate:(Apple) -> boolean의 test 메서드의 시그니처와 일치하지 않는다.
Predicate<Apple> p = (Apple a) -> a.getWeight();
3.3 람다 활용 : 실행 어라운드 패턴
실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 실행 어라운드 패턴 이라고 부른다.
public String processFile() throws IOException{
try(BufferedReader br =
new BufferedReader(new FileReader("./data.txt"))){
return br.readLine();
}
}
1단계 : 동작 파라미터화를 기억하라
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
2단계 : 함수형 인터페이스를 이용해서 동작 전달
함수형 인터페이스 자리에 람다를 사용할 수 있다. 따라서 BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
정의한 인터페이스를 processFile 매서드의 인수로 전달할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException{
...
}
3단계 : 동작 실행
processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
4단계 : 람다 전달
이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.
한 행 처리 코드
String oneLine = processFile((BufferedReader br) -> br.readLine());
두 행 처리 코드3.4 함수형 인터페이스 사용
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
함수형 인터페이스의 추상메서드를 함수 디스크립터 (function descriptor)라고 한다.
3.4.1 Predicate
java.util.function.Predicate 인터페이스는 test라는 추상 메서드를 정의하며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언을 반환한다.
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <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<>(Arrays.asList("java", "", "banna","","test"));
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings,nonEmptyStringPredicate);
3.4.2 Consumer
- java.util.function.Consumer 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <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)
);
3.4.3 Function
- java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply 정의
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
public <T, R> List map(List list, Function<T, R> f) {
List result = new ArrayList<>();
for (T t : list) {
result.add(f.apply(t));
}
return result;
}
List l = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length(); // <- Function의 apply 메서드를 구현하는 람다
);
기본형 특화
특화된 형식의 함수형 인터페이스
기본형을 참조형으로 변환하는 기능인 박싱 (Boxing), 참조형을 기본형으로 변환하는 언박싱 (Unboxing), 박싱과 언박싱이 자동으로 이루어지는 오토박싱(autoboxing)이라는 기능도 제공한다.
ex) int가 Integer로 박싱됨List<Integer> list = new ArrayList<>(); for(int i=300;i<400;i++){ list.add(i); }
자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱을 피할수 있도록 함수형 인터페이스 제공
다음 예제에서는 IntPredicate는 1000이라는 값을 박싱하지는 않지만 Predicate는 1000이라는 값을 Integer 객체로 박싱한다.
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;
oddNumbers.test(1000); // 거짓 (박싱)
3.5 형식 검사, 형식 추론, 제약
3.5.1 형식 검사
어떤 컨텍스트에서 기대되는 람다 표현식의 형식을 대상형식 이라고 한다.
List<Apple> heavierThan150gram = filter(inventory, (Apple apple) -> apple.getWeitht() > 150);
다음과 같은 순서로 형식 확인 과정이 진행된다.
- filter 메서드 선언 확인
- 두번째 파라미터로 Predicate 형식 (대상 형식)을 기대
- Predicate은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스
- test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터 묘사
- filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다.
3.5.2 같은 람다, 다른 함수형 인터페이스
- 대상 형식이라는 특징으로 같은 람다 표현식도 호환되는 추상 매서드가 여러개가 되면 다른 함수형 인터페이스로 사용될 수 있다.
Callable<Integer> c = () -> 42;
PrevillegedAction<Integer> p = () -> 42;
Comparator c1 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction c2 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction c3 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
3.5.3 형식 추론
- 컴파일러는 람다 표현식의 파라미터의 파라미터 형식에 접근할 수 있으므로 람다 문법에서 이를 생략할 수 있다.
List<Apple> greenApples =
filter(inventory, apple -> GREEN.equals(apple.getColor())); // 파라미터 a에는 형식을 지정하지 않았다.
여러 파라미터를 포함하는 표현식에서는 가독성 향상이 더 두드러진다.
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
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;
3.6 매서드 참조
- 기존 코드
inventory.sort((Apple a1,Apple a2)->a1.getWeight().compareTo(a2.getWeight()));
- 매서드 참조와 java.util.Comparator.comparing을 활용한 코드
inventory.sort(comparing(Apple::getWeight));
3.6.1 요약메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다.
람다 | 매서드 참조 단축 표현 |
---|---|
(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)(String s) | System out::println |
->this.isValidName(s) | this::isValidName |
매서드 참조를 만드는 방법
-> 매서드 참조는 세 가지 유형으로 구분할 수 있다.
- 정적 메서드 참조
-> Integer의 parseInt매서드는 Integer::parseInt로 표현할 수 있다. - 다양한 형식의 인스턴스 매서드 참조
->String의 length 메서드는 String::length로 표현할 수 있다. - 기존 객체의 인스턴스 메서드 참조
-> 예를 들어 Transcation 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transaction 객체에는 getValue메서드가 있다면, 이를 expensiveTransaction::getValue 라고 표현할 수 있습니다.
-> 비공개 메서드를 정의한 상황에서 유용하게 활용이 가능 하다
List<String> str = Array.asList("a", "b", "A", "B");
str.sort((s1, s2) -> s1.comparToIgnoreCase(s2));
//메서드 참조 사용
List str = Array.asList("a", "b", "A", "B");
str.sort(String::compareToIgnoreCase);
3.6.2 생성자 참조
ClassName::new 처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
Color(int,int,int) 처럼 인수가 세 개인 생성자를 사용하려면 직접 함수형 인터페이스를 생성해야 한다.
public interface TriFunction<T, U, V, R> {
R apply (T t, U u, V v);
}
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
3.7.1 1단계:코드 전달
static class AppleComparator implements Comparator<Apple> {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getWeight() - a2.getWeight();
}
}
inventory.sort(new AppleComparator());
3.7.2 2단계:익명 클래스 사용
한 번만 사용할 Comparator를 위 코드처럼 구현하는 것보다는 익명 클래스를 이용하는 것이 좋다.
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple a1, Apple a2) {
return a1.getWeight() - a2.getWeight();
}
});
3.7.3 3단계:람다 표현식 사용
inventory.sort((Apple a1, Apple a2)->a1.getWeight() - a2.getWeight());
자바 컴파일러는 람다 표현식이 사용된 콘텍스트를 활용해서 람다의 파라미터 형식을 추론한다.
inventory.sort((a1, a2)->a1.getWeight() - a2.getWeight());
코드를 다음처럼 간소화할 수 있다.
import static java.util.Comparator.comparing;
inventory.sort(comparing(apple -> apple.getWeight()));
3.7.4 4단계:매서드 참조 사용
inventory.sort(comparing(Apple::getWeight));
3.8 람다 표현식을 조합할 수 있는 유용한 매서드
예를 들어 Comparator,Function,Predicate같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 매서드를 제공한다.
3.8.1 Comparator 조합
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
역정렬
inventory.sort(java.util.Comparator.comparing(Apple::getWeight).reversed()); //무게를 내림차순으로 정렬
Comperator 연결
두 사과 비교 후 컬러가 같다면 컬러별로 사과 정렬
inventory.sort(comparing(Apple::getWeight) .reversed() .thenComparing(Apple::getColor));
3.8.2 Predicate 조합
- negate,and,or 세가지 메서드 제공
- negate를 이용한 빨간색이 아닌 사과
Predicate<Apple> notRedApple = redApple.negate(); //기존 프레디케이트 객체 redApple의 결과를 반전시킨 객체를 만든다.
- and를 이용한 빨간색이면서 무거운 사과
Predicate<Apple> redAndHeavyApple =
redApple.and(apple -> apple.getWeight() > 150);
or을 이용한 150 이상 또는 녹색 사과
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150) .or(apple -> GREEN.equals(apple.getColor()));
3.8.3 Function 조합
andThen, compose 두 가지 디폴트 메서드 제공
andThen 매서드는 주어진 함수를 먼저 적용한 결과를 다른 함수의 입력으로 전달하는 함수를 반환한다.
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1); // 4 반환
- compose 매서드는 인수로 주어진 함수를 먼저 실행한 다음에 그 결과를 외부 함수의 인수로 제공한다.
f.andThen(g)에서 andThen 대신에 compose를 사용하면 g(f(x))가 아니라 f(g(x))라는 수식이 된다.
Function<Integer, Integer> f = x -> x+1;
Function<Integer,Integer> g = x -> x*2;
Function<Integer,Integer> h = f.compose(g);
int result = h.apply(1); //3 반환
public static class Letter{
public static String addHeader(String text){
return "From Raoul, Mario and Alan: "+text;
}
public static String addFooter(String text){
return text + " Kind regards";
}
public static String checkSpelling(String text){
return text.replaceAll("labda","test");
}
}
Function<String,String> addHeader = Letter::addHeader;
Function<String,String> transformationPipeline =
addHeader.andThen(Letter::checkSpelling)
.andThen(Letter::addFooter);
3.10 마치며
- 람다 표현식은 익명 함수의 일종이다. 이름은 없지만, 파라미터 리스트, 바디, 반환 형식을 가지며 예외를 던질 수 있다.
- 람다 표현식으로 간결한 코드를 구현할 수 있다.
- 함수형 인터페이스는 하나의 추상 매서드만을 정의하는 이인터페이스다.
- 함수형 인터페이스를 기대하는 곳에서만 람다 표현식을 사용할 수 있다.
- 람다 표현식을 이용해서 함수형 인터페이스의 추상 매서드를 즉석으로 제공할 수 있으며 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급된다.
- java.util.function 패키지는
등을 포함해서 자주 사용하는 다양한 함수형 인터페이스를 제공한다.Predicate<T>, Function<T,R>, Supplier<T>, BinaryOperator<T>