본문 바로가기

BackEnd/Java

[JAVA] Stream API - 가독성 좋은 간결한 프로그래밍

Stream이란?

자바 8부터 도입된 기능으로, 데이터의 흐름을 함수형 스타일로 처리할 수 있도록 해주는 API이다.

  • Stream은 탐색, 정렬, 필터링, 매핑 등을 선언형(무엇을 할지) 방식으로 처리할 수 있게 한다.
  • 컬렉션(List, Set 등)의 데이터를 하나씩 순차적으로 처리하는 파이프라인이다.

파이프라인이란?

  • 시스템의 효율을 높이기 위해 명령문을 수행하면서 몇 가지 특수한 작업들을 병렬 처리하도록 설계된 것을 말한다.
    • 쉽게 말해, 데이터를 여러 단계에 걸쳐 처리하는 흐름을 의미함.
  • Stream에서 데이터를 중간 처리 연산자들을 거쳐 최종 연산으로 보내는 일련의 흐름(체인 구조)를 말함.

 

명령형 방식 코드

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alex");

List<String> result = new ArrayList<>(); //중간 연산 결과를 저장하기 위한 List

for(String name : names){
	if(name.startsWith("A")){ //필터링
    	result.add(name.toUpperCase()); //대문자 변환
    }
}
Collections.sort(result); //정렬

 

 

Stream 방식(선언형) 코드

//데이터 소스 → 중간 연산 1 → 중간 연산 2 → ... → 최종 연산

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alex");	//데이터 소스(Stream 생성)
	.filter(name -> name.startsWith("A"))	//중간 연산 1 (필터)
    .map(String::toUpperCase)	//중간 연산 2 (변형)
    .sorted()	//중간 연산 3 (정렬)
    .collect(Collectors.toList());	//최종 연산   (수집)
    
 //결과적으로 "Alice", "Alex"만 남고, toUpperCase, sorted 연산으로 "ALEX","ALICE"로 정렬되어 결과에 담기게 된다.

 


파이프라인의 구성 요소

단계 종류 설명 예시 메서드
1단계 데이터 소스 처리할 데이터를 스트림으로 변환 .stream(), .of(), .range()
2단계 중간 연산 데이터 필터링, 매핑, 정렬 등 .filter(), .map(), .sorted()
3단계 최종 연산 결과 반환하고 스트림 종료 .collect(), .forEach(), .count()

 

주의  중요 특징 : 지연 평가

  • filter, map, sorted와 같은 중간 연산은 실행되지 않고 대기 상태이다.
  • 최종 연산이 호출되기 전까지 아무 일도 일어나지 않는다.
  • 최종 연산이 호출되면, 그때서야 파이프라인이 실행되고, 처리됨.

 

 

 


Stream의 기능

기능 설명 예시 메서드
탐색 조건에 맞는 데이터를 찾음 filter(), findFirst(), anyMatch()
변형(매핑) 데이터를 바꿔줌 map(), mapToInt()
정렬 정렬된 스트림 생성 sorted()
집계 결과를 합침 count(), sum(), max(), collect()
루프 반복 실행 forEach()

 

Stream의 특징

  • Stream은 저장소가 아니라 흐름이다. → 데이터를 저장하는 것이 X
  • 자동으로 정렬하는 것이 X → 중간 연산자 sorted()를 써야 정렬됨.
  • 탐색만 하는 것이 X → 필터링, 변형, 집계 등 다양한 연산이 가능하다.
  • 1회성 객체이다 → 한 번 쓰고 나면 재사용 X
    • 만약 재사용 시도시, 'IllegalStateException' 오류가 발생함.
  • 중간 연산은 지연 평가되며, 최종 연산이 실행될 때만 동작함.
//예시
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alex");
Stream<String> stream = names.stream();

boolean hasAlex = stream.anyMatch(name -> name.equals("Alex")); //사용함.
boolean hasBob = stream.noneMatch(name -> name.equals("Bob")); //오류 발생.

//.anyMath() 연산으로 stream이 소비된 후 닫혔고, 
//이후 noneMatch()연산으로 이미 닫힌 stream을 재사용했기 때문에 IllegalStateException 발생.

 

//해결 방법
//1. Stream을 다시 생성 -> names.stream()을 매번 새로 호출하면 됨.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alex");

boolean hasAlex = names.stream().anyMath(name -> name.equals("Alex"));
boolean hasBob = namse.stream().noneMatch(name -> name.equals("Bob"));
//해결 방법
//2. 결과를 저장해서 재사용
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alex");

Stream<String> stream = names.stream();
List<String> list = stream.collect(Collectors.toList()); // 스트림 종료 후 재사용

boolean hasAlex = list.contains("Alex");
boolean hasBob = !list.contains("Bob");