1. 개요

이 포괄적 인 자습서에서는 생성에서 병렬 실행에 이르기까지 Java 8 Streams의 실제 사용을 살펴 봅니다.

이 자료를 이해하려면 독자는 Java 8 (람다 표현식, 선택 사항, 메소드 참조) 및 Stream API에 대한 기본 지식이 있어야합니다 . 이 주제에 더 익숙해 지려면 이전 기사 인 Java 8의 새로운 기능Java 8 Streams 소개를 참조하십시오 .

2. 스트림 생성

다양한 소스의 스트림 인스턴스를 만드는 방법에는 여러 가지가 있습니다. 일단 생성되면 인스턴스 는 소스를 수정하지 않으므로 단일 소스에서 여러 인스턴스를 생성 할 수 있습니다.

2.1. 빈 스트림

빈 스트림을 생성하는 경우 empty () 메서드를 사용해야합니다 .

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

요소가없는 스트림에 대해 null반환하지 않도록 생성시 빈 () 메서드를 자주 사용합니다 .

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

2.2. 컬렉션의 흐름

또한 모든 유형의 컬렉션 ( Collection, List, Set ) 의 스트림을 만들 수 있습니다 .

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

2.3. 어레이 스트림

배열은 스트림의 소스가 될 수도 있습니다.

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);

2.4. Stream.builder ()

빌더를 사용할 때 원하는 유형을 문의 오른쪽 부분에 추가로 지정해야합니다. 그렇지 않으면 build () 메서드가 Stream <Object> 의 인스턴스를 만듭니다 .

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

2.5. Stream.generate ()

생성 () 메소드는 받아 공급 <T> 요소를 생성한다. 결과 스트림은 무한하므로 개발자는 원하는 크기를 지정해야합니다. 그렇지 않으면 generate () 메서드가 메모리 제한에 도달 할 때까지 작동합니다.

Stream<String> streamGenerated =
  Stream.generate(() -> "element").limit(10);

위의 코드는 값이 "element" 인 10 개의 문자열 시퀀스를 만듭니다 .

2.6. Stream.iterate ()

무한 스트림을 만드는 또 다른 방법은 iterate () 메서드를 사용하는 것입니다.

Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);

결과 스트림의 첫 번째 요소는 iterate () 메서드 의 첫 번째 매개 변수입니다 . 이후의 모든 요소를 ​​생성 할 때 지정된 기능이 이전 요소에 적용됩니다. 위의 예에서 두 번째 요소는 42입니다.

2.7. 원시 스트림

Java 8은 int, longdouble 의 세 가지 기본 유형에서 스트림을 생성 할 수있는 가능성을 제공합니다 . 으로 스트림 <T> 제네릭 인터페이스이며, 제네릭 형식 매개 변수로 사용 프리미티브 수있는 방법은 없습니다, 세 가지 새로운 특수 인터페이스가 만들어졌습니다 : IntStream, LongStream, DoubleStream을.

새로운 인터페이스를 사용하면 불필요한 자동 박싱이 완화되어 생산성이 향상됩니다.

IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);

범위 (INT startInclusive, INT endExclusive) 에있어서, 상기 제 파라미터의 파라미터와 제 순서화 된 스트림을 생성한다. 단계가 1과 같은 후속 요소의 값을 증가시킵니다. 결과는 마지막 매개 변수를 포함하지 않으며 시퀀스의 상한 일뿐입니다.

rangeClosed (INT startInclusive, INT endInclusive)  방법은 단 하나 개의 차이와 같은 일을, 두 번째 요소가 포함된다. 이 두 가지 방법을 사용하여 세 가지 유형의 프리미티브 스트림을 생성 할 수 있습니다.

Java 8 이후 Random 클래스는 기본 스트림을 생성하기위한 광범위한 메소드를 제공합니다. 예를 들어 다음 코드 는 세 가지 요소가 있는 DoubleStream을 만듭니다 .

Random random = new Random();
DoubleStream doubleStream = random.doubles(3);

2.8. 문자열의 흐름

String 클래스 chars () 메서드를 사용하여 스트림을 생성하기위한 소스로  String사용할 수도 있습니다 . JDK에는 CharStream대한 인터페이스가 없으므로 대신 IntStream사용하여 문자 스트림을 나타냅니다.

IntStream streamOfChars = "abc".chars();

다음 예제는 지정된 RegEx 에 따라 문자열 을 하위 문자열나눕니다 .

Stream<String> streamOfString =
  Pattern.compile(", ").splitAsStream("a, b, c");

2.9. 파일 스트림

또한 Java NIO 클래스 파일을 사용 하면 lines () 메서드를 통해 텍스트 파일의 Stream <String> 을 생성 할 수 있습니다 . 텍스트의 모든 줄은 스트림의 요소가됩니다.

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

캐릭터 세트 의 인수로 지정 될 수 라인 () 방법.

3. 스트림 참조

중간 작업 만 호출되는 한 스트림을 인스턴스화하고 액세스 가능한 참조를 가질 수 있습니다. 터미널 작업을 실행하면 스트림에 액세스 할 수 없습니다 .

이를 증명하기 위해 우리는 작업 순서를 연결하는 것이 모범 사례라는 사실을 잠시 잊어 버릴 것입니다. 불필요한 자세한 설명 외에도 기술적으로 다음 코드가 유효합니다.

Stream<String> stream = 
  Stream.of("a", "b", "c").filter(element -> element.contains("b"));
Optional<String> anyElement = stream.findAny();

그러나 터미널 작업을 호출 한 후 동일한 참조를 재사용하려고하면 IllegalStateException 이 트리거됩니다 .

Optional<String> firstElement = stream.findFirst();

는 AS IllegalStateException이가 A는 RuntimeException을 , 컴파일러는 문제에 대해 시그널링 화 (signaling)되지 않습니다. 따라서 Java 8 스트림은 재사용 할 수 없음 을 기억하는 것이 매우 중요합니다 .

이런 종류의 행동은 논리적입니다. 요소를 저장하는 것이 아니라 기능적 스타일의 요소 소스에 유한 한 작업 시퀀스를 적용하도록 스트림을 설계했습니다.

따라서 이전 코드가 제대로 작동하도록하려면 몇 가지 사항을 변경해야합니다.

List<String> elements =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"))
    .collect(Collectors.toList());
Optional<String> anyElement = elements.stream().findAny();
Optional<String> firstElement = elements.stream().findFirst();

4. 스트림 파이프 라인

데이터 소스의 요소에 대해 일련의 작업을 수행하고 그 결과를 집계하려면 소스 , 중간 작업터미널 작업 세 부분이 필요 합니다.

중간 작업은 수정 된 새 스트림을 반환합니다. 예를 들어, 요소가 거의없는 기존 스트림의 새 스트림을 만들려면 skip () 메서드를 사용해야합니다.

Stream<String> onceModifiedStream =
  Stream.of("abcd", "bbcd", "cbcd").skip(1);

하나 이상의 수정이 필요한 경우 중간 작업을 연결할 수 있습니다. 현재 Stream <String> 의 모든 요소를 처음 몇 문자의 하위 문자열 로 대체해야한다고 가정 해 보겠습니다 . skip ()map () 메소드 를 연결하여이를 수행 할 수 있습니다 .

Stream<String> twiceModifiedStream =
  stream.skip(1).map(element -> element.substring(0, 3));

보시다시피 map () 메서드는 람다 식을 매개 변수로 사용합니다. 람다에 대해 자세히 알아 보려면 Lambda 표현식 및 기능 인터페이스 : 팁 및 모범 사례 자습서를 참조하십시오 .

스트림 자체는 쓸모가 없습니다. 사용자는 터미널 작업의 결과에 관심이 있습니다.이 결과는 스트림의 모든 요소에 적용되는 특정 유형의 값 또는 작업 일 수 있습니다. 스트림 당 하나의 터미널 작업 만 사용할 수 있습니다.

스트림을 사용하는 정확하고 가장 편리한 방법 은 스트림 소스, 중간 작업 및 터미널 작업의 체인 인 스트림 파이프 라인을 사용하는 것입니다.

List<String> list = Arrays.asList("abc1", "abc2", "abc3");
long size = list.stream().skip(1)
  .map(element -> element.substring(0, 3)).sorted().count();

5. Lazy 호출

중간 작업은 지연됩니다. , 터미널 작업 실행에 필요한 경우에만 호출됩니다.

예를 들어, 메서드 호출하자 () wasCalled , 이 호출 할 때마다 내부 카운터를 증가한다 :

private long counter;
 
private void wasCalled() {
    counter++;
}

이제 filter () 작업에서 wasCalled () 메서드를 호출 해 보겠습니다 .

List<String> list = Arrays.asList(“abc1”, “abc2”, “abc3”);
counter = 0;
Stream<String> stream = list.stream().filter(element -> {
    wasCalled();
    return element.contains("2");
});

우리는 세 가지 요소의 소스를 가지고, 우리가 있다고 가정 할 수 있습니다 필터 () 메서드를 세 번 호출됩니다,와의 값 카운터 변수가 그러나 3 것, 변하지 않는이 코드를 실행 카운터를 모두에서, 그것은이다 여전히 0이므로 filter () 메서드는 한 번도 호출되지 않았습니다. 터미널 작업이 누락 된 이유입니다.

map () 연산과 터미널 연산 인 findFirst () 를 추가하여이 코드를 약간 다시 작성해 보겠습니다 . 또한 로깅을 사용하여 메서드 호출 순서를 추적하는 기능을 추가합니다.

Optional<String> stream = list.stream().filter(element -> {
    log.info("filter() was called");
    return element.contains("2");
}).map(element -> {
    log.info("map() was called");
    return element.toUpperCase();
}).findFirst();

결과 로그는 filter () 메서드를 두 번 호출 하고 map () 메서드를 한 번 호출했음을 보여줍니다 . 파이프 라인이 수직으로 실행되기 때문입니다. 이 예에서 스트림의 첫 번째 요소는 필터의 조건자를 충족하지 않았습니다. 그런 다음 필터 를 통과 한 두 번째 요소에 대해 filter () 메서드를 호출했습니다 . 세 번째 요소에 대해 filter ()호출하지 않고 파이프 라인을 통해 map () 메서드로 이동했습니다.

로 findFirst () 하나 개의 요소에 의해 동작 만족시킨다. 따라서이 특정 예제에서 지연 호출을 사용하면 두 개의 메서드 호출을 피할 수 있습니다. 하나는 filter ()에 대한 것이고 다른 하나는 map ()에 대한 것입니다.

6. 실행 순서

성능 관점 에서 올바른 순서는 스트림 파이프 라인에서 작업 연결의 가장 중요한 측면 중 하나입니다.

long size = list.stream().map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).skip(2).count();

이 코드를 실행하면 카운터 값이 3만큼 증가합니다. , 스트림 map () 메서드를 세 번 호출 했지만 크기은 1입니다. 따라서 결과 스트림에는 단 하나의 요소 만 있으며 세 번 중 두 번은 아무 이유없이 값 비싼 map () 작업을 실행했습니다 .

우리는의 순서를 변경하는 경우 스킵 ()Map () 방법 , 카운터가 하나 증가합니다. 따라서 map () 메서드를 한 번만 호출합니다 .

long size = list.stream().skip(2).map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).count();

이것은 우리에게 다음 규칙을 가져옵니다 : 스트림의 크기를 줄이는 중간 작업은 각 요소에 적용되는 작업보다 먼저 배치되어야합니다. 따라서 s kip (), filter ()distinct ()같은 메서드를 스트림 파이프 라인의 맨 위에 유지해야합니다.

7. 스트림 감소

API에는 count (), max (), min ()sum ()같이 스트림을 유형 또는 기본 요소로 집계하는 많은 터미널 작업이 있습니다. 그러나 이러한 작업은 미리 정의 된 구현에 따라 작동합니다. 그렇다면 개발자가 Stream의 감소 메커니즘을 사용자 정의해야한다면 어떻게 될까요? 이를 수행 할 수있는 두 가지 방법, reduce () collect () 메소드가 있습니다.

7.1. 감소 () 메소드

이 메서드에는 세 가지 변형이 있으며 서명과 반환 유형이 다릅니다. 다음 매개 변수를 가질 수 있습니다.

identity – 누산기의 초기 값 또는 스트림이 비어 있고 누산 할 것이없는 경우 기본값

accumulator – 요소 집계 논리를 지정하는 함수입니다. 누산기가 감소하는 모든 단계에 대해 새 값을 생성하므로 새 값의 양은 스트림의 크기와 동일하며 마지막 값만 유용합니다. 이것은 성능에 그리 좋지 않습니다.

결합기 – 누산기의 결과를 집계하는 함수. 서로 다른 스레드의 누산기 결과를 줄이기 위해 병렬 모드에서만 결합기를 호출합니다.

이제이 세 가지 방법을 살펴 보겠습니다.

OptionalInt reduced =
  IntStream.range(1, 4).reduce((a, b) -> a + b);

감소 = 6 (1 + 2 + 3)

int reducedTwoParams =
  IntStream.range(1, 4).reduce(10, (a, b) -> a + b);

reduceTwoParams = 16 (10 + 1 + 2 + 3)

int reducedParams = Stream.of(1, 2, 3)
  .reduce(10, (a, b) -> a + b, (a, b) -> {
     log.info("combiner was called");
     return a + b;
  });

결과는 이전 예제 (16)와 동일하며 로그인이 없습니다. 이는 결합기가 호출되지 않았 음을 의미합니다. 결합기를 작동하려면 스트림이 병렬이어야합니다.

int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()
    .reduce(10, (a, b) -> a + b, (a, b) -> {
       log.info("combiner was called");
       return a + b;
    });

여기의 결과는 다르며 (36) 결합기가 두 번 호출되었습니다. 여기서 감소는 다음 알고리즘에 의해 작동합니다. 누산기는 스트림의 모든 요소를 identity 에 추가하여 세 번 실행했습니다 . 이러한 작업은 병렬로 수행됩니다. 결과적으로 (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;)이 있습니다. 이제 결합기는이 세 가지 결과를 병합 할 수 있습니다. 이를 위해서는 두 번의 반복이 필요합니다 (12 + 13 = 25; 25 + 11 = 36).

7.2. 수집 () 메소드

스트림 축소는 다른 터미널 작업 인 collect () 메서드로 도 실행할 수 있습니다 . 감소 메커니즘을 지정하는 Collector 유형의 인수를 허용합니다 . 대부분의 일반적인 작업을 위해 이미 생성되고 미리 정의 된 수집기가 있습니다. 수집기 유형 의 도움으로 액세스 할 수 있습니다 .

이 섹션에서는 모든 스트림의 소스로 다음 List사용합니다 .

List<Product> productList = Arrays.asList(new Product(23, "potatoes"),
  new Product(14, "orange"), new Product(13, "lemon"),
  new Product(23, "bread"), new Product(13, "sugar"));

스트림을 컬렉션으로 변환 ( Collection, List 또는 Set ) :

List<String> collectorCollection = 
  productList.stream().map(Product::getName).collect(Collectors.toList());

문자열로 줄이기 :

String listToString = productList.stream().map(Product::getName)
  .collect(Collectors.joining(", ", "[", "]"));

소목 () 메소드는 1 내지 3 개의 파라미터들 (단락, 접두사, 접미사)에서있을 수있다. joiner () 사용에 대한 가장 편리한 점은 개발자가 구분 기호를 적용하지 않고 접미사를 적용하기 위해 스트림이 끝났는지 확인할 필요가 없다는 것입니다. 수집가 가 처리합니다.

스트림의 모든 숫자 요소의 평균값 처리 :

double averagePrice = productList.stream()
  .collect(Collectors.averagingInt(Product::getPrice));

스트림의 모든 숫자 요소 합계 처리 :

int summingPrice = productList.stream()
  .collect(Collectors.summingInt(Product::getPrice));

averagingXX (), summingXX ()summarizingXX () 메서드 는 기본 요소 ( int, long, double ) 및 래퍼 클래스 ( Integer, Long, Double ) 와 함께 작동 할 수 있습니다 . 이러한 방법의 한 가지 더 강력한 기능은 매핑을 제공하는 것입니다. 결과적으로 개발자는 collect () 메소드 전에 추가 map () 작업 을 사용할 필요가 없습니다 .

스트림 요소에 대한 통계 정보 수집 :

IntSummaryStatistics statistics = productList.stream()
  .collect(Collectors.summarizingInt(Product::getPrice));

IntSummaryStatistics 유형의 결과 인스턴스를 사용하여 개발자는 toString () 메서드를 적용하여 통계 보고서를 만들 수 있습니다 . 결과는 "IntSummaryStatistics {count = 5, sum = 86, min = 13, average = 17,200000, max = 23}"에 공통된 문자열됩니다.

getCount (), getSum (), getMin (), getAverage ()getMax () 메소드를 적용 하여이 객체에서 count, sum, minaverage대한 개별 값을 쉽게 추출 할 있습니다. 이러한 모든 값은 단일 파이프 라인에서 추출 할 수 있습니다.

지정된 함수에 따라 스트림 요소 그룹화 :

Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
  .collect(Collectors.groupingBy(Product::getPrice));

위의 예에서 스트림은 모든 제품을 가격별로 그룹화 하는 Map 으로 축소되었습니다 .

일부 술어에 따라 스트림의 요소를 그룹으로 나누기 :

Map<Boolean, List<Product>> mapPartioned = productList.stream()
  .collect(Collectors.partitioningBy(element -> element.getPrice() > 15));

추가 변환을 수행하기 위해 수집기를 푸시 :

Set<Product> unmodifiableSet = productList.stream()
  .collect(Collectors.collectingAndThen(Collectors.toSet(),
  Collections::unmodifiableSet));

이 특별한 경우에 수집기는 스트림을 Set 로 변환 한 다음 그로부터 변경할 수없는 Set 을 생성 했습니다.

커스텀 컬렉터 :

어떤 이유로 사용자 지정 수집기를 만들어야하는 경우 가장 쉽고 간단한 방법 수집기 유형의 of () 메서드를 사용하는 것 입니다 .

Collector<Product, ?, LinkedList<Product>> toLinkedList =
  Collector.of(LinkedList::new, LinkedList::add, 
    (first, second) -> { 
       first.addAll(second); 
       return first; 
    });

LinkedList<Product> linkedListOfPersons =
  productList.stream().collect(toLinkedList);

이 예제에서 Collector 의 인스턴스 LinkedList <Persone>으로 축소되었습니다 .

8. 병렬 스트림

Java 8 이전에는 병렬화가 복잡했습니다. ExecutorServiceForkJoin 의 출현은 개발자의 삶을 약간 단순화했지만, 특정 실행기를 만드는 방법, 실행 방법 등을 기억할 가치가있었습니다. Java 8은 기능적 스타일에서 병렬 처리를 수행하는 방법을 도입했습니다.

API를 사용하면 병렬 모드에서 작업을 수행하는 병렬 스트림을 만들 수 있습니다. 스트림의 소스가 Collection 또는 array 인 경우 parallelStream () 메서드를 사용하여 수행 할 수 있습니다 .

Stream<Product> streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
  .map(product -> product.getPrice() * 12)
  .anyMatch(price -> price > 200);

스트림의 소스가 Collection 또는 배열이 아닌 경우 parallel () 메서드를 사용해야합니다.

IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();

내부적으로 Stream API는 자동으로 ForkJoin 프레임 워크를 사용하여 작업을 병렬로 실행합니다. 기본적으로 공통 스레드 풀이 사용되며 (적어도 현재로서는) 일부 사용자 지정 스레드 풀을 할당 할 방법이 없습니다. 이것은 병렬 수집기의 사용자 지정 집합을 사용하여 극복 할 수 있습니다.

병렬 모드에서 스트림을 사용하는 경우 작업을 차단하지 마십시오. 또한 작업을 실행하는 데 비슷한 시간이 필요한 경우 병렬 모드를 사용하는 것이 가장 좋습니다. 한 작업이 다른 작업보다 훨씬 오래 지속되면 전체 앱의 워크 플로가 느려질 수 있습니다.

병렬 모드의 스트림은 sequential () 메서드를 사용하여 다시 순차 모드로 변환 할 수 있습니다 .

IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();

9. 결론

Stream API는 강력하지만 요소 시퀀스를 처리하기위한 도구 모음을 이해하기 쉽습니다. 적절히 사용하면 엄청난 양의 상용구 코드를 줄이고 더 읽기 쉬운 프로그램을 만들고 앱의 생산성을 향상시킬 수 있습니다.

이 기사에 표시된 대부분의 코드 샘플에서는 스트림을 사용하지 않고 그대로 두었 습니다 (close () 메서드 또는 터미널 작업을 적용하지 않았습니다 ). 실제 앱에서는 메모리 누수로 이어질 수 있으므로 인스턴스화 된 스트림을 사용하지 않은 상태로 두지 마십시오.

이 문서와 함께 제공되는 전체 코드 샘플은 GitHub에서 사용할 수 있습니다.