본문 바로가기

JAVA

[Java] 자바 스트림 API에 대해 (Java8 Stream Guide & Example)

728x90

스트림API를 이해하기 위해서는 lamdba expressions. Optional, method references에 사전학습 하는 것을 권장합니다.

Java Method Reference

Java Lamdba Expressions

Java Optional

 

Stream API는 자바 8에 추가되어 다수의 데이터를 다룰 때 사용하는 배열이나 컬렉션의 비효율적인 면을 개선하기 위해 탄생한 방법입니다.(데이터를 입출력할 때 사용하는 스트림( I/O Streams )(ex: FileInputStream)과는 다른 개념입니다.)

스트림 API는 데이터를 추상화하여 다루어 다양한 데이터 타입을 같은 방법으로 다룰 수 있게 됩니다.

StreamAPI 는

  1. 컬렉션(이하 기존 반복문)은 외부 반복을 통해 작업하지만 스트림은 내부 반복(internal iteration)으로 작업합니다.
  2. 반복문은 재사용이 가능하지만 스트림은 한 번만 사용할 수 있습니다.
  3. 스트림은 원본 데이터를 변경하지 않습니다.
  4. 스트림의 연산은 필터-맵 기반의 API를 사용해 지연(lazy) 연산을 통해 성능을 최적화 합니다.
  5. parallelStream() 메소드를 통한 손쉬운 병렬 처리를 지원합니다.

지연(lazy) 연산: 인자 하나 씩 조건을 순회하여 불필요한 연산을 수행을 방지한다.

parallelStream(): 스트림 구현을 멀티쓰레드로 처리한다.
(일반적인 병럴처리는 멀티쓰레드를 구현하여 처리하여야 하지만 parallelStream을 통해 간단하게 병렬처리가 가능함)

스트림 API의 동작 흐름

  1. 스트림의 생성
  2. 스트림의 중개 연산 (스트림의 변환)
  3. 스트림의 최종 연산 (스트림의 사용)

스트림의 생성

스트림 API는 다양한 데이터 소스에 적용할 수 있습니다.

  1. 컬렉션
  2. 배열
  3. 가변 매개변수
  4. 지정된 범위의 연속된 정수
  5. 특정 타입 난수들
  6. 람다 표현식
  7. 파일
  8. 빈 스트림

컬렉션

자바의 Collection 인터페이스에는 stream() 메소드가 정의되어 있어 다른 컬랙션 구현 클래스에서도 stream() 메소드로 스트림을 생성할 수 있습니다.

ArrayList<Integer> list = new ArrayList<Integer>();

list.add(4);
list.add(2);
list.add(3);
list.add(1); 

// 컬렉션에서 스트림 생성
Stream<Integer> stream = list.stream();

// forEach() 메소드를 이용한 스트림 요소의 순차 접근
stream.forEach(System.out::println);
// 실행 결과
// 4
// 2
// 3
// 1

Stream 클래스의 forEach() 메소드는 해당 스트림 요소를 하나씩 소모해가며 순차적으로 요소에 접근합니다.
(같은 스트림에서는 한 번 밖에 호출할 수 없음)

배열

String[] arr = new String[]{"넷", "둘", "셋", "하나"};

// 배열에서 스트림 생성
Stream<String> stream1 = Arrays.stream(arr);
stream1.forEach(e -> System.out.print(e + " "));
System.out.println();
// 실행결과: 넷 둘 셋 하나

// 배열의 특정 부분만을 이용한 스트림 생성
Stream<String> stream2 = Arrays.stream(arr, 1, 3);
stream2.forEach(e -> System.out.print(e + " "));
// 실행결과: 둘 셋

Arrays 클래스의 stream() 메소드는 전체 배열뿐만 아니라 배열의 특정 부분만을 이용하여 스트림을 생성할 수도 있습니다.

람다 표현식

// iterate()
IntStream stream = Stream.iterate(2, n -> n + 2); // 2, 4, 6, 8, 10, ...

// generate()
Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneStream = Stream.generate(()->1);

