راهنمای عملی Stream API در جاوا
در این مقاله قصد داریم شما را با API مهمی از زبان جاوا که در نسخه ۸ جاوا معرفی شد، یعنی stream API (که از این به بعد با نام جویبار میشناسیم) آشنا کنیم. این API رویکرد «برنامهنویسی اعلانی» را برای پیمایش و انجام عملیات روی یک مجموعه (collection) ارائه داد. تا نسخه۷ جاوا حلقههای for
و foreach
تنها گزینههای موجود برای پیمایش و انجام عملیات روی مجموعهها بودند که رویکرد برنامهنویسی دستوری (imperative) داشتند. در این مقاله ضمن آشنا کردن شما با API جویبار (stream API)، توضیح خواهیم داد که چگونه عملیات متداولی که روی مجموعهها انجام میدادیم توسط جویبارها انتزاعیسازی میشوند.
در برنامهنویسی دستوری (imperative programming) توسعهدهنده از ساختارهای موجود در زبان استفاده میکند تا دو چیز را بیان کند:
- چه کاری باید انجام شود؟ (what to do)
- این کار چگونه انجام میشود؟ (how to do)
این در حالی است که در برنامهنویسی اعلانی (declarative programming)، توسعهدهنده تنها روی کاری که باید انجام شود (what to do) تمرکز میکند و مدیریت قسمت «چگونگی اجرا» بر عهده زبان یا چارچوب مورد نظر خواهد بود. به همین خاطر کدهای نوشتهشده به سبک اعلانی، مختصر هستند و استعداد کمتری برای بروز خطا دارند.
عملیاتی که معمولا روی یک مجموعه انجام میشوند را میتوان به صورت زیر دستهبندی کرد. اگرچه دستهبندی زیر کامل نیست اما بیشتر مواردی که در برنامهنویسی روزانه با آنها سروکار داریم را پوشش میدهد. ما هم در این مقاله برای معرفی جویبارها عملیات زیر را در مثالها پیادهسازی میکنیم:
- نگاشت (map)
- فیلتر (filter)
- جستجو (search)
- مرتبسازی (sort)
- خلاصهسازی (summary)
- گروهبندی (group)
در مثالها از مجموعهای از اشیای کلاس Person
استفاده میکنیم. برای فهم راحت، پیادهسازی ساده این کلاس به صورت زیر است:
public class Person { private final String name; private final Gender gender; private final int age; public Person (String name, int age, Gender gender) { this.name = name; this.age = age; this.gender = gender; } public String getName () { return name; } public int getAge () { return age; } public Gender getGender () { return gender; } } public enum Gender{ MALE, FEMALE, OTHER }
معرفی اجمالی جویبارها
پیش از معرفی دقیق توابع جویبارها، اجازه دهید ابتدا برای درک خودِ API جویباری مثالی بزنیم:
List<Person> people = ... // bulding a stream List<String> namesOfPeopleBelow20 = people.stream() // pipelining a computation .filter(person -> person.getAge() < 20) // pipelining another computation .map(Person::getName) // terminating a stream .collect(Collectors.toList());
در مثال بالا، چندین عملیات برای تشکیل چیزی شبیه به یک خط لوله پردازش در کنار هم قرارگرفتهاند. این همان چیزی است که ما تحت عنوان Stream Pipeline (خط لوله جویبار) میشناسیم. هر خط لوله جویبار از سه قسمت زیر تشکیل شدهاست:
۱. ساخت جویبار
در مثال فوق مجموعهای از اشیا کلاس Person
با نام people
داریم. برای ساخت جویباری از اشیا people، متد ()stream
روی مجموعه people فراخوانی شده است؛ این متد از نسخه ۸ به واسط Collection
اضافه شده است.
به غیر از واسط Collection
، میتوان با متدهای زیر هم stream تولید کرد:
- متد
stream
از کلاسArrays
- توابع ایجادکنندهی جویبار مانند
()Stream.iterate
و()Stream.generate
(*) - متد
range
از کلاسIntStream
(*) توضیح مترجم: برای مطالعه بیشتر میتوانید در مورد iterate و generate و range در منابع معرفیشده بیشتر بخوانید.
۲. عملیات میانی (intermediate operation)
بعد از ایجاد شیِ جویبار، شما میتوانید مثل الگوی طراحی builder، صفر، یک یا چندین عملیات میانی را در کنار هم قرار دهید و آنها را بر روی جویبار اعمال کنید. همه متدهایی که در مثال بالا مشاهده کردیم اعم از map
و filter
، متدهایی از واسط Stream
هستند. این متدها برای فراهم کردن امکان استفاده از قابلیت chaining (زنجیرهسازی)، همگی نمونهای از جنس Stream
برمیگردانند. از آنجا که این عملیات نمونهای از جنس خودِ Stream
برمیگردانند، عملیات میانی خوانده میشوند.
۳. عملیات پایانی (terminal operations)
پس از انجام تمامی محاسبات میانی، شما باید با اعمال یک عملگر پایانی، به خط لوله پایان دهید و از نتیجه عملیات استفاده کنید. مشابه عملیات میانی، عملیات پایانی هم متدهایی از واسط Stream هستند اما در خروجی، نوعی به غیر از Stream
برمیگردانند. در مثال فوق، متد collect(Collectors.toList())
نمونهای از واسط List
برمیگرداند. بر این اساس که کدام عمل پایانی استفاده میشود، نوع خروجی میتواند خود یک مجموعه باشد یا نباشد.
هم اکنون بیایید نگاهی داشته باشیم به تعدادی عملیات که میتوان با جویبار آنها را پیادهسازی کرد. عملیاتی که در مثالها یاد میگیریم میتوانند به تنهایی بر روی جویبار اعمال شوند و یا برای مقاصد پیچیدهتر میتوانیم چند عملیات را ترکیب کنیم.
-
نگاشت (map)
نگاشت یا map به معنای تبدیل هر مقدار از عناصر در مجموعه به مقدار دیگری است. فرض کنید میخواهیم اسامی مجموعهای از افراد را استخراج کنیم؛ در چنین موردی برای تبدیل «افراد» به «اسامی» متناظر آنها باید از عملیات نگاشت استفاده کنیم.
در مثال زیر هر عنصر از جنس People
را به عنصری از نوع رشته که نشاندهنده نام همان فرد است، نگاشت میکنیم.
توضیح: عبارت Person::getName
یک ارجاع به متد است که با عبارت ()person -> person.getName
برابر بوده و نمونهای از جنس واسط Function است.
List<String> namesOfPeople = people.stream() .map (Person::getName) .collect (Collectors.toList());
-
فیلتر (filter)
همانطور که از نام آن برمیآید، عملیات پالایش یا به اصطلاح فیلتر کردن را روی مجموعهای از اعضا انجام میدهد. همهی اشیای جویبار وارد فیلتر میشوند ولی تنها اشیایی از آن عبور کرده (و در جویبارِ خروجی باقی میمانند) که شرط گذاشتهشده در فیلتر (که توسط نمونهای از Predicate بیان میشود) را برآورده کنند.
در مثال زیر، برای ساخت مجموعهای متشکل از افراد زیر ۲۰ سال، از عبارت person -> person.getAge() < 20
استفاده شده است.
// filtering using predicate List<Person> listOfPersonBelow20 = people.stream() .filter (person -> person.getAge() < 20) .collect (Collectors.toList());
انتخاب تعدادی از عناصر بر اساس تعداد هم میتواند نوعی عملیات فیلترینگ باشد. به همین منظور عملگرهای skip
و limit
در جویبارها پیادهسازی شدهاند. در مثال زیر، ۲ نفر ابتدایی نادیده گرفته و ۱۰ نفر بعدی انتخاب میشوند.
// count based filtering List<Person> smallerListOfPeople = people.stream() .skip (2) .limit (10) .collect (Collectors.toList());
-
جستوجو (search)
هدف جستجو در مجموعه، پیدا کردن یک عنصر خاص (find) یا صرفا اطلاع از وجود آن عنصر خاص در مجموعه است (match). این جستجوها بر اساس یک «شاخص» صورت میگیرد که این شاخص نیز نمونهای از Predicate
است.
در ازای جستجو (find) ممکن است مقداری برگردد یا اصلا هیچ مقداری برنگردد، به همین خاطر در ازای جستجوی یک عنصر، نمونهای از نوع Optional
خروجی داده میشود.
در طرف مقابل، نوع داده برگشتی در ازای جستجو برای وجود یک عنصر (match) از نوع boolean خواهد بود که نشاندهنده این است که آیا عنصری با مشخصات خواستهشده پیدا شد یا خیر.
در مثالهای زیر، جستجو برای یک عنصر خاص (find) توسط متد ()findAny
و جستجو برای اطلاع از وجود یک عنصر (match) توسط متد ()anyMatch
انجام میشود.
// searching for an element Optional<Person> any = people.stream() .filter (person -> person.getAge() < 20) .findAny(); // searching for existence boolean isAnyOneInGroupLessThan20Years = people.stream() .anyMatch (person -> person.getAge() < 20);
-
مرتبسازی (sort)
اگر میخواهید عناصر موجود در جویبار را مرتب کنید، میتوانید از عملگر میانی sorted استفاده کنید. اگر اشیا داخل جویبار به طور پیشفرض قابل مقایسه نباشند (رابط comparable را پیادهسازی نکنند)، میتوانید به این متد یک Comparator هم پاس دهید. برای اطلاعات بیشتر این لینک را مطالعه کنید.
در مثال زیر، مجموعه نهایی بر اساس سن و به ترتیب نزولی مرتب شده است.
List<Person> peopleSortedOldestToYuong = people.stream() .sorted(Comparator.comparing (Person::getAge).reversed()) .collect(Collectors.toList());
بر خلاف سایر عملیاتی که تا به حال دیدهایم، عملیات
sorted
یک عمل stateful است. یعنی قبل از اینکه عملگر، نتیجه مرتبسازی را در اختیار عملگر بعدی قرار دهد، باید همه عناصر موجود در جویبار را دریافت کردهباشد. متدdistinct
مثال دیگری از این نوع عملیات است.
-
خلاصهسازی (summary)
گاهی اوقات نیاز دارید اطلاعاتی را از مجموعه استخراج کنید. برای مثال، استخراج مجموعِ سن افراد. این کار با استفاده از عملیات پایانی در جویبار ممکن میشود. متدهای ()reduce
و()collect
از انواع عملیات پایانی هستند که به منظور استخراج اطلاعات فراهم شدهاند.
همچنین عملیات سطح بالاتری مانند sum و count و summaryStatistics و… وجود دارند که بر پایهی عملیات reduce و collect ایجاد شدهاند.
// calculating sum using reduce terminal operator people.stream() .mapToInt(Person::getAge) .reduce(0, (total, currentValue) -> total + currentValue); // calculating sum using sum terminal operator people.stream() .mapToInt(Person::getAge) .sum(); // calculating count using count terminal operator people.stream() .mapToInt(Person::getAge) .count(); // calculating summary IntSummaryStatistics ageStatistics = people.stream() .mapToInt(Person::getAge) .summaryStatistics(); ageStatistics.getAverage(); ageStatistics.getCount(); ageStatistics.getMax(); ageStatistics.getMin(); ageStatistics.getSum();
-
گروهبندی (grouping)
گاهی اوقات نیاز داریم مجموعهای را به چندین گروه تقسیمبندی کنیم، جویبار این قابلیت را به ما میدهد تا یک جویبار را بر اساس یک فاکتور معین گروهبندی کنیم. ساختمان دادهای که توسط گروهبندی ایجاد میشود، Map
است که «فاکتور گروهبندی» به عنوان کلیدها و «صفات مربوط به هر گروه خاص» به عنوان مقادیر کلیدهای این Map
هستند. API جویبار، متد ()Collectors.groupingBy
را برای این کار ارائه کرده است.
در مثالهای زیر، گروهبندی بر اساس جنسیت انجام شده و تفاوت در مقادیر کلیدها است.
در مثال اول مجموعهای از اشیا Person
برای هر گروه ایجاد شده است.
// Grouping people by gender Map<Gender, List<Person>> peopleByGender = people.stream() .collect(Collectors.groupingBy( Person::getGender,Collectors.toList()) );
در مثال دوم، برای استخراج نام هر Person و ایجاد مجموعهای از این اسامی، از متد ()Collectors.mapping
استفاده شده است.
// Grouping person names by gender Map<Gender, List<String>> nameByGender = peopleJavaCupIR@.stream() .collect(Collectors.groupingBy( Person::getGender, Collectors.mapping(Person::getName, Collectors.toList())));
در مثال سوم، سن هر Person استخراج شده و گروهبندی بر اساس میانگین سنی صورت گرفته است.
// Grouping average age by gender Map<Gender, Double> averageAgeByGender = people.stream() .collect(Collectors.groupingBy( Person::getGender, Collectors.averagingInt(Person::getAge) ));
جمعبندی
در این مقاله دیدیم که جویبارها از طریق پیادهسازی مکانیزم خط لوله (pipeline)، امکانات زیادی برای کار روی مجموعهها ارائه میدهند. این رابط برنامهنویسی بر پایه برنامهنویسی اعلانی (declarative) ایجاد شده و همین مسئله باعث میشود کدهای نوشتهشده با جویبارها دقیقتر، مختصر و کمخطاتر باشند. امیدواریم با مطالعه این مقاله به اهمیت و قدرت جویبارها پی برده باشید و در برنامهنویسی روزانه خود از آنها استفاده کنید.
منبع: praveergupta.in
مترجم: امیرحسین زارعی
.
.
آدرس کانال تلگرام: JavaCupIR@
آدرس اکانت توییتر: JavaCupIR@
آدرس صفحه اینستاگرام: javacup.ir
آدرس گروه لینکدین: Iranian Java Developers
👌👌
عالی بود ممنون.