※ 일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용이 있으면 지적해주시면 감사하겠습니다.
Stream API
Stream API는 Data를 파이프 라인 형식으로 처리하기 위한 API이다. Collection, 배열, 파일등 데이터의 집합체(Data Source)에서 각각의 요소를 꺼내서 그것을 처리의 흐름(Stream)에 전달하기 위한 구성을 제공한다.
Stream에 대해서 함수 조작을 한 결과를 Stream으로 반환하는 "중간조작"과 처리 결과를 Data로 반환하는 "종단조작"이 있다.
중간조작도 종단조작도 메소드 인수로 함수형 인터페이스를 받는 경우가 많으므로, 여기서 람다식의 지식을 이용하면 똑똑하게 코드를 작성할 수 있다.
import java.util.ArrayList;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// ①Data Source를 준비한다.
var list = new ArrayList<String>(
Arrays.asList("tokyo", "nagoya", "osaka", "fukuoka", "hokkaido", "okinawa"));
// ②스트림을 만든다.
list.stream().
filter(s -> s.length() > 5). // ③중간처리를 실행한다.
map(String::toUpperCase).
forEach(System.out::println); // ④종단처리를 실행한다.
// 結果:NAGOYA FUKUOKA HOKKAIDO OKINAWA
}
}
Stream API의 처리는 다음과 같이 구성되어 있다.
- Data Source 준비한다.
- Stream을 생성한다.
- 추출, 가공등의 중간 처리를 한다.
- 출력, 집계등의 종단 처리를 한다.
이 코드에 빗대어 설명하자면 다음과 같다.
- 먼저 ArrayList의 Data Source를 만든다.
- Stream을 생성한다. 여기서는 ArrayList<String>이 바탕이되므로 Stream 메소드도 Stream<String> 오브젝트를 반환한다.
- filter 메소드로 "문자수가 5보다 큰 겂만 취함" 그리고 map 메소드로 "대문자로 변환함" 중간 처리는 여러개 있어도 좋고, 생략해도 좋다.
- forEach 메소드로 얻은 값을 System.out::println메소드로 출력한다. 종단 처리는 생략할 수 없다.
Stream 만들기
Collection/배열로 생성
Collection의 경우
Collection.stream()
배열의 경우
Arrays.stream(T[])
Map의 경우
Map.entrySet().stream()
stream()메소드의 병렬판으써, parallelStream()메소드도 있다. stream을 parallelStream로 바꿔쓰는 것으로 병렬처리가 가능해진다.
다루는 요소수가 많은 경우, 병렬처리를 유효로하는 것으로 효율적인 처리가 가능한 경우가 있다. 기존의 Stream을 병렬화, 혹은 직렬화하는 것이 가능하다.
Stream 클래스로 Stream 생성
Stream 클래스로는 Stream 생성하기 위해 Factory Method가 있다. 기장 기본이되는 것은 지정된 가변 인수를 Stream으로 변환하는 of메소드이다.
var stream = Stream.of("tokyo","osaka","nagoya");
stream.forEach(System.out::println); // tokyo, osaka, nagoya
그 외에도 generate(), builder(), concat(), iterator()가 있다. 여기서는 설명을 생략하도록 하겠다.
프리미티브형의 Stream 생성
IntStream
int로 특수화된 스트림
LongStream
long으로 특수화된 스트림
DoubleStream
double으로 특수화된 스트림
IntStream.range(int start, int endExclusive)[제 2인수는 범위외 : 열린 공간]
IntStream.rangeClosed(int start,int endInclusive)[제 2인수는 범위내 : 닫힌 공간]
IntStream을 사용한 반복문 처리의 예는 아래와 같다. for문을 사용한 경우와 비교하면 더욱 심플해진다.
for(int i = 1; i <= 5 ; i++){
System.out.println(i);
}
IntStream.rangeClosed(1, 5).forEach(System.out::println);
Java의 제네릭스 모델 인수에서는 프리미티브형은 사용할 수 없기 때문에, Stream<int>와 같은 작성법은 에러가 발생한다.
중간처리
Stream에 처리되는 값을 추출, 가공하는 역할을 가진다. 중간 처리가 실행되는 것은 어디까지나 단말 처리가 호출되는 타이밍이므로, 호출될때마다 실행되는 것은 아니다.
filter
지정된 조건에 따라 값이 추출된다.
Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("t")).forEach(System.out::println); //tokyo
map
전달된 값을 가공한다.
Stream.of("tokyo", "nagoya", "osaka").map(s -> s.length)).forEach(System.out::println); //5, 6, 5
생성직후에 Stream<String>이었던 것이 map메소드 후에는 Stream<Integer>이 되어 있으므로 주의해야한다.
sorted
요소를 정렬해준다.
Stream.of("tokyo", "nagoya", "osaka").sorted().forEach(System.out::println); // nagoya, osaka, tokyo
Stream.of(2,1,3).sorted().forEach(System.out::println); // 1, 2, 3
기본적인 동작은 자연 순서에 따른 정렬이다. 문자열의 경우 사전 순서, 숫자는 크기에 따른 정렬이다. 자체 정렬 규칙을 지정하기 위해서는 정렬 규칙을 람다식으로 지정하면 된다. sorted()의 인수는 Comparator 인터페이스이다.
Stream.of("tokyo", "nagoya", "osaka").
sorted((str1, str2) -> str1.length() - str2.length()).forEach(System.out::println); // tokyo, osaka, nagoya
skip/limit
- skip : m번째까지의 요소를 잘라 획득
- limit : n+1번째 이후의 요소를 잘라 획득
IntStream.range(1, 10).skip(3).limit(5).forEach(System.out::println); // 4, 5, 6, 7, 8
peek
Stream의 도중 상태를 확인한다.
peek 메소드 자체는 Stream에 영향을 주지 않으므로, 주로 디버그용으로 사용된다.
Stream.of("tokyo", "nagoya", "osaka").peek(System.out::println).sorted().forEach(System.out::println);
//정렬전의 결과:tokyo, nagoya, osaka ← peek의 println
//정렬후의 결과:nagoya, osaka, tokyo ← forEach의 println
distinct
값의 중복을 제거해준다.
Stream.of("tokyo", "nagoya", "osaka", "osaka", "nagoya", "tokyo").distinct().forEach(System.out::println);
// tokyo, nagoya, osaka
종단처리
Stream에 처리되는 값을 마지막으로 출력, 집계하기 위한 역할을 가진다. Stream은 종단 처리의 호출을 트리거로 하여 최종적으로 정리하여 처리해주기 때문에, 중간 처리와 달리 종단처리는 생략할 수 없다.
종단처리한 Stream을 재이용하는 것은 안되기 때문에, 다시 Stream 처리를 하고 싶은 경우, Stream 그 자체를 Data Source에서 재생성할 필요가 있다.
forEach
각각의 요소를 순서대로 처리한다.
Stream.of("tokyo", "nagoya", "osaka").forEach(v -> System.out.println(v)); // tokyo, nagoya, osaka
Stream.of("tokyo", "nagoya", "osaka").forEach(System.out::println); // tokyo, nagoya, osaka
findFirst
맨 앞의 값을 획득한다.
System.out.println(Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("t")).findFirst().orElse("empty"));
// tokyo
빈 Stream의 경우가 있으므로, findFirst메소드의 반환값은 Optional형이다.
System.out.println(Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("a")).findFirst().orElse("empty"));
// empty
anyMatch/allMatch/noneMatch
값이 특정의 조건을 만족하는가를 판정한다.
순서대로 "조건식이 true가 되는 요소가 존재하는가", "조건식을 모두 true로 충족하는가", "조건식 모두 true가 되지 않는가"이다.
System.out.println(Stream.of("tokyo", "nagoya", "osaka").anyMatch(v -> v.length() == 5)); // true
System.out.println(Stream.of("tokyo", "nagoya", "osaka").allMatch(v -> v.length() == 5)); // false
System.out.println(Stream.of("tokyo", "nagoya", "osaka").noneMatch(v -> v.length() == 5)); // false
toArray
Stream 처리의 결과를 문자열 배열로 변환해준다.
var list = Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("t")).toArray();
collect(콜렉션 변환)
Stream 처리의 결과를 Collection으로 변환한다. collect메소드에는 Collectors 클래스로 제공되고 있는 변환 메소드를 전달한다.
List으로의 변환은 toList, Set으로의 변환은 toSet, Map으로의 변환은 Map을 사용한다.
var list = Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.startsWith("t")).collect(Collectors.toList());
collect 메소드는 콜렉션 변환 전용의 메소드라기 보다는 리덕션 처리를 행하는 메소드도 된다. 리덕션 처리의 경우는 뒤에서 설명하도록 하겠다.
min/max
최소값, 최대값을 구한다. 인수에는 비교 규정(Comparator)을 지정할 필요가 있다. 반환값이 Optional형이므로, orElse경우하게 된다.
System.out.println(Stream.of(1, 3, 2).min((int1, int2) -> int1 - int2).orElse(-1)); // 1
System.out.println(Stream.of(1, 3, 2).min((int1, int2) -> int2 - int1).orElse(-1)); // 3
System.out.println(Stream.of(8, 7, 9).max((int1, int2) -> int1 - int2).orElse(-1)); // 9
System.out.println(Stream.of(8, 7, 9).max((int1, int2) -> int2 - int1).orElse(-1)); // 7
count
요소의 개수를 구한다.
System.out.println(Stream.of("tokyo", "nagoya", "osaka").filter(s -> s.length() > 5).count()); // 1
reduce
Stream의 값을 하나로 묶는다(Reduction).reduce 메소드는 3종류의 오버로드를 제공하고 있다.
인수 1개인 경우
Optional reduce(BinaryOperator accumulator);
반환값이 Optional형이 되므로, orElse 경유하게 된다.
인수는 연산결과를 저장하기 위한 변수 result, 각각의 요소를 받기 위한 변수 str이 존재한다.
System.out.println(
Stream.of("tokyo", "nagoya", "osaka").sorted()
.reduce((result, str) -> { return result + "," + str;}).orElse("")); // nagoya,osaka,tokyo
인수 2개인 경우
T reduce(T identity, BinaryOperator accumulator);
제 1인수에는 초기값을 받을 수가 있다. 결과는 null이 아닌 것을 명시하기 비해 Optional형이 아니게 된다. 따라서 OrElse는 불필요하다.
System.out.println(
Stream.of("tokyo", "nagoya", "osaka").sorted()
.reduce("hokkaido", (result, str) -> { return result + "," + str;})); //hokkaido,nagoya,osaka,tokyo
인수 3개인 경우
U reduce(U identity, BiFunction accumulator, BinaryOperator combiner);
조금 어려울지도 모르겠지만, Stream의 요소형과 마지막 요소형이 다른 경우에 사용한다. 여기서 예는 생략한다.
collect(리덕션 조작)
Stream내의 요소를 Collection등으로 정리한다. reduce가 Stream내의 요소를 int, String과 같은 단일 값으로 리덕션 처리하는 것에 반해, collect는 Collection/StringBuilder과 같이 가변적인 변수에 값을 축적한 후에 반환한더(가변 리덕션).
참고자료
'IT > 언어' 카테고리의 다른 글
[JavaScript] Promise의 사용법 ( + asycn / await의 사용법 ) (0) | 2023.02.05 |
---|---|
[JUnit] JUnit5의 테스트 클래스 작성 (0) | 2023.01.31 |
[Java] Optional 사용법 (0) | 2023.01.29 |
[Vue.js] Vue Router과 간단한 사용법 (0) | 2023.01.16 |
[Vue.js] export default에 대해서 (0) | 2023.01.16 |