개요
스트림 API는 컬렉션, 배열 등의 요소를 좀 더 간편하고 알기 쉽게 다룰 수 있도록 해주는 기능이다.

사전적 의미처럼, 스트림 API를 사용하면 메소드에 메소드에 메소드를 잇는 방식(Method Chaining)으로 코드를 짤 수 있게 된다.
우리가 마실 수 있는 물을 얻게 되는 과정을 생각해보면 이해가 조오오오금 쉬워질지도 모르겠다.

보통의 물은 여러가지 요인으로 인해 마시기 어렵지만, 우리의 몸에 맞게끔 고오급 정수 과정을 거치면 마셔도 문제 없는 깨끗한 물을 얻을 수 있게 된다. 스트림이 딱 그것이다. 오염된 물에서 중금속을 걸러내고, 소독 처리를 한다. 오염된 물이 데이터 덩어리이고, 정수 과정이 중간 연산, 물을 마시는 것이 최종 연산이라 할 수 있겠다.
더 모르겠는데요!
사전 준비
거의 모든 기능에 람다식을 사용하므로, 람다식에 관한 이해가 있어야 한다.
함수형 프로그래밍 패러다임에 대해서도 알고 있으면 좋다.
사용
import java.util.ArrayList;
record Fruit(String name, int price) {}
public class Test {
public static void main(String[] args) {
ArrayList<Fruit> fruitsBasket = new ArrayList<>(Arrays.asList(
new Fruit("사과", 7000),
new Fruit("망고스틴", 5000),
new Fruit("바나나", 3000),
new Fruit("파인애플", 7500),
new Fruit("딸기", 12000),
new Fruit("복숭아", 4000)));
}
}
위와 같은 단순한 코드가 있다. 여기서 나는 6000원 이하인 과일만 골라서 콘솔에 표시하려고 한다. 그렇다면 코드는...
for (Fruit fruit : fruitsBasket) {
if (fruit.price() <= 6000) {
System.out.println(fruit);
}
}
다음과 같이 만들게 된다. 하지만 스트림을 사용하면,
fruitsBasket.stream()
.filter(fruit -> fruit.price() <= 6000)
.forEach(fruit -> System.out.println(fruit));
정도로 한 줄에 축약하는 것이 가능하다. 반복문이나 조건문의 블록{} 도 없고, break나 continue, 다른 내부 변수조차도 사용할 수가 없다. 사용할 필요도 없고, 오롯이 내가 구현하고자 하는 코드에만 집중할 수 있다.
그게 그거 같은데요? 되려 for문 쓰는 쪽이 더 낫지 않음?
코드는 사람 수만큼 많고, 그 중에 정답은 없다... 그저 더 나은 방법만이 있을 뿐... 실제로 위와 같은 단순한 코드에서는 for문이 더 직관적이고 알기 쉬울 수 있다.
스트림을 쓰는 이유는 대표적으로 다음과 같다.
1. 복잡한 코드도 충분히 간결하게 표현할 수 있다.
2. 함수형 프로그래밍 패러다임을 따를 수 있다.
3. 병렬 처리에 대한 메소드가 따로 있기 때문에 스레드 안전하다. for문으로도 구현할 수 있지만 구현하다보면 삶이 고달프다...
4. 병렬 처리가 되고, 연산이 필요한 때에만 해당 요소를 갖다 쓰기 때문에(지연 평가) 엄청나게 큰 데이터 덩어리도 무리없이 소화할 수 있다.
그렇다고 스트림이 신이고 무적이라는 것은 아니다. 다음과 같은 경우에는 for문이 유리하다.
1. 단순한 형태의 반복문(탐색, 순회 등)이나 원시 타입(int, char...)을 요소로 사용하는 연산은 for를 사용한 반복문이 훨씬 빠르다.
2. 복잡하게 구조화가 이루어진 코드에서는 for문이 더 가독성이 나을 수 있다.
3. 스트림에서는 람다식으로 인해 디버깅이 빡세지만, for문은 그 문제에서 자유롭다.
자바 버전도 어느덧 22를 바라보고 있기에, 스트림의 기능이 분명 8 때보다는 개선되었다고 생각한다. 연산이 아주 중요한 상황이 아니라면 어느 쪽을 써도 괜찮을 것 같긴 한데...
대표적인 메소드
스트림은 [ stream().중간연산().최종연산() ] 의 형태로 사용한다. 중간 연산 메소드를 통해 스트림 내부의 요소를 변환하고, 걸러내게 되며, 최종 연산 메소드를 통해 해당 스트림에 대한 마무리 작업을 실행한다.
중간 연산 메소드
중간 연산 메소드는 모두 스트림 객체를 반환한다. 다만 기존 스트림 객체를 변화시키는 것이 아니라, 새로운 스트림 객체를 만들어서 반환하는 것이다.
a.filter(...).distinct(...) 라는 구문이 있다면, a스트림과 filter된 b스트림, 그리고 distinct까지 된 c스트림이 공존하는 것.
filter() : if문과 같다. 특정 조건을 만족하는 요소만으로 스트림을 정리한다.
map() : 이전 요소를 기반으로 하여 새로운 요소로 변환한다. 요소에 값을 더하는 것은 기본이고, int가 String이 될수도 있고, 그냥 메소드를 실행시키는 것도 가능하다.
flatMap() : 2차원 배열이나 2차원 컬렉션 같은 것들을 쭉 나열해서 1차원 스트림으로 만들어준다.
distinct() : 요소 간의 중복을 제거한다.
sorted() : 요소를 특정 순서에 맞춰 정렬한다.
limit() : 처음부터 주어진 인덱스까지로 요소의 수를 제한한다.
skip() : 처음부터 주어진 인덱스까지의 요소를 건너 뛴 나머지 요소로 스트림을 만든다.
최종 연산 메소드
최종 연산 메소드는 대부분 스트림과 관련 없는 타입을 반환하거나, 아예 반환하지 않는다.
forEach() : foreach문과 같다. 각 요소에 대해 각각의 동작을 실행한다.
peek() : forEach의 중간 연산 버전 느낌. 이전 연산의 요소들을 없애버리지 않고 그대로 전달해준다.
count() : 요소의 개수를 센다.
collect() : 요소를 컬렉션 객체로 반환한다.
reduce() : 요소를 전부 합해 하나의 값으로 반환한다.
min, max : 최대, 최솟값을 반환한다.
anyMatch, allMatch, noneMatch : 특정 조건을 만족하는 요소가 하나, 전부, 아예 없는지를 체크한다.
findFirst, findAny : 가장 첫번째 요소나 아무 위치에 있는 요소를 반환한다.
'프로그래밍 > Java' 카테고리의 다른 글
| 람다식 (Lambda expression) (0) | 2024.02.17 |
|---|---|
| 컬렉션 프레임워크 (0) | 2024.02.12 |
| 메모용 - Java 8~14 버전까지 (0) | 2024.02.12 |
| 스레드 ( Thread ) (0) | 2022.02.11 |
| 어노테이션 ( Annotation ) (0) | 2022.02.10 |