람다 표현식을 어떻게 만드는지, 어떻게 사용하는지, 어떻게 코드를 간결하게 만들 수 있는지 알아보자.
람다가 자바 8 이전의 자바로 할 수 없었던 일을 제공하는 것은 아니다. 다만 동작 파라미터를 이용할 때 익명 클래스 등 판에 박힌 코드를 구현할 필요가 없다. 즉 코드가 간결하고 유연해진다.
Predicate
@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)) {
result.add(t);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public <T> void forEach(List<T> list, Comsumer<T> c) P
for(T t: list) {
c.accept(t);
}
}
forEach(
Arrays.asList(1,2,3,4,5),
(Integer i) -> System.out.println(i)
);
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("lamdas", "in", "action"),
(String s) -> s.length()
);
함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉 예외를 던지는 람다 표현식을 만들려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try/catch 블록으로 감싸야 한다.
자원 처리에 사용하는 순환 패턴은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어진다. 설정과 정리 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는데 이와 같은 형식의 코드를 실행 어라운드 패턴이라고 부른다.
다음 예제는 한 행을 읽는 코드다.
public String processFile() thorws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까?
기존의 설정, 정리 과정은 재사용하고 processFile 메서드만 다른 동작을 수행하도록 명령할 수 있다면 좋을 것이다.
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public String processFile(BufferedReaderProcessor p) throws IOException {
}
public String processFile(BufferedReaderProcessor p) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}
한 행을 처리하는 코드
String oneLine = processFile((BufferedReader br) -> br.readLine());
두 행을 처리하는 코드
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
대상 형식: 어떤 콘텍스트에서 기대되는 람다 표현식의 형식
람다가 사용되는 콘텍스트를 이용해서 람다의 형식을 추론할 수 있습니다.
람다 표현식은 대상 형식의 형식을 알 수 있기 때문에 람다 파리미터의 형식을 추론할 수 있습니다. 상황에 따라 명시적으로 형식을 포함하는 것이 좋을 때도 있고 형식을 배제하는 것이 가독성을 향상시킬 때도 있습니다. 어떤 방법이 좋은지 정해진 규칙은 없습니다.
왜 지역 변수에 이런 제약이 필요할까? 우선 내부적으로 인스턴스 변수와 지역 변수는 태생부터 다르다. 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치한다. 람다에서 지역 변수에 바로 접근할 수 있다는 가정하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공한다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 할당해야 한다는 제약이 생기는 것이다.
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a1) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
//형식 추론에 의해 다음과 같이 더 줄일 수 있습니다.
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
//Comparator의 정적 메서드 사용
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
inventory.sort(comparing(apple -> apple.getWeight()));
inventory.sort(comparing(Apple::getWeight());