محمد عباسی
محمد عباسی
خواندن ۲۲ دقیقه·۴ سال پیش

جاوا، جاوا و بازهم جاوا (قسمت اول و البته آخر!!)


سلام!
خوشحالم که در اینجا هستید، من قرار است یک سلسله مقاله درمورد جاوا بنویسم و مفتخرم از اینکه می‌تونم در اینجا نتایج را با شما به اشتراک بگذارم. اما دقیقا قرار است از چه صحبت کنم؟

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

مدت‌ها خودم را با این بهانه‌ها توجیح می‌کردم که امکانات جاوا ۸ بیشتر مختص به برنامه‌نویسی فانکشنال است و فعلا ارتباطی بکار من ندارد و این تصور را هم (به غلط) داشتم که Streamها نیز مربوط به مبحث I/O می‌شود، هر زمان که در کار به این مسائل برخوردم به سراغ ‌آنها خواهم رفت. اما من حتا نگاهی درست و درخور به این مباحث نداشتم و خودم نیز میدانستم که از این بهانه‌ها نیز نمی‌توانم با قاطعیت دفاع کنم. به همین خاطر پس از مصاحبه به سراغ فراگیری امکانات جاوا ۸ رفتم!

اما یک لحظه صبر کنید! الان که این مقاله نگارش می‌شود جاوا ۱۲ هم منتشر شده است. من تقریبا ۵ نسخه از جاوایی که اکنون ارائه شده است عقب هستم، کمی برای من فاجعه‌بار است! البته از سال ۲۰۱۷ که جاوا ۹ ارائه شده است سرعت انتشار نسخه‌های مختلف بسیار شدت گرفته است، یعنی توسعه‌دهندگان زبان جاوا برای زنده نگهداشتن این زبان تمام تلاش و توان خودشان را بکار بسته‌اند، اما من استفاده کننده همچنان اندرخم جاوا ۸ هستم! اگر بخواهیم بازار زبان جاوا در ایران از بین نرود (یا کم‌رنگ نشود) باید از تمام توان این زبان که برای نیازهای روز در حال توسعه است، به درستی استفاده کنیم. فلذا تصمیم گرفتم تمام امکانات زبان جاوا را پس از نسخه‌ی ۷ یادبگیرم و ارائه دهم.

JDK 1.0 (January 23, 1996)
JDK 1.1 (February 19, 1997)
J2SE 1.2 (December 8, 1998)
J2SE 1.3 (May 8, 2000)
J2SE 1.4 (February 6, 2002)
J2SE 5.0 (September 30, 2004)
Java SE 6 (December 11, 2006)
Java SE 7 (July 28, 2011)
Java SE 8 (March 18, 2014)
Java SE 9 (September 21, 2017)
Java SE 10 (March 20, 2018)
Java SE 11 (September 25, 2018)
Java SE 12 (March 19, 2019)

دوستان، این مسیر، یک ماجراجویی فوق‌العاده و هیجان‌انگیز برای من است، دوست دارید در این راه با من همراه باشید؟ من از به اشتراک گذاشتن این مطالب نیز هیجان‌زده هستم.

مطالب قرار است چگونه باشند؟ قطعا مثل تمام مقالاتی که من می‌پسندم و تلاش می‌کنم مقالاتم آنگونه باشد، قرار است ریز و با بیان جزئیات به شکلی صریح و واضح و دقیق باشد. این مقالات سری هستند، یعنی یک سیر زمانی دارند، ولی هر موضوع مستقل گفته خواهد شد، اما این را هم درنظر داشته باشید، که خیلی از مباحث بطور ذاتی به مباحثی دیگر مرتبط هستند. بطور مثال در تشریح جاوا ۸ من تمام امکانات را به استقلال بیان خواهم کرد، ولی چطور باید Method Referenceها را فهمید وقتی فرد از Functional Method و Lambda بی‌اطلاع است؟ پس پیشنهاد من این است تا جاییکه امکانش هست از ابتدا شروع کنید.
آیا احتمال خطا وجود دارد؟ قطعا! لطفا اگر اشتباه نگارشی دیدید لطف کنید و اطلاع دهید و از آن مهم‌تر اگر اشکال فنی دیدید خواهش می‌کنم حتما زمان بگذارید و اطلاع دهید! چرا که هم من دچار اشتباه هستم و هم شاید خیلی‌های دیگر را نیز به اشتباه بندازم (اینجای کار یک وظیفه‌ی صنفی است).

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


