람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화 한 것.
람다 표현식은 파라미터 + 화살표 + 바디
로 이루어진다.
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
(String s) -> s.length() // String 형식의 파라미터를 받으며, int 반환 람다 표현식에는 return 문이 함축되어있어서 명시하지 않아도 된다.
(Apple a) -> a.getWeight() > 150 // Apple 형식의 파라미터를 받으며, boolean 반환
(int x, int y) -> {
System.out.println("Result : ");
System.out.println(x+y);
}
// int 형식의 파라미터 두 개를 가지며 리턴 값이 없다.
() -> 42 // 파라미터가 없으며 42를 반환한다.
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
- 람다의 기본 문법
(parameteres) -> expression
(parameteres) -> { statements; }
람다에서 표현식이 하나인데 return문을 사용하는 경우 블록으로 감싸야 한다.
// x
(String s) -> return "Alan" + i;
// o
(String s) -> {return "Alan" + i;}
함수형 인터페이스는 오직 하나의 추상메서드만 지정하는 인터페이스
이다. 예를들어 Predicate<T>
, 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은 인수와 반환값이 없으므로 void 반환 () -> void
로 표기할 수 있다.
람다 표현식은 함수형 인터페이스
를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있다.
그리고 void 반환이 한 개 인 경우에는 중괄호로 감쌀 필요가 없다.
@FunctionallInterface는 함수형 인터페이스에 붙는 어노테이션이다. (즉, 함수형 인터페이스를 가리키는 어노테이션)
@FunctionallInterface로 선언했지만 실제로 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다. 예를 들어 추상 메서드가 한 개 이상이라면
Multiple nonoverriding abstract methods found in interface Foo
같은 에러가 발생할 수 있다.
자원 처리(예를 들면 데이터베이스의 파일 처리)에 사용하는 순환 패턴(recurrent pattern)
은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어 진다. 설정(setup)과 정리(cleanup) 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다.
실행 어라운드 패턴(execute around pattern)
public String processFile() throws IOException {
try ( BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
try-with-resources 구문을 사용하여 자원을 닫지 않아도 된다.
현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있다. 만약 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까?
바로 processFile()을 동작 파라미터화 시키는 것이다.
- 한 번에 두 줄 출력
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
함수형 인터페이스 자리에 람다를 사용할 수 있다. 따라서 BufferedReader -> String 과 IOException을 던질(throw) 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다. 이 인터페이스를 BufferedReaderProcessor라고 정의하자.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
// ...
}
이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader -> String)와 일치하는 람다를 전달할 수 있다.
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으며, 전달된 코드는 함수형 인터페이스의 인스턴스로 전달된 코드와 같은 방식으로 처리한다.
public String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedREader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.
String oneLine = processFile((BufferedReaer br) -> br.readLine());
자바8 라이브러리 설계자들은 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공한다.
java.util.function.Predicate 인터페이스는 test 라는 추상 메서드를 정의히ㅏ며 test는 제네릭 형식 T의 객체를 인수로 받아 불리언으로 반환한다. 따로 정의할 필요없이 바로 사용할 수 있다.
아래 예제처럼 String 객체를 인수로 받는 람다를 정의할 수 있다.
@FunctionalInterface
public interface Predicat<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;
}
Predicat<String> nonEmptyStrinPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Predicate 인터페이스의 자바독 명세(Javadoc specification)를 보면 and나 or 같은 메서드도 있음을 알 수 있다.
java.util.function.Consumer 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반화하는 accept라는 추상메서드를 정의한다.
T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 Consumer 인터페이스를 사용할 수 있다.
예를 들어 Integer 리스트를 인수로 받아서
각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 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)
);
java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반화하는 추상 메서드 apply를 정의한다.
입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 활용할 수 있다.(예를 들면 사과의 무게 정보를 추출하거나 문자열을 길이와 매핑).
다음은 String 리스트를 인수로 받아 각 String의 길이를 포함하는 Integer 리스트로 변환하는 map 메서드를 정의하는 예제이다.
@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;
}
// [7,2,6]
List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), (Strin s) -> s.length()); // Function의 apply 메서드를 구현하는 람다
- 함수형 인터페이스를 선언한다.(사실 java.util.Function.* 에 가보면 Predicate, Consumer, Function 등 함수형 인터페이스가 있다.)
- 이 함수형 인터페이스를 동작(전략) 파라미터화 시켜서(즉, 함수형 인터페이스를 매개변수로 받는) 자신이 원하는 로직을 처리하는 메서드를 만든다.
- 해당 메서드를 호출할때 파라미터로 람다를 넘길 수 있다.(여기서 람다는, 함수형 인터페이스의 메서드를 구현하는 역할)
자바의 모든 형식은 참조형(reference type) (Byte, Integer, Object, List) 아니면 기본형(primitive type)에 해당한다. 자바에서는 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능을 박싱(boxing)
이라고한다. 반대로 참조형을 기본형으로 변환하는 동작을 언박싱(unboxing)
이라고 한다.
그리고 이러한 동작이 자동으로 이루어지는 오토박싱(autoboxing)
이라는 기능도 제공한다.
- autoboxing
List<Integer> list = new ArrayList<>();
for(int k=0; k<500; k++) {
list.add(k); // int > Integer
}
하지만 이런 변환 과정은 비용이 소모된다. 박싱한 값은 기본형을 감싸는 래퍼이며 힙에 저장된다. 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.
람다 표현식을 처음 설명할 때 람다로 함수형 인터페이스의 인스턴스를 마들 수 있다고 언급했다. 람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다. 따라서 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다.
람다가 사용되는 콘텍스트(context)
를 이용해서 람다의 형식(type)
을 추론할 수 있다. 어떤 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식(target type)
이라고 한다.
자바 컴파일러는 람다 표현식이 사용된 콘테긋트(대상 형식)를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론한다.
대상형식 = () -> ...;
- Comparator 객체를 만드는 코드
// 형식 추론 하지 않음
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 형식 추론 함
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
지금까지 살펴본 모든 람다 표현식은 인수를 자신의 바디 안에서만 사용했다. 하지만 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수
(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다. 이와 같은 동작을 람다 캡처링
이라고 한다.
다음은 portNumber 변수를 캡처하는 람다 예제이다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
하지만 자유 변수에도 제약이 있다. 람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록) 할 수 있다.
하지만 그러러면 지역 변수는 명시적으로 final로 선언되어 있어야 하거나, 실질적으로 final로 선언된 변수와 똑같이 사용되어야 한다.
아래는 portNumber에 값을 두 번 할당하므로 컴파일 할 수 없는 코드이다.
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
원칙적으로 클로저란 함수의 비지역 변수를 자유롭게 참조할 수 있는 함수의 인스턴스를 가리킨다. 예를 들어 클로저를 다른 함수의 인수로 전달 할 수 있다. 클로저는 클로저 외부에 정의된 변수의 값에 접근하고, 값을 바꿀 수 있다. 자바 8의 람다와 익명 클래스는 클로저와 비슷한 도작을 수행한다. 람다와 익명 클래스 모두 메서드의 인수로 전달 될 수 있으며, 자신의 외부 영역의 변수에 접근할 수 있다. 다만 람다와 익명 클래스는 람다가 정의된 메서드의 지역 변수의 값은 바꿀 수 없다. 람다가 정의된 메서드의 지역 변숫값은 final 변수여야 한다. 덕분에 람다는 벼누가 아닌 값에 국한되어 어떤 동작을 수행한다는 사실이 명확해진다. 이전에도 설명한 것 처럼 지역 변숫값은 스택에 존재하므로 자신을 정의한 스레드와 생존을 같이 해야 하며 따라서 지역 변수는 final 이어야 한다. 가변 지역 변수를 새로운 스레드에서 캡처할 수 있다면, 안전하지 않은 동작을 수행할 가능성이 생긴다.(인스턴스 변수는 스레드가 공유하는 힙에 존재하므로 특별한 제약이 없다.)
메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다. 때로는 람다 표현식 보다 메서드 참조를 사용하는 것이 더 가독성이 좋으며 자연스러울 수 있다.
다음은 기존 코드다.
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
다음은 메서드 참조와 java.util.Comparaptor.comparing을 활용한 코드다.
inventory.sort(comparing(Apple::getWeight));
메서드 참조는 특정 메서드만을 호출하는 람다의 축약형이라고 생각할 수 있다. 메서드 명을 참조하기 때문에 가독성을 높일 수 있다.
메서드를 참조하는
방법은 메서드명 앞에 구분자(::)를 붙이면 된다.
- Example
// (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)
System.out::println
// (String s) -> this.isValidName(s)
this::isValidName
메서드 참조는 세 가지 유형으로 구분할 수 있다.
-
정적 메서드 참조
- 예를 들어 Integer의 parseInt 메서드는
Integer::parseInt
로 표현할 수 있다.
- 예를 들어 Integer의 parseInt 메서드는
-
다양한 형식의 인스턴스 메서드 참조
- 예를 들어 Strin의 length 메서드는
String::length
로 표현할 수 있다.
- 예를 들어 Strin의 length 메서드는
-
기존 객체의 인스턴스 메서드 참조
- 예를 들어 Transaction 객체를 할당받은 expensiveTransaction 지역 변수가 있고, Transacation 객체에는 getValue 메서드가 있다면
- 이를,
expensiveTransaction::getValue
로 표현할 수 있다.
-
세 가지 종류의 람다 표현식을 메서드 참조로 바꾸는 방법
- (args) -> ClassName.staticMethod(args)
- ClassName::staticMethod
- (arg0, rest) -> arg0.instanceMethod(rest) ... arg0은 className 형식
- ClassName::instanceMethod
- (args) -> expr.oinstanceMethod(args)
- expr::instanceMethod
- (args) -> ClassName.staticMethod(args)
위에서 설명한 기법을 이용하면 람다 표현식을 메서드 참조를 사용해서 다음 처럼 줄일 수 있다.
List<String> str = Arrays.asList("a", "b", "A", "B");
str.sort(String::compareToIgnoreCae);
즉, 메서드 참조는 콘텍스트의 형식과 일치해야 한다.
ClassName::new
처럼 클래스명과 new 키워드를 이용해서 기존 생성자의 참조를 만들 수 있다.
// Supplier<Apple> c1 = () -> new Apple();
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
- Quiz
Color(int, int, int) 처럼 인수가 세 개인 생성자의 생성자 참조를 사용하려면 어떻게 해야할까?
- 정답
생성자 참조 문법은 ClassName::new 이므로 Color 생성자의 참조는 Color::new가 된다.
하지만 이를 이용하려면 생성자 참조와 일치하는 시그처를 갖는 함수형 인터페이스
가 필요하다.
아래와 같이 만들 수 있다.
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
아래 처럼 생성자 참조를 사용할 수 있다.
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
Comparator, Function, Predicate 같은 함수형 인터페이스는 람다 표현식을 조합할 수 있도록 유틸리티 메서드를 제공한다.
이 메서드가 바로 디폴트 메서드(default method)
이다.
-
Comparator 조합
-
역정렬
사과의 무게를 내림차순으로 정렬하고 싶다면 어덯게 해야 할까? Comparator 인터페이스 자체에서 주어진 비교자의 순서를 뒤바꾸는 reverse라는 디폴트 메서드를 제공한다.
inventory.sort(comparing(Apple::getWeight).reversed());
만약 무게가 같은 사과가 존재한다면 어떻게 해야 할까? 무게가 같은경우 국가별로 정렬할 수 도 있다.
inventory.sort(comapring(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
Predicate 조합
Predicate 인터페이슨느 복잡한 프레디케이트를 만들 수 있도록 negate, and, or
세 가지 메서드를 제공한다.
- 빨간색이 아닌 사과
Predicate<Apple> notRedApple = redApple.negate();
- and 를 사용하여 빨간색이면서 무거운 사과 선택
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
- or 를 사용하여 빨간색이면서 무거운 사과 또는 그냥 녹색 사과
Predicate<Apple> readAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));
Function 조합
Function 인터페이스는 andThen
과 compose
두 가지 디폴트 메서드를 제공한다.
예를 들어 숫자를 증가 시키는 x -> x + 1 이라는 f 함수가 있고, 숫자에 2를 곱하는 g 함수가 있다고 가정하자.
- andThen
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // g(f(x))
int result = h.apply(1); // 4를 반환
- compose
Function<Integer, Integer> h = f.compose(g); // f(g(x))
-
람다는
파라미터 -> (화살표) 바디
로 이루어진다.- (parameters) -> expression;
- (parameters) -> { statements; }
-
많은 디폴트 메서드가 존재하더라도
추상 메서드
가 하나이면 함수형 인터페이스이다. -
람다 표현식의 시그니처를 서술하는 메서드를
함수 디스크립터
라고 한다.