자바 스트림 정리 - 4. 스트림 예제

자바 스트림 API를 사용하는 여러가지 예제들을 알아보자.

#java #stream


목차



스트림 API 예제

이번 포스팅에서는 스트림 API를 사용하는 여러 가지 예제들을 살펴봅니다. 예제에서 사용하는 Person 클래스는 아래와 같이 미리 작성되있음을 가정합니다.

class Person {
    private String name;
    private int age;
    private String phoneNumber;

    public Person(String name, int age, String phoneNumber) {
        this.name = name;
        this.age = age;
        this.phoneNumber = phoneNumber;
    }
    
    // getter, setter 생략
}



List<V> to Map<K, V>

List 형태를 Map 형태로 바꿔봅시다. List<V> 형태와 같이 특정 오브젝트 타입의 리스트를 오브젝트의 한 필드를 키로 하는 Map<K, V> 형태로 변경합니다.

List<Person> personList = new ArrayList<>();
personList.add(new Person("짱구", 23, "010-1234-1234"));
personList.add(new Person("유리", 24, "010-2341-2341"));
personList.add(new Person("철수", 29, "010-3412-3412"));
personList.add(new Person("맹구", 25, null));

// Function.identity는 t -> t, 항상 입력된 인자(자신)를 반환합니다.
Map<String, Person> personMap = personList.stream()
        .collect(Collectors.toMap(Person::getName, Function.identity()));

처음 스트림 API를 접했을 때 위와 같은 축약형 코드가 난해했던 경험이 있는데요. 아래와 같이 람다표현식이나 메서드 참조를 모두 풀어서 이해한 적도 있던 것 같습니다.

Map<String, Person> personMap = personList.stream()
        .collect(Collectors.toMap(new Function<Person, String>() {
            @Override
            public String apply(Person person) {
                return person.getName();
            }
        }, new Function<Person, Person>() {
            @Override
            public Person apply(Person person) {
                return person;
            }
        }));

추가적으로 filter 메서드를 사용하면 특정 조건에 일치한 형태만 골라낼 수 있습니다.

Map<String, Person> personMap = personList.stream()
        .filter(person -> person.getAge() > 24) // 25살 이상만 골라낸다.
        .collect(Collectors.toMap(Person::getName, Function.identity()));

한편 Collectors.toMap 메서드를 수행할 때 하나의 키에 매핑되는 값이 2개 이상인 경우 IllegalStateException 예외가 발생합니다. 이럴 때는 BinaryOperator를 사용하여 아래와 같이 저장할 값을 선택할 수 있습니다.

Map<Integer, Person> personMap = personList.stream()
        .collect(Collectors.toMap(
                o -> o.getAge(),
                Function.identity(),
                (oldValue, newValue) -> newValue)); // 중복되는 경우 새 값으로 넣는다.

아니면 중복 키(duplicatekey)를 허용할 수도 있는데요. Collectors.groupingBy 메서드를 사용하여 조금 다른 형태로 중복키를 허용한 리스트 형태로 담을 수도 있습니다.

// List 형태로 담는다.
Map<Integer, List<Person>> duplicatedMap = personList.stream()
        .collect(Collectors.groupingBy(Person::getAge));



스트림 내에서 null 제외하기

위에서 살펴본 filter 메서드를 적절하게 사용하면 스트림 내의 null 값을 제외시킬 수 있습니다.

Stream<String> stream = Stream.of("철수", "훈이", null, "유리", null);
List<String> filteredList = stream.filter(Objects::nonNull)
        .collect(Collectors.toList());        



조건에 일치한 요소 찾기

filter 메서드와 findFirst 메서드로 조건에 일치한 가장 첫 요소를 찾을 수 있습니다.

List<Person> personList = new ArrayList<>();
personList.add(new Person("짱구", 23, "010-1234-1234"));
personList.add(new Person("유리", 24, "010-2341-2341"));
personList.add(new Person("맹구", 23, "010-3412-3412"));

// 짱구
Person person = personList.stream()
        .filter(p -> p.getAge() == 23)
        .findFirst().get();

findFirst 메서드 대신에 findAny 메서드도 가능합니다. 단, 일반 스트림에서는 동일한 요소(짱구)가 결과로 나오지만 병렬 스트림에서는 매 실해마다 다를 수 있습니다. 순서에 상관없이 조건에 충족한 요소를 찾고 싶을 때 findAny 메서드가 효과적일 수 있습니다.

// 짱구 또는 맹구
Person person = personList.parallelStream()
        .filter(p -> p.getAge() == 23)
        .findAny().get();



스트림 정렬하기

스트림을 주어진 조건으로 정렬할 수 있습니다. 예를 들어 나이(age) 값을 기준으로 오름차순 정렬을 한다면,

List<Person> personList = new ArrayList<>();
personList.add(new Person("짱구", 25, "010-1234-1234"));
personList.add(new Person("유리", 24, "010-2341-2341"));
personList.add(new Person("맹구", 23, "010-3412-3412"));
personList.add(new Person("훈이", 26, "010-4123-4123"));

// 맹구, 유리, 짱구, 훈이
personList.stream()
        .sorted(Comparator.comparing(Person::getAge))
        .forEach(p -> System.out.println(p.getName()));

Comparator.comparing 메서드에 reversed 메서드를 추가하면 역순으로도 정렬할 수 있습니다. 내부를 살펴보면 자바8 버전에서 추가된 Collections.reverseOrder를 사용하여 역순으로 정렬합니다.

// 훈이, 짱구, 유리, 맹구
personList.stream()
        .sorted(Comparator.comparing(Person::getAge).reversed())
        .forEach(p -> System.out.println(p.getName()));



reduce로 결과 구하기

reduce 메서드로 스트림을 하나의 결과로 연산할 수 있습니다. 예를 들어 아래와 같이 숫자로 구성된 리스트 내의 요소를 모두 더해 합계(sum)를 구할 수 있습니다.

List<Integer> list = List.of(5, 4, 2, 1, 6, 7, 8, 3);
        
// 36
Integer result = list.stream()
        .reduce(0, (value1, value2) -> value1 + value2);

박싱 비용을 줄이기 위한 IntStream처럼 기본형에 특화된 스트림으로도 처리할 수 있습니다. primitive type인 int 형태로 반환됩니다.

// 36
int intResult = list.stream()
        // 또는 .mapToInt(x -> x).sum();
        .mapToInt(Integer::intValue).sum();

아래와 같이 “Swift 라는 문자열보다 길고 리스트 중에서 가장 긴 문자열” 이라는 특정 조건을 만족한 것만 추출할 수도 있습니다.

List<String> list = List.of("Java", "C++", "Python", "Ruby");

// Python
String result = list.stream()
        .reduce("Swift", (val1, val2) ->
                val1.length() >= val2.length() ? val1 : val2);



단일 컬렉션 만들기

2차원 배열과 같은 요소를 flatmap 메서드를 사용하여 중첩 구조를 제거하고 단일 컬렉션으로 만들 수 있습니다.

String[][] names = new String[][]{
        {"짱구", "철수"}, {"훈이", "맹구"}
};

// 리스트로
List<String> list = Arrays.stream(names)
        .flatMap(Stream::of)
        .collect(Collectors.toList());
        
// 1차원 배열로
String[] flattedNames = Arrays.stream(names)
        .flatMap(Stream::of).toArray(String[]::new);



이어서

스트림 API를 이용한 여러가지 예제에 대해서 알아보았습니다. 이어지는 포스팅에서는 스트림 API를 사용하면서 주의할 점에 대해서 알아봅니다.