티스토리 뷰
Java Stream API 란?
Java8 API에 새로 추가된 기능입니다. 컬렉션, 배열 등을 함수형 프로그래밍을 활용하여 반복 처리하도록 도와주는 API입니다.
그럼 Stream API를 왜 쓸까?
1. 컬렉션 데이터를 선언형으로 코드를 구현할 수 있습니다.
With For-Loop
List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish dish : menu) {
if(dish.getCalories < 400) {
lowCarloricDishes.add(dish);
}
}
With Stream API
List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
2. 골치아프게 멀티스레드 코드를 별도로 구현하지 않고, 병렬로 처리할 수 있습니다.(parallelStream() 과 같은 걸 사용해서)
List<String> lowCaloricDishesName =
menu.parallelStream()
.filter(d -> d.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
다만, Stream API는 매우 비싼 연산이기 때문에, 멋져보인다고 막 남용하면 안됩니다. 너무나도 좋은 글을 참고하였습니다.
Stream API을 활용하여 Collection 데이터를 연산하는 방법을 알아보겠습니다.
Stream 생성
Stream API의 생성 과정은 크게 세가지로 나뉘어집니다.
- Collection과 같은 데이터 소스로 Stream 생성
- Stream 파이프라인을 구성할 중간연산
- 파이프라인을 수행하고 결과를 만들 최종연산
List<String> words = Arrays.asList("show", "me", "the", "money", "money");
List<String> distinctWords =
words.stream() // 1. Stream 생성
.filter(word -> word.length() > 3) // 2. 중간연산
.distinct() // 2. 중간연산
.collect(Collectors.toList()); // 3. 최종연산
아래는 중간 연산과 최종 연산의 예입니다.
연산 종류 | 연산 | Return Type | Parameter | Descriptor |
중간 연산 | filter | Stream<T> | Predicate<T> | T -> boolean |
map | Stream<R> | Function<T, R> | T -> R | |
limit | Stream<T> | - | ||
sorted | Stream<T> | Comparator<T> | (T, T) -> int | |
distinct | Stream<T> | - |
연산 종류 | 연산 | Return Type | 설명 | |
최종 연산 | forEach | void | 스트림의 각 요소를 소비하면서 람다를 적용함 | |
count | long | 스트림의 요소 개수를 반환함 | ||
collect | Collection | 스트림을 Collection 형태로 반환함 |
함수형 인터페이스 사용
연산의 파라미터로 java.util.function 에 정의되어있는 함수형 인터페이스를 사용합니다. 대표적으로 Predicate, Consumer, Function 세개의 함수형 인터페이스를 알아봅니다.
Predicate
filter의 파라미터로 사용됩니다. 제네릭의 T 타입 객체를 파라미터로 받아서, boolean 타입을 리턴하는 test 추상 메소드를 정의합니다.
filter와 사용할 때, boolean 타입으로 반환 할 filtering 조건식을 filter 메소드의 인자로 넘겨줍니다.
@FunctionalInterface
public interface Predicate<T> {
...
boolean test(T t);
...
}
@Test
void test_predicate(){
List<Integer> integers = Arrays.asList(3, 4, 10, 11, 100, 101, 102, 500, 2000);
Predicate<Integer> overHundredPredicate = i -> (i > 100);
assertThat(integers.stream()
.filter(overHundredPredicate)
.count()).isEqualTo(4);
}
Consumer
인터페이스명처럼 소비만 하고, 별도의 리턴은 하지않습니다.
peek() 나 foreach() 메소드의 파라미터 값으로 활용 되고, 인자로 받아서 실행 후 별도 리턴없이 종료됩니다.
@FunctionalInterface
public interface Consumer<T> {
...
void accept(T t);
...
}
@Test
void test_consumer(){
List<Integer> integers = Arrays.asList(3, 4, 10);
Consumer<Integer> integerConsumer = System.out::println;
integers.stream()
.forEach(integerConsumer);
}
Function
인자를 받아서 특정 값으로 반환하는 apply 추상 메소드를 구현합니다.
주로 stream의 map()과 사용하여, 스트림 안의 값들을 하나씩 특정 값으로 변환합니다.
@FunctionalInterface
public interface Function<T, R> {
...
R apply(T t);
...
}
@Test
void test_function(){
List<Integer> integers = Arrays.asList(3, 4, 10);
Function<Integer, String> integerToString = i -> i.toString();
List<String> stringList =
integers.stream()
.map(integerToString)
.collect(Collectors.toList());
}
Stream 특징
일회성
Stream은 한번 소비되면 재활용이 불가능합니다. 아래와 같이 한번 사용 후 재사용 시 IllegalStateException 예외를 던집니다.
@Test
void should_ThrowIllegalStateException_When_CallStreamTwice() {
List<String> words = Arrays.asList("show", "me", "the", "money", "money");
Stream<String> stream =
words.stream()
.filter(word -> word.length() > 3)
.distinct();
assertThat(stream.findFirst().get()).isEqualTo("show"); // 1번째 호출
assertThrows(IllegalStateException.class,
() -> stream.findFirst(), // 2번째 호출
"stream has already been operated upon or closed");
}
실무에서 Stream API 사용해보기
Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
public class LoginHistory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idx;
@Column(columnDefinition = "datetime DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime createdAt;
...
DTO
public class LoginHistoryFindResponseDto {
...
public static LoginHistoryFindResponseDto from(LoginHistory loginHistory){
return LoginHistoryFindResponseDto
.builder()
.createdAt(loginHistory.getCreatedAt())
.deviceType(loginHistory.getDevice())
.loginTag(loginHistory.getTag())
.build();
}
Service
아래와 같이 Service Layer에서 조회한 Entity List를 DTO List로 반환 하는 용도로 사용합니다.
@Service
@RequiredArgsConstructor
public class LogService {
private final LoginHistoryRepository loginHistoryRepository;
...
@Transactional(readOnly = true)
public List<LoginHistoryFindResponseDto> findAllLoginHistory(User user){
List<LoginHistoryFindResponseDto> loginHistories =
loginHistoryRepository.findAllByUser(user)
.stream()
.map(LoginHistoryFindResponseDto::from)
.collect(Collectors.toList());
return loginHistories;
}
'Programming > JAVA' 카테고리의 다른 글
Java NIO ByteBuffer 사용해보기 (0) | 2021.04.22 |
---|---|
Immutable Object(불변 객체) (405) | 2021.04.06 |
String, StringBuffer, StringBuilder의 차이점이 뭘까? (0) | 2020.08.07 |
네이버 책 검색 OPEN API 활용하기 (820) | 2016.06.23 |
[JAVA] TCP 1:1 채팅 (0) | 2016.04.18 |