जावा स्ट्रीम एक बार बंद क्यों हैं?


239

C # के विपरीत IEnumerable, जहां एक निष्पादन पाइपलाइन को जितनी बार हम चाहें, निष्पादित कर सकते हैं, जावा में एक धारा केवल एक बार 'पुनरावृत्त' हो सकती है।

टर्मिनल ऑपरेशन के लिए कोई भी कॉल धारा को बंद कर देता है, यह अनुपयोगी हो जाता है। यह This फीचर ’बहुत सारी शक्ति निकाल लेता है।

मुझे लगता है कि इसका कारण तकनीकी नहीं है । इस अजीब प्रतिबंध के पीछे क्या डिजाइन विचार थे?

संपादित करें: मैं किस बारे में बात कर रहा हूं, यह प्रदर्शित करने के लिए, C # में क्विक-सॉर्ट के निम्नलिखित कार्यान्वयन पर विचार करें:

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));
}

अब यह सुनिश्चित करने के लिए, मैं वकालत नहीं कर रहा हूं कि यह त्वरित प्रकार का एक अच्छा कार्यान्वयन है! हालांकि यह स्ट्रीम ऑपरेशन के साथ संयुक्त लैम्ब्डा अभिव्यक्ति की अभिव्यंजक शक्ति का महान उदाहरण है।

और यह जावा में नहीं किया जा सकता है! मैं एक स्ट्रीम से यह भी नहीं पूछ सकता कि क्या यह अनुपयोगी होने के बिना खाली है।


4
क्या आप एक ठोस उदाहरण दे सकते हैं जहां धारा को बंद करना "शक्ति को दूर ले जाता है"?
रोजेरियो

23
यदि आप एक बार से अधिक स्ट्रीम से डेटा का उपयोग करना चाहते हैं, तो आपको इसे एक संग्रह में डंप करना होगा। यह काफी है कि यह कैसे है है काम करने के लिए: या तो आप गणना फिर से करना धारा उत्पन्न करने के लिए है, या आप मध्यवर्ती परिणाम स्टोर करने के लिए किया है।
लुई वासरमैन

5
ठीक है, लेकिन एक ही स्ट्रीम पर एक ही संगणना फिर से करना गलत लगता है। गणना करने से पहले एक दिए गए स्रोत से एक धारा बनाई जाती है, ठीक उसी तरह जैसे कि पुनरावृत्तियों को प्रत्येक पुनरावृत्ति के लिए बनाया जाता है। मैं अभी भी एक वास्तविक ठोस उदाहरण देखना चाहूंगा; अंत में, मैं शर्त लगाता हूं कि प्रत्येक समस्या को हल करने के लिए एक स्वच्छ तरीका है, एक बार धाराओं के साथ, सी # के enumerables के साथ एक संगत तरीका मौजूद है।
रोजेरियो 20

2
यह मेरे लिए पहली बार भ्रमित करने वाला था, क्योंकि मुझे लगा कि यह सवाल C # s IEnumerableकी धाराओं से संबंधित होगाjava.io.*
SpaceTrucker

9
ध्यान दें कि IE # में कई बार IEnumerable का उपयोग करना एक नाजुक पैटर्न है, इसलिए प्रश्न का आधार थोड़ा त्रुटिपूर्ण हो सकता है। IEnumerable के कई कार्यान्वयन इसे अनुमति देते हैं लेकिन कुछ नहीं करते हैं! कोड विश्लेषण उपकरण आपको ऐसा काम करने के खिलाफ चेतावनी देते हैं।
सैंडर

जवाबों:


368

मेरे पास स्ट्रीम API के शुरुआती डिज़ाइन से कुछ याद हैं जो डिज़ाइन तर्क पर कुछ प्रकाश डाल सकते हैं।

2012 में वापस, हम लैम्ब्डा को भाषा में जोड़ रहे थे, और हम एक संग्रह-उन्मुख या "बल्क डेटा" ऑपरेशंस का सेट चाहते थे, जिसे लैम्ब्डा का उपयोग करके क्रमादेशित किया गया, जो समानता की सुविधा प्रदान करेगा। इस बिंदु द्वारा एक साथ lazily संचालन का विचार अच्छी तरह से स्थापित किया गया था। हम परिणामों को संग्रहीत करने के लिए मध्यवर्ती संचालन भी नहीं चाहते थे।

हमें जिन मुख्य मुद्दों पर निर्णय लेने की आवश्यकता थी, वे थे कि श्रृंखला की वस्तुएं एपीआई में कैसी दिखती थीं और उन्होंने डेटा स्रोतों को कैसे झुका दिया। स्रोत अक्सर संग्रह होते थे, लेकिन हम एक फ़ाइल या नेटवर्क से आने वाले डेटा का समर्थन करना चाहते थे, या एक यादृच्छिक संख्या जनरेटर से, जैसे-पर-मक्खी उत्पन्न डेटा।

डिजाइन पर मौजूदा काम के कई प्रभाव थे। अधिक प्रभावशाली लोगों में Google का अमरूद पुस्तकालय और स्काला संग्रह पुस्तकालय थे। (यदि कोई भी अमरूद के प्रभाव के बारे में आश्चर्यचकित है, तो ध्यान दें कि केविन बॉरिलिन , अमरूद के मुख्य विकासकर्ता थे JSR-335 लैंबडा विशेषज्ञ समूह में थे।) स्काला संग्रह में, हमने मार्टिन ओडस्की की इस बात को विशेष रुचि के रूप में पाया: भविष्य- प्रूफिंग स्काला कलेक्शंस: म्यूटेबल से पर्सेंटेज टू पैरेलल । (स्टैनफोर्ड EE380, 2011 1 जून)

उस समय हमारा प्रोटोटाइप डिजाइन चारों ओर आधारित था Iterable। परिचित ऑपरेशन filter, mapऔर इसके बाद के विस्तार (डिफ़ॉल्ट) तरीके थे Iterable। कॉलिंग ने एक श्रृंखला में एक ऑपरेशन जोड़ा और दूसरे को वापस कर दियाIterable । एक टर्मिनल ऑपरेशन की तरह श्रृंखला को स्रोत तक countबुलाया जाएगा iterator(), और संचालन प्रत्येक चरण के Iterator के भीतर लागू किया गया था।

चूंकि ये Iterables हैं, आप कॉल कर सकते हैं iterator() विधि को एक से अधिक बार । फिर क्या होना चाहिए?

यदि स्रोत एक संग्रह है, तो यह ज्यादातर ठीक काम करता है। संग्रह Iterable हैं, और प्रत्येक कॉल iterator()एक अलग Iterator इंस्टेंस बनाने के लिए है जो किसी भी अन्य सक्रिय उदाहरणों से स्वतंत्र है, और प्रत्येक संग्रह को स्वतंत्र रूप से ट्रैवर्स करता है। महान।

अब क्या होगा अगर स्रोत एक-शॉट है, जैसे फ़ाइल से लाइनें पढ़ना? हो सकता है कि पहले Iterator को सभी मान प्राप्त हों, लेकिन दूसरा और बाद वाला खाली होना चाहिए। हो सकता है कि मूल्यों को इटरेटर के बीच इंटरलेय किया जाना चाहिए। या हो सकता है कि प्रत्येक Iterator को सभी समान मान मिले। फिर, क्या होगा यदि आपके पास दो पुनरावृत्तियाँ हों और एक दूसरे से आगे निकल जाए? जब तक वे पढ़ते हैं तब तक किसी को दूसरे Iterator में मानों को बफर करना होगा। इससे भी बदतर, क्या होगा यदि आप एक Iterator प्राप्त करें और सभी मान पढ़ें, और उसके बाद ही दूसरा Iterator प्राप्त करें। अब मूल्य कहां से आते हैं? क्या उन सभी के लिए एक आवश्यकता है कि वे बफर्ड बनें बस में मामला किसी एक दूसरे इटरेटर चाहता है?

स्पष्ट रूप से, एक-शॉट स्रोत पर कई Iterators की अनुमति देने से बहुत सारे प्रश्न उठते हैं। हमारे पास उनके लिए अच्छे उत्तर नहीं थे। यदि आप कॉल करते हैं तो क्या होता है, इसके लिए हम लगातार, पूर्वानुमानित व्यवहार चाहते थेiterator() दो बार । इसने हमें कई ट्रैवर्स को रोकने की ओर धकेल दिया, जिससे पाइपलाइनों को एक-शॉट बना दिया गया।

हमने अन्य लोगों को इन मुद्दों पर टकराते हुए भी देखा। JDK में, अधिकांश Iterables संग्रह या संग्रह जैसी वस्तुएं हैं, जो कई ट्रैवर्सल की अनुमति देती हैं। यह कहीं भी निर्दिष्ट नहीं है, लेकिन एक अलिखित उम्मीद थी कि Iterables एकाधिक ट्रैवर्सल की अनुमति देता है। एक उल्लेखनीय अपवाद NIO DirectoryStream है इंटरफ़ेस है। इसकी विशिष्टता में यह दिलचस्प चेतावनी शामिल है:

जबकि DirectoryStream Iterable का विस्तार करता है, यह एक सामान्य उद्देश्य Iterable नहीं है क्योंकि यह केवल एक Iterator का समर्थन करता है; दूसरे या बाद के पुनरावृत्ति प्राप्त करने के लिए पुनरावृत्ति विधि को लागू करना IllegalStateException को फेंकता है।

[मूल में बोल्ड]

यह काफी असामान्य और अप्रिय लग रहा था कि हम नए Iterables का एक पूरा गुच्छा नहीं बनाना चाहते थे जो एक बार ही हो सकता है। इसने हमें Iterable का उपयोग करने से दूर कर दिया।

इस समय के बारे में, ब्रूस एकेल के एक लेख में दिखाई दिया कि उन्होंने स्काला के साथ होने वाली परेशानी का वर्णन किया है। उन्होंने यह कोड लिखा होगा:

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

यह बहुत सीधा है। यह Registrantवस्तुओं में पाठ की पंक्तियों को पार करता है और उन्हें दो बार प्रिंट करता है। सिवाय इसके कि यह वास्तव में केवल एक बार उन्हें प्रिंट करता है। यह पता चला है कि उसने सोचा कि registrantsयह एक संग्रह था, जब वास्तव में यह एक पुनरावृत्त है। foreachएक खाली पुनरावृत्ति का सामना करने के लिए दूसरी कॉल , जिसमें से सभी मान समाप्त हो गए हैं, इसलिए यह कुछ भी प्रिंट नहीं करता है।

इस तरह के अनुभव ने हमें आश्वस्त किया कि यदि कई ट्रैवर्सल का प्रयास किया जाता है, तो स्पष्ट रूप से अनुमानित परिणाम होना बहुत महत्वपूर्ण था। इसने डेटा संग्रह करने वाले वास्तविक संग्रहों से आलसी पाइपलाइन जैसी संरचनाओं के बीच अंतर करने के महत्व पर भी प्रकाश डाला। यह बदले में नए स्ट्रीम इंटरफ़ेस में आलसी पाइपलाइन संचालन को अलग करने और केवल उत्सुक, म्यूटेटिव संचालन को सीधे संग्रह में रखता है। ब्रायन गोएट्ज ने इसके लिए औचित्य समझाया है।

संग्रह-आधारित पाइपलाइनों के लिए कई ट्रैवर्सल की अनुमति देने के बारे में क्या है लेकिन इसे गैर-संग्रह-आधारित पाइपलाइनों के लिए अस्वीकार करना है? यह असंगत है, लेकिन यह समझदार है। यदि आप नेटवर्क से मान पढ़ रहे हैं, तो अवश्य आप उन्हें फिर से नहीं बदल सकते। यदि आप उन्हें कई बार पार करना चाहते हैं, तो आपको उन्हें एक संग्रह में स्पष्ट रूप से खींचना होगा।

लेकिन चलो संग्रह-आधारित पाइपलाइनों से कई ट्रैवर्सल की अनुमति दें। मान लीजिए कि आपने ऐसा किया:

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

( intoऑपरेशन अब वर्तनी है collect(toList())।)

यदि स्रोत एक संग्रह है, तो पहली into()कॉल स्रोत में वापस Iterators की एक श्रृंखला बनाएगी, पाइपलाइन संचालन को निष्पादित करेगी और परिणामों को गंतव्य में भेज देगी। दूसरी कॉल into()Iterators की एक और श्रृंखला बना सकती है और पाइपलाइन संचालन को फिर से निष्पादित कर सकती है । यह स्पष्ट रूप से गलत नहीं है, लेकिन इसमें प्रत्येक तत्व के लिए दूसरी बार सभी फ़िल्टर और मानचित्र संचालन करने का प्रभाव है। मुझे लगता है कि कई प्रोग्रामर इस व्यवहार से हैरान रह गए होंगे।

जैसा कि मैंने ऊपर उल्लेख किया है, हम अमरूद डेवलपर्स से बात कर रहे थे। उनके पास एक बढ़िया चीज़ है आइडिया ग्रेवयार्ड जहां वे उन विशेषताओं का वर्णन करते हैं जो उन्होंने कारणों के साथ लागू नहीं करने का फैसला किया । आलसी संग्रह का विचार बहुत अच्छा लगता है, लेकिन यहां उनके बारे में क्या कहना है। एक List.filter()ऑपरेशन पर विचार करें जो एक रिटर्न देता है List:

यहां सबसे बड़ी चिंता यह है कि बहुत सारे ऑपरेशन महंगे, रैखिक-समय के प्रस्ताव बन जाते हैं। यदि आप किसी सूची को फ़िल्टर करना चाहते हैं और एक सूची वापस प्राप्त करना चाहते हैं, और केवल एक संग्रह या एक Iterable नहीं है, तो आप उपयोग कर सकते हैं ImmutableList.copyOf(Iterables.filter(list, predicate)), जो "सामने बताता है" यह क्या कर रहा है और यह कितना महंगा है।

एक विशिष्ट उदाहरण लेने के लिए, सूची पर get(0)या उसकी लागत क्या है size()? आमतौर पर इस्तेमाल की जाने वाली कक्षाओं के लिए ArrayList, वे O (1) हैं। लेकिन अगर आप इनमें से किसी एक को लाज़िली-फ़िल्टर्ड सूची में कहते हैं, तो उसे फ़िल्टरिंग को बैकिंग सूची पर चलाना होगा, और अचानक ये सभी ऑपरेशन O (n) हैं। इससे भी बदतर, यह है कि हर ऑपरेशन पर समर्थन सूची को पीछे छोड़ना पड़ता है ।

यह हमें बहुत आलसी लग रहा था । कुछ कार्यों को सेट करना और वास्तविक निष्पादन को स्थगित करना है जब तक कि आप "गो" न करें। यह इस तरह से चीजों को स्थापित करने के लिए एक और है जो संभावित रूप से बड़ी मात्रा में पुनर्संयोजन को छुपाता है।

गैर-रैखिक या "नो-रीयूज़" धाराओं को अस्वीकार करने के प्रस्ताव में, पॉल सैंडोज़ ने उन्हें "अप्रत्याशित या भ्रमित करने वाले परिणामों" को जन्म देने के रूप में अनुमति देने के संभावित परिणामों का वर्णन किया । उन्होंने यह भी उल्लेख किया कि समानांतर निष्पादन चीजों को और भी पेचीदा बना देगा। अंत में, मैं जोड़ूंगा कि साइड इफेक्ट के साथ एक पाइपलाइन ऑपरेशन मुश्किल और अस्पष्ट बग पैदा करेगा यदि ऑपरेशन को अप्रत्याशित रूप से कई बार निष्पादित किया गया था, या प्रोग्रामर की अपेक्षा कम से कम एक अलग संख्या में। (लेकिन जावा प्रोग्रामर साइड इफेक्ट्स के साथ लैम्ब्डा एक्सप्रेशन नहीं लिखते हैं, क्या वे करते हैं?

तो यह जावा 8 स्ट्रीम एपीआई डिजाइन के लिए मूल तर्क है जो एक-शॉट ट्रैवर्सल की अनुमति देता है और इसके लिए कड़ाई से रैखिक (कोई शाखा नहीं) पाइपलाइन की आवश्यकता होती है। यह कई अलग-अलग स्ट्रीम स्रोतों में निरंतर व्यवहार प्रदान करता है, यह स्पष्ट रूप से आलसी को उत्सुक संचालन से अलग करता है, और यह एक सीधा निष्पादन मॉडल प्रदान करता है।


के संबंध में IEnumerable, मैं C # और .NET के एक विशेषज्ञ से बहुत दूर हूं, इसलिए यदि मैं कोई गलत निष्कर्ष निकालता हूं, तो मुझे सुधारा जाना (धीरे ​​से) सही होगा। हालांकि, यह प्रकट होता है, जो IEnumerableकई ट्रैवर्सल को अलग-अलग स्रोतों के साथ अलग-अलग व्यवहार करने की अनुमति देता है; और यह नेस्टेड IEnumerableसंचालन की एक शाखा संरचना की अनुमति देता है , जिसके परिणामस्वरूप कुछ महत्वपूर्ण पुनर्संयोजन हो सकता है। जबकि मैं सराहना करता हूं कि विभिन्न सिस्टम अलग-अलग ट्रेडऑफ बनाते हैं, ये दो विशेषताएं हैं जो हमने जावा 8 स्ट्रीम एपीआई के डिजाइन में बचने की मांग की थीं।

ओपी द्वारा दिया गया क्विकसॉर्ट उदाहरण दिलचस्प है, हैरान करने वाला है, और मुझे यह कहते हुए खेद है, कुछ भयावह है। कॉलिंग QuickSortएक लेता है IEnumerableऔर एक रिटर्न देता है IEnumerable, इसलिए कोई भी छंटनी वास्तव में तब तक नहीं की जाती है जब तक कि अंतिम IEnumerableट्रैवर्स न हो जाए। हालांकि, कॉल क्या करता है, इसका निर्माण एक पेड़ की संरचना है, IEnumerablesजो विभाजन को दर्शाता है कि एस्कॉर्ट वास्तव में ऐसा नहीं कर रहा है। (यह आलसी संगणना है, सब के बाद।) यदि स्रोत में एन तत्व हैं, तो पेड़ अपने व्यापक स्तर पर एन तत्व चौड़ा होगा, और यह गहरा (एन) स्तर होगा।

यह मुझे लगता है - और एक बार फिर, मैं सी # या .NET विशेषज्ञ नहीं हूं - कि यह कुछ सहज-दिखने वाली कॉल का कारण होगा, जैसे कि पिवट चयन ints.First(), वे देखने के मुकाबले अधिक महंगा होना। पहले स्तर पर, निश्चित रूप से, यह ओ (1) है। लेकिन पेड़ में एक विभाजन पर विचार करें, दाएं हाथ के किनारे पर। इस विभाजन के पहले तत्व की गणना करने के लिए, पूरे स्रोत का पता लगाया जाना चाहिए, एक O (N) ऑपरेशन। लेकिन चूँकि ऊपर के विभाजन आलसी हैं, इसलिए उन्हें ओ (lg N) तुलनाओं की आवश्यकता होती है, उन्हें पुन: प्रतिष्ठित किया जाना चाहिए। तो धुरी का चयन करना एक O (N lg N) ऑपरेशन होगा, जो एक पूरे सॉर्ट की तरह महंगा है।

लेकिन हम वास्तव में तब तक नहीं सुलझते जब तक कि हम वापस लौटे नहीं IEnumerable। मानक क्विकसॉर्ट एल्गोरिथ्म में, विभाजन का प्रत्येक स्तर विभाजन की संख्या को दोगुना कर देता है। प्रत्येक विभाजन केवल आधा आकार है, इसलिए प्रत्येक स्तर O (N) जटिलता पर रहता है। विभाजन का वृक्ष हे (lg N) ऊँचा है, इसलिए कुल कार्य O (N lg N) है।

आलसी IEnumerables के पेड़ के साथ, पेड़ के नीचे N विभाजन हैं। प्रत्येक विभाजन की गणना के लिए एन तत्वों के एक ट्रावेल की आवश्यकता होती है, जिनमें से प्रत्येक को पेड़ की तुलना में एलजी (एन) की आवश्यकता होती है। पेड़ के तल पर सभी विभाजनों की गणना करने के लिए, O (N ^ 2 lg N) तुलना की आवश्यकता होती है।

(क्या यह सही है? मैं शायद ही इस पर विश्वास कर सकता हूं। कोई कृपया मेरे लिए यह जाँच करें।)

किसी भी मामले में, यह वास्तव में अच्छा है कि IEnumerableइस तरह का उपयोग गणना की जटिल संरचनाओं के निर्माण के लिए किया जा सकता है। लेकिन अगर यह कम्प्यूटेशनल जटिलता को बढ़ाता है जितना मुझे लगता है कि यह करता है, तो ऐसा लगेगा कि इस तरह से प्रोग्रामिंग करना कुछ ऐसा है जिससे बचा जाना चाहिए जब तक कि कोई बहुत सावधान न हो।


35
सबसे पहले, महान और गैर-कृपालु उत्तर के लिए धन्यवाद! यह अब तक का सबसे सटीक और बिंदु स्पष्टीकरण है। जहां तक ​​क्विकसॉर्ट उदाहरण जाता है, ऐसा लगता है कि आप ints के बारे में सही हैं। पुनरावृत्ति के स्तर के रूप में बढ़ते हुए। मेरा मानना ​​है कि यह आसानी से 'gt' और 'lt' की गणना उत्सुकता से किया जा सकता है (ToArray के साथ परिणाम एकत्र करके)। यह कहा जा रहा है, यह निश्चित रूप से आपकी बात का समर्थन करता है कि प्रोग्रामिंग की इस शैली में अप्रत्याशित प्रदर्शन मूल्य हो सकता है। (दूसरी टिप्पणी में जारी रखें)
विटाली

18
दूसरी ओर, C # के साथ अपने अनुभव से (अधिक 5 वर्ष) मैं बता सकता हूं कि 'अनावश्यक' संगणनाओं को जड़ से उखाड़ फेंकना इतना कठिन नहीं है कि एक बार जब आप किसी प्रदर्शन के मुद्दे से टकरा जाते हैं (या मना कर दिया जाता है, अगर किसी ने अप्राप्य बना दिया और एक परिचय दिया पक्ष वहाँ प्रभावित)। यह सिर्फ मुझे लगता था कि संभावनाओं की तरह सी # की कीमत पर, एपीआई की शुद्धता सुनिश्चित करने के लिए बहुत अधिक समझौता किया गया था। आपने निश्चित रूप से मुझे अपनी बात को समायोजित करने में मदद की है।
विटाली

7
@Vitaliy विचारों की निष्पक्ष सोच के लिए धन्यवाद। मैंने इस उत्तर की जांच करने और लिखने से C # और .NET के बारे में थोड़ा सीखा।
स्टुअर्ट मार्क्स

10
छोटी टिप्पणी: ReSharper एक विजुअल स्टूडियो एक्सटेंशन है जो C # के साथ मदद करता है। उपरोक्त QuickSort कोड के साथ ReSharper प्रत्येक उपयोग के लिएints एक चेतावनी जोड़ता है : "IEnumerable के संभावित कई गणन"। एक IEenumerableसे अधिक बार उपयोग करना संदेहास्पद है और इससे बचना चाहिए। मैं इस प्रश्न (जो मैंने उत्तर दिया है) की ओर भी इशारा करता हूं, जो कि .net दृष्टिकोण (खराब प्रदर्शन के अलावा) के साथ कुछ केवेट्स दिखाता है: सूची <T> और IEnumerable अंतर
कोबी

4
@ कोबी बहुत दिलचस्प है कि ReSharper में इस तरह की चेतावनी है। आपके उत्तर के लिए सूचक के लिए धन्यवाद। मुझे C # / नेट का पता नहीं है, इसलिए मुझे इसे सावधानी से चुनना होगा, लेकिन यह ऊपर बताए गए डिज़ाइन चिंताओं के समान मुद्दों को प्रदर्शित करता है।
स्टुअर्ट मार्क्स

122

पृष्ठभूमि

जबकि प्रश्न सरल प्रतीत होता है, वास्तविक उत्तर के लिए कुछ पृष्ठभूमि की आवश्यकता होती है। यदि आप निष्कर्ष पर जाना चाहते हैं, तो नीचे स्क्रॉल करें ...

अपनी तुलना बिंदु उठाओ - बुनियादी कार्यक्षमता

बुनियादी अवधारणाओं का उपयोग करते हुए, C # की IEnumerableअवधारणा जावाIterable से अधिक निकटता से संबंधित है , जो आप जितने चाहें उतने Iterators बनाने में सक्षम हैं। IEnumerablesसृजन करनाIEnumerators । जावा का Iterableनिर्माणIterators

प्रत्येक अवधारणा का इतिहास समान है, दोनों में IEnumerable और Iterableडेटा संग्रह के सदस्यों पर 'प्रत्येक' के लिए स्टाइल की अनुमति देने के लिए एक मूल प्रेरणा है। यह एक निरीक्षण है, क्योंकि वे दोनों केवल इससे अधिक की अनुमति देते हैं, और वे अलग-अलग प्रगति के माध्यम से उस स्तर पर पहुंचे, लेकिन यह एक महत्वपूर्ण सामान्य विशेषता है।

आइए उस सुविधा की तुलना करें: दोनों भाषाओं में, यदि कोई वर्ग IEnumerable/ को लागू करता है Iterable, तो उस वर्ग को कम से कम एक विधि (C # के लिए, यह GetEnumeratorऔर यह जावा के लिए है iterator()) को लागू करना चाहिए । प्रत्येक मामले में, उस ( IEnumerator/ Iterator) से लौटा उदाहरण आपको डेटा के वर्तमान और बाद के सदस्यों तक पहुंचने की अनुमति देता है। इस सुविधा का उपयोग प्रत्येक भाषा वाक्य रचना में किया जाता है।

अपनी तुलना बिंदु उठाओ - बढ़ी हुई कार्यक्षमता

IEnumerableC # में कई अन्य भाषा सुविधाओं ( अधिकतर लिनाक से संबंधित ) की अनुमति देने के लिए विस्तारित किया गया है । जोड़ी गई विशेषताओं में चयन, अनुमान, एकत्रीकरण आदि शामिल हैं। इन एक्सटेंशनों में SQL और संबंधपरक डेटाबेस अवधारणाओं के समान सेट-थ्योरी में उपयोग से एक मजबूत प्रेरणा है।

जावा 8 में स्ट्रीम और लैम्बडा का उपयोग करके कार्यात्मक प्रोग्रामिंग की एक डिग्री को सक्षम करने के लिए कार्यक्षमता भी जोड़ी गई है। ध्यान दें कि जावा 8 स्ट्रीम मुख्य रूप से सेट सिद्धांत से प्रेरित नहीं हैं, लेकिन कार्यात्मक प्रोग्रामिंग द्वारा। बावजूद, बहुत सारे समानताएं हैं।

तो, यह दूसरा बिंदु है। C # की गई एन्हांसमेंट्स को एन्हांसमेंट के रूप में लागू किया गया थाIEnumerable अवधारणा में । जावा में, हालांकि, बनाया संवर्द्धन lambdas और स्ट्रीम का नया आधार अवधारणाओं बनाने, और फिर भी से परिवर्तित करने के लिए एक अपेक्षाकृत मामूली रास्ता बनाने के द्वारा लागू किया गया Iteratorsऔर Iterablesस्ट्रीम करने के लिए, और वीजा प्रतिकूल।

इसलिए, IEnumerable की जावा की स्ट्रीम अवधारणा से तुलना करना अधूरा है। आपको जावा में संयुक्त धाराओं और संग्रह एपीआई की तुलना करने की आवश्यकता है।

जावा में, स्ट्रीम Iterables या Iterators के समान नहीं हैं

धाराओं को उसी तरह से हल करने के लिए डिज़ाइन नहीं किया गया है जैसे कि पुनरावृत्तियाँ हैं:

  • Iterators डेटा के अनुक्रम का वर्णन करने का एक तरीका है।
  • स्ट्रीम डेटा ट्रांसफ़ॉर्मेशन के अनुक्रम का वर्णन करने का एक तरीका है।

एक साथ Iterator , आपको एक डेटा मान मिलता है, इसे संसाधित करें और फिर एक और डेटा मान प्राप्त करें।

धाराओं के साथ, आप एक साथ फ़ंक्शंस के अनुक्रम को चेन करते हैं, फिर आप स्ट्रीम में एक इनपुट वैल्यू फीड करते हैं, और संयुक्त अनुक्रम से आउटपुट वैल्यू प्राप्त करते हैं। ध्यान दें, जावा शब्दों में, प्रत्येक फ़ंक्शन एक ही Streamउदाहरण में समझाया गया है । स्ट्रीम API आपको एक अनुक्रम लिंक करने की अनुमति देता हैStream एक तरह उदाहरणों के जो परिवर्तन अभिव्यक्तियों के अनुक्रम को जंजीर बनाता है।

को पूरा करने के लिए Streamअवधारणा , आपको स्ट्रीम को खिलाने के लिए डेटा का स्रोत चाहिए, और एक टर्मिनल फ़ंक्शन जो स्ट्रीम का उपभोग करता है।

जिस तरह से आप मूल्यों को धारा में खिलाते हैं वह वास्तव में एक से हो सकता है Iterable, लेकिन Streamअनुक्रम ही नहीं हैIterable , वह वास्तव में ए , यह एक कंपाउंड फंक्शन है।

A Streamका अर्थ आलसी होना भी है, इस अर्थ में कि यह तभी काम करता है जब आप इससे मूल्य का अनुरोध करते हैं।

धाराओं की इन महत्वपूर्ण मान्यताओं और विशेषताओं पर ध्यान दें:

  • Stream जावा में, एक परिवर्तन इंजन है यह, एक राज्य में एक डेटा आइटम बदल देती है दूसरे राज्य में किया जा रहा है।
  • धाराओं में डेटा ऑर्डर या स्थिति की कोई अवधारणा नहीं है, बस जो कुछ भी उनसे पूछा जाता है, उसे रूपांतरित करें।
  • धाराओं को कई स्रोतों से डेटा की आपूर्ति की जा सकती है, जिसमें अन्य धाराएँ, Iterators, Iterables, Collections शामिल हैं।
  • आप एक धारा को "रीसेट" नहीं कर सकते, जो "परिवर्तन को पुन: उत्पन्न करना" जैसा होगा। डेटा स्रोत को रीसेट करना संभवतः वही है जो आप चाहते हैं।
  • किसी भी समय (जब तक कि धारा एक समानांतर धारा न हो, जिस बिंदु पर प्रति थ्रेड 1 आइटम हो) वहां तार्किक रूप से केवल 1 डेटा आइटम 'उड़ान में' होता है। यह डेटा स्रोत से स्वतंत्र है जो धारा को आपूर्ति करने के लिए 'तैयार' से अधिक हो सकता है, या स्ट्रीम कलेक्टर जिसे कई मूल्यों को एकत्र करने और कम करने की आवश्यकता हो सकती है।
  • धाराएँ केवल डेटा स्रोत या कलेक्टर (जो अनंत भी हो सकती हैं) द्वारा अनबाउंड (अनंत), सीमित हो सकती हैं।
  • धाराएँ 'चेनेबल' हैं, जो एक स्ट्रीम को फ़िल्टर करने का आउटपुट है, दूसरी स्ट्रीम है। किसी धारा द्वारा बदले जा सकने वाले और बदले जाने वाले मूल्यों को दूसरी धारा में आपूर्ति किया जा सकता है जो एक अलग परिवर्तन करता है। डेटा, इसकी रूपांतरित अवस्था में एक धारा से दूसरी धारा में बहती है। आपको डेटा को एक स्ट्रीम से हस्तक्षेप करने और खींचने की आवश्यकता नहीं है और इसे अगले पर प्लग करें।

सी # तुलना

जब आप मानते हैं कि जावा स्ट्रीम एक सप्लाई, स्ट्रीम और कलेक्ट सिस्टम का एक हिस्सा है, और स्ट्रीम्स और इटरेटर्स को अक्सर कलेक्शंस के साथ एक साथ उपयोग किया जाता है, तो यह कोई आश्चर्य नहीं है कि यह उन्हीं अवधारणाओं से संबंधित है, जो कठिन हैं लगभग सभी एक ही में एम्बेडेड है IEnumerable C # में अवधारणा से जुड़े हैं।

IE के सभी भाग (और निकट संबंधी अवधारणाएं) जावा Iterator, Iterable, Lambda और स्ट्रीम अवधारणाओं के सभी में स्पष्ट हैं।

छोटी चीजें हैं जो जावा अवधारणाएं कर सकती हैं जो IEnumerable, और वीज़ा-वर्सा में कठिन हैं।


निष्कर्ष

  • यहां कोई डिज़ाइन समस्या नहीं है, बस भाषाओं के बीच अवधारणाओं के मिलान में समस्या है।
  • धाराएँ समस्याओं को एक अलग तरीके से हल करती हैं
  • धाराएँ जावा में कार्यक्षमता जोड़ती हैं (वे काम करने का एक अलग तरीका जोड़ती हैं, वे कार्यक्षमता को दूर नहीं ले जाती हैं)

धाराओं को जोड़ना आपको समस्याओं को हल करते समय अधिक विकल्प देता है, जो कि 'शक्ति बढ़ाने' के रूप में वर्गीकृत करना उचित है, न कि 'कम करना', 'दूर करना', या इसे 'सीमित' करना।

जावा स्ट्रीम एक बार बंद क्यों हैं?

यह सवाल गुमराह करने वाला है, क्योंकि स्ट्रीम फंक्शन सीक्वेंस हैं, डेटा नहीं। स्ट्रीम को खिलाने वाले डेटा स्रोत के आधार पर, आप डेटा स्रोत को रीसेट कर सकते हैं, और उसी, या अलग स्ट्रीम को फीड कर सकते हैं।

C # के IEnumerable के विपरीत, जहां एक निष्पादन पाइपलाइन को जितनी बार चाहें उतनी बार निष्पादित किया जा सकता है, जावा में एक स्ट्रीम को केवल एक बार 'iterated' किया जा सकता है।

एक तुलना IEnumerableएक को Streamगुमराह है। आप जिस संदर्भ का उपयोग करने के लिए कह रहे हैं IEnumerableउसे जितनी बार चाहें निष्पादित किया जा सकता है, जावा की तुलना में सबसे अच्छा है Iterables, जिसे आप जितनी बार चाहें उतनी बार पुनरावृत्त कर सकते हैं। एक जावा अवधारणा Streamका सबसेट का प्रतिनिधित्व करता है IEnumerable, और डेटा की आपूर्ति करने वाला सबसेट नहीं, और इस प्रकार 'पुनर्मिलन' नहीं हो सकता।

टर्मिनल ऑपरेशन के लिए कोई भी कॉल धारा को बंद कर देता है, यह अनुपयोगी हो जाता है। यह This फीचर ’बहुत सारी शक्ति निकाल लेता है।

पहला कथन सत्य है, एक अर्थ में। 'सत्ता छीन लेता है' बयान नहीं है। आप अभी भी इसे IEnumerables स्ट्रीम की तुलना कर रहे हैं। स्ट्रीम में टर्मिनल ऑपरेशन लूप के लिए 'ब्रेक' क्लॉज की तरह है। आप हमेशा एक और स्ट्रीम करने के लिए स्वतंत्र हैं, यदि आप चाहते हैं, और यदि आप आवश्यक डेटा की फिर से आपूर्ति कर सकते हैं। फिर, यदि आप इस कथन के लिए IEnumerableअधिक पसंद Iterableकरते हैं, तो जावा इसे ठीक करता है।

मुझे लगता है कि इसका कारण तकनीकी नहीं है। इस अजीब प्रतिबंध के पीछे क्या डिजाइन विचार थे?

कारण तकनीकी है, और साधारण कारण के लिए कि यह क्या है का एक सबसेट स्ट्रीम करता है। स्ट्रीम सब्मिट डेटा आपूर्ति को नियंत्रित नहीं करता है, इसलिए आपको आपूर्ति को रीसेट करना चाहिए, स्ट्रीम को नहीं। उस संदर्भ में, यह इतना अजीब नहीं है।

QuickSort उदाहरण

आपके त्वरित उदाहरण में हस्ताक्षर हैं:

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

आप इनपुट IEnumerableको डेटा स्रोत के रूप में मान रहे हैं :

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

इसके अतिरिक्त, वापसी मूल्य IEnumerableभी है, जो डेटा की आपूर्ति है, और चूंकि यह एक सॉर्ट ऑपरेशन है, इसलिए उस आपूर्ति का क्रम महत्वपूर्ण है। यदि आप जावा Iterableवर्ग को इसके लिए उपयुक्त मेल मानते हैं , विशेष रूप से इसका Listविशेषज्ञता Iterable, क्योंकि सूची डेटा की आपूर्ति है जिसमें एक गारंटीकृत आदेश या पुनरावृत्ति है, तो आपके कोड के बराबर जावा कोड होगा:

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));
}    

ध्यान दें कि एक बग (जिसे मैंने पुन: पेश किया है), इसमें सॉर्ट डुप्लिकेट मानों को इनायत से नहीं संभालता है, यह एक 'यूनिक वैल्यू' सॉर्ट है।

यह भी ध्यान दें कि कैसे जावा कोड डेटा स्रोत ( List) का उपयोग करता है , और विभिन्न बिंदुओं पर अवधारणाओं को स्ट्रीम करता है, और यह कि C # में उन दो 'व्यक्तित्व' को केवल व्यक्त किया जा सकता है IEnumerable। हालाँकि, मेरे पास Listआधार प्रकार के रूप में उपयोग किया जाता है, मैं और अधिक सामान्य उपयोग कर सकता था Collection, और एक छोटे से इटर्-से-स्ट्रीम रूपांतरण के साथ, मैं और भी सामान्य उपयोग कर सकता थाIterable


9
यदि आप एक धारा को 'पुनरावृत्त' करने की सोच रहे हैं, तो आप इसे गलत कर रहे हैं। एक धारा परिवर्तनों की श्रृंखला में समय में एक विशेष बिंदु पर डेटा की स्थिति का प्रतिनिधित्व करती है। डेटा एक स्ट्रीम स्रोत में सिस्टम में प्रवेश करता है, फिर एक धारा से दूसरी में बदल जाता है, बदलती अवस्था के रूप में यह जाता है, जब तक कि इसे एकत्र नहीं किया जाता है, कम किया जाता है, या अंत में डंप किया जाता है। ए Streamएक पॉइंट-इन-टाइम कॉन्सेप्ट है, न कि 'लूप ऑपरेशन' .... (
कॉन्टेस्ट

7
स्ट्रीम के साथ, आपके पास X जैसी दिखने वाली स्ट्रीम में प्रवेश करने वाला डेटा है, और Y की तरह दिखने वाली स्ट्रीम से बाहर निकल रहा है। एक फ़ंक्शन है जो स्ट्रीम करता है जो कि परिवर्तन करता है f(x)। स्ट्रीम फ़ंक्शन को एनकैप्सुलेट करता है, यह उस डेटा को एनकैप्सुलेट नहीं करता है जो प्रवाहित होता है
रॉल्फल

4
IEnumerableयादृच्छिक मूल्यों की आपूर्ति कर सकते हैं, अनबाउंड हो सकते हैं, और डेटा मौजूद होने से पहले सक्रिय हो सकते हैं।
आर्टुरो टॉरेस सांचेज़

6
@ वैतालि: कई विधियाँ जो IEnumerable<T>एक परिमित संग्रह का प्रतिनिधित्व करने की अपेक्षा करती हैं, जो कई बार पुनरावृत्त हो सकती हैं। कुछ चीजें जो IEnumerable<T>कि चलने-फिरने योग्य हैं, लेकिन उन शर्तों को पूरा नहीं करती हैं क्योंकि कोई भी अन्य मानक इंटरफ़ेस बिल को फिट नहीं करता है, लेकिन कई बार पुनरावृत्त होने की उम्मीद करने वाले तरीकों से परिमित संग्रह की उम्मीद की जा सकती है, अगर उन शर्तों का पालन नहीं किया जाता है, तो इसे दुर्घटनाग्रस्त होने की संभावना है ।
सुपरकैट

5
quickSortयदि यह वापस आ गया तो आपका उदाहरण बहुत सरल हो सकता है Stream; यह दो .stream()कॉल और एक कॉल को बचाएगा .collect(Collectors.toList())। यदि आप कोड के Collections.singleton(pivot).stream()साथ प्रतिस्थापित करते हैं तो Stream.of(pivot)लगभग पठनीय हो जाता है ...
Holger

22

Streams के चारों ओर Spliterators बनाए जाते हैं जो कि स्टेटफुल, म्यूटेबल ऑब्जेक्ट होते हैं। उनके पास "रीसेट" कार्रवाई नहीं है और वास्तव में, इस तरह की उलटी कार्रवाई का समर्थन करने की आवश्यकता है "बहुत अधिक शक्ति ले जाएगा"। Random.ints()इस तरह के अनुरोध को कैसे संभालना चाहिए?

दूसरी ओर, Streamएस के लिए जो एक पूर्वव्यापी मूल है, फिर से उपयोग किए जाने के लिए एक समकक्ष Streamका निर्माण करना आसान है । बस Streamएक पुन: प्रयोज्य विधि के निर्माण के लिए किए गए चरणों को रखें । ध्यान रखें कि इन चरणों को दोहराना एक महंगा ऑपरेशन नहीं है क्योंकि ये सभी चरण आलसी ऑपरेशन हैं; वास्तविक कार्य टर्मिनल ऑपरेशन के साथ शुरू होता है और वास्तविक टर्मिनल ऑपरेशन के आधार पर पूरी तरह से अलग कोड निष्पादित हो सकता है।

यह इस तरह की विधि के लेखक पर निर्भर करता है, यह निर्दिष्ट करने के लिए कि विधि को दो बार कॉल करने का क्या अर्थ है: क्या यह बिल्कुल उसी क्रम को पुन: उत्पन्न करता है, जैसा कि एक असंबद्ध सरणी या संग्रह के लिए बनाई गई धाराएं करती हैं, या क्या यह एक धारा का उत्पादन करती है। समान शब्दार्थ लेकिन विभिन्न तत्व जैसे यादृच्छिक किलों की एक धारा या कंसोल इनपुट लाइनों की एक धारा, आदि।


वैसे, भ्रम से बचने के लिए, एक टर्मिनल ऑपरेशन खपत करता है Streamजो धारा पर कॉलिंग के रूप में बंद करने से अलग होता है (जो संबंधित संसाधनों जैसे धाराओं, जैसे द्वारा उत्पादित ) के लिए आवश्यक है।Streamclose()Files.lines()


ऐसा लगता है कि भ्रम का एक बहुत की तुलना गुमराह की वजह से उपजी IEnumerableके साथ Stream। एक IEnumerableवास्तविक प्रदान करने की क्षमता का प्रतिनिधित्व करता है IEnumerator, इसलिए Iterableजावा में इसकी तरह । इसके विपरीत, Streamयह एक प्रकार का पुनरावृत्त है और तुलनीय है IEnumeratorइसलिए यह दावा करना गलत है कि इस प्रकार के डेटा प्रकार का .NET में कई बार उपयोग किया जा सकता है, इसके लिए समर्थन IEnumerator.Resetवैकल्पिक है। यहां चर्चा किए गए उदाहरण इस तथ्य का उपयोग करते हैं कि नए एस IEnumerableलाने के लिए उपयोग किया जा सकता है और यह जावा के साथ भी काम करता है ; आप एक नया प्राप्त कर सकते हैं । यदि जावा डेवलपर्स ने संचालन को जोड़ने का फैसला किया , तो यह वास्तव में तुलनीय था और यह उसी तरह काम कर सकता था। IEnumeratorCollectionStreamStreamIterable सीधे , तो मध्यवर्ती संचालन दूसरे को वापस कर देगाIterable

हालांकि, डेवलपर्स ने इसके खिलाफ फैसला किया और इस सवाल पर निर्णय पर चर्चा की गई । सबसे बड़ा बिंदु उत्सुक संग्रह संचालन और आलसी स्ट्रीम संचालन के बारे में भ्रम है। .NET एपीआई को देखकर, मुझे (हां, व्यक्तिगत रूप से) यह उचित लगता है। हालांकि यह IEnumerableअकेले देखने में उचित लगता है , एक विशेष संग्रह में संग्रह में हेरफेर करने के कई तरीके होंगे सीधे और बहुत सारे तरीके एक आलसी को लौटाते हैं IEnumerable, जबकि एक विधि की विशेष प्रकृति हमेशा सहज रूप से पहचानने योग्य नहीं होती है। सबसे खराब उदाहरण मैंने पाया (कुछ मिनट मैं इसे देखा अंदर), है List.Reverse()जिसका नाम से मेल खाता है वास्तव में विरासत में मिला के नाम (यह एक्सटेंशन तरीकों के लिए सही टर्मिनस है?) Enumerable.Reverse(), जबकि एक पूरी तरह से विपरीत व्यवहार कर रहे हैं।


बेशक, ये दो अलग-अलग निर्णय हैं। पहला एक Streamप्रकार से भिन्न बनाने के लिए Iterable/ Collectionऔर दूसरा Streamएक प्रकार के पुनरावृति के बजाय एक अन्य प्रकार के चलने के लिए। लेकिन ये निर्णय एक साथ किए गए थे और यह मामला हो सकता है कि इन दो निर्णयों को अलग करने पर कभी विचार नहीं किया गया। इसे .NET के दिमाग में तुलनीय होने के साथ नहीं बनाया गया था।

वास्तविक एपीआई डिजाइन का निर्णय एक बेहतर प्रकार के इटरेटर को जोड़ना था SpliteratorSpliterators पुराने Iterableएस द्वारा प्रदान किया जा सकता है (जो इस तरह से ये रेट्रोफिटेड थे) या पूरी तरह से नए कार्यान्वयन हैं। फिर, Streamउच्च-स्तरीय फ्रंट-एंड के बजाय उच्च स्तर के Spliteratorएस के रूप में जोड़ा गया । बस। आप इस बारे में चर्चा कर सकते हैं कि क्या एक अलग डिज़ाइन बेहतर होगा, लेकिन यह उत्पादक नहीं है, यह नहीं बदलेगा, जिस तरह से वे अब डिज़ाइन किए गए हैं।

एक और कार्यान्वयन पहलू है जिस पर आपको विचार करना है। Streams अपरिवर्तनीय डेटा संरचनाएँ नहीं हैं । प्रत्येक मध्यवर्ती ऑपरेशन Streamपुराने को घेरते हुए एक नया उदाहरण लौटा सकता है, लेकिन यह इसके बजाय अपने स्वयं के उदाहरण में हेरफेर कर सकता है और खुद को वापस कर सकता है (जो एक ही ऑपरेशन के लिए भी दोनों को करने से रोकता नहीं है)। आम तौर पर ज्ञात उदाहरण ऐसे ऑपरेशन हैं जैसे parallelया unorderedजो एक और कदम नहीं जोड़ते हैं लेकिन पूरी पाइपलाइन में हेरफेर करते हैं)। इस तरह के एक परिवर्तनशील डेटा संरचना और पुन: उपयोग करने का प्रयास (या इससे भी बदतर, एक ही समय में कई बार इसका उपयोग करते हुए) यह अच्छी तरह से नहीं खेलता है ...


पूर्णता के लिए, यहाँ जावा Streamएपीआई में अनुवादित आपका क्विकॉर्ट उदाहरण है। यह दर्शाता है कि यह वास्तव में "बहुत अधिक शक्ति को दूर नहीं करता है"।

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());
}

1
खैर, उपभोग करता है या नहीं, इसका उपभोग करने की कोशिश फिर से एक अपवाद फेंकता है कि धारा पहले से ही बंद थी , भस्म नहीं। जैसा कि आपने यादृच्छिक पूर्णांकों की एक धारा को रीसेट करने में समस्या के रूप में कहा था, यह एक रीसेट ऑपरेशन के सटीक अनुबंध को परिभाषित करने के लिए पुस्तकालय के लेखक पर निर्भर है।
विटाली

2
नहीं, संदेश "स्ट्रीम पहले ही चालू या बंद हो गया है" और हम एक "रीसेट" ऑपरेशन के बारे में बात नहीं कर रहे थे, लेकिन दो या अधिक टर्मिनल ऑपरेशनों को कॉल करना, Streamजबकि स्रोत के रीसेट Spliteratorकरना निहित होगा। और मुझे पूरा यकीन है कि अगर यह संभव था, तो एसओ पर सवाल थे जैसे "क्यों हर बार count()दो बार Streamअलग-अलग परिणाम देता है", आदि ...
होल्गर

