CodeGym /행동 /JAVA 25 SELF /컬렉션과 스트림에서의 람다 사용

컬렉션과 스트림에서의 람다 사용

JAVA 25 SELF
레벨 48 , 레슨 1
사용 가능

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를 변경하려고 하면 — 컴파일러가 오류를 보고합니다.

표: 람다와 함께 쓰는 컬렉션/스트림의 주요 메서드

컬렉션/스트림 메서드 무엇을 하는가 람다 형태 예시
forEach(Consumer<T>)
각 요소에 대해 수행
x -> ...
list.forEach(s -> ...)
removeIf(Predicate<T>)
조건에 맞는 요소를 제거
x -> ...
list.removeIf(s -> ...)
sort(Comparator<T>)
요소 정렬
(a, b) -> ...
list.sort((a, b) -> ...)
replaceAll(UnaryOperator<T>)
각 요소를 변환하여 대체
x -> ...
list.replaceAll(s -> ...)
filter(Predicate<T>)
스트림 필터링
x -> ...
stream.filter(s -> ...)
map(Function<T, R>)
요소 변환
x -> ...
stream.map(s -> ...)
forEach(Consumer<T>)
스트림 반복(iteration)
x -> ...
stream.forEach(s -> ...)
sorted(Comparator<T>)
스트림에서 정렬
(a, b) -> ...
stream.sorted((a, b) -> ...)

7. 흔한 실수

오류 №1: 람다가 너무 길다. 람다 표현식 안에 이미 5줄의 코드, 조건, 반복문, try-catch까지 있다면 — 해당 코드를 별도의 메서드로 분리하는 편이 좋습니다. 람다는 짧은 로직에 적합합니다.

오류 №2: 변경 가능한 변수를 사용한다. 람다 내부에서 외부 메서드의 변수를 변경하려고 하면(예: 카운터) 컴파일러가 허용하지 않습니다. 변수는 final이거나 effectively final이어야 합니다.

오류 №3: 컬렉션/스트림 메서드가 항상 원본 컬렉션을 바꾸는 것은 아니다. 예를 들어 stream().filter(...)는 원본 리스트를 변경하지 않고 새로운 스트림을 반환합니다. 컬렉션이 필요하다면 collect(Collectors.toList())를 사용하세요.

오류 №4: 람다 표현식의 타입이 맞지 않는다. 메서드가 예를 들어 Comparator<T>를 받는데, 매개변수 하나짜리 람다(두 개가 아니라)를 전달하면 — 컴파일 오류가 발생합니다.

오류 №5: 중첩된 람다로 가독성을 잃는다. map, filter, forEach가 연쇄되고 각 람다 안에 또 람다가 있으면 — 코드는 읽기 어려워집니다. 이런 경우에는 표현식을 단계별로 나누거나 일부를 메서드로 분리하는 것이 좋습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION