۲۹ شهریور ۱۴۰۳

Techboy

اخبار و اطلاعات روز تکنولوژی

جمع کننده های جریان: روشی جدید برای دستکاری جریان های جاوا

اولین تور از اپراتورهای جریان قابل تنظیم جدید در رابط جاوا 22 جاوا.util.stream.Gatherers.

اولین تور از اپراتورهای جریان قابل تنظیم جدید در رابط جاوا ۲۲ جاوا.util.stream.Gatherers.

جاوا ۲۲ جمع‌آورنده‌های جریان را معرفی می‌کند، مکانیزم جدیدی برای دستکاری جریان‌های داده. جمع‌آورنده‌های جریان، ویژگی ارائه‌شده برای JEP 461 هستند که به توسعه‌دهندگان اجازه می‌دهند اپراتورهای میانی سفارشی ایجاد کنند که عملیات پیچیده را ساده می‌کند. در نگاه اول، گردآورنده‌های جریان کمی پیچیده و مبهم به نظر می‌رسند، و ممکن است تعجب کنید که چرا به آنها نیاز دارید. اما هنگامی که با وضعیتی مواجه می شوید که به نوع خاصی از دستکاری جریان نیاز دارد، گردآورنده ها به یک افزونه واضح و خوشایند برای Stream API تبدیل می شوند.

API Stream و گردآورندگان جریان

جریان‌های جاوا مجموعه‌های پویا از عناصر را مدل‌سازی می‌کنند. همانطور که مشخصات می‌گوید، “جریان یک دنباله مقادیر به طور بالقوه نامحدود است.”

این بدان معناست که می‌توانید به‌طور بی‌پایان جریان‌های داده را مصرف کرده و روی آن‌ها کار کنید. به این فکر کنید که در کنار رودخانه ای نشسته اید و جریان آب را تماشا می کنید. شما هرگز فکر نمی کنید منتظر بمانید تا رودخانه تمام شود. با نهرها، شما تازه کار را با رودخانه و هر چیزی که در آن وجود دارد شروع می کنید. وقتی کارتان تمام شد، کنار می‌روید.

API جریان دارای چندین روش داخلی برای کار بر روی عناصر در یک دنباله از مقادیر. اینها اپراتورهای عملکردی مانند filter و map هستند. 

در Stream API، جریان‌ها با منبع رویدادها شروع می‌شوند و عملیاتی مانند filter و map به عنوان عملیات “واسطه” شناخته می‌شوند. هر عملیات میانی جریان را برمی گرداند، بنابراین می توانید آنها را با هم بسازید. اما با استفاده از Stream API، جاوا شروع به اعمال هیچ یک از این عملیات نمی کند تا زمانی که جریان به یک عملیات “پایانه” برسد. این از پردازش کارآمد حتی با بسیاری از اپراتورهای زنجیر شده با هم پشتیبانی می کند.

اپراتورهای میانی داخلی Stream قدرتمند هستند، اما نمی‌توانند کل حوزه مورد نیاز قابل تصور را پوشش دهند. برای موقعیت هایی که خارج از جعبه هستند، ما به راهی برای تعریف عملیات سفارشی نیاز داریم. گردآورندگان این راه را به ما می دهند.

چه کاری می توانید با جمع کننده های جریان انجام دهید

بگویید که در کنار رودخانه هستید و برگها با اعدادی که روی آنها نوشته شده است شناور هستند. اگر می‌خواهید کار ساده‌ای انجام دهید، مانند ایجاد آرایه‌ای از تمام اعداد زوجی که می‌بینید، می‌توانید از روش filter داخلی استفاده کنید:


List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.stream().filter(number -> number % 2 == 0).toArray()
// result: { 2, 4, 6 }

در مثال بالا، با آرایه‌ای از اعداد صحیح (منبع) شروع می‌کنیم و سپس آن را به یک جریان تبدیل می‌کنیم و فیلتری را اعمال می‌کنیم که فقط آن اعدادی را که تقسیم بر دو آنها باقی نمی‌ماند، برمی‌گرداند. تماس toArray() تماس ترمینال است. این معادل بررسی یکنواختی هر برگ و کنار گذاشتن آن در صورت عبور است.

ASP.NET Core برنامه نویسی وب تمام پشته را در NET 8 دریافت می کند

روش‌های داخلی گردآورنده‌های جریانی

java.util.stream.Gatherers با تعدادی توابع داخلی ارائه می شود که به شما امکان می دهد عملیات میانی سفارشی بسازید. بیایید نگاهی به کارهایی که هر کدام انجام می دهند بیاندازیم.

روش windowFixed

اگر بخواهید تمام برگ های شناور را بردارید و آنها را در سطل های دوتایی جمع کنید، چه؟ این برای با اپراتورهای کاربردی داخلی انجام دهید. این نیاز به تبدیل یک آرایه تک رقمی به آرایه ای از آرایه ها دارد. 

روش windowFixed راه ساده‌تری برای جمع‌آوری برگ‌های شما در سطل‌ها است:


Stream.iterate(0, i -> i + 1)
  .gather(Gatherers.windowFixed(2))
  .limit(5)
  .collect(Collectors.toList());

این می گوید: یک جریان بر اساس تکرار اعداد صحیح با ۱ به من بدهید. هر دو عنصر را به یک آرایه جدید تبدیل کنید. پنج بار انجام دهید. در نهایت، جریان را به List تبدیل کنید. نتیجه این است:


[[۰, ۱], [۲, ۳], [۴, ۵], [۶, ۷], [۸, ۹]]

پنجره مانند حرکت دادن یک قاب بر روی جریان است. به شما امکان می دهد عکس های فوری بگیرید. 

روش windowSliding

یک تابع پنجره دیگر windowSliding، که مانند windowFixed() عمل می کند، به جز اینکه هر پنجره به جای اینکه در انتهای آن از عنصر بعدی در آرایه منبع شروع شود. از آخرین پنجره این یک مثال است:


Stream.iterate(0, i -> i + 1)
   .gather(Gatherers.windowSliding(2))
   .limit(5)
   .collect(Collectors.toList());

خروجی این است:


[[۰, ۱], [۱, ۲], [۲, ۳], [۳, ۴], [۴, ۵]]

خروجی windowSliding را با خروجی windowFixed مقایسه کنید و تفاوت را خواهید دید. هر زیرآرایه در windowSliding حاوی آخرین عنصر زیرآرایه قبلی است، برخلاف windowFixed.

روش Gatherers.fold

Gatherers.fold مانند نسخه اصلاح شده Stream.reduce. دیدن اینکه در کجای fold() نسبت به reduce() مفید است، کمی ظریف است. بحث خوبی در این مقاله یافت می‌شود. در اینجا چیزی است که نویسنده، ویکتور کلانگ، درباره تفاوت‌های بین fold و reduce می‌گوید:

جعبه ابزار کم کد پلت فرم Salesforce را به برنامه های Slack گسترش می دهد

تا کردن یک تعمیم کاهش است. با کاهش، نوع نتیجه همان نوع عنصر است، ترکیب کننده تداعی کننده است و مقدار اولیه یک هویت برای ترکیب کننده است. برای یک تا، این شرایط لازم نیست، هر چند ما از موازی پذیری صرف نظر می کنیم.

بنابراین می بینیم که reduce نوعی fold است. کاهش یک جریان را می گیرد و آن را به یک مقدار واحد تبدیل می کند. Folding نیز این کار را انجام می دهد، اما الزامات را کاهش می دهد: ۱) اینکه نوع برگشتی از همان نوع عناصر جریان باشد. ۲) اینکه ترکیب کننده تداعی کننده است. و ۳) اینکه مقدار اولیه موجود در fold یک تابع مولد واقعی است، نه یک مقدار استاتیک.

نیاز دوم مربوط به موازی سازی است که به زودی در مورد آن با جزئیات بیشتر صحبت خواهم کرد. فراخوانی Stream.parallel در یک جریان به این معنی است که موتور می‌تواند کار را به چند رشته تقسیم کند. این فقط در صورتی کار می کند که اپراتور انجمنی باشد. یعنی اگر ترتیب عملیات بر نتیجه تأثیری نداشته باشد، کار می کند.

در اینجا یک استفاده ساده از fold وجود دارد:


Stream.of("hello","world","how","are","you?")
  .gather(
    Gatherers.fold(() -> "", 
      (acc, element) -> acc.isEmpty() ? element : acc + "," + element
    )
   )
  .findFirst()
  .get();

این مثال مجموعه ای از رشته ها را می گیرد و آنها را با کاما ترکیب می کند. همان کار انجام شده توسط reduce:


String result = Stream.of("hello", "world", "how", "are", "you?")
  .reduce("", (acc, element) -> acc.isEmpty() ? element : acc + "," + element);

می توانید ببینید که با fold، یک تابع (() -> "") را به جای مقدار اولیه ("").  این بدان معناست که اگر به مدیریت پیچیده تری از آغازگر نیاز دارید، می توانید از تابع closure استفاده کنید. 

اکنون بیایید در مورد مزایای fold با توجه به انواع مختلف فکر کنیم. فرض کنید جریانی از انواع شیء مختلط داریم و می‌خواهیم رخدادها را بشماریم:


var result = Stream.of(1,"hello", true).gather(Gatherers.fold(() -> 0, (acc, el) -> acc + 1));
// result.findFirst().get() = 3

نتیجه var ۳ است. توجه کنید که جریان دارای یک عدد، یک رشته و یک Boolean است. انجام یک کار مشابه با reduce دشوار است زیرا آرگومان انباشته (acc) به شدت تایپ شده است:


// bad, throws exception:
var result = Stream.of(1, "hello", true).reduce(0, (acc, el) -> acc + 1);
// Error: bad operand types for binary operator '+'

می‌توانیم از گردآورنده برای انجام این کار استفاده کنیم:


var result2 = Stream.of("apple", "banana", "apple", "orange")
  .collect(Collectors.toMap(word -> word, word -> 1, Integer::sum, HashMap::new));

اما اگر به منطق درگیر بیشتری نیاز داشته باشیم، دسترسی به توابع اولیه و بدنه تاشو را از دست دادیم.

روش Gatherers.scan

Scan چیزی شبیه windowFixed است اما عناصر را به جای آرایه در یک عنصر جمع می کند. مجدداً، یک مثال وضوح بیشتری را نشان می دهد (این مثال از جاوادوکس):


Stream.of(1,2,3,4,5,6,7,8,9)
  .gather(
    Gatherers.scan(() -> "", (string, number) -> string + number)
  )
  .toList();

خروجی این است:


["۱", "۱۲", "۱۲۳", "۱۲۳۴", "۱۲۳۴۵", "۱۲۳۴۵۶", "۱۲۳۴۵۶۷", "۱۲۳۴۵۶۷۸", "۱۲۳۴۵۶۷۸۹"]

بنابراین، scan به ما امکان می‌دهد در میان عناصر جریان حرکت کنیم و آنها را به صورت تجمعی ترکیب کنیم.

روش mapConcurrent

با mapConcurrent، می‌توانید حداکثر تعداد رشته‌ها را برای استفاده همزمان در اجرای تابع map تعیین کنید. از رشته های مجازی استفاده خواهد شد. در اینجا یک مثال ساده وجود دارد که همزمانی را به چهار رشته محدود می‌کند در حالی که اعداد را مربع می‌کند (توجه داشته باشید که mapConcurrent برای چنین مجموعه داده ساده‌ای بیش از حد است):


Stream.of(1,2,3,4,5).gather(Gatherers.mapConcurrent(4, x -> x * x)).collect(Collectors.toList());
// Result: [1, 4, 9, 16, 25]

علاوه بر حداکثر رشته، mapConcurrent دقیقاً مانند تابع استاندارد map کار می‌کند.

کلمه ای در مورد موازی سازی

Stream API امکان موازی سازی عملیات با Stream.parallel را فراهم می کند. این می تواند سرعت را هنگام کار با مجموعه های بزرگ افزایش دهد. گردآورندگان می توانند از این ویژگی استفاده کنند، به شرطی که به شرط تداعی احترام بگذارند. عملیات موازی سازی تنها در هنگام برخورد با جریان های بزرگ ضروری است. 

نتیجه گیری

تا زمانی که گردآورندگان جریان به‌عنوان یک ویژگی تبلیغ شوند، همچنان باید از پرچم --enable-preview برای دسترسی به رابط Gatherer و ویژگی‌های آن استفاده کنید. یک راه آسان برای آزمایش استفاده از JShell است: $ jshell --enable-preview.

اگرچه نیاز روزانه نیست، جمع‌آورنده‌های جریان برخی از شکاف‌های طولانی‌مدت در Stream API را پر می‌کنند و توسعه و سفارشی‌سازی برنامه‌های کاربردی جاوا را برای توسعه‌دهندگان آسان‌تر می‌کنند.