1
यह अलग-अलग परिणाम देने के लिए गणना () के लिए बिल्कुल मान्य है। गणना () एक स्ट्रीम पर एक क्वेरी है, और यदि स्ट्रीम उत्परिवर्तनीय है (या अधिक सटीक होने के लिए, स्ट्रीम एक उत्परिवर्तित संग्रह पर एक क्वेरी के परिणाम का प्रतिनिधित्व करता है) तो यह अपेक्षित है। सी # एपीआई पर एक नजर है। वे इन सभी मुद्दों पर इनायत से पेश आते हैं।
विटाली

4
जिसे आप "बिल्कुल वैध" कहते हैं, वह एक सहज ज्ञान युक्त व्यवहार है। आखिरकार, परिणाम को संसाधित करने के लिए कई बार स्ट्रीम का उपयोग करने के बारे में पूछना मुख्य प्रेरणा है, अलग-अलग तरीकों से एक ही होने की उम्मीद है। एस के गैर-पुन: प्रयोज्य प्रकृति के बारे में एसओ पर हर सवाल Streamअब तक कई बार टर्मिनल संचालन को कॉल करके एक समस्या को हल करने के प्रयास से उत्पन्न होता है (जाहिर है, अन्यथा आप नोटिस नहीं करते हैं) जिसके कारण Streamएपीआई ने अनुमति दी तो चुपचाप टूट गया समाधान प्रत्येक मूल्यांकन पर विभिन्न परिणामों के साथ। यहाँ एक अच्छा उदाहरण है
होल्गर

