آموزش برنامه نویسی جاوا: چند مبحث پراکنده

چهارشنبه ۹ تیر ۱۳۹۵ - ۱۶:۳۰
مطالعه 12 دقیقه
ما با چگونگی روش‌های مقداردهی اولیه‌ در جاوا آشنا شدیم. در این جلسه جدا از آموزش مفاهیم جدید، نگاهی گذرا به مفاهیم قبلی خواهیم داشت و سپس آن‌ها را تکمیل می‌کنیم. به عنوان مثال سازنده یا Constructor یکی از روش‌های مقداردهی اولیه است که استفاده از چندین Constructor در برنامه، کاری بسیار رایج است. هنگامی که چندین سازنده را در برنامه می‌نویسیم، اصطلاحا می‌گوییم سازنده را Overload کرده‌ایم. در ادامه‌ی آموزش با این مفهوم آشنا می‌شوید.
تبلیغات

همانطور که قبلا هم گفته شد، سازنده یا Constructor نوع خاصی از متد است که همنام با نام کلاس است و مقدار برگشتی نیز ندارد (حتی void). هنگامی که از روی یک کلاس، آبجکتی را ایجاد می‌کنیم، سازنده‌ی کلاس مورد نظر را فراخوانی کرده‌ایم. این سازنده ممکن است در کلاس مورد نظر نوشته نشده باشد، اگر هیچ سازنده‌ای در کلاس نوشته نشده باشد، جاوا به صورت خودکار یک سازنده‌ی پیش فرض بدون پارامتر تولید می‌کند. اما اگر برنامه نویس خودش به صورت دستی سازنده‌ای را در کلاس تعریف کند، جاوا دیگر سازنده‌ای تعریف نمی‌کند و Constructor‌ را که برنامه نویس تعریف کرده است در نظر می‌گیرد. به کُد زیر توجه کنید:

package ir.zoomit;public class Test { private int age;

// Constructor public Test(int a) { age = a; } }

در کد بالا ابتدا یک متغیر (ویژگی یا فیلد) با نام age و سپس یک سازنده نیز تعریف کرده‌ایم که ویژگی age را Set می‌کند. در هر زمانی که ما بخواهیم کلاس Test را new کنیم، حتما باید یک عدد از نوع عدد صحیح به عنوان پارامتر سازنده در نظر بگیریم. در غیر اینصورت با خطای کامپایل مواجه می‌شویم و برنامه‌ی ما کامپایل نمی‌شود. اما فرض کنید که در کلاس Test یک متد getter برای ویژگی age تعریف شده است و ما می‌خواهیم متد getter را فراخوانی کنیم. بنابراین برای فراخوانی متد getter، حتما نیاز به یک آبجکت از جنس کلاس Test داریم. مسلما همانطور که در آموزش‌های قبلی توضیح داده شده، برای دسترسی به آبجکت کلاس Test، حتما باید کلاس Test را new کنیم. همانطور که مشخص است، کلاس Test یک سازنده بیشتر ندارد و آن یک سازنده هم یک پارامتر دارد که هنگام فراخوانی سازنده، حتما باید مقداری را برای پارامتر در نظر بگیریم. اما ما نمی‌خواهیم که این کار را انجام دهیم و فقط می‌خواهیم به آبجکت کلاس Test دسترسی پیدا کنیم. راه حل چیست؟ راه حل این است که در کلاس Test یک سازنده‌ی دیگری تعریف کنیم که پارامتر نداشته باشد، در این صورت هنگام new کردن کلاس Test، از سازنده‌ای استفاده می‌کنیم که پارامتر ندارد و دیگر نیازی نیست که عدد صحیح به عنوان پارامتر سازنده در نظر بگیریم. به کُد زیر توجه کنید:

package ir.zoomit;public class Test { private int age; public Test(int a) { age = a; } public Test() { }}

همانطور که مشاهده می‌کنید، در کلاس Test یک Constructor دیگری تعریف کردیم که هیچ پارامتری ندارد. در اینجا اصطلاحا سازنده را Overload کرده‌ایم. Overload کردن به این معنی است که در یک کلاس، متد‌هایی همنام تعریف می‌کنیم، اما این متد‌های همنام، اصطلاحا امضا‌های متفاوتی دارند. امضا‌های متد‌ها کدام است؟ امضا‌های متد‌ها پارامتر‌های متد‌ها است. یعنی ما در کلاسمان می‌توانیم به هر تعداد متد همنامی که می‌خواهیم تعریف کنیم، اما پارامتر‌های مختلفی داشته باشد. توجه کنید اگر پارامتر‌های آن یکسان باشد، با خطای کامپایل مواجه می‌شویم. همانطور هم که گفته شد، سازنده نوعی متد خاص در جاوا است، بنابراین سازنده را هم می‌توانیم Overload کنیم.

نکته‌ی بسیار مهمی که وجود دارد این است که ما هنگامی که یک متد یا سازنده را در کلاس Overload می‌کنیم، و بعدا می‌خواهیم یکی از متد‌ها یا سازنده‌ها را فراخوانی کنیم، جاوا از کجا می‌فهمد که ما می‌خواهیم به عنوان مثال از متد یا سازنده‌ای استفاده کنیم که یک پارامتر از نوع عدد صحیح دارد و نه متد یا سازنده‌ای که هیچ پارامتری ندارد؟ نکته‌ی بسیار مهمی است، اما تشخیص آن هم بسیار ساده است. همانطور که گفته شد ما نمی‌توانیم دو (یا بیشتر) متد همنام با امضا‌های یکسان (پارامتر‌های یکسان) تعریف کنیم، و جاوا هم از پارامتر‌های متد‌ها تشخیص می‌دهد که ما می‌خواهیم از چه متد یا سازنده‌ای استفاده کنیم. به تصویر زیر توجه کنید:

Overload

ما از محیط توسعه‌ی اکلیپس استفاده می‌کنیم. بعد از اینکه کلیدواژه‌ی new را نوشتیم، نام کلاسی که می‌خواهیم از روی آن آبجکتی تولید شود را می‌نویسیم و سپس دکمه‌های ترکیبی CTRL + SPACE را نگه می‌داریم. اکلیپس پیشنهاداتی را می‌دهد. اگر دقت کنید در عکس دو کاری که می‌توانیم با کلاس Test انجام دهیم را با یک مستطیل آبی رنگ مشخص کرده‌ایم. پیشنهاد اول استفاده از سازنده‌ای بدون پارامتر است و پیشنهاد دوم استفاده از سازنده‌ای با یک پارامتر که باید عدد صحیحی را به آن پاس دهیم. اگر گزینه‌ی دوم را انتخاب کنیم، حتما باید یک عدد صحیح در نظر بگیریم، اما از آنجایی که گفتیم قصد ما فقط ایجاد یک شی است، بنابراین از گزینه‌ی اول استفاده می‌کنیم. در آخر کُد کلاس MainApp به صورت زیر است:

package ir.zoomit;public class MainApp { public static void main(String[] args) { Test test = new Test(); }}

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

پس اگر بخواهیم به طور خلاصه مفهوم Overload کردن را توضیح دهیم، این است که هنگامی که چند متد همنام، ولی با امضا‌های مختلف (پارامتر‌های مختلف) در یک کلاس را بنویسیم، اصطلاحا متد را Overload کرده‌ایم. به کُد زیر توجه کنید:

package ir.zoomit;public class Test { public void show() { System.out.println("Show without parameter."); } public void show(int age) { System.out.println("Show with one parameter (Integer)"); } public void show(String name) { System.out.println("Show with one parameter (String)"); } public int show(int a, int b) { return (a + b); }}

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

Destructor

تا این مرحله به طور کامل با مفهوم سازنده یا Constructor آشنا شده‌ایم. سازنده وظیفه‌ی ایجاد اشیا را بر عهده دارد. همانطور که دقت کردید، با new کردن، سازنده‌ی کلاس مورد نظر را فراخوانی می‌کنیم. اما در مقابل سازنده که وظیفه‌ی ایجاد اشیاء را بر عهده دارد، موجود دیگری وجود دارد با نام Destructor یا نابودگر که شی را آزاد می‌کند. یعنی به همان صورتی که برنامه نویس شی را ایجاد می‌کند، خودش هم باید شی را از بین ببرد. ابتدا این خبر خوب را بدانید که در جاوا موجودی به نام Destructor وجود ندارد و در زبان‌های برنامه نویسی دیگری مثل ++C وجود دارد. اما چه چیزی جایگزین Destructor در جاوا است؟ به هر حال آبجکت‌های مُرده باید از بین بروند. در جاوا موجودی وجود دارد با نام زباله روب یا Garbage Collector که وظیفه‌ی آزاد سازی اشیا را بر عهده دارد و برنامه نویس اصلا درگیر این ماجرا‌ها نمی‌شود و در جاوا امکان تعریف Destructor وجود ندارد.

با توجه به اینکه در جاوا Destructor وجود ندارد، اما یک متدی وجود دارد با نام ()finalize که می‌توانیم آن را در هر کلاسی پیاده سازی کنیم. هر زمانی که زباله روب (Garbage Collector) بخواهد اجرا شود تا اشیا را آزاد کند یا اشیا را بکُشد، دقیقا قبل از اجرای زباله روب، متد ()finalize را اجرا می‌کند تا اگر شی حرف آخری برای گفتن دارد را بگوید و بعد از بین برود. بنابراین اگر Garbage Collector اجرا شود، متد ()finalize نیز اجرا می‌شود، در غیر این صورت متد ()finalize اجرا نمی‌شود. به کُد زیر توجه کنید:

package ir.zoomit;public class MainApp { private int age; public MainApp(int a) { age = a; } public void finalize() { System.out.println("Finalize..."); } public static void main(String[] args) { function(); } public static void function() { MainApp test = new MainApp(22); System.out.println(test.age); }}

برنامه‌ی بالا بسیار ساده است. یک فیلد برای کلاس با نام age تعریف کرده‌ایم، یک سازنده نوشته‌ایم و داخل سازنده مقدار فیلد age را Set کرده‌ایم و سپس یک متد با نام function تعریف کرده‌ایم که این متد ابتدا یک آبجکت از روی کلاس MainApp ایجاد می‌کند و مقدار عددی 22 را برای پارامتر سازنده در نظر می‌گیرد و بعد هم مقدار age را خروجی استاندارد چاپ می‌کند. در این کلاس متد ()finalize نیز تعریف شده است و پیاده سازی ساده‌ای هم دارد. یعنی اگر متد ()finalize اجرا شود، در خروجی استاندارد عبارت ...Finalize چاپ می‌شود. اما اگر این برنامه را به شکلی که اینجا است اجرا کنید، در خروجی استاندارد فقط مقدار فیلد age چاپ می‌شود و متد ()finalize اجرا نمی‌شود. علت چیست؟ مگر مدیریت حافظه به دست زباله روب نیست؟ پس چرا اجرا نمی‌شود؟ علت این است که این برنامه آنقدر کوتاه است که Garbage Collector وقت نمی‌کند اجرا شود و برنامه سریعا تمام می‌شود. اما ما می‌توانیم به صورت دستی Garbage Collector را مجبور کنیم تا اجرا شود. بنابراین کُد ما به صورت زیر تغییر می‌کند.

package ir.zoomit;public class MainApp { private int age; public MainApp(int a) { age = a; } public void finalize() { System.out.println("Finalize..."); } public static void main(String[] args) { function(); System.gc(); } public static void function() { MainApp test = new MainApp(22); System.out.println(test.age); }}

به بدنه‌ی متد main در کد بالا توجه کنید. ما با نوشتن کُد:

System.gc();

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

نکته: اجرای دستی Garbage Collector کار توصیه شده‌ای نیست و باید اجازه داد تا خود جاوا هر زمانی که صلاح دید، زباله روب را فراخوانی کند.

چرا ازمتد ()finalize استفاده می‌کنیم؟

درست است که آزاد سازی حافظه توسط زباله روب انجام می‌شود، اما دقت کنید که فقط آزاد سازی حافظه توسط زباله روب انجام می‌شود و Garbage Collector کار دیگری انجام نمی‌دهد. ممکن است یک شی، یکی از فایل‌های سیستم را در اختیار داشته باشد و در حال نوشتن و خواندن داده از آن باشد. اگر این شی قبل از اینکه فایل اشغال شده را آزاد کند، توسط Garbage Collector از حافظه پاک شود، فایل همچنان اشغال باقی می‌ماند و توسط آبجکت‌های دیگر قابل دسترسی نخواهد بود. بنابراین در اینجا لازم است که از متد ()finalize استفاده کنیم. یعنی به عنوان مثال فایلی که شی، آن را باز کرده است در متد ()finalize آزاد کند و بعد از حافظه پاک شود. نکته‌ای که باید به آن توجه کنید این است که در کل پیاده سازی متد ()finalize کار رایجی نیست. یعنی اینطور نیست که در هر کلاسی که تعریف کنیم، این متد را هم پیاده سازی کنیم. متد ()finalize را فقط برای همین موارد یا برای لاگ زدن پیاده‌سازی می‌کنند و در کل کاربرد کمی دارد.

دو نوع از کاربرد‌های کلیدواژه‌ی this

به کُد زیر توجه کنید:

package ir.zoomit;public class Test { private int age; public Test(int a) { age = a; }}

یک تکه کد بسیار ساده‌ای است که Constructor مقدار ویژگی age را تنظیم می‌کند. برنامه به این صورت است که وقتی کلاس Test را new می‌کنیم، یک عدد صحیح به عنوان پارامتر به سازنده پاس می‌دهیم و سازنده مقدار پارامتر خودش را برابر با متغیر age قرار می‌دهد. با این کار فیلد age مقداردهی شده است (وظیفه‌ی سازنده مقداردهی اولیه است). اما برای راحتی کار بهتر است که نام پارامتر سازنده را نیز همان age در نظر بگیریم. این کار باعث می‌شود که هنگام خواندن کُد، راحت‌تر کُد را بخوانیم و متوجه شویم. به کُد زیر توجه کنید:

package ir.zoomit;public class Test { private int age; public Test(int age) { age = age; }}

همانطور که مشاهده می‌کنید، کُد قبلی را تغییر دادیم و نام پارامتر سازنده را همان نام فیلد کلاس در نظر گرفتیم. اما به بدنه‌ی سازنده دقت کنید. ما می‌خواهیم مقدار age‌ که پارامتر سازنده است را به age‌ نسبت دهیم که به عنوان فیلد کلاس در نظر گرفته شده است. در بدنه‌ی سازنده‌ی فوق، مقدار age پارامتر را به خودش (همان age سازنده) نسبت می‌دهد، در صورتی که ما می‌خواهیم به فیلد کلاس نسبت دهیم. در اینجا مجبور می‌شویم که از کلیدواژه‌ی this استفاده کنیم. به کُد زیر توجه کنید:

package ir.zoomit;public class Test { private int age; public Test(int age) { this.age = age; }}

همانطور که در بدنه‌ی سازنده مشاهده می‌کنید، با استفاده از کلیدواژه‌ی this، مشخص کرده‌ایم که منظور ما از age، ویژگی کلاس است، نه پارامتر سازنده. بنابراین با این کار، کامپایلر جاوا مقدار age‌ پارامتر سازنده را به age‌ که ویژگی کلاس است نسبت می‌دهد.

کاربرد دوم کلیدواژه‌ی this، فراخوانی یک سازنده، از داخل یک سازنده‌ی دیگر است. همانطور که گفته شد، ما می‌توانیم سازنده‌ها را Overload کنیم. گاهی می‌خواهیم در یک زمان دو سازنده را فراخوانی کنیم. یعنی زمانی که یک سازنده را فراخوانی می‌کنیم، در همان لحظه سازنده‌ی دیگر نیز فراخوانی شود یا به عبارت دیگر به عنوان مثال با فراخوانی سازنده‌ی دوم، عملیاتی که در سازنده‌ی اول پیاده سازی شده است، اجرا شود. به کُد زیر توجه کنید:

package ir.zoomit;public class Test { private int age; private String name; public Test(int age) { this.age = age; } public Test(int age, String name) { this.name = name; }}

در کلاس بالا دو ویژگی تعریف شده است. یکی ویژگی age و یکی ویژگی name. فیلد age در Constructor اول مقداردهی شده است و در سازنده‌ی دوم، ویژگی name. نکته‌ی دیگر اینکه ما همزمان نمی‌توانیم دو Constructor را فراخوانی کنیم. (یعنی هنگام new کردن کلاس، فقط از یک سازنده می‌توانیم استفاده کنیم، نه بیشتر). حالا فرض کنید ما هنگام new کردن کلاس Test، می‌خواهیم از سازنده‌ای استفاده کنیم که دو پارامتر دارد (سازنده‌ی دوم) و همزمان هم می‌خواهیم هم فیلد age مقداردهی شود و هم فیلد name. در سازنده‌ی دوم فیلد age مقداردهی نشده است. بنابراین ما دو راه داریم. یکی اینکه همان پیاده سازی‌ که برای سازنده‌ی اول نوشته‌ایم را برای سازنده‌ی دوم هم بنویسیم. کُد زیر:

package ir.zoomit;public class Test { private int age; private String name; public Test(int age) { this.age = age; } public Test(int age, String name) { this.age = age; this.name = name; }}

همانطور که مشاهده می‌کنید ما پیاده سازی سازنده‌ی اول را، در سازنده‌ی دوم کپی کرده‌ایم. خط زیر را کپی کردیم:

this.age = age;

کُد بالا در سازنده‌ی اول وجود دارد و ما این کُد را عینا در سازنده‌ی دوم هم کپی کردیم. در مهندسی نرم افزار کپی کردن کاری بسیار نادرست است و ما باید از Code Reuse (استفاده مجدد از کد) استفاده کنیم. بنابراین بجای اینکه کد را کپی کنیم، از روش‌هایی استفاده کنیم که باعث استفاده‌ی مجدد از کد می‌شود. بهتر است کُد بالا را به صورت زیر تغییر دهیم:

package ir.zoomit;public class Test { private int age; private String name; public Test(int age) { this.age = age; } public Test(int age, String name) { this(age); this.name = name; }}

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

نکته‌ی دیگری که باید به آن توجه کنید این است که هنگامی که می‌خواهیم در داخل یک سازنده با استفاده از کلیدواژه‌ی this یک سازنده‌ی دیگر را فراخوانی کنیم، this را باید در اولین خط سازنده بنویسیم، در غیر این صورت با خطای کامپایل مواجه می‌شویم.

گروه لینکدین

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

تبلیغات
داغ‌ترین مطالب روز

نظرات

تبلیغات