چرا؟

چرا باید فیچرهای جدید زبان را آموخت؟ چه سوال پرواضح‌ای! اگر شما هم مثل من از آن دست آدم‌هایی هستید که تقریبا از جاوا ۷ استفاده می‌کنید، حتما میدونید که دائما در حال استفاده از امکاناتی هستید که در جاوا ۶ وجود نداشته است.
در واقع امکانات بسیار گسترده‌ای با کمک مفاهیم جدید ارائه شده است که عدم آشنایی با آنها یک ضعف محسوب می‌شود. دانستن امکانات جدید بقدری بدیهی بنظر می‌رسد که گمان می‌کنم همه می‌دانیم که فراگیری آن‌ها یک ضرورت است.
از طرفی، هرچه ما در استفاده از امکانات جدید تنبل باشیم، دنیا و برنامه‌نویسان حرفه‌ای اینگونه نخواهند بود، فلذا به مرور دست‌خط برنامه‌نویسی با زبان جاوا متحول خواهد شد. بعبارتی شما سینتکسی را در کدهای دیگران مشاهده خواهید کرد که الزامش را درک نخواهید کرد. تصور کنید، نتوانید چند خط کد را به زبانی که سال‌ها با آن سر و کله زده‌اید بخوانید! فاجعه‌ است!
برنامه‌نویسی فانکشنال هم به جاوا ۸ اضافه شده است، یعنی یک پارادایم برنامه‌نویسی به آن اضافه شده است، چطور می‌توان از کنار آن به راحتی گذشت و بی‌اهمیت بود؟
کدها در نسخه‌های جدید، ساده‌تر و گویا‌تر هم شده‌اند! می‌خواهید از کنار آنها ساده‌ بگذرید؟ مطمئنم که چنین نخواهید کرد، در غیر این‌صورت اینجا حضور نداشتید!
هرچه بیشتر این دلایل را ذکر می‌کنم، بیشتر عذاب وجدان آزارم می‌دهد که چرا تا مدت‌ها نسبت به آن بی‌اهمیت بوده‌ام! بگذارید این عذاب را به این نحو (نگارش این سلسله مقالات) به پایان برسانم!


آنچه در ادامه خواهید دید!

بنظرم بد نیست نگاهی به ویژگی‌ها و امکاناتی که به نسخه‌ی ۷ اضافه شد بیاندازیم، بعبارتی بهتر از نسخه‌ی ۷ شروع کنیم. نسخه‌ای که در سال ۲۰۱۱ منتشر شد و باعث خوشحالی برنامه‌نویسان جاوا ۶ بود.
پس پیش‌به‌سوی تشریح امکاناتی که در جاوا ۷ ارائه شد!

فهرست

  • پروژه‌ی Coin
  • ویژگی Diamond Operator
  • استفاده از رشته در عبارات switch
  • مدیریت خودکار منابع
  • فرم‌دهی زیبای اعداد
  • ذخیره‌ی باینری
  • بهبود مدیریت خطا
  • کتابخانه‌ی جدید برای کار با فایل‌سیستم (NIO 2.0)



پروژه‌ی Coin

جاوا ۷ چندین ویژگی در قالب پروژه‌ی Coin به زبان اضافه کرد. این ویژگی‌ها برای برنامه‌نویسان بسیار کاربردی بود. اما پروژه‌ی Coin دقیقا چیست؟ از فوریه‌ی سال ۲۰۰۹ تا مارس ۲۰۰۹ فراخوانی داده شد تا افراد پروپزال‌های خودشان را برای اضافه کردن به زبان جاوا ارسال کنند، نزدیک به ۷۰ پروپزال ارسال شد که از این بین ۵ پروپزال نهایی شد تا در قالب پروژه‌ی Coin در جاوا ایجاد شوند. در نهایت هم تصمیم گرفته شد این ۵ پروپزال نهایی به JDK 7 اضافه شود.
هدف این پروژه شناسایی مجموعه تغییرات کوچک در زبان بود. در آخر لیست زیر نهایی شد:

  • Strings in switch
  • Binary integral literals and underscores in numeric literals
  • Multi-catch and more precise rethrow
  • Improved type inference for generic instance creation (diamond operator)
  • try-with-resources statement
  • Simplified varargs method invocation

