Java 8 Stream – Java Stream

자바 8 스트림 API 튜토리얼에 오신 것을 환영합니다. 지난 몇 개의 자바 8 포스트에서는 자바 8 인터페이스 변경 사항함수형 인터페이스와 람다 표현식을 살펴보았습니다. 오늘은 자바 8에서 도입된 주요 API 중 하나인 자바 스트림을 살펴보겠습니다.

자바 8 스트림

  1. 자바 8 스트림
  2. 컬렉션과 자바 스트림
  3. 자바 8 스트림의 함수형 인터페이스
    1. 함수와 바이함수
    2. 프레디케이트와 바이프레디케이트
    3. 컨슈머와 바이컨슈머
    4. 공급자
  4. java.util.Optional
  5. java.util.Spliterator
  6. Java Stream 중간 및 최종 연산
  7. Java Stream 단축 연산
  8. Java Stream 예제
    1. Java Stream 생성
    2. Java Stream을 컬렉션 또는 배열로 변환
    3. Java Stream 중간 연산
    4. Java Stream 최종 연산
  9. Java 8 Stream API 제한사항

Java Stream

Java Stream API 예제를 살펴보기 전에, 왜 필요한지 알아보겠습니다. 정수 리스트를 반복하고 10보다 큰 모든 정수의 합을 찾고 싶다고 가정해보겠습니다. Java 8 이전에는 다음과 같은 방식으로 수행됩니다:

private static int sumIterator(List<Integer> list) {
	Iterator<Integer> it = list.iterator();
	int sum = 0;
	while (it.hasNext()) {
		int num = it.next();
		if (num > 10) {
			sum += num;
		}
	}
	return sum;
}

위 접근 방식에는 세 가지 주요 문제점이 있습니다:

  1. 우리는 정수의 합을 알고 싶지만, 이터레이션 방법도 제공해야 합니다. 이를 외부 반복(external iteration)이라고도 부르며, 클라이언트 프로그램이 리스트를 반복하는 알고리즘을 처리합니다.
  2. 프로그램은 순차적으로 진행되며, 병렬로 처리하기 쉽지 않습니다.
  3. 심지어 간단한 작업도 많은 코드가 필요합니다.

이러한 모든 단점을 극복하기 위해 Java 8 Stream API가 도입되었습니다. Java Stream API를 사용하여 내부 반복을 구현할 수 있습니다. 이는 자바 프레임워크가 이터레이션을 제어하기 때문에 더 나은 방법입니다. 내부 반복은 순차 및 병렬 실행, 주어진 기준에 따른 필터링, 매핑 등 여러 기능을 제공합니다. 대부분의 Java 8 Stream API 메소드 인수는 함수형 인터페이스이므로 람다 표현식이 잘 작동합니다. Java Streams를 사용하여 위의 로직을 한 줄의 문장으로 작성하는 방법을 살펴보겠습니다.

private static int sumStream(List<Integer> list) {
	return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}

위 프로그램은 자바 프레임워크의 반복 전략, 필터링 및 매핑 메소드를 활용하며 효율성을 높일 수 있습니다. 먼저 Java 8 Stream API의 핵심 개념을 살펴보고, 가장 일반적으로 사용되는 메소드를 이해하기 위해 몇 가지 예제를 살펴보겠습니다.

컬렉션과 Java Stream

A collection is an in-memory data structure to hold values and before we start using collection, all the values should have been populated. Whereas a java Stream is a data structure that is computed on-demand. Java Stream doesn’t store data, it operates on the source data structure (collection and array) and produce pipelined data that we can use and perform specific operations. Such as we can create a stream from the list and filter it based on a condition. Java Stream operations use functional interfaces, that makes it a very good fit for functional programming using lambda expression. As you can see in the above example that using lambda expressions make our code readable and short. Java 8 Stream internal iteration principle helps in achieving lazy-seeking in some of the stream operations. For example filtering, mapping, or duplicate removal can be implemented lazily, allowing higher performance and scope for optimization. Java Streams are consumable, so there is no way to create a reference to stream for future usage. Since the data is on-demand, it’s not possible to reuse the same stream multiple times. Java 8 Stream support sequential as well as parallel processing, parallel processing can be very helpful in achieving high performance for large collections. All the Java Stream API interfaces and classes are in the java.util.stream package. Since we can use primitive data types such as int, long in the collections using auto-boxing and these operations could take a lot of time, there are specific classes for primitive types – IntStream, LongStream and DoubleStream.

Java 8 Stream의 기능적 인터페이스

Java 8 Stream API 메서드에서 일반적으로 사용되는 기능적 인터페이스 몇 가지는 다음과 같습니다:

  1. Function과 BiFunction: Function은 하나의 유형의 인수를 가져와 다른 유형의 인수를 반환하는 함수를 나타냅니다. Function<T, R>은 T가 함수의 입력 유형이고 R이 함수의 결과 유형인 일반 형태입니다. 원시 유형을 처리하기 위해, 특정 Function 인터페이스인 – ToIntFunction, ToLongFunction, ToDoubleFunction, ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction, LongToIntFunction, LongToDoubleFunction, IntToLongFunction, IntToDoubleFunction 등이 있습니다. Function 또는 해당 원시 타입의 스트림이 사용되는 일부 Stream 메서드는 다음과 같습니다:
    • <R> Stream<R> map(Function<? super T, ? extends R> mapper)
    • IntStream mapToInt(ToIntFunction<? super T> mapper) – long 및 double을 반환하는 원시 특정 스트림도 동일하게 작동합니다.
    • IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – long 및 double도 동일하게 작동합니다.
    • <A> A[] toArray(IntFunction<A[]> generator)
    • <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
  2. Predicate 및 BiPredicate: 이는 스트림의 요소에 대해 테스트되는 조건을 나타냅니다. 이는 자바 스트림에서 요소를 필터링하는 데 사용됩니다. Function과 마찬가지로 int, long 및 double에 대한 원시 특정 인터페이스도 있습니다. Predicate 또는 BiPredicate 특수화가 사용되는 일부 스트림 메서드는 다음과 같습니다:
    • Stream<T> filter(Predicate<? super T> predicate)
    • boolean anyMatch(Predicate<? super T> predicate)
    • boolean allMatch(Predicate<? super T> predicate)
    • boolean noneMatch(Predicate<? super T> predicate)
  3. Consumer와 BiConsumer: 단일 입력 인수를 받아 결과를 반환하는 작업을 나타냅니다. 자바 스트림의 모든 요소에 대해 작업을 수행하는 데 사용할 수 있습니다. 자바 8 Stream에서 Consumer, BiConsumer 또는 해당 기본 타입 특화 인터페이스가 사용되는 일부 메서드는 다음과 같습니다:
    • Stream<T> peek(Consumer<? super T> action)
    • void forEach(Consumer<? super T> action)
    • void forEachOrdered(Consumer<? super T> action)
  4. Supplier: Supplier는 스트림에서 새로운 값을 생성할 수 있는 작업을 나타냅니다. 스트림에서 Supplier 인수를 사용하는 일부 메서드는 다음과 같습니다:
    • public static<T> Stream<T> generate(Supplier<T> s)
    • <R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner)

java.util.Optional

Java Optional은 null이 아닌 값이 포함되어 있을 수도 있고 없을 수도 있는 컨테이너 객체입니다. 값이 존재하는 경우, isPresent()는 true를 반환하고 get()은 값을 반환합니다. Stream의 종단 연산은 Optional 객체를 반환합니다. 이러한 메소드 중 일부는 다음과 같습니다:

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • Optional<T> min(Comparator<? super T> comparator)
  • Optional<T> max(Comparator<? super T> comparator)
  • Optional<T> findFirst()
  • Optional<T> findAny()

java.util.Spliterator

Java 8 Stream API에서 병렬 실행을 지원하기 위해 Spliterator 인터페이스가 사용됩니다. Spliterator의 trySplit 메소드는 원래 Spliterator의 일부 요소를 관리하는 새로운 Spliterator를 반환합니다.

Java Stream 중간 및 종단 연산

새로운 Stream을 반환하는 Java Stream API 작업은 중간 작업이라고합니다. 대부분의 경우, 이러한 작업은 게으른(lazy) 성격을 가지므로 새로운 스트림 요소를 생성하고 다음 작업으로 보냅니다. 중간 작업은 최종 결과를 생성하지 않는 작업입니다. 일반적으로 사용되는 중간 작업은 filtermap입니다. Java 8 Stream API 작업은 결과를 반환하거나 부작용을 생성합니다. 스트림에서 터미널 메서드를 호출하면 스트림이 소비되고 그 후에는 스트림을 사용할 수 없습니다. 터미널 작업은 성급한(eager) 성격입니다. 즉, 결과를 반환하기 전에 스트림의 모든 요소를 처리합니다. 일반적으로 사용되는 터미널 메서드는 forEach, toArray, min, max, findFirst, anyMatch, allMatch 등입니다. 터미널 메서드는 반환 유형에서 식별할 수 있으며, 스트림을 반환하지 않습니다.

Java Stream 단축 연산

중간 작업은 무한한 스트림에 대해 유한한 스트림을 생성할 수 있는 경우 단축(short circuiting) 작업이라고합니다. 예를 들어, limit()skip()은 두 가지 단축 중간 작업입니다. 터미널 작업은 무한한 스트림에 대해 유한한 시간 안에 종료될 수 있는 경우 단축 작업이라고합니다. 예를 들어, anyMatch, allMatch, noneMatch, findFirstfindAny는 단축 터미널 작업입니다.

자바 스트림 예제

I have covered almost all the important parts of the Java 8 Stream API. It’s exciting to use this new API features and let’s see it in action with some java stream examples.

자바 스트림 생성

배열과 컬렉션으로부터 자바 스트림을 생성하는 여러 가지 방법이 있습니다. 간단한 예제를 통해 살펴보겠습니다.

  1. Stream.of()를 사용하여 동일한 유형의 데이터로 스트림을 생성할 수 있습니다. 예를 들어, int 또는 Integer 객체들의 그룹으로부터 정수형 자바 스트림을 생성할 수 있습니다.

    Stream<Integer> stream = Stream.of(1,2,3,4);
    
  2. Stream.of()를 사용하여 객체 배열로부터 스트림을 반환할 수 있습니다. autoboxing을 지원하지 않으므로, 기본 자료형 배열은 전달할 수 없습니다.

    Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4}); 
    //작동 잘 됨
    
    Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4}); 
    //컴파일 시간 오류, Type mismatch: cannot convert from Stream<int[]> to Stream<Integer>
    
  3. 순차 스트림을 생성하기 위해 Collection의 stream()을 사용하고, 병렬 스트림을 생성하기 위해 parallelStream()을 사용할 수 있습니다.

    List<Integer> myList = new ArrayList<>();
    for(int i=0; i<100; i++) myList.add(i);
    		
    //순차 스트림
    Stream<Integer> sequentialStream = myList.stream();
    		
    //병렬 스트림
    Stream<Integer> parallelStream = myList.parallelStream();
    
  4. Stream.generate()Stream.iterate() 메소드를 사용하여 스트림을 생성할 수 있습니다.

    Stream<String> stream1 = Stream.generate(() -> {return "abc";});
    Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
    
  5. Arrays.stream()String.chars() 메소드를 사용합니다.

    LongStream is = Arrays.stream(new long[]{1,2,3,4});
    IntStream is2 = "abc".chars();
    

Java 스트림을 컬렉션 또는 배열로 변환하는 방법

Java 스트림에서 컬렉션 또는 배열을 얻는 여러 가지 방법이 있습니다.

  1. 우리는 자바 Stream의 collect() 메소드를 사용하여 스트림에서 List, Map 또는 Set을 얻을 수 있습니다.

    Stream<Integer> intStream = Stream.of(1,2,3,4);
    List<Integer> intList = intStream.collect(Collectors.toList());
    System.out.println(intList); // [1, 2, 3, 4] 출력
    
    intStream = Stream.of(1,2,3,4); //스트림이 닫혔으므로 다시 생성해야 합니다
    Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10));
    System.out.println(intMap); // {1=11, 2=12, 3=13, 4=14} 출력
    
  2. 우리는 스트림의 toArray() 메소드를 사용하여 스트림에서 배열을 만들 수 있습니다.

    Stream<Integer> intStream = Stream.of(1,2,3,4);
    Integer[] intArray = intStream.toArray(Integer[]::new);
    System.out.println(Arrays.toString(intArray)); // [1, 2, 3, 4] 출력
    

자바 Stream 중간 연산

일반적으로 사용되는 자바 Stream 중간 연산 예제를 살펴보겠습니다.

  1. 스트림 filter() 예제: filter() 메소드를 사용하여 조건에 맞는 스트림 요소를 테스트하고 필터링된 리스트를 생성할 수 있습니다.

    List<Integer> myList = new ArrayList<>();
    for(int i=0; i<100; i++) myList.add(i);
    Stream<Integer> sequentialStream = myList.stream();
    
    Stream<Integer> highNums = sequentialStream.filter(p -> p > 90); //90보다 큰 숫자 필터링
    System.out.print("90보다 큰 숫자=");
    highNums.forEach(p -> System.out.print(p+" "));
    //결과: "90보다 큰 숫자=91 92 93 94 95 96 97 98 99 "
    
  2. 스트림 map() 예제: map()을 사용하여 함수를 스트림에 적용할 수 있습니다. 문자열 리스트에 대문자 함수를 적용하는 방법을 살펴봅시다.

    Stream<String> names = Stream.of("aBc", "d", "ef");
    System.out.println(names.map(s -> {
    		return s.toUpperCase();
    	}).collect(Collectors.toList()));
    //결과: [ABC, D, EF]
    
  3. sorted() 예제: Comparator 인수를 전달하여 스트림 요소를 정렬하는 데 sorted()를 사용할 수 있습니다.

    Stream<String> names2 = Stream.of("aBc", "d", "ef", "123456");
    List<String> reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
    System.out.println(reverseSorted); // [ef, d, aBc, 123456]
    
    Stream<String> names3 = Stream.of("aBc", "d", "ef", "123456");
    List<String> naturalSorted = names3.sorted().collect(Collectors.toList());
    System.out.println(naturalSorted); //[123456, aBc, d, ef]
    
  4. flatMap() 예제: flatMap()을 사용하여 리스트 스트림에서 스트림을 생성할 수 있습니다. 간단한 예제로 이 의문을 해결해 봅시다.

    Stream<List<String>> namesOriginalList = Stream.of(
    	Arrays.asList("Pankaj"), 
    	Arrays.asList("David", "Lisa"),
    	Arrays.asList("Amit"));
    // List<String>에서 String 스트림으로 스트림을 평면화
    Stream<String> flatStream = namesOriginalList
    	.flatMap(strList -> strList.stream());
    
    flatStream.forEach(System.out::println);
    

자바 스트림 종단 연산

자바 스트림 종단 연산 예제를 살펴보겠습니다.

  1. 스트림 reduce() 예제: reduce()를 사용하여 스트림의 요소에 대한 축소(reduction)를 수행하고, 연관성 있는 누적 함수를 사용하여 Optional을 반환할 수 있습니다. 스트림에서 정수를 곱하는 방법을 알아보겠습니다.

    Stream<Integer> numbers = Stream.of(1,2,3,4,5);
    		
    Optional<Integer> intOptional = numbers.reduce((i,j) -> {return i*j;});
    if(intOptional.isPresent()) System.out.println("곱셈 = "+intOptional.get()); //120
    
  2. 스트림 count() 예제: 이 종단 연산을 사용하여 스트림의 항목 수를 계산할 수 있습니다.

    Stream<Integer> numbers1 = Stream.of(1,2,3,4,5);
    		
    System.out.println("스트림의 요소 수="+numbers1.count()); //5
    
  3. Stream forEach() 예제: 이를 사용하여 스트림을 반복할 수 있습니다. iterator 대신에 이를 사용할 수 있습니다. 스트림의 모든 요소를 출력하는 방법을 살펴보겠습니다.

    Stream<Integer> numbers2 = Stream.of(1,2,3,4,5);
    numbers2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
    
  4. Stream match() 예제: Stream API의 일치 메서드에 대한 몇 가지 예제를 살펴보겠습니다.

    Stream<Integer> numbers3 = Stream.of(1,2,3,4,5);
    System.out.println("Stream contains 4? "+numbers3.anyMatch(i -> i==4));
    //Stream contains 4? true
    
    Stream<Integer> numbers4 = Stream.of(1,2,3,4,5);
    System.out.println("Stream contains all elements less than 10? "+numbers4.allMatch(i -> i<10));
    //Stream contains all elements less than 10? true
    
    Stream<Integer> numbers5 = Stream.of(1,2,3,4,5);
    System.out.println("Stream doesn't contain 10? "+numbers5.noneMatch(i -> i==10));
    //Stream doesn't contain 10? true
    
  5. 스트림 findFirst() 예제: 이것은 단축 평가(short circuiting) 종단 연산입니다. D로 시작하는 스트림에서 첫 번째 문자열을 찾는 방법을 살펴보겠습니다.

    Stream<String> names4 = Stream.of("Pankaj","Amit","David", "Lisa");
    Optional<String> firstNameWithD = names4.filter(i -> i.startsWith("D")).findFirst();
    if(firstNameWithD.isPresent()){
    	System.out.println("D로 시작하는 첫 번째 이름="+firstNameWithD.get()); //David
    }
    

Java 8 스트림 API의 제한 사항

Java 8 스트림 API는 리스트와 배열을 처리하기 위한 많은 새로운 기능을 제공하지만, 몇 가지 제한 사항도 있습니다.

  1. 상태 없는 람다 표현식: 병렬 스트림을 사용하고 람다 표현식이 상태를 가지면 임의의 응답이 발생할 수 있습니다. 간단한 프로그램으로 살펴보겠습니다. StatefulParallelStream.java

    package com.journaldev.java8.stream;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    public class StatefulParallelStream {
    
    	public static void main(String[] args) {
    
    		List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
    		List<Integer> result = new ArrayList<Integer>();
    		 
    		Stream<Integer> stream = ss.parallelStream();
    		 
    		stream.map(s -> {
    		        synchronized (result) {
    		          if (result.size() < 10) {
    		            result.add(s);
    		          }
    		        }
    				return s;
    		    }).forEach( e -> {});
    		 System.out.println(result);   
    	}
    }
    

    위의 프로그램을 실행하면 다른 결과가 나옵니다. 이는 스트림이 반복되는 방식에 따라 달라지기 때문이며, 병렬 처리에 대한 순서를 정의하지 않았기 때문입니다. 순차 스트림을 사용하면 이러한 문제가 발생하지 않습니다.

  2. 스트림이 한 번 사용되면 나중에 사용할 수 없습니다. 위의 예제에서 볼 수 있듯이 매번 스트림을 생성하고 있습니다.

  3. 스트림 API에는 많은 메소드가 있으며 가장 혼란스러운 부분은 오버로드된 메소드입니다. 이로 인해 학습 곡선이 시간이 오래 걸립니다.

이것이 Java 8 스트림 예제 튜토리얼의 모든 내용입니다. 이 기능을 사용하여 병렬 처리를 통해 코드를 읽기 쉽고 성능이 개선되도록 할 예정입니다. 참조: Java Stream API 문서

Source:
https://www.digitalocean.com/community/tutorials/java-8-stream