Search
Duplicate

Java - 스트림(stream)

글감
Java
작성자
작성 일자
2024/02/08 06:43
상태
완료
공개여부
공개
Date
생성자
작업자

스트림이란

기존 Java에서는 컬렉션 데이터를 처리하려면 for문이나 foreach 루프문을 사용하면서 컬렉션 내 요소들을 하나씩 다루어야 했다. 만약 반복문 내에서 복잡한 처리가 필요하거나 컬렉션의 크기가 커지면, 루프문의 사용으로 인해 성능이 저하되는 문제가 발생했다.
// 빨간색 사과 필터링 List<Apple> redApples = forEach(appleList, (Apple apple) -> apple.getColor().equals("RED")); // 무게 순서대로 정렬 redApples.sort(Comparator.comparing(Apple::getWeight)); // 사과 고유번호 출력 List<Integer> redHeavyAppleUid = new ArrayList<>(); for (Apple apple : redApples) redHeavyAppleUid.add(apple.getUidNum());
Java
복사
스트림을 사용하지 않을 때
Java 8부터 이런 문제를 해결하기 위해 소스에서 추출된 연속된 요소들에 접근하여 데이터 처리 연산을 정의할 수 있는 스트림(stream)을 도입했다.
List<Integer> redHeavyAppleUid = appleList.stream() .filter(apple -> apple.getColor().equals("RED")) // 빨간색 사과 필터링 .sorted(Comparator.comparing(Apple::getWeight)) // 무게 순서대로 정렬 .map(Apple::getUidNum).collect(Collectors.toList()); // 사과 고유번호 출력
Java
복사
스트림을 사용할 때
스트림은 데이터의 흐름을 의미하며, 컬렉션이나 배열, I/O 자원 등의 소스로부터 제공되는 API를 통해 데이터를 소비한다. 그 과정에서 주어지는 소스의 데이터 순서를 그대로 유지하여 정렬된 순서를 보장할 수 있다.
또한 스트림에서는 paralleStream 메서드를 제공하여, 별도의 멀티스레드 구현없이도 간편하게 병렬처리가 가능해진다.
List<Integer> redHeavyAppleUid = appleList.parallelStream() // 병렬 처리 .filter(apple -> apple.getColor().equals("RED")) // 빨간색 사과 필터링 .sorted(Comparator.comparing(Apple::getWeight)) // 무게 순서대로 정렬 .map(Apple::getUidNum).collect(Collectors.toList()); // 사과 고유번호 출력
Java
복사
위 코드는 stream() 메서드를 통해 appleList라는 소스(Source)에서 연속된 데이터들을 얻어 스트림을 생성한다. 해당 스트림에 Stream API인 filter, sorted, map, collect의 데이터 처리 연산을 적용하고 처리 결과를 반환한다.
IntStream, DoubleStream, LongStream 등의 형태를 기본형 스트림으로 제공하여, 오토박싱 등의 불필요한 과정을 생략할 수 있고 더 효율적으로 사용할 수 있다. 추가적으로 .sum()이나 .average()와 같은 Stream<T>에는 없는 더 유용한 메서드를 제공하기도 한다.
이처럼 Java 8에서는 스트림을 도입하여 더 간결하고 가독성과 유연성이 좋은 코드를 작성할 수 있다.

스트림의 특징

스트림의 데이터 처리
스트림은 말그대로 데이터의 흐름일 뿐 데이터를 담고 있는 저장소나 컬렉션이 아니다.
스트림을 통해 데이터를 가공하는 과정에서 원본 데이터 소스를 Read-Only로 가져오며 변경하지 않는다.
스트림은 Iterator처럼 일회용으로 사용되며, 스트림을 다시 사용하려면 새로 생성해야 한다.
파이프 라이닝
스트림 연산들을 서로 연결하여, 큰 파이프 라인을 구성할 수 있도록 스트림 자신을 반환한다.
스트림 자신을 반환하기 때문에 lazinessshort-circuiting과 같은 최적화 방식을 적용할 수 있다.
laziness 최종연산이 실행되기 전까지, 중간연산을 실행하지 않고 대기
short-circuiting 여러 개의 조건이 중첩된 상황에서 값이 결정되면, 더 이상 불필요한 실행을 하지 않도록 해서 실행 속도를 증가시키는 기법
스트림 파이프 라이닝에는 중간 연산자와 최종 연산자가 있다.
List<String> highCaloriesFoodName = foodList.stream() .filter(food -> food.getCalories() > 400) .map(Food::getName) .limit(3) .collect(Collectors.toList()); System.out.println(highCaloriesFoodName);
Java
복사
위 코드에서 filter, map, limit은 중간 연산자로 파이프 라인을 형성할 수 있도록 스트림 자신을 반환한다. collect는 최종 연산자로 파이프라인을 처리하여 결과를 반환한다.
연산 파이프라인은 데이터베이스의 SQL 질의문과 비슷하다.
내부 반복
반복자(Iterator)를 통해 명시적으로 순회하는 컬렉션과는 다르게, 스트림 자체에서 내부 반복 기능을 제공한다.
스트림의 내부 반복 기능에 람다식으로 함수를 넘겨주어 별도의 반복문 없이 내부 순회와 처리 로직을 적용할 수 있다.

스트림의 연산

위에서도 언급되어 있듯이, 스트림의 연산들은 중간 연산최종 연산으로 구분할 수 있다. 데이터 소스를 중간 연산을 통해 가공하고 최종 연산으로 결과를 저장한다.
중간 연산
스트림의 파이프 라이닝을 위해 스트림 자신을 반환하여 여러 중간 연산을 수행할 수 있는 연산들이다.
최적화를 위해 Laziness가 적용되어 있어, 최종 연산을 만나기 전까지 아무 연산도 수행하지 않고 기다린다.
최종 연산
파이프라이닝을 통해 연산된 결과를 반환하는 연산을 말한다.
List나 Integer, void 등 다양한 형태로 결과를 받을 수 있다.

스트림과 컬렉션의 차이점

스트림과 컬렉션은 둘 다 연속된 요소를 가지는 자료구조 인터페이스를 제공한다. 둘의 가장 큰 차이점은 내 컴퓨터에 저장된 동영상 파일과 인터넷에서 동영상 스트리밍의 차이정도로 생각할 수 있다.
데이터 계산 시점
컬렉션의 자료구조가 포함하는 모든 요소들을 메모리에 저장하는 방식이다. 그렇기 때문에 컬렉션의 모든 요소는 컬렉션에 추가되기 전에 계산되어야 한다.
스트림에서는 요청할 때, 사용자가 필요로 하는 일부분만 계산하는 고정된 자료구조이다.
반복의 일회성
컬렉션의 경우에는 같은 소스에 대해 여러 번 반복해서 처리할 수 있다.
스트림에서는 소비(Consume)의 개념을 사용하기 때문에, 소비한 요소에 대해 접근할 수 없어 여러 번 반복해서 처리할 수 없다.
Consume<List<Food>> c = (foodList) -> { for (food : foodList) System.out.println(food); } c.accept(foodList); // 정상 c.accept(foodList); // 정상 Stream<Food> s = foodList.stream(); s.forEach(System.out::println); // 정상 s.forEach(System.out::println); // IlleagalStateException 발생
Java
복사
반복 구조
컬렉션은 사용자가 반복문을 직접 명시하는 외부반복 개념이다.
스트림은 라이브러리를 사용하는 내부반복 개념이다.
List<String> foodNameList = new ArrayList<>(); for (Food food : foodList){ foodNameList.add(food.getName()); } List<String> foodNameList = foodList.stream() .map(Food::getName) .collect(Collectors.toList());
Java
복사

스트림 생성하기

배열 스트림
String[] array = new Integer[]{1, 2, 3}; Stream<Integer> stream1 = Arrays.stream(array); Stream<Integer> stream2 = Arrays.stream(array, 1, 3); // index 1 포함, 3 제외
Java
복사
컬렉션 스트림
List<Integer> list = Arrays.asList(1, 2, 3); Stream<Integer> stream = list.stream();
Java
복사
스트림 builder
Stream<Integer> stream = Stream<Integer>builder() .add(1) .add(2) .add(3) .build();
Java
복사
스트림 generator
// generate 시그니처 : Supplier 함수형 인터페이스를 매개변수로 받는다. // public static<T> Stream<T> generate(Supplier<T> s) { ... } // Hello 문자열을 무한대로 생성하는 스트림 Stream<String> stream = Stream.generate(() -> "Hello").limit(5);
Java
복사
스트림 Iterator
// 100부터 10씩 증가하는 숫자를 생성하는 iterate Stream<Integer> stream = Stream.iterate(100, n -> n + 10).limit(5);
Java
복사
빈 스트림
Stream<Integer> stream = Stream.empty();
Java
복사
기본형 스트림
IntStream intStream = IntStream.range(1, 10); IntStream boxedIntStream = IntStream.range(1, 10).boxed(); // 오토박싱 LongStream longStream = LongStream.range(1, 100); LongStream boxedLongStream = LongStream.range(1, 100).boxed(); // 오토박싱
Java
복사
문자열 스트림
IntStream stream1 = "Hello, world".chars(); // (72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100) Stream<String> stream2 = Pattern.compile(",").splitAsStream("Apple,Banana,Melon");
Java
복사
파일 스트림
Stream<String> stream = Files.lines(Path.get("test.txt"), Charset.forName("UTF-8"));
Java
복사
스트림 연결
Stream<Integer> stream1 = Stream.of(1, 2, 3); Stream<Integer> stream2 = Stream.of(4, 5, 6); Stream<Integer> stream = Stream.concat(stream1, stream2); // 1, 2, 3, 4, 5, 6
Java
복사

스트림 가공하기 - 중간 연산

Filter
데이터를 필터링하는 스트림으로, boolean을 반환하는 람다식을 넣어 true면 다음 스트림을 진행하고 false면 데이터를 버린다.
// 스트림의 Food 요소들 중 calories가 400을 넘는 값만 필터링 foodList.stream().filter(food -> food.getCalories() > 400)
Java
복사
Map, FlatMap
스트림을 새로운 형태의 데이터 요소를 가진 스트림으로 변환한다.
FlatMap의 경우 중첩 구조를 제거하고 다음 스트림으로 진행한다.
// Food 컬렉션에서 생성한 스트림으로부터 food 이름들의 스트림으로 변환 foodList.stream().map(Food::getName())
Java
복사
limit, skip
스트림 데이터 중 특정 부분의 데이터들만 선택하여 다음 스트림으로 진행한다.
// foodList 스트림 중 앞에서 10번째부터 10개의 데이터만 선택 foodList.stream().skip(10).limit(10)
Java
복사
sorted
스트림의 요소들을 정렬하고 다음 스트림으로 진행한다.
foodList.stream().sorted() foodList.stream().sorted(Comparator.comparingInt(Food::getCalories))
Java
복사

데이터 가공하기 - 최종 연산

anyMatch, allMatch, nonMatch
스트림의 데이터들 중 특정 조건을 만족하는 데이터가 있는지 확인한다.
// foodList 스트림 중 Apple 문자열을 이름에 포함하고 있는 데이터가 있는지 확인(true/false 반환) foodList.stream().anyMatch(food -> food.getName().contains("Apple"))
Java
복사
findFirst, findAny
스트림을 순회하며 조건에 맞는 값 중 가장 먼저 찾은 하나를 찾아온다.
foodList.stream().findFirst(food -> food.getName().contains("Apple")) foodList.stream().findAny(food -> food.getName().contains("Apple"))
Java
복사
findFirst와 findAny의 경우 스트림을 직렬처리하는 경우에는 차이가 없지만, 병렬로 처리하는 경우 findFirst는 가장 앞쪽에 있는 요소를 반환하고 findAny는 멀티 스레드 중 가장 먼저 찾은 요소를 반환한다. findAny로 병렬 처리를 하는 경우 뒤쪽의 요소가 반환될 수도 있다.
count, sum, min, max
각 목적에 맞는 수학적 연산을 한 후 그 결과를 반환한다.
reduce
스트림 요소를 줄여가면서 하나씩 누적 연산을 수행 후 결과를 반환한다.
// 1부터 10까지의 숫자들을 하나씩 누적해서 더한다. Integer sum = Stream.of(1, 2, 3) .reduce((total, y) -> total + y); System.out.println("sum: " + s); //sum: 6 // 초기값을 지정해 줄 수 있다. Integer sum = Stream.of(1, 2, 3) .reduce(10, Integer::sum); System.out.println("sum: " + sum); //sum: 16 // 세 번째 인자로 병렬처리 결과를 합치는 로직을 받을 수 있다. Integer sum = ArrayList.asList(1, 2, 3) .parallelStream() .reduce(10, Integer::sum, (a, b) -> a + b); // sum: 36 // 여기서 36이 되는 이유는 병렬처리로 10 + 1 / 10 + 2 / 10 + 3 을 각각 계산 후 값들을 더했기 때문이다.
Java
복사
collect
스트림의 요소들을 원하는 자료형으로 변환하여 저장
foodList.stream().map(Food::getName()).collect(Collectors.toSet()); // Set으로 변환 foodList.stream().map(Food::getName()).collect(Collectors.toList()); // List로 변환 foodList.stream().map(Food::getName()).collect(Collectors.joining(", ")); // 하나의 String으로 join foodList.stream().collect(Collectors.averagingInt(Food::getCalories)); // 각 요소의 Calories의 평균값을 반환 foodList.stream().collect(Collectors.summingInt(Food::getCalories)); // 각 요소의 Calories의 합을 반환
Java
복사