تنظيم البيانات المتشابهة في مجموعات يسهل التعامل معها
بعد إتقانك لمفاهيم التحكم في التدفق (الشروط والحلقات) وكيفية تنظيم الكود باستخدام الدوال، حان الوقت للانتقال إلى موضوع حيوي آخر في البرمجة: **هياكل البيانات (Data Structures)**. وتعتبر **المصفوفات (Arrays)** هي البوابة الأولى والأكثر شيوعًا لدخول هذا العالم.
المصفوفات تمكنك من التعامل مع مجموعات كبيرة من البيانات المتشابهة بكفاءة، مما يفتح آفاقًا جديدة لبناء برامج أكثر قوة وقدرة على معالجة المعلومات المنظمة. ستتعلم في هذه الوحدة كيفية تعريف المصفوفات، التعامل مع عناصرها، واستخدامها في برامجك.
بنهاية هذه الوحدة، ستكون قادرًا على:
عند بناء البرامج، نحتاج في كثير من الأحيان إلى التعامل مع **مجموعة من البيانات المتشابهة**. تخيل أن لديك قائمة بدرجات 10 طلاب، أو أسماء 50 موظفًا، أو بيانات لـ 1000 منتج. بدون مفهوم المصفوفات، كان سيتعين عليك تعريف متغير لكل قيمة على حدة، مما سيجعل الكود طويلاً جداً، صعب القراءة، وأكثر عرضة للأخطاء.
// تخزين 3 درجات بدون مصفوفات
int grade1 = 85;
int grade2 = 92;
int grade3 = 78;
// ماذا لو كان لدينا 100 درجة؟ سيكون الكود طويلاً جداً!
هنا جاء مفهوم **المصفوفة (Array)** ليحل هذه المشكلة. المصفوفة هي عبارة عن **حاوية** يمكنها الاحتفاظ بمجموعة من القيم **من نفس النوع**، وتكون هذه القيم مخزنة بشكل متجاور في الذاكرة. يمكننا الوصول إلى كل قيمة في المصفوفة بشكل فردي وسريع باستخدام رقم خاص يسمى **الفهرس (Index)**. يبدأ هذا الفهرس دائمًا من 0 ويستمر حتى 'طول المصفوفة ناقص واحد'.
تخيل المصفوفة كرف كتب طويل مقسم إلى خانات مرقمة، تبدأ من الخانة رقم 0. كل خانة مخصصة لنوع معين من الكتب (مثلاً، كتب الرياضيات فقط). لو أردت الوصول إلى الكتاب الخامس، ستذهب إلى الخانة رقم 4 (بسبب البدء من الصفر).
لنغوص أكثر في طبيعة المصفوفات في جافا:
int[] grades = {90, 85, 95};
// grades[0] هي 90
// grades[1] هي 85
// grades[2] هي 95
هذا لا يعني أن القيم المخزنة داخل المصفوفة لا يمكن تغييرها. يمكنك تغيير قيمة أي عنصر في أي وقت، لكن عدد العناصر (الحجم) يظل ثابتًا.
تقدم المصفوفات العديد من الفوائد التي تجعلها أداة لا غنى عنها في البرمجة:
على الرغم من مزاياها العديدة، إلا أن المصفوفات تعاني من قيد رئيسي وحيوي يجب الانتباه إليه:
هذا القيد يمكن أن يكون مشكلة في التطبيقات التي تحتاج إلى التعامل مع كميات غير معروفة أو متغيرة من البيانات.
لمعالجة هذه المشكلة وتوفير مرونة أكبر في إدارة مجموعات البيانات، وفرت لغة جافا مكتبة قوية تسمى **Collections Framework**. هذه المكتبة تحتوي على هياكل بيانات ديناميكية (مثل `ArrayList` و `LinkedList`) التي تسمح لك بإضافة أو إزالة العناصر بعد الإنشاء، وتعديل حجمها تلقائياً حسب الحاجة. سيتم دراسة هذه المكتبة لاحقاً في مقرر البرمجة المتقدمة.
قبل استخدام المصفوفة، يجب عليك تعريفها (Declaration)، وهو ما يخبر المترجم بنوع البيانات التي ستخزنها هذه المصفوفة. يوجد أكثر من طريقة لتعريف المصفوفة في جافا:
int[] numbers; // مصفوفة من الأعداد الصحيحة
String[] names; // مصفوفة من النصوص
double[] values; // مصفوفة من الأعداد العشرية
int [] numbers; // الأقواس بعد نوع البيانات مع مسافة
int numbers[]; // الأقواس بعد اسم المتغير (تأتي من لغة C/C++)
يمكنك أيضًا تعريف مصفوفات **متعددة الأبعاد**، وأكثرها شيوعًا هي **المصفوفات ثنائية البعد (2D Arrays)**، والتي تتطلب زوجين من الأقواس:
int[][] matrix; // مصفوفة ثنائية البعد (جدول من الأعداد الصحيحة)
عملية التعريف (Declaration) تخبر جافا بأنك ستستخدم مصفوفة، ولكنها **لا تُنشئ المصفوفة فعليًا في الذاكرة** ولا تُخصص لها مساحة بعد. لإنشاء المصفوفة، تحتاج إلى استخدام الكلمة المحجوزة `new`، وهو ما سنتناوله في القسم التالي.
بعد تعريف المصفوفة، الخطوة التالية هي **إنشاؤها (Initialization)**، أي تخصيص مساحة لها في الذاكرة وإعدادها للاستخدام. يتم ذلك باستخدام الكلمة المحجوزة `new`. يوجد طريقتان رئيسيتان لإنشاء المصفوفة:
في هذه الطريقة، تحدد حجم المصفوفة (عدد العناصر) عند إنشائها. جميع عناصر المصفوفة ستحصل على قيمها الافتراضية بناءً على نوع البيانات.
int[] numbers = new int[5];
// تُنشئ مصفوفة اسمها 'numbers' يمكنها تخزين 5 أعداد صحيحة.
// قيمها الابتدائية ستكون {0, 0, 0, 0, 0}
public class CreateArrayExample1 {
public static void main(String[] args) {
// تعريف وإنشاء مصفوفة من 3 عناصر لتخزين درجات الطلاب
String[] studentNames = new String[3];
// تعبئة القيم لاحقًا
studentNames[0] = "علي";
studentNames[1] = "فاطمة";
studentNames[2] = "خالد";
System.out.println("اسم الطالب الأول: " + studentNames[0]);
System.out.println("اسم الطالب الثاني: " + studentNames[1]);
System.out.println("اسم الطالب الثالث: " + studentNames[2]);
}
}
الناتج:
اسم الطالب الأول: علي
اسم الطالب الثاني: فاطمة
اسم الطالب الثالث: خالد
هنا، تقوم بتعريف المصفوفة وتعبئة قيمها الأولية مباشرةً في نفس السطر. في هذه الحالة، يقوم المترجم بتحديد حجم المصفوفة تلقائيًا بناءً على عدد القيم التي قدمتها.
int[] scores = {10, 20, 30, 40, 50};
// تُنشئ مصفوفة اسمها 'scores' بحجم 5 عناصر وتُعين لها القيم المحددة.
public class CreateArrayExample2 {
public static void main(String[] args) {
// تعريف وإنشاء مصفوفة تحتوي على درجات الطلاب مباشرةً
int[] studentGrades = {95, 88, 76, 91, 80};
System.out.println("درجة الطالب الأول: " + studentGrades[0]);
System.out.println("درجة الطالب الثالث: " + studentGrades[2]);
System.out.println("درجة الطالب الأخير: " + studentGrades[4]);
}
}
الناتج:
درجة الطالب الأول: 95
درجة الطالب الثالث: 76
درجة الطالب الأخير: 80
لفهم كيفية عمل المصفوفات، من المفيد أن نتخيل كيف يتم تخزينها في ذاكرة الحاسوب. عندما تُنشئ مصفوفة، تُخصص لها جافا مساحة **متجاورة** (Contiguous) في الذاكرة. كل "خلية" في هذه المساحة مخصصة لعنصر واحد من عناصر المصفوفة.
الفهرس (Index) هو ما يسمح لجافا بالقفز مباشرة إلى الموقع الصحيح في الذاكرة للوصول إلى العنصر المطلوب.
سيتم تخزين هذه القيم في الذاكرة بهذا الشكل (تخيل أن كل مربع هو "خلية" في الذاكرة):
موقع الذاكرة | القيمة | الفهرس
-------------|----------|--------
(عنوان بدء المصفوفة)
| 10 | a[0]
| 20 | a[1]
| 30 | a[2]
| 40 | a[3]
| 50 | a[4]
إذا أردت الوصول إلى `a[2]` (القيمة 30)، فإن جافا تعرف فوراً مكانها في الذاكرة بناءً على موقع بداية المصفوفة ونوع حجم البيانات والفهرس المطلوب.
الحلقات (Loops) هي الأداة المثالية للمرور على جميع عناصر المصفوفة أو جزء منها وتنفيذ عملية معينة عليها (مثل طباعة قيمها، جمعها، أو البحث عن قيمة معينة).
هذه الطريقة شائعة جدًا وتُستخدم عندما تحتاج إلى الوصول إلى **الفهرس** نفسه لكل عنصر، سواء لطباعته، أو لتعديل قيمة العنصر في ذلك الفهرس. يمكنك استخدام خاصية **`.length`** للمصفوفة للحصول على عدد عناصرها.
public class ForLoopWithArray {
public static void main(String[] args) {
int[] numbers = {10, 20, 30, 40, 50};
int sum = 0;
System.out.println("طباعة عناصر المصفوفة باستخدام for التقليدية:");
for(int i = 0; i < numbers.length; i++) { // يبدأ i من 0 ويستمر حتى أقل من numbers.length (أي 4)
System.out.println("العنصر في الفهرس " + i + " هو: " + numbers[i]);
sum += numbers[i]; // sum = sum + numbers[i];
}
System.out.println("مجموع عناصر المصفوفة هو: " + sum);
}
}
الناتج المتوقع:
طباعة عناصر المصفوفة باستخدام for التقليدية:
العنصر في الفهرس 0 هو: 10
العنصر في الفهرس 1 هو: 20
العنصر في الفهرس 2 هو: 30
العنصر في الفهرس 3 هو: 40
العنصر في الفهرس 4 هو: 50
مجموع عناصر المصفوفة هو: 150
هذه الحلقة أبسط وأكثر إيجازًا وهي مثالية عندما تحتاج فقط للمرور على **كل عنصر** في المصفوفة دون الاهتمام بالفهرس. لا يمكنك استخدامها لتعديل عناصر المصفوفة مباشرةً (إلا إذا كانت عناصر كائنات قابلة للتعديل).
البنية: `for (DataType elementVariable : arrayName) { ... }`
public class ForEachLoopWithArray {
public static void main(String[] args) {
String[] names = {"علي", "سارة", "خالد", "ليلى"};
System.out.println("طباعة عناصر المصفوفة باستخدام foreach:");
for(String name : names) { // لكل عنصر (name) من نوع String في مصفوفة names
System.out.println("الاسم: " + name);
}
}
}
الناتج المتوقع:
طباعة عناصر المصفوفة باستخدام foreach:
الاسم: علي
الاسم: سارة
الاسم: خالد
الاسم: ليلى
المصفوفات، كونها كائنات في جافا، يمكن تمريرها إلى الدوال كـ **وسائط (Arguments)**، كما يمكن للدوال أن ترجع مصفوفات كـ **قيم مرجعة (Return Values)**. هذا يعزز من تنظيم الكود وقابليته لإعادة الاستخدام.
عندما تمرر مصفوفة إلى دالة، فإن ما يتم تمريره هو **مرجع (Reference)** للمصفوفة، وليس نسخة منها. هذا يعني أن أي تغييرات تُجرى على عناصر المصفوفة داخل الدالة ستنعكس على المصفوفة الأصلية في دالة `main` (أو الدالة التي استدعتها).
public class PassArrayToMethod {
// دالة تستقبل مصفوفة من الأعداد الصحيحة وتطبع عناصرها
// لاحظ أننا نُمرر 'int[] array' كباراميتر
public static void printArray(int[] array) {
System.out.print("عناصر المصفوفة هي: ");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
System.out.println(); // لسطر جديد بعد الطباعة
}
// دالة لتعديل العنصر الأول في المصفوفة
public static void modifyFirstElement(int[] arr) {
System.out.println("تعديل العنصر الأول في المصفوفة داخل الدالة...");
arr[0] = 99; // تغيير قيمة العنصر الأول
}
public static void main(String[] args) {
int[] myNumbers = {1, 2, 3, 4, 5};
System.out.print("قبل التعديل: ");
printArray(myNumbers); // طباعة المصفوفة قبل التعديل
modifyFirstElement(myNumbers); // استدعاء دالة التعديل
System.out.print("بعد التعديل: ");
printArray(myNumbers); // طباعة المصفوفة بعد التعديل (ستلاحظ أن العنصر الأول تغير)
}
}
الناتج المتوقع:
قبل التعديل: عناصر المصفوفة هي: 1 2 3 4 5
تعديل العنصر الأول في المصفوفة داخل الدالة...
بعد التعديل: عناصر المصفوفة هي: 99 2 3 4 5
يمكن لدالة أن تُنشئ مصفوفة جديدة داخلها، أو تعدّل على مصفوفة موجودة ثم تُعيدها كقيمة مرجعة. يجب أن يكون نوع الإرجاع للدالة هو نوع المصفوفة (مثال: `int[]` أو `String[]`).
public class ReturnArrayFromMethod {
// دالة تستقبل مصفوفة وتعيد مصفوفة جديدة معكوسة الترتيب
public static int[] reverseArray(int[] originalArray) {
// إنشاء مصفوفة جديدة بنفس حجم المصفوفة الأصلية
int[] reversedResult = new int[originalArray.length];
// تعبئة المصفوفة الجديدة بالترتيب العكسي
for (int i = 0; i < originalArray.length; i++) {
reversedResult[i] = originalArray[originalArray.length - 1 - i];
}
return reversedResult; // إرجاع المصفوفة الجديدة
}
// دالة مساعدة لطباعة المصفوفة (يمكن استخدامها من المثال السابق)
public static void printArray(int[] array) {
System.out.print("عناصر المصفوفة هي: ");
for (int num : array) {
System.out.print(num + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] original = {10, 20, 30, 40, 50};
System.out.print("المصفوفة الأصلية: ");
printArray(original);
int[] reversed = reverseArray(original); // استدعاء الدالة وتخزين المصفوفة المرجعة
System.out.print("المصفوفة المعكوسة: ");
printArray(reversed);
}
}
الناتج المتوقع:
المصفوفة الأصلية: عناصر المصفوفة هي: 10 20 30 40 50
المصفوفة المعكوسة: عناصر المصفوفة هي: 50 40 30 20 10
المصفوفة ثنائية البعد هي في الواقع **مصفوفة من المصفوفات**. يمكن تخيلها كـ **جدول (Table)** أو شبكة تتكون من **صفوف (Rows)** و **أعمدة (Columns)**. تُستخدم لتخزين البيانات التي لها علاقة ثنائية أو تحتاج إلى تمثيل شبكي، مثل مصفوفة لعبة الشطرنج، أو درجات الطلاب في عدة مواد.
لتعريف مصفوفة ثنائية البعد، نستخدم زوجين من الأقواس المربعة `[][]`.
يمكن تهيئتها مباشرةً بالشكل التالي:
int[][] matrix = {
{1, 2, 3}, // الصف 0
{4, 5, 6}, // الصف 1
{7, 8, 9} // الصف 2
};
للوصول إلى عنصر معين، نستخدم فهرس الصف ثم فهرس العمود:
* `matrix[0][0]` → العنصر في الصف الأول، العمود الأول (القيمة 1).
* `matrix[1][1]` → العنصر في الصف الثاني، العمود الثاني (القيمة 5).
* `matrix[2][0]` → العنصر في الصف الثالث، العمود الأول (القيمة 7).
عادةً ما نستخدم حلقتين **`for` متداخلتين (Nested loops)** للمرور على جميع عناصر المصفوفة ثنائية البعد: الحلقة الخارجية للصفوف، والحلقة الداخلية للأعمدة.
public class TwoDArrayExample {
public static void main(String[] args) {
int[][] gameBoard = {
{1, 0, 1},
{0, 1, 0},
{1, 0, 1}
};
System.out.println("طباعة عناصر لوحة اللعب:");
// الحلقة الخارجية للصفوف
for (int row = 0; row < gameBoard.length; row++) {
// الحلقة الداخلية للأعمدة في الصف الحالي
for (int col = 0; col < gameBoard[row].length; col++) {
System.out.print(gameBoard[row][col] + " ");
}
System.out.println(); // بعد الانتهاء من كل صف، ننتقل لسطر جديد
}
}
}
الناتج المتوقع:
طباعة عناصر لوحة اللعب:
1 0 1
0 1 0
1 0 1
توفّر جافا بعض الكلاسات المساعدة التي تحتوي على دوال جاهزة (Built-in Methods) لتسهيل التعامل مع المصفوفات وإجراء عمليات شائعة عليها. أبرز هذه الكلاسات هي **`java.util.Arrays`** و **`java.lang.System`**.
هذا الكلاس يقدم مجموعة واسعة من دوال الخدمة الثابتة (static methods) للعمليات على المصفوفات أحادية البعد. لاستخدامه، تحتاج عادةً إلى استيراده في بداية ملف جافا الخاص بك: `import java.util.Arrays;`
import java.util.Arrays;
public class ArraySortExample {
public static void main(String[] args) {
int[] nums = {5, 2, 8, 1, 9};
Arrays.sort(nums); // ترتب المصفوفة: الآن nums ستكون {1, 2, 5, 8, 9}
System.out.println("المصفوفة بعد الترتيب: " + Arrays.toString(nums));
}
}
الناتج: `المصفوفة بعد الترتيب: [1, 2, 5, 8, 9]`
import java.util.Arrays;
public class ArrayEqualsExample {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2, 3};
int[] arr3 = {3, 2, 1};
System.out.println("هل arr1 تساوي arr2؟ " + Arrays.equals(arr1, arr2)); // الناتج: true
System.out.println("هل arr1 تساوي arr3؟ " + Arrays.equals(arr1, arr3)); // الناتج: false
}
}
import java.util.Arrays;
public class ArrayFillExample {
public static void main(String[] args) {
int[] data = new int[5]; // مصفوفة بحجم 5 عناصر
Arrays.fill(data, 100); // تعبئة جميع العناصر بالقيمة 100
System.out.println("المصفوفة بعد التعبئة: " + Arrays.toString(data)); // الناتج: [100, 100, 100, 100, 100]
}
}
import java.util.Arrays;
public class ArrayToStringExample {
public static void main(String[] args) {
int[] numbers = {10, 20, 30};
System.out.println("المصفوفة كنص: " + Arrays.toString(numbers)); // الناتج: المصفوفة كنص: [10, 20, 30]
}
}
يحتوي هذا الكلاس على دالة واحدة مهمة لنسخ المصفوفات، وهي دالة ثابتة (static) لا تحتاج لاستيرادها صراحةً لأنها جزء من حزمة `java.lang` التي تُستورد تلقائيًا.
import java.util.Arrays; // نحتاجها لطباعة المصفوفة بشكل سهل
public class ArrayCopyExample {
public static void main(String[] args) {
int[] source = {1, 2, 3, 4, 5};
int[] destination = new int[5]; // يجب أن تكون مصفوفة الوجهة موجودة مسبقًا
// نسخ جميع العناصر من source (بدءًا من الفهرس 0) إلى destination (بدءًا من الفهرس 0)، بطول source.length
System.arraycopy(source, 0, destination, 0, source.length);
System.out.println("المصفوفة المصدر: " + Arrays.toString(source));
System.out.println("المصفوفة الوجهة بعد النسخ: " + Arrays.toString(destination));
}
}
الناتج:
المصفوفة المصدر: [1, 2, 3, 4, 5]
المصفوفة الوجهة بعد النسخ: [1, 2, 3, 4, 5]
اختبر فهمك لمفهوم المصفوفات من خلال حل التمارين التالية. حاول كتابة الكود بنفسك قبل البحث عن الحلول!
اكتب برنامج جافا يُنشئ مصفوفة من الأعداد الصحيحة (مثلاً بحجم 5 عناصر) ويقوم بتعبئتها بقيم من اختيارك. ثم استخدم حلقة لحساب مجموع جميع العناصر في هذه المصفوفة واطبعه.
public class ArraySum {
public static void main(String[] args) {
// إنشاء وتعبئة المصفوفة
int[] numbers = {10, 20, 30, 40, 50}; // مثال
int sum = 0;
// اكتب الكود هنا لحساب المجموع باستخدام حلقة for أو foreach
System.out.println("مجموع عناصر المصفوفة هو: " + sum);
}
}
اكتب دالة اسمها `findMax` تستقبل مصفوفة من الأعداد الصحيحة كباراميتر، وترجع أكبر قيمة موجودة في هذه المصفوفة. ثم استدعِ هذه الدالة من `main` واطبع القيمة الكبرى.
public class FindMaxInArray {
// اكتب الدالة findMax هنا
// مثال: public static int findMax(int[] arr) { ... }
public static void main(String[] args) {
int[] data = {15, 7, 22, 9, 30, 1};
// استدعِ الدالة findMax واطبع النتيجة
// مثال: System.out.println("أكبر قيمة في المصفوفة هي: " + findMax(data));
}
}
اكتب برنامج جافا يُنشئ مصفوفة ثنائية البعد تمثل جدولاً بسيطاً (مثلاً 3 صفوف و 3 أعمدة). قم بتعبئتها بقيم من اختيارك (مثل أعداد صحيحة). ثم استخدم حلقات متداخلة لطباعة محتويات المصفوفة بحيث تبدو كجدول منظم على الكونسول.
public class PrintTwoDArray {
public static void main(String[] args) {
int[][] myMatrix = {
{10, 11, 12},
{20, 21, 22},
{30, 31, 32}
};
System.out.println("طباعة المصفوفة ثنائية البعد:");
// اكتب الكود هنا لطباعة المصفوفة باستخدام حلقات متداخلة
}
}
اكتب دالة اسمها `reverseInPlace` تستقبل مصفوفة من الأعداد الصحيحة كباراميتر، وتقوم بعكس ترتيب عناصر هذه المصفوفة **دون إنشاء مصفوفة جديدة**. أي يجب أن يتم التعديل على المصفوفة الأصلية مباشرةً. (تلميح: استخدم متغيرًا مؤقتًا لتبديل القيم).
import java.util.Arrays;
public class ReverseArrayInPlace {
// اكتب الدالة reverseInPlace هنا
// مثال: public static void reverseInPlace(int[] arr) { ... }
public static void main(String[] args) {
int[] originalArray = {1, 2, 3, 4, 5, 6};
System.out.println("المصفوفة الأصلية: " + Arrays.toString(originalArray));
// استدعِ الدالة reverseInPlace
// مثال: reverseInPlace(originalArray);
System.out.println("المصفوفة بعد العكس: " + Arrays.toString(originalArray));
}
}
```
لقد اكتسبتَ في هذه الوحدة الحاسمة فهمًا قوياً لمفهوم **المصفوفات (Arrays)** في جافا، وهي حجر الزاوية في التعامل مع البيانات المجمعة.
تُعد المصفوفات أداة أساسية لأي مبرمج، وستجدها مستخدمة بكثرة في مختلف أنواع التطبيقات. إن إتقانك لها سيفتح لك الباب لفهم هياكل بيانات أكثر تعقيدًا.
في الوحدة القادمة، سننتقل إلى أحد أهم وأقوى مفاهيم البرمجة الكائنية (OOP) في جافا: **الكائنات والكلاسات (Objects and Classes)**، وهو ما سيأخذ قدراتك البرمجية إلى مستوى جديد تماماً.