자바 스트림 java stream


 

스트림에서 중간 작업 추가

 

스트림을 다른 스트림에 매핑

스트림 매핑은 함수를 사용하여 요소를 변환하는 것으로 구성됩니다. 이 변환은 해당 스트림에서 처리되는 요소의 유형을 변경할 수 있지만 유형을 변경하지 않고도 요소를 변환 할 수도 있습니다.

스트림을 다른 스트림에 매핑 할 수 있습니다 map() 이것을 취하는 방법 Function 논쟁으로. 스트림 매핑은 해당 스트림에서 처리 된 모든 요소가 해당 기능을 사용하여 변환됨을 의미합니다.

코드 패턴은 다음과 같습니다:

List<String> strings = List.of("one", "two", "three", "four");
Function<String, Integer> toLength = String::length;
Stream<Integer> ints = strings.stream()
                              .map(toLength);

이 코드를 복사하여 IDE에 붙여 넣어 실행할 수 있습니다. 당신은 아무것도 보지 못하고 왜 그런지 궁금 할 것입니다.

그 대답은 실제로 간단합니다. 해당 스트림에 정의 된 터미널 작업이 없습니다. 당신의 반사는 그것을 알아 차리고이 코드가 아무것도하지 않는다는 것을 깨달아야합니다. 데이터를 처리하지 않습니다. "이 코드는 무엇을하고 있습니까?"라는 질문에 대답하기 위해 "아무것도"라는 유효한 답변이 하나뿐입니다".

처리 된 요소를 목록에 넣는 매우 유용한 터미널 작업을 추가하겠습니다: collect(Collectors.toList()). 이 코드가 실제로 무엇을하는지 확실하지 않으면 걱정하지 마십시오. 이 자습서 뒷부분에서 다룰 것입니다. 코드는 다음과 같습니다.

List<String> strings = List.of("one", "two", "three", "four");
List<Integer> lengths = strings.stream()
                               .map(String::length)
                               .collect(Collectors.toList());
System.out.println("lengths = " + lengths);

이 코드를 실행하면 다음이 인쇄됩니다:

lengths = [3, 3, 5, 4]

이 패턴이 Stream<Integer>, 에 의해 반환 map(String::length). 당신은 또한 그것을 전문화 할 수 있습니다 IntStream 전화로 mapToInt() 일반 대신 map() 전화. 이 mapToInt() 방법은 ToIntFuction<T> 논쟁으로. 변경 .map(String::length) 에 .mapToInt(String::length) 이전 예제에서는 컴파일러 오류가 발생하지 않습니다. 방법 참조 String::length 두 유형 모두 될 수 있습니다: Function<String, Integer> 과 ToIntFunction<String>.

없습니다 collect() 방법을 Collector 전문화 된 스트림에 대한 논쟁으로. 당신이 사용하는 경우 mapToInt(), 적어도이 패턴으로는 더 이상 결과를 목록으로 수집 할 수 없습니다. 대신 해당 스트림에 대한 통계를 얻을 수 있습니다. 이 summaryStatistics() 방법은 매우 편리하며 이러한 특수한 원시 유형 스트림에서만 사용할 수 있습니다.

List<String> strings = List.of("one", "two", "three", "four");
IntSummaryStatistics stats = strings.stream()
                                    .mapToInt(String::length)
                                    .summaryStatistics();
System.out.println("stats = " + stats);

결과는 다음과 같습니다:

stats = IntSummaryStatistics{count=4, sum=15, min=3, average=3,750000, max=5}

세 가지 방법이 있습니다 Stream 기본 유형의 스트림으로: mapToInt()mapToLong() 과 mapToDouble().

 

스트림 필터링

필터링은 술어를 사용하여 스트림으로 처리 된 일부 요소를 버리는 것입니다. 이 방법은 객체 스트림과 기본 유형의 스트림에서 사용할 수 있습니다.

길이 3의 문자 문자열을 계산해야한다고 가정합니다. 이 코드를 작성하여 다음을 수행 할 수 있습니다:

List<String> strings = List.of("one", "two", "three", "four");
long count = strings.stream()
                    .map(String::length)
                    .filter(length -> length == 3)
                    .count();
System.out.println("count = " + count);

이 코드를 실행하면 다음이 생성됩니다:

count = 2

Stream API의 다른 터미널 작업을 방금 사용했습니다, count(), 처리 된 요소의 수를 계산합니다. 이 방법은 long, 따라서 많은 요소를 계산할 수 있습니다. 당신이 넣을 수있는 것보다 더 ArrayList.

 

1 : p 관계를 처리하기 위해 스트림 플랫 매핑

우리가 보자 flatMap 예제에서의 조작. 두 개의 엔티티가 있다고 가정하십시오: State 과 City. ᅡ state 인스턴스는 여러 개를 보유 city 목록에 저장된 인스턴스.

다음은 City 수업.

public class City {
    
    private String name;
    private int population;

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

다음은 State 클래스와 관계 City 수업.

public class State {
    
    private String name;
    private List<City> cities;

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

코드가 상태 목록을 처리하고 있다고 가정하면 어느 시점에서 모든 도시의 인구를 계산해야합니다.

다음 코드를 작성할 수 있습니다:

List<State> states = ...;

int totalPopulation = 0;
for (State state: states) {
    for (City city: state.getCities()) {
        totalPopulation += city.getPopulation();
    }
}

System.out.println("Total population = " + totalPopulation);

이 코드의 내부 루프는 다음 스트림으로 쓸 수있는 맵 감소 형식입니다:

totalPopulation += state.getCities().stream().mapToInt(City::getPopulation).sum();

상태의 루프와이 스트림 사이의 연결은 맵 / 감소 패턴에 잘 맞지 않습니다, 스트림을 루프에 넣는 것은 코드의 좋은 패턴이 아닙니다.

이것이 바로 플랫 맵 운영자의 역할입니다. 이 연산자는 객체간에 일대 다 관계를 열고 이러한 관계에서 스트림을 만듭니다. 그만큼 flatMap() 메소드는 특수 함수를 인수로 가져 와서 Stream 목적. 주어진 클래스와 다른 클래스 사이의 관계는이 함수에 의해 정의됩니다.

이 예제의 경우이 기능은 다음과 같이 간단합니다 List<City> 에 State 수업. 다음과 같은 방식으로 작성할 수 있습니다.

Function<State, Stream<City>> stateToCity = state -> state.getCities().stream();

이 목록은 필수가 아닙니다. 당신이 있다고 가정 Continent 보유하는 수업 Map<String, Country>, 여기서 키는 ( 국가의 국가 코드 ) CAN, 멕시코의 MEX, 프랑스의 FRA 등입니다. 그 Continent 클래스에는 방법이 있습니다 getCountries() 이지도를 반환합니다.

이 경우이 기능을 이런 식으로 작성할 수 있습니다.

Function<Continent, Stream<Country>> continentToCountry = 
    continent -> continent.getCountries().values().stream();

그만큼 flatMap() 메소드는 두 줄로 스트림을 처리했습니다.

  • 첫 번째 단계는이 함수를 사용하여 스트림의 모든 요소를 매핑하는 것입니다. 에서 Stream<State> 그것은 Stream<Stream<City>>, 모든 주가 도시 스트림에 매핑되기 때문입니다.
  • 두 번째 단계는 생성되는 스트림 스트림을 평탄화하는 것입니다. 각 주 (에 대해 ) 하나의 스트림 스트림을 갖는 대신, 모든주의 모든 도시가있는 단일 스트림으로 끝납니다.

따라서 플랫 맵 연산자 덕분에 중첩 된 루프 패턴으로 작성된 코드는 다음과 같이 될 수 있습니다.

List<State> states = ...;

int totalPopulation = 
        states.stream()
              .flatMap(state -> state.getCities().stream())
              .mapToInt(City::getPopulation)
              .sum();

System.out.println("Total population = " + totalPopulation);

 

Flatmap 및 MapMulti를 사용하여 요소 변환 검증

그만큼 flatMap 스트림 요소의 변환을 확인하는 데 작업을 사용할 수 있습니다.

정수를 나타내는 문자열 문자열 스트림이 있다고 가정하십시오. 다음을 사용하여 정수로 변환해야합니다 Integer.parseInt(). 불행히도 이러한 문자열 중 일부가 손상되었습니다. 일부는 비어 있거나 널이거나 끝에 빈 문자가있을 수 있습니다. 이 모든 것이 파싱으로 실패합니다 NumberFormatException. 물론이 스트림을 필터링하여 술어가있는 버그가있는 문자열을 제거 할 수 있지만 가장 안전한 방법은 try-catch 패턴을 사용하는 것입니다.

필터를 사용하는 것이 올바른 방법은 아닙니다. 쓸 술어는 다음과 같습니다.

Predicate<String> isANumber = s -> {
    try {
        int i = Integer.parseInt(s);
        return true;
    } catch (NumberFormatException e) {
        return false;
    }
};

이 첫 번째 결함은 실제로 변환이 작동하는지 여부를 확인하기 위해 변환을 수행해야한다는 것입니다. 그런 다음 매핑 기능에서 다시 수행해야합니다. 다음에 실행됩니다.하지 마십시오! 두 번째 결함은 캐치 블록에서 돌아 오는 것이 결코 좋은 생각이 아니라는 것입니다.

이 문자열에 적절한 정수가있을 때 정수를 반환하고 손상된 문자열 인 경우 정수를 반환해야합니다. 이것은 flatmapper의 직업입니다. 정수를 구문 분석 할 수 있으면 결과와 함께 스트림을 반환 할 수 있습니다. 다른 경우에는 빈 스트림을 반환 할 수 있습니다.

그런 다음 다음 기능을 작성할 수 있습니다.

Function<String, Stream<Integer>> flatParser = s -> {
    try {
        return Stream.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
    }
    return Stream.empty();
};

List<String> strings = List.of("1", " ", "2", "3 ", "", "3");
List<Integer> ints = 
    strings.stream()
           .flatMap(flatParser)
           .collect(Collectors.toList());
System.out.println("ints = " + ints);

이 코드를 실행하면 다음과 같은 결과가 발생합니다. 결함이있는 모든 문자열이 자동으로 제거되었습니다.

ints = [1, 2, 3]

이 플랫 맵 코드 사용은 잘 작동하지만 오버 헤드가 있습니다. 처리해야하는 스트림의 각 요소에 대해 하나의 스트림이 생성됩니다. Java SE 16부터 스트림 API에 메소드가 추가되었습니다. 이 경우 정확히 추가되었습니다 : 0 또는 1 개의 객체로 많은 스트림을 만들 때. 이 방법은 mapMulti() 그리고 BiConsumer 논쟁으로.

이 BiConsumer 두 가지 인수를 소비합니다:

  • 매핑해야하는 스트림 요소
  • ᅡ Consumer 이 BiConsumer 매핑 결과로 전화해야 함

요소를 사용하여 소비자에게 전화하면 해당 요소가 결과 스트림에 추가됩니다. 매핑을 수행 할 수없는 경우 바이 콘 서머는이 소비자를 호출하지 않으며 요소가 추가되지 않습니다.

이것으로 패턴을 다시 쓰겠습니다 mapMulti() 방법.

List<Integer> ints =
        strings.stream()
               .<Integer>mapMulti((string, consumer) -> {
                    try {
                        consumer.accept(Integer.parseInt(string));
                    } catch (NumberFormatException ignored) {
                    }
               })
               .collect(Collectors.toList());
System.out.println("ints = " + ints);

이 코드를 실행하면 이전과 동일한 결과가 생성됩니다. 모든 잘못된 문자열이 자동으로 제거되었지만 이번에는 다른 스트림이 생성되지 않았습니다.

ints = [1, 2, 3]

이 방법을 사용하려면 컴파일러에 Consumer 결과 스트림에 요소를 추가하는 데 사용됩니다. 호출하기 전에이 유형을 입력하는이 특수 구문으로 수행됩니다 mapMulti(). Java 코드에서 자주 볼 수있는 구문이 아닙니다. 정적 및 비 정적 컨텍스트에서 사용할 수 있습니다.

 

중복 제거 및 스트림 정렬

스트림 API에는 두 가지 방법이 있습니다, distinct() 과 sorted(), 복제본을 감지하고 제거하고 스트림 요소를 정렬합니다. 그만큼 distinct() 방법은 hashCode() 과 equals() 복제본을 발견하는 방법. 그만큼 sorted() 메소드에는 비교기를 사용하는 과부하가 있으며 스트림의 요소를 비교하고 정렬하는 데 사용됩니다. 비교기를 제공하지 않으면 Stream API는 스트림의 요소가 비교 가능한 것으로 가정합니다. 그렇지 않다면 ClassCastException 제기됩니다.

이 자습서의 이전 부분에서 스트림은 데이터를 저장하지 않는 빈 개체 여야한다는 것을 기억할 수 있습니다. 이 규칙에는 몇 가지 예외가 있으며이 두 가지 방법이 있습니다.

실제로 복제본을 발견하기 위해 distinct() 메소드는 스트림의 요소를 저장해야합니다. 요소를 처리 할 때 먼저 해당 요소가 이미 보이는지 확인합니다.

같은 것입니다 sorted() 방법. 이 방법은 모든 요소를 저장 한 다음 처리 파이프 라인의 다음 단계로 보내기 전에 내부 버퍼에 정렬해야합니다.

그만큼 distinct() 언 바운드 ( 무한 ) 스트림에서 메소드를 사용할 수 있습니다 sorted() 방법은 할 수 없습니다.

 

스트림의 요소 제한 및 건너 뛰기

스트림 API는 스트림 요소를 선택하는 두 가지 방법을 제공합니다. 인덱스를 기반으로하거나 술어를 사용합니다.

첫 번째 방법은 skip() 과 limit() 방법, 둘 다 long 논쟁으로. 이 방법을 사용할 때 피해야 할 작은 함정이 있습니다. 스트림에서 중간 메소드가 호출 될 때마다 새 스트림이 생성된다는 점을 명심해야합니다. 그래서 당신이 전화하면 limit() 후 skip(), 새 스트림에서 시작하는 요소를 세는 것을 잊지 마십시오.

1부터 시작하여 모든 정수의 스트림이 있다고 가정하십시오. 정수 스트림에서 3과 8 사이의 정수를 선택해야합니다. 당신은 전화를 유혹 할 수 있습니다 skip(2).limit(8), 첫 번째 스트림에서 계산 된 바운드를 전달합니다. 불행히도 이것은 스트림이 작동하는 방식이 아닙니다. 두 번째 전화 limit(8) 3에서 시작하는 스트림에서 작동하므로 11까지 정수를 선택합니다. 이는 필요하지 않습니다. 올바른 코드는 다음과 같습니다.

List<Integer> ints = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);

List<Integer> result = 
    ints.stream()
        .skip(2)
        .limit(5)
        .collect(Collectors.toList());

System.out.println("result = " + result);

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

result = [3, 4, 5, 6, 7]

이해하는 것이 중요합니다 skip(2) 요소를 처리하는 스트림에서 호출되었습니다 1, 2, 3, ..., 요소를 처리하는 다른 스트림을 생성합니다 3, 4, 5, 6, ....

그래서 limit(3) 해당 스트림의 처음 5 개 요소를 선택하여 3, 4, 5, 6, 7.

Java SE 9는이 분야에서 두 가지 방법을 더 도입했습니다. 스트림의 인덱스를 기반으로 요소를 건너 뛰고 제한하는 대신 술어의 값을 기준으로합니다.

  • dropWhile(predicate)이러한 요소에 술어를 적용 할 때까지 스트림에서 처리 된 요소를 삭제합니다. 이 시점에서 해당 스트림에 의해 처리 된 모든 요소는 다음 스트림으로 전송됩니다.
  • takeWhile(predicate)반대로 :이 요소에 대한 술어의 적용이 거짓이 될 때까지 요소를 다음 스트림으로 전송합니다.

이러한 방법은 문처럼 작동합니다. 한번 dropWhile() 처리 된 요소가 흐르도록 문을 열면 닫히지 않습니다. 한번 takeWhile() 문을 닫으면 다시 열 수 없으며 다음 작업으로 더 이상 요소가 전송되지 않습니다.

 

연결 스트림

스트림 API는 여러 스트림을 하나로 연결하기위한 여러 패턴을 제공합니다. 가장 확실한 방법은 다음에 정의 된 공장 방법을 사용하는 것입니다 Stream 인터페이스: concat().

이 방법은 두 개의 스트림을 가져와 첫 번째 스트림에 의해 생성 된 요소와 두 번째 스트림의 요소로 스트림을 생성합니다.

이 방법이 여러 스트림을 연결하기 위해 vararg를 사용하지 않는 이유가 궁금 할 것입니다.

두 개의 스트림을 결합하는 한이 방법을 사용하는 것이 좋습니다. 둘 이상의 것이 있으면 JavaDoc API 설명서에서 플랫 맵 사용을 기반으로 다른 패턴을 사용하도록 권장합니다.

이것이 예제에서 어떻게 작동하는지 봅시다.

List<Integer> list0 = List.of(1, 2, 3);
List<Integer> list1 = List.of(4, 5, 6);
List<Integer> list2 = List.of(7, 8, 9);

// 1st pattern: concat
List<Integer> concat = 
    Stream.concat(list0.stream(), list1.stream())
          .collect(Collectors.toList());

// 2nd pattern: flatMap
List<Integer> flatMap =
    Stream.of(list0.stream(), list1.stream(), list2.stream())
          .flatMap(Function.identity())
          .collect(Collectors.toList());

System.out.println("concat  = " + concat);
System.out.println("flatMap = " + flatMap);

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

concat  = [1, 2, 3, 4, 5, 6]
flatMap = [1, 2, 3, 4, 5, 6, 7, 8, 9]

사용하는 것이 더 좋은 이유 flatMap() 방법은 concat() 연결 중에 중간 스트림을 만듭니다. 사용할 때 Stream.concat(), 두 스트림을 연결하기 위해 새 스트림이 작성됩니다. 세 개의 스트림을 연결해야하는 경우 첫 번째 연결을 처리하기위한 첫 번째 스트림을 만들고 두 번째 연결을 위해 두 번째 스트림을 작성하게됩니다. 따라서 각 연결에는 매우 빨리 버려지는 스트림이 필요합니다.

플랫 맵 패턴을 사용하면 모든 스트림을 잡고 플랫 맵을 수행하기 위해 단일 스트림을 만들 수 있습니다. 오버 헤드가 훨씬 낮습니다.

이 두 패턴이 추가 된 이유가 궁금 할 것입니다. 마치 concat() 실제로 유용하지 않습니다. 실제로 콘캣에 의해 생성 된 스트림과 플랫 맵 패턴 사이에는 미묘한 차이가 있습니다.

연결중인 두 스트림의 소스 크기를 알고 있으면 결과 스트림의 크기도 알려져 있습니다. 실제로, 그것은 단순히 두 개의 연결된 스트림의 합입니다.

스트림에서 플랫 맵을 사용하면 결과 스트림에서 처리 할 알 수없는 요소 수가 생성 될 수 있습니다. 스트림 API는 결과 스트림에서 처리 될 요소 수를 추적하지 못합니다.

다시 말해 : concat는 SIZED 플랫 맵은 그렇지 않지만 스트림. 이 SIZED 속성은 스트림이 가질 수있는 속성이며이 자습서의 뒷부분에서 다룹니다.

 

스트림 디버깅

런타임에 스트림에 의해 처리 된 요소를 검사하는 것이 때때로 편리 할 수 있습니다. Stream API에는 다음과 같은 방법이 있습니다 peek() 방법. 이 방법은 데이터 처리 파이프 라인을 디버깅하는 데 사용됩니다. 프로덕션 코드에서이 방법을 사용해서는 안됩니다.

응용 프로그램에서 부작용을 수행하기 위해이 방법을 사용하지 마십시오.

이 방법은 소비자를 스트림의 각 요소에서 API에 의해 호출되는 인수로 사용합니다. 이 방법이 실제로 적용되는 것을 보자.

List<String> strings = List.of("one", "two", "three", "four");
List<String> result =
        strings.stream()
                .peek(s -> System.out.println("Starting with = " + s))
                .filter(s -> s.startsWith("t"))
                .peek(s -> System.out.println("Filtered = " + s))
                .map(String::toUpperCase)
                .peek(s -> System.out.println("Mapped = " + s))
                .collect(Collectors.toList());
System.out.println("result = " + result);

이 코드를 실행하면 콘솔에 다음이 표시됩니다.

Starting with = one
Starting with = two
Filtered = two
Mapped = TWO
Starting with = three
Filtered = three
Mapped = THREE
Starting with = four
result = [TWO, THREE]

이 출력을 분석해 봅시다.

  1. 처리 할 첫 번째 요소는 하나. 필터링 된 것을 볼 수 있습니다.
  2. 두 번째는 . 이 요소는 필터를 통과 한 다음 대문자에 매핑됩니다. 그런 다음 결과 목록에 추가됩니다.
  3. 세 번째는 , 또한 필터를 통과하고 결과 목록에 추가되기 전에 대문자에 매핑됩니다.
  4. 네 번째이자 마지막은  필터링 단계에서 거부됩니다.

이 자습서 앞부분에서 보았던 한 가지 점이 있습니다. 스트림은 처리해야하는 모든 요소를 하나씩 처리합니다, 스트림의 시작부터 끝까지. 이것은 전에 언급되었으며 이제는 실제로 볼 수 있습니다.

당신은 이것을 볼 수 있습니다 peek(System.out::println) 패턴은 코드를 디버그하지 않고도 스트림에서 처리 된 요소를 하나씩 따르는 데 매우 유용합니다. 중단 점을 어디에 두어야하는지주의해야하기 때문에 스트림 디버깅이 어렵습니다. 대부분의 경우 스트림 처리에 중단 점을 적용하면 Stream 계면. 이것은 당신이 필요로하는 것이 아닙니다. 대부분의 경우 이러한 중단 점을 람다 표현식 코드에 넣어야합니다.