람다 표현식은 메소드로 전달할 수 있는 익명 함수를 단순화한 것이다.
익명
보통의 메소드와는 달리 이름이 없으므로 익명이라고 표현한다. 또한 구현해야 할 코드에 대한 걱정거리가 줄어든다.
함수
람다는 메소드 처럼 특정 클래스에 종속이 되지 않으므로 함수라고 칭한다. 하지만 메소드 처럼의 파라미터 리스트, 바디, 반환 형식, 가능한 예외 리스트를 포함한다.
전달
람다 표현식을 메소드 인수로 전달하거나 변수로 저장할 수 있다.
간결성
익명 클래스 처럼 많은 자질구레한 코드를 구현할 의무는 없다.
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
이렇게 두 사과에 대하여 무게를 비교하는 가존 코드를 람다를 사용을 하면
Comparator<Apple> byWeight =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
이렇게 기존의 코드에 비하여 훨씬 간단하게 작성이 가능하다.
Comparator<Apple> byWeight =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
이 코드를 보면
->을 기준으로 세 부분으로 이루어 진다.
(Apple a1, Apple a2)->a1.getWeight().compareTo(a2.getWeight());람다 기본 문법
(parameters) -> expression
(parameters) -> { statements; }
유효한 람다 표현식
(String s) -> s.length() (Apple a) -> a.getWeight() > 150 (int x, int y) -> { System.out.println("~~"); } () -> 42 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
람다를 필터 메소드에서 사용이 가능하다. 추가적으로 함수형 인터페이스라는 문맥에서 람다 표현식을 사용할 수 있다.
함수형 인터페이스는 정확히 하나의 추상 메소드를 지정하는 인터페이스다.
자바API의 함수형 인터페이스로는 Runnable, Comparator등이 있다.
// java.lang.Runnable
public interface Runnable {
void run();
}
// java.util.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
// java.util.concurrent.Callable
public interface Callable<V> {
V call() throws Exception;
}
다음 인터페이스 중 어느것이 함수형 인터페이스인가?
public interface Adder { int add(int a, int b); } public interface SmartAdder extends Adder { int add(double a, double b); } public interface Nothing
이러한 질문이 있을 때 정답은 Adder만이 함수형 인터페이스다.
SmartAdder는 두 추상 add메소드(Adder 상속)를 포함하므로 함수형 인터페이스가 아니다.
마지막으로 Nothing은 아무런 추상 메소드가 없기에 함수형 인터페이스가 아니다.
그러면 이러한 함수형 인터페이스로 무엇을 할 수 있는가?
람다 표현식으로 함수형 인터페이스의 추상 메소드를 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다. 함수형 인터페이스보다는 덜 깔끔하지만 익명 내부 클래스로도 같은 기능을 구현할 수 있다.
함수형 인터페이스의 추상 메소드 시그니처는 람다 표현식의 시그니처를 가리킨다. 람다 표현식의 시그니처를 서술하는 메소드를 함수 디스크립터라고 말 한다.
예를 들어서 Runnable인터페이스의 유일한 추상 메소드 run은 인수와 반환값이 없으므로 Runnable 인터페이스는 인수와 반환값이 없는 시그니처로 생각하면 된다.
(Apple, Apple) -> int
위 코드는 2개의 Apple를 인수로 받아 int를 반환하는 함수를 가리킨다.
람다와 동작 파라미터화로 유연하고 간결한 코드를 구현하는데 있어 도움을 주는 실용적인 예제를 보면 자원 처리에 사용하는 순환 패턴은 자원을 열고, 동작을 한 다음에 자원을 닫는 순서로 구성되어 있다.
즉 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다. 이와 같은 형식의 코드를 실행 어라운드 패턴이라고 한다.

public String processFile() throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))) {
return br.readline(); // <- 실제 필요한 작업을 수행하는 행
}
}
위 예제 코드는 자바 7에 추가된 try-with-resources 구문이 사용되었다. 아를 사용하면 자원을 명시적으로 닫을 필요가 없으므로 간결한 코드를 구현하는데 도움을 준다.
processFile 메소드가 한 번에 두 행을 읽게 하려면 BufferedReader를 인수로 받아 String을 반환하는 람다가 필요하다.
아래의 코드는 BufferedReader에서 한 번에 두 행을 출력하는 코드다.
String result = processFile((BufferedReader br) ->
br.readLine() + br.readLine());
함수형 인터페이스 자리에 람다를 사용할 수 있다. 그렇기에 BufferedReader -> String과 IOException을 던질 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
정의한 인터페이스를 processFile 메서드의 인수로 전달
public String processFile(BufferedReaderProcessor p) throws IOException{
...
}
BufferedReaderProcessor에 정의된 메소드의 시그니처 (BufferedReader -> String)와 일치하는 람다를 전달할 수 있다.
processFile 바디 내에서 BufferedReaderProcessor 객체의 process를 호출할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.
// 한 행을 처리
String oneLine = processFile((BufferedReader br) -> br.readLine());
// 두 행을 처리
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
이렇게 함수형 인터페이스를 이용하여 람다를 전달하는 방법을 이야기 했다.
java.util.function.Predicate<T> 인터페이스는 test라는 추상 메소드를 정의하며 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))
resuls.add(t);
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listsOfStrings, nonEmptyStringPredicate);
java.util.function.Consumer<T> 인터페이스는 제네릭 형식 T 객체를 받아서 void를 반환하는 accept라는 추상 메서드를 정의한다.
T형식의 객체를 인수로 받아 어떠한 동작을 수행하고 싶을 때 Consumer인터페이스를 사용할 수 있다.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public void <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); // <- Consumer의 accept 메서드를 구현하는 람다
);
java.util.function.Function<T,R> 인터페이스는 제네릭 형식 T를 인수로 받아서 제네릭 형식 R 객체를 반환하는 추상 메서드 apply 정의한다.
입력을 출력으로 매핑하는 람다를 정의할 때 Function 인터페이스를 사용할 수 있다.
아래의 코드는 String리스트를 인수로 받아 각 String의 길이를 포함하는 Integrt 리스트로 변환하는 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;
}
List<Integer> l = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length(); // <- Function의 apply 메서드를 구현하는 람다
);
위 3개의 활용 예제를 보았다. 하지만 특화된 형식의 함수형 인터페이스도 있다.
자바의 모든 형식은 참조형 아니면 기본형에 해당한다.
하지만 제네릭 파라미터에는 참조형만이 가능하다. 이는 제네릭 내부 구현 때문에 어쩔 수 없다.
자바에서 기본형을 참조형으로 변환하는 기능을 제공한다. 이 기능은 박싱, 언박싱이라고 한다. 또한 개발에 있어 편리하게 자동으로 박싱, 언박싱이 이루어지는 오토박싱이라는 기능도 제공한다.
List<Integer> list = new ArrayList<>();
for (int i = 300, i < 400; i++) {
list.add(i);
}
위 코드에서 int가 Integer로 박싱이 된다. 하지만 이런 변환 과정은 비용이 소모가 된다.
박싱한 값은 기본형을 감싸는 래퍼며 힙에 저장된다. 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.
자바 8에서는 기본형을 입출력으로 사용하는 상황에서 오토박싱 동작을 피할 수 있도록 특별한 버전의 함수형 인터페이스를 제공한다.
public interface InPredicate {
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);
위 코드는 IntPredicate는 100이라는 값을 박싱하지 않지만, Predcate<Integer>는 1000이라는 값을 Integer객체로 박싱한다.