Stream인터페이스로 구현된 IntStream나 LongStream 을 통해 매개변수를 받아 람다표현식으로 반환하는 방법입니다.

iterate() 메소드는 시드(seed)로 명시된 값을 람다 표현식에 사용하여 반환된 값을 다시 시드로 사용하는 방식으로 무한 스트림을 생성합니다.

반면 generate() 메소드는 매개변수가 없는 람다 표현식으로 무한 스트림을 생성합니다.

파일

파일의 한 행(line)을 요소로 하는 스트림을 생성하기 위해 java.nio.file.Files 클래스에는 lines() 메소드가 정의되어 있습니다.

또한, java.io.BufferedReader 클래스의 lines() 메소드를 이용하면 파일 뿐 아니라 다른 입력에서도 데이터를 읽어올 수 있습니다.

그 외 다른 데이터 타입의 생성도 있지만 이런 게 있다고 알아두고, 필요할 때 검색을 통해 사용하면 좋을 듯 싶네요.

스트림의 중간(중개) 연산(intermediate operation)

생성된 스트림을 이용해 필요한 데이터를 만드는 단계입니다.

  1. 스트림 필터링: filter(), distinct()
  2. 스트림 변환: map(), flatMap()
  3. 스트림 제한: limit(), skip()
  4. 스트림 정렬: sorted()
  5. 스트림 연산결과 확인: peek()

스트림 필터링

IntStream stream1 = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);
IntStream stream2 = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6);

// 스트림에서 중복된 요소를 제거함.
stream1.distinct().forEach(e -> System.out.print(e + " "));
System.out.println();

// 스트림에서 홀수만을 골라냄.
stream2.filter(n -> n % 2 != 0).forEach(e -> System.out.print(e + " "));

filter() 메소드는 해당 스트림에서 주어진 조건에 맞는 요소만으로 새로운 스트림을 반환합니다. 또한 distinct() 메소드는 해당 스트림에서 중복을 제거하여 반환합니다. (내부에서 equals() 메소드를 사용함)

스트림 변환

map() 메소드는 스트림의 요소들을 원하는 형태로 변환합니다.

latMap() 메소드는 스트림의 요소가 배열일 때, 단일 원소 스트림으로 반환합니다.

// Map 예시
Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "JAVASCRIPT");
stream.map(s -> s.length()).forEach(System.out::println);
// 4 3 4 10

// FlatMap 예시
String[] arr = {"I study hard", "You study JAVA", "I am hungry"};
Stream<String> stream = Arrays.stream(arr);
stream.flatMap(s -> Stream.of(s.split(" +"))).forEach(System.out::println);
// I study hard You study JAVA I am hungry

스트림 제한

limit() 메소드는 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환합니다.

skip() 메소드는 해당 스트림의 첫 요소부터 전달된 개수만큼을 제외한 나머지로 새로운 스트림을 반환합니다.

IntStream stream1 = IntStream.range(0, 10);
IntStream stream2 = IntStream.range(0, 10);
IntStream stream3 = IntStream.range(0, 10);

stream1.skip(4).forEach(n -> System.out.print(n + " "));
System.out.println();

stream2.limit(5).forEach(n -> System.out.print(n + " "));
System.out.println();

stream3.skip(3).limit(5).forEach(n -> System.out.print(n + " "));
// 4 5 6 7 8 9
// 0 1 2 3 4
// 3 4 5 6 7

스트림 정렬

sorted() 메소드는 비교자(comparator)를 이용해 정렬합니다.

기본 값으로는 사전 편찬 순(natural order)로 정렬합니다.

Stream<String> stream1 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");
Stream<String> stream2 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS");

stream1.sorted().forEach(s -> System.out.print(s + " "));
System.out.println();
stream2.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " "));

// CSS HTML JAVA JAVASCRIPT
// JAVASCRIPT JAVA HTML CSS

스트림 연산 결과 확인

peek() 메소드는 스트림에서 요소를 확인할 때 사용합니다.

주로 디버깅 용도로 많이 사용됩니다.

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

stream.peek(s -> System.out.println("원본 스트림 : " + s))
    .skip(2)
    .peek(s -> System.out.println("skip(2) 실행 후 : " + s))
    .limit(5)
    .peek(s -> System.out.println("limit(5) 실행 후 : " + s))
    .sorted()
    .peek(s -> System.out.println("sorted() 실행 후 : " + s))
    .forEach(n -> System.out.println(n));

/*
원본 스트림 : 7
원본 스트림 : 5
원본 스트림 : 5
skip(2) 실행 후 : 5
limit(5) 실행 후 : 5
원본 스트림 : 2
skip(2) 실행 후 : 2
limit(5) 실행 후 : 2
원본 스트림 : 1
skip(2) 실행 후 : 1
limit(5) 실행 후 : 1
원본 스트림 : 2
skip(2) 실행 후 : 2
limit(5) 실행 후 : 2
원본 스트림 : 3
skip(2) 실행 후 : 3
limit(5) 실행 후 : 3
sorted() 실행 후 : 1
1
sorted() 실행 후 : 2
2
sorted() 실행 후 : 2
2
sorted() 실행 후 : 3
3
sorted() 실행 후 : 5
5
*/

작동하는 순서를 살펴보면 중개 연산에 따라 스트림이 실시간으로 바뀌며 출력되는 값도 바뀌며 중개연산에 따라 스트림이 어떻게 변화하는지 알 수 있습니다.

스트림 최종 연산(terminal operation)

스트림 객체는 마지막 최종 연산으로 요소를 소모하고, 결과를 표시합니다.

  1. 요소의 출력 : forEach()
  2. 요소의 소모 : reduce()
  3. 요소의 검색 : findFirst(), findAny()
  4. 요소의 검사 : anyMatch(), allMatch(), noneMatch()
  5. 요소의 통계 : count(), min(), max()
  6. 요소의 연산 : sum(), average()
  7. 요소의 수집 : collect()

forEach / 요소 출력

명시된 동작을 수행합니다.

반환 타입이 void이므로 보통 스트림의 모든 요소를 출력하는 용도로 많이 사용합니다.

Stream<String> stream = Stream.of("넷", "둘", "셋", "하나");
stream.forEach(System.out::println);
// 넷 둘 셋 하나

reduce() / 요소 소모

첫 번째와 두 번째 요소를 가지고 연산을 수행한 뒤, 그 결과와 세 번째 요소를 가지고 또 다시 연산을 수행합니다.

각 문자열 요소를 “++” 기호로 연결하여 출력하는 예제입니다.

Stream<String> stream1 = Stream.of("넷", "둘", "셋", "하나");
Stream<String> stream2 = Stream.of("넷", "둘", "셋", "하나");

Optional<String> result1 = stream1.reduce((s1, s2) -> s1 + "++" + s2);
result1.ifPresent(System.out::println);

String result2 = stream2.reduce("시작", (s1, s2) -> s1 + "++" + s2);
System.out.println(result2);

// 넷++둘++셋++하나
// 시작++넷++둘++셋++하나

 

reduce()의 반환 타입은 Optional가 아닌 T 타입입니다.

findFirst(), findAny() / 요소의 검색

해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환합니다.

두 메소드 모두 빈 스트림에서는 빈 Optional을 반환합니다.

 

다음 예제는 스트림의 첫 번째 위치한 요소를 출력하는 예제입니다.

IntStream stream1 = IntStream.of(4, 2, 7, 3, 5, 1, 6);
IntStream stream2 = IntStream.of(4, 2, 7, 3, 5, 1, 6);

OptionalInt result1 = stream1.sorted().findFirst();
System.out.println(result1.getAsInt());

OptionalInt result2 = stream2.sorted().findAny();
System.out.println(result2.getAsInt());

// 1
// 1

두 메소드 같은 기능이지만 병렬 스트림의 경우에는 findAny를 사용해야 더 정확한 연산 결과를 반환합니다.

 