در ادامه تک تک این ویژگی‌ها را به تفصیل بحث خواهیم کرد. صرفا می‌خواستم بگویم بخشی از تغییرات در قالب پروژه‌ی Coin به جاوا ۷ اضافه شدش.


ویژگی Diamond Operator

می‌دانیم وقتی که با Genericها کار می‌کنیم باید Type آنها را مشخص کنیم. بطور مثال وقتی یک HashMap تعریف می‌کنیم که کلید آن String و مقدار آن Integer است باید به شکل زیر بنویسیم:

Map<String, Integer> myMap = new HashMap<String, Integer>();

خب همانطور که مشاهده می‌کنید Type در هر دو طرف عملگر انتساب (=) مشخص شده است. که خب البته کمی پرگویی بی‌فایده بنظر می‌رسه. بنظرتون کامپایلر نمی‌تونه از سمت چپ تشخیص بده Type آبجکت چی هست تا مجبور نباشیم در سمت راست آن را تکرار کنیم؟ جواب این است که تا جاوای ۷ خیر، کامپایلر نمی‌توانست! اما از جاوای ۷ به بعد کافی است که فقط سمت چپ را بنویسید. پس در جاوای ۷ چنین می‌نویسیم:

Map<String, Integer> myMap = new HashMap<>();

بخاطر اینکه صرفا یک <> خالی می‌گذاریم، به آن عملگر لوزی می‌گویند (شبیه به لوزی است). حتا می‌توانیم از گذاشتن عملگر لوزی هم صرف نظر کنیم، در این صورت کامپایلر صرفا اخطارهای type-safety تولید خواهد کرد:

Map<String, Integer> myMap = new HashMap ();

استفاده از رشته در عبارات switch

عبارت‌های switch یا انواع primitive را پشتیبانی می‌کردند یا انواع enumerated. اما از جاوا ۷ نوع دیگری به آنها اضافه شد، یعنی نوع String!
پیش از جاوا ۷ برنامه‌نویس مجبور بود برای مقایسه‌ی چندین رشته از if-else استفاده کند، اما از جاوا ۷ به بعد می‌تواند رشته‌ها را در عبارت سوییچ بکار گیرد. به مثال زیر توجه کنید:

