IEnumerable
실행 파이프 라인을 원하는만큼 여러 번 실행할 수있는 C #과 달리 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));
}
확실히, 나는 이것이 빠른 정렬의 좋은 구현이라고 주장하지 않습니다! 그러나 스트림 조작과 결합 된 람다 표현의 표현력의 훌륭한 예입니다.
그리고 Java로는 할 수 없습니다! 스트림을 사용할 수 없게 만들지 않고 빈 스트림인지 물어볼 수도 없습니다.
답변
Streams API의 초기 디자인에서 디자인의 이론적 근거를 밝힐 수있는 몇 가지 사항이 있습니다.
2012 년에 우리는 언어에 람다를 추가하고 병렬 처리를 용이하게하는 컬렉션 지향 또는 “대량 데이터”연산 세트를 원했습니다. 게으른 체인 작업의 아이디어는이 시점에서 잘 확립되었습니다. 또한 중간 작업에서 결과를 저장하고 싶지 않았습니다.
우리가 결정해야 할 주요 문제는 체인의 객체가 API에서 어떻게 보이는지와 데이터 소스에 어떻게 연결되어 있는지였습니다. 소스는 종종 수집 이었지만 파일이나 네트워크에서 오는 데이터 또는 난수 생성기 등의 데이터를 즉시 지원하기를 원했습니다.
기존 작업이 디자인에 많은 영향을 미쳤습니다. 더 영향력있는 것은 구글의 구아바 도서관과 스칼라 컬렉션 도서관이었습니다. (구아바의 영향에 대해 아무도 놀라지 않는다면 구아바의 수석 개발자 인 Kevin Bourrillion 은 JSR-335 Lambda 에 있다는 점에 유의하십시오. . 전문가 그룹) 스칼라 컬렉션에, 우리는 특히 관심을 마틴 오더 스키하여이 이야기를 발견 Future- 교정 스칼라 컬렉션 : Mutable에서 Persistent, Parallel까지 . (Stanford EE380, 2011 년 6 월 1 일)
당시 우리의 프로토 타입 디자인은 주위에 Iterable
있었습니다. 친숙한 작업은 filter
, map
, 등의 확장자 (기본) 방법이었다 Iterable
. 하나를 호출하면 체인에 작업을 추가하고 다른 것을 반환Iterable
. 터미널 작업 은 체인을 소스로 count
불러 iterator()
오며 각 단계의 반복자 내에서 작업이 구현되었습니다.
이것들은 Iterables이므로 iterator()
메소드를 두 번 이상 . 그러면 어떻게됩니까?
소스가 컬렉션 인 경우 대부분 잘 작동합니다. 컬렉션은 반복 가능하며 각 호출은iterator()
은 다른 활성 인스턴스와 독립적 인 고유 한 Iterator 인스턴스 생성하고 각 컬렉션을 독립적으로 통과합니다. 큰.
이제 파일에서 라인을 읽는 것과 같이 소스가 원샷 인 경우 어떻게해야합니까? 첫 번째 Iterator는 모든 값을 가져야하지만 두 번째 이후의 값은 비어 있어야합니다. 반복자 사이에 값을 인터리브해야 할 수도 있습니다. 또는 각 반복자가 동일한 값을 가져와야 할 수도 있습니다. 그렇다면 두 개의 이터레이터가 있고 하나가 다른 반복자보다 앞당기면 어떨까요? 누군가 읽을 때까지 두 번째 Iterator의 값을 버퍼링해야합니다. 더 나쁜 것은, 하나의 Iterator를 얻고 모든 값을 읽은 다음 두 번째 Iterator 를 얻는 다면 어떨까요? 가치는 지금 어디에서 오는가? 누군가 두 번째 Iterator를 원할 경우를 대비 하여 모두 버퍼링해야 합니까?
분명히 원샷 소스에 여러 반복자를 허용하면 많은 질문이 제기됩니다. 우리는 그들에게 좋은 대답이 없었습니다. 우리는 당신이 전화하면 어떻게 될지 일관되고 예측 가능한 행동을 원했습니다.iterator()
두 번 했습니다. 이로 인해 여러 순회를 허용하지 않고 파이프 라인을 한 번에 만들 수있었습니다.
또한 다른 사람들이 이러한 문제에 부딪 치는 것을 관찰했습니다. JDK에서 대부분의 Iterable은 콜렉션 또는 콜렉션 유사 오브젝트이며 여러 순회를 허용합니다. 어디에도 지정되지 않았지만 Iterables가 다중 순회를 허용한다는 기록되지 않은 기대가있는 것 같습니다. 주목할만한 예외는 NIO DirectoryStream 인터페이스입니다. 사양에는 다음과 같은 흥미로운 경고가 포함됩니다.
DirectoryStream은 Iterable을 확장하지만 단일 Iterator 만 지원하므로 범용 Iterable이 아닙니다. 반복자 메소드를 호출하여 두 번째 또는 후속 반복자를 확보하면 IllegalStateException이 발생합니다.
[원본으로 굵게]
이것은 독특하고 불쾌한 것처럼 보였으며 한 번만 할 수있는 새로운 Iterable을 많이 만들고 싶지 않았습니다. 이로 인해 Iterable을 사용하지 못하게되었습니다.
이시기에 브루스 에켈 (Bruce Eckel) 의 기사가 스칼라와 관련된 문제를 묘사 한 것으로 나타났다. 그는이 코드를 작성했다 :
// Scala
val lines = fromString(data).getLines
val registrants = lines.map(Registrant)
registrants.foreach(println)
registrants.foreach(println)
꽤 간단합니다. 텍스트 줄을 Registrant
개체 로 구문 분석 하고 두 번 인쇄합니다. 실제로는 한 번만 인쇄합니다. 그는 생각했다registrants
그것이 반복자 일 때 컬렉션 . 두 번째 호출은 foreach
모든 값이 소진 된 빈 반복자 를 만나므로 아무것도 인쇄하지 않습니다.
이러한 경험은 여러 번의 순회를 시도 할 경우 명확하게 예측 가능한 결과를 얻는 것이 매우 중요하다는 것을 우리에게 확신 시켰습니다. 또한 데이터를 저장하는 실제 컬렉션과 게으른 파이프 라인 유사 구조를 구별하는 것이 중요하다는 점을 강조했습니다. 결과적으로 게으른 파이프 라인 작업을 새로운 Stream 인터페이스로 분리하고 컬렉션에서 직접 열성적인 변이 작업 만 유지했습니다. Brian Goetz는 이에 대한 이론적 근거를 설명 했습니다.
컬렉션 기반 파이프 라인에는 다중 순회를 허용하지만 컬렉션 기반이 아닌 파이프 라인에는 허용하지 않는 것은 어떻습니까? 일관성이 없지만 합리적입니다. 물론 네트워크에서 값을 읽는다면 당신은 다시 통과 할 수 없다. 여러 번 트래버스하려면 트래버스를 명시 적으로 가져와야합니다.
그러나 컬렉션 기반 파이프 라인에서 여러 순회를 허용하도록하겠습니다. 당신이 이것을했다고 가정 해 봅시다.
Iterable<?> it = source.filter(...).map(...).filter(...).map(...);
it.into(dest1);
it.into(dest2);
( into
이제 철자가 철자가되었습니다 collect(toList())
.)
소스가 콜렉션 인 경우 첫 번째 into()
호출은 소스에 반복기 체인을 다시 작성하고 파이프 라인 조작을 실행 한 후 결과를 대상으로 보냅니다. 두 번째 호출 into()
은 다른 반복자 체인을 작성하고 파이프 라인 조작을 다시 실행 합니다 . 이것은 분명히 잘못된 것은 아니지만 각 요소에 대해 모든 필터 및 맵 작업을 두 번 수행하는 효과가 있습니다. 많은 프로그래머들이이 행동에 놀랐을 것이라고 생각합니다.
위에서 언급했듯이 우리는 구아바 개발자들과 대화하고있었습니다. 그들이 가지고있는 멋진 것 중 하나는 Idea Graveyard입니다. 여기 에는 이유와 함께 구현 하지 않기로 결정한 기능이 설명되어 있습니다 . 게으른 컬렉션에 대한 아이디어는 꽤 멋지게 들리지만 여기에 그들이 말해야 할 것이 있습니다. 다음 List.filter()
을 반환 하는 작업을 고려하십시오 List
.
여기서 가장 큰 관심사는 너무 많은 작업이 비싸고 선형적인 시간 제안이된다는 것입니다. 컬렉션이나 Iterable뿐만 아니라 목록을 필터링하고 목록을 다시 가져 오려면을 사용할 수 있습니다
ImmutableList.copyOf(Iterables.filter(list, predicate))
.
구체적인 예를 촬영하려면 비용 무엇 get(0)
이나 size()
목록에이? 와 같이 일반적으로 사용되는 클래스 ArrayList
는 O (1)입니다. 그러나 지연 필터링 된 목록에서이 중 하나를 호출하면 백업 목록에서 필터를 실행해야하며 갑자기 이러한 작업은 모두 O (n)입니다. 더 나쁜 것은 모든 작업 에서 지원 목록을 탐색해야한다는 것 입니다.
이것은 우리에게 게으름을 너무 많이 보였다 . 일부 작업을 설정하고 “이동”할 때까지 실제 실행을 연기하는 것이 한 가지입니다. 잠재적으로 많은 양의 재 계산을 숨기는 방식으로 설정하는 것이 또 다른 방법입니다.
비선형 또는 “재사용 불가능”스트림을 허용하지 않기 위해 Paul Sandoz 는 “예기치 않거나 혼란스러운 결과”를 발생 시키는 잠재적 결과 를 설명했습니다 . 또한 병렬 실행으로 인해 작업이 더욱 까다로워 질 것이라고 언급했습니다. 마지막으로, 부작용이 발생한 파이프 라인 작업은 작업이 예기치 않게 여러 번 실행되거나 프로그래머가 예상 한 것과 다른 횟수로 실행되는 경우 어렵고 모호한 버그로 이어질 수 있다고 덧붙였습니다. (그러나 Java 프로그래머는 부작용이있는 람다 식을 쓰지 않습니까? 그렇습니까?)
따라서 Java 8 Streams API 디자인의 기본 이론은 원샷 통과를 허용하고 엄격하게 선형 (분기 없음) 파이프 라인이 필요합니다. 여러 다른 스트림 소스에서 일관된 동작을 제공하고 지연 작업과 지연 동작을 명확하게 분리하며 간단한 실행 모델을 제공합니다.
와 관련하여 IEnumerable
C # 및 .NET의 전문가와는 거리가 멀기 때문에 잘못된 결론을 도출하면 (신중하게) 수정되는 것에 감사드립니다. 그러나 IEnumerable
여러 순회가 다른 소스와 다르게 동작 할 수있는 것으로 보입니다 . 그리고 중첩 IEnumerable
연산 의 분기 구조를 허용하므로 상당한 재 계산이 발생할 수 있습니다. 시스템마다 다른 장단점이 있지만 Java 8 Streams API 디자인에서 피해야 할 두 가지 특성이 있습니다.
OP가 제시 한 퀵 정렬 예제는 흥미롭고 수수께끼이며 다소 무섭습니다. 호출은를 QuickSort
가져 와서 IEnumerable
를 반환 IEnumerable
하므로 최종 IEnumerable
이 통과 될 때까지 실제로 정렬이 수행되지 않습니다 . 그러나 호출이하는 것처럼 보이는 IEnumerables
것은 퀵 정렬이 실제로 수행하지 않고 분할을 반영 하는 트리 구조를 구축 하는 것입니다. (이것은 결국 게으른 계산입니다.) 소스에 N 개의 요소가 있으면 트리는 가장 넓은 N 개의 요소가되고 lg (N) 레벨의 깊이가됩니다.
다시 한 번 C # 또는 .NET 전문가가 아니므로을 통해 피벗 선택과 같은 무해한 통화가 ints.First()
외형보다 비싸 질 것 같습니다. 물론 첫 번째 수준은 O (1)입니다. 그러나 오른쪽 가장자리의 나무 깊이에있는 파티션을 고려하십시오. 이 파티션의 첫 번째 요소를 계산하려면 전체 소스를 순회해야합니다 (O (N) 작업). 그러나 위의 파티션은 게 으르므로 O (lg N) 비교를 요구하여 다시 계산해야합니다. 따라서 피벗을 선택하는 것은 O (N lg N) 작업이 될 것이며 이는 전체 종류만큼 비쌉니다.
그러나 우리는 반환 된을 횡단 할 때까지 실제로 정렬하지 않습니다 IEnumerable
. 표준 퀵 정렬 알고리즘에서 각 분할 수준은 분할 수를 두 배로 늘립니다. 각 파티션의 크기는 절반에 불과하므로 각 수준은 O (N) 복잡성으로 유지됩니다. 파티션 트리는 O (lg N) 높이이므로 총 작업량은 O (N lg N)입니다.
게으른 IEnumerables 트리를 사용하면 트리 아래쪽에 N 개의 파티션이 있습니다. 각 파티션을 계산하려면 N 요소의 순회가 필요하며 각 요소는 트리에서 lg (N) 비교가 필요합니다. 트리의 맨 아래에있는 모든 파티션을 계산하려면 O (N ^ 2 lg N) 비교가 필요합니다.
(이것이 맞나요? 나는 이것을 믿을 수 없습니다. 누군가 나를 위해 이것을 확인하십시오.)
어쨌든 IEnumerable
복잡한 계산 구조를 구축하기 위해 이런 식으로 사용될 수있는 것은 참으로 시원합니다 . 그러나 내가 생각하는 것만 큼 계산 복잡성을 증가시키는 경우이 방법을 프로그래밍하면 매우 신중하지 않으면 피해야 할 것 같습니다.
답변
배경
질문은 단순 해 보이지만 실제 답변에는 이해하기위한 배경이 필요합니다. 결론으로 건너 뛰려면 아래로 스크롤하십시오.
비교 포인트 선택-기본 기능
C #의 IEnumerable
개념은 기본 개념을 사용하여 원하는 수의 반복자 를 만들 수있는 JavaIterable
와 더 밀접한 관련 이 있습니다. 을 만듭니다 . 자바의 창조IEnumerables
IEnumerators
Iterable
Iterators
각 개념의 역사는 모두 점에서 유사하다 IEnumerable
와 Iterable
스타일 데이터 수집의 구성원에 걸쳐 반복 ‘는 각각에 대해 -‘허용하는 기본 의욕을 가지고있다. 그것들은 단지 그 이상을 허용하고 다른 진행을 통해 그 단계에 도달했기 때문에 지나치게 단순화되었지만, 그것은 공통적 인 특징입니다.
이 기능을 비교해 보겠습니다. 두 언어 모두 클래스가 IEnumerable
/를 구현하는 경우 Iterable
해당 클래스는 최소한 하나의 메소드를 구현해야합니다 (C # GetEnumerator
및 Java의 경우 iterator()
). 두 경우 모두 해당 인스턴스 ( IEnumerator
/ Iterator
) 에서 반환 된 인스턴스 를 사용하면 현재 및 후속 데이터 멤버에 액세스 할 수 있습니다. 이 기능은 for-each 언어 구문에서 사용됩니다.
비교 지점 선택-기능 향상
IEnumerable
C #에서는 여러 가지 다른 언어 기능 ( 주로 Linq 관련 ) 을 허용하도록 확장되었습니다 . 추가 된 기능에는 선택, 프로젝션, 집계 등이 포함됩니다. 이러한 확장에는 SQL 및 관계형 데이터베이스 개념과 유사하게 집합 이론에서 사용하는 데 큰 동기가 있습니다.
Java 8에는 Streams 및 Lambdas를 사용하여 어느 정도의 기능 프로그래밍이 가능하도록 기능이 추가되었습니다. Java 8 스트림은 주로 집합 이론이 아니라 기능적 프로그래밍에 의해 동기가 부여됩니다. 그럼에도 불구하고 많은 유사점이 있습니다.
이것이 두 번째 요점입니다. C #의 향상된 기능은 IEnumerable
개념 의 향상된 기능으로 구현되었습니다 . 자바에서하지만, 만든 향상은 람다 및 스트림의 새 기본 개념을 창조하고 또한 변환에서 상대적으로 사소한 방법을 작성하여 구현 된 Iterators
및 Iterables
스트림에, 그리고 비자의 경우도 마찬가지입니다.
따라서 IEnumerable을 Java의 Stream 개념과 비교하는 것은 불완전합니다. Java에서 결합 된 Streams and Collections API와 비교해야합니다.
Java에서 스트림은 Iterables 또는 Iterator와 동일하지 않습니다.
스트림은 반복자와 같은 방식으로 문제를 해결하도록 설계되지 않았습니다.
- 반복자는 데이터 시퀀스를 설명하는 방법입니다.
- 스트림은 일련의 데이터 변환을 설명하는 방법입니다.
를 사용하면 Iterator
데이터 값을 가져 와서 처리 한 다음 다른 데이터 값을 얻을 수 있습니다.
Streams를 사용하면 일련의 함수를 함께 연결 한 다음 입력 값을 스트림에 공급하고 결합 된 시퀀스에서 출력 값을 가져옵니다. Java 용어로 각 함수는 단일 Stream
인스턴스로 캡슐화됩니다 . Streams API를 사용하면 Stream
일련의 변환 표현식을 연결하는 방식으로 일련의 인스턴스 를 연결할 수 있습니다 .
Stream
개념 을 완성하려면 스트림을 공급할 데이터 소스와 스트림을 소비하는 터미널 기능이 필요합니다.
스트림에 값을 공급하는 방식은 실제로에서 온 것일 수 Iterable
있지만 Stream
시퀀스 자체는가 아니며 Iterable
복합 함수입니다.
A Stream
는 또한 값을 요청할 때만 작동한다는 점에서 게 으르도록 설계되었습니다.
스트림의 다음과 같은 중요한 가정과 기능에 유의하십시오.
Stream
Java의 A 는 변환 엔진이며 한 상태의 데이터 항목을 다른 상태로 변환합니다.- 스트림에는 데이터 순서 나 위치에 대한 개념이 없으며 요청한 내용 만 변환하면됩니다.
- 스트림에는 다른 스트림, 반복자, 반복 가능 항목, 콜렉션,
- 스트림을 “재설정”할 수 없습니다. “변환 재 프로그래밍”과 같습니다. 데이터 소스를 재설정하는 것이 좋습니다.
- 스트림에 논리적으로 하나의 데이터 항목 ‘비행 중’만 있습니다 (스트림이 병렬 스트림이 아닌 경우 스레드 당 1 개의 항목이 있음). 이것은 스트림에 공급 될 현재 아이템 ‘준비’이상을 가질 수있는 데이터 소스 또는 다수의 값을 집계하고 감소시켜야하는 스트림 수집기와는 무관하다.
- 스트림은 언 바운드 (무한), 데이터 소스 또는 콜렉터 (무한도 가능)에 의해서만 제한 될 수 있습니다.
- 스트림은 하나의 스트림을 필터링 한 결과 인 ‘체인 가능’이며 또 다른 스트림입니다. 스트림에 입력되고 변환 된 값은 다른 변환을 수행하는 다른 스트림에 제공 될 수 있습니다. 변환 된 상태의 데이터는 한 스트림에서 다음 스트림으로 흐릅니다. 한 스트림에서 데이터를 개입시키고 끌어 올 필요가 없습니다.
C # 비교
Java 스트림이 공급, 스트림 및 수집 시스템의 일부일뿐 아니라 스트림 및 반복자가 종종 콜렉션과 함께 사용된다고 생각할 때, 동일한 개념과 관련이있는 것은 놀라운 일이 아닙니다. 거의 모든 것이 IEnumerable
C # 의 단일 개념에 포함되어 있습니다.
IEnumerable (및 밀접한 관련 개념)의 일부는 모든 Java Iterator, Iterable, Lambda 및 Stream 개념에서 분명합니다.
Java 개념이 IEnumerable에서 더 어렵고 그 반대로 할 수있는 작은 일이 있습니다.
결론
- 여기에는 디자인 문제가 없으며 언어 간의 개념을 일치시키는 데 문제가 있습니다.
- 스트림은 다른 방식으로 문제를 해결
- 스트림은 Java에 기능을 추가합니다 (다른 방식으로 작업을 수행하지만 기능을 제거하지는 않습니다)
스트림을 추가하면 문제를 해결할 때 더 많은 선택을 할 수 있습니다. 문제를 ‘감소’, ‘감소’또는 ‘제한’하는 것이 아니라 ‘능력 강화’로 분류하는 것이 좋습니다.
Java Streams가 왜 일회용입니까?
스트림은 데이터가 아니라 함수 시퀀스이기 때문에이 질문은 잘못된 것입니다. 스트림을 공급하는 데이터 소스에 따라 데이터 소스를 재설정하고 동일하거나 다른 스트림을 공급할 수 있습니다.
실행 파이프 라인을 원하는만큼 실행할 수있는 C #의 IEnumerable과 달리 Java에서는 스트림을 한 번만 ‘반복’할 수 있습니다.
비교 IEnumerable
A와 것은 Stream
잘못이다. 말하는 데 사용하는 컨텍스트는 IEnumerable
원하는 횟수만큼 실행할 수 있으며 원하는 횟수만큼 Iterables
반복 할 수있는 Java와 비교하는 것이 가장 좋습니다 . Java Stream
는 IEnumerable
개념 의 서브 세트를 나타내며 데이터를 제공하는 서브 세트가 아니므로 ‘재실행’할 수 없습니다.
터미널 작업을 호출하면 스트림이 닫히고 사용할 수 없게됩니다. 이 ‘기능’은 많은 힘을 빼앗아갑니다.
첫 번째 진술은 어떤 의미에서는 사실입니다. ‘권력을 빼앗다’는 말은 아니다. 여전히 IEnumerables와 Streams를 비교하고 있습니다. 스트림의 터미널 작업은 for 루프의 ‘break’절과 같습니다. 원하는 경우 언제든지 필요한 데이터를 다시 제공 할 수있는 경우 다른 스트림을 자유롭게 이용할 수 있습니다. 다시 말하지만, 이 문장에 대해 IEnumerable
더 같다고 생각하면 Iterable
Java가 잘 수행합니다.
나는 그 이유가 기술적 인 것이 아니라고 생각합니다. 이 이상한 제한 뒤에 디자인 고려 사항은 무엇입니까?
그 이유는 기술적 인 것이며, 단순한 이유 때문에 Stream이 생각하는 것의 일부를 Stream합니다. 스트림 서브 세트는 데이터 공급을 제어하지 않으므로 스트림이 아닌 공급을 재설정해야합니다. 그런 맥락에서 그리 이상하지 않습니다.
빠른 정렬 예
퀵 정렬 예제에는 다음과 같은 서명이 있습니다.
IEnumerable<int> QuickSort(IEnumerable<int> ints)
입력 IEnumerable
을 데이터 소스로 취급하고 있습니다 .
IEnumerable<int> lt = ints.Where(i => i < pivot);
또한 리턴 값 IEnumerable
도 데이터의 공급이며, 이는 정렬 작업이므로 해당 공급의 순서가 중요합니다. Java Iterable
클래스가 이것에 대한 적절한 일치, 특히의 List
전문화로 간주되는 경우 Iterable
List는 순서 또는 반복이 보장되는 데이터 공급이므로 코드와 동등한 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 코드가 어떻게 데이터 소스 ( List
)를 사용 하고 다른 시점에서 개념을 스트리밍하는지, C #에서이 두 ‘인격’은 그냥로 표현 될 수 있습니다 IEnumerable
. 또한 List
기본 유형으로 사용했지만 더 일반적인을 사용할 수 있었고 Collection
작은 반복자에서 스트림으로 변환하면 더 일반적인을 사용할 수있었습니다.Iterable
답변
Stream
Spliterator
상태 기반의 가변 객체 인 주위에 빌드 됩니다. 그들은 “재설정”동작이 없으며 실제로 이러한 되감기 동작을 지원해야한다면 “많은 힘을 빼앗을 것”입니다. 어떻게 것 Random.ints()
같은 요청을 처리하기로한다?
반면에, Stream
추적 가능한 원점을 가진 s의 Stream
경우 다시 사용할 등가물 을 쉽게 구성 할 수 있습니다. 를 구성하기위한 단계를 Stream
재사용 가능한 방법으로 넣으십시오 . 이 단계를 모두 반복하는 것이 비용이 많이 드는 작업이 아니라는 점을 명심하십시오. 실제 작업은 터미널 작업으로 시작하며 실제 터미널 작업에 따라 완전히 다른 코드가 실행될 수 있습니다.
메소드를 두 번 호출하는 것이 무엇을 의미하는지 지정하는 것은 해당 메소드의 작성자에게 달려 있습니다. 수정되지 않은 배열이나 콜렉션에 대해 작성된 스트림이 수행하는 것과 동일한 순서를 정확하게 재현합니까? 임의의 정수 스트림 또는 콘솔 입력 라인 스트림 등과 같은 유사한 의미론이지만 다른 요소
그런데, 혼동을 피하기 위해, 터미널 동작 은 스트림 을 호출 할 때 와 같이 폐쇄 하는 것과 Stream
구별되는 것을 소비 한다 (이는 예를 들어에 의해 생성 된 것과 같은 관련 자원을 갖는 스트림에 필요함 ).Stream
close()
Files.lines()
많은 혼란이 비교 misguiding에서 비롯된 것으로 보인다 IEnumerable
과를 Stream
. 은 IEnumerable
실제를 제공 할 수있는 능력을 나타내는 IEnumerator
, 그래서 그 같은 Iterable
자바있다. 반대로 a Stream
는 일종의 반복자이며 이에 필적하기 IEnumerator
때문에 .NET에서 이러한 종류의 데이터 유형을 여러 번 사용할 수 있다고 주장하는 것은 잘못 IEnumerator.Reset
입니다. 지원 은 선택 사항입니다. 여기서 논의 된 예제는 새로운IEnumerable
것을 가져 오는 데 사용할 수 있고 Java 에서도 작동 한다는 사실을 사용합니다 . 당신은 새로운를 얻을 수 있습니다 . Java 개발자가에 작업 을 추가하기로 결정한 경우 실제로 비교할 수 있으며 동일한 방식으로 작동 할 수 있습니다. IEnumerator
Collection
Stream
Stream
Iterable
직접 중간 작업이 다른 작업을 반환Iterable
그러나 개발자는 이에 대해 결정하고 결정은 이 질문 에서 논의됩니다 . 가장 큰 요점은 간절한 수집 작업과 게으른 스트림 작업에 대한 혼란입니다. .NET API를 살펴보면 개인적으로 그렇습니다. IEnumerable
혼자 보는 것이 합리적으로 보이지만 특정 Collection에는 Collection을 직접 조작하는 많은 메소드와 lazy를 반환하는 많은 IEnumerable
메소드가 있지만 메소드의 특정 특성이 항상 직관적으로 인식되는 것은 아닙니다. 내가 찾은 최악의 예 (몇 분 만에)는 완전히 모순되는 동작을하면서 상속 List.Reverse()
된 이름과 정확히 일치 하는 이름입니다 (확장 방법에 대한 올바른 종말입니까?) Enumerable.Reverse()
.
물론 이것들은 두 가지 별개의 결정입니다. 첫 번째 Stream
는 Iterable
/ Collection
와 구별되는 유형 을 만들고 두 번째 Stream
는 다른 종류의 반복 가능한 것이 아니라 일종의 한 번 반복자를 만드는 것입니다. 그러나 이러한 결정은 함께 이루어졌으며이 두 결정의 분리가 고려되지 않은 경우 일 수 있습니다. .NET을 염두에두고 만들어지지 않았습니다.
실제 API 디자인 결정은 향상된 반복자 유형 인을 추가하는 것이 었습니다 Spliterator
. Spliterator
구식 Iterable
(이것이 개조 된 방식) 또는 완전히 새로운 구현에 의해 제공 될 수있다 . 그런 다음 Stream
다소 낮은 수준 Spliterator
의 고급 프런트 엔드로 추가되었습니다 . 그게 다야. 다른 디자인이 더 나을지 여부에 대해 논의 할 수도 있지만, 지금은 디자인 방식에 따라 생산적이지 않고 변경되지 않습니다.
고려해야 할 또 다른 구현 측면이 있습니다. Stream
s는 변경할 수없는 데이터 구조 가 아닙니다 . 각 중간 작업은 Stream
이전 인스턴스를 캡슐화 하는 새 인스턴스를 반환 할 수 있지만 대신 자체 인스턴스를 조작하고 자체를 반환 할 수도 있습니다 (동일한 작업에 대해 두 가지를 모두 수행 할 수는 없음). 일반적으로 알려진 예는 작업이 좋아하다 parallel
거나 unordered
하는 또 다른 단계를 추가하지만, 전체 파이프 라인을 조작하지 않음). 이러한 변경 가능한 데이터 구조를 가지고 재사용을 시도하거나 더 많은 시간을 동시에 사용하는 것은 좋지 않습니다.
완전성을 위해 다음은 Java Stream
API로 변환 된 빠른 정렬 예제 입니다. 그것은 실제로 “많은 힘을 빼앗아 가지”않는다는 것을 보여줍니다.
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);
}
실제로 열거 형을 반복 할 때마다 컴파일러는 열거자를 만듭니다. 열거자는 재사용 할 수 없습니다. 추가 호출 MoveNext
은 false를 반환하며 처음으로 재설정 할 수있는 방법이 없습니다. 숫자를 다시 반복하려면 다른 열거 자 인스턴스를 작성해야합니다.
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 스트림) 예외를 throw하도록을 작성할 수 있습니다 .
또한 열거 형 빠른 정렬은 RandomNumberStream
?
결론
따라서 가장 큰 차이점은 .NET을 사용하면 시퀀스의 요소에 액세스해야 할 때마다 백그라운드에서 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
사용 된 바와 같이 Stream
소스 상태이며, 현재의 위치를 저장한다. 우리가 이것을 재생할 때 우리 Stream
는 끝에서 다시 시작합니다.
이 문제를 해결하기위한 여러 가지 옵션이 있습니다.
-
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);
-
이것에 대한 또 다른 (약간 더 좋지만 완벽하지 않은) 해결책 은 현재 카운터를 재설정 할 수있는 용량을 포함하는 자체
ArraySpliterator
(또는 유사한Stream
소스) 를 작성하는 것 입니다. 우리가 그것을 사용하여 생성한다면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);
-
이 문제에 대한 가장 좋은 해결책은 (내 의견으로는) 새 연산자가에서 호출 될 때 파이프 라인에
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
예상대로.