본문 바로가기
학습/Java

map vs flatMap

코동이 2021. 5. 16.

 쇼핑몰 토이프로젝트 중에 리스트 안에 리스트를 조회해야하는 일이 생겼습니다. 단순하게 map을 2번 사용해서 리스트 안으로 들어가고 싶었는데, 오류가 났습니다. flatMap을 알아내어 해결했습니다. 그래서 map과 flatMap의 차이를 비교하고 사용하고자 합니다.해당 글은 map과 flatMap의 차이를 정리하기 때문에 map에 대한 기본 개념은 따로 적지 않습니다.

 

 예시를 통해서 비교하는 것이 가장 빠르다고 생각합니다. Stream은 map을 사용한다면 주석으로 반환형을 꼭 써보면서 공부하면 학습 능률이 커집니다.

 

        List<String> fruit = new ArrayList<>();
        fruit.add("Apple");
        fruit.add("mango");
        fruit.add("pineapple");
        fruit.add("kiwi");
        System.out.println("List of fruit-" + fruit);

        // 1
        List<Integer> integerList = fruit.stream() //Stream<String>
                .map(String::length) //Stream<Integer>
                //.map(f -> f.length())
                .collect(Collectors.toList()); //List<Integer> [5,5,9,4]
        // 2
        List<Integer> integerList2 = Stream.of(fruit) //Stream<List<String>>
                .flatMap(List::stream) //Stream<String>
                //.flatMap(x -> x.stream())
                .map(String::length) //Stream<Integer>
                .collect(Collectors.toList()); //List<Integer> [5,5,9,4]
                
        // 3                
        Integer fruitFirst = fruit.stream() //Stream<String>
                .map(String::length) //Stream<Integer>
                .findFirst() //Optional<Integer> 5
                .orElseThrow(() -> new Exception("nothing")); 

답은 설명 끝에 있습니다.

 

- 1번

List<Integer> integerList = fruit.stream() //Stream<String>
                .map(String::length) //Stream<Integer>
                //.map(f -> f.length())
                .collect(Collectors.toList()); //List<Integer> [5,5,9,4]

*fruit.stream()

정의된 fruit은 반환형이 List<String>이지만 fruit.stream()을 하면 Stream<String> 형으로 변환됩니다. 

자바 문서에는 stream()을 다음과 같이 설명합니다.

stream()
Returns a sequential Stream with this collection as its source

즉, 컬렉션을 소스로 가진 것을 순차적인 Stream을 반환합니다. 따라서 List안에 있는 String을 기준으로 List<String>이 Stream<String>이 됩니다.

 

*map(String::length)

"::"은 메서드 참조로 String 클레스의 length함수를 사용하겠다는 의미입니다. 즉, String을 int로 변환시킵니다. 처음에는 익숙하지 않은 표현이지만 "클래스::메서드명" 형태로 익숙해지면 가독성이 좋습니다. 우리가 일반적으로 사용하는 람다식은 ".map(f -> f.length())"입니다.

 

*collect(Collectors.toList())

Stream<Integer>들을 List<Integer>로 리스트로 변환시킵니다.

 

- 2번

List<Integer> integerList2 = Stream.of(fruit) //Stream<List<String>>
                .flatMap(List::stream) //Stream<String>
                //.flatMap(x -> x.stream())
                .map(String::length) //Stream<Integer>
                .collect(Collectors.toList()); //List<Integer> [5,5,9,4]

*Stream.of(fruit)

1번의 fruit.stream()과 Stream.of(fruit)의 차이를 비교하면 좋습니다. 먼저 Stream의 of메서드를 살펴봅시다.

of()
Returns a sequential Stream containing a single element.
Params: t – the single element

요소를 넣으면, 요소를 포함하여 순차적인 Stream을 반환합니다.

Stream.of(fruit) Stream<List<String>>
fruit.stream() Stream<String>

*flatMap(List::stream)

 flatMap()은 스트림의 요소에 1:다 변형을 적용하고, 새로운 스트림으로 "평평하게 폅니다." 쉽게 말해, Stream 안에 여러개의 list가 있을 때 모든 것을 합쳐서 하나의 새로운 스트림으로 만들기 위해 사용합니다. Stream.of(fruit)은  반환형이Strem<List<String>>인데, 1:다를 통해 Stream<String>으로 하나로 통합한 평평한 스트림을 새로 만들 때 사용합니다. 해당 설명이 이해가 가지 않는다면 아래에 나오는 새로운 예시 1번을 추가적으로 확인하면 쉬워집니다.

 

 List::stream은 1번에서 사용했던 stream()을 이용한다는 의미입니다. stream()은 컬렉션을 순차적인 스트림으로 변환하다고 하였습니다. 따라서 Stream<List<String>>에서 List<String>이 String으로 변환되어 최종적으로 Stream<String>으로 바뀝니다. 메서드 참조가 익숙하지 않다면 람다식으로 "x -> x.stream()"으로 표현할 수 있습니다.

 

- 3번