anyMatch(), allMatch(), noneMatch() / 요소의 검사

  1. anyMatch() : 스트림 일부 요소가 조건 만족 시 true 반환
  2. allMatch() : 스트림 모든 요소가 조건 만족 시 true 반환
  3. noneMatch() : 스트림 모든 요소가 만족하지 않은 경우 true 반환

세 메소드 모두 인자로 Predicate 객체를 전달받으며 boolean 값으로 반환합니다.

IntStream stream1 = IntStream.of(30, 90, 70, 10);
IntStream stream2 = IntStream.of(30, 90, 70, 10);

System.out.println(stream1.anyMatch(n -> n > 80));
System.out.println(stream2.allMatch(n -> n > 80));
// true
// false

 

count(), min(), max() / 요소의 통계

count() 메소드는 스트림의 요소 개수를 long 타입의 값으로 반환합니다. max()와 min() 메소드는 스트림의 가장 큰 값과 작은 값을 참조하는 Optional 객체를 반환합니다.

sum(), average() / 요소의 연산

IntStream 이나 DoubleStream 같은 기본 타입 스트림은 sum과 average 메소드를 통해 합과 평균을 구할 수 있습니다. 래핑된 기본 타입의 Optional 객체를 반환합니다.

collect() / 요소의 수집

collect() 메소드는 인수로 전달되는 Collectors 객체로 요소를 수집합니다.사용자가 직접 Collector 인터페이스르 구현해 정의할 수도 있습니다.

 

용도별 Collectors 메소드는 다음과 같습니다.

  1. 스트림을 배열이나 컬렉션으로 변환 : toArray(), toCollection(), toList(), toSet(), toMap()
  2. 요소의 통계와 연산 메소드와 같은 동작을 수행 : counting(), maxBy(), minBy(), summingInt(), averagingInt() 등
  3. 요소의 소모와 같은 동작을 수행 : reducing(), joining()
  4. 요소의 그룹화와 분할 : groupingBy(), partitioningBy()

 

collect() 메소드를 이용해 스트림을 리스트로 변환하는 예제

Stream<String> stream = Stream.of("넷", "둘", "하나", "셋");

List<String> list = stream.collect(Collectors.toList());
Iterator<String> iter = list.iterator();

while(iter.hasNext()) {
    System.out.print(iter.next() + " ");
}

 

partioningBy() 메소드로 글자 수에 따라 홀수와 짝수로 나누는 예제

Stream<String> stream = Stream.of("HTML", "CSS", "JAVA", "PHP");

Map<Boolean, List<String>> patition = stream.collect(Collectors.partitioningBy(s -> (s.length() % 2) == 0));

List<String> oddLengthList = patition.get(false);
System.out.println(oddLengthList);

List<String> evenLengthList = patition.get(true);
System.out.println(evenLengthList);

 

 

스트림API(함수형 프로그래밍)는 높은 추상화레벨로 다루어지기 때문에 한 눈에 이해하기 어려운 경우가 많아 굳이 Stream을 사용해야 할까는 생각이 듭니다. (속도 또한 for 반복문이 더 빠른 편이라고 합니다)

하지만 스트림API는 원본 데이터를 변형하지 않고, 어떤 데이터 형태도 Stream이라는 추상객체로 동일한 흐름으로 사용하며 무엇보다 간결한 코드를 작성할 수 있다는 활용가치가 높습니다.

스트림API는 활용 방법에 따라 간결하고, 더 고급스러운 코드를 짤 수 있습니다. 무조건 어느 한쪽이 더 좋다고 말할 수 있는 방법은 없다고 생각해서 상황에 따라 적절한 방법을 사용하면 좋을 것 같습니다.

 

 

참고 자료

https://stackify.com/streams-guide-java-8/

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

'JAVA' 카테고리의 다른 글

[Java] 메소드참조란? (MethodReferene란?)  (0) 2022.03.03
[Java] Java Optional에 대해  (0) 2022.03.02
[Java] 람다표현식 (Java-LamdbaExpressions)  (0) 2022.02.28
[JAVA] JDBC 사용하기  (0) 2021.03.16