آموزش مقدماتی جاوا (بخش چهارم) — از صفر تا صد

۱۵۱ بازدید
آخرین به‌روزرسانی: ۰۶ شهریور ۱۴۰۲
زمان مطالعه: ۹ دقیقه
آموزش مقدماتی جاوا (بخش چهارم) — از صفر تا صد

در این سری مقالات آموزش مقدماتی جاوا که این مطلب آخرین مورد از آن محسوب می‌شود به معرفی مفاهیم این زبان برنامه‌نویسی به روشی ساده و مؤثر پرداختیم. اگر یک برنامه‌نویس حرفه‌ای جاوا اسکریپت باشید نیز مطالعه این مطالب توصیه می‌شوند، زیرا به عنوان یک یادآوری برای مواردی که آموخته‌اید مفید هستند. در بخش قبلی این مقالات به بررسی مفهوم نخ، Mutex و Semaphore، مدیریت خطا، کلاس Observable و اینترفیس Observer و روش‌های نوشتن و خواندن فایل پرداختیم. در این بخش نیز برخی مفاهیم پیشرفته‌تر مانند برنامه‌نویسی تابعی و همچنین اینترفیس تابعی مورد بررسی قرار می‌گیرند.

پشتیبانی از برنامه‌نویسی تابعی در جاوا 8

«برنامه‌نویسی تابعی» (Functional Programming) جایگزینی برای برنامه‌نویسی شیءگرا است که پیرامون مفهوم «تابع‌های محض» (Pure Functions) شکل گرفته است. اپلیکیشن‌های تابعی از حالت مشترک اجتناب می‌کنند و میل دارند که فشرده‌تر بوده و قابلیت پیش‌بینی بیشتری نسبت به کدهای شیءگرا داشته باشند. تابع محض در یک معنی ریاضیاتیِ محض، دارای خصوصیات زیر است:

  • کاملاً «بی‌حالت» (stateless) است، یعنی هیچ متغیر یا مقداری ذخیره نمی‌شود و هیچ متغیر سراسری ندارد.
  • عدم وجود حافظه به معنی نبود عوارض جانبی حافظه نیز هست.
  • ارزیابی موازی اجرا می‌شود و نیازی به همگام‌سازی وجود ندارد. از این رو بسیار ساده‌تر از چند نخی است.

با بهره‌گیری از این سبک برنامه‌نویسی تابعی می‌توانیم کدهای شیءگرای تمیزتر و با خوانایی بیشتر و همچنین اشکال‌های کمتر تولید کنیم. اینک سؤال این است که چگونه می‌توان برنامه‌نویسی تابعی را در جاوا اجرا کرد و مزیت کلی آن چیست؟

ذخیره‌سازی تابع در یک شیء

در جاوا 8، اینترفیس <java.util.Function<T، R معرفی شده است. این اینترفیس می‌تواند تابعی را در خود ذخیره کند که یک آرگومان می‌گیرد و یک شیء بازگشت می‌دهد، T ژنریک یک نوع آرگومان است و R نوعی شیء است که بازگشت می‌دهد. برای نمونه به مثال زیر توجه کنید:

1public static Integer calculate(Function<Integer, Integer> function, Integer value) {
2return function.apply(value);
3}

اینترفیس تابعی

اینترفیس تابعی اینترفیسی است که دارای متد منفرد مجرد است. این نوع اینترفیس‌ها تنها یک کارکرد را می‌توانند نمایش دهند. از جاوا 8 به بعد عبارت‌های لامبدا می‌توانند برای نمایش وهله‌ای از یک اینترفیس تابعی استفاده شوند. Runnable ،Comparator و Comparable برخی از نمونه‌های اینترفیس‌های تابعی هستند.

1class Test
2{
3 public static void main(String args[])
4 {
5 new Thread(new Runnable()
6  {
7  @Override
8  public void run()
9  {
10    System.out.println(“Hello”);
11   }
12 }).start();
13 }
14}

عبارت لامبدا برای اینترفیس تابعی استفاده می‌شود:

1class Test
2{
3 public static void main(String args[])
4 {
5 new Thread(()->
6 {System.out.println(“Hello lambda”);}).start();
7 }
8}

در ادامه عبارت لامبدا را به تفصیل بررسی خواهیم کرد.

متد پیش‌فرض در اینترفیس

اینترفیس‌ها همواره فیلدهای عمومی استاتیک «نهایی» (final) داشته‌اند. از جاوا 8 به بعد اینترفیس‌ها می‌توانند متدهای پیش‌فرض داشته باشند. تا پیش از جاوا 8 ما باید اشیای کلاس درونی ناشناسی می‌داشتیم یا این اینترفیس‌ها را پیاده‌سازی می‌کردیم. ایده اصلی متد پیش‌فرض این است که یک متد اینترفیس با پیاده‌سازی پیش‌فرض است و کلاس انشعاب یافته می‌تواند پیاده‌سازی دقیق‌تری از آن ارائه دهد.

