본문 바로가기
Programming Language 👅

[Java] Stream API

by 서니서닝 2023. 4. 10.
728x90

Java Collection Framework, JCF

Java 언어에서 사용되는 데이터 구조를 관리하고 처리하기 위한 라이브러리 집합 
Collection(List, Set, Queue)과 Map으로 구성 되어 있다.
  • List:
    • 순서가 있는 데이터를 저장
    • 데이터의 중복을 허용
    • ArrayList, LinkedList 등
  • Set:
    • 순서가 없는 데이터를 저장
    • 데이터의 중복을 허용하지 않음
    • HashSet, TreeSet 등
  • Queue:
    • 데이터를 순서대로 저장하고, 데이터를 추가할 때는 뒤쪽에, 삭제할 때는 앞쪽에서부터 처리
    • LinkedList, PriorityQueue 등
  • Map:
    • Key-Value 쌍으로 데이터를 저장
    • Key는 중복될 수 없다
    • HashMap, TreeMap 등

 

Q. 왜 자바에서 배열보다 Collection 사용을 지향할까?
Java Collection을 사용하면 데이터를 조작할 때 다양한 api를 사용할 수 있다.

 


✨ Stream API

Java 8부터 추가된 기능
컬렉션(Collection)이나 배열(Array) 등의 요소(Element)들을 처리하며,
람다식(Lambda Expression)을 이용한 함수형 프로그래밍(Functional Programming)을 지원한다.

특징

  • 원본 데이터를 변경하지 않는다.
  • 외부 반복을 통해 작업하는 컬렉션과 달리 내부 반복(internal iteration)을 통해 작업을 수행한다.
  • 재사용이 가능한 컬렉션과 달리 단 한번만 사용 가능
  • 필터(filter)-맵(map) 기반의 API를 사용하여 지연(lazy) 연산을 통해 성능을 최적화 한다.
  • parallelStream() 메서드를 통한 손쉬운 병렬 처리를 지원한다.

http://www.tcpschool.com/java/java_stream_concept

동작 흐름

  • 스트림 생성(Stream Creation):
    • 컬렉션(Collection), 배열(Array), 파일 등의 데이터 소스로부터 스트림(Stream)을 생성
  • 중간 연산(Intermediate Operation):
    • 생성된 스트림에 대해 여러 가지 중간 연산을 수행
    • 이러한 중간 연산들은 필터링(Filtering), 변환(Mapping), 정렬(Sorting), 그룹핑(Grouping) 등 다양한 작업을 수행할 수 있다.
  • 최종 연산(Terminal Operation):
    • 중간 연산들을 거쳐 처리된 스트림을 최종적으로 처리
    • 이러한 최종 연산들은 요소의 개수를 세는 count(), 요소들을 하나로 합치는 reduce(), 요소들을 출력하는 forEach() 등이 있다.

✍ 스트림 생성

Stream API는 다양한 데이터 소스에서 생성할 수 있다. 그 중 가장 일반적인 방법은 컬렉션(Collection)에서 스트림을 생성하는 것이다.

 

1) 컬렉션(Collection) - List 객체

List<String> list = Arrays.asList("Java", "Python", "C++", "JavaScript");

// List 객체에서 스트림 생성하기
Stream<String> stream = list.stream();

2) 배열(Array)

int[] arr = {1, 2, 3, 4, 5};

// int 배열에서 스트림 생성하기
IntStream stream = Arrays.stream(arr);

3) 파일(File)

텍스트 파일에서 한 줄씩 읽어와서 스트림을 생성하는 방법

Path path = Paths.get("file.txt");

// 파일에서 스트림 생성하기
Stream<String> stream = Files.lines(path);

위의 코드에서는 Files 클래스의 lines() 메소드를 호출하여 파일에서 스트림을 생성한다.

4) 지정된 범위의 연속된 정수

  • IntStream.range() 메서드 : 시작값(start)을 포함하고 종료값(end)을 포함하지 않는 범위의 정수 스트림을 생성
IntStream.range(1, 5)
  • IntStream.rangeClosed() 메서드:  시작값(start)과 종료값(end)을 모두 포함하는 범위의 정수 스트림을 생성
IntStream.rangeClosed(1, 4)

5) 람다 표현식

람다 표현식을 이용하여 스트림을 생성하는 방법은 다양하다.

  • Stream.generate() 메서드 : generate() 메서드는 Supplier 함수형 인터페이스를 인자로 받아서, Supplier가 반환하는 값을 무한히 스트림으로 생성한다.
  • 예를 들어, 1부터 시작하는 무한한 정수 스트림을 생성하는 코드는 다음과 같다.
Stream<Integer> infiniteStream = Stream.generate(() -> 1);
  • Stream.iterate() 메서드를 : seed 값을 시작으로, UnaryOperator 함수형 인터페이스를 인자로 받아서, 해당 함수를 반복적으로 적용하여 스트림을 생성한다.
  • 예를 들어, 1부터 시작하여 이전 값에 2를 더한 값으로 이루어진 스트림을 생성하는 코드는 다음과 같다.
Stream<Integer> stream = Stream.iterate(1, n -> n + 2);
  • Stream.iterate() 메서드를 이용하여 무한한 스트림을 생성할 때는, 항상 종료 조건을 명시해주어야 한다.
  • 그렇지 않으면 무한한 반복으로 인해 프로그램이 무한히 실행되거나, OutOfMemoryError가 발생할 수 있다.

 

  • limit() 메서드 :  종료 조건을 명시하는 방법, 스트림의 크기를 제한하는 메서드로, 인자로 주어진 숫자만큼의 요소를 가진 스트림을 반환
  • 예를 들어, 이전의 홀수 스트림 예제에서 처음 10개의 요소를 가진 스트림을 생성하는 코드는 다음과 같습니다.
Stream<Integer> stream = Stream.iterate(1, n -> n + 2).limit(10);

 

이외에도 Stream.of() 메소드를 사용하여 요소들을 직접 전달하거나 empty() 메소드를 통해 빈 스트림을 생성하는 방법이 있다.

 


🧮 중간 연산

요소(Element)를 가져와 처리한 후, 새로운 스트림을 반환하는 연산 
중간 연산은 여러 개의 연속된 메소드 호출을 통해 적용할 수 있으며, 각 중간 연산은 다음 중간 연산이나 최종 연산(terminal operation)과 연결된다. 
Stream 형태로 결과를 반환하기 때문에 연속적으로 연결하여 사용할 수 있다.

 

  • 스트림의 중간 연산은 지연 평가(lazy evaluation)를 지원한다.
  • 지연 평가는 요청이 있을 때만 연산을 수행하므로, 필요하지 않은 연산을 수행하지 않고 스트림 처리 속도를 높일 수 있다
  • 스트림의 중간 연산은 스트림을 변환하지 않고 요소를 처리하기 때문에, 스트림의 처리 속도를 높이는 중요한 역할을 한다. 이를 통해 코드의 가독성을 높이고, 개발자가 스트림을 보다 쉽게 사용할 수 있습니다.

 

1) filter(Predicate<T> predicate)

주어진 조건(predicate)에 해당하는 요소만 남기고 나머지 요소는 제외한다.

ex) 문자열 스트림에서 길이가 5 이상인 요소만 남기는 경우

Stream<String> stream = Stream.of("Java", "Python", "C++", "JavaScript");

// 길이가 5 이상인 요소만 남기기
Stream<String> filteredStream = stream.filter(s -> s.length() >= 5);

2) map(Function<T, R> mapper)

각 요소를 주어진 함수에 인수로 전달하여, 그 반환값으로 이루어진 새로운 스트림을 반환한다.

ex) 문자열 스트림에서 각 문자열을 대문자로 변환하는 경우

Stream<String> stream = Stream.of("Java", "Python", "C++", "JavaScript");

// 각 문자열을 대문자로 변환하기
Stream<String> mappedStream = stream.map(s -> s.toUpperCase());

3) sorted(Comparator<T>comparator) :

비교자(comparator)를 이용하여 요소를 정렬, 없을 경우 오름차순

ex) 숫자 스트림을 오름차순으로 정렬하는 경우

IntStream stream = IntStream.of(3, 1, 4, 1, 5, 9, 2, 6, 5);

// 숫자를 오름차순으로 정렬하기
IntStream sortedStream = stream.sorted();

4)distinct() :

중복된 요소를 제거

ex) 숫자 스트림에서 중복된 숫자를 제거하는 경우

IntStream stream = IntStream.of(3, 1, 4, 1, 5, 9, 2, 6, 5);

// 중복된 숫자 제거하기
IntStream distinctStream = stream.distinct();

filter()distinct() 연산은 불필요한 요소를 제거하므로 스트림 처리 속도를 높일 수 있다.

map() 연산은 요소를 변환하여 스트림의 크기를 줄일 수 있으므로, 메모리 사용량을 줄일 수 있다.

 

그 외에도 flatMap, limit, skip, peek 등이 있다.


🧾 최종 연산

트림의 처리 결과를 반환하거나, 스트림의 요소를 처리하면서 최종 결과를 반환 
최종 연산은 스트림을 소모하므로, 최종 연산 이후에는 스트림을 재사용할 수 없다.
Stream 인터페이스에 정의되어 있다.

 

  • forEach() : 스트림의 모든 요소에 대해 주어진 동작을 수행
  • count() : 스트림의 요소 개수를 반환
  • reduce() : 스트림의 요소를 이용해 값을 계산하고, 결과를 반환
  • collect() : 스트림의 요소를 수집해서 새로운 컬렉션을 생성하거나, 요소를 그룹화하거나, 통계 정보를 계산하는 등의 작업을 수행
  • min() / max() : 스트림에서 최솟값 혹은 최댓값을 반환
  • anyMatch() / allMatch() / noneMatch() : 스트림의 요소 중 하나라도 조건을 만족하는지, 모두 조건을 만족하는지, 하나도 조건을 만족하지 않는지 확인
  • findFirst() / findAny() : 스트림의 첫 번째 요소를 반환

 

Stream API를 이용해 List에 저장된 문자열 중 길이가 5 이상인 문자열의 개수를 구하는 예시

List<String> words = Arrays.asList("apple", "banana", "orange", "watermelon", "grape");
long count = words.stream()
                  .filter(word -> word.length() >= 5)
                  .count();
System.out.println(count); // 출력 결과: 3

위 코드에서는 filter() 메소드로 길이가 5 이상인 문자열만 선택한 후, count() 메소드로 선택된 문자열의 개수를 계산한다.

이때, count() 메소드는 최종 연산이므로, 결과값을 반환하면서 스트림이 소모된다.

 

 

 

📖 reference

StreamAPI 나도 한 번 써보자!

[JAVA] 자바_스트림 API (Stream API) 사용하기

http://www.tcpschool.com/java/java_stream_concept

728x90

댓글