Processing Data in Memory Using the Stream API [스트림 API]

 


스트림 API를 사용하여 메모리에서 데이터 처리

 

스트림 API 소개

스트림 API는 람다 표현식 다음에 Java SE 8에 추가 된 두 번째로 중요한 기능 일 것입니다. 간단히 말해서, Stream API는 잘 알려진 맵 필터 감소 알고리즘을 JDK에 구현하는 것입니다.

Collections Framework는 JVM 메모리에 데이터를 저장하고 구성하는 것입니다. Stream API를 Collections Framework의 컴패니언 프레임 워크로보고이 데이터를 매우 효율적으로 처리 할 수 있습니다. 실제로 컬렉션에서 스트림을 열어 포함 된 데이터를 처리 할 수 있습니다.

여기서 멈추지 않습니다. Stream API는 컬렉션의 데이터를 처리하는 것보다 훨씬 더 많은 것을 할 수 있습니다. JDK는 I / O 소스를 포함하여 다른 소스에서 스트림을 생성하는 몇 가지 패턴을 제공합니다. 또한 적은 노력으로 자신의 요구에 완벽하게 맞는 고유 한 데이터 소스를 만들 수 있습니다.

Stream API를 마스터하면 매우 표현적인 코드를 작성할 수 있습니다. 올바른 정적 가져 오기로 컴파일 할 수있는 작은 스 니펫이 있습니다:

List<String> strings = List.of("one","two","three","four");
var map = strings.stream()
                 .collect(groupingBy(String::length, counting()));
map.forEach((key, value) -> System.out.println(key + " :: " + value));

이 코드는 다음을 인쇄합니다.

이 코드를 실행하면 다음과 같은 결과가 발생합니다.

3 :: 2
4 :: 1
5 :: 1

Stream API에 익숙하지 않더라도이를 사용하는 코드를 읽으면 언뜻보기에 수행중인 작업에 대한 아이디어를 얻을 수 있습니다.

 

맵 필터 감소 알고리즘 소개

Stream API 자체에서 다이빙하기 전에 필요한 맵 필터 감소 알고리즘의 요소를 살펴 보겠습니다.

이 알고리즘은 데이터를 처리하는 매우 고전적인 알고리즘입니다. 예를 들어 봅시다. 세트가 있다고 가정하십시오 Sale 날짜, 제품 참조 및 양의 세 가지 속성을 가진 객체. 단순화를 위해 금액은 정수라고 가정합니다. 여기가 Sale 수업.

public class Sale {
    private String product;
    private LocalDate date;
    private int amount;

    // constructors, getters, setters
    // equals, hashCode, toString
}

3 월에 판매 총액을 계산해야한다고 가정하십시오. 다음 코드를 작성하게 될 것입니다.

List<Sale> sales = ...; // this is the list of all the sales
int amountSoldInMarch = 0;
for (Sale sale: sales) {
    if (sale.getDate().getMonth() == Month.MARCH) {
        amountSoldInMarch += sale.getAmount();
    }
}
System.out.println("Amount sold in March: " + amountSoldInMarch);

이 간단한 데이터 처리 알고리즘에서 세 단계를 볼 수 있습니다.

첫 번째 단계는 3 월에 발생한 판매 만 고려하는 것입니다. 너는 필터링 주어진 기준에 따라 처리중인 일부 요소를. 이것이 바로 필터링 단계입니다.

두 번째 단계는 sale 목적. 당신은 전체 객체에 관심이 없습니다. 당신이 필요로하는 것은 amount 재산. 너는 매핑 그만큼 sale 양에 대한 객체, 즉 int 값. 이것이 매핑 단계입니다. 처리중인 객체를 다른 객체 또는 값으로 변환하는 것으로 구성됩니다.

마지막 단계는이 모든 금액을 한 금액으로 합산하는 것입니다. SQL 언어에 익숙하다면이 마지막 단계가 집계처럼 보입니다. 실제로, 그것은 동일합니다. 이 합계는 절감 개별 금액의 한 금액입니다.

그건 그렇고, SQL 언어는 이런 종류의 처리를 읽을 수있는 방식으로 표현하는 데 매우 효과적입니다. 필요한 SQL 코드는 읽기 매우 쉽습니다:

select sum(amount)
from Sales
where extract(month from date) = 3;

 

알고리즘 프로그래밍 대신 결과 지정

SQL에서 작성중인 것은 필요한 결과에 대한 설명입니다. 3 월에 이루어진 모든 판매 금액의 합계입니다. 효율적으로 계산하는 방법을 알아내는 것은 데이터베이스 서버의 책임입니다.

이 양을 계산하는 Java 스 니펫은이 양이 어떻게 계산되는지에 대한 단계별 설명입니다. 명령적인 방식으로 정확하게 설명됩니다. Java 런타임이이 계산을 최적화 할 여지가 거의 없습니다.

Stream API의 두 가지 목표는보다 읽기 쉽고 표현적인 코드를 작성하고 Java 런타임에 계산을 최적화 할 수있는 흔들림 공간을 제공하는 것입니다.

 

객체를 다른 객체 또는 값에 매핑

맵 필터 감소 알고리즘의 첫 번째 단계는 매핑 단계. 매핑은 객체를 변환하거나 처리중인 값으로 구성됩니다. 매핑은 일대일 변환입니다. 10 개의 객체 목록을 매핑하면 10 개의 변환 된 객체 목록이 표시됩니다.

스트림 API에서 매핑 단계는 하나 이상의 제약 조건을 추가합니다. 컬렉션을 처리하고 있다고 가정하십시오 주문 사물. 목록 또는 다른 주문 된 객체 소스 일 수 있습니다. 해당 목록을 매핑 할 때 얻는 첫 번째 객체는 소스에서 첫 번째 객체의 매핑이어야합니다. 다시 말해, 매핑 단계는 객체의 순서를 존중합니다. 섞지 않습니다.

매핑은 객체의 유형을 변경합니다. 숫자는 변경되지 않습니다.

매핑은 Function 기능 인터페이스. 실제로 함수는 모든 유형의 객체를 가져 와서 다른 유형의 객체를 반환 할 수 있습니다. 또한 특수 함수는 객체를 기본 유형으로 매핑하고 다른 방식으로 둘러 쌀 수 있습니다.

 

객체 필터링

반면에 필터링은 처리중인 객체에 닿지 않습니다. 단지 일부를 선택하고 다른 것을 제거하기로 결정합니다.

필터링하면 개체 수가 변경됩니다. 유형이 변경되지 않습니다.

필터링은 Predicate 기능 인터페이스. 실제로 술어는 모든 유형의 객체 또는 기본 유형을 취할 수 있으며 부울 값을 반환합니다.

 

결과를 생성하기 위해 객체 감소

감소 단계는 보이는 것보다 더 까다 롭습니다. 현재로서는 SQL 집계와 동일한 종류의 정의라는이 정의를 살 것입니다. 생각하다 카운트최소맥스평균. 그런데 이러한 모든 집계는 Stream API에서 지원됩니다.

이 경로에서 당신을 기다리는 것에 대한 힌트를 제공하기 위해 : 축소 단계를 사용하면 목록, 세트, 모든 종류의 맵을 포함하여 데이터로 복잡한 구조를 구축 할 수 있습니다, 또는 자신을 만들 수있는 구조물까지. 이 페이지의 첫 번째 예를 살펴보십시오 collect() 메소드 groupingBy() 공장 방법. 이 객체는 수집가. 감소는 수집기를 사용하여 데이터를 수집하는 것으로 구성 될 수 있습니다. 수집기는이 자습서의 뒷부분에서 자세히 다룹니다.

 

맵 필터 감소 알고리즘 최적화

다른 예를 들어 봅시다. 도시 모음이 있다고 가정하십시오. 각 도시는 City 이름과 인구, 즉 그 안에 사는 사람들의 수라는 두 가지 속성을 가진 수업. 주민 수가 100k 이상인 도시에 거주하는 총 인구를 계산해야합니다.

Stream API를 사용하지 않으면 다음 코드를 작성하게됩니다.

List<City> cities = ...;

int sum = 0;
for (City city: cities) {
    int population = city.getPopulation();
    if (population > 100_000) {
        sum += population;
    }
}

System.out.println("Sum = " + sum);

도시 목록에서 다른지도 필터 감소 처리를 인식 할 수 있습니다.

이제 약간의 실험을 해보자. 스트림 API가 존재하지 않는다고 가정하자 map() 그리고 filter() 방법은 Collection 인터페이스뿐만 아니라 sum() 방법.

이러한 ( 가상 ) 메소드를 사용하면 이전 코드가 다음이 될 수 있습니다.

int sum = cities.map(city -> city.getPopulation())
                .filter(population -> population > 100_000)
                .sum();

가독성과 표현력의 관점에서이 코드는 이해하기 매우 쉽습니다. 이러한지도 및 필터 방법이 왜 추가되지 않았는지 궁금 할 것입니다 Collection 인터페이스?

좀 더 깊이 파고 들어 보자 map() 과 filter() 방법? 우리가 Collections Framework에 있기 때문에 컬렉션을 반환하는 것은 자연스러운 것 같습니다. 이 방법으로이 코드를 작성할 수 있습니다.

Collection<Integer> populations         = cities.map(city -> city.getPopulation());
Collection<Integer> filteredPopulations = populations.filter(population -> population > 100_000);
int sum                                 = filteredPopulations.sum();

호출을 연결하면 가독성이 향상 되더라도이 코드는 여전히 정확해야합니다.

이제이 코드를 분석하겠습니다.

  • 첫 번째 단계는 매핑 단계입니다. 1,000 개의 도시를 처리해야하는 경우이 매핑 단계에서 1,000 개의 정수를 생성하여 컬렉션에 넣습니다.
  • 두 번째 단계는 필터링 단계입니다. 그것은 모든 요소를 통과하고 주어진 기준에 따라 그 중 일부를 제거합니다. 테스트 할 또 다른 1,000 개의 요소와 생성 할 다른 컬렉션 일 것입니다.

이 코드는 콜렉션을 반환하므로 모든 도시를 매핑 한 다음 결과 정수 콜렉션을 필터링합니다. 이것은 매우 다르게 작동합니다 루프 용 당신이 처음에 썼다는 것. 이 중간 정수 모음을 저장하면 특히 처리 할 도시가 많은 경우 많은 오버 헤드가 발생할 수 있습니다. for 루프에는이 오버 헤드가 없습니다. 중간 구조에 저장하지 않고 결과의 정수를 직접 요약합니다.

이 오버 헤드는 나쁘고 더 나빠질 수있는 경우가 있습니다. 컬렉션에 10 만 명 이상의 주민이 거주하는 도시가 있는지 알아야한다고 가정하십시오. 아마도 컬렉션의 첫 번째 도시는 그런 도시 일 것입니다. 이 경우 거의 노력없이 결과를 얻을 수 있습니다. 먼저, 도시에서 모든 인구를 수집 한 다음 필터링하고 결과가 비어 있는지 여부를 확인하는 것이 어리 석습니다.

명백한 공연상의 이유로 map() 반환하는 방법 Collection 에 Collection 인터페이스가 올바른 방법이 아닙니다. 메모리와 CPU 모두에서 높은 오버 헤드로 불필요한 중간 구조를 만들게됩니다.

이것이 이유입니다 map() 과 filter() 메소드가 추가되지 않았습니다 Collection 계면. 대신, 그들은 Stream 계면.

올바른 패턴은 다음과 같습니다.

Stream<City> streamOfCities         = cities.stream();
Stream<Integer> populations         = streamOfCities.map(city -> city.getPopulation());
Stream<Integer> filteredPopulations = populations.filter(population -> population > 100_000);
int sum = filteredPopulations.sum(); // in fact this code does not compile; we'll fix it later

그만큼 Stream 인터페이스는 매핑되거나 필터링 된 객체를 저장하기 위해 중간 구조를 만들지 않습니다. 여기 map() 과 filter() 메소드는 여전히 새로운 스트림을 반환합니다. 따라서이 코드가 작동하고 효율적이기 위해서는 이러한 스트림에 데이터를 저장하지 않아야합니다. 이 코드에서 생성 된 스트림, streamOfCitiespopulations 과 filteredPopulations 모두 빈 개체 여야합니다.

스트림의 매우 중요한 속성으로 이어집니다:

스트림은 데이터를 저장하지 않는 객체입니다.

스트림 API는 스트림 패턴으로 비 스트림 객체를 만들지 않는 한 데이터 계산이 수행되지 않도록 설계되었습니다. 이전 예에서는 스트림에서 처리 된 요소의 합계를 계산하고 있습니다.

이 합계 연산은 계산을 트리거합니다 cities 스트림의 모든 작업을 통해 목록이 하나씩 당겨집니다. 먼저 필터링 단계를 통과하면 매핑 된 다음 필터링하고 요약합니다.

스트림은 루프에 해당하는 것을 쓰는 것과 같은 순서로 데이터를 처리합니다. 이러한 방식으로 메모리 오버 헤드가 없습니다. 또한 컬렉션의 모든 요소를 거치지 않고도 결과를 얻을 수있는 경우가 있습니다.

스트림을 사용하는 것은 작업 파이프 라인을 만드는 것입니다. 어느 시점에서 귀하의 데이터는이 파이프 라인을 통과하여 변환, 필터링 된 다음 결과 생성에 참여합니다.

파이프 라인은 스트림에서 일련의 메소드 호출로 구성됩니다. 각 통화는 다른 스트림을 생성합니다. 그런 다음 어느 시점에서 마지막 호출로 결과가 생성됩니다. 다른 스트림을 반환하는 작업을 중간 작업이라고합니다. 반면, void를 포함하여 다른 것을 반환하는 작업을 터미널 작업이라고합니다.

 

중간 작업으로 파이프 라인 만들기

중간 작업은 다른 스트림을 반환하는 작업입니다. 이러한 작업을 호출하면 데이터를 처리하지 않고 기존 작업 파이프 라인에서 하나 이상의 작업이 추가됩니다. 스트림을 반환하는 방법으로 모델링됩니다.

 

터미널 작업으로 결과 계산

터미널 작업은 스트림을 반환하지 않는 작업입니다. 이러한 작업을 호출하면 스트림 소스의 요소가 소비됩니다. 이러한 요소는 한 번에 하나의 요소 인 중간 작업의 파이프 라인에 의해 처리됩니다.

터미널 작업은 void를 포함하여 스트림 이외의 것을 반환하는 방법으로 모델링됩니다.

스트림에서 둘 이상의 중간 또는 터미널 메소드를 호출 할 수 없습니다. 그렇게하면 IllegalStateException 다음과 같은 메시지와 함께 : "스트림이 이미 작동 중이거나 닫혔습니다".

 

특수한 숫자의 스트림으로 권투를 피하십시오

Stream API는 네 가지 인터페이스를 제공합니다.

첫 번째는 Stream, 모든 종류의 객체에 대한 작업 파이프 라인을 정의하는 데 사용할 수 있습니다.

그런 다음 숫자 스트림을 처리하기위한 세 가지 특수 인터페이스가 있습니다: IntStreamLongStream 과 DoubleStream.이 세 가지 스트림은 권투 및 언 박싱을 피하기 위해 래퍼 유형 대신 숫자에 기본 유형을 사용합니다. 그들은 정의 된 방법과 거의 동일한 방법을 가지고 있습니다 Stream, 몇 가지 예외가 있습니다. 숫자를 처리하기 때문에 존재하지 않는 터미널 작업이 있습니다 Stream:

  • sum(): 합계를 계산
  • min()max(): 스트림의 최소 또는 최대 수를 계산
  • average(): 숫자의 평균값을 계산
  • summaryStatistics(): 이 호출은 여러 통계를 전달하는 특수 객체를 생성합니다. 모두 통계를 한 번에 데이터로 계산합니다. 이러한 통계는 해당 스트림에서 처리되는 요소 수, 최소, 최대, 합계 및 평균입니다.

 

모범 사례 준수

보시다시피이 방법이 중간 인 경우에도 스트림에서 하나의 메소드 만 호출 할 수 있습니다. 따라서 필드 나 로컬 변수에 스트림을 저장하는 것은 쓸모없고 때로는 위험합니다. 스트림을 인수로 사용하는 방법을 작성하는 것도 위험 할 수 있습니다. 수신 한 스트림이 아직 작동하지 않았 음을 확신 할 수 없기 때문입니다. 그 자리에서 스트림을 만들고 소비해야합니다.

스트림은 소스에 연결된 객체입니다. 이 소스에서 처리하는 요소를 가져옵니다. 이 소스는 스트림 자체에 의해 수정되어서는 안됩니다. 그렇게하면 지정되지 않은 결과가 발생합니다. 경우에 따라이 소스는 불변이거나 읽기 전용이므로 그렇게 할 수는 없지만 가능한 경우가 있습니다.

사용 가능한 방법이 많이 있습니다 Stream 인터페이스,이 자습서에서 대부분을 볼 수 있습니다. 스트림 자체 외부의 일부 변수 또는 필드를 수정하는 작업을 작성하는 것은 항상 피할 수있는 나쁜 생각입니다. 스트림에 부작용.