3
वास्तव में, आपका उदाहरण पूरी तरह से प्रदर्शित करता है कि क्या होता है अगर एक प्रोग्रामर कई टर्मिनल संचालन को लागू करने के निहितार्थ को नहीं समझता है। ज़रा सोचिए कि जब इनमें से प्रत्येक ऑपरेशन तत्वों के एक पूरी तरह से अलग सेट पर लागू होगा, तब क्या होगा। यह केवल तभी काम करता है जब धारा का स्रोत प्रत्येक क्वेरी पर समान तत्व वापस करता है लेकिन यह बिल्कुल गलत धारणा है जिसके बारे में हम बात कर रहे थे।
होल्गर

8

मुझे लगता है कि जब आप पर्याप्त रूप से देखते हैं तो दोनों के बीच बहुत कम अंतर होते हैं।

यह चेहरा है, 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लिए बस झूठी वापसी होगी, और इसे शुरू करने के लिए रीसेट करने का कोई तरीका नहीं है। यदि आप फिर से संख्याओं पर पुनरावृति करना चाहते हैं, तो आपको एक और गणनाकर्ता उदाहरण बनाने की आवश्यकता होगी।


यह बताने के लिए कि IEnumerable के पास जावा स्ट्रीम के समान 'सुविधा' हो सकती है, को बेहतर ढंग से समझने के लिए, मानने योग्य मानें कि संख्याओं का स्रोत एक स्थिर संग्रह नहीं है। उदाहरण के लिए, हम एक एन्यूमरेबल ऑब्जेक्ट बना सकते हैं, जो 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एक अपवाद को फेंकने के लिए लिख सकते थे यदि आप कई बार इस पर पुनरावृति करने की कोशिश करते हैं, तो वास्तव में अनुपयोगी (जावा स्ट्रीम की तरह) अनुपयोगी बना देता है।

