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

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


목차


스트림 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를 사용하면서 주의할 점에 대해서 알아봅니다.


댓글을 남기시려면 Github 로그인을 해주세요 :D


Hi, there!

Thanks for visiting my blog.
Please let me know if there are any mistakes in my post.