스트림에서 중간 작업 추가
스트림을 다른 스트림에 매핑
스트림 매핑은 함수를 사용하여 요소를 변환하는 것으로 구성됩니다. 이 변환은 해당 스트림에서 처리되는 요소의 유형을 변경할 수 있지만 유형을 변경하지 않고도 요소를 변환 할 수도 있습니다.
스트림을 다른 스트림에 매핑 할 수 있습니다 map()
이것을 취하는 방법 Function
논쟁으로. 스트림 매핑은 해당 스트림에서 처리 된 모든 요소가 해당 기능을 사용하여 변환됨을 의미합니다.
코드 패턴은 다음과 같습니다:
이 코드를 복사하여 IDE에 붙여 넣어 실행할 수 있습니다. 당신은 아무것도 보지 못하고 왜 그런지 궁금 할 것입니다.
그 대답은 실제로 간단합니다. 해당 스트림에 정의 된 터미널 작업이 없습니다. 당신의 반사는 그것을 알아 차리고이 코드가 아무것도하지 않는다는 것을 깨달아야합니다. 데이터를 처리하지 않습니다. "이 코드는 무엇을하고 있습니까?"라는 질문에 대답하기 위해 "아무것도"라는 유효한 답변이 하나뿐입니다".
처리 된 요소를 목록에 넣는 매우 유용한 터미널 작업을 추가하겠습니다: collect(Collectors.toList())
. 이 코드가 실제로 무엇을하는지 확실하지 않으면 걱정하지 마십시오. 이 자습서 뒷부분에서 다룰 것입니다. 코드는 다음과 같습니다.
이 코드를 실행하면 다음이 인쇄됩니다:
이 패턴이 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()
방법은 매우 편리하며 이러한 특수한 원시 유형 스트림에서만 사용할 수 있습니다.
결과는 다음과 같습니다:
세 가지 방법이 있습니다 Stream
기본 유형의 스트림으로: mapToInt()
, mapToLong()
과 mapToDouble()
.
스트림 필터링
필터링은 술어를 사용하여 스트림으로 처리 된 일부 요소를 버리는 것입니다. 이 방법은 객체 스트림과 기본 유형의 스트림에서 사용할 수 있습니다.
길이 3의 문자 문자열을 계산해야한다고 가정합니다. 이 코드를 작성하여 다음을 수행 할 수 있습니다:
이 코드를 실행하면 다음이 생성됩니다:
Stream API의 다른 터미널 작업을 방금 사용했습니다, count()
, 처리 된 요소의 수를 계산합니다. 이 방법은 long
, 따라서 많은 요소를 계산할 수 있습니다. 당신이 넣을 수있는 것보다 더 ArrayList
.
1 : p 관계를 처리하기 위해 스트림 플랫 매핑
우리가 보자 flatMap
예제에서의 조작. 두 개의 엔티티가 있다고 가정하십시오: State
과 City
. ᅡ state
인스턴스는 여러 개를 보유 city
목록에 저장된 인스턴스.
다음은 City
수업.
다음은 State
클래스와 관계 City
수업.
코드가 상태 목록을 처리하고 있다고 가정하면 어느 시점에서 모든 도시의 인구를 계산해야합니다.
다음 코드를 작성할 수 있습니다:
이 코드의 내부 루프는 다음 스트림으로 쓸 수있는 맵 감소 형식입니다:
상태의 루프와이 스트림 사이의 연결은 맵 / 감소 패턴에 잘 맞지 않습니다, 스트림을 루프에 넣는 것은 코드의 좋은 패턴이 아닙니다.
이것이 바로 플랫 맵 운영자의 역할입니다. 이 연산자는 객체간에 일대 다 관계를 열고 이러한 관계에서 스트림을 만듭니다. 그만큼 flatMap()
메소드는 특수 함수를 인수로 가져 와서 Stream
목적. 주어진 클래스와 다른 클래스 사이의 관계는이 함수에 의해 정의됩니다.
이 예제의 경우이 기능은 다음과 같이 간단합니다 List<City>
에 State
수업. 다음과 같은 방식으로 작성할 수 있습니다.
이 목록은 필수가 아닙니다. 당신이 있다고 가정 Continent
보유하는 수업 Map<String, Country>
, 여기서 키는 ( 국가의 국가 코드 ) CAN, 멕시코의 MEX, 프랑스의 FRA 등입니다. 그 Continent
클래스에는 방법이 있습니다 getCountries()
이지도를 반환합니다.
이 경우이 기능을 이런 식으로 작성할 수 있습니다.
그만큼 flatMap()
메소드는 두 줄로 스트림을 처리했습니다.
- 첫 번째 단계는이 함수를 사용하여 스트림의 모든 요소를 매핑하는 것입니다. 에서
Stream<State>
그것은Stream<Stream<City>>
, 모든 주가 도시 스트림에 매핑되기 때문입니다. - 두 번째 단계는 생성되는 스트림 스트림을 평탄화하는 것입니다. 각 주 (에 대해 ) 하나의 스트림 스트림을 갖는 대신, 모든주의 모든 도시가있는 단일 스트림으로 끝납니다.
따라서 플랫 맵 연산자 덕분에 중첩 된 루프 패턴으로 작성된 코드는 다음과 같이 될 수 있습니다.
Flatmap 및 MapMulti를 사용하여 요소 변환 검증
그만큼 flatMap
스트림 요소의 변환을 확인하는 데 작업을 사용할 수 있습니다.
정수를 나타내는 문자열 문자열 스트림이 있다고 가정하십시오. 다음을 사용하여 정수로 변환해야합니다 Integer.parseInt()
. 불행히도 이러한 문자열 중 일부가 손상되었습니다. 일부는 비어 있거나 널이거나 끝에 빈 문자가있을 수 있습니다. 이 모든 것이 파싱으로 실패합니다 NumberFormatException
. 물론이 스트림을 필터링하여 술어가있는 버그가있는 문자열을 제거 할 수 있지만 가장 안전한 방법은 try-catch 패턴을 사용하는 것입니다.
필터를 사용하는 것이 올바른 방법은 아닙니다. 쓸 술어는 다음과 같습니다.
이 첫 번째 결함은 실제로 변환이 작동하는지 여부를 확인하기 위해 변환을 수행해야한다는 것입니다. 그런 다음 매핑 기능에서 다시 수행해야합니다. 다음에 실행됩니다.하지 마십시오! 두 번째 결함은 캐치 블록에서 돌아 오는 것이 결코 좋은 생각이 아니라는 것입니다.
이 문자열에 적절한 정수가있을 때 정수를 반환하고 손상된 문자열 인 경우 정수를 반환해야합니다. 이것은 flatmapper의 직업입니다. 정수를 구문 분석 할 수 있으면 결과와 함께 스트림을 반환 할 수 있습니다. 다른 경우에는 빈 스트림을 반환 할 수 있습니다.
그런 다음 다음 기능을 작성할 수 있습니다.
이 코드를 실행하면 다음과 같은 결과가 발생합니다. 결함이있는 모든 문자열이 자동으로 제거되었습니다.
이 플랫 맵 코드 사용은 잘 작동하지만 오버 헤드가 있습니다. 처리해야하는 스트림의 각 요소에 대해 하나의 스트림이 생성됩니다. Java SE 16부터 스트림 API에 메소드가 추가되었습니다. 이 경우 정확히 추가되었습니다 : 0 또는 1 개의 객체로 많은 스트림을 만들 때. 이 방법은 mapMulti()
그리고 BiConsumer
논쟁으로.
이 BiConsumer
두 가지 인수를 소비합니다:
- 매핑해야하는 스트림 요소
- ᅡ
Consumer
이BiConsumer
매핑 결과로 전화해야 함
요소를 사용하여 소비자에게 전화하면 해당 요소가 결과 스트림에 추가됩니다. 매핑을 수행 할 수없는 경우 바이 콘 서머는이 소비자를 호출하지 않으며 요소가 추가되지 않습니다.
이것으로 패턴을 다시 쓰겠습니다 mapMulti()
방법.
이 코드를 실행하면 이전과 동일한 결과가 생성됩니다. 모든 잘못된 문자열이 자동으로 제거되었지만 이번에는 다른 스트림이 생성되지 않았습니다.
이 방법을 사용하려면 컴파일러에 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까지 정수를 선택합니다. 이는 필요하지 않습니다. 올바른 코드는 다음과 같습니다.
이 코드는 다음을 인쇄합니다.
이해하는 것이 중요합니다 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 설명서에서 플랫 맵 사용을 기반으로 다른 패턴을 사용하도록 권장합니다.
이것이 예제에서 어떻게 작동하는지 봅시다.
이 코드를 실행하면 다음과 같은 결과가 발생합니다:
사용하는 것이 더 좋은 이유 flatMap()
방법은 concat()
연결 중에 중간 스트림을 만듭니다. 사용할 때 Stream.concat()
, 두 스트림을 연결하기 위해 새 스트림이 작성됩니다. 세 개의 스트림을 연결해야하는 경우 첫 번째 연결을 처리하기위한 첫 번째 스트림을 만들고 두 번째 연결을 위해 두 번째 스트림을 작성하게됩니다. 따라서 각 연결에는 매우 빨리 버려지는 스트림이 필요합니다.
플랫 맵 패턴을 사용하면 모든 스트림을 잡고 플랫 맵을 수행하기 위해 단일 스트림을 만들 수 있습니다. 오버 헤드가 훨씬 낮습니다.
이 두 패턴이 추가 된 이유가 궁금 할 것입니다. 마치 concat()
실제로 유용하지 않습니다. 실제로 콘캣에 의해 생성 된 스트림과 플랫 맵 패턴 사이에는 미묘한 차이가 있습니다.
연결중인 두 스트림의 소스 크기를 알고 있으면 결과 스트림의 크기도 알려져 있습니다. 실제로, 그것은 단순히 두 개의 연결된 스트림의 합입니다.
스트림에서 플랫 맵을 사용하면 결과 스트림에서 처리 할 알 수없는 요소 수가 생성 될 수 있습니다. 스트림 API는 결과 스트림에서 처리 될 요소 수를 추적하지 못합니다.
다시 말해 : concat는 SIZED
플랫 맵은 그렇지 않지만 스트림. 이 SIZED
속성은 스트림이 가질 수있는 속성이며이 자습서의 뒷부분에서 다룹니다.
스트림 디버깅
런타임에 스트림에 의해 처리 된 요소를 검사하는 것이 때때로 편리 할 수 있습니다. Stream API에는 다음과 같은 방법이 있습니다 peek()
방법. 이 방법은 데이터 처리 파이프 라인을 디버깅하는 데 사용됩니다. 프로덕션 코드에서이 방법을 사용해서는 안됩니다.
응용 프로그램에서 부작용을 수행하기 위해이 방법을 사용하지 마십시오.
이 방법은 소비자를 스트림의 각 요소에서 API에 의해 호출되는 인수로 사용합니다. 이 방법이 실제로 적용되는 것을 보자.
이 코드를 실행하면 콘솔에 다음이 표시됩니다.
이 출력을 분석해 봅시다.
- 처리 할 첫 번째 요소는 하나. 필터링 된 것을 볼 수 있습니다.
- 두 번째는 두. 이 요소는 필터를 통과 한 다음 대문자에 매핑됩니다. 그런 다음 결과 목록에 추가됩니다.
- 세 번째는 세, 또한 필터를 통과하고 결과 목록에 추가되기 전에 대문자에 매핑됩니다.
- 네 번째이자 마지막은 사 필터링 단계에서 거부됩니다.
이 자습서 앞부분에서 보았던 한 가지 점이 있습니다. 스트림은 처리해야하는 모든 요소를 하나씩 처리합니다, 스트림의 시작부터 끝까지. 이것은 전에 언급되었으며 이제는 실제로 볼 수 있습니다.
당신은 이것을 볼 수 있습니다 peek(System.out::println)
패턴은 코드를 디버그하지 않고도 스트림에서 처리 된 요소를 하나씩 따르는 데 매우 유용합니다. 중단 점을 어디에 두어야하는지주의해야하기 때문에 스트림 디버깅이 어렵습니다. 대부분의 경우 스트림 처리에 중단 점을 적용하면 Stream
계면. 이것은 당신이 필요로하는 것이 아닙니다. 대부분의 경우 이러한 중단 점을 람다 표현식 코드에 넣어야합니다.