نکته ای در مورد پاس دادن آرایه به متدها در جاوا

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

در زبان جاوا داده ها به صورت کلی دو نوع هستند انواع اوّلیه (primitive datatypes) و انواع ارجاعی (reference datatypes)

انواع داده ی اوّلیه موارد زیر هستند و هر نوع دیگری در گروه انواع ارجاعی قرار می‌گیرد.

  • byte
  • short
  • int
  • long
  • char
  • boolean
  • float
  • double

وقتی که یک متغییر اعلان می کنیم در واقع داریم برای قسمتی از حافظه اسم انتخاب می کنیم که از این به بعد می توانیم به آن قسمت با استفاده از آن اسم دسترسی داشته باشیم. تفاوت انواع اوّلیه و ارجاعی در این زمینه این است که برای انواع اوّلیه مقدار خود داده در آن قسمت از حافظه ذخیره می شود در حالی که برای انواع ارجاعی اینگونه نیست. اشیائی که از جنس انواع ارجاعی در قسمت دیگری از حافظه ذخیره می شوند و در متغییر مربوط به آن ها ارجاعی به آن اشیاء قرار داده می شود.

این ها را که خودم می دانستم !

حالا به نکته ای که قرار بود در موردش صحبت کنیم می رسیم ! سه نوع فرستادن متغییر به توابع در زبان های مختلف دیده می شود:

  • ارسال با مقدار (pass by value)
  • ارسال با نشانگر (pass by pointer)
  • ارسال با ارجاع (pass by reference)

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

فرستادن مقادیر از انواع اوّلیه به متدها سرراست است و نکته ی خاصی ندارد، مقدار برای متد فرستاده می‌شود و هر تغییری روی آن ایجاد شود تاثیری روی متغییری که به متد پاس داده شده است ندارد ولی وقتی با انواع ارجاعی سر کار داریم شرایط کمی متفاوت است، به کد زیر توجّه کنید:

public class MethodExample {
	public static void main(String[] args) {
		int[] arr1 = {1,4,2,5,3};
		printArr(arr1);
		
		int[] arr2 = sort(arr1);
		printArr(arr1);
		printArr(arr2);
	}
	
	public static int[] sort(int[] arr) {
		int n = arr.length;
		for (int i=0; i<n-1; i++)
			for (int j=i+1; j<n; j++)
				if (arr[i] > arr[j]) {
					int temp = arr[j];
					arr[j] = arr[i];
					arr[i] = temp;
				}
		return arr;
	}
	
	public static void printArr(int[] arr) {
		for (int i: arr)
			System.out.printf(&quot%d\t&quot,i);
		System.out.print(&quot\n&quot);
	}
}

متد sort که در خط 11 تعریف شده است کارش این است که یک آرایه از اعداد بگیرد و آن ها را به صورت صعودی مرتب کند و برگرداند.
متد printArr که در خط 23 تعریف شده است کارش این است که یک آرایه از اعداد بگیرد و آن ها را در خروجی نمایش دهد.
در متد main که entry point برنامه ی ما است ابتدا یک آرایه از اعداد صحیح ایجاد کرده و با استفاده از متد printArr در خروجی نمایش داده ایم، سپس این آرایه را به متد sort پاس داده ایم و مقداری که متد sort بر می گرداند را در متغییری به نام arr2 قرار داده ایم و سپس arr2 و arr1 را به ترتیب با استفاده از متد printArr خروجی داده ایم. با توجّه به این که جاوا از روش pass by value برای ارسال مقادیر به متدها استفاده می‌کند انتظار داریم نتیجه چنین چیزی باشد:

result:
1	4	2	5	3	
1	4	2	5	3	
1	2	3	4	5

ولی در واقع با نتیجه ی زیر روبرو می شویم:

result:
1	4	2	5	3	
1	2	3	4	5	
1	2	3	4	5

که این به این معنی است که آرایه ی اوّلیه ی ما دچار تغییر شده و این به نظر برخلاف چیزی است که بالاتر گفته بودیم ولی اینطور نیست !

... تفاوت انواع اوّلیه و ارجاعی در این زمینه این است که برای انواع اوّلیه مقدار خود داده در آن قسمت از حافظه ذخیره می شود در حالی که برای انواع ارجاعی اینگونه نیست. اشیائی که از جنس انواع ارجاعی در قسمت دیگری از حافظه ذخیره می شوند و در متغییر مربوط به آن ها ارجاعی به آن اشیاء قرار داده می شود.
روشی که زبان جاوا برای این کار استفاده می‌کند روش ارسال با مقدار است، یعنی وقتی ما اسم متغییری را به عنوان آرگومان به پارامتر متدی می‌دهیم مقدار آن متغییر کپی می‌شود و در اختیار متغییری که در قسمت پارامترهای آن متد تعریف شده قرار می گیرد.

با دقت کردن به این دو نکته میتوان فهمید که قضیه از چه قرار است ! آرایه یک نوع ارجاعی است و در متغییری که اعلان کرده بودیم مقداری که قرار گرفته بود در واقع یک ارجاع به آن آرایه بود و نه خود آرایه و وقتی که این مقدار به متد داده شد در واقع ارجاع به آرایه در اختیار آن قرار گرفت و این یعنی متد sort هم به همان شئ دسترسی دارد که متد main ما دسترسی داشت و با تغییر دادن این آرایه در واقع روی آرایه ی اصلی هم تاثیر می گذارد.