switch (token) { case (&quotone&quot): return &quotToken one identified" case (&quottwo&quot): return &quotToken one identified" case (&quotthree&quot): return &quotToken one identified" case (&quotfour&quot): return &quotToken one identified" default: return &quotNo token was identified" }


مدیریت خودکار منابع

یکی از بهترین ویژگی‌هایی که در جاوا ۷ اضافه شد و منجر شد خیلی از سهل‌انگاری‌های برنامه‌نویسان نادیده گرفته شد همین ویژگی try-with-resources می‌باشد. پیش‌تر (قبل از جاوا ۷) برنامه‌نویس منابعی را که در بلاک try-catch گرفته بود، به کمک بلاک finally رها می‌کرد. البته آزادسازی در بلاک finally اجباری نبود، ولی رهاسازی منابع اخذ شده اجباری بود (حال به هر شکلی). اما از نسخه‌ی ۷ به بعد اگر برنامه‌نویس منابع را به کمک try-with-resources اخذ کند، خود کامپایلر در انتهای کار منابع را به درستی آزاد می‌کند و چنانچه برنامه‌نویس هم فراموش کند، مشکلی پیش نخواهد آمد.
اما چطور این اتفاق می‌افتد؟ پاکسازی به کمک اینترفیس جدیدی (در نسخه‌ی جاوا ۷) بنام AutoCloseable رخ می‌هد. متد close این اینترفیس توسط خود JVM فراخوانی می‌شود، درست در پایان بلاک try.
توجه کنید، وقتی منبعی را به کمک try-with-resources اخذ می‌کنید، دیگر خودتان نباید بصورت دستی متد close() آن منبع را فراخوانی کنید. خود JVM این کار را انجام خواهد داد. دستی فراخوانی کردن آن منجر به رفتارهای غیرقابل انتظار خواهد شد.

public class ResourceManagementInJava7 { public static void main(String[] args) { try (BufferedReader br = new BufferedReader(newFileReader(&quot/tmp/test.txt&quot))) { String sCurrentLine; while ((sCurrentLine = br.readLine()) != null) { System.out.println(sCurrentLine); } } catch (IOException e) { e.printStackTrace(); } } }

اگر کد بالا کمی شلوغ و گیج کننده است به سینتکس زیر دقت کنید. تنها کافی است که منبع را در پرانتزی مقابل try اخذ کنیم:

try(resources_to_be_cleant) { // your code }

نکته‌ی حائز اهمیت این است که اگر قرار باشد چندین منبع را اخذ کنیم، آنها را با سیمی‌کالن (;) جدا خواهیم کرد.
تکرار کنم که هر منبعی که اینترفیس AutoCloseble را implement کرده باشد، می‌تواند درون try-with-resources قرار بگیرد. این اینترفیس سوپرکلاسِ اینترفیس java.io.Closeable است که تنها یک متد close() دارد که توسط خود JVM فراخوانی می‌شود.


فرم‌دهی زیبای اعداد

اعداد با ارقام بالا، مخصوصا صفرهای زیاد چشم را آزار می‌دهند. مثلا 100000000 چند صفر دارد؟ چه عددی است؟ ما معمولا ارقام را به شکل 100,000,000 مشاهده می‌کنیم و به کمک جدا کننده‌ای (,) براحتی تشخیص می‌دهیم که چه عددی است.
یکی از ویژگی‌های جالبی که در جاوا ۷ اضافه شد، و من کمتر در کدهای دیگران میبینم که استفاده بشود، امکان استفاده از خط زیرین (_) در اعداد است. بطور کلی خط زیرین (یا همان Underscore) در اعداد نادیده گرفته می‌شود (یعنی عدد را عوض نمی‌کنند) و صرفا برای فرم‌دهی و خواناتر شدن برای انسان است. بطور مثال شما می‌توانید در کد جاوا عدد 100000000 را با خط‌زیرین خوانا تر کنید.

/** * Supported in int * */ int improvedInt = 10_00_000; /** * Supported in float * */ float improvedFloat = 10_00_000f; /** * Supported in long * */ float improvedLong = 10_00_000l; /** * Supported in double * */ float improvedDouble = 10_00_000;

از این قابلیت می‌توانید در نمایش اعداد باینری هم استفاده کنید. البته خود باینری‌ها خودشان یک موضوع قابل بحث جدا در جاوا ۷ هستند، به آن می‌پردازیم!


ذخیره‌ی باینری

در جاوا ۷ شما این امکان را دارید که مقادیر باینری (دودویی) را با پیشوند '0b' یا '0B' در متغییرهای عددی نظیر byte، short، int و long ذخیره کنید. پیش از JDK 7 فقط امکان استفاده از مقادیر اُکتال (با پیشوند '0') یا هگزادسیمال (با پیشوند '0x' یا '0X') وجود داشت.
با استفاده از این ویژگی دیگر برنامه‌نویس مجبور به convert این مقادیر نخواهد بود.

int sameVarOne = 0b01010000101; //or if use the number formatting feature as well. int sameVarTwo = 0B01_010_000_101;

همانطور که ملاحظه می‌کنید امکان فُرم‌دهی با خطِ‌زیرین هم محیا می‌باشد.


بهبود مدیریت خطا

چند اتفاق خوب تو بخش مدیریت‌ خطاها رخ دادش. جاوا ۷ امکان multi-catch رو برای catch گرفتن چندین نوع از exceptionها تنها با یک خط بلاک catch را اضافه کرد.
اجازه دهید با یک مثال آن را شرح دهم، فرض کنید متدی دارید که ۳ اکسپشن پرتاب می‌کند. در این وضعیت بطور معمول (تا نسخه‌ی ۶) باید چنین کرد:

public voidoldMultiCatch() { try{ methodThatThrowsThreeExceptions(); } catch(ExceptionOne e) // log and deal with ExceptionOne } catch(ExceptionTwo e) { // log and deal with ExceptionTwo } catch(ExceptionThree e) { // log and deal with ExceptionThree } }

همانطور که مشاهده می‌کنید میتوانید بی‌شمار اکسپشن‌ را یکی پس از دیگری catch کنید. البته حتما به خاطر دارید که ترتیب اکسپشن‌ها نیز اهمیت دارد. این رفتار (تعداد زیادی بلاک catch) بشدت خطا پرور است! جاوا ۷ امکانی اضافه کرد در زبان تا این شکل زشت تغییر یابد، نگاهی به کد تغییر کرده در جاوا ۷ بیاندازید:

public voidnewMultiCatch() { try{ methodThatThrowsThreeExceptions(); } catch(ExceptionOne | ExceptionTwo | ExceptionThree e) { // به این خط دقت کنید // log and deal with all Exceptions } }

خب، همانطور که مشاهده می‌کنید چندین اکسپشن تنها در یک بلاک catch با استفاده از عملگر '|' مشخص شده است.
شاید برای شما یک سوال پیش بیاید، اگر بخواهیم برای یکی از اکسپشن‌ها رفتاری جدا درنظر بگیریم چکار باید کرد؟ جواب ساده است، باید از multi multi-catch استفاده کرد. یعنی آن دسته از اکسپشن‌هایی را که قرار است رفتار مشابهی با آنها داشته باشیم را با عملگر | در یک بلاک قرار می‌دهیم و آن‌هایی را که می‌خواهیم جدا کنیم را نیز همچون سابق یک بلاک catch جدا برای آن درنظر می‌گیریم. کد زیر را نگاه کنید:

public voidnewMultiMultiCatch() { try{ methodThatThrowsThreeExceptions(); } catch(ExceptionOne e) { // log and deal with ExceptionOne } catch(ExceptionTwo | ExceptionThree e) { // log and deal with ExceptionTwo and ExceptionThree } }

خب همانطور که واضح است در مثال بالا ExceptionTwo و ExceptionThree در یک بلاک قرار داده شده است و به یک شکل با‌ آنها رفتار می‌شود اما ExceptionOne جدا در نظر گرفته شده است و منطقی منحصربه‌فرد برای مدیریت اکسپشن دارد.


کتابخانه‌ی جدید برای کار با فایل‌سیستم (NIO 2.0)

برای کار با فایل‌ها از خیلی قدیم‌ترها پکیجی وجود داشت بنام java.io که احتمالا با آن آشنا هستید، شاید هم نباشید! اما بعدتر، در جاوا ۴ (سال ۲۰۰۲) پکیج جدیدی عرضه شد بنام java.nio که منظورشان از nio همان new io بوده است. که خب امکانات بیشتری نسبت به پکیج io به آن اضافه شده بود.
اما در نهایت در جاوای ۷، پکیجی بنام java.io.file ارائه شد که سعی می‌کرد محدودیت پکیج‌های io نظیر کپی کردن فایل‌ها را برطرف کند. به این پکیج جدید (یعنی java.io.file) به اختصار nio 2.0 هم می‌گویند.
این پکیج شامل کلاس‌ها و اینترفیس‌های مهمی نظیر Path، Paths، FileSystem، FileSystems و Files است. که در ادامه به تفصیل آن‌ها را بررسی خواهیم کرد.

  • کلاس Paths و اینترفیس Path

کلاس Paths یک کلاس کمکی است که تنها یک متد استاتیک بنام get دارد. متد get() یک آدرس را می‌گیرد و یک شی از نوع Path بر می‌گرداند. هر شی از نوع Path هم اطلاعاتی در مورد فایل یا دایرکتوری مدنظر دارد.

Path path = Paths.get(&quot/home/mujan/Desktop/file1&quot);

این کلاس و اینترفیس در بسته‌ی java.nio.file هستند، یعنی در جاوای ۷ اضافه شدند، فلذا توصیه می‌شود از این پس بجای استفاده از کلاس File، از امکانات جدید جاوا ۷ استفاده کنید. البته متدهایی نیز برای تبدیل آبجکت‌های Path به File و بلعکس نیز وجود دارد که برای ایجاد سازگاری با نسخه‌های جدید یا قدیم می‌توانید استفاده کنید. متدهای toPath و toFile برای این منظور است.

Path path = Paths.get(&quot/home/mujan/Desktop/file1&quot); File file = Path.toFile(path);
  • کلاس Files

کلاس Files هم در بسته‌ی java.nio.file قرار دارد و مملو از متدهای استاتیک کاربردی و متنوع است. با کمک متدهای این کلاس می‌توانید فایل کپی کنید، جابجا کنید، بنویسید، حذف کنید، ویژگی‌های آن را دریافت کنید یا دایرکتوری ایجاد کنید و خیلی از کارهای دیگر! باز هم توصیه می‌شود بجای استفاده از کلاس File از کلاس Files استفاده کنید.
خب، به‌گمانم هیچ چیز مثل مثال نمی‌تواند ویژگی‌های این کلاس پرکاربرد را نمایش دهد. البته دقت داشته باشید که برای کار با کتابخانه‌های io حتما باید exception را هندل کنید (حال یا با try-catch یا throws کردن). ولی من در اینجا فقط خط به خط کاربردهای کلاس را لیست می‌کنم و کاری به این مسائل نخواهم داشت.

Files.createDirectory(Paths.get(&quot/home/mujan/new_dir&quot);

کد بالا همانطور که مشخص است منجر به ایجاد یک دایرکتوری (new_dir) در آدرس /home/mujan می‌شود.

میدونید Symbolic Link چیه؟ اگر کاربر ویندوز هستید یک چیز مشابه به shortcutها هستش. بچه‌های لینوکسی احتمالا با symbolic linkها کار کردند. با کلاس Files امکان ایجاد سیمبولیک لینک هم وجود داره:

File.createSymbolicLink(Paths.get(&quot/home/dest&quot), Paths.get(&quot/home/src&quot));

اما برای خواندن و نوشتن چکار باید کرد؟ به قطعه کد زیر دقت کنید، ابتدا یک شی از کلاس Path بنام src ایجاد می‌کنم که یک فایل text می‌باشد، سپس یکبار آن را بصورت باینری می‌خوانم و بعد بصورت text و همچین قابل write بودن و نبودن آن را نیز تست می‌کنم. سپس سعی می‌کنم که فایلی مشابه آن در جای دیگری بنویسم، و در نهایت یک کپی از فایل ایجاد می‌کنم:

Path src = Paths.get(&quot/home/mujan/text&quot); byte[] bytes = Files.readAllBytes(src); List<String> strings = Files.readAllLines(src); boolean check = Files.isWritable(src); Files.write(byte, Paths.get(&quot/home/mujan/anotherBytesFile&quot)); // save Bytes Files.write(string, Paths.get(&quot/home/mujan/anotherStringFile&quot)); // save String Files.write(string, Paths.get(&quot/home/mujan/otherStrinFile&quot), StandardOpenOption.APPEND)); * Files.copy(src, Paths.get(&quot/home/mujan/anothorFile&quot));

خب به کدهای بالا نگاه کنید، تقریبا واضح و شفاف هستند. دقت کردید که عملیات خواندن از فایل با کمک متد readAllLines و readAllBytes چقدر راحت است؟ یا همچنین عملیات نوشتن. اگر پیاده‌سازی‌های آن را هم نگاه کنید خیلی بهینه نوشته شدند و از buffered reader و writer هم استفاده کردند.
اما اجازه دهید توضیحی کوتاه راجع به خطی بدهم که با * مشخص کرده‌ام. در این خط با یک رشته را می‌نویسیم ولی یک Open Option هم بعنوان پارامتر پاس می‌دهیم. با کمک Open Optionها مشخص می‌کنیم که میخواهیم چه رفتاری با فایل شود. آیا اطلاعات جدید در فایل append شوند (اضافه شوند به انتهای فایل) یا بر روی فایل overwrite شوند؟ این قبیل رفتارها را میتوانید با Open Optionها مشخص کنید.

  • اعلان تغییرات فایل

کار ما با بسته‌ی NIO 2 تمام نشده است. یکی از امکانات جالب و کمتر شناخته‌شده‌ی جاوا ۷، امکان اعلان تغییرات فایل است. از آن دست فیچرهایی هم هست که مدت زیادی منتظر ماند تا در نهایت در کتابخانه‌ی NIO 2 عرضه شد. من در ادامه در مورد اینترفیس WatchService صحبت خواهم کرد.
دقت کردید وقتی تو IDE کد می‌زنید تغییرات فایل‌ها رو بهتون نشون میده؟ یعنی مثلا فایلی که تغییر میدید رو با ی رنگ دیگه نشون میده. چطور این کار رو انجام میده؟ اون‌ها دارند از امکانی بنام file change notification استفاده می‌کنند که تقریبا در تمام فایل‌سیستم‌ها وجود دارد. بطور کلی می‌توان کدی نوشت که فایل‌سیستم را مانیتور کند که آیا دایرکتوری یا فایل خاصی تغییر می‌کند یا خیر. اما این راه‌حل برای زمانیکه تعداد فایل‌ها و دایرکتوری‌هایی که می‌خواهیم رصد کنیم به صد یا هزار می‌رسد، مناسب نیست. بعبارتی این سولوشن scale پذیر نیست.
در کتاب‌خانه‌ی NIO 2 در جاوا ۷، APIای بنام WatchService عرضه شد که یک راه‌حل قابل توسعه برای رصد کردن تغییرات فایل‌ها و دایرکتوری‌ها ‌می‌باشد. خب یک API ساده و شفاف است، برای این کار بهینه نوشته شده و دیگه نیازی نیست ما خودمون چیزی را پیاده سازی کنیم. اما چطور کار می‌کند؟
برای استفاده از امکان WatchService، اولین گام ایجاد یک نمونه‌ (instance) از WatchService با استفاده از کلاس java.nio.file.FileSystems می‌باشد.

WatchService watchService = FileSystems.getDefault().newWatchService();

سپس باید شی‌ای از جنس Path ساخت، که آدرس دایرکتوری مدنظر ما برای مانیتور کردن را در خود دارد:

Path path = Paths.get(&quot/home/mujan/myDirForMonitor&quot);

بعد از این گام، وقت آن است که این path را در watch service ثبت (register) کنیم. توی این مرحله، ۲ مفهوم مهم وجود دارد که باید درک کنید، یک کلاس StandardWatchEventKinds و کلاس WatchKey. قطعه کد زیر برای ثبت path را نگاه کنید تا ببینید که چطور از آن‌ها استفاده شده است، تا در ادامه در مورد آنها به تفصیل صحبت کنم:

WatchKey watchKey = path.register(watchService, StandardWatchEventKinds...);

خب دو موضوع مهم و قابل توجه این‌جا مطرح است: اول فراخوانیِ متدِ ثبت آدرس (path registration) که بعنوان اولین پارامتر نمونه‌ی (instance) کلاس watch service را میگیرد و سپس یک VarArgs از StandardWatchEventKindsها. موضوع مهم دوم retrun type فرایند ثبت است که یک نمونه (instance) از WatchKey می‌باشد. خب بریم همانطور که قول دادم این ۲ موضوع را به تفصیل باز کنیم.

کلاس StandardWatchEventKinds، کلاسی است که نمونه‌های آن به watch service می‌گوید که چه نوع از رخدادها (events) را برای دایرکتوری مدنظر درنظر بگیرد (یعنی حواسش به چه اتفاق‌هایی باشد، یا به بعبارتی نسبت به چه اتفاق‌هایی واکنش نشان دهد). چهار رخداد برای نظاره کردن (watch) وجود دارد.

  • رخداد StandardWatchEventKinds.ENTRY_CREATE: زمانی رخ می‌دهد که یک موجودیت جدید در دایرکتوری تحت نظر ایجاد شود. ممکنه مربوط به ساخت یک فایل جدید باشد یا تغییر نام یک فایل موجود.
  • رخداد StandardWatchEventKinds.ENTRY_MODIFY: زمانی رخ میدهد که یک موجودیت موجود در دایرکتوری مدنظر تغییر کند. تمام انواع تغییرات یک فایل این رخداد را اجرا می‌کند. توی بعضی از پلتفرم‌ها حتا تغییر ویژگی‌های فایل هم این رخداد را اجرا می‌کند.
  • رخداد StandardWatchEventKinds.ENTRY_DELETE: زمانی رخ می‌دهد که یک موجودیت در دایرکتوی تحت نظر پاک شود، تغییرنام دهد و یا جابه‌جا شود.
  • رخداد StandardWatchEventKinds.OVERFLOW: برای رخدادهای گم‌شده و درنظرگرفته نشده اتفاق‌ می‌افتد. ما کاری به آن نداریم :)

اما کلاس WatchKey. این کلاس وظیفه ی ثبت دایرکتوری با watch service را دارد. نمونه یا شی آن رخدادهایی که اتفاق افتاده‌اند را هم نمایش می‌دهد. وقتی ی رخدادی پیش میاد watch service به ما متد callback به ما نمی‌دهد. ما فقط می‌تونیم به چند روش poll کنیم.
استفاده از poll API:

WatchKey watchKey = watchService.poll();

این API بهمون میگه که چه رخدادی اتفاق افتاده است، و اگر هم چیزی رخ نداده باشد null برمی‌گرداند.

WatchKey watchKey = watchService.poll(long timeout, TimeUnit units);

این API هم همینطور، با این تفاوت که اگر رخدادی نباشد مدت زمانی را منتظر می‌ماند و درجا null برنمی‌گرداند. و در نهایت آخرین متد:

WatchKey watchKey = watchService.take();

این API متفاوت رفتار می‌کند: تا زمانی که رخدادی اتفاق بیافتد صبر می‌کند (بلاک می‌کند).

نکته‌ی بسیار مهم در رابطه با شی WatchKey این است که چه با poll یا چه با take ریترن (return) شود، دیگر رخدادی را ثبت نمی‌کند مگر این که متد reset آن فراخوانی شود:

watchKey.reset();

به این معنا که هربار پس از poll شدن، شی watch key از صف سرویس watch خارج می‌شود. فراخوانی متد reset دوباره برای ثبت رخدادهای دیگر شی watch key را درون صف قرار می‌دهد.

خب اجازه دهید یک مثال کامل با استفاده از این سرویس نشان دهیم تا تمام ابهامات احتمالی مرتفع شود. کد زیر را خوب نگاه کنید:

import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; public class Main { public static void main(String[] args) throws IOException, InterruptedException { WatchService watchService = FileSystems.getDefault().newWatchService(); Path path = Paths.get(&quot/home/mujan/Desktop&quot); path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); WatchKey key; while ((key = watchService.take()) != null) { for (WatchEvent<?> event : key.pollEvents()) { System.out.println(&quotEvent kind:&quot + event.kind() + &quot. File affected: &quot + event.context() + &quot.&quot); } key.reset(); } watchService.close(); } }




حرف نهایی و چرا این قسمت، قسمت نهایی شد؟

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

در این مدت به دلایلی بنده از زبان جاوا و تکنولوژی جاوا فاصله گرفتم، و به سمت C# و .Net Core رفتم :دی

صرفا برای اینکه شاااید این مطالب به درد کسی بخورد، آنها را منتشر کردم، ولی برای من ادامه آن بی معنا است!

از این پس نیز مقاله جاوایی نخواهیم داشت. مگر بر علیه آن :دی

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