इसके अलावा, आपके लागू करने योग्य-आधारित त्वरित सॉर्ट का क्या मतलब है जब एक पर लागू किया जाता है RandomNumberStream?


निष्कर्ष

इसलिए, सबसे बड़ा अंतर यह है कि .NET आपको पृष्ठभूमि में IEnumerableएक नया निर्माण करके पुन: उपयोग करने की अनुमति देता IEnumeratorहै, जब भी अनुक्रम में तत्वों तक पहुंचने की आवश्यकता होगी।

यह निहित व्यवहार अक्सर उपयोगी होता है (और राज्य के रूप में 'शक्तिशाली'), क्योंकि हम बार-बार एक संग्रह पर पुनरावृति कर सकते हैं।

लेकिन कभी-कभी, यह निहित व्यवहार वास्तव में समस्याएं पैदा कर सकता है। यदि आपका डेटा स्रोत स्थिर नहीं है, या उपयोग करने के लिए महंगा है (जैसे डेटाबेस या वेब साइट), तो बहुत सारी मान्यताओं IEnumerableको त्यागना होगा; पुन: उपयोग नहीं है कि सीधे आगे


2

स्ट्रीम एपीआई में "रन वन्स" प्रोटेक्शन में से कुछ को बायपास करना संभव है; उदाहरण के लिए हम 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से शुरू करते हैं।

इस चुनौती को हल करने के लिए हमारे पास कई विकल्प हैं:

  1. हम 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(या समान 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);
  3. इस समस्या का सबसे अच्छा समाधान (मेरी राय में) पाइपलाइन Spliteratorमें उपयोग किए जाने वाले किसी भी स्टेटफुल एस की एक नई प्रतिलिपि बनाना है , Streamजब नए ऑपरेटरों को इनवाइट किया जाता है Stream। यह और अधिक जटिल है और लागू करने के लिए शामिल है, लेकिन अगर आप तीसरे पक्ष के पुस्तकालयों का उपयोग करने में कोई आपत्ति नहीं करते हैं, तो साइक्लॉप्स-रिएक्शन का 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

जैसा सोचा था।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.