본문 바로가기
코딩 공부/web & Java

[Java] Stream

by 현장 2025. 4. 2.

Stream

Java 8부터 추가된 기술로 람다를 활용해 배열과 컬렉션을 함수형으로 간단하게 처리할 수 있는 기술입니다.

 

기존의 for문과 Iterator를 사용하면 코드가 길어져서 가독성과 재사용성이 떨어지며 데이터 타입마다 다른 방식으로 다뤄야 하는 불편함이 있었습니다. 하지만 스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메소드를 정의해 놓아서 데이터 소스에 상관없이 모두 같은 방식으로 다룰 수 있으므로 코드의 재사용성이 높아지게 됩니다.

 

스트림에 대한 내용은 아래와 같이 크게 세 가지로 나눌 수 있습니다.

  1. 생성하기 : 스트림 인스턴스 생성
  2. 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(intermediate operations)
  3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업(terminal operations)
전체 ➡️ 맵핑 ➡️ 필터링 1 ➡️ 필터링 2 ➡️ 결과 만들기 ➡️ 결과물

🏷️ 특징

  • 원본 데이터 소스를 변경하지 않고 읽기만 합니다.
  • 일회용이어서 한번 사용하면 닫혀서 재사용이 불가능합니다.
  • 최종 연산 전까지 중간 연산을 수행하지 않습니다.
  • 작업을 내부 반복으로 처리하기 때문에 forEach()는 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용하게 됩니다.
  • 멀티쓰레드 사용하기 때문에 병렬 처리가 쉽습니다. 
  • 기본형 스트림을 제공하기 때문에 Stream<Integer> 대신 IntStream이 제공되어서 오토박싱과 언박싱 등의 불필요한 과정이 생략되고 숫자의 경우 유용한 메소드를 추가로 제공합니다. (ex. ".sum()", ".average()" 등)

🏷️ 사용법

1. 생성하기

보통 배열과 컬렉션을 이용해서 스트림을 만들지만 이 외에도 다양한 방법으로 스트림을 만들 수 있습니다.

✅ 배열 스트림

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);

 

✅ 컬렉션 스트림

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

✅ Stream.builder()

Stream<String> builderStream = 
  Stream.<String>builder()
    .add("a")
    .add("b")
    .add("c")
    .build(); // ["a", "b", "c"]

✅ Stream.generate()

Stream<String> generatedStream = 
  Stream.generate(() -> "a").limit(5); // ["a", "a", "a", "a", "a"]

Stream.iterate()

Stream<Integer> iteratedStream = 
  Stream.iterate(10, n -> n * 2).limit(5); // [10, 20, 30, 40, 50]

✅ 기본 타입형 스트림

IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4] // int 타입
LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5] // long 타입

✅ 병렬 스트림

Stream<String> parallelStream = productList.parallelStream();

2. 가공하기

전체 요소 중에서 다음과 같은 API 를 이용해서 내가 원하는 것만 뽑아낼 수 있습니다. 이러한 가공 단계를 중간 작업이라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있습니다.

✅ Filtering

필터(filter)은 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업입니다. 인자로 받는 Predicate는 boolean 을 리턴하는 함수형 인터페이스로 평가식이 들어가게 됩니다.

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);

Stream<Integer> stream = 
  nums.stream()
      .filter(num -> num % 2 == 0); // [2, 4]

✅ Mapping

맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환해줍니다. 이 때 값을 변환하기 위한 람다를 인자로 받습니다.

List<String> strings = Arrays.asList("a", "b", "c");

Stream<String> stream = 
  strings.stream()
  .map(String::toUpperCase); // ["A", "B", "C"]

Sorting

정렬의 방법은 다른 정렬과 마찬가지로 Comparator 를 이용합니다.

List<Integer> nums = Arrays.asList(2, 1, 3);
Stream<String> stream = nums.stream()
	.sorted() // [1,2,3] (오름차순)
    .sorted(Comparator.reverseOrder()); // [3,2,1] (내림차순)
    
List<String> strings = Arrays.asList("aa", "b","ccc");
Stream<String> stream = strings.stream()
	.sorted(Comparator.comparingInt(String::length)); // [ccc,aa,b] // 문자열 길이 기준 정렬

기타 연산

Stream<String> stream = list.stream()
	.distinct() // 중복 제거
        .limit(max) // 최대 크기 제한
        .skip(n)    // 앞에서부터 n개 skip하기
        .peek(System.out::println) // 중간 작업결과 확인

3. 결과 만들기

가공한 스트림을 가지고 내가 사용할 결과값으로 만들어내는 단계로 스트림을 끝내는 최종 작업입니다.

✅ Calculating

스트림 API 는 다양한 종료 작업을 제공합니다. 최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있습니다.

IntStream stream = list.stream()
    .count()   //스트림 요소 개수 반환
    .sum()     //스트림 요소의 합 반환
    .min()     //스트림의 최소값 반환
    .max()     //스트림의 최대값 반환
    .average() //스트림의 평균값 반환

✅ Reduction

스트림의 요소를 하나씩 지나가면서 누적연산을 수행합니다.

IntStream.range(1, 4) // [1, 2, 3]
  .reduce((a, b) -> { // 앞에 숫자가 없는 경우 2개의 값을 가리킴
    return Integer.sum(a, b);
  }); // 6

IntStream.range(1, 4) // [1, 2, 3]
  .reduce(10, (res, num) -> // 2개의 인자가 주어진 경우 첫번째 인자의 초기값은 숫자를 가리킴
  	res + num
  ); // 16

✅ Collecting

스트림의 요소를 원하는 자료형으로 변환합니다.

List<Member> memberList = 
  Arrays.asList(
    new Member("kang", 30),
    new Member("kim", 28),
    new Member("park", 31),
);

// List로 반환
List<String> collectorCollection =
  memberList.stream()
    .map(Member::getName)
    .collect(Collectors.toList()); // ["kang", "kim", "park"]

// joining: 문자열 연결 or 문자열 사이에 값 넣기
String listToString = 
 memberList.stream()
  .map(Member::getName)
  .collect(Collectors.joining()); // "kangkimpark"
  
// summarizingInt: 평균, 합계 등 모두 반환
IntSummaryStatistics statistics = 
 memberList.stream()
  .collect(Collectors.summarizingInt(Member::getAge));
// IntSummaryStatistics {count=3, sum=89, min=28, average=29.6666, max=31}

// groupingBy: 특정 요소 그룹지어 Map으로 반환
Map<Integer, List<Product>> collectorMapOfLists =
 memberList.stream()
  .collect(Collectors.groupingBy(Member::getAge));
// {30: [Member{name="kang", age="30"}, ...}

// collectingAndThen: collecting 이후 추가 작업 수행
Set<Product> unmodifiableSet = 
 productList.stream()
  .collect(
      Collectors.collectingAndThen(
          Collectors.toSet(),
          Collections::unmodifiableSet)
      ); // set으로 변경 후 수정 불가 set으로 다시 변경

✅ Matching

매칭은 조건식 람다 Predicate 를 받아서 해당 조건을 만족하는 요소가 있는지 체크한 결과를 리턴합니다. 다음과 같은 세 가지 메소드가 있습니다

  • 하나라도 조건을 만족하는 요소가 있는지 (anyMatch)
  • 모두 조건을 만족하는지 (allMatch)
  • 모두 조건을 만족하지 않는지 (noneMatch)
List<String> names = Arrays.asList("kang", "kim", "park");

boolean anyMatch = names.stream()
  .anyMatch(name -> name.contains("a")); // ["kang", "park"]
boolean allMatch = names.stream()
  .allMatch(name -> name.length() <= 3); // ["kim"]
boolean noneMatch = names.stream()
  .noneMatch(name -> name.endsWith("k")); // ["park"]

✅ Iterating

foreach 는 요소를 돌면서 실행되는 최종 작업입니다. 보통 System.out.println 메소드를 넘겨서 결과를 출력할 때 사용하곤 합니다.

members.stream()
	.map(Person::getName)
    .forEach(System.out::println);
    //결과를 출력 (peek는 중간 작업, forEach는 최종 작업)

✅ Finding

스트림에서 하나의 요소를 반환합니다.

Member member = memberList
	.stream()
 	.findAny() //먼저 찾은 요소 하나 반환, 병렬 스트림의 경우 첫번째 요소가 보장되지 않음
	.findFirst() //첫번째 요소 반환

📖 Reference

yun.log

Java 스트림 Stream (1) 총정리

'코딩 공부 > web & Java' 카테고리의 다른 글

[Spring] BCryptPasswordEncoder  (0) 2025.04.04
[Java] Comparable과 Comparator  (0) 2025.04.04
[JPA] CRUDRepository와 JPARepository  (0) 2025.04.01
[Mockito] ArgumentMatchers 및 any,eq  (1) 2024.10.21
[JPA] @PrePersist와 @PreUpdate  (0) 2024.10.18