چرا همچنان از کلاس مجرد استفاده نمی‌کنیم؟

کلاس‌های مجرد همچنان یک روش برای معرفی حالت یا پیاده‌سازی متدهای اصلی شیء که با حالت تزویج شده‌اند، محسوب می‌شوند. متدهای پیش‌فرض در اینترفیس به نام «رفتار محض» (pure behavior) نامیده می‌شوند. نمونه‌ای از متد پیش‌فرض متد nullLast در اینترفیس Comparator است.

1Comparator<Obj> newComparator = Comparator.nullLast(oldComparator);

از آنجا که عبارت لامبدا معرفی شده است، امکان افزودن پشتیبانی عبارت لامبدا در مجموعه اینترفیس‌های کنونی وجود دارد. برای افزودن پشتیبانی از عبارت لامبدا، باید همه اینترفیس‌های فریمورک collection را که مفهوم متدهای پیش‌فرض در اینترفیس‌ها را معرفی کرده‌اند بازنویسی کنیم.

بنابراین اینترفیس‌ها با پشتیبانی کردن از برنامه‌نویسی تابعی به صورت «بی‌حالت» در آمده‌اند و می‌توانند چندین متد پیش‌فرض استاتیک داشته باشند.

عدم ادامه همگام‌سازی روی متدهای اینترفیس

همگام‌سازی به ظرفیت کنترل دسترسی چندین نخ روی منابع مشترک گفته می‌شود. متدهای استاتیک همگام‌سازی شده یک قفل روی کلاس «Class» دارند و از این رو وقتی یک نخ وارد یک متد استاتیک همگام‌سازی شده می‌شود، خود کلاس از سوی مانیتور نخ قفل می‌شود و هیچ نخ دیگری نمی‌تواند وارد متدهای همگام‌سازی شده استاتیک روی آن کلاس شود. این رویه خلاف وضعیت متدهای وهله‌ای است، چون چندین نخ می‌توانند به طور همزمان برای وهله‌های مختلف به متدهای وهله‌ای همگام‌سازی شده یکسانی دسترسی داشته باشند.

برای نمونه متد Run در کلاس runnable می‌تواند همگام‌سازی شود. اگر متد Run را به صورت همگام‌سازی شده درآورید، در این صورت قفل روی شیء runnable پیش از اجرایی شدن متد Run اشغال می‌شود.

همگام‌سازی به قفل کردن مرتبط است. قفل کردن به هماهنگی دسترسی مشترک به حالت mutable مربوط است. هر شیء باید یک سیاست همگام سای داشته باشد که تعیین کند کدام قفل از کدام متغیرهای حالت محافظت می‌کند. اشیای زیادی از «الگوی مانیتور جاوا» (Java Monitor Pattern) برای سیاست همگام‌سازی‌شان استفاده می‌کنند که در آن حالت، شیء از سوی یک قفل درونیش مورد محافظت قرار می‌گیرد.

اما اینترفیس‌ها حالت اشیایی را که در آن دخیل شده‌اند مالک نمی‌شوند. کلاس‌های فرعی می‌توانند متدهایی را که به صورت همگام‌سازی شده در سوپر کلاس‌ها اعلان شده‌اند «باطل» (override) کنند و همگام‌سازی را به طرز مؤثری حذف کنند. این وضعیت ممکن است این حس اطمینان نادرست را در شما ایجاد کند که این کار در جهت امنیت نخ صورت گرفته است و هیچ پیام خطای دیگری به شما نخواهد گفت که از سیاست همگام‌سازی نادرستی استفاده می‌کنید.

Optional-ها

همه ما از null-ها و بررسی null-ها متنفر هستیم. این که برای همه آرگومان‌ها باید بررسی کنیم که null است یا نه، کار دشواری است. در جاوا 8، <java.util.Optional<T برای مدیریت اشیا معرفی شده است و از آن بهتر ممکن نیست. این یک شیء کانتینر است که شیء دیگری را نگهداری می‌کند. T ژنریک نوعی شیء است که می‌خواهید نگهداری کنید:

1Integer i = 5;
2Optional<Integer> optionalInteger = Optional.of(i);

کلاس Optional هیچ سازنده عمومی ندارد. اگر شئیی Null نباشد و قرار هم نباشد Null شود، برای ایجاد یک optional از (Optional.of(object استفاده می‌کنیم؛ اما برای اشیای nullable از (Optional.ofNullable(object استفاده می‌شود.

عبارت‌های لامبدا

عبارت‌های لامبدا روشی خوانا و گویا برای کدنویسی پردازش لیست‌ها و Collection-ها است. این متد بدون اعلان است، یعنی modifier دسترسی و اعلان مقدار بازگشتی نام ندارد. بدین ترتیب تلاش ما برای اعلان کردن و نوشتن متد جداگانه برای کلاس حامل کاهش می‌یابد.

لامبداها در اغلب موارد دارای خصوصیات زیر هستند:

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

عبارت‌های لامبدا این امکان را به ما می‌دهند که با یک کارکرد به عنوان آرگومان متد یا در واقع با کد به صورت داده رفتار کنیم.

1MathOperation addition = (int a، int b) -> a + b;
2addition(a،b);

Stream

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

Stream-ها همچنین اشیایی «تغییرناپذیر» (immutable) و یک بار مصرف هستند. زمانی که یک Stream پیمایش شد، دیگر نمی‌توان آن را پیمایش کرد. هر بار که توسعه‌دهندگان یک Stream را دستکاری می‌کنند، در واقع یک Stream جدید ایجاد می‌کنند.

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

مثال‌هایی از کاربردهای مختلف Stream را در ادامه مشاهده می‌کنید:

Stream ساده

1public void convertObjects() {
2Stream<String> objectStream = Stream.of(“Hello”, “World”);
3}

تبدیل آرایه‌ها به Stream

1public void convertStuff() {
2String[] array = {“hello”، “world”};
3Stream<String> arrayStream = Arrays.stream(array);
4}

تبدیل لیست‌های چندگانه به Stream

1public void concatList() {
2List<Integer> list1 = Arrays.asList(1، 2، 3);
3List<Integer> list2 = Arrays.asList(4، 5، 6);
4Stream.of(list1، list2) //Stream<List<Integer>>
5.flatMap(List::stream) //Stream<Integer>
6.forEach(System.out::println); // 1 2 3 4 5 6
7}

استفاده از فیلتر برای تعریف شرایطی در Stream

1public void filterNull() {
2Stream.of(2، 1، null، 3).filter(Objects::nonNull).map(num -> num)
3// without filter، you would’ve got a NullPointerExeception
4.forEach(System.out::println);
5}

استفاده از کلکتورها برای تبدیل Stream به لیست

1public void showCollect() {
2List<Integer> filtered = Stream.of(0، 1، 2، 3).filter(num -> num).collect(Collectors.toList());
3}

اجرای وظایف کاهشی

1public void showReduceSum() {
2 int[] array = {23,43,56,97,32};
3 Arrays.stream(array).reduce((x,y) -> x+y).ifPresent(s -> System.out.println(s));
4}

مرتب‌سازی داده‌ها در Stream

1public void showSort() {
2Stream.of(3, 2, 4, 0).sorted((c1, c2) -> c1 — c2).forEach(System.out::println); // 0 2 3 4
3}

نکات دیگر

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

پشتیبانی از برنامه‌نویسی واکنشی در جاوا 9

«برنامه‌نویسی واکنشی» (Reactive Programming) به طور کامل در مورد گردش داده‌ها است. گردش داده‌های ارسالی از یک کامپوننت به کامپوننت دیگر انتشار می‌یابد. باس‌های رویداد، رویدادهای کلیک، فیدهای توییتر همگی استریم رویداد همگام‌سازی شده یا استریم داده‌های مشابه محسوب می‌شوند.

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

جاوا 9 یک API به نام Flow و اینترفیس مشترک برای برنامه‌نویسی واکنشی معرفی کرده است که روشی گام به گام برای پیاده‌سازی برنامه‌نویسی واکنشی محسوب می‌شود. API-های Flow جنبه ارتباطی را پوشش می‌دهند که به ما امکان می‌دهد از برنامه‌نویسی واکنشی استفاده کنیم و به کتابخانه‌های اضافی مانند RxJava یا Project Reactor و موارد دیگر نیاز نداشته باشیم. Flow چهار اینترفیس تو در تو دارد:

<Flow.Processor<T،R

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

1public static interface Processor<T,R> extends Subscriber<T>, Publisher<R> {
2}

<Flow.Publisher<T

برای تولید/انتشار آیتم‌ها و سیگنال‌های کنترل

1@FunctionalInterface
2public static interface Publisher<T> {
3public void subscribe(Subscriber<? super T> subscriber);
4}

<Flow.Subscriber<T

گیرنده پیام‌ها باید اینترفیس مشترک را پیاده‌سازی کند تا آن پیام‌ها و سیگنال‌ها را دریافت کند.

1public static interface Subscriber<T> {
2public void onSubscribe(Subscription subscription);
3public void onNext(T item);
4public void onError(Throwable throwable);
5public void onComplete();
6}

Flow.Subscription

برای لینک کردن ناشر و مشترک

1public static interface Subscription {
2public void request(long n);
3public void cancel();
4}

<java.util.concurrent.SubmissionPublisher<T

بدین ترتیب API به نام Flow تنها یک کلاس پیاده‌سازی از ناشر دارد که <Flow.Publisher<T را پیاده‌سازی می‌کند و یک تولیدکننده آیتم‌ها است که با ابتکار استریم‌های واکنشی مطابقت دارد.

در مثالی که در بخش زیر ارائه می‌کنیم به روشنی Flow و متدهای مختلف اینترفیس‌ها به همراه کاربردهایش معرفی شده‌اند.

انتشار و مصرف پیام‌ها

در یک گردش واکنشی ساده ما یک ناشر داریم که پیام‌هایی را منتشر می‌کند و یک مشترک ساده نیز وجود دارد که پیام‌ها را به محض رسیدن مصرف می‌کند و این فرایندی یک مرحله‌ای است. ناشر یک استریم از داده‌ها را منتشر می‌کند که مشترک به صورت ناهمگام در آن ثبت‌نام کرده است.

با بررسی مثال زیر می‌بینیم که کلاس SubmissionPublisher یک Publisher را پیاده‌سازی می‌کند. این ناشر یک متد به نام ()subscribe دارد که ثبت‌نام کنندگان برای دریافت رویدادها از سوی ناشر از آن استفاده می‌کنند و متد submit در SubmissionPublisher نیز آیتم‌ها را ارائه می‌کند.

1public class FlowMain {
2public static void main(String[] args) {
3SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
4MySubscriber<String> subscriber1 = new MySubscriber<>(“One”);
5MySubscriber<String> subscriber2 = new MySubscriber<>(“Two”);
6publisher.subscribe(subscriber1);
7publisher.subscribe(subscriber2);
8publisher.submit(“Hello”);
9publisher.submit(“Universe”);
10try {
11Thread.sleep(1000);
12} catch (InterruptedException e) {
13e.printStackTrace();
14}
15publisher.close();
16}
17}

در ادامه به بررسی متدهای اینترفیس subscriber می‌پردازیم.

(onSubscribe(subcription

ناشر این متد را زمانی اجرا می‌کند که یک فرد جدید ثبت‌نام می‌کند. به طور معمول یا این فرد ذخیره می‌شود تا بعداً برای ارسال سیگنال مثلاً برای درخواست آیتم‌های بیشتر استفاده شود و یا ثبت‌نام لغو می‌شود. همچنین می‌توان بی‌درنگ از آن استفاده کرد و چند آیتم را به آن ارسال کرد. این همان کاری است که در این مثال انجام داده‌ایم.

(onNext(item

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

(onError(throwable

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

()onComplete

این آیتم زمانی اجرا می‌شود که ناشر آیتم‌های بیشتری برای ارسال نداشته باشد و بدین ترتیب ثبت‌نام تکمیل شده باشد. در ادامه پیاده‌سازی یک مشترک را مشاهده می‌کنید:

1public class MySubscriber<T> implements Subscriber<T> {
2private Subscription subscription;
3private String name;
4public MySubscriber(String name) {
5this.name = name;
6}
7@Override
8public void onComplete() {
9System.out.println(name +: Completed”);
10}
11@Override
12public void onError(Throwable t) {
13System.out.println(name +: Weird... its an error”);
14t.printStackTrace();
15}
16@Override
17public void onNext(T msg) {
18System.out.println(name +:+ msg.toString() + “ received a message”);
19subscription.request(1);
20}
21@Override
22public void onSubscribe(Subscription subscription) {
23System.out.println(name +: onSubscribe”);
24this.subscription = subscription;
25subscription.request(1);
26}
27}

این وضعیت چه تفاوتی با الگوی Observable دارد؟

سؤالی که در ای جا ممکن است مطرح شود این است که ما قبلاً کلاس Observable و اینترفیس Observer را در جاوا داشتیم. بنابراین چرا این الگوی جدید معرفی شده و چه تفاوتی دارد؟

یکی از تفاوت‌های مهم در الگوی Publisher-Subscriber این است که ناشر و مشترک همدیگر را نمی‌شناسند و ارتباط از طریق صف‌ها یا بروکرها انجام می‌شود. بنابراین دارای «تزویج سست» (loosely coupled) است. در الگوی Observable، مشاهده‌گر همه اشیا را می‌شناسد. الگوی Publisher/Subscriber در اغلب موارد به یک روش ناهمگام پیاده‌سازی می‌شود و می‌تواند روی چند اپلیکیشن یا میکروسرویس استفاده شود در حالی که الگوی observer کاملاً همگام است.

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

اگر این نوشته برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
madhupathy
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *