thenullpage.com
정렬은 이 기준으로 해라, 이 조건에 맞는 것만 남겨라. 값이 아니라 할 일을 넘겨야 하는 자리가 자주 생긴다. 자바에는 메서드만 떼어 넘기는 방법이 없어서 오랫동안 익명 클래스로 감싸서 넘겼다. 넘기고 싶은 건 한 줄인데 껍데기가 다섯 줄이다. 그 껍데기를 걷어낸 문법이 람다다.
여섯 줄이 한 줄이 된다. 이름 길이 순으로 정렬하는 코드를 두 방식으로 적어 본다.
List<String> names = new ArrayList<>(List.of("최운영팀장", "박씨", "김개발"));
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [박씨, 김개발, 최운영팀장]
둘은 완전히 같은 일을 한다. 익명 클래스에서 내용이 담긴 줄은 return 한 줄뿐이고 나머지는 자바가 이미 아는 정보다. 인터페이스가 무엇인지는 sort가 받는 타입으로 정해져 있고, 메서드가 하나뿐이니 이름도 정해져 있고, 매개변수 타입도 거기서 나온다. 람다는 그 정보를 전부 지우고 매개변수와 몸통만 남긴 표기다.
(a, b) -> a.length() - b.length() // 매개변수 둘
s -> s.length() // 하나면 괄호도 뺀다
() -> System.out.println("실행") // 없으면 빈 괄호
s -> { int len = s.length(); return len * 2; } // 중괄호를 열면 return
화살표 오른쪽이 한 줄이면 그 값이 그대로 반환값이라 return을 적지 않는다.
람다가 들어갈 자리는 추상 메서드 하나짜리 인터페이스다. 직접 만들어 보면 정체가 드러난다.
@FunctionalInterface
interface Calc {
int apply(int a, int b);
}
Calc add = (a, b) -> a + b;
Calc mul = (a, b) -> a * b;
System.out.println(add.apply(3, 4)); // 7
System.out.println(mul.apply(3, 4)); // 12
람다는 그 인터페이스를 구현한 객체가 된다. add에 담긴 것은 식이 아니라 Calc 한 개다. apply를 부르는 순간 화살표 오른쪽이 실행된다. @FunctionalInterface는 추상 메서드가 둘 이상이면 컴파일을 막아 주는 표시다. 필수는 아니지만 붙여 두면 실수를 그 자리에서 잡아 준다.
자주 쓰는 모양은 이미 만들어져 있다. java.util.function 패키지에 들어 있어 따로 만들 일이 거의 없다.
Function<String, Integer> len = s -> s.length();
Predicate<String> isLong = s -> s.length() > 3;
Consumer<String> show = s -> System.out.println("이름: " + s);
Supplier<String> maker = () -> "김개발";
System.out.println(len.apply("김개발")); // 3
System.out.println(isLong.test("최운영팀장")); // true
show.accept("박씨"); // 이름: 박씨
System.out.println(maker.get()); // 김개발
넷을 가르는 기준은 받는 값과 주는 값이다. Function은 받아서 돌려주고, Predicate는 받아서 true나 false를 돌려주고, Consumer는 받기만 하고 안 돌려주고, Supplier는 받지 않고 돌려주기만 한다. 부르는 메서드 이름이 apply, test, accept, get으로 다르다는 것만 외우면 된다. 두 개를 받는 자리는 BiFunction을 쓴다. 조건 여럿은 isLong.and(other)나 isLong.negate()처럼 묶고 뒤집을 수 있다.
메서드 하나만 부르는 람다는 더 줄인다. 콜론 두 개짜리 표기를 메서드 참조라고 한다.
Function<String, Integer> a = s -> s.length();
Function<String, Integer> b = String::length;
names.forEach(s -> System.out.println(s));
names.forEach(System.out::println);
Supplier<List<String>> listMaker = ArrayList::new;
System.out.println(listMaker.get()); // []
String::length는 받은 문자열의 length를 부른다는 뜻, System.out::println은 정해진 객체의 println을 부른다는 뜻, ArrayList::new는 생성자를 부른다는 뜻이다. 받은 값을 그대로 넘길 때만 줄일 수 있어서 s -> s.length() * 2처럼 결과를 더 가공하면 람다로 둬야 한다.
람다 안에서 밖의 지역 변수는 읽기만 된다. 고치려 들면 컴파일이 막힌다.
int bonus = 10;
Function<Integer, Integer> plus = x -> x + bonus;
System.out.println(plus.apply(5)); // 15
int count = 0;
Runnable r = () -> count++;
// 컴파일 에러: local variables referenced from a lambda
// expression must be final or effectively final
final을 적지 않아도 한 번 넣고 안 고치면 그냥 쓸 수 있다. 람다는 만들어진 자리를 떠나 한참 뒤 다른 곳에서 실행될 수 있는데 그때 원래 지역 변수는 이미 사라진 뒤다. 그래서 값을 복사해 들고 가고, 복사본과 원본이 어긋나지 않도록 변경을 아예 막는다.
표준 라이브러리가 이미 람다를 받는다. 새로 배운 문법을 쓸 자리는 이미 널려 있다.
List<String> list = new ArrayList<>(List.of("최운영팀장", "박씨", "김개발"));
list.removeIf(s -> s.length() < 3);
System.out.println(list); // [최운영팀장, 김개발]
Map<String, Integer> counter = new HashMap<>();
counter.merge("김개발", 1, (old, add) -> old + add);
counter.merge("김개발", 1, (old, add) -> old + add);
System.out.println(counter); // {김개발=2}
removeIf는 Predicate를 받아 조건에 맞는 것을 지운다. Iterator를 꺼내 돌리던 일이 한 줄이 된다. merge는 키가 없으면 두 번째 값을 넣고, 있으면 기존 값과 새 값을 람다에 넘겨 나온 값으로 바꾼다. 키가 있는지 검사하는 if가 사라진다.
정리하면 셋이다. 람다는 추상 메서드가 하나뿐인 인터페이스 자리에 들어가 그 인터페이스를 구현한 객체가 된다는 것, Function과 Predicate와 Consumer와 Supplier는 받는 값과 주는 값으로 갈린다는 것, 밖의 지역 변수는 값이 변하지 않을 때만 갖다 쓸 수 있다는 것이다. Calc를 직접 쳐 놓고 뺄셈과 나눗셈 람다를 하나씩 더 만들어 보면 손에 붙는다.