programing

Java Streams가 일회성인 이유는 무엇입니까?

copyandpastes 2022. 7. 13. 21:45
반응형

Java Streams가 일회성인 이유는 무엇입니까?

C#과 달리IEnumerable실행 파이프라인을 원하는 횟수만큼 실행할 수 있는 Java에서는 스트림을 한 번만 '반복'할 수 있습니다.

터미널 조작에 대한 콜은 스트림을 닫고 사용할 수 없게 됩니다.이 '기능'은 많은 전력을 빼앗는다.

나는 이것의 이유가 전문적이지 않다고 생각한다.이 이상한 제약의 배후에 있는 설계상의 고려사항은 무엇이었습니까?

편집: 이 내용을 설명하기 위해 C#에서 Quick-Sort의 다음 구현을 검토합니다.

IEnumerable<int> QuickSort(IEnumerable<int> ints)
{
  if (!ints.Any()) {
    return Enumerable.Empty<int>();
  }

  int pivot = ints.First();

  IEnumerable<int> lt = ints.Where(i => i < pivot);
  IEnumerable<int> gt = ints.Where(i => i > pivot);

  return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt));
}

확실히 말씀드리면, 저는 이것이 퀵 소트의 좋은 구현이라고 옹호하는 것은 아닙니다!그러나 이는 스트림 연산과 결합된 람다 표현의 표현력을 보여주는 좋은 예입니다.

자바에서는 할 수 없어요!스트림은 사용할 수 없게 만들지 않고서는 비어 있는지조차 물어볼 수 없습니다.

Streams API의 초기 디자인에서 몇 가지 기억이 있는데, 이는 디자인 근거를 좀 더 명확하게 할 수 있을 것 같습니다.

2012년에 우리는 언어에 람다를 추가했습니다. 우리는 람다를 사용하여 프로그래밍된 수집 지향 또는 "대량 데이터" 연산 세트를 병렬 처리를 용이하게 하려고 했습니다.이 점에 의해, 업무를 나태하게 결속시키는 생각은 잘 확립되어 있었다.중간 작업에서 결과를 저장하는 것도 원치 않았습니다.

우리가 결정해야 할 주요 문제는 API에서 체인 내의 객체가 어떻게 생겼는지와 그것들이 데이터 소스에 어떻게 연결되었는지를 결정하는 것이었습니다.대부분의 경우 소스는 수집이었지만 파일이나 네트워크에서 나오는 데이터 또는 랜덤 번호 생성기에서 즉시 생성되는 데이터도 지원하고자 했습니다.

기존 작업의 영향이 디자인에 많이 있었습니다.구글의 Guava 라이브러리와 Scala 컬렉션 라이브러리(Guava의 영향에 놀라는 사람이 있다면 Guava의 수석 개발자 Kevin Bourrillion이 JSR-335 Lambda 전문가 그룹에 있었다는 에 유의하십시오.)스칼라 컬렉션에 관해 마틴 오더스키의 이 강연은 특히 흥미로웠다.미래형 스칼라 컬렉션: 가변형에서 지속형, 병렬형. (Stanford EE380, 2011년 6월 1일)

그 당시 우리의 프로토타입 디자인은Iterable· 익숙한 조작filter,map확장 방식(디폴트)은 다음과 같습니다.Iterable. 하나를 호출하면 체인에 작업이 추가되고 다른 하나가 반환됩니다.Iterable. 터미널 운영과 같은 것입니다.count전화하다iterator()운영은 각 스테이지의 반복기 내에서 구현되었습니다.

이것들은 반복가능성이기 때문에iterator()메서드를 여러 번 사용합니다.그럼 어떻게 되는 거죠?

소스가 컬렉션인 경우 대부분 정상적으로 작동합니다.수집은 반복할 수 있으며, 각 콜은 다음과 같습니다.iterator()는 다른 액티브인스턴스로부터 독립된 개별 반복 인스턴스를 생성하여 각각 독립적으로 컬렉션을 통과합니다.엄청나.

파일의 행을 읽는 것처럼 소스가 원샷이면 어떻게 될까요?첫 번째 반복기는 모든 값을 가져오지만 두 번째 이후의 값은 비워 두어야 합니다.이 값은 반복자 간에 인터리브해야 할 수도 있습니다.또는 각 반복자가 동일한 값을 얻을 수도 있습니다.그러면 두 개의 반복기가 있고 한 쪽이 다른 쪽보다 더 앞서면 어떨까요?누군가 두 번째 반복기의 값을 읽을 때까지 버퍼링해야 할 것입니다.게다가 1개의 Iterator를 취득해, 모든 값을 읽어내고 나서, 2번째 Iterator를 취득하면 어떻게 됩니까.그 가치들은 이제 어디서 나올까요?만약 누군가가 두 번째 반복기를 원할 때를 대비해서 모두 버퍼링해야 하나요?

원샷 소스에 여러 개의 반복기를 허용하면 많은 문제가 발생합니다.우리는 그들에게 좋은 답이 없었다.델은 고객님이 전화하실 경우 발생하는 일에 대해 일관되고 예측 가능한 행동을 원했습니다.iterator()두 번이나요 여러 번 통과를 불허하는 쪽으로 내몰려서 파이프라인이 원샷이 됐어요

우리는 또한 다른 사람들이 이러한 문제에 부딪히는 것을 관찰했다.JDK에서 대부분의 반복 가능은 여러 통과를 허용하는 컬렉션 또는 컬렉션과 유사한 개체입니다.어디에도 명시되어 있지 않지만, Iterables가 여러 개의 통과를 허용한다는 불문율이 있는 것 같습니다.눈에 띄는 예외는 NIO DirectoryStream 인터페이스입니다.이 사양에는 다음과 같은 흥미로운 경고가 포함되어 있습니다.

Directory Stream은 Itable을 확장하지만 Iterator를 1개만 지원하므로 범용 Iterable이 아닙니다.Iterator 메서드를 호출하여 두 번째 이후의 Iterator를 취득하면 Iterable State Exception이 느려집니다.

[원래 대담]

이것은 매우 이례적이고 불쾌해 보였기 때문에 우리는 한 번뿐인 새로운 Iterables를 만들고 싶지 않았습니다.이로 인해 Itable을 사용하지 않게 되었습니다.

이 시기에 브루스 에클이 쓴 기사가 스칼라와 문제가 된 부분을 묘사했다.그는 이 코드를 작성했다:

// Scala
val lines = fromString(data).getLines
val registrants = lines.map(Registrant)
registrants.foreach(println)
registrants.foreach(println)

꽤 간단해.텍스트 행을 해석하여Registrant오브젝트 및 인쇄를 2회 실시합니다.딱 한 번만 출력해준다는 것만 빼면요알고 보니 그는 그렇게 생각했다.registrants사실 반복기일 때는 컬렉션이었어요.에의 두 번째 콜foreach빈 반복기가 발견되어 모든 값이 소진되었기 때문에 아무것도 출력되지 않습니다.

이러한 경험은 다중 횡단을 시도할 경우 명확하게 예측 가능한 결과를 얻는 것이 매우 중요하다는 것을 확신시켰다.또한 데이터를 저장하는 실제 수집과 느린 파이프라인과 유사한 구조를 구별하는 것의 중요성을 강조했다.그 결과 느린 파이프라인 작업이 새로운 스트림 인터페이스로 분리되고 Collections에 대한 열정적이고 변이적인 작업만 직접 유지되었습니다.Brian Goetz는 그것에 대한 근거를 설명했다.

수집 기반 파이프라인에는 다중 통과를 허용하지만 비수집 기반 파이프라인에는 허용하지 않는 것은 어떻습니까?일관성은 없지만, 합리적이죠.네트워크에서 값을 읽는 경우 물론 다시 통과할 수 없습니다.여러 번 이동하려면 명시적으로 집합으로 끌어와야 합니다.

수집 기반 파이프라인에서 여러 개의 통과를 허용하는 방법에 대해 살펴보겠습니다.예를 들어 다음과 같습니다.

Iterable<?> it = source.filter(...).map(...).filter(...).map(...);
it.into(dest1);
it.into(dest2);

(the.into조작의 철자가 바뀌었습니다.collect(toList()).)

source가 컬렉션인 경우 첫 번째는into()call은 소스로 돌아가는 반복기 체인을 만들고 파이프라인 작업을 실행한 후 결과를 대상으로 전송합니다.에의 두 번째 콜into()는 다른 반복기 체인을 생성하고 파이프라인 작업을 다시 실행합니다.이는 명백히 잘못된 것은 아니지만 각 요소에 대해 모든 필터 및 맵 작업을 두 번째로 수행하는 효과가 있습니다.많은 프로그래머들이 이 동작에 놀랐을 거라고 생각합니다.

앞서 말씀드린 바와 같이, 우리는 Guava 개발자들과 이야기를 나누었습니다.그들이 가지고 있는 멋진 것 중 하나는 Idea Graveyze입니다.이것에서는, 실장하지 않기로 기능에 대해, 그 이유와 함께 설명하고 있습니다.게으른 컬렉션에 대한 생각은 꽤 멋있게 들리지만, 그들이 그것에 대해 말하는 것은 이렇다.예를 들어List.filter()를 반환하는 조작List:

여기서 가장 큰 문제는 너무 많은 운영이 비용이 많이 드는 선형 시간 제안이 된다는 것입니다.목록을 필터링하여 목록을 가져오려면 Collection이나 Itherable뿐만 아니라ImmutableList.copyOf(Iterables.filter(list, predicate))그것은 그것이 무엇을 하고 있는지 그리고 얼마나 비싼지를 "앞으로 진술"하는 것입니다.

구체적인 예를 들면, 의 비용은 얼마인가?get(0)또는size()목록에?일반적으로 사용되는 수업의 경우ArrayListO(1)입니다.단, 이들 중 하나를 필터가 느슨한 목록으로 호출하면 필터가 백업목록에서 실행되어야 합니다.이러한 조작은 갑자기 O(n)가 됩니다.게다가 모든 작업에서 지원 목록을 통과해야 합니다.

이것은 우리에게 너무 게으른 처럼 보였다.일부 작업을 설정하고 실제 실행을 "시작"할 때까지 연기하는 것도 한 가지 방법입니다.또 하나의 방법은 잠재적으로 많은 양의 재계산을 숨기는 방식으로 작업을 설정하는 것입니다.

Paul Sandoz는 비선형 스트림 또는 "재사용 불가" 스트림을 허용하지 않을 것을 제안하면서 이러한 스트림을 허용하면 "예상치 못한 또는 혼란스러운 결과"가 발생할 수 있는 잠재적 결과를 설명했습니다.그는 또한 병렬 실행이 상황을 더 까다롭게 만들 것이라고 언급했다.마지막으로, 부작용이 있는 파이프라인 연산은 예기치 않게 여러 번 또는 프로그래머가 예상한 횟수보다 더 많이 실행된다면 어렵고 불분명한 버그로 이어질 수 있다는 것을 덧붙입니다.(하지만 자바 프로그래머들은 부작용을 수반하는 람다 표현을 쓰지 않습니다.)그런가요?

이것이 원샷 트래버설을 가능하게 하는 Java 8 Streams API 설계의 기본적인 근거이며, 엄밀하게 선형(브런칭 없음) 파이프라인이 필요합니다.여러 스트림 소스에 걸쳐 일관된 동작을 제공하고 게으른 작업과 열심인 작업을 명확하게 구분하며 간단한 실행 모델을 제공합니다.


에 대해서IEnumerable저는 C#과 C#의 전문가와는 거리가 멀어요.NET, 잘못된 결론을 도출하면 정정해 주시면 감사하겠습니다.다만, 인 것 같다.IEnumerable복수의 트래버설을 다른 소스로 동작시킬 수 있습니다.또, 네스트 된 브런치 구조가 가능하게 됩니다.IEnumerable중요한 재계산이 발생할 수 있습니다.시스템마다 트레이드오프가 다르다는 것은 감사하지만 Java 8 Streams API 설계에서는 이 두 가지 특징을 피하려고 했습니다.

OP가 제시한 퀵소트 예는 흥미롭고, 곤혹스러우며, 죄송하지만 다소 소름끼치게 합니다.부르기QuickSort한 번 찍다IEnumerable및 반환한다.IEnumerable그래서 마지막까지 실제로 정렬이 이루어지지 않습니다.IEnumerable통과합니다.다만, 콜이 하는 것은, 다음의 트리 구조를 구축하는 것으로 보입니다.IEnumerables이는 QuickSort가 실제로 하지 않아도 할 수 있는 파티셔닝을 반영하고 있습니다.(이것은 결국 게으른 계산입니다.)소스에 N개의 요소가 있는 경우 트리는 가장 넓은 N개의 요소가 되며 깊이는 lg(N) 레벨이 됩니다.

제가 보기에는, 다시 한 번 말하지만, 저는 C#이나 C#이 아닙니다.NET 익스퍼트 - 이를 통해 피벗 선택 등 특정 악의 없는 외관상의 콜이 발생할 수 있습니다.ints.First()보기보다 비싸게 팔리고 있습니다.첫 번째 레벨은 물론 O(1)입니다.그러나 오른쪽 끝에 있는 트리의 깊숙한 곳에 있는 파티션을 고려해 보십시오.이 파티션의 첫 번째 요소를 계산하려면 전체 소스인 O(N) 연산을 통과해야 합니다.그러나 위의 파티션은 게으르기 때문에 O(lg N) 비교가 필요하므로 재계산해야 합니다.따라서 피벗을 선택하는 것은 O(N lg N) 작업이므로 전체 종류만큼 비용이 많이 듭니다.

하지만 우리는 실제로 우리가 돌아오는 곳을 지나칠 때까지 분류하지 않는다.IEnumerable표준 QuickSort 알고리즘에서는 파티션의 각 레벨은 파티션의 수를 2배로 합니다.각 파티션은 절반 크기이므로 각 레벨은 O(N) 복잡도로 유지됩니다.파티션 트리가 O(lg N) 높이로 되어 있기 때문에 총 작업량은 O(N lg N)입니다.

느린 IEnumerables 트리를 사용하여 트리 하단에 N개의 파티션이 있습니다.각 파티션을 계산하려면 N개의 요소를 통과해야 합니다. 각 요소는 트리에서 lg(N) 비교가 필요합니다.트리 하단의 모든 파티션을 계산하려면 O(N^2 lg N) 비교가 필요합니다.

(이게 맞나?나는 이것을 거의 믿을 수 없다.누가 이것 좀 확인해 주세요.)

어쨌든, 는 것은 정말 멋지다.IEnumerable이 방법으로 복잡한 계산 구조를 구축할 수 있습니다.하지만 제 생각만큼 계산의 복잡성을 높인다면 이런 프로그래밍은 극도로 조심하지 않는 한 피해야 할 것 같습니다.

배경

질문은 간단해 보이지만 실제 답변은 이해가 되기 위해서는 몇 가지 배경지식이 필요합니다.결론으로 넘어가려면 아래로 스크롤...

비교 포인트 선택 - 기본 기능

기본 개념을 사용하여 C#의IEnumerable컨셉은 Java와 더욱 밀접하게 관련되어 있습니다.Java는 원하는 만큼 반복기를 만들 수 있습니다.Iterable만들다Iterators

각 개념의 역사는 유사하다.IEnumerable그리고.Iterable데이터 수집 구성원을 대상으로 '각자를 위한' 스타일을 반복할 수 있도록 하는 기본적인 동기를 가지고 있습니다.이는 둘 다 그 이상의 것을 허용하기 때문에 지나치게 단순화된 것이며, 또한 서로 다른 과정을 거쳐 그 단계에 도달했지만, 어쨌든 이것은 중요한 공통적인 기능입니다.

이 기능을 비교해 봅시다.두 언어 모두 클래스에서 구현되는 경우IEnumerable/Iterable이 클래스는 적어도1개의 메서드를 실장할 필요가 있습니다(C#의 경우,GetEnumeratorJava의 경우iterator()각 케이스에서 인스턴스가 반환되었습니다(IEnumerator/Iterator)를 사용하면 데이터의 현재 멤버와 후속 멤버에 액세스할 수 있습니다.이 기능은 각 언어의 구문에서 사용됩니다.

비교 포인트 선택 - 확장 기능

IEnumerableC# 에서는 다른 언어 기능(대부분의 Linq 관련)을 사용할 수 있도록 확장되어 있습니다.추가된 기능으로는 선택, 투영, 집약 등이 있습니다.이러한 확장 기능에는 SQL 및 관계형 데이터베이스 개념과 마찬가지로 집합 이론에서 사용하는 강력한 동기가 있습니다.

Java 8은 또한 Streams와 Lambdas를 사용하여 일정 수준의 기능 프로그래밍을 가능하게 하는 기능이 추가되었습니다.Java 8 스트림은 주로 집합론에 의해 동기 부여되는 것이 아니라 기능적 프로그래밍에 의해 동기 부여됩니다.그럼에도 불구하고, 많은 유사점들이 있다.

자, 두 번째 포인트입니다.C#에 대한 확장기능은 C#에 대한 확장기능은IEnumerable개념.그러나 Java에서는 Lambdas와 Streams의 새로운 기본 개념을 만들고 변환하는 비교적 간단한 방법을 만들어냄으로써 향상된 기능이 구현되었습니다.Iterators그리고.Iterables스트림, 그리고 비자-versa로요.

따라서 IEnumerable을 Java의 Stream 개념과 비교하는 것은 불완전합니다.Java의 Streams and Collections API와 비교해야 합니다.

Java에서 스트림은 반복 가능 또는 반복기와 동일하지 않습니다.

스트림은 반복기와 같은 방식으로 문제를 해결하도록 설계되지 않았습니다.

  • 반복기는 데이터의 순서를 설명하는 방법입니다.
  • 스트림은 데이터 변환 시퀀스를 설명하는 방법입니다.

를 사용하여Iterator데이터 값을 가져와 처리한 다음 다른 데이터 값을 가져옵니다.

스트림에서는 일련의 함수를 체인으로 연결한 다음 스트림에 입력 값을 공급하고 결합된 시퀀스에서 출력 값을 가져옵니다.주의: Java 용어로 각 함수는 1개로 캡슐화되어 있습니다.Stream사례.Streams API를 사용하면 일련의 링크에 링크할 수 있습니다.Stream일련의 변환식을 체인하는 방식으로 인스턴스(instance 。

를 완료하려면Stream개념에서는 스트림을 공급하기 위한 데이터 소스와 스트림을 소비하는 터미널 함수가 필요합니다.

스트림에 값을 입력하는 방법은 실제로 다음과 같습니다.Iterable단,Stream시퀀스 그 자체가Iterable복합 함수입니다.

A Stream또한 값은 사용자가 요구할 때만 기능한다는 의미에서 게으름을 의도하고 있습니다.

스트림의 중요한 전제 조건과 기능에 주의해 주십시오.

  • A StreamJava는 변환 엔진으로, 한 상태의 데이터 항목을 다른 상태로 변환합니다.
  • 스트림에는 데이터 순서나 위치에 대한 개념이 없으며, 요청 사항이 무엇이든 변환하기만 하면 됩니다.
  • 스트림은 다른 스트림, 반복기, 반복기, 수집, 수집 등 다양한 소스에서 데이터를 제공할 수 있습니다.
  • 스트림을 "변환"할 수 없습니다. 즉, "변환"과 같습니다.데이터 소스를 리셋하는 것이 가장 바람직할 것입니다.
  • 스트림에는 항상 논리적으로 하나의 데이터 항목만 '이동 중'으로 존재합니다(스트림이 병렬 스트림일 경우 스레드당 하나의 항목이 있음).이는 스트림에 '준비 완료'되는 현재 항목보다 많은 데이터 소스나 여러 값을 집계하고 줄여야 하는 스트림 수집기와는 무관합니다.
  • 스트림은 바인딩되지 않거나(무한), 데이터 소스에 의해서만 제한되거나 수집기(무한일 수도 있음)로 제한될 수 있습니다.
  • 스트림은 'chainable'이며, 한 스트림을 필터링한 결과도 다른 스트림입니다.스트림에 의해 입력 및 변환된 값은 다른 변환을 수행하는 다른 스트림에 공급될 수 있습니다.데이터는 변환된 상태에서 스트림 간에 흐릅니다.개입하여 한 스트림에서 데이터를 꺼내 다음 스트림에 연결할 필요가 없습니다.

C# 비교

Java Stream은 공급, 스트림 및 수집 시스템의 일부일 뿐이며 Stream과 반복기는 종종 컬렉션과 함께 사용된다는 점을 고려할 때 거의 모든 것이 하나의 시스템에 포함된 동일한 개념과 관련짓는 것이 어려운 것은 당연합니다.IEnumerableC#의 개념입니다.

IEnumerable의 일부(및 밀접하게 관련된 개념)는 Java Iterator, Iterable, Lambda 및 Stream의 모든 개념에서 나타납니다.

자바 개념이 IEnumerable 및 visa에서 더 어려운 작은 것들이 있습니다.


결론

  • 디자인상의 문제는 없습니다.단, 언어간의 컨셉을 매칭하는 데 문제가 있을 뿐입니다.
  • 스트림은 다른 방법으로 문제를 해결합니다.
  • 스트림은 Java에 기능을 추가합니다(다른 작업 방식을 추가하며 기능을 빼앗기지 않습니다).

스트림을 추가하면 문제 해결 시 선택의 폭이 넓어집니다.이것은 '감소', '탈취', '제한'이 아닌 '파워 강화'로 분류하는 것이 적절합니다.

Java Streams가 일회성인 이유는 무엇입니까?

스트림은 함수 시퀀스이지 데이터가 아니기 때문에 이 질문은 잘못 알고 있습니다.스트림을 공급하는 데이터 원본에 따라 데이터 소스를 재설정하고 동일한 스트림 또는 다른 스트림을 제공할 수 있습니다.

실행 파이프라인을 원하는 횟수만큼 실행할 수 있는 C#의 IEnumerable과 달리 Java에서는 스트림을 한 번만 '반복'할 수 있습니다.

의 비교IEnumerable에 대해서Stream잘못 알고 있습니다.사용하는 콘텍스트는IEnumerable원하는 횟수만큼 실행할 수 있으며 Java에 비해 최적입니다.Iterables원하는 횟수만큼 반복할 수 있습니다.자바어Stream의 서브셋을 나타냅니다.IEnumerable데이터를 제공하는 하위 집합이 아닌 개념이므로 '확장'할 수 없습니다.

터미널 조작에 대한 콜은 스트림을 닫고 사용할 수 없게 됩니다.이 '기능'은 많은 전력을 빼앗는다.

첫 번째 진술은 어떤 의미에서는 사실이다.'권력을 빼앗는다'는 말은 그렇지 않다.Streams it IEnumerables를 비교하고 있습니다.스트림의 터미널 동작은 for 루프의 break 구와 같습니다.필요에 따라 필요한 데이터를 재공급할 수 있다면 언제든지 다른 스트림을 사용할 수 있습니다.다시 한 번 말씀드리지만IEnumerable더 닮다Iterable이 스테이트먼트에 대해서, Java는 그것을 잘 하고 있습니다.

나는 이것의 이유가 전문적이지 않다고 생각한다.이 이상한 제약의 배후에 있는 설계상의 고려사항은 무엇이었습니까?

이유는 기술적이고 단순한 이유로 스트림은 생각의 하위 집합이기 때문입니다.스트림 서브셋은 데이터 공급을 제어하지 않으므로 스트림이 아닌 공급을 리셋해야 합니다.그런 맥락에서, 그것은 그렇게 이상하지 않다.

QuickSort 예시

QuickSort 예제에는 다음과 같은 시그니처가 있습니다.

IEnumerable<int> QuickSort(IEnumerable<int> ints)

입력을 처리하고 있습니다.IEnumerable데이터 소스로서:

IEnumerable<int> lt = ints.Where(i => i < pivot);

또한 반환값은 다음과 같습니다.IEnumerable또한 데이터의 공급이며, 이것은 정렬 작업이기 때문에 해당 공급의 순서는 중요합니다.Java를 고려한다면Iterable이에 적합한 클래스, 구체적으로는List전문화IterableList는 순서 또는 반복이 보장된 데이터의 공급원이므로 코드와 동등한 Java 코드는 다음과 같습니다.

Stream<Integer> quickSort(List<Integer> ints) {
    // Using a stream to access the data, instead of the simpler ints.isEmpty()
    if (!ints.stream().findAny().isPresent()) {
        return Stream.of();
    }

    // treating the ints as a data collection, just like the C#
    final Integer pivot = ints.get(0);

    // Using streams to get the two partitions
    List<Integer> lt = ints.stream().filter(i -> i < pivot).collect(Collectors.toList());
    List<Integer> gt = ints.stream().filter(i -> i > pivot).collect(Collectors.toList());

    return Stream.concat(Stream.concat(quickSort(lt), Stream.of(pivot)),quickSort(gt));
}    

중복된 값을 정상적으로 처리하지 못하는 버그(복제)가 있습니다.그것은 '고유값' 정렬입니다.

또, Java 코드가 데이터 소스를 사용하는 방법에 주의해 주세요(ListC#에서는 이 두 가지 '개성'을 하나의 포인트로 표현할 수 있습니다.IEnumerable또, 비록 쓰긴 했지만List베이스 타입으로, 보다 일반적인 타입을 사용할 수 있었습니다.Collection, 그리고 작은 반복기에서 스트림으로 변환하면 더 일반적인 것을 사용할 수 있습니다.Iterable

Stream는 를 중심으로 구축되어 있습니다.Spliterator스테이트풀한 가변 객체입니다.그들은 "리셋" 액션이 없으며, 실제로 이러한 되감기 액션을 지원하도록 요구하는 것은 "많은 힘을 빼앗는다"는 것이다.그런 요청을 어떻게 처리합니까?

한편,Stream역추적 가능한 기원을 가진 s는 동등한 것을 쉽게 구축할 수 있습니다.Stream다시 사용할 수 있습니다.이 시스템을 구축하기 위해 만들어진 단계를 적용하기만 하면 됩니다.Stream재사용 가능한 방법으로 변환합니다.이러한 스텝을 반복하는 것은 비용이 많이 드는 조작은 아니라는 점에 유의해 주십시오.실제 작업은 터미널 조작에서 시작되며 실제 터미널 조작에 따라서는 전혀 다른 코드가 실행될 수 있습니다.

이러한 메서드의 작성자는 메서드가 두 번 호출하는 의미를 지정합니다.이 메서드는 수정되지 않은 어레이 또는 수집용으로 작성된 스트림과 정확히 같은 시퀀스를 재현하는지, 아니면 랜덤 int의 스트림이나 콘솔 입력 라인의 스트림과 같은 다른 시멘틱스를 가진 스트림을 생성하는지 등입니다.tc.


덧붙여서, 혼란을 피하기 위해 터미널 조작은Stream이것은, 폐색과는 다릅니다.Stream부르는 대로close()(예를 들어 에 의해 생성되는 것과 같은 관련 리소스를 가진 스트림에 필요함)Files.lines()).


많은 혼란은 비교의 잘못된 지침에서 기인하는 것으로 보인다.IEnumerable와 함께Stream. ANIEnumerable실제 정보를 제공할 수 있는 능력을 나타냅니다.IEnumerator그래서...Iterable자바어.반면,Stream일종의 반복기이며, 그에 필적합니다.IEnumerator따라서 이러한 종류의 데이터가 에서 여러 번 사용될 수 있다고 주장하는 것은 잘못된 것입니다.NET, 지원IEnumerator.Reset는 옵션입니다.여기서 논의된 예들은 오히려 다음과 같은 사실을 사용한다.IEnumerable새로운 것을 가져오는 데 사용할 수 있다 IEnumerator및 Java와 함께 동작합니다.Collections도 마찬가지입니다.새로운 것을 입수할 수 있습니다.Stream자바 개발자가 추가하기로 결정한 경우Stream에의 조작Iterable직접, 중간 작업을 통해 다른 작업을 반환한다.Iterable, 그것은 정말 비교가 되었고 같은 방식으로 작동할 수 있었다.

그러나 개발자들은 이에 반대하기로 결정하였고, 그 결정은 이 질문에서 논의되었다.가장 큰 포인트는 열성적인 수집 작업과 느린 스트림 작업에 대한 혼란입니다.를 참조해 주세요.NET API는 (개인적으로는) 정당하다고 생각합니다.이치에 맞는 것 같지만IEnumerable특정 컬렉션은 컬렉션을 직접 조작하는 많은 메서드와 lazy를 반환하는 많은 메서드를 가집니다.IEnumerable그러나 방법의 특정 특성을 항상 직관적으로 인식할 수 있는 것은 아닙니다.내가 발견한 최악의 예(몇 분 이내에 살펴본 결과)는 완전히 모순되는 동작을 하면서도 상속받은 사람의 이름과 정확히 일치하는 이름(확장 메서드의 올바른 끝점입니까?)입니다.


물론, 이것들은 두 가지 다른 결정입니다.제일 먼저 만드는 것Stream와는 다른 타입Iterable/Collection그리고 두 번째로 만들어야 할 것은Stream또 다른 종류의 반복이 아닌 일종의 반복입니다.그러나 이러한 결정은 함께 이루어졌고 이 두 결정을 분리하는 것은 고려되지 않은 경우일 수 있다.에 필적하는 것은 아닙니다.NET을 염두에 두고 있습니다.

실제 API 설계 결정은 개선된 유형의 반복기, 즉,Spliterator.Spliterator는 구식이 제공할 수 있습니다.Iterable또는 완전히 새로운 구현입니다.그리고나서,Stream고급 프런트 엔드로 낮은 레벨에 추가되었다Spliterators. 바로 그거야.다른 디자인이 더 나은지 논의해 볼 수도 있지만, 생산적이지 않고 현재 설계 방식을 고려할 때 변경되지 않을 것입니다.

다른 구현 측면도 고려해야 합니다. Streams는 불변의 데이터 구조가 아닙니다.각 중간 작업은 새 작업을 반환할 수 있습니다.Stream이전 인스턴스를 캡슐화하는 대신 자체 인스턴스를 조작하고 자신을 반환할 수도 있습니다(같은 작업에 대해 두 가지 작업을 모두 수행할 수도 있음).일반적으로 알려진 예로는 다음과 같은 작업이 있습니다.parallel또는unordered다른 단계를 추가하지 않고 전체 파이프라인을 조작할 수 있습니다.이러한 가변 데이터 구조를 가지고 재사용을 시도하는 것(또는 동시에 여러 번 사용하는 것)은 그다지 효과가 없습니다.


완전성을 위해 Java로 번역된 QuickSort의 예를 다음에 나타냅니다.StreamAPI. 이것은 실제로 "많은 힘을 빼앗지 않는다"는 것을 보여줍니다.

static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {

  final Optional<Integer> optPivot = ints.get().findAny();
  if(!optPivot.isPresent()) return Stream.empty();

  final int pivot = optPivot.get();

  Supplier<Stream<Integer>> lt = ()->ints.get().filter(i -> i < pivot);
  Supplier<Stream<Integer>> gt = ()->ints.get().filter(i -> i > pivot);

  return Stream.of(quickSort(lt), Stream.of(pivot), quickSort(gt)).flatMap(s->s);
}

다음과 같이 사용할 수 있습니다.

List<Integer> l=new Random().ints(100, 0, 1000).boxed().collect(Collectors.toList());
System.out.println(l);
System.out.println(quickSort(l::stream)
    .map(Object::toString).collect(Collectors.joining(", ")));

보다 콤팩트하게 쓸 수 있습니다.

static Stream<Integer> quickSort(Supplier<Stream<Integer>> ints) {
    return ints.get().findAny().map(pivot ->
         Stream.of(
                   quickSort(()->ints.get().filter(i -> i < pivot)),
                   Stream.of(pivot),
                   quickSort(()->ints.get().filter(i -> i > pivot)))
        .flatMap(s->s)).orElse(Stream.empty());
}

자세히 보면 그 둘 사이에 차이가 거의 없다고 생각해요.

그 얼굴에서IEnumerable는 재사용 가능한 구성인 것 같습니다.

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };

foreach (var n in numbers) {
    Console.WriteLine(n);
}

그러나 컴파일러는 실제로 약간의 작업을 수행하고 있습니다.다음 코드를 생성합니다.

IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5 };

IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext()) {
    Console.WriteLine(enumerator.Current);
}

실제로 열거형을 반복할 때마다 컴파일러는 열거형을 만듭니다.열거자를 재사용할 수 없습니다. 추가 호출:MoveNextfalse가 반환되기 때문에 처음부터 리셋할 수 없습니다.숫자를 반복하려면 다른 열거자 인스턴스를 만들어야 합니다.


IEnumerable이 Java 스트림과 동일한 '기능'을 가지고 있음을 더 잘 설명하기 위해 번호의 소스가 정적 컬렉션이 아닌 열거형을 고려하십시오.예를 들어, 5개의 랜덤 번호의 시퀀스를 생성하는 열거형 객체를 작성할 수 있습니다.

class Generator : IEnumerator<int> {
    Random _r;
    int _current;
    int _count = 0;

    public Generator(Random r) {
        _r = r;
    }

    public bool MoveNext() {
        _current= _r.Next();
        _count++;
        return _count <= 5;
    }

    public int Current {
        get { return _current; }
    }
 }

class RandomNumberStream : IEnumerable<int> {
    Random _r = new Random();
    public IEnumerator<int> GetEnumerator() {
        return new Generator(_r);
    }
    public IEnumerator IEnumerable.GetEnumerator() {
        return this.GetEnumerator();
    }
}

이전 어레이 기반 열거형 코드와 매우 유사하지만 두 번째 반복은numbers:

IEnumerable<int> numbers = new RandomNumberStream();

foreach (var n in numbers) {
    Console.WriteLine(n);
}
foreach (var n in numbers) {
    Console.WriteLine(n);
}

우리가 두 번째로 반복할 때numbers같은 의미에서는 재사용할 수 없는 다른 숫자의 시퀀스를 얻을 수 있습니다.아니면, 우리가 그 글을 쓸 수도 있었다.RandomNumberStream여러 번 반복하려고 하면 예외가 발생하므로 열거형(Java 스트림 등)을 실제로 사용할 수 없게 됩니다.

또한, 열거형 기반의 빠른 정렬이 에 적용되었을 때 무엇을 의미합니까?RandomNumberStream?


결론

그래서 가장 큰 차이점은 그거에요.NET을 사용하면, Ethernet 의 재이용할 수 있습니다.IEnumerable암묵적으로 새로운 것을 창조함으로써IEnumerator시퀀스 내의 요소에 액세스 할 필요가 있을 때마다 백그라운드로 이동합니다.

이 암묵적인 동작은 컬렉션에 대해 반복적으로 반복할 수 있기 때문에 종종 유용합니다(또한 '강력한' 내용도 있습니다.

하지만 때로는 이러한 암묵적인 행동이 실제로 문제를 일으킬 수 있습니다.데이터 소스가 정적이지 않거나 액세스 비용이 많이 드는 경우(데이터베이스나 웹 사이트 등),IEnumerable폐기해야 한다; 재사용은 그리 간단하지 않다

예를 들어 Stream API에서 일부 "한 번 실행" 보호를 무시할 수 있습니다.java.lang.IllegalStateException예외('스트림은 이미 작동되었거나 닫힘' 메시지 포함)를 참조하고 재사용함으로써Spliterator(대신Stream직접).

예를 들어, 이 코드는 예외를 발생시키지 않고 실행됩니다.

    Spliterator<String> split = Stream.of("hello","world")
                                      .map(s->"prefix-"+s)
                                      .spliterator();

    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);


    replayable1.forEach(System.out::println);
    replayable2.forEach(System.out::println);

