- Published on
- •👁️
Java Stream API (미완)
- Authors

- Name
- River
상세 설명
스트림(Stream)이란?
- 데이터를 처리하는 컨베이어 벨트
- 데이터 소스를 추상화하여, 데이터를 다루는 방법에 통일성을 제공하는 API
- 리스트든, 배열이든, 파일이든 모두
.stream()으로 동일하게 처리 가능
- 리스트든, 배열이든, 파일이든 모두
- "어떻게"가 아닌 "무엇을" 처리할지 명시하는 선언형 프로그래밍 지원
전통적인 방식
List<String> names = new ArrayList<>(); for (Employee emp : employees) { if (emp.getSalary() > 5000) { names.add(emp.getName().toUpperCase()); } }스트림
List<String> names = employees.stream() .filter(emp -> emp.getSalary() > 5000) .map(emp -> emp.getName().toUpperCase()) .collect(Collectors.toList());
Java 스트림은 함수형 프로그래밍 패러다임을 Java에 도입한 강력한 도구입니다. 불변성, 지연 평가, 병렬 처리 등의 특성을 활용하여 효율적이고 안전한 데이터 처리가 가능합니다.
스트림의 주요 특징
불변성 (Immutability)
스트림 연산은 원본 데이터가 아닌 별도의 스트림을 통해 결과를 생성
여러 스레드에서 동시에 사용해도 안전하다.
이러한 특성 덕분에 병렬 처리가 쉽다
Optional<String> result = words.parallelStream() // 이것만 바꾸면 된다. .filter(word -> expensiveOperation(word)) .findFirst();
지연 평가 (Lazy Evaluation)
List<String> words = Arrays.asList("apple", "banana", "cherry", "date"); Stream<String> stream = words.stream() .filter(word -> { System.out.println("Filtering: " + word); // 이 시점에선 실행 안됨! return word.length() > 4; }) .map(word -> { System.out.println("Mapping: " + word); // 이것도 실행 안됨! return word.toUpperCase(); }); System.out.println("스트림 생성 완료!"); // 여기까지는 데이터가 흐르지 않는다. // 최종 연산을 호출 => 데이터가 흐른다. List<String> result = stream.collect(Collectors.toList());스트림 생성 완료! Filtering: apple Mapping: apple Filtering: banana Mapping: banana Filtering: cherry Mapping: cherry Filtering: date위의 예시처럼 최종 연산이 호출되기 전까지 중간 연산은 실행되지 않는다.
즉, 스트림 생성은 스트림 설계도를 완성하는 것이지, 데이터를 흘려보내지 않는 상태이다.
limit(3)이 있으면 3개만 처리하고 나머지는 아예 건드리지 않는다.limit(),findFirst(),findAny(),anyMatch(),allMatch()등
이를 통해 불필요한 연산을 최소화 가능
- 사실 for문과 break 등의 조합으로도 성능은 거의 비슷하다.
일회성 (Single Use)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Stream<Integer> stream = numbers.stream(); // 첫 번째 사용 => 5 long count = stream.count(); // 두 번째 사용 => 에러 발생! int sum = stream.mapToInt(n -> n).sum();한 번 정의한 스트림은 1번만 사용할 수 있다
- 재사용 시 에러 발생 (IllegalStateException)
스트림은 한 번 사용(최종 연산 실행)하면 닫혀서 재사용 불가
필요 시 스트림을 다시 생성해야 한다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // 매번 새로운 스트림 생성 long count = numbers.stream().count(); int sum = numbers.stream().mapToInt(n -> n).sum(); // 또는 Supplier 패턴 사용 Supplier<Stream<Integer>> streamSupplier = () -> numbers.stream(); long count2 = streamSupplier.get().count(); int sum2 = streamSupplier.get().mapToInt(n -> n).sum();
스트림 생성 방법
컬렉션으로부터 생성 ⭐
List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream();가장 일반적인 방법
Collection 인터페이스
Collection (최상위 인터페이스) ├── List ├── Set └── Queue └── Deque (Queue를 상속)
Map 객체으로부터 생성
Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3); Stream<String> keyStream = map.keySet().stream(); // 키만 Stream<Integer> valueStream = map.values().stream(); // 값만 Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream(); // 키-값 쌍- Map은 Collection을 상속받지 않는다.
- Map 자체는
stream()메서드가 존재하지 않는다.- 다른 구조로 변형 후 사용한다.
배열으로부터 생성 ⭐
String[] arr = new String[]{"a", "b", "c"}; Stream<String> stream = Arrays.stream(arr); int[] intArr = {1, 2, 3, 4, 5}; IntStream intStream = Arrays.stream(intArr);Arrays.stream()메서드 사용- 기본 타입 배열은 특화된 스트림 생성 가능
- IntStream, LongStream, DoubleStream 등
직접 생성
Stream<String> stream = Stream.of("apple", "banana", "cherry");Stream.of()메서드로 임의의 개수의 요소로부터 스트림 생성
범위 지정 생성 ⭐
IntStream rangeStream = IntStream.range(1, 5); // 1, 2, 3, 4 IntStream closedStream = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5- IntStream, LongStream의
range()또는rangeClosed()사용
- IntStream, LongStream의
무한 스트림 생성
// "hello" 문자열이 무한히 생성되는 스트림 (5개로 제한) Stream<String> generatedStream = Stream.generate(() -> "hello").limit(5); // 1부터 시작해 2씩 더해지는 무한 스트림 (5개로 제한) Stream<Integer> iteratedStream = Stream.iterate(1, n -> n + 2).limit(5);Stream.generate()또는Stream.iterate()사용limit()과 함께 사용해야 한다.
특화 스트림
Primitive Specialized Streams (프리미티브 특화 스트림)
특화 스트림 종류
IntStreamLongStreamDoubleStream
Stream API는 기본적으로 객체를 다루지만, boxing과 unboxing으로 인해 성능이 저하될 수 있기 때문에 이러한 특화 스트림을 제공한다.
즉, 성능을 위해 박싱 없이 원시 타입을 직접 다루는 것이다.
boxing/unboxing 오버헤드 발생
Integer[] integerArr = {1, 2, 3, 4, 5}; int sum1 = Arrays.stream(integerArr) // Stream<Integer> .mapToInt(Integer::intValue) // IntStream으로 변환 (언박싱) .sum();boxing/unboxing 없음
int[] intArr = {1, 2, 3, 4, 5}; int sum2 = Arrays.stream(intArr).sum(); // 바로 계산
관련 생성 메서드
IntStream.range(1, 10); // 1부터 9까지 DoubleStream.of(1.2, 3.4, 5.6); // double 값들을 직접 전달 LongStream.iterate(0, n -> n + 2); // 무한 스트림편리한 특화 스트림 메서드
int[] numbers = {10, 20, 30, 40, 50}; int sum = Arrays.stream(numbers).sum(); OptionalDouble average = Arrays.stream(numbers).average(); OptionalInt max = Arrays.stream(numbers).max(); OptionalInt min = Arrays.stream(numbers).min(); IntSummaryStatistics stats = Arrays.stream(numbers).summaryStatistics();- 합계, 평균, 최댓값, 최솟값
summaryStatistics()으로 한번에 구할 수도 있다.
특화 스트림.boxed( )
boxed( ) 메서드
IntStream intStream = IntStream.of(1, 2, 3, 4, 5); Stream<Integer> boxedStream = intStream.boxed();특화 스트림을 일반 스트림으로 변환하는 메서드
특화 스트림(
IntStream…)은collect()메서드가 없다. ⇒ 변환 후 처리int[] numbers = {1, 2, 3, 4, 5}; List<Integer> list = Arrays.stream(numbers) .boxed() .collect(Collectors.toList());IntStream⇒Stream<Integer>
상세 설명
Stream의 중간 연산이란?
- 스트림 파이프라인의 중간 단계를 구성하는 연산
- 항상 새로운 스트림을 반환하여 메서드 체이닝 가능
- 중간 연산의 작업은 지연 평가되어 최종 연산 호출 시에만 실행된다.
주요 중간 연산
filter(Predicate<T> predicate)List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) // 짝수만 필터링 .collect(Collectors.toList()); // [2, 4, 6]- 주어진 조건에 맞는 요소만을 포함하는 새로운 스트림 반환
map(Function<T, R> mapper)List<String> words = Arrays.asList("apple", "banana", "cherry"); List<Integer> lengths = words.stream() .map(String::length) // 각 단어를 길이(Integer)로 변환 .collect(Collectors.toList()); // [5, 6, 6]- 각 요소를 주어진 함수에 따라 다른 값으로 변환
sorted()/sorted(Comparator<T> comparator)List<String> fruits = Arrays.asList("banana", "apple", "cherry"); List<String> sortedFruits = fruits.stream() .sorted() // 알파벳 순서로 정렬 .collect(Collectors.toList()); // ["apple", "banana", "cherry"] List<String> sortedByLength = fruits.stream() .sorted(Comparator.comparingInt(String::length)) // 문자열 길이 기준 .collect(Collectors.toList()); // ["apple", "cherry", "banana"]- 요소들을 순서 또는 주어진 비교자에 따라 정렬
- 비교자는 Comparator, 람다식 등을 사용 가능
distinct()List<Integer> duplicates = Arrays.asList(1, 2, 2, 3, 3, 3); List<Integer> distinctNumbers = duplicates.stream() .distinct() .collect(Collectors.toList()); // [1, 2, 3]- 중복된 요소를 제거하여 고유한 요소만 포함하는 스트림 반환
flatMap(Function<T, Stream<R>> mapper)⭐List<List<Integer>> nestedList = Arrays.asList( Arrays.asList(1, 2), Arrays.asList(3, 4) // [[1, 2], [3, 4]] ); List<Integer> flatList = nestedList.stream() .flatMap(Collection::stream) // 각 리스트를 스트림으로, 이를 다시 하나 스트림으로 합침 .collect(Collectors.toList()); // [1, 2, 3, 4]- 여러 상자를 모두 풀어서 하나의 큰 상자로 만드는 것
각 요소를 스트림으로 변환한 후, 생성된 모든 스트림을 하나로 평면화하여 반환
중요한 것은
flatMap은 Stream을 요구한다는 것이다.주로 중첩된 구조를 풀 때 사용 (이중 배열 ⇒ 단일 배열)
map()과flatMap()List<String> words = Arrays.asList("Hello", "World"); List<String[]> mapped = words.stream() .map(word -> word.split("")) // String을 String[]로 변환 .collect(Collectors.toList());- 결과 (이중 배열)
[["H","e","l","l","o"], ["W","o","r","l","d"]]
List<String> words = Arrays.asList("Hello", "World"); List<String> flatMapped = words.stream() .flatMap(word -> Arrays.stream(word.split(""))) // String[]을 Stream<String>으로 .collect(Collectors.toList());- 결과 (평면화)
["H","e","l","l","o","W","o","r","l","d"]
- 결과 (이중 배열)
limit(long maxSize)/skip(long n)List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> limited = numbers.stream() .limit(5) .collect(Collectors.toList()); // [1, 2, 3, 4, 5] List<Integer> skipped = numbers.stream() .skip(5) .collect(Collectors.toList()); // [6, 7, 8, 9, 10]limit- 스트림의 첫 번째부터 지정된 개수만큼의 요소만 포함
skip- 스트림의 첫 번째부터 지정된 개수만큼 요소를 건너뛰고 나머지 요소 포함
peek(Consumer<T> action)List<String> result = words.stream() .filter(word -> word.length() > 3) .peek(word -> System.out.println("After filter: " + word)) // 중간 결과 확인 .map(String::toUpperCase) .peek(word -> System.out.println("After map: " + word)) .collect(Collectors.toList());- 각 요소에 대해 지정된 동작을 수행하되, 스트림의 요소는 변경하지 않음
- 주로 디버깅이나 로깅 목적으로 사용
- 앞서 계속 언급했지만, 최종 연산 실행 전에
peek()도 마찬가지로 실행 안된다.
takeWhile( ) / dropWhile( )
filter()연산은 모든 요소를 다 검사하지만 이것은 다르다.takeWhile(Predicate predicate)List<Integer> numbers = Arrays.asList(1, 2, 3, 6, 4, 5, 7, 8); List<Integer> result = numbers.stream() .takeWhile(n -> n < 5) // 5보다 작은 동안만 계속 .collect(Collectors.toList()); // [1, 2, 3] (6에서 멈춤!)- 조건이 false가 되는 순간까지만 가져오기
dropWhile(Predicate predicate)List<Integer> numbers = Arrays.asList(1, 2, 3, 6, 4, 5, 7, 8); List<Integer> result = numbers.stream() .dropWhile(n -> n < 5) // 5보다 작은 동안 버리기 .collect(Collectors.toList()); // [6, 4, 5, 7, 8] (6부터 시작!)- 조건이 false가 되는 순간까지 다 버리기
mapToXXX
mapToInt,mapToLong,mapToDoublewords.stream() .mapToInt(String::length) .sum();- 특화 스트림으로 변환
특화 스트림으로 변환하는 이유는 박싱/언박싱 오버헤드를 줄이기 위함이다.
List<String> words = Arrays.asList("apple", "banana", "cherry"); int totalLength1 = words.stream() .map(String::length) // Stream<Integer> (박싱) .mapToInt(Integer::intValue) // IntStream으로 변환 (언박싱) .sum(); int totalLength2 = words.stream() .mapToInt(String::length) // 바로 IntStream .sum();
Comparator
메서드 참조 사용 (추천)
employees.stream() .sorted(Comparator.comparing(Employee::getName)) // 이름순 .sorted(Comparator.comparingInt(Employee::getAge)) // 나이순 .sorted(Comparator.comparingDouble(Employee::getSalary)) // 급여순 .collect(Collectors.toList());역순 정렬 (
.reversed())employees.stream() .sorted(Comparator.comparing(Employee::getSalary).reversed()) .collect(Collectors.toList());다중 조건 정렬 (
.reversed.Comparing())employees.stream() .sorted(Comparator.comparing(Employee::getDepartment) // 1차: 부서명 .thenComparing(Employee::getName)) // 2차: 이름 .collect(Collectors.toList());null 안전 정렬 (
Comparator.nullsLast())employees.stream() .sorted(Comparator.comparing( Employee::getName, Comparator.nullsLast(String::compareTo) )).collect(Collectors.toList());
주요 중간 연산
employees.stream()
.sorted((a, b) -> a.getName().compareTo(b.getName()))
.sorted((a, b) -> Double.compare(b.getSalary(), a.getSalary()))
.collect(Collectors.toList());
상세 설명
스트림의 최종 연산이란?
- 스트림 파이프라인의 마지막 단계를 담당하는 연산
- 스트림이 아닌 구체적인 결과값을 반환 (
void포함) - 최종 연산이 호출되어야 지연된 중간 연산들이 실제로 실행된다.
collect( )
collect(Collector<T, A, R> collector)- 스트림의 요소들을 수집하여 컬렉션이나 다른 형태로 변환
리스트로 수집
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Anna"); List<String> nameList = names.stream().collect(Collectors.toList());Collectors.toList()
맵으로 수집
Map<String, Integer> nameLengthMap = names.stream() .collect(Collectors.toMap(name -> name, String::length));Collectors.toMap(name -> name, String::length)- 이름 ⇒ 이름의 길이
그룹핑
Map<Character, List<String>> groupedByName = names.stream() .collect(Collectors.groupingBy(name -> name.charAt(0))); // {A=[Alice, Anna], B=[Bob], C=[Charlie]}Collectors.groupingBy(name -> name.charAt(0))- 이름의 첫 글자로 그룹화
문자열로 결합
String joinedNames = names.stream().collect(Collectors.joining(", ")); // "Alice, Bob, Charlie, Anna"Collectors.joining(", ")
reduce( )
reduce(T identity, BinaryOperator<T> accumulator)- 모든 스트림 요소를 하나의 값으로 집계하는 연산
- 초기값(identity)과 두 요소를 하나로 합치는 로직(BinaryOperator)을 제공
reduce(BinaryOperator<T> accumulator)- Optional 반환List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // 최댓값 Optional<Integer> max = numbers.stream() .reduce(Integer::max); // Optional[5] // 최솟값 Optional<Integer> min = numbers.stream() .reduce(Integer::min); // Optional[1] // 곱하기 Optional<Integer> product = numbers.stream() .reduce((a, b) -> a * b); // Optional[120] // 빈 스트림의 경우 Optional<Integer> empty = Arrays.<Integer>asList().stream() .reduce(Integer::sum); // Optional.empty()- Optional 반환 ⇒ 빈 스트림 가능성
reduce(T identity, BinaryOperator<T> accumulator)- 값 직접 반환List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); // 합계 (초기값 0) int sum = numbers.stream() .reduce(0, Integer::sum); // 15 // 곱하기 (초기값 1) int product = numbers.stream() .reduce(1, (a, b) -> a * b); // 120 // 문자열 연결 List<String> words = Arrays.asList("Hello", "World", "Java"); String result = words.stream() .reduce("", (a, b) -> a + " " + b); // " Hello World Java"- 초기값을 함께 넣어서 안전하다.
reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner)List<String> words = Arrays.asList("apple", "banana", "cherry"); // 모든 문자열 길이의 합 (타입 변환) int totalLength = words.stream() .reduce( 0, // 초기값 (Integer) (sum, word) -> sum + word.length(), // 누적 함수 (Integer, String) -> Integer Integer::sum // 결합 함수 (병렬 처리용) ); // 17- 이종 타입 + 병렬 처리용
- 실무 예시
복잡한 객체 집계
class Order { private double amount; private String status; // getters... } List<Order> orders = getOrders(); // 완료된 주문의 총 금액 double totalCompletedAmount = orders.stream() .filter(order -> "COMPLETED".equals(order.getStatus())) .reduce(0.0, (sum, order) -> sum + order.getAmount(), Double::sum);문자열 조합
List<String> names = Arrays.asList("김철수", "이영희", "박민수"); String nameList = names.stream() .reduce("참석자: ", (result, name) -> result + name + ", ");- "참석자: 김철수, 이영희, 박민수, "
최고 급여 직원 찾기
Optional<Employee> highestPaid = employees.stream() .reduce((emp1, emp2) -> emp1.getSalary() > emp2.getSalary() ? emp1 : emp2)조건부 집계
int evenSum = numbers.stream() .filter(n -> n % 2 == 0) .reduce(0, Integer::sum);
forEach( )
forEach(Consumer<T> action)- 스트림의 각 요소에 대해 지정된 동작을 수행
- 반환 값이 없으며(void), 주로 결과를 출력할 때 사용
Stream forEach vs Collection forEach
List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); // 컬렉션의 forEach names.forEach(System.out::println); // 순서 보장 // 스트림의 forEach names.stream().forEach(System.out::println); // 순서 보장 // 병렬 스트림의 forEach names.parallelStream().forEach(System.out::println); // 순서 보장 안됨!forEach vs forEachOrdered
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 병렬 처리 - 순서 무작위 numbers.parallelStream() .filter(n -> n > 5) .forEach(System.out::println); // 출력: 6, 8, 7, 10, 9 (순서 보장 X) // 병렬 처리 - 순서 보장 numbers.parallelStream() .filter(n -> n > 5) .forEachOrdered(System.out::println); // 출력: 6, 7, 8, 9, 10 (순서 보장)- 병렬 처리에서 중요
- 실무 예시
데이터 검증 및 로깅
List<User> users = getUsers(); users.stream() .filter(user -> user.getAge() < 18) .forEach(user -> { log.warn("미성년자 사용자 발견: {}", user.getName()); // 별도 처리 로직 sendNotificationToParent(user); });파일 쓰기
List<String> logs = getLogMessages(); try (PrintWriter writer = new PrintWriter("output.txt")) { logs.stream() .filter(log -> log.contains("ERROR")) .forEach(writer::println); }이메일 발송
List<Customer> customers = getVipCustomers(); customers.stream() .filter(customer -> customer.hasUnreadPromotions()) .forEach(customer -> { emailService.sendPromotionEmail(customer.getEmail()); customer.markPromotionAsSent(); });캐시 업데이트
Map<String, Object> cache = new ConcurrentHashMap<>(); processedData.stream() .forEach(data -> cache.put(data.getId(), data.getValue()));
조건 검사 연산 - xxxMatch( )
조건 검사 연산
List<Integer> numbers = Arrays.asList(2, 4, 6, 8, 10); boolean hasOdd = numbers.stream() .anyMatch(n -> n % 2 != 0); // 홀수가 하나라도 있나? => false boolean allEven = numbers.stream() .allMatch(n -> n % 2 == 0); // 모두 짝수? => true boolean noOdd = numbers.stream() .noneMatch(n -> n % 2 != 0); // 홀수가 하나도 없나? => true- 주어진 조건을 스트림의 요소들이 만족하는지 여부를 확인
- 종류
anyMatchallMatchnoneMatch
검색 및 계산 연산
count()long count = numbers.stream().count(); // 5- 요소의 개수 세기
findFirst()Optional<Integer> firstElement = numbers.stream().findFirst(); // Optional[2]- 첫 번째 요소를 찾는다.
- 결과는
Optional<T>로 반환
findAny()Optional<Integer> anyElement = numbers.stream().findAny();- 임의의 요소를 찾음 (병렬 스트림에서 유용)
min()/max()Optional<Integer> min = numbers.stream().min(Integer::compareTo); Optional<Integer> max = numbers.stream().max(Integer::compareTo);- 최솟값, 최댓값
상세 설명
고급 컬렉터와 병렬 스트림
- 내용이 추가될 예정입니다
상세 설명
Optional과 특화 스트림
- 내용이 추가될 예정입니다