یک استریم اگر با یک عملیات پایانی تمام نشود، هیچ دادهای پردازش نمیکند. قبلاً عملیات پایانی reduce() را پوشش دادهایم و چندین عملیات پایانی دیگر را در مثالهای مختلف دیدید. اکنون عملیاتهای پایانی دیگری که میتوانید روی یک استریم استفاده کنید را معرفی میکنیم.
استفاده از متد reduce() سادهترین راه برای کاهش یک استریم نیست. باید مطمئن شوید عملگر دوتایی شما associative است، سپس باید بدانید آیا عنصر همانی (identity element) دارد یا نه. نکات زیادی برای بررسی وجود دارد تا مطمئن شوید کد شما درست است و نتایج مورد انتظار را تولید میکند. اگر میتوانید از استفاده از متد reduce() اجتناب کنید، حتماً این کار را بکنید، چون اشتباه کردن با آن بسیار آسان است.
خوشبختانه، Stream API راههای بسیار دیگری برای کاهش استریمها به شما ارائه میدهد: sum()، min() و max() که هنگام معرفی استریمهای تخصصی اعداد پوشش دادیم متدهای راحتی هستند که میتوانید به جای فراخوانیهای معادل reduce() از آنها استفاده کنید. متدهای بیشتری را در این بخش پوشش خواهیم داد که باید بشناسید تا از استفاده از متد reduce() اجتناب کنید. در واقع، باید از این متد reduce() به عنوان آخرین راهحل استفاده کنید، فقط وقتی راه دیگری ندارید.
متد count() در تمام رابطهای استریم موجود است: هم در استریمهای تخصصی و هم در استریمهای اشیاء. فقط تعداد عناصر پردازششده توسط آن استریم را به صورت یک long برمیگرداند. این عدد میتواند بسیار بزرگ باشد، در واقع بزرگتر از Integer.MAX_VALUE، چون یک long است. پس یک استریم میتواند بیشتر از آنچه در یک ArrayList قرار دهید بشمارد.
شاید بپرسید چرا به چنین عدد بزرگی نیاز دارید. در واقع، میتوانید استریمهایی از منابع متعدد بسازید، از جمله منابعی که حجم عظیمی عناصر، بیشتر از Integer.MAX_VALUE تولید میکنند. حتی اگر اینطور نباشد، ایجاد یک عملیات میانی که تعداد عناصر استریم شما را ضرب کند آسان است. متد flatMap() که قبلاً در این آموزش پوشش دادیم میتواند این کار را انجام دهد. راههای زیادی وجود دارد که ممکن است در نهایت بیشتر از Integer.MAX_VALUE عنصر برای پردازش داشته باشید. به همین دلیل Stream API از آن پشتیبانی میکند.
مثالی از متد count() در عمل به صورت زیر است.
اجرای این کد نتیجه زیر را تولید میکند.
count = 4
متد forEach() Stream API به شما امکان میدهد هر عنصر استریم را به یک نمونه از رابط Consumer منتقل کنید. این متد برای چاپ عناصر پردازششده توسط یک استریم بسیار مفید است. این دقیقاً کاری است که کد زیر انجام میدهد.
اجرای این کد خروجی زیر را چاپ میکند.
ONE
TWO
این متد آنقدر ساده است که ممکن است وسوسه شوید در موارد نادرست از آن استفاده کنید.
به یاد داشته باشید که عبارات lambda شما باید از تغییر حوزهی بیرونی خود اجتناب کنند. گاهی تغییر state بیرونی انجام effect های جانبی نامیده میشود. مورد consumer خاص است چون consumer بدون effect جانبی کار زیادی انجام نمیدهد. در واقع، فراخوانی System.out.println() یا IO.println() یک effect جانبی روی کنسول برنامه شما ایجاد میکند.
بیایید مثال زیر را در نظر بگیریم.
اجرای کد قبلی خروجی زیر را چاپ میکند.
result = [ONE, TWO]
پس ممکن است وسوسه شوید از این کد استفاده کنید چون ساده است و «فقط کار میکند». خب، چندین چیز نادرست در این کد وجود دارد. بیایید آنها را بررسی کنیم.
فراخوانی result::add تمام عناصر پردازششده توسط آن استریم را به لیست خارجی result با تغییر آن لیست از داخل استریم اضافه میکند. این consumer یک effect جانبی به متغیری خارج از حوزهی خود استریم ایجاد میکند.
دسترسی به چنین متغیری عبارت lambda شما را به یک capturing lambda expression تبدیل میکند. ایجاد چنین عبارات lambda کاملاً قانونی است؛ فقط باید بدانید انجام این کار hit عملکردی مهمی دارد. اگر عملکرد در برنامه شما مهم است، باید از نوشتن capturing lambda ها اجتناب کنید.
علاوه بر این، این نحوهی نوشتن مانع از موازی کردن این استریم میشود. اگر این کار را انجام دهید، thread های متعددی همزمان به لیست result شما دسترسی خواهند داشت. این لیست نمونهای از ArrayList است، نه کلاسی که برای مدیریت دسترسی همزمان طراحی شده.
دو الگو برای ذخیرهی عناصر یک استریم در یک لیست دارید. مثال زیر الگوی اول را نشان میدهد که از شیء Collector استفاده میکند.
اجرای کد قبلی خروجی زیر را چاپ میکند.
result = [ONE, TWO]
این collector یک نمونه از ArrayList ایجاد میکند و عناصر پردازششده توسط استریم شما را به آن اضافه میکند. بنابراین این الگو هیچ effect جانبیای ایجاد نمیکند و hit عملکردی ندارد.
موازیسازی و همزمانی توسط Collector API خود مدیریت میشود، پس میتوانید با خیال راحت این استریم را موازی کنید.
این الگوی کد به همان اندازه ساده و خوانا کد قبلی است. هیچ یک از معایب ایجاد effect های جانبی در یک شیء consumer را ندارد. این قطعاً الگویی است که باید در کد خود استفاده کنید.
از Java SE 16 به بعد، الگوی دوم و حتی سادهتر وجود دارد.
نتیجه مشابه است.
result = [ONE, TWO]
این الگو نمونهی خاصی از List تولید میکند که غیرقابل تغییر (unmodifiable) است. اگر به لیست قابل تغییر نیاز دارید، باید به الگوی اول collector پایبند باشید. همچنین ممکن است از جمعآوری استریم شما در یک نمونه از ArrayList بهتر عمل کند. این نکته در پاراگراف بعدی پوشش داده میشود.
Stream API راههای متعددی برای جمعآوری تمام عناصر پردازششده توسط یک استریم در یک collection به شما میدهد. در بخش قبلی دو الگو از آنها را دیدید. بقیه را ببینیم.
چندین سؤال وجود دارد که قبل از انتخاب الگوی مورد نیاز باید از خود بپرسید.
ArrayList راحت هستید؟ یا LinkedList ترجیح میدهید؟List جمعآوری کنید؟Stream API میتواند تمام این شرایط را مدیریت کند.
قبلاً در مثال قبلی از این الگو استفاده کردهاید. سادهترین الگویی است که میتوانید استفاده کنید و عناصر را در نمونهای از ArrayList برمیگرداند.
اگر تعداد زیادی عنصر وجود داشته باشد که آرایهی داخلی ArrayList نتواند آنها را ذخیره کند، آرایه فعلی در آرایهی بزرگتری کپی میشود و توسط garbage collector مدیریت میشود. این مکانیزم توسط ArrayList پیادهسازی شده.
اگر میخواهید از این کار اجتناب کنید و تعداد عناصری که استریم شما تولید خواهد کرد را میدانید، میتوانید از collector Collectors.toCollection() استفاده کنید که یک supplier به عنوان آرگومان میگیرد تا collection را ایجاد کند.
الگوی زیر استفاده میکند:
result = [ONE, TWO]
در این مثال، result یک لیست غیرقابل تغییر است.
از Java SE 16 به بعد، راه بهتری برای جمعآوری داده در لیست غیرقابل تغییر وجود دارد که در برخی موارد میتواند کارآمدتر باشد. الگو به صورت زیر است.
result = [ONE, TWO]
چگونه میتواند کارآمدتر باشد؟ الگوی اول، بر اساس استفاده از collector، ابتدا عناصر شما را در یک ArrayList ساده جمعآوری و سپس آن را مهر میزند تا غیرقابل تغییر شود. Stream.toList() که به Java SE 16 اضافه شده این بهینهسازی را پیادهسازی کرده.
اگر نیاز دارید داده را در لیست خود یا لیست شخص ثالث خارج از JDK جمعآوری کنید، میتوانید از الگوی Collectors.toCollection() استفاده کنید.
چون رابط Set از رابط Collection ارثبری میکند، میتوانید از الگوی Collectors.toCollection(HashSet::new) استفاده کنید. اما Collector API الگوی تمیزتری به شما میدهد: Collectors.toSet().
برای Set غیرقابل تغییر، Collectors.toUnmodifiableSet() وجود دارد.
Stream API مجموعهی خود از overload های متد toArray() را دارد. دو مورد وجود دارد. اولی toArray() ساده است که نمونهای از Object[] برمیگرداند. دومی IntFunction<A[]> میگیرد. نوشتن پیادهسازی این تابع در واقع بسیار آسان است: String[]::new.
Stream API متدهای متعددی برای این کار به شما میدهد. متدهای max() و min() از استریمهای تخصصی اعداد قبلاً پوشش داده شدند. این عملیاتها عنصر همانی ندارند، پس تعجب نکنید که همگی object های optional برمیگردانند.
رابط Stream نیز متدهای max() و min() دارد که آنها نیز optional برمیگردانند. تفاوت با استریم اشیاء این است که عناصر Stream واقعاً میتوانند هر نوعی باشند. برای محاسبهی حداکثر یا حداقل، پیادهسازی باید این اشیاء را مقایسه کند. به همین دلیل برای این متدها باید یک Comparator ارائه دهید.
خروجی زیر را در کنسول چاپ میکند.
longest = three
به یاد داشته باشید تلاش برای باز کردن یک شیء optional خالی
NoSuchElementExceptionایجاد میکند. فقط وقتی مطمئنید استریم شما دادهای برای پردازش ندارد این اتفاق میافتد. در این مثال ساده، استریم شما چندین رشتهی کاراکتر بدون عملیات filter پردازش میکند. این استریم نمیتواند خالی باشد.
چون پیادهسازیهای تخصصی استریم اعداد میدانند چگونه اعداد را مقایسه کنند، نیازی به ارسال Comparator نیست.
Stream API دو عملیات پایانی برای یافتن یک عنصر به شما میدهد: findFirst() و findAny(). این دو متد هیچ آرگومانی نمیگیرند و یک عنصر واحد از استریم شما برمیگردانند. برای مدیریت صحیح مورد استریمهای خالی، این عنصر در یک شیء optional پیچیده شده.
درک اینکه کدام عنصر برگردانده میشود مستلزم درک این موضوع است که استریمها میتوانند مرتب باشند. یک استریم مرتب به سادگی استریمی است که ترتیب عناصر مهم است و توسط Stream API حفظ میشود. به طور پیشفرض، استریمی که از هر منبع مرتب ایجاد میشود خودش مرتب است.
روی چنین استریمی، داشتن اولین، دومین یا سومین عنصر معنادار است. یافتن اولین عنصر چنین استریمی پس کاملاً منطقی است.
اگر استریم شما مرتب نیست، یا اگر ترتیب در پردازش استریم شما از بین رفته، یافتن اولین عنصر نامعلوم است و فراخوانی findFirst() در واقع هر عنصر تصادفی از استریم برمیگرداند.
توجه کنید فراخوانی findFirst() بررسیهایی در پیادهسازی استریم انجام میدهد تا مطمئن شود اولین عنصر آن استریم را دریافت میکنید. اگر استریم شما یک استریم موازی باشد، این میتواند هزینهبر باشد. در بسیاری موارد که یافتن اولین عنصر یافتشده مهم نیست، باید از findAny() به جای findFirst() استفاده کنید.
سه متد وجود دارد:
anyMatch(predicate): اگر یک عنصر پیدا شود که با predicate مطابقت داشته باشد true برمیگرداند. برای استریم خالی false برمیگرداند.allMatch(predicate): اگر تمام عناصر استریم با predicate مطابقت داشته باشند true برمیگرداند. برای استریم خالی true برمیگرداند.noneMatch(predicate): اگر هیچ عنصری با predicate مطابقت نداشته باشد true برمیگرداند. برای استریم خالی true برمیگرداند.شاید متوجه تفاوت مهمی بین عملیاتهای پایانی مختلفی که اینجا پوشش دادیم شده باشید.
برخی از آنها نیاز به پردازش تمام داده مصرفشده توسط استریم شما دارند. این مورد عملیاتهای COUNT، MAX، MIN، AVERAGE و همچنین فراخوانیهای forEach()، toList() یا toArray() است.
برای آخرین عملیاتهای پایانی که پوشش دادیم اینطور نیست. متدهای findFirst() یا findAny() به محض یافتن یک عنصر پردازش داده شما را متوقف میکنند، بدون توجه به اینکه چند عنصر باقی مانده. همین مورد برای anyMatch()، allMatch() و noneMatch() صادق است.
هنوز مواردی وجود دارد که این متدهای آخر باید تمام عناصر را پردازش کنند:
findFirst() و findAny() فقط وقتی ممکن است که تمام عناصر پردازش شده باشند.false برای anyMatch() نیز نیاز به پردازش تمام عناصر استریم دارد.true برای allMatch() و noneMatch() نیز نیاز به پردازش تمام عناصر استریم دارد.این متدها در Stream API short-circuiting (مسدودکننده) نامیده میشوند چون میتوانند بدون نیاز به پردازش تمام عناصر استریم شما نتیجه تولید کنند.
برخی موارد وجود دارد که باید عناصر خود را در یک لیست غیرقابل تغییر انباشته کنید. ممکن است این موضوع متناقض به نظر برسد، چون جمعآوری شامل اضافه کردن عناصر به یک container قابل تغییر است. در واقع، نحوهی کار Collector API به همین صورت است. در انتهای این عملیات انباشت، Collector API میتواند یک عملیات اختیاری آخر انجام دهد که در این مورد شامل مهر زدن لیست قبل از برگرداندن آن است.
برای این کار، کافی است از الگوی زیر استفاده کنید.
اجرای این کد نتیجه زیر را تولید میکند.
result = [ONE, TWO]
در این مثال، result یک لیست غیرقابل تغییر است.
از Java SE 16 به بعد، راه بهتری برای جمعآوری داده در لیست غیرقابل تغییر وجود دارد که در برخی موارد میتواند کارآمدتر باشد. الگو به صورت زیر است.
چگونه میتواند کارآمدتر باشد؟ الگوی اول که بر پایهی استفاده از collector ساخته شده، ابتدا عناصر شما را در یک ArrayList ساده جمعآوری میکند و سپس آن را مهر میزند تا هنگام اتمام پردازش غیرقابل تغییر شود. آنچه کد شما میبیند فقط لیست غیرقابل تغییر ساخته شده از این ArrayList است.
همانطور که میدانید، نمونهای از ArrayList بر پایهی یک آرایهی داخلی با اندازه ثابت ساخته شده. این آرایه میتواند پر شود. در آن صورت، پیادهسازی ArrayList آن را تشخیص داده و در آرایهی بزرگتری کپی میکند. این مکانیزم برای کلاینت شفاف است، اما هزینهی اضافی دارد: کپی کردن این آرایه زمانبر است.
در برخی موارد Stream API میتواند تعداد عناصری که باید پردازش شوند را قبل از مصرف کل استریم ردیابی کند. در آن صورت، ایجاد آرایهی داخلی با اندازهی مناسب کارآمدتر است چون از هزینهی کپی کردن آرایههای کوچک به آرایههای بزرگتر اجتناب میکند.
این بهینهسازی در متد Stream.toList() پیادهسازی شده که به Java SE 16 اضافه شده. اگر آنچه نیاز دارید لیست غیرقابل تغییر است، باید از این الگو استفاده کنید.
اگر نیاز دارید داده را در لیست خود یا لیست شخص ثالث خارج از JDK جمعآوری کنید، میتوانید از الگوی Collectors.toCollection() استفاده کنید. Supplier ای که برای تنظیم اندازهی اولیهی نمونهی ArrayList خود استفاده کردید، میتواند برای ساخت هر پیادهسازی از Collection نیز استفاده شود، از جمله پیادهسازیهایی که بخشی از JDK نیستند. فقط کافی است یک supplier ارائه دهید. در مثال زیر، supplier ای برای ایجاد نمونهای از LinkedList فراهم میکنیم.
اجرای مثال قبلی خروجی زیر را چاپ میکند.
Class of result = class java.util.LinkedList
result = [ONE, TWO]
چون رابط Set از رابط Collection ارثبری میکند، میتوانید از الگوی Collectors.toCollection(HashSet::new) برای جمعآوری داده در نمونهای از Set استفاده کنید. این خوب است، اما Collector API همچنان الگوی تمیزتری به شما میدهد: Collectors.toSet().
اجرای کد قبلی خروجی زیر را چاپ میکند.
Class of result = class java.util.HashSet
result = [ONE, TWO]
ممکن است بپرسید آیا تفاوتی بین این دو الگو وجود دارد. پاسخ بله است، تفاوت ظریفی وجود دارد که بعداً در این آموزش خواهید دید.
اگر آنچه نیاز دارید Set غیرقابل تغییر است، Collector API الگوی دیگری برای شما دارد: Collectors.toUnmodifiableSet().
اجرای کد قبلی خروجی زیر را چاپ میکند.
Class of result = class java.util.ImmutableCollections$Set12
result = [ONE, TWO]
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/