Stream으로 야무지게 collect하기

@hongo · March 05, 2023 · 3 min read

Stream으로 야무지게 collect해야지~

사다리 게임을 함께 했던 페어 에밀이 stream으로 collect를 참 잘해서 많이 배웠다...! 배운걸 정리할겸 블로그에도 올려보려고 한다.ㅎㅎ Streamcollect()메소드를 사용하면 연산 결과를 List나 Set등 Collection으로 묶어 반환할 수 있다. Stream을 사용해서 객체를 야무지게 모아보자!

📌 toList() & toSet()

collect 메소드로 가장 많이 사용하는 기능이다ㅎㅎ. Stream의 요소들을 List, Set로 묶어 반환한다.

List<String> example = Arrays.asList("aa", "bb", "bb", "cc");
List <String> listResult = example.stream().collect(Collectors.toList()); // aa, bb, bb, cc
List<String> setResult = example.stream().collect(Collectors.toSet()) // aa, bb, cc
  • 방어적 복사

CollectorstoList(), toSet()addAll()메소드를 사용해 방어적 복사를 수행한다. 방어적 복사를 수행한 복사본은 원본과 다른 컬렉션 객체를 참조하게 되지만, 객체 내부에 있는 원소들은 원본과 동일한 주소를 참조한다. 예시를 보며 이해를 해보자!


  • 이름을 저장하는 Name이라는 객체가 존재한다. 세 개의 Name을 만들어서 리스트를 생성해보자.
Name n1 = new Name("aa");
Name n2 = new Name("bb");
Name n3 = new Name("cc");

List<Name> names  = new ArrayList<>(Arrays.asList(n1, n2, n3));
List<Name> newNames = names.stream().collect(Collectors.toList());;

  • List 컬렉션 변경
List<Name> names  = Arrays.asList(n1, n2, n3); // aa, bb, cc
List<Name> newNames = names.stream().collect(Collectors.toList()); //aa, bb, cc
names.add(new Name("dd"));

// names : aa, bb, cc, dd
// newNames : aa, bb, cc

복사본은 원본과 다른 주소를 가지는 컬렉션 객체이기에, names에 변화가 생겨도 newNames에 영향을 끼치지 않는다.


  • List 컬렉션 내부 객체 변경
List<Name> names  = Arrays.asList(n1, n2, n3); // aa, bb, cc
List<Name> newNames = names.stream().collect(Collectors.toList()); //aa, bb, cc

n1.setName("dd");
// names : dd, bb, cc
// newNames : dd, bb, cc

객체 내부에 있는 원소들은 원본과 동일한 주소를 참조하므로, 내부 객체의 값이 변경되면 namesnewNames 모두 변화가 생긴다.


이를 피하고 싶다면 new로 내부 객체를 새로 생성한뒤 collect하는 방법이 있다.

List<Name> newNames = names.stream().map(iter -> new Name(iter.getName())).collect(Collectors.toUnmodifiableList());

📌 toMap()

ListSet 외에 Map으로도 Stream 요소들을 모을 수 있다. toMap의 매개변수를 보면 다음과 같다!

Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction,
                                Supplier<M> mapSupplier)
  • keyMapper : Map의 key를 만드는 함수
  • valueMapper : Map의 value를 만드는 함수
  • mergeFunction : 중복되는 키 값이 들어올 경우 수행되는 함수
  • mapSupplier : 결과와 함께 비어있는 새 맵의 타입을 반환하는 함수

예시를 통해 toMap을 어떻게 활용하는지 봐보자!

Map<Name, Integer> nameTable = names.stream().collect(Collectors.toMap(name -> name, name -> name.length());

nameTableName객체를 key로, key값인 Name의 길이를 value로 가지는 Map이다. toMap의 인자로 key와 value를 만드는 함수를 넣어 Map을 생성할 수 있다.


  • key값이 중복일 경우
Map<Name, Integer> nameTable = names.stream().collect(Collectors.toMap(name -> name,
                name -> name.length(),
                (old, now) -> old));

mergeFunction을 인자로 넣어, 어떤 key의 값을 사용할 것인지를 정할 수 있다.


  • 새 맵의 타입으로 반환하고 싶은 경우
Map<Name, Integer> nameTable = names.stream().collect(Collectors.toMap(name -> name,
                name -> name.length(),
                (old, now) -> old),              					               LinkedHashMap::new);

mapSupplier 에 반환하고 싶은 타입의 Map을 지정할 수 있다.

📌 Unmodifiable Collection

toUnmodifiableList(), toUnmodifiableSet(), toUnmodifiableMap을 사용하면 수정이 불가능한 컬렉션 객체를 반환한다. Unmodifiable Collectionset(), add(), addAll()등의 메소드를 호출하면 UnsupportedOperationException이 발생한다.

물론 방어적 복사를 수행하므로, toUnmodifiableList()로 생성한 리스트의 내부 객체가 변경될수도 있다는 점을 유의해야한다.

List<Name> result = names.stream().collect(Collectors.toUnmodifiableList());

📌 toCollection()

toSet, toList, toMap은 Collection을 지정할 수 없다.

toCollection()을 사용해 다른 타입의 Collection으로 묶는 것이 가능하다.

List<String> result = givenList.stream()
  .collect(toCollection(LinkedList::new))
List<String> result = givenList.stream()
  .collect(toCollection(LinkedList::new))

📌 collectingAndThen()

collect()를 완료한 직후 결과에 대해 다른 작업을 수행할 수 있다.

public class Name {
	...
    @Override
    public String toString() {
        return "저는" + name +"입니다!";
    }
}
String result = names.stream().collect(collectingAndThen(toList(), List::toString));
System.out.println(result);

// 실행 결과
// [저는aa입니다!, 저는bb입니다!, 저는cc입니다!]

📌 joining()

Stream 요소가 char이나 String일 경우, 결합할 수 있다.

String result = givenList.stream()
  .collect(joining())

  • 구분자, prefix, suffix 설정도 가능
String result = givenList.stream() //"aa", "vv"
  .collect(joining(" ", "pre", "suf"));
@hongo
홍고 블로그