Integer fruitFirst = fruit.stream() //Stream<String>
                .map(String::length) //Stream<Integer>
                .findFirst() //Optional<Integer> 5
                .orElseThrow(() -> new Exception("nothing"));

*findFirst()

findFirst()는 .collect(Collectors.toList()) 대신 사용합니다. 마지막에 리스트로 통합하는 대신 첫번째 요소를 찾도록 합니다. findFirst()는 Optional이 반환형이므로 orElseThrow()로 포함했습니다.

List<List<Integer>> number = new ArrayList<>();
number.add(Arrays.asList(1, 2));
number.add(Arrays.asList(3, 4));
number.add(Arrays.asList(5, 6));
number.add(Arrays.asList(7, 8));

// 1
List<Integer> flatList
        = number.stream() //Stream<List<Integer>>
        .flatMap(List::stream) //Stream<Integer>
        .collect(Collectors.toList()); //List<Integer> [1,2,3,4,5,6,7,8]

// 2
List<Integer> flatList3
        = number.stream() //Stream<List<Integer>>
        .map(list -> list.get(0)) //Stream<Integer>
        .collect(Collectors.toList()); // List<Integer> [1,3,5,7]

flatMap을 사용하는 것은 보통 다음과 같이 중첩된 list인 경우입니다. 리스트 안에 리스트를 하나로 통합할 때 주로 사용합니다.

 

-1번

List<Integer> flatList
        = number.stream() //Stream<List<Integer>>
        .flatMap(List::stream) //Stream<Integer>
        .collect(Collectors.toList()); //List<Integer> [1,2,3,4,5,6,7,8]

*flatMap(List::stream)

위에서도 썼지만, stream()은 컬렉션 요소를 순차적인 Stream으로 반환합니다. flatMap과 함께 사용하면 Stream<List<Integer>>에 있는 컬렉션 요소 List<Integer>들을 하나로 통합하는 새로운 Stream<Integer>을 만듭니다. 

 

- 2번

List<Integer> flatList3
        = number.stream() //Stream<List<Integer>>
        .map(list -> list.get(0)) //Stream<Integer>
        .collect(Collectors.toList()); // List<Integer> [1,3,5,7]

*map(list -> list.get(0))

list.get(0)을 통해 각 list에서 첫번째 인덱스의 요소들만 뽑아낼 수 있습니다. 첫번째 요소들만 빼냈으므로 더이상 List<Integer>의 형태가 아닙니다. 따라서 Stream<Integer>로 변환됩니다.

 

Stream과 관련해서 List뿐만 아니라 Array에서 사용하는 것도 알아봅니다.

String[][] array = new String[][]{{"a","b"}, {"b","c"}, {"c","d"}};

// 1
List<String> listResult = Stream.of(array) //Stream<String[][]>
        .flatMap(Stream::of) //Stream<String>
        .collect(Collectors.toList()); //List<String> [a,b,b,c,c,d]
                
// 2
String[] arrayResult = Stream.of(array) //Stream<String[][]>
        .map(array -> array[0]) //Stream<String>
        .toArray(String[]::new); //String[] [a,b,c]

- 1번

List<String> listResult = Stream.of(array) //Stream<String[][]>
        .flatMap(Stream::of) //Stream<String>
        .collect(Collectors.toList()); //List<String> [a,b,b,c,c,d]

*Stream.of(array)

일반 배열에 Stream.of(array)를 한다면, 단지 Stream을 씌우고 끝입니다. 따라서 String[][]이 Stream<String[][]>으로 변환됩니다.

 

*flatMap(Stream::of)

of() 메서드의 매개변수 T...values는 배열이 입력되었을 때 인식되며 다음과 같이 반환합니다.

public static<T> Stream<T> of(T... values) {
  return Arrays.stream(values);
}

항상 Arrays 형태로 반환합니다. 그 형태를 flatMap을 통해 평평하게 펼칩니다.

 

- 2번

String[] arrayResult = Stream.of(array) //Stream<String[][]>
        .map(array -> array[0]) //Stream<String>
        .toArray(String[]::new); //String[] [a,b,c]

*map(array -> array[0])

list에서와 마찬가지로 2차원 배열에서 각 배열의 첫번째 요소만 꺼낼 수 있습니다.

 

*toArray(String[]::new)

뽑아낸 String들을 마지막에 String[] 문자열 배열로 변환시킵니다.

 

*결론

stream에서 map과 flatMap에 대해 예제를 바탕으로 정리를 했습니다. 이번 기회에 스스로 Stream 반환형을 써보면서 짐작만 하던 부분들을 더욱 선명하게 알 수 있었습니다. 

반응형

'학습 > Java' 카테고리의 다른 글

POJO / Hibernate  (0) 2021.08.01
전략패턴이란?  (0) 2021.08.01
JJWT  (1) 2021.03.03
Could not autowire. No beans of 'Mapper' type found  (0) 2021.03.02
public API에서 AllArgsConstructor 사용하지 말 것!!!  (0) 2021.02.21