Java

[Java] Stream(스트림)

pseudocoder_ 2024. 4. 6. 18:12
728x90

Stream이란

- 자바에서의 Stream(스트림)은 원하는 결과를 얻기 위해 다양한 메서드들을 제공하는 객체의 모음이다.

- 쉽게 말해 객체를 묶음 단위로 처리하기 위해 사용하는 것이 바로 스트림인 것이다.

- Stream API는 자바 8에서 처음 등장했으며, 데이터의 모음(Collection)을 처리하기 위해 사용된다.

 

기존에도 같은 목적을 가진 Collection 프레임워크를 통해 다수의 데이터를 효과적으로 처리할 수 있었다. 그럼 Collection를 놔두고 굳이 Stream을 사용해야 할 이유가 무엇일까?

 

Stream이 지니는 장점에는 명확하고 읽기 쉬운 코드로 인한 좋은 가독성 등 몇 개의 장점이 있지만, Collection과 비교했을 때 명확한 차별점이자 장점이 존재한다.

 

 

Collection vs Stream

Collection과 Stream의 가장 큰 차이점은 데이터 계산 시점이다.

 

Collection 프레임워크의 경우 값이 Collection 내부에 저장되는 것이 아니라, 메모리 영역에 모두 저장되고 Collection에는 연산이 끝난 값이 추가된다. 따라서 Collection에 값을 추가하는 경우에는 for-each와 같은 외부 반복(명시적으로 개발자가 반복 처리)을 통해 연산이 종료된 데이터를 하나씩 가져와 Collection에 추가하는 것이다.

 

Stream은 연산이 끝난 값을 추가하는 것이 아니라, 요청에 필요한 값을 Stream 내에서 계산한다. 추출 요소만 선언되면 알아서 반복처리가 진행되는 내부 반복이 사용된다. 다만 Collection에서 자유로운 데이터 추가 및 제거가 가능한 것과는 달리 Stream의 요소는 추가/제거가 불가능하다.

Collection과 Stream 비교

 

Stream의 내부 반복 덕분에 병렬처리(멀티 스레딩)가 가능하다는 큰 장점이 있다. Collection의 외부 반복은 명시적으로 컬렉션 항목을 하나씩 가져와서 처리하기 때문에 병럴처리에 적합하지 않다. 물론 Collection도 synchronized를 통해 병렬처리가 가능하긴 하다.

 

 

Stream 생성

1. 빈 스트림(Empty Stream)

빈 스트림을 생성한다. 빈 스트림은 요소가 없는 스트림이 null을 반환하는 것을 막기 위해서 쓰인다.

Stream<String> streamEmpty = Stream.empty();

public Stream<String> streamOf(List<String> list) {
    return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}

 

 

2. 컬렉션 스트림(Stream of Collection)

Collection(Collection, List, Set) 타입의 스트림 생성이 가능하다. Collection의 stream() 메서드를 통해 생성 가능하다.

Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();

 

 

3. 배열 스트림(Stream of Array)

배열을 스트림화 하는 것도 가능하다.

Stream<String> streamOfArray = Stream.of("a", "b", "c");

 

배열의 일부분이나 기존에 존재하는 배열로 새로운 스트림을 만드는 것도 가능하다.

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

 

 

4. Stream.builder()

builder() 메서드를 통해 스트림 생성이 가능하다. builder() 메서드 사용시에는 생성할 스트림의 타입을 지정해주어야 한다. 

지정해주지 않으면 Object 타입의 스트림이 자동으로 생성된다.

Stream<String> streamBuilder = Stream.<String>builder().add("a").add("b").add("c").build();

 

아래와 같이 Stream의 타입을 String으로 지정하고 builder() 메서드 사용 시에 타입을 지정해주지 않으면 에러가 뜨게 되는데, 생성되는 Stream의 타입이 Object인 것을 확인할 수 있다.

import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        Stream<String> streamBuilder = Stream.builder().build();
    }
}

 

 

5. Stream.generate()

스트림을 생성하는 또 다른 Stream의 generate() 메서드 사용 시, Supplier<T>에 해당하는 람다식으로 값을 넣는다.

해당 방법의 경우 별도로 Stream의 크기 제한을 걸어주지 않으면 시스템의 메모리 한계까지 크기가 증가하기 때문에 제한을 해주는 것이 중요하다.

// String타입의 Stream 내의 요소 중 값이 "element"와 같은 요소를 최대 5개까지
Stream<String> streamGenerated = Stream.generate(() -> "element").limit(5);

 

 

6. Stream.iterate()

Stream iterate()를 통해 스트림의 요소에 반복적인 연산을 시행할 수 있다. Stream generate()와 마찬가지로 limit을 지정해주지 않으면 요소의 연산이 무한정을 발생하기 때문에 limit을 통해 연산 횟수를 제한해 주어야 한다.

Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(5);
streamIterated.forEach(System.out::println);

 

위의 코드를 수행할 경우 아래의 결과값이 반환된다. iterate(start value, 람다식의 반복 수행할 연산).limit(제한 횟수)

 

 

7. Primitive 타입 Stream

Stream은 제네릭 인터페이스(Stream<T>)이기 때문에 Primitive 타입의 스트림을 생성하기 위해서는 별도의 인터페이스를 활용한다. 인터페이스의 종류로는 IntStream, LongStream, DoubleStream이 있다.

 

range(start, end) 메서드는 start 값부터 end - 1 값을 요소로 지니는 Stream을 생성한다. 두 번째 parameter end 값을 포함하지 않는다.

closedRange(start, end) 메서드는 start 값부터 end 값을 요소로 지니는 Stream을 생성한다.

 

1) IntStream

IntStream intStream = IntStream.range(1, 11);
System.out.println("intStream: ");
intStream.forEach(i -> System.out.print(i + " "));

IntStream closedIntStream = IntStream.rangeClosed(1, 11);
System.out.println("\nclosedIntStream: ");
closedIntStream.forEach(i -> System.out.print(i + " "));

 

 

2) LongStream

LongStream longStream = LongStream.range(1, 10);
System.out.println("longStream: ");
longStream.forEach(i -> System.out.print(i + " "));

LongStream closedLongStream = LongStream.rangeClosed(1, 10);
System.out.println("\nclosedLongStream: ");
closedLongStream.forEach(i -> System.out.print(i + " "));

 

 

3) DoubleStream

DoubleStream에는 range 메서드가 존재하지 않으며, DoubleStream.of() 메서드를 사용해서 명시적으로 요소를 선언해주거나, iterate() 메서드를 사용해서 스트림을 생성한다.

DoubleStream doubleStream = DoubleStream.of(1,2,3,4,5);
System.out.println("DoubleStreamOf: ");
doubleStream.forEach(i -> System.out.print(i + " "));

DoubleStream doubleStreamIterate = DoubleStream.iterate(1, i -> i + 1).limit(5);
System.out.println("\nDoubleStreamIterate: ");
doubleStreamIterate.forEach(i -> System.out.print(i + " "));

 

 

8. 문자열 스트림(String Stream) / 문자 스트림(Char Stream)

IntStream, LongStream 같은 Primitive 타입 스트림 CharStream이 존재하지 않기 때문에 문자 스트림은 String 클래스의 chars() 메서드를 활용하여 생성한다. 

IntStream streamOfChars = "abcde".chars();
System.out.println("charStream: ");
streamOfChars.forEach(c -> System.out.print(c + " "));

Stream<String> streamOfString = Pattern.compile(",").splitAsStream("a,b,c,d,e");
System.out.println("\nstringStream: ");
streamOfString.forEach(s -> System.out.print(s + " "));

 

 

9. 파일 스트림(Stream of File)

자바의 비동기 입출력, 파일 잠금, 멀티플렉싱 등의 기능을 제공하는 NIO 클래스(New Input/Output)의 lines() 메서드를 통해 파일을 문장 단위로 읽어올 수 있다. 파일에서 읽어온 문장들을 요소로 지니는 스트림을 생성할 수 있다.

// IOException 핸들링 필요
Path path = Paths.get("C:\\file.txt");
Stream<String> streamOfStrings = Files.lines(path);
Stream<String> streamWithCharset = Files.lines(path, Charset.forName("UTF-8"));

 

 

 

스트림 연산

스트림의 연산과정은 중간 연산(intermediate operation)과 최종 연산(terminal operation)으로 나뉜다.

 

스트림 중간 연산을 통해 인스턴스화 된 스트림 객체에 접근하고 요소들을 뽑아 내고 요소에 대한 가공이나 필터링 작업을 진행한다. 중간 연산을 수행하는 메서드들을 통해 결과를 생성해주는 새로운 스트림 객체를 리턴한다.

 

중간 연산을 통해 리턴된 새로운 스트림 객체 자체는 유의미한 결과 값이 아니기 때문에, 해당 스트림 객체를 출력하거나 컬렉션으로 모으기 위해서 최종 연산이라는 마무리 작업이 필요하다. 최종 연산을 거쳐 int, String 등의 유의미한 결과을 반환할 수 있다.

 

 

1. 중간 연산의 종류

중간 연산은 모두 Stream을 반환한다.

 

- filter(Predicate): Boolean을 반환하는 Predicate를 받아서 true인 것들을 요소로 가지는 Stream을 반환한다.

- distinct(): 중복 요소를 제거한다.

- limit(int n): 주어진 사이즈 이하의 스트림을 반환한다.

- skip(int n): 처음 요소 n개를 제외한 스트림을 반환한다.

- map(Function): 매핑 함수의 result를 요소로 지니는 스트림을 반환한다.

// 스트림의 모든 요소에 3을 곱한 값을 출력
List<Integer> arr = Arrays.asList(3, 6, 9, 12, 15);
arr.stream().map(number -> number * 3).forEach(i -> System.out.print(i + " "));

 

 

- flatMap(): 각 원본 요소에 대해 여러개의 새로운 요소를 생성하고, 이를 평면화하여 하나의 스트림으로 결합한다. 

// 2차원 배열 내의 각 배열에 stream을 적용하여 결과를 평탄화 된 1차원 배열로 리턴한다.
List<List<Integer>> nestedList = Arrays.asList(
        Arrays.asList(1,2,3),
        Arrays.asList(4,5,6),
        Arrays.asList(7,8,9)
);
List<Integer> flatMappedList = nestedList.stream()
        .flatMap(innerList -> innerList.stream().map(element -> element * 3))
        .collect(Collectors.toList());

System.out.println(flatMappedList); // [3, 6, 9, 12, 15, 18, 21, 24, 27]

 

 

2. 최종 연산의 종류

- Boolean allMatch(Predicate): 스트림의 모든 요소가 Predicate와 일치하는지 검사한다. 결과를 찾는 즉시 실행이 종료된다.

- Boolean anyMatch(Predicate)

- Boolean noneMatch(Predicate)

 

- Optional findAny(): 스트림에서 임의의 요소를 반환한다. 

- Optional findFirst(): 스트림의 첫 번째 요소를 찾는다. 

- T reduce(초기값, Function): 스트림의 모든 요소를 처리해서 값으로 도출한다. 

- R collect(): 스트림을 reduce() 처리하고 list, map, 정수 형식의 컬렉션을 반환한다. 

- void forEach(lambda): 스트림의 각 요소를 돌면서 람다식을 적용한다. 

- Long count: 스트림의 요소 갯수를 반환한다. 

 

 

 

References

https://gyoogle.dev/blog/computer-language/Java/Stream.html

 

JAVA Stream | 👨🏻‍💻 Tech Interview

JAVA Stream Java 8버전 이상부터는 Stream API를 지원한다 자바에서도 8버전 이상부터 람다를 사용한 함수형 프로그래밍이 가능해졌다. 기존에 존재하던 Collection과 Stream은 무슨 차이가 있을까? 바로 **'

gyoogle.dev

 

https://www.geeksforgeeks.org/stream-in-java/

 

Stream In Java - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

https://velog.io/@adam2/JAVA8%EC%9D%98-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

 

JAVA8의 스트림 알아보기

스트림은 자바8에 새롭게 추가된 기능으로, 선언형(sql같은 질의형)으로 데이터(컬렉션, 배열, 파일, iterate...)를 처리할 수 있다. 자바8의 함수형 패러다임의 시작으로 람다를 이용해 함수형으로

velog.io

https://www.baeldung.com/java-8-streams

 

 

728x90