نكتهاي كه وجود دارد اين است كه بسياري از برنامه نويسان جاوا معتقدند كه جاوا Call By Value است و عدهاي ديگر نيز معتقدند كه جاوا Call By Reference است. اينكه جاوا را چه مينامند اصلا اهميتي ندارد، بلكه مهم اين است كه شما رفتار جاوا را بدانيد كه در هر وضعيت چگونه عمل ميكند.
نگاهي كوتاه به مفاهيم گذشته
قبل از اينكه وارد بحث اصلي شويم، بهتر است نگاهي كوتاه به مفاهيم گذشته كنيم. اطلاع و تسلط داشتن كامل به مفاهيمي كه در گذشته آموزش داده شده است، بسيار مهم و لازم براي فهميدن و درك مطالب اين جلسه است. پس با دقت مطالعه كنيد.
همانطور كه ميدانيد در جاوا دو نوع داده وجود دارد كه عبارت هستند از دادههاي اوليه (Primitive Data Type) ودادههاي ارجاعي (Reference Data Type). دادههاي اوليه (Primitive Data Type) هشت نوع بودند: byte,short, int, long, char, float, double, boolea. اين هشت نوع داده به صورت پيشفرض در جاوا تعريف شدهاند و استفاده از آنها در يك مثال ساده به صورت زير است:
package ir.zoomit;
public class MainApp {
public static void main(String[] args) {
int integerNumber = 10;
char character = 'A';
boolean b = true;
}
}
با استفاده از Primitive Data Typeها ميتوانيم دادههاي پيچيدهتري را بسازيم. به عنوان مثال كلاس String كه براي كار با رشتهها استفاده ميشود، در دل خودش از دادهي كاركتر (char) استفاده كرده است. String يك كلاس است، بنابراين جزء دادههاي ارجاعي (Reference Data Type) به حساب ميآيد. به عبارت ديگر تمام كلاسها كه از روي آنها اشيائي ايجاد ميشود، جزء دادههاي ارجاعي هستند و فقط و فقط آن هشت دادهاي كه در بالا گفته شد جزء دادههاي پايه به حساب ميآيند.
چند نكته در مورد دادههاي پايه و ارجاعي
دقيقا منظور از ارجاع يا Reference چيست؟ به طور كلي براي استفاده از دادههاي ارجاعي، حتما بايد ابتدا از روي آنها يك آبجكت ساخت. به صورت زير:
package ir.zoomit;
public class MainApp {
public static void main(String[] args) {
Person p = new Person(); // Object Creation OR Instantiation
}
}
class Person {
}
در كد بالا دو كلاس وجود دارد. يكي كلاس اصلي (MainApp) كه در آن متد معروف main پيادهسازي شده است و ديگري كلاس Person كه هيچ پياده سازي ندارد. همانطور كه مشاهده ميكنيد در داخل متد main يك آبجكت از روي كلاس Person ساختهايم.
نكته: اصطلاحا به ساخت آبجكت از روي يك كلاس Object Creation يا Instantiation گفته ميشود. حتما اصطلاحات انگليسي را با تلفظ درست ياد بگيريد و به كار ببريد.
در بسياري از كتابها و منابع آموزشي به اشتباه گفته ميشود كه p در كد بالا شي يا آبجكت است. در صورتي كه كاملا اشتباه است. ما در آموزشهاي قبلي هم در مورد اين موضوع صحبت كرديم، اما بهتر است دوباره نگاهي سطح پايينتر به اين موضوع داشته باشيم.
همانطور كه ميدانيد براي ساختن يك شي از روي يك كلاس، بايد آن كلاس را new كنيم. هنگامي كه با استفاده از عملگرnew اقدام به ساخت يك شي از روي يك كلاس ميكنيم، عملگر new در واقع دو كار را انجام ميدهد. ابتدا يك شي جديد در حافظهي Heap ايجاد ميكند و سپس يك Reference يا ارجاعي از آن شي ساخته شده را برميگرداند. يعني ما با استفاده از آن ارجاع، ميتوانيم به شي ساخته شده در حافظه دسترسي داشته باشيم. براي اينكه بتوانيم با استفاده از ارجاع به شيئي در حافظه دسترسي داشته باشيم، بايد ارجاع را در يك متغيري ذخيره كنيم. اين متغير در كد بالا، p است. بنابراين p شي نيست، بلكه يك Reference يا ارجاعي به شي در حافظه است.
نكتهي ديگري كه بايد از آن اطلاع داشته باشيد و قبلا هم به آن اشاره شده است، اين است كه در تكه كد زير:
Person p = new Person();
متغير p در حافظهي Stack (استك) ايجاد شده است و آبجكت يا شي در حافظهي Heap «هيپ».
براي درك بهتر مسئلهي فوق، به تصوير زير توجه كنيد:
تصوير فوق به صورت دقيق اين مسئله را روشن ميكند، بنابراين با دقت توجه كنيد. در اين عكس در يك متُدي با نامMethod1 دو داده از نوع دادههاي اوليه يا Primitive Data Type تعريف كرده است و بعد هم از روي يك كلاسي با نام Class1 آبجكتي ايجاد كرده است. به Line1 (گوشهي بالا سمت چپ تصوير) توجه كنيد. وقتي كه در برنامه يك داده از نوع عدد صحيح تعريف شده است، در حافظهي Stack اين متغير ايجاد و مقداردهي شده است. توجه كنيد كه Primitive Data Typeها همانطور كه از نامشان پيداست، دادههاي اوليه هستند و نميتوانيم آنها را new كنيم. newكردن فقط مختص كلاسها است كه از روي آنها اشيائي ايجاد ميشود. بنابراين وقتي در برنامه دادهاي از نوع دادههاي اوليه تعريف ميكنيم، آن داده در حافظهي Stack ذخيره و مقداردهي ميشود. اگر با درس Data structure يا ساختمان دادهها آشنايي داشته باشيد، مبحثي است با نام Stack يا پُشته كه در آن اصطلاح LIFO را براي Stack در نظر گرفتهاند كه مخفف: Last In First Out است. يعني اينكه آخرين دادهاي كه وارد Stack ميشود، اولين دادهاي است كه از آن خارج ميشود. اگر به تصوير فوق نيز نگاه كنيد، استك را همانند يك ليوان كشيده است كه يك سَر آن بسته و يك سَر ديگر باز است. دادهها وقتي وارد استك ميشوند، روي يكديگر قرار ميگيرند. اگر به خط بعدي برنامه نگاه كنيد، دوباره دادهاي از نوع دادههاي اوليه تعريف و مقداردهي شده است. حالا به Line2 توجه كنيد. همانطور كه مشاهده ميكنيد، در استك متغير y روي متغير i قرار گرفته است. پس تا اينجاي كار، متغير y آخرين متغير يا دادهاي است كه وارد Stack شده است، پس اولين متغير يا دادهاي است كه از استك خارج ميشود.
نكته: پس تا اينجا متوجه شديم كه دادههاي پايه در حافظهي Stack ذخيره و مقداردهي ميشوند.
حالا به سراغ ادامهي كد ميرويم. در ادامه برنامه ميخواهد از روي يك كلاس، آبجكتي در حافظه ايجاد كند و ارجاع يا Reference آن آبجكت را در متغيري با نام cls1 ذخيره كند. حالا به Line3 توجه كنيد. متغير cls1 در حافظهي Stack ايجاد شده است و همانطور كه در تصوير نيز مشاهده ميكنيد، در مقابل آن و در داخل پرانتز عبارت (ref) را نوشته است كه منظور همان Reference يا ارجاع است. باز هم به تصوير دقت كنيد. در Line3 حافظهي Heap را هم كشيده است و متغير cls1 در حال اشاره كردن به آبجكتي است كه در Heap ايجاد شده است. پس با توضيحات فوق بايد مفهومReference يا ارجاع را كاملا درك كرده باشيد.
توضيحات در مورد مفهوم ارجاع يا Reference به طور كلي گفته شد. اما در اينجا قصد داريم به يك نكته اشاره كنيم تا يك سوءتفاهم را برطرف كنيم.
همانطور كه قبلا هم در آموزشها اشاره شده است، در جاوا موجودي با نام زباله روب يا Garbage Collector وجود دارد. وظيفهي GC پاكسازي حافظهي Heap است. به عبارت ديگر Garbage Collector هر از چندگاهي به حافظهي Heap سر ميزند و اشياء به اصطلاح مُرده را پاك و حافظه را آزاد ميكند. نكتهي بسيار مهم دقيقا همين جا است كه Garbage Collector فقط و فقط حافظهي Heap را پاكسازي ميكند. اگر به تصوير فوق نگاه كنيد، در بخش exiting method، بعد از پايان برنامه حافظهي Stack خالي شده است، اما در حافظهي Heap آبجكت ساخته شده همچنان وجود دارد. نكته اينجا است كه در تمام زبانهاي برنامه نويسي (چه زباني مثل جاوا كه Garbage Collector دارد و به صورت خودكار حافظهي Heap را پاكسازي ميكند، و چه زبانهايي مثل ++C كه پاكسازي اشياء نيز بر عهدهي برنامه نويس است)، حافظهي Stack به صورت خودكار پاك ميشود و پاكسازي حافظهي Stack اصلا ربطي به وجود Garbage Collector ندارد و در تمام زبانها اين كار به صورت خودكار انجام ميشود. به اين دليل به نكتهي بالا پرداخته شد كه در بعضي از كتابها و منابع آموزشي، براي توضيح نحوهي كار Garbage Collector، مثالي همانند مثال فوق ميآورند و ميگويند مثلا y، i يا cls1 توسط Garbage Collector پاك ميشوند كه كاملا غلط و اشتباه است.
ارسال پارامتر به متُد
متُدها نشان دهندهي رفتار يك برنامه هستند. وقتي يك متُد را تعريف ميكنيم، براي آن با توجه به كاري كه قرار است در برنامه انجام دهيم، پياده سازيهاي مختلفي در نظر ميگيريم. به كد زير توجه كنيد:
package ir.zoomit;
public class MainApp {
public static void main(String[] args) {
Person p = new Person(); // Object Creation OR Instantiation
p.show(); // Method Invocation
}
}
class Person {
public void show() {
System.out.println("Method Invocation");
}
}
در بالا يك برنامهي بسيار ساده نوشتهايم. ابتدا يك متُد با نام ()show در كلاس Person نوشتهايم كه اين متُد پياده سازي بسيار سادهاي دارد (در حد چاپ كردن يك رشته در خروجي استاندارد). در كلاس اصلي در داخل متد main، ابتدا آبجكتي از روي كلاس Person ايجاد كردهايم و سپس توسط آن آبجكت، متُد موجود در كلاس Person را فراخواني كردهايم.
نكته: اصطلاحا به فراخواني متُد، Method Invocation ميگويند.
متُدي كه در بالا تعريف كردهايم، يك متُد بدون پارامتر است. حالا اگر بخواهيم يك متُد با پارامتر تعريف كنيم، بايد متغيريهايي را در داخل پرانتزهاي باز و بستهي جلوي نام متد، تعريف كنيم. يك متد ميتواند صفر يا بيش از صفر پارامتر داشته باشد. به كد زير توجه كنيد:
package ir.zoomit;
public class MainApp {
public static void main(String[] args) {
Person p = new Person(); // Object Creation OR Instantiation
p.sum(5, 10); // Method Invocation
}
}
class Person {
public void sum(int a, int b) {
int sum = 0;
sum = a + b;
System.out.println(sum);
}
}
در برنامهي فوق، ابتدا در داخل كلاس Person متُدي با نام sum تعريف كردهايم كه ميخواهيم اين متُد دو عدد را دريافت كند و سپس آن دو عدد را جمع كند و در خروجي استاندارد چاپ كند. اگر دقت كنيد متُد ()sum در داخل پرانتزهاي باز و بستهي جلويش، دو عدد صحيح تعريف شده است. اين دو عدد پارامترهاي متُد ()sum هستند. چند نكته در مورد پارامترهاي متُد:
اگر قرار است متُد بيش از يك پارامتر داشته باشد (همانند متُد فوق)، بايد با استفاده از علامت «,» متغيرها را از يكديگر جدا كنيد. اما اگر يك متغير است، نيازي به اين كار نيست.
نكتهي دوم اين است كه اگر ميخواهيم متُدي را فراخواني كنيم كه داراي پارامتر است، در هنگام فراخواني متُد، حتما بايد براي پارامترهاي متُد، دادههايي را از همان نوعي كه در متُد تعريف شدهاند در نظر بگيريم. در غير اين صورت با خطاي كامپايل مواجه ميشويم. اگر به كد بالا دقت كنيد، در متُد main هنگام فراخواني متُد ()sum، دو عدد ۵ و ۱۰ را براي پارامترهاي آن در نظر گرفتهايم. اگر آن دو عدد را در نظر نميگرفتيم، با خطاي كامپايل مواجه ميشديم. پس در برنامهي بالا ما يك متُد تعريف كردهايم كه داراي دو پارامتر است و هنگام فراخواني آن متُد (Method Invocation)، دو داده براي پارامترهاي آن در نظر گرفتهايم.
Parameter Passing: A Deeper Look
حالا ميخواهيم نگاهي دقيقتر به ارسال پارامتر به متُدها داشته باشيم و وارد موضوعي شويم كه تمام مطالبي كه تا قبل از اين گفته شد، براي فهميدن اين موضوع لازم است.
در حالت كلي ارسال پارامتر به يك متُد، در سه صورت انجام ميشود كه عبارت هستند از:
- Call By Value
- Call By Pointer
- Call By Reference
در زبان ++C مفهومي است با نام اشارهگر يا Pointer كه در زبان جاوا وجود ندارد. البته طراحان زبان جاوا ارجاعها يا Referenceها را نوع خاص و محدود شدهي اشارهگر در جاوا ميدانند. نكتهي مهمي كه بايد به آن توجه كنيد اين است كه با توجه به اين موضوع كه در زبان جاوا اشارهگر وجود ندارد، درست نيست كه براي جاوا از مفهوم Call By Pointer استفاده كنيم. اما چرا ما در اينجا براي آموزش جاوا اين سه اصطلاح را نوشتيم؟ اين سه مُدل يك مفهوم كلي است و شايد بهتر باشد با زبانهاي ديگري مثل ++C و #C نيز كار كنيد تا درك درستي از هركدام از اين مفاهيم پيدا كنيد، زيرا عنوان اين سه مُدل شفاف و گويا نيست و فهمدين و درك آنها نيازمند تجربه است. به عنوان مثال مُدلي كه در ++C به آن Call By Reference ميگويند، اصلا در جاوا وجود ندارد و در مقابل مُدلي كه در جاوا معمولا به آن Call By Reference ميگويند، در ++C با عنوان Call By Pointer شناخته ميشود. بنابراين اگر كسي از شما بپرسد كه Call By Reference چيست، بايد سوالش را دقيقتر و كاملتر كند كه منظورش در كدام زبان برنامه نويسي است.
نكتهي بسيار مهم: در جاوا اگر متغيرهايي از نوع دادههاي اوليه (Primitive Data Type) را به متُد پاس بدهيم يا ارسال كنيم، رفتار جاوا Call By Value است و اگر دادههاي ارجاعي (Reference Data Type) مثل كلاس String را ارسال كنيم، رفتار جاوا Call By Reference، چيزي شبيه Call By Pointer در ++C است.
حالا اجازه دهيد با مثال كار خود را پيش ببريم. ابتدا توضيح Call By Value. به كد زير توجه كنيد:
package ir.zoomit;
public class MainApp {
public static void main(String[] args) {
int a = 5;
int b = 10;
System.out.println("Before: " + "a=" + a + ", " + "b=" + b);
badSwap(a, b);
System.out.println("After: " + "a=" + a + ", " + "b=" + b);
}
private static void badSwap(int a, int b) {
int temp = 0;
temp = a;
a = b;
b = temp;
System.out.println("In badSwap() Method: " + "a=" + a + ", " + "b=" + b);
}
}
در برنامهي بالا يك كلاس بيشتر تعريف نكردهايم كه همان كلاس اصلي است. اما در داخل اين كلاس دو متُد وجود دارد. يكي متُد main كه نقطهي شروع هر برنامهي جاوا است و ديگري متُد ()badSwap كه پياده سازي آن به اين شكل است كه دو پارامتر به عنوان ورودي دريافت ميكند و سپس مقادير آنها را با يكديگر عوض ميكند. يعني بعد از اجراي متُد ()badSwap، مقدار متغير a كه ۵ است بايد ۱۰ شود و مقدار b كه ۱۰ است، ۵. در پياده سازي متُد main قبل از اجرا و بعد از اجراي متُد ()badSwap مقادير متغيرهاي a و b را در خروجي استاندارد چاپ كردهايم تا متوجه تغييرات (جا به جايي مقادير متغيرها) شويم. همچنين در داخل متُد ()badSwap نيز مقادير a و b را چاپ كردهايم.
حالا قبل از اجراي برنامه، سعي كنيد فقط با نگاه كردن به كدها، برنامه را روي يك تكه كاغذ يا ذهن خود Trace و اجرا كرده و خروجي برنامه را پيدا كنيد.
نكته: Trace كردن به اين معني است كه اگر برنامه با خطايي مواجه شود، برنامهنويس خط به خط برنامه را دقيقا ميخواند و تمام رويدادهايي كه در برنامه رُخ ميدهد را ثبت ميكند تا متوجه شود كه دقيقا چه اتفاقي ميافتد. البته در برنامههاي بزرگ اصلا به اين شيوه عمل نميكنند و از مبحث Logging استفاده ميكنند. استفاده از Trace كردن، جدا از پيدا كردن خطا در برنامه، ميتوان از آن براي بهتر فهميدن برنامه نيز كمك گرفت.
تمرين بيشتر: در يوتيوب ويدئوهايي براي آموزش Trace كردن وجود دارد. Hand Tracing را جستجو كنيد و ويدئوهايي كه در اين زمينه توليد شده است را تماشا كنيد. درضمن اصلا نيازي نيست كه آموزشهايي كه در يوتيوب وجود دارد حتما براي زبان جاوا باشد، بلكه فقط كافي است نحوهي Trace كردن را فرا بگيريد.
بعد از اينكه خودتان برنامه را به صورت دستي Trace كرديد، برنامه را اجرا كنيد. بعد از اجراي برنامهي فوق، با خروجي زير مواجه ميشويد:
Before: a=5, b=10
In badSwap() method: a=10, b=5
After: a=5, b=10
همانطور كه در خروجي فوق مشاهده ميكنيد، مقادير a و b قبل و بعد از اجراي متُد هيچ تغييري نكرده است و فقط تغييرات در داخل متُد ()badSwap است. يعني جاوا در اين قسمت رفتار Call By Value از خودش نشان داده است.اما علت تغيير نكردن مقادير چيست؟ بسيار ساده است. هنگامي كه در داخل متُد main، متُد ()badSwap را فراخواني و مقادير a و b را به آن ارسال كردهايم، جاوا در حقيقت خود اصل مقادير را به متُد ارسال نكرده است، بلكه كُپي از دادهها را به متُد ارسال كرده است. بنابراين در داخل متُد ()badSwap هر اتفاقي كه بيفتد، روي كُپي دادهها تغييرات ايجاد ميشود و اصل دادهها هيچ تغييري نميكنند. به هم دليل است كه وقتي مقادير a و b را در داخل متُد ()badSwap در كنسول چاپ ميكنيم، مقاديرشان تغيير كرده است، اما بعد از آن خير.
نكته: پس هميشه اين نكته را به خاطر داشته باشيد كه وقتي دادههايي به متُد ارسال ميكنيد كه از نوع دادههاي اوليه (Primitive Data Type) هستند، جاوا رفتار Call By Value از خودش نشان ميدهد.
حالا اجازه دهيد در مورد Call By Reference كه شبيه Call By Pointer در ++C است صحبت كنيم.
به كد زير توجه كنيد:
package ir.zoomit;
public class MainApp {
public static void main(String[] args) {
}
}
class Person {
private String str1;
private String str2;
public String getStr1() {
return str1;
}
public void setStr1(String str1) {
this.str1 = str1;
}
public String getStr2() {
return str2;
}
public void setStr2(String str2) {
this.str2 = str2;
}
}
كد فوق بسيار ساده است. دو كلاس داريم، يكي كلاس اصلي (MainApp) و ديگري كلاس Person. كلاس Person داراي دو ويژگي يا Property يا فيلد است. طبق آموزشهاي قبل فيلدهاي كلاس Person متغيرهاي str1 و str2 كه از جنس كلاس String هستند. در اين كلاس چهار متُد تعريف شده است. به دليل اينكه Encapsulation رُخ داده است، متُدهاي getter و setter براي دسترسي به فيلدهاي كلاس تعريف شده است.
حالا در ادامه ميخواهيم در كلاس اصلي و در متُد main از روي كلاس Person يك آبجكت ايجاد كنيم و با استفاده از شيئي كه در دست داريم، فيلدهاي كلاس Person كه str1 و str2 هستند را مقداردهي كنيم. پس بايد كُدمان را به صورت زير كامل كنيم:
package ir.zoomit;
public class MainApp {
public static void main(String[] args) {
// Object Creation OR Instantiation
Person a = new Person();
Person b = new Person();
a.setStr1("Java");
b.setStr2("C++");
System.out.println(
"Before: " + "str1=" + a.getStr1() + ", " + "str2=" + b.getStr2());
swapNames(a, b);
System.out.println(
"After: " + "str1=" + a.getStr1() + ", " + "str2=" + b.getStr2());
}
static void swapNames(Person a, Person b) {
String tmp = a.getStr1();
a.setStr1(b.getStr2());
b.setStr2(tmp);
}
}
class Person {
private String str1;
private String str2;
public String getStr1() {
return str1;
}
public void setStr1(String str1) {
this.str1 = str1;
}
public String getStr2() {
return str2;
}
public void setStr2(String str2) {
this.str2 = str2;
}
}
برنامه بسيار ساده است. دقيقا همان جا به جايي است كه در بخش قبل براي اعداد انجام داديم، اينجا براي رشتهها انجام دادهايم و ميخواهيم امتحان كنيم كه آيا مقادير دو رشتهي str1 و str2 با يكديگر عوض ميشوند يا خير؟ باز هم پيشنهاد ميكنيم برنامه را قبل از اجرا در ذهن خود Trace كنيد و به خروجي آن برسيد.
حالا اگر برنامه را اجرا كنيم، با خروجي زير مواجه ميشويم:
Before: str1=Java, str2=C++
After: str1=C++, str2=Java
همانطور كه مشاهده ميكنيد اين بار مقادير متغيرها با يكديگر عوض شد. علت چيست؟ همانطور كه گفته شد، دادههاي ارجاعي (Reference Data Type)، به طور مستقيم خودِ داده را نگه داري نميكنند، بلكه ارجاعي به آن داده در حافظه هستند. در كد بالا متغيرهاي a و b در متُد ()swapNames به همان جايي در حافظه ارجاع ميدهند (اشاره ميكنند) كه متغيرهاي a و b در متُد main به آن اشاره ميكنند. بنابراين وقتي با كمك متغيرهاي a و b در متُد ()swapNames محتواي (State) يك آبجكت را تغيير دهيم، محتواي a و b در متُد main نيز تغيير ميكند. بنابراين ما در جاوا به كمك ارجاعهايي كه به به صورت پارامتر به متُدها پاس ميشوند، ميتوانيم محتوا يا وضعيت يا State اشياء را تغيير دهيم. نكتهاي كه وجود دارد اين است كه هويت يا Identity اشياء را نميتوانيم تغيير بدهيم. به عبارت ديگر پارامترهايي كه به متُد پاس ميشوند، در انتهاي فراخواني متُد، هويتشان حفظ ميشود. به عنوان مثال متغيرهاي a و b در متُد main، پس از پايان متُد، هويتشان تغيير نميكند و همچنان به همان جايي در حافظه اشاره ميكنند كه قبلا اشاره ميكردند. اما بعد از اجراي متُد، وضعيتشان، حالتشان يا State آنها تغيير كرده است.
نكتهي پاياني: به عنوان نكتهي آخر، در مورد كلاسهاي تغيير ناپذير يا Immutable Classها تحقيق كنيد. به عنوان مثال String يك كلاس Immutable است. ابتدا اين مبحث را مطالعه كنيد و سپس دادههايي از جنس كلاسهاي Immutable را به متُدها ارسال كنيد و سعي كنيد محتواي آنها را تغيير دهيد.
در آخر از كساني كه با زبان جاوا آشنا هستند درخواست داريم تا آموزشها را با دقت مطالعه كنند تا اگر ايرادي در آموزشها بود مطرح كنند تا آنها را برطرف كنيم.