람다식이란 자바 8부터 등장한 기능이다. 람다식은 자세히는 모르고 그냥 이런식으로 사용할 수 있구나 하고 사용하는 사람들이 있을 것이다. 나도 그랬고 그래도 별로 상관 없을 것 같았다. 하지만 람다식은 많은 곳에서 사용한다. 특히 함수형 인터페이스와 가까워서 여러가지 간단한 구현이 가능하도록 해준다. 그래서 람다식에 대해서 자세히 알 필요가 있어서 공부하게 되었다.
람다식이란?
public class Lambda {
public void lambda() {
// 람다식
MyFunc myFunc1 = i -> { return; };
myFunc1.func(1);
// 익명 클래스
MyFunc myFunc2 = new MyFunc() {
@Override
public void func(int a) {
return;
}
};
myFunc2.func(1);
}
// 함수형 인터페이스
@FunctionalInterface
private interface MyFunc {
public abstract void func(int a);
}
}
람다식은 메서드를 식으로 표현하는 기능이다.
메서드에서 접근제한자, 반환 타입, 메서드명을 제외하고 괄호 오른쪽에 -> 를 붙여 만들 수 있다.
함수형 인터페이스는 하나의 추상 메서드를 가질 수 있는 인터페이스이다. 이를 바로 사용하고자 한다면 방법이 크게 두가지가 있다. 람다식과 익명 클래스를 사용하는 방법이다. 위의 경우는 func()라는 메서드를 구현해야 하는데 매개변수가 int형이고 리턴값이 없는 메서드를 구현하면 된다. 이를 람다식과 익명 클래스를 통해 구현할 수 있다.
여기서 잠깐 식(expression)과 문장(statement)의 차이점을 살펴보자면
식은 값으로 평가될 수 있는 코드를 말한다. 그래서 다른 식에 포함될 수 있고, 문장의 일부가 될 수도 있다.
문장은 프로그램의 실행 단위이다. 특정한 동작을 수행하고 ;(세미콜론)으로 끝난다.
위의 코드를 보면 람다식과 익명 클래스의 차이점을 볼 수 있다.
람다식은 식이기 때문에 홀로 사용할 수 없다. 다른 식에 대입하거나 문장에 속해있어야 한다. 그리고 일회성이기 때문에 재사용하기 위해서는 이를 어딘가에 저장해야 한다. 이는 익명 클래스도 동일하다.
위의 경우에서는 함수형 인터페이스의 func()의 구현을 람다식으로 하여 MyFunc라는 인터페이스의 구현체를 myFunc1이라는 변수에 할당해주었다. 이후부터는 해당 변수를 이용하여 func() 메서드를 호출할 수 있다.
두 방법은 사실상 똑같은 기능을 수행한다. 하지만 람다식은 익명 클래스를 사용하는 대신 간단하게 메서드를 재정의 할 수 있다.
작성 방법
public class Lambda {
public static void lambda1() {
// 모두 작성
MyFuncOne myFunc1 = (int a) -> { return String.valueOf(a); };
// 타입 제거
MyFuncOne myFunc2 = (a) -> { return String.valueOf(a); };
// 소괄호 제거
MyFuncOne myFunc3 = a -> { return String.valueOf(a); };
// 중괄호 & return 제거
MyFuncOne myFunc4 = a -> String.valueOf(a);
// 메서드 참조 사용
MyFuncOne myFunc5 = String::valueOf;
}
@FunctionalInterface
private interface MyFuncOne {
public abstract String func(int a);
}
}
람다식은 여러가지 방법으로 작성할 수 있다. 큰 틀은 같지만 세세하게 사용하는 문법이 다르다.
위의 코드를 통해 매개변수가 하나이고 리턴값이 있는 경우에 대해 알아보자.
작성 가능한 규칙은 다음과 같다.
- 타입 추론이 가능한 경우에 타입 생략 가능
- 매개변수가 하나이면 소괄호 생략 가능
- 리턴하는 문장만 있으면 중괄호 & return 생략 가능
- 메서드 참조 사용 가능
다른 것은 다 알기 쉬운데 메서드 참조는 뭘까?
만약 람다식에서 위의 경우처럼 하나의 메서드만 호출하는 경우 메서드를 참고하는 방식으로 사용할 수 있다.
클래스명::메서드명으로 참조할 수 있다.
단, 아무 메서드나 되는 것은 아니고 매개변수 타입, 개수, 순서, 리턴값이 같아야 한다. 위의 경우는 int형을 매개변수로 String을 반환하는 func()와 String의 valueOf()가 이에 해당하기 때문에 가능한 것이다.
확실히 굳이 func() 안에서 String::valueOf()을 호출하는 것보다 바로 String::valueOf()를 호출하는 것이 좋은 선택이다.
다음은 매개변수 개수가 2개 이상인 경우를 알아보자.
public class Lambda2 {
public static void lambda2() {
// 모두 작성
MyFuncTwo myFunc1 = (int a, String b) -> { return String.valueOf(a) + b; };
// 타입 제거
MyFuncTwo myFunc2 = (a, b) -> { return String.valueOf(a) + b; };
// 중괄호 & return 제거
MyFuncTwo myFunc3 = (int a, String b) -> String.valueOf(a) + b;
// 타입 & 중괄호 & return 제거
MyFuncTwo myFunc4 = (a, b) -> String.valueOf(a) + b;
}
@FunctionalInterface
private interface MyFuncTwo {
public abstract String func(int a, String b);
}
}
작성 가능한 규칙은 매개변수가 1개일 때와 대부분 같지만 다른 점은 매개변수가 2개 이상이면 소괄호를 생략할 수 없다.
참고로 return을 명시했을 때는 중괄호를 생략할 수 없다.
사용 예시
이제 람다식에 대해서 알겠지만 구체적으로 어느 곳에서 사용하는 것일까?
사용 가능한 곳은 매우 많이 있지만 그 중 자주 사용되는 예를 살펴보자. Comparator는 두 객체를 비교할 때 자주 사용되는 인터페이스이다. compare()를 개발자가 구현하여 임의로 비교를 할 수 있게 되어있다. 해당 인터페이스는 @FunctionalInterface가 붙어 있으므로 당연히 람다식도 사용 가능하다!
문자열을 담고 있는 배열에서 문자열의 첫 번째 글자 기준 오름차순으로, 글자가 같으면 두 번째 글자 기준 내림차순으로 정렬한다고 해보자.
public static void lambdaTest1() {
String[] arr = new String[]{"dc", "bh", "gi", "ki", "db"};
arr = Arrays.stream(arr)
.sorted(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return Character.compare(o1.charAt(0), o2.charAt(0));
}
}.thenComparing(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return Character.compare(o1.charAt(1), o2.charAt(1));
}
}))
.toArray(String[]::new);
for (String s : arr) System.out.printf("%s ", s);
}
먼저 익명 클래스를 이용해서 해결해보자. 비교를 이중으로 하려면 thenComparing()이라는 메서드를 추가로 사용해야 한다. 익명 클래스는 메서드를 오버라이드 하고 메서드 자체를 다시 적어주어야 한다. IDE의 자동완성 기능이 없으면 전부 자신이 작성해야 한다...
다음은 똑같은 코드를 람다식으로 바꿔보자.
public static void lambdaTest2() {
String[] arr = new String[]{"dc", "bh", "gi", "ki", "db"};
arr = Arrays.stream(arr)
.sorted(
((Comparator<String>)((s1, s2) -> Character.compare(s1.charAt(0), s2.charAt(0))))
.thenComparing((s1, s2) -> Character.compare(s2.charAt(1), s1.charAt(1)))
)
.toArray(String[]::new);
for (String s : arr) System.out.printf("%s ", s);
}
람다식으로 변경했다. 확실히 이전보다 작성할 내용도 줄었고 보기도 조금 편해졌다.
람다식은 타입을 지정하지 않고 그냥 사용할 수 있긴 하지만 위의 예시처럼 Comparator<String>이라고 지정해야 thenComparing() 메서드를 사용할 수 있다.
'공부 > Java' 카테고리의 다른 글
[Java] 동적 프록시 (Dynamic Proxy) (0) | 2024.07.23 |
---|---|
[Java][Spring] ThreadLocal 사용하기 (0) | 2024.07.13 |
[Java] 생성자 (Constructor) (0) | 2024.02.22 |
[Java] 날짜 및 시간 관련 클래스 (Date, Calendar, Timestamp, LocalDate 등) (0) | 2023.12.25 |
[Java] JDBC (Java Database Connectivity) (0) | 2023.12.22 |