티스토리 뷰
개념
- 스트림은 파라미터로 제공되는 함수(
Function<T, R>
관련된 함수형 인터페이스)를 적용해서 기존 요소를 새로운 요소로 매핑시키는map
이라는 메서드를 제공한다. - 기존 값을 변경한다는 개념보다는 새로운 값을 만든다는 개념이므로 변환(transforming) 에 매핑(mapping) 이라는 단어를 사용한다.
-
map
의 기본 개념은 위의 이미지와 같다. -
또한
map
은 반환하는 형태에 따라 여러 메서드가 존재한다.mapToInt
,mapToLong
,mapToDouble
,mapToObj
가 다음에 해당한다.- 일반 스트림과 기본형 특화 스트림은 위의 메서드 중에서 타입에 따라 3가지씩 가지고 있고 없는 메서드는 결국 자기 자신 타입의 스트림을 반환하는 것이기 때문에
map
메서드가 그 역할을 하는 것이다. - 예를 들어,
IntStream
의 경우map
,mapToLong
,mapToDouble
,mapToObj
가 존재하며map
자체가IntStream
을 반환하고Stream
의 경우map
,mapToInt
,mapToLong
,mapToDouble
가 존재하며map
자체가Stream
을 반환한다.
map 메서드
- 위에서 설명한 가장 기본적인 형태의
map
메서드이다. - 일반 스트림과 기본형 특화 스트림 모두에서 제공하는 메서드이다.
- 스트림 별로
map
의 파라미터를 살펴보면 다음과 같다.
Stream의 map
메서드
IntStream의 map
메서드
LongStream의 map
메서드
DoubleStream의 map
메서드
-
map
메서드의 경우 결국 자기 자신 타입의 스트림을 반환하는 것을 알 수 있다. -
일반 스트림에서는
Function<T, R>
이 사용되고 있는데 T가 변환 전 타입 R이 변환 후 타입이다.- T와 R의 타입이 같을 수도 있다.
- 필자의 경우도 마찬가지였지만 스트림을 처음 공부하는 사람들이 이런 부분을 놓치기 쉽고 응용을 어려워한다.
-
기본형 특화 스트림에서는 타입의 변화가 없기 때문에 각각에 맞는 특화 타입의
UnaryOperator
가 사용되는 것은 어찌보면 당연하다.
mapToInt 메서드
- 스트림을
IntStream
으로 변환해주는 메서드다. IntStream
을 제외한 모든 스트림에서 동일하게 제공하는 메서드다.
-
위의 이미지처럼 일반 스트림에서는
map
메서드와 다르게ToIntFunction<? super T> mapper
를 파라미터로 전달한다.ToIntFunction
라는 인터페이스의 이름에서 알 수 있듯이 기본Function<T, R>
에서 T만 제네릭 타입으로 전달받고 R은 int로 정해져있다.- 다른 기본형 특화 스트림에서는
LongToIntFunction
나DoubleToIntFunction
을 파라미터로 전달한다. - 인터페이스의 이름에서부터 알 수 있듯이 기본
Function<T, R>
에서 T는 long과 double R은 int로 정해져있기 때문에 제네릭 타입을 받지 않는다.
mapToLong 메서드
- 스트림을
LongStream
으로 변환해주는 메서드다. LongStream
을 제외한 모든 스트림에서 동일하게 제공하는 메서드다.
mapToInt
와 이하 내용은 유사하다.
mapToDouble 메서드
- 스트림을
DoubleStream
으로 변환해주는 메서드다. DoubleStream
을 제외한 모든 스트림에서 동일하게 제공하는 메서드다.
mapToInt
와 이하 내용은 유사하다.
mapToObj 메서드
- 기본형 특화 스트림을
Stream
으로 변환해주는 메서드다.
-
위의 이미지는
IntStream
에서mapToObj
를 살펴본 내용인데IntFunction<U>
를 파라미터로 받는다.- 기본형 특화 스트림의 종류에 따라
LongFunction<U>
,DoubleFunction<U>
이 사용된다. - 인터페이스의 이름에서 알 수 있듯이 기본
Function<T, R>
에서 T가 int, long, double이고 R이 U가 된다.
- 기본형 특화 스트림의 종류에 따라
예제
-
아래 예제에서 사용되는 내용들은 다양한 상황을 연출하기 위해서 임의로 만들어진 내용이므로 특정 속성에 대해서 왜 저 타입이 사용되었는지 의문을 가지지 말자!
-
예제 코드에서 사용되고 있는 스펙
- Java 15 preview (record라는 새로운 클래스 개념을 사용하기 위해서 해당 프리뷰 버전을 사용. 하위 버전의 경우는 일반 클래스를 생성한 후 getter를 만들고 사용하면 됨) - 참고
- JUnit 5
- Gradle 6.7
기본 데이터 생성
public record Sports(int no, String name, int burnedCalories, boolean isIndoor) {
}
public record ProductEntity(Long id, String name, String category, LocalDate createdAt) {
}
public record Car(Long id, String name, LocalDate createdAt) {
}
private final MapUsage mapUsage = new MapUsage();
private final List<Sports> sports = List.of(
new Sports(1, "걷기", 40, false),
new Sports(2, "스쿼시", 126, true),
new Sports(3, "런닝머신", 110, true),
new Sports(4, "등산", 84, false));
private final List<ProductEntity> productEntities = List.of(
new ProductEntity(1L, "Aventador", "CAR", LocalDate.of(2018, 3, 28)),
new ProductEntity(2L, "Ramen", "FOOD", LocalDate.of(2001, 4, 11)),
new ProductEntity(3L, "Lego", "TOY", LocalDate.of(1980, 11, 5)),
new ProductEntity(4L, "Veyron", "CAR", LocalDate.of(2009, 12, 9)),
new ProductEntity(5L, "Modern Java in Action", "BOOK", LocalDate.of(2019, 8, 1)));
문자열을 문자열 길이로 변환(String -> Integer)
String
타입의 데이터를 그 길이로 변환하는 메서드를 만들어보자.
public List<Integer> getLength(List<String> strings) {
return strings.stream()
.map(String::length)
.collect(Collectors.toList());
}
- 주어진 단어들의 길이를 반환해보자.
@Test
@DisplayName("문자열을 문자열 길이로 변환(String -> Integer)")
void getLengthTest() {
final var strings = List.of("banana", "apple", "orange");
final var expected = List.of(6, 5, 6);
final var result = mapUsage.getLength(strings);
assertIterableEquals(expected, result);
}
- 테스트가 성공한다.
정수를 문자열로 변환(int -> String)
int
기본 타입을 주어진 문자열과 합쳐서 반환하는 메서드를 만들어보자.
public List<String> generateStrings(String prefix, int size) {
return IntStream.range(0, size)
.mapToObj(i -> prefix + i)
.collect(Collectors.toList());
}
- 여기서 0부터 size까지의 정수 스트림을 만들기 위해
IntStream.range()
메서드를 사용했다. - 기본형 특화 스트림(
IntStream
)을 일반 스트림으로 변환하기 위해mapToObj()
메서드가 사용되었다. - waiting- 이라는 문자열에 0부터 size까지의 숫자를 붙인 문자열을 반환해보자.
@Test
@DisplayName("정수를 문자열로 변환(int -> String)")
void generateStringsTest() {
final var expected = List.of("waiting-0", "waiting-1", "waiting-2");
final var result = mapUsage.generateStrings("waiting-", 3);
assertIterableEquals(expected, result);
}
- 테스트가 성공한다.
실외 스포츠들의 이름 가져오기(Sports -> String)
Sports
타입의 리스트를String
타입으로 변환하는 메서드를 만들어보자.
public List<String> getSportsNamesByIndoor(List<Sports> sports, boolean isIndoor) {
return sports.stream()
.filter(s -> s.isIndoor() == isIndoor)
.map(Sports::name)
.collect(Collectors.toList());
}
- 실외 스포츠들의 이름을 조회해보자.
@Test
@DisplayName("실외 스포츠들의 이름 가져오기(Sports -> String)")
void getSportsNamesByIndoorTest() {
final var expected = List.of("걷기", "등산");
final var result = mapUsage.getSportsNamesByIndoor(sports, false);
assertIterableEquals(expected, result);
}
- 테스트가 성공한다.
스포츠들의 칼로리 평균 구하기(Sports -> int)
Sports
타입의 리스트를 기본 정수 자료형(int
)으로 변환하는 메서드를 만들어보자.
public OptionalDouble getAverage(List<Sports> sports) {
return sports.stream()
.mapToInt(Sports::burnedCalories)
.average();
}
-
위 코드에서
mapToInt()
메서드를 통해서 레퍼런스 타입의 일반 스트림을 기본형 특화 스트림인IntStream
으로 변환했다. -
IntStream
의 종단 연산 중 하나인average()
메서드를 사용해서 평균을 구했다. -
average()
메서드의 경우에는 종단 연산에 대한 글을 쓸 때 다시 언급할 예정이다. 간단하게 평균을 구하는 메서드이고 반환 타입이OptionalDouble
인데 이것은 기본형 특화Optional
이다.- sports가 빈 리스트일 경우에 평균을 구하는 로직에서
Division by Zero
가 발생할 수 있으므로 이 경우에 결괏값이 없을 수 있다는 것을 명시적으로 알려주기 위해서OptionalDouble
형태로 반환해주는 것 같다.
- sports가 빈 리스트일 경우에 평균을 구하는 로직에서
-
주어진 스포츠들의 칼로리 평균을 구해보자.
@Test
@DisplayName("스포츠들의 칼로리 평균 구하기(Sports -> int)")
void getAverageTest() {
final var expected = 90.0;
final var result = mapUsage.getAverage(sports);
assertTrue(result.isPresent());
assertEquals(expected, result.getAsDouble());
}
- 테스트가 성공한다.
getAverage(sports)
의 반환형이OptionalDouble
이기 때문에 먼저 result가 비어있는지 체크하기 위해result.isPresent()
를 사용했고 이후에result.getAsDouble()
메서드를 사용해서 실제값을 가져오도록 했다.
문자열 대문자로 변환(String -> String)
String
타입을 같은String
타입으로 변환해보자.
public List<String> getUpperStrings(List<String> strings) {
return strings.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
}
- 스트림의 특성상 원본 strings의 데이터는 변하지 않는다.
- 주어진 문자열들을 대문자로 변환해보자.
@Test
@DisplayName("문자열 대문자로 변환(String -> String)")
void getUpperStringsTest() {
final var strings = List.of("banana", "apple", "orange");
final var expected = List.of("BANANA", "APPLE", "ORANGE");
final var result = mapUsage.getUpperStrings(strings);
assertIterableEquals(expected, result);
}
- 테스트가 성공한다.
상품(Entity)에서 자동차(CAR)를 분류한 후 DTO로 변환(ProductEntity -> Car)
- 좀 더 실무에서 사용될 법한 간단한 예시를 만들어보자.
public List<Car> getCarsFromProductEntities(List<ProductEntity> productEntities) {
return productEntities.stream()
.filter(p -> "CAR".equals(p.category()))
.map(p -> new Car(p.id(), p.name(), p.createdAt()))
.collect(Collectors.toList());
}
- 상품(Entity)을 데이터베이스에서 가져왔다고 하고 다른 레이어에서 사용할 수 있도록 DTO로 변환해보자.
@Test
@DisplayName("상품(Entity)에서 자동차(CAR)를 분류한 후 DTO로 변환(ProductEntity -> Car)")
void getCarsFromProductEntitiesTest() {
final var expected = List.of(
new Car(1L, "Aventador", LocalDate.of(2018, 3, 28)),
new Car(4L, "Veyron", LocalDate.of(2009, 12, 9)));
final var result = mapUsage.getCarsFromProductEntities(productEntities);
assertIterableEquals(expected, result);
}
- 테스트가 성공한다.
이상 Stream API에서 사용하는 중간 연산 중 가장 중요하고 많이 사용되는 map
관련 메서드에 대해 살펴보았다.
'프로그래밍 > Java' 카테고리의 다른 글
[Stream API] 중간 연산 - peek 메서드 (0) | 2021.01.07 |
---|---|
[Stream API] 중간 연산 - flatMap 메서드 (0) | 2021.01.05 |
[Stream API] 중간 연산 - slicing 관련 메서드들 (0) | 2021.01.03 |
[Stream API] 중간 연산 - distinct 메서드 (0) | 2020.12.31 |
[Stream API] 중간 연산 - filter 메서드 (2) | 2020.12.30 |
- Total
- Today
- Yesterday
- 변경사항
- jdk14
- java
- 토이 프로젝트
- lambda
- flatMapToInt
- 충북 콕! 콕!
- Stream API
- flaMap
- 람다
- 목표
- import문
- #배열 #array #map 함수
- flatMapToDouble
- java14
- IntelliJ
- #예제 #example #가계부 #Account Book
- 개발자
- 다짐
- 익명 클래스
- Java8
- flatMapToLong
- #React #ReactJS #리액트
- 중간 연산
- modern java
- 스트림
- 회고
- mapToObj
- 자바
- 계획
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |