طرق طباعة المصفوفات في Java: دليل شامل للمطورين

دقائق القراءة: 7

تُعد المصفوفات (Arrays) من هياكل البيانات الأساسية في برمجة Java، وتُستخدم لتخزين مجموعة من العناصر من نفس النوع في مواقع ذاكرة متجاورة. في Java، المصفوفات هي كائنات (objects) بحد ذاتها، مما يعني أنها ترث جميع الطرق (methods) من الفئة Object. يمكننا تخزين عدد ثابت من العناصر في المصفوفة.

عندما نحاول طباعة مصفوفة باستخدام الطريقة المباشرة System.out.println()، قد نفاجأ بالنتيجة. لنأخذ مصفوفة بسيطة من الأعداد الصحيحة كمثال:

int [] intArray = { 2, 5, 46, 12, 34 };

إذا حاولنا طباعتها مباشرة:

System.out.println(intArray);
// output: [I@74a14482

لماذا لم تطبع Java محتوى المصفوفة؟ وما الذي يحدث في الكواليس؟

تُحوّل الطريقة System.out.println() الكائن الذي نمرره إليها إلى سلسلة نصية (String) عن طريق استدعاء الطريقة String.valueOf(). وإذا نظرنا إلى تنفيذ الطريقة String.valueOf()، سنجد ما يلي:

public static String valueOf (Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

إذا كان الكائن الممرر هو null، فإنها تُرجع “null”، وإلا فإنها تستدعي الطريقة obj.toString(). في النهاية، تستدعي System.out.println() الطريقة toString() لطباعة المخرجات. إذا لم تقم الفئة الخاصة بهذا الكائن بتجاوز (override) تنفيذ الطريقة Object.toString()، فسيتم استدعاء الطريقة الافتراضية Object.toString().

تُرجع الطريقة الافتراضية Object.toString() سلسلة نصية بالصيغة getClass().getName() + '@' + Integer.toHexString(hashCode()). بعبارة أبسط، تُرجع: “اسم الفئة @ رمز تجزئة الكائن (hash code)”.

في المخرجات السابقة [I@74a14482، يشير الرمز [ إلى أن هذا كائن مصفوفة، ويشير الرمز I إلى نوع العناصر (int). أما 74a14482 فهو التمثيل السداسي العشري غير الموقّع لرمز التجزئة الخاص بالمصفوفة.

مطور يعمل على شاشة عرض تعرض أكواد Java ومصفوفات بيانات

عند إنشاء فئاتنا المخصصة، من أفضل الممارسات تجاوز الطريقة Object.toString() لتوفير تمثيل نصي مفيد للكائن. لا يمكننا طباعة المصفوفات في Java باستخدام الطريقة System.out.println() وحدها. بدلاً من ذلك، يمكننا استخدام الطرق التالية لطباعة المصفوفة:

  • الحلقات التكرارية: for loop و for-each loop
  • طريقة Arrays.toString()
  • طريقة Arrays.deepToString()
  • طريقة Arrays.asList()
  • واجهة Java Iterator
  • واجهة Java Stream API

دعنا نستعرض هذه الطرق واحدة تلو الأخرى.

1. استخدام الحلقات التكرارية: for loop و for-each loop

تُعد الحلقات التكرارية من الطرق الأساسية والبسيطة لطباعة عناصر المصفوفة عن طريق المرور على كل عنصر على حدة. سنستعرض هنا مثالين لكل من حلقة for التقليدية وحلقة for-each المحسّنة.

حلقة for التقليدية

تسمح لنا حلقة for بالوصول إلى العناصر باستخدام مؤشر (index) كل عنصر. إليك مثال:

int [] intArray = { 2, 5, 46, 12, 34 };
for (int i = 0; i < intArray.length; i++){
    System.out.print(intArray[i]);
}
// output: 25461234

ملاحظة: جميع فئات التغليف (Wrapper classes) تتجاوز الطريقة Object.toString() وتُرجع تمثيلاً نصيًا لقيمتها.

حلقة for-each المحسّنة

تُعرف أيضًا باسم “الحلقة المعززة”، وهي طريقة أبسط للمرور على جميع عناصر المصفوفة أو أي مجموعة قابلة للتكرار دون الحاجة إلى التعامل مع المؤشرات بشكل مباشر. إليك مثال:

int [] intArray = { 2, 5, 46, 12, 34 };
for (int i : intArray){
    System.out.print(i);
}
// output: 25461234

2. طريقة Arrays.toString()

تُعد الطريقة Arrays.toString() طريقة ثابتة (static method) تابعة للفئة Arrays، والتي تنتمي إلى الحزمة java.util. تُرجع هذه الطريقة تمثيلاً نصيًا لمحتويات المصفوفة المحددة. يمكننا استخدامها لطباعة المصفوفات أحادية البعد (one-dimensional arrays).

تُحوّل عناصر المصفوفة إلى سلاسل نصية باستخدام الطريقة String.valueOf()، كما في المثال التالي:

int [] intArray = { 2, 5, 46, 12, 34 };
System.out.println(Arrays.toString(intArray));
// output: [2, 5, 46, 12, 34]

بالنسبة للمصفوفات من نوع المرجع (reference type array)، يجب التأكد من أن فئة نوع المرجع تتجاوز الطريقة Object.toString() لضمان الحصول على تمثيل نصي ذي معنى. على سبيل المثال:

public class Test {
    public static void main (String[] args) {
        Student[] students = { new Student("John"), new Student("Doe")};
        System.out.println(Arrays.toString(students));
        // output: [Student{name='John'}, Student{name='Doe'}]
    }
}

class Student {
    private String name;

    public Student (String name) {
        this.name = name;
    }

    public String getName () {
        return name;
    }

    public void setName (String name) {
        this.name = name;
    }

    @Override
    public String toString () {
        return "Student{" + "name='" + name + '\'' + '}';
    }
}

ملاحظة هامة: هذه الطريقة غير مناسبة للمصفوفات متعددة الأبعاد (multidimensional arrays). فهي تُحوّل المصفوفات متعددة الأبعاد إلى سلاسل نصية باستخدام Object.toString()، مما يصف هويتها (hash code) بدلاً من محتوياتها الفعلية. على سبيل المثال:

// إنشاء مصفوفة متعددة الأبعاد
int [][] multiDimensionalArr = { { 2, 3 }, { 5, 9 } };
System.out.println(Arrays.toString(multiDimensionalArr));
// output: [[I@74a14482, [I@1540e19d]

لحل هذه المشكلة، يمكننا استخدام الطريقة Arrays.deepToString() لطباعة المصفوفات متعددة الأبعاد بشكل صحيح.

3. طريقة Arrays.deepToString()

تُرجع الطريقة Arrays.deepToString() تمثيلاً نصيًا “عميقًا” لمحتويات المصفوفة المحددة. إذا كان العنصر عبارة عن مصفوفة من نوع بدائي (primitive type array)، فإنه يتم تحويله إلى سلسلة نصية عن طريق استدعاء التحميل الزائد المناسب (appropriate overloading) للطريقة Arrays.toString(). هذا يجعلها مثالية لطباعة المصفوفات متعددة الأبعاد.

طباعة مصفوفات بدائية متعددة الأبعاد

إليك مثال على مصفوفة بدائية متعددة الأبعاد:

// إنشاء مصفوفة متعددة الأبعاد
int [][] multiDimensionalArr = { { 2, 3 }, { 5, 9 } };
System.out.println(Arrays.deepToString(multiDimensionalArr));
// output: [[2, 3], [5, 9]]

طباعة مصفوفات مرجعية متعددة الأبعاد

إذا كان العنصر عبارة عن مصفوفة من نوع مرجع (reference type array)، فإنه يتم تحويله إلى سلسلة نصية عن طريق استدعاء Arrays.deepToString() بشكل متكرر (recursively). يجب التأكد من تجاوز الطريقة Object.toString() في الفئة المرجعية للحصول على مخرجات واضحة.

class Teacher {
    private String name;
    public Teacher(String name) { this.name = name; }
    @Override
    public String toString() { return "Teacher{name= '" + name + "' }"; }
}

// مثال على مصفوفة مرجعية متعددة الأبعاد
Teacher[][] teachers = {{ new Teacher("John"), new Teacher("David") }, { new Teacher("Mary")} };
System.out.println(Arrays.deepToString(teachers));
// output: [[Teacher{name= 'John' }, Teacher{name= 'David' }],[Teacher{name= 'Mary' }]]

ملاحظة: يمكن أيضًا طباعة المصفوفات أحادية البعد من نوع المرجع باستخدام هذه الطريقة.

Integer[] oneDimensionalArr = { 1, 4, 7 };
System.out.println(Arrays.deepToString(oneDimensionalArr));
// output: [1, 4, 7]

4. طريقة Arrays.asList()

تُرجع هذه الطريقة قائمة ذات حجم ثابت (fixed-size list) مدعومة بالمصفوفة المحددة. يمكن استخدامها لتحويل المصفوفة إلى قائمة ومن ثم طباعة القائمة.

Integer[] intArray = { 2, 5, 46, 12, 34 };
System.out.println(Arrays.asList(intArray));
// output: [2, 5, 46, 12, 34]

لقد قمنا بتغيير نوع المصفوفة من int إلى Integer، لأن List هي مجموعة (collection) تحتفظ بقائمة من الكائنات. عند تحويل مصفوفة إلى قائمة، يجب أن تكون مصفوفة من نوع مرجع (reference type).

تستدعي Java داخليًا الطريقة Arrays.asList(intArray).toString(). تستخدم هذه التقنية داخليًا الطريقة toString() الخاصة بنوع العناصر داخل القائمة.

مثال مع فئة Teacher المخصصة

Teacher[] teacher = { new Teacher("John"), new Teacher("Mary") };
System.out.println(Arrays.asList(teacher));
// output: [Teacher{name='John'}, Teacher{name='Mary'}]

ملاحظة هامة: لا يمكننا طباعة المصفوفات متعددة الأبعاد باستخدام هذه الطريقة. على سبيل المثال:

Teacher[][] teachers = {{ new Teacher("John"), new Teacher("David") }, { new Teacher("Mary") }};
System.out.println(Arrays.asList(teachers));
// output: [[Lcom.thano.article.printarray.Teacher;@1540e19d, [Lcom.thano.article.printarray.Teacher;@677327b6]

كما نرى، تعود المشكلة الأصلية حيث يتم طباعة رموز التجزئة بدلاً من المحتويات الفعلية للمصفوفات الداخلية.

5. واجهة Java Iterator

على غرار حلقة for-each، يمكننا استخدام واجهة Iterator للمرور عبر عناصر المصفوفة وطباعتها. يتم إنشاء كائن Iterator عن طريق استدعاء الطريقة iterator() على أي كائن Collection. سيتم استخدام هذا الكائن للتكرار على عناصر تلك المجموعة.

إليك مثال يوضح كيفية طباعة مصفوفة باستخدام واجهة Iterator:

Integer[] intArray = { 2, 5, 46, 12, 34 };
// إنشاء قائمة من نوع Integer
List<Integer> list = Arrays.asList(intArray);
// إنشاء مُكرّر (iterator) لقائمة Integer
Iterator<Integer> it = list.iterator();
// طالما أن القائمة تحتوي على عناصر للتكرار
while (it.hasNext()) {
    System.out.print(it.next());
}
// output: 25461234

6. واجهة Java Stream API

تُستخدم واجهة Stream API لمعالجة مجموعات الكائنات. يُعد الـ “Stream” تسلسلاً من الكائنات لا يُغير هيكل البيانات الأصلي، بل يوفر فقط النتيجة وفقًا للعمليات المطلوبة. بمساعدة عملية forEach() الطرفية، يمكننا التكرار عبر كل عنصر من عناصر الـ “Stream”.

على سبيل المثال:

Integer[] intArray = { 2, 5, 46, 12, 34 };
Arrays.stream(intArray).forEach(System.out::print);
// output: 25461234

تُقدم Stream API طريقة حديثة وفعالة للتعامل مع المجموعات، وهي مفيدة بشكل خاص للعمليات المعقدة على البيانات.

الخلاصة التقنية

في هذا الدليل الشامل، استعرضنا الطرق المتعددة والفعالة لطباعة المصفوفات في Java، بدءًا من فهم سبب عدم كفاءة الطباعة المباشرة باستخدام System.out.println() وصولاً إلى الحلول المتقدمة. لقد رأينا أن الطريقة الافتراضية Object.toString() تُقدم معلومات حول هوية الكائن وليس محتواه، مما يجعل تجاوزها ضروريًا في الفئات المخصصة.

تُعد الحلقات التكرارية (for و for-each) حلولًا مباشرة وفعالة للمصفوفات أحادية البعد. بينما توفر الطريقة Arrays.toString() حلاً أنيقًا لطباعة المصفوفات أحادية البعد بتنسيق مقروء، وتُبرز أهمية تجاوز toString() للفئات المرجعية. أما للمصفوفات متعددة الأبعاد، فإن Arrays.deepToString() هي الخيار الأمثل، حيث تتعمق في محتويات المصفوفات المتداخلة.

كما تطرقنا إلى استخدام Arrays.asList() لتحويل المصفوفات المرجعية إلى قوائم قابلة للطباعة، وواجهة Iterator للتكرار المرن، وأخيرًا Java Stream API كطريقة حديثة وفعالة لمعالجة وطباعة عناصر المصفوفة. اختيار الطريقة الأنسب يعتمد على نوع المصفوفة (أحادية/متعددة الأبعاد، بدائية/مرجعية) ومتطلبات التنسيق للمخرجات. فهم هذه الطرق يُمكن المطورين من التعامل بفعالية مع المصفوفات وتقديم مخرجات واضحة ومفيدة في تطبيقاتهم.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *