1. 람다 표현식과 컬렉션
람다 표현식이 등장하기 전 컬렉션을 어떻게 처리했는지 떠올려 봅시다. 문자열 리스트가 있고, 이를 화면에 출력하고 싶다고 가정해 보겠습니다:
List<String> list = Arrays.asList("kot", "pyos", "yozh");
for (String s : list) {
System.out.println(s);
}
아주 간단합니다. 하지만 예를 들어 리스트에서 모든 빈 문자열을 제거하고 싶다면 조건이 있는 반복문을 작성해야 하고, 경우에 따라서는 이터레이터를 사용해야 합니다(그렇지 않으면 ConcurrentModificationException이 발생할 수 있습니다). 또 예를 들어 문자열 길이로 정렬하려면 다음과 같습니다:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
이처럼 간단한 작업에도 이미 5줄의 코드와 수많은 “소음” 괄호가 들어갑니다. 어떻게 최적화할 수 있을까요? 답은 이미 알고 계시죠: 람다 표현식입니다.
컬렉션 메서드에서의 람다 표현식 사용
Java 8부터 컬렉션 인터페이스에는 함수형 인터페이스를 받는 새로운 메서드들이 추가되었습니다. 즉, 여기에 람다 표현식을 전달할 수 있습니다. 가장 많이 쓰이는 것들은 다음과 같습니다:
- forEach(Consumer<T> action)
- removeIf(Predicate<T> filter)
- sort(Comparator<T> c)
- replaceAll(UnaryOperator<T> operator)
예제: forEach
리스트의 모든 요소를 화면에 출력(옛 방식):
for (String s : list) {
System.out.println(s);
}
이제 — 람다 사용:
list.forEach(s -> System.out.println(s));
또는 더 짧게, “Java 구루”처럼 놀고 싶다면:
list.forEach(System.out::println); // method reference, 나중에 살펴봅니다
예제: removeIf
리스트에서 모든 빈 문자열 제거:
List<String> animals = new ArrayList<>(Arrays.asList("kot", "", "pyos", "yozh", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [kot, pyos, yozh]
예제: sort
문자열 길이로 리스트 정렬:
List<String> animals = Arrays.asList("kot", "pyos", "yozh", "slon");
animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [kot, pyos, yozh, slon]
예제: replaceAll
모든 문자열을 대문자로 변환:
List<String> animals = new ArrayList<>(Arrays.asList("kot", "pyos", "yozh"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [KOT, PYOS, YOZH]
2. Stream API와 람다 표현식
Java 8과 함께 Stream API가 등장했습니다 — 함수형 스타일로 컬렉션을 처리하는 강력한 도구입니다. 스트림을 사용하면 필터링, 변환, 정렬, 컬렉션 수집을 메서드 체이닝으로 수행할 수 있습니다. 그리고 이 모든 메서드는 람다 표현식을 받습니다!
중요: Stream API에 대한 전체 설명은 뒤에서 다루고, 여기서는 람다의 역할을 이해하기 위한 기초 예제만 살펴봅니다.
예제: 필터링
3자보다 긴 문자열만 남기기:
List<String> animals = Arrays.asList("kot", "slon", "yozh", "krokodil");
animals.stream()
.filter(s -> s.length() > 3)
.forEach(System.out::println); // slon, yozh, krokodil
예제: 변환(map)
모든 문자열을 대문자로 만들기:
List<String> animals = Arrays.asList("kot", "slon", "yozh");
List<String> upper = animals.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(upper); // [KOT, SLON, YOZH]
예제: 정렬
원본을 변경하지 않고 길이 기준으로 정렬된 리스트 얻기:
List<String> animals = Arrays.asList("kot", "slon", "yozh", "krokodil");
List<String> sorted = animals.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(sorted); // [kot, slon, yozh, krokodil]
3. 익명 클래스와의 비교
같은 코드를 익명 클래스와 람다로 각각 작성하면 어떻게 보이는지 비교해 봅시다.
문자열 길이로 정렬
익명 클래스:
animals.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
람다 표현식:
animals.sort((a, b) -> a.length() - b.length());
결론:
람다 표현식은 줄 수를 크게 줄이고 코드를 더 읽기 쉽게 만듭니다. 괄호도 적고, 잡음도 줄어들고 — 핵심만 남습니다!
빈 문자열 제거
익명 클래스:
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
});
람다 표현식:
animals.removeIf(s -> s.isEmpty());
4. 실습: 람다 표현식으로 푸는 짧은 과제
사용자 리스트를 다루는 미니 프로그램에서 람다 표현식을 실제로 적용해 봅시다.
예제 1: 나이로 사용자 필터링
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
);
users.stream()
.filter(u -> u.age >= 18)
.forEach(System.out::println); // Bob (25)
예제 2: 사용자 이름으로 정렬
List<User> users = Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alisa (17), Bob (25), Charli (15)]
예제 3: 사용자 리스트를 이름 리스트로 변환
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(names); // [Alisa, Bob, Charli]
예제 4: 미성년자 모두 제거
List<User> users = new ArrayList<>(Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
));
users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]
5. 유용한 뉘앙스
특징: 영역(scope)과 “effectively final” 변수
람다 표현식은 외부 메서드의 변수를 사용할 수 있지만, 그 변수는 final 이거나 “effectively final”(즉, 초기화 이후 변경되지 않음)이어야 합니다.
int minAge = 18;
users.stream()
.filter(u -> u.age >= minAge)
.forEach(System.out::println);
람다에서 사용된 후에 minAge를 변경하려고 하면 — 컴파일러가 오류를 보고합니다.
표: 람다와 함께 쓰는 컬렉션/스트림의 주요 메서드
| 컬렉션/스트림 메서드 | 무엇을 하는가 | 람다 형태 | 예시 |
|---|---|---|---|
|
각 요소에 대해 수행 | |
|
|
조건에 맞는 요소를 제거 | |
|
|
요소 정렬 | |
|
|
각 요소를 변환하여 대체 | |
|
|
스트림 필터링 | |
|
|
요소 변환 | |
|
|
스트림 반복(iteration) | |
|
|
스트림에서 정렬 | |
|
7. 흔한 실수
오류 №1: 람다가 너무 길다. 람다 표현식 안에 이미 5줄의 코드, 조건, 반복문, try-catch까지 있다면 — 해당 코드를 별도의 메서드로 분리하는 편이 좋습니다. 람다는 짧은 로직에 적합합니다.
오류 №2: 변경 가능한 변수를 사용한다. 람다 내부에서 외부 메서드의 변수를 변경하려고 하면(예: 카운터) 컴파일러가 허용하지 않습니다. 변수는 final이거나 effectively final이어야 합니다.
오류 №3: 컬렉션/스트림 메서드가 항상 원본 컬렉션을 바꾸는 것은 아니다. 예를 들어 stream().filter(...)는 원본 리스트를 변경하지 않고 새로운 스트림을 반환합니다. 컬렉션이 필요하다면 collect(Collectors.toList())를 사용하세요.
오류 №4: 람다 표현식의 타입이 맞지 않는다. 메서드가 예를 들어 Comparator<T>를 받는데, 매개변수 하나짜리 람다(두 개가 아니라)를 전달하면 — 컴파일 오류가 발생합니다.
오류 №5: 중첩된 람다로 가독성을 잃는다. map, filter, forEach가 연쇄되고 각 람다 안에 또 람다가 있으면 — 코드는 읽기 어려워집니다. 이런 경우에는 표현식을 단계별로 나누거나 일부를 메서드로 분리하는 것이 좋습니다.
GO TO FULL VERSION