프로그래머스 사이트에서 알고리즘 문제를 풀다 보면
다른 사람의 답변에서 Stream이 자주 쓰이는 것을 볼 수 있다.
주어진 조건 내에서 해결하겠다는 고집으로 배열만을 사용하고, 정렬을 직접 구현하려 했는데
문제 난이도가 올라 갈수록 한계에 부딪혀 점점 API의 필요성을 느끼게 되었다.
이전에는 컬렉션에 대해서 알아봤기 때문에 이번엔 Stream을 공부하며
알고리즘 문제를 더 간편하게 해결해보려 한다.
🚀 Java Stream이란?
Java의 Stream API는 컬렉션(List, Set 등)과 배열을 더 쉽게 다룰 수 있도록 도와주는 기능이다.
함수형 프로그래밍 스타일을 지원하며, 코드의 가독성을 높이고 성능을 최적화할 수 있다.
✔ 주요 특징
- 데이터를 반복문 없이 처리 (for 없이 map, filter, forEach 활용 가능)
- 원본 데이터를 변경하지 않음 (불변성 유지)
- 병렬 처리 가능 (parallelStream() 사용 시 멀티코어 CPU 활용)
- 중간 연산(Intermediate)과 최종 연산(Terminal)으로 나뉨
1. Stream의 기본 개념
Stream은 크게 3단계로 구성.
1️⃣ 생성 (데이터 소스에서 스트림 생성)
2️⃣ 중간 연산 (데이터 변환, 필터링 등 가공)
3️⃣ 최종 연산 (결과 반환, 출력, 집계 등)
여기서 생성은 .stream() 으로 Stream을 생성하는 과정을 의미하고 주요 기능은 크게 중간 연산과 최종 연산으로 나뉜다.
2. Stream 주요 연산
(1) 중간 연산 (Intermediate)
데이터를 가공(변환, 필터링)하지만, 최종 연산이 실행될 때까지 수행되지 않음
(= "lazy evaluation" → 최종 연산을 만나야 실행됨)
filter(Predicate) | 특정 조건을 만족하는 요소만 선택 | stream.filter(n -> n % 2 == 0) |
map(Function) | 요소를 변환 | stream.map(n -> n * 2) |
sorted() | 정렬 (기본 or Comparator) | stream.sorted() |
distinct() | 중복 제거 | stream.distinct() |
limit(n) | 처음 n개만 선택 | stream.limit(3) |
skip(n) | 처음 n개 건너뛰기 | stream.skip(2) |
(2) 최종 연산 (Terminal)
스트림의 요소를 처리하고 결과를 반환하는 연산 (한 번 실행되면 스트림이 종료됨)
forEach(Consumer) | 모든 요소를 반복 | stream.forEach(System.out::println) |
collect(Collectors) | 리스트, 맵 등의 컬렉션으로 변환 | stream.collect(Collectors.toList()) |
count() | 요소 개수 반환 | stream.count() |
anyMatch(Predicate) | 조건을 만족하는 요소가 하나라도 있으면 true | stream.anyMatch(n -> n > 10) |
allMatch(Predicate) | 모든 요소가 조건을 만족하면 true | stream.allMatch(n -> n > 0) |
noneMatch(Predicate) | 모든 요소가 조건을 만족하지 않으면 true | stream.noneMatch(n -> n < 0) |
findFirst() | 첫 번째 요소 반환 (Optional) | stream.findFirst() |
findAny() | 임의의 요소 반환 (Optional) | stream.findAny() |
reduce(BinaryOperator) | 누적 연산 수행 | stream.reduce((a, b) -> a + b) |
3. parallelStream()을 활용한 병렬 처리
parallelStream()을 사용하면 여러 CPU 코어를 활용해 더 빠르게 데이터 처리 가능!
import java.util.Arrays;
import java.util.List;
public class ParallelExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 병렬 스트림 활용 (멀티코어 CPU 사용)
numbers.parallelStream()
.map(n -> n * 2)
.forEach(System.out::println);
}
}
4. Stream을 사용할 때 주의할 점
- 스트림은 1회성
- 한 번 최종 연산을 수행하면 다시 사용할 수 없음.
- 데이터가 많으면 parallelStream() 사용 고려
- 대량 데이터 처리 시 병렬 스트림을 활용하면 성능 향상 가능.
- 기본형 스트림 (IntStream, LongStream, DoubleStream) 활용
- Stream<Integer>보다 IntStream을 사용하면 오토박싱/언박싱 비용을 줄일 수 있음.
5. 결론
- Stream을 사용하면 데이터 처리를 더 간결하게 할 수 있음.
- map(), filter(), sorted() 등을 조합하여 효율적인 데이터 변환이 가능함.
- 최적의 성능을 위해 parallelStream()과 기본형 스트림(IntStream)을 활용하는 것도 고려해야 함.
https://school.programmers.co.kr/learn/courses/30/lessons/68644
프로그래머스
SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
import java.util.*;
class Solution {
public int[] solution(int[] numbers) {
Set<Integer> sumSet = new TreeSet<>();
int len = numbers.length;
for(int i=0;i<len;i++){
for(int j=i+1;j<len;j++){
sumSet.add(numbers[i] + numbers[j]);
}
}
return sumSet.stream().mapToInt(Integer::intValue).toArray();
}
}
import java.util.*;
class Solution {
public int[] solution(int[] numbers) {
List<Integer> sumList = new LinkedList<>();
int len = numbers.length;
for(int i=0;i<len;i++){
for(int j=i+1;j<len;j++){
sumList.add(numbers[i] + numbers[j]);
}
}
return sumList.stream()
.mapToInt(Integer::intValue)
.distinct()
.sorted()
.toArray();
}
}
위 문제에서 사용된 Set과 Stream을 이용한 소스코드이다.
Set과 List를 이용하였고, 이전에 작성한 컬렉션 관련 포스팅 기반으로 작성하면
문제에서 중복된 값은 제거하고 정렬하는 방식을 사용한다.
이에 정렬과 중복 제거를 위해 TreeSet을 사용하고 Stream을 통해 배열로 변환한다.
물론 List를 사용하고 Stream에서 중복 제거와 정렬도 할 수 있지만,
실행 시간에서 근소한 차이를 보여주기 때문에 목적에 맞는 컬렉션인 Set을 사용하는 것을 선택한다.
'Java > 공부' 카테고리의 다른 글
효율적인 리팩토링을 위한 전략 (0) | 2025.01.24 |
---|---|
자바 컬렉션 프레임워크 종류와 사용 사례 (0) | 2025.01.23 |
의존성 주입에 대한 관점 (0) | 2025.01.21 |