Java8에서 도입된 람다표현식은 추상메서드가 1개인 인터페이스를 사용할 때 익명클래스를 사용하지 않고 코드의 구현부만 넘길 수 있개 해주는 문법으로써 코드의 간결성을 높일 수 있다.
다음 상황을 가정해보자.
1. 유저 리스트를 정렬하고 싶다.
2. 나이순으로, 이름순으로 각각 한번씩!
코드로 이렇게 작성할 수 있을 거 같다.
List<User> users = ...
Collections.sort(users, new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
return o1.getName().compareTo(o2.getName());
}
});
// Do something...
Collections.sort(users, new Comparator<User>() {
@Override
public int compare(User o1, User o2) {
return Integer.compare(o1.getAge(), o2.getAge());
}
});
코드에서 Comparator 인터페이스의 익명클래스 두 개를 만들어 인스턴스를 생성했다.
그런데 구현해야할 추상 메서드가 오직 하나인 인터페이스를 사용하는데에는 이 정도도 과해보인다. 정렬 기준이 추가될때 마다 저 코드가 반복된다고 생각하면 더 그렇다. 람다는 이런 번거로운 작업을 좀 더 간소화해준다. 람다를 사용하면 익명클래스를 사용하지 않고도 함수(Function)를 파라미터로 넘길 수 있다. 클래스를 사용하지 않기 때문에 메서드가 아닌 함수라는 용어를 사용한다.
람다를 사용한 코드는 다음과 같은 형태이다.
List<User> users = ...
Collections.sort(users, (o1, o2)->{
return o1.getName().compareTo(o2.getName());
});
// Do something...
Collections.sort(users, (o1, o2)->{
return Integer.compare(o1.getAge(), o2.getAge());
});
(파라미터) -> {구현부}의 형태를 띄고 있다.
그럼 람다로 넘길 수 있는 지 아닌지는 어디서 결정되는 걸까?
Comparator 인터페이스를 살펴보면 알 수 있다.
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
// Others...
}
구현해야 할 추상메서드가 1개인 인터페이스에 @FunctionalInterface 어노테이션을 붙여주면 람다식으로 사용할 수 있다. 추상메서드는 1개여야 하지만 default나 static 메서드에는 제한이 없다.
참고로 Comparator 인터페이스를 다시보면 헷갈릴 수 있는 부분이 있는데
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
// Others...
}
default, static이 붙은 걸 제외해도 equals 메서드를 추상메서드로 제공하고 있다는 점이다. 즉 2개의 추상메서드가 있다. 그렇다면 내가 넘긴 람다식이 compare에 매칭되는지 equals에 매칭되는지 어떻게 알겠는가? 여기에 대해서는 다음과 같이 말하고 있다.
Functional Interface의 정의에서 Object 클래스의 public 메서드는 제외한다.
equals는 이미 모든클래스의 조상인 Object클래스에 이미 구현되어 있으므로 이런 경우에는 추상메서드로써 남겨놔도 FunctionalInterface로 사용할 수 있게끔 해서 편의성을 가져가는 것이 아닐까 한다.
Anoymous Inner Class와의 차이
public class Main {
public static void main(String[] args) {
List<Integer> iList = new ArrayList<>();
iList.sort(new Comparator<Integer>() {
@Override
public int compare(Integer integer, Integer t1) {
return 0;
}
});
}
}
위의 코드를 컴파일하면 Main$1.class, Main.class 두 개의 class 파일이 생성된다.
public class Main {
public static void main(String[] args) {
List<Integer> iList = new ArrayList<>();
iList.sort((i1, i2) -> i2 - i1);
}
}
람다로 작성 후 컴파일하면 Main.class만 생성된다. 별도의 클래스를 생성하지 않고 outer class의 private static 메서드로 컴파일된다.
어떻게 컴파일되는지 좀 더 자세하게 알아보고 싶다면 아래 글을 읽어보는 것도 좋을 것이다.
https://dzone.com/articles/how-lambdas-and-anonymous-inner-classesaic-work
람다에 대한 딥다이브를 원한다면 참고할 수 있는 오라클 강의도 있다.
https://www.infoq.com/presentations/lambda-invokedynamic/