: 자바 8에서 추가된 기능으로 자바 컬렉션 데이터를 선언형으로 처리할 수 있도록한다.
사실 자바 컬렉션 데이터라고 했지만 배열과 같은 원시형 데이터 나열들도 스트림 처리를 할 수 있다. 이러한 스트림을 사용하면 좋은 점은 멀티스레드 코드를 사용하지 않고도 데이터를 투명하게 병렬도 처리할 수 있다는 점에서 굉장히 매력적이다.
스트림의 특징
이렇게 3가지로 줄일 수 있다.
Stream에 존재하는 다양한 메서드
List<Integer> list = List.of(1,2,3,4);
list.stream()....(중간 - 최종 연산)
int[] arr = {1,2,3,4};
Arrays.stream(arr)....(중간 - 최종 연산)
filter, sorted 와 같은 중간연산은 다른 스트림을 반환한다.(정확히는 메소드의 이름처럼 데이터를 필터링하거나 정렬한 후의 스트림을 반환)이를 통해서 여러개의 중간연산을 결합하여 원하는 흐름을 만들 수 있다.
code1
List<Integer> list = List.of(1, 2, 3, 4,5,6,7,8);
List<Integer> result = list.stream().filter(num -> num % 2 == 0).collect(Collectors.toList());
System.out.println(result);
code1 실행 결과
[2, 4, 6, 8]
code2
List<Integer> result2 = list.stream()
.filter(num -> num % 2 == 0)
.map(num -> num * 2)
.collect(Collectors.toList());
code2 실행 결과
[4, 8, 12, 16]
특히 코드2를 보면 filter와 map 중간 연산을 결합하여 원하는 결과가 만들어 졌음을 알 수 있다.
사실 이부분은 굉장히 분량이 많다. 특히나 외울것이 많은 데 그 이유는 방대한 양의 데이터를 원하는 모양으로 가공할 수 있도록 도와주는 최종연산의 메소드와 그에 맞는 인자들이 다양하기 때문이다.
그래서 이와 관련된 자세한 내용은 추후에 모던 자바인 액션 스터디를 기약하도록 하고 지금은 간단하게 사용할 수 있는 메소드와 인자를 설명하도록 하겠다.
List<Integer> result2 = list.stream()
.filter(num -> num % 2 == 0)
.map(num -> num * 2)
.collect(Collectors.toList());
long result3 = list.stream()
.filter(num -> num % 2 == 0)
.map(num -> num * 2)
.count();
list.stream()
.filter(num -> num % 2 == 0)
.map(num -> num * 2)
.forEach();
최종 연산은 중간연산이 모두 끝난 후 마지막에 스트림을 모으는 역할을 하는 연산으로 이 기능이 끝난후에는 스트림이 종료된다.
동작순서
위에서 사용했던 코드를 고쳐서 동작 순서를 보도록 하자.스트림의 중간연산으로 peek를 지원해주는데 사실 peek는 중간연산이라기에 는 조금 애매한 것이 스트림에 있는 데이터의 흐름을 간단히 볼 수 있게 해주는 역할 만한다.
List<Integer> result2 = list.stream()
.peek(num->{
System.out.println("first : " + num);
})
.filter(num -> num % 2 == 0)
.peek(num-> System.out.println("second : " + num))
.map(num -> num * 2)
.peek(num-> System.out.println("finish : " + num))
.collect(Collectors.toList());
결과
first : 1
first : 2
second : 2
finish : 4
first : 3
first : 4
second : 4
finish : 8
first : 5
first : 6
second : 6
finish : 12
first : 7
first : 8
second : 8
finish : 16
데이터가 하나하나 순서대로 동작하고 있음을 볼 수 있다.
List<Integer> result2 = list.parallelStream()
.peek(num->{
System.out.println("first : " + num);
})
.filter(num -> num % 2 == 0)
.peek(num-> System.out.println("second : " + num))
.map(num -> num * 2)
.peek(num-> System.out.println("finish : " + num))
.collect(Collectors.toList());
System.out.println(result2);
result
first : 8
first : 4
first : 2
first : 7
first : 1
first : 5
second : 8
first : 6
second : 6
second : 4
finish : 12
first : 3
finish : 8
finish : 16
second : 2
finish : 4
보이는가 스트림을 병렬로 처리하는 parallelStream을 사용하니 순서가 예측할 수 없게 변하였다. 따라서 스트림의 병렬성을 볼 수 있는 좋은 코드가 된다.
성능 향상
솔직히 이와 관련해서 이야기 해야할 것이 많고 논쟁이 좀 있다. 스트림과 일반 반복문을 비교해서 생각해보자
List<Integer> list = List.of(1,2,3,4,5,6);
List<Integer> res1 = new ArrayList<>();
for(int i = 0; i<list.size(); i++){
if(list.get(i)%2 == 0){
res1.add(list.get(i));
}
}
List<Integer> res2 = list.stream()
.filter(num->num%2 == 0)
.collect(Collectors.toList());
위 두개의 코드는 같은 기능을 한다. 하지만 반복문을 사용했을 때 는 코드가 지저분한 느낌이 들지만 스트림을 사용하면 한줄로 만들어 버릴 수 있다. 하지만 성능적인 측면에서는 반복문이 현재의 경우 훨씬 빠르게 동작할 것이다.
자세한 내용은 아래에 링크를 통해서 성능에 관해서 살펴보도록...
https://jypthemiracle.medium.com/java-stream-api는-왜-for-loop보다-느릴까-50dec4b9974b