람다를 이해하려면 익명 클래스를 먼저 알아둘 필요가 있다.
1️⃣ 별도의 클래스 파일을 만들지 않고 코드 내에서 일회성으로 정의해 사용한다.
2️⃣ 인터페이스, 클래스(일반, 추상)의 구현과 상속을 활용해 구현할 수 있다.
3️⃣ 람다에서는 인터페이스를 사용한 익명 클래스가 활용된다.
public interface Calculator {
int sum(int a, int b);
}
public class Main {
public static void main(String[] args) {
// 익명 클래스 활용
Calculator calculator1 = new Calculator() {
@Override
public int sum(int a, int b) {
return a + b;
}
};
int ret1 = calculator1.sum(1, 1);
System.out.println("ret1 = " + ret1); // 출력: ret1 = 2
}
}
람다는 익명 클래스를 더 간결하게 표현하는 문법이라고 생각하면 된다.
함수형 인터페이스를 통해서 구현하는 걸 권장한다.
함수형 인터페이스는 단 하나의 추상 메서드만 가지도록 강제하는 어노테이션이다.
람다식을 활용하면 컴파일 시점에 컴파일러가 람다 표현식을 보고 메서드를 추론하는 과정이 일어난다.
만약 아래와 같은 코드가 있다고 가정해보자.
public interface Calculator {
int sum(int a, int b);
int sum(int a, int b, int c); // 오버로딩으로 선언 가능, 모호성 발생
}
사용자가 어떤 메서드를 사용하려고 하는지 컴파일러가 판단하기 어려워진다.
따라서 모호성이 발생하지 않게, 하나의 추상 메서드만 가지도록 아래와 같이 구현하는 게 권장된다.
@FunctionalInterface // 함수형 인터페이스 선언
public interface Calculator {
int sum(int a, int b);
int sum(int a, int b, int c); // 어노테이션으로 인해 선언 불가
}
하나의 추상 메서드만 작성한다면, 어노테이션이 없는 일반 인터페이스를 통해서도 사용할 수 있다.
이번엔 람다식을 매개변수로 전달하는 방법을 코드를 통해 알아보자.
public class Main {
public static int calculate(int a, int b, Calculator calculator) {
return calculator.sum(a, b);
}
public static void main(String[] args) {
Calculator cal1 = new Calculator() {
@Override
public int sum(int a, int b) {
return a + b;
}
};
// 익명 클래스를 변수에 담아 전달
int ret2 = calculate(3, 3, cal1);
System.out.println("ret2 = " + ret2); // 출력: ret2 = 6
}
}
public class Main {
public static int calculate(int a, int b, Calculator calculator) {
return calculator.sum(a, b);
}
public static void main(String[] args) {
Calculator cal2 = (a, b) -> a + b;
// 람다식을 변수에 담아 전달
int ret3 = calculate(4, 4, cal2);
System.out.println("ret3 = " + ret3); // 출력: ret3 = 8
}
}
public class Main {
public static int calculate(int a, int b, Calculator calculator) {
return calculator.sum(a, b);
}
public static void main(String[] args) {
// 람다식을 직접 매개변수로 전달
int ret4 = calculate(5, 5, (a, b) -> a + b);
System.out.println("ret4 = " + ret4); // 출력: ret4 = 10
}
}
이 과정을 통해 람다식을 사용하면 컴파일 과정에서 1번 과정의 코드처럼 변환된다는 것을 추론할 수 있다.
훨씬 간단해 보이기만 하는 람다식의 장단점을 알아보자.
장점
효율적으로 람다식을 활용하면 코드가 간결해지고, 재사용성이 극대화된다는 장점이 있다.
단점
람다식을 사용할 때, 연산 방식에 따라 모든 원소를 전부 순회하는 경우가 생길 수 있다.
이런 경우엔 일반 반복문보다 성능이 느릴 수도 있다.
또한 람다식을 남용하면 오히려 코드를 이해하기 어려울 수 있다.
그래서 람다식을 사용할 땐 적절한 주석을 추가하는 게 좋다.
주석 없이 무분별하게 사용하면 흔히 말하는 스파게티 코드가 될 가능성이 높다.
1️⃣ 스트림은 데이터를 효율적으로 처리할 수 있는 흐름이다.
2️⃣ 선언형 스타일로 가독성이 굉장히 뛰어나다.
3️⃣ 컬렉션 (List, Set 등)과 함께 자주 활용된다.
각 요소를 10배로 변환 후 출력하는 예시로 for문과 스트림을 비교해보자.
public class Main {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));
// for 명령형 스타일
List<Integer> ret1 = new ArrayList<>();
for (Integer num : arrayList) {
int multipliedNum = num * 10; // 각 요소 * 10
ret1.add(multipliedNum);
}
System.out.println("ret1 = " + ret1);
}
}
public class Main {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));
// 스트림 선언형 스타일
List<Integer> ret2 = arrayList.stream() // 1. 데이터 준비
.map(num -> num * 10) // 2. 중간 연산 등록 (람다식 적용)
.collect(Collectors.toList()); // 3. 최종 연산
System.out.println("ret2 = " + ret2);
}
}
스트림의 구문을 살펴보면 총 3단계(준비 → 중간 연산 → 최종 연산)로 구성되며,
실행 과정에서는 다음과 같은 방식으로 동작하게 된다.
// 1. 데이터 준비: 스트림 생성
Stream<Integer> stream = arrayList.stream();
// 2. 중간 연산 등록: 각 요소를 10배로 변환 로직 등록
Stream<Integer> mappedStream = stream.map(num -> num * 10);
// 3. 최종 연산: 최종 결과 리스트로 변환
List<Integer> ret2 = mappedStream.collect(Collectors.toList());
List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));
// filter() + map()
List<Integer> ret3 = arrayList.stream() // 1. 데이터 준비: 스트림 생성
.filter(num -> num % 2 == 0) // 2. 중간 연산: 짝수만 필터링
.map(num -> num * 10) // 3. 중간 연산: 10배로 변환
.collect(Collectors.toList()); // 4. 최종 연산: 리스트로 변환
System.out.println(ret3); // 출력: [20, 40]
스트림의 장단점 역시 알아보자.
장점
제일 큰 장점은 람다와 동일하게 가독성을 크게 향상 시킬 수 있다.
단점
단점으론 중간 연산이 체이닝으로 연결되기 때문에, 디버깅 시 어느 단계에서 문제가 발생하는지 파악이 어렵고, 소량의 데이터에서는 Stream API를 사용하는 것보다 일반 루프를 사용하는 것이 성능 측면에서 더 효율적일 가능성이 크다.
그리고 오늘 느낀 점이 하나 있다.
람다와 스트림을 1년 전에 학원에서 배웠을 땐, 처음보는 구문이라 그런지 거부감이 들었다.
그땐 왜 이걸 써야 하는지 굳이 필요성을 느끼지 못해서 복습도 하지 않았었다.
근데 곰곰히 생각해보니까, 람다와 스트림을 활용해서 프로젝트를 맡거나 유지보수를 해야 하는 상황이 올 수도 있다.
그래서 이번에 블로그에 정리하면서 제대로 공부해봤다.
아직 손에 익진 않았지만, 배우고 나니까 구현도 쉽고 가독성도 훨씬 좋다는 걸 느꼈다.
개발자는 열린 마음으로 여러 기술들을 받아들여야 한다는 걸 다시 한번 깨달은 하루다.