스트림 (Stream)

하마·2025년 3월 4일

Java

목록 보기
7/8

스트림이란?


Stream (Java Platform SE 8)

  • 데이터를 효율적으로 처리할 수 있는 흐름
  • 선언형 스타일로 가독성이 굉장히 뛰어남
  • 데이터 준비 -> 중간 연산 -> 최종 연산 순으로 처리됨
  • 스트림은 컬렉션(List, Set 등) 과 함께 자주 활용됨

스트림 살펴보기


특징

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

데이터가 가공되는 순서

단계설명API
1. 데이터 준비컬렉션을 스트림으로 변환stream() , parallelStream()
2. 중간 연산 등록데이터 변환 및 필터링map() , filter() , sorted()
3. 최종 연산최종 처리 및 데이터 변환collect() , forEach() , count()

반복문과 스트림을 비교해보자

List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

위와 같은 arrayList 리스트가 있다고 치자.
각 요소에 10을 곱한 후 반환하는 코드를 작성해보자.

반복문(명령형 스타일)을 사용하는 코드

public class Main {
    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // ✅ 반복문 - 명령형 스타일
        List<Integer> ret = new ArrayList<>();
        for (Integer num : arrayList) {
            int multipliedNum = num * 10;
            ret1.add(multipliedNum);
        }
        System.out.println("ret = " + ret); 
    }
}

스트림(선언형 스타일)을 사용하는 코드

public class Main {
    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // ✅ 스트림 - 선언적 스타일
        List<Integer> ret = arrayList
                                .stream()
                                .map(num -> num * 10)
                                .collect(Collectors.toList());
                                
        System.out.println("ret = " + ret);
    }
}

같은 기능을 하지만, 가독성이 훨씬 좋아진 모습을 보인다.

각 단계 설명

  • stream()map()collect() 순으로 데이터 흐름을 처리합니다.
arrayList.stream()  // 1. 데이터 준비
		 .map()     // 2. 중간 연산 등록
		 .collect() // 3. 최종 연산
  • stream() : 데이터 준비
    • 데이터를 스트림으로 변환하여 연산 흐름을 만들 준비합니다.
// 1. 데이터 준비: 스트림 생성
Stream<Integer> stream = arrayList.stream();
  • map() : 중간 연산 등록
    • 각 요소를 주어진 함수에 적용해서 변환합니다.
    • 즉시 실행되지 않습니다.
// 2. 중간 연산 등록: 각 요소를 10배로 변환하는 로직 등록
Stream<Integer> mappedStream = stream.map(num -> num * 10);
  • collect() : 최종 연산
    • 결과를 원하는 형태(List, Set)로 수집합니다.
// 3. 최종 연산: 최종 결과 리스트로 변환
List<Integer> ret2 = mappedStream.collect(Collectors.toList());
  • 반복문 없이 간결하게 한 줄로 데이터 변환이 가능합니다.
// ✅ 한 줄로 표현 가능
List<Integer> ret2 = arrayList.stream() // 1. 데이터 준비
	                          // 2. 중간 연산 등록
    						  .map(num -> num * 10)     
      	                      // 3. 최종 연산
                       		  .collect(Collectors.toList());

스트림과 람다식의 활용


1. 익명 클래스를 변수에 담아 활용

public class Main {
    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // ✅ 1. 익명클래스를 변수에 담아 활용
        Function<Integer, Integer> function = new Function<>() {
            @Override
            public Integer apply(Integer integer) {
                return integer * 10;
            }
        };
        
        List<Integer> ret = arrayList.stream()
                					 .map(function)
                					 .collect(Collectors.toList());
                                
        System.out.println("ret = " + ret);
    }
}

2. 람다식을 변수로 활용

public class Main {
    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // ✅ 2. 람다식을 변수에 담아 활용
        Function<Integer, Integer> functionLambda = (num -> num * 10);
        List<Integer> ret = arrayList.stream()
				                	 .map(functionLambda)
                					 .collect(Collectors.toList());
                                
        System.out.println("ret = " + ret);
    }
}

3. 람다식을 매개변수에 직접 활용

public class Main {
    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // ✅ 3. 람다식을 직접 활용
        List<Integer> ret = arrayList.stream()
				                	  .map(num -> num * 10)
                					  .collect(Collectors.toList());
                                
        System.out.println("ret = " + ret);
    }
}

중간 연산 여러개 사용하기 (filter + map)


  • 다양한 중간 연산을 조립하여 데이터 처리 흐름을 만들 수 있습니다.
  • 선언형 스타일로 코드의 유지보수성가독성이 뛰어납니다.
  • 예시로 짝수만 10을 곱하고, 짝수만 반환하는 코드를 작성해봅니다.

1. 반복문

List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

// ✅ for 사용 예제
List<Integer> ret = new ArrayList<>();
for (int num : arrayList) {
    int remain = num % 2;
    if (remain == 0) {
        int data = num * 10;
        ret.add(data);
    }
}

System.out.println(ret); // 출력: [20, 40]

2. 스트림

List<Integer> arrayList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

// ✅ filter() + map() 사용 예제
List<Integer> ret = arrayList.stream() // 1. 데이터 준비: 스트림 생성
                        	 // 2. 중간 연산: 짝수만 필터링
				        	 .filter(num -> num % 2 == 0)
                        	 // 3. 중간 연산: 10배로 변환
        					 .map(num -> num * 10)
        					 // 4. 최종 연산: 리스트로 변환
                        	 .collect(Collectors.toList());

System.out.println(ret); // 출력: [20, 40]

스트림 사용 절차


1. 스트림 만들기

  • 배열 스트림 : Arrays.stream()
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
  • 컬렉션 스트림 : .stream()
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(); 
  • 람다식 : Stream.generate() , iterate()
Stream<String> generatedStream = Stream.generate(() -> "a")
									   .limit(3);

Stream<Integer> iteratedStream = Stream.iterate(0, n -> n + 2)
									   .limit(5); //0,2,4,6,8

생성할 때 스트림의 크기가 정해져있지 않기(무한하기)때문에 최대 크기를 제한해줘야 한다.

  • 기본 타입형 스트림
IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4]
  • 병렬 스트림: parallelStream()
Stream<String> parallelStream = list.parallelStream();

2. 중간 연산 (가공하기)

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

위와 같이 a , b , c 가 있는 리스트로 데이터를 가공해보자.

Filtering

  • 스트림 내 요소들을 걸러내는 작업
  • 조건문 역할
Stream<String> stream = list.stream()
    						// 'a'가 들어간 요소만 선택  [a]
							.filter(list -> list.contains("a"));

람다식의 리턴값은 boolean
true 인 경우만 다음 단계 진행

Mapping

  • 스트림 내 요소들을 하나씩 특정 값으로 변환하는 작업
  • 값을 변환하기 위한 람다를 인자로 받는다.
  • 스트림을 원하는 모양의 새로운 스트림으로 변환하고싶을 때 사용
Stream<String> stream = list.stream()
							// [A,B,C]
							.map(String::toUpperCase);
                            // 문자열 -> 정수로 변환
                            .map(Integers::parseInt);

스트림에 있는 값을 원하는 메소드에 입력값으로 넣으면
메소드 실행 결과(반환 값)가 담긴다.

Sorting

  • 스트림 내 요소들을 정렬하는 작업
  • Comparator 사용
Stream<String> stream = list.stream()
							// [a,b,c] 오름차순 정렬
							.sorted()
                            // [c,b,a] (내림차순)
                            .sorted(Comparator.reverseOrder())
    
List<String> list = Arrays.asList("a","bb","ccc");
Stream<String> stream = list.stream()
							// [ccc,bb,a]
                            // 문자열 길이 기준 정렬
							.sorted(Comparator.comparingInt(String::length)) 

기타 연산

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

3. 최종 연산 (결과 만들기)

Calculating

기본형 타입을 사용하는 경우
스트림 내 요소들로 최소, 최대, 합, 평균 등을 구하는 연산을 수행할 수 있다.

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

Reduction

스트림의 요소를 하나씩 줄여가며 누적연산을 수행

IntStream stream = IntStream.range(1,5);

// reduce(초기값, (누적 변수,요소) -> 수행문)
// 10 + 1+2+3+4+5 = 25
stream.reduce(10, (total, num) -> total + num);

Collection

스트림의 요소를 원하는 자료형으로 반환

// 예시 리스트
List<Person> members = Arrays.asList(new Person("lee",26),
									 new Person("kim", 23),
									 new Person("park", 23));
  1. toList() - 리스트로 반환
members.stream()
	   .map(Person::getLastName)
       // [lee, kim, park]
	   .collect(Collectors.toList());
  1. joining() - 작업 결과를 하나의 스트링으로 이어 붙이기
members.stream()
	   .map(Person::getLastName)
       // <lee+kim+park>
	   .collect(Collectors.joining(delimiter = "+" , prefix = "<", suffix = ">");
  1. groupingBy() - 그룹지어서 Map 으로 반환
members.stream()
       .collect(Collectors.groupingBy(Person::getAge));
       // { 26 = [Person{lastName = "lee", age = 26}],
       //   23 = [Person{lastName = "kim", age = 23},
       //		  Person{lastName = "park", age = 23}] }
  1. collectingAndThen() - collecting 이후 추가 작업 수행
members.stream()
	    // Set으로 collect한 후 수정불가한 set으로 변환하는 작업 실행
	   .collect(Collectors.collectingAndThen (Collectors.toSet(),
    									      Collections::unmodifiableSet));

Matching

특정 조건을 만족하는 요소가 있는지 체크한 결과를 반환

  • anyMatch() - 하나라도 만족하는 요소가 있는지
  • allMatch() - 모두 만족하는지
  • noneMatch() - 모두 만족하지 않는지
List<String> members = Arrays.asList("Lee", "Park", "Hwang");

boolean matchResult = members.stream()
							 // w를 포함하는 요소가 있는지, True
							 .anyMatch(members->members.contains("w"));

boolean matchResult = members.stream()
							 // 모든 요소의 길이가 4 이상인지, False
							 .allMatch(members->members.length() >= 4);
                             
boolean matchResult = members.stream()
							 // t로 끝나는 요소가 하나도 없는지, True
							 .noneMatch(members->members.endsWith("t"));

Iterating

forEach() 로 스트림을 돌면서 실행되는 작업

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

Finding

스트림에서 하나의 요소를 반환함

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

✅ 스트림 체이닝 작성 스타일


위 예시 코드에서 작성한 스타일로 들여쓰기를 적용한다.

List<Integer> ret = arrayList.stream()
                             .map(num -> num * 10)
                             .collect(Collectors.toList());
  • .stream()List 의 메소드이므로, 같은 줄에 둔다.
  • 체이닝 시 들여쓰기는 . 과 동일한 위치에 둔다.

❌ 잘못된 스타일 예시

// 불필요한 줄 개행
List<Integer> ret = arrayList
    .stream()
    .map(num -> num * 10)
    .collect(Collectors.toList());
// 들여쓰기 불균형
List<Integer> ret = arrayList.stream()
    .map(num -> num * 10)
        .collect(Collectors.toList());

참고자료


챕터 3-6 : 스트림(Stream)
[Java] 스트림 (Stream) 정리

0개의 댓글