حالا که فهمیدیم چه اتفاقی می‌افتد چکار کنیم ؟

حل کردن این مسئله کار سختی نیست، شما می توانید یک متد بنویسید که یک آرایه جدید درست کند و اعضای این آرایه را در آرایه ی جدید و در خانه های متناظر آن قرار دهد و آرایه ی جدید را برگرداند. البته راه دیگر و ساده تری هم وجود دارد ! استفاده کردن از متد clone روی آرایه:

arr.clone()

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

public static void main(String[] args) {
		int[] arr1 = {1,4,2,5,3};
		printArr(arr1);
		
		int[] arr2 = sort(arr1);
		printArr(arr1);
		printArr(arr2);
	}

با نتیجه ی زیر روبرو می شویم:

result:
1	4	2	5	3	
1	4	2	5	3	
1	2	3	4	5

که نتیجه ی مورد انتظار و دلخواه ماست.

ولی هنوز داستان تمام نشده است !

حالا که نکته ی بالا را می دانیم ممکن است فکر کنیم دیگر می‌توانیم با خیال راحت به کد زدن بپردازیم ولی همیشه اینطور نیست ! به مثال زیر توجّه کنید:

public class TwoDArr {
	public static void main(String[] args) {
		int[][] arr1 = {{1,2,3},{4,5,6},{7,8,9}};
		
		System.out.println(&quotarr1 before plusTwo method call:&quot);
		print2DArr(arr1);

		int[][] arr2 = plusTwo(arr1.clone());
		System.out.println(&quotarr2:&quot);
		print2DArr(arr1);

		System.out.println(&quotarr1 after plusTwo method call:&quot);
		print2DArr(arr1);
	}
	
	public static void print2DArr(int[][] arr) {
		for (int[] row: arr) {
			for (int item: row)
				System.out.printf(&quot%d\t&quot, item);
			System.out.print(&quot\n&quot);
		}
	}
	
	public static int[][] plusTwo(int[][] arr){
		for (int i=0; i < arr.length; i++)
			for (int j=0; j< arr[i].length; j++)
				arr[i][j] += 2;
		return arr;
	}
}

کار متد print2DArr این است که یک آرایه دو بعدی به عنوان پارامتر بگیرد و آن را خروجی دهد و کار متد plusTwo این است که تمام عناصر آرایه ی دو بعدی ای که می‌گیرد را به علاوه ی 2 کند و نتیجه را برگرداند. حالا که از متد clone استفاده کردیم انتظار داریم که arr1 ما در روند این برنامه تغییری نکند ولی با اجرا کردن این کد در کمال ناباوری با این نتیجه روبرو می‌شویم:

arr1 before plusTwo method call:
1	2	3	
4	5	6	
7	8	9	
arr2:
3	4	5	
6	7	8	
9	10	11	
arr1 after plusTwo method call:
3	4	5	
6	7	8	
9	10	11	

خدایا بسه دیگه خسته شدیم

لطفاً آرامش خود را حفظ کنید تا دلیل این اتفاق را توضیح بدهم و این مطلب را به پایان ببریم.
اتفاقی که افتاد به خاطر ماهیت آرایه های دو بعدی است ! یک آرایه ی دو بعدی در واقع آرایه ای از آرایه هاست. اگر بخواهیم این جمله را جور دیگری بگوییم که علت این اتفاق را برای ما مشخص کند می‌توانیم بگوییم یک آرایه ی دو بعدی آرایه ای است که هر خانه از آن ارجاع به آرایه ای دیگر است. با کمی بررسی و تفکر می توان فهمید که وقتی آرایه ی دو بعدی را ‌clone می کنیم. اتفاقی که می افتد این است که آرایه ی دو بعدی جدیدی ایجاد می شود و اعضای این آرایه (یعنی ارجاع های داخل خانه های این آرایه) در آرایه ی جدید کپی می شوند. در نتیجه هر چند که آرایه دو بعدی ما کلون شده است ولی سطرهای ما در واقع همان سطر های آرایه ی اصلی هستند و با تغییر دادن اعضای این آرایه ی دو بعدی آرایه ی اوّلیه هم تغییر می کند. این اتفاق برای آرایه های تک بعدی ای که انواع ارجاعی تغییر پذیر(mutable) دارند هم صادق است !

حالا این مسئله را چطور حل کنیم ؟

راه حل این مسئله این است که آرایه ی دو بعدی جدیدی بسازیم که تعداد سطرهایش با آرایه ی اوّلیه ی ما برابر است و بعد ارجاع به clone هر کدام از سطرهای آرایه ی اوّلیه را در خانه ی مرتبط با سطر مربوطه در آرایه ی جدید قرار دهیم. برای سایر انواع ارجاعی هم باید در روندی مشابه این روند هر کدام از اشیا را clone کنیم و ارجاع به clone آن ها را در آرایه ی جدید قرار بدهیم.



ممنون که این مطلب را مطالعه کردید و امیدوارم برایتان مفید بوده باشد، خوشحال می شوم نظر شما را در مورد این مطلب بشنوم.

جهت مطالعه ی مطالب بیشتر در مورد برنامه نویسی و موضوعات مرتبط و مشاهده ی محتواهای دیگری که در این موضوعات تولید می ‌کنم لطفا کانال تلگرام من را بررسی کنید:
t.me/amirMosadeghi