단, 출력은

prefix-hello
prefix-world

출력을 두 번 반복하는 대신.이것은, 그 이유는,ArraySpliterator로서 사용되다Streamsource는 스테이트풀이며 현재 위치를 저장합니다.이걸 재생하면Stream마지막에 다시 시작하는 거야

이 과제를 해결하기 위한 많은 옵션이 있습니다.

  1. 스테이트리스(stateless)를 이용해서Stream등의 작성 방법Stream#generate()외부적으로는 자체 코드로 상태를 관리해야 합니다.또, 그 사이에 리셋 할 필요가 있습니다.Stream"일시":

    Spliterator<String> split = Stream.generate(this::nextValue)
                                      .map(s->"prefix-"+s)
                                      .spliterator();
    
    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);
    
    
    replayable1.forEach(System.out::println);
    this.resetCounter();
    replayable2.forEach(System.out::println);
    
  2. 이것에 대한 또 다른(약간 더 낫지만 완벽하지는 않은) 해결책은, 델의 독자적인 솔루션을 작성하는 것입니다.ArraySpliterator(또는 유사)Streamcurrent counter를 리셋할 수 있는 용량이 포함되어 있습니다.만약 우리가 그걸 이용해서Stream다시 볼 수 있을 것 같아요.

    MyArraySpliterator<String> arraySplit = new MyArraySpliterator("hello","world");
    Spliterator<String> split = StreamSupport.stream(arraySplit,false)
                                            .map(s->"prefix-"+s)
                                            .spliterator();
    
    Stream<String> replayable1 = StreamSupport.stream(split,false);
    Stream<String> replayable2 = StreamSupport.stream(split,false);
    
    
    replayable1.forEach(System.out::println);
    arraySplit.reset();
    replayable2.forEach(System.out::println);
    
  3. (내 생각에) 이 문제에 대한 최선의 해결책은 스테이트풀한 복사본을 새로 만드는 것입니다.Spliterator에서 사용되는Stream에서 새로운 연산자가 호출될 때 파이프라인Stream이 방법은 구현이 더 복잡하고 복잡하지만 서드파티 라이브러리를 사용하는 것이 괜찮으시다면 cyclops-react에는Stream(공개:저는 이 프로젝트의 개발 책임자입니다.)

    Stream<String> replayableStream = ReactiveSeq.of("hello","world")
                                                 .map(s->"prefix-"+s);
    
    
    
    
    replayableStream.forEach(System.out::println);
    replayableStream.forEach(System.out::println);
    

이것은 인쇄됩니다.

prefix-hello
prefix-world
prefix-hello
prefix-world

역시나

그 이유는 Iterator나 BufferedReader와 같이 정의에 따라 한 번만 사용할 수 있는 스트림을 만들 수 있기 때문입니다.Stream은 BufferedReader를 사용하여 텍스트 파일을 끝까지 읽는 것과 동일한 방식으로 소비된다고 생각할 수 있습니다.파일 끝에 도달하면 Buffered Reader는 기존 파일을 중지하지 않고 아무것도 얻을 수 없기 때문에 사용할 수 없게 됩니다.파일을 다시 읽으려면 새 판독기를 작성해야 합니다.스트림도 마찬가지입니다.스트림의 송신원을 2회 처리하는 경우는, 2개의 다른 스트림을 작성할 필요가 있습니다.

언급URL : https://stackoverflow.com/questions/28459498/why-are-java-streams-once-off

반응형