دليل شامل لـ List API في Java: الدوال، الأنواع، ومقارنة الأداء
القوائم (Lists) هي هياكل بيانات شائعة الاستخدام في كل لغة برمجة تقريباً. في هذا الدليل، سنتعمق في استكشاف واجهة برمجة تطبيقات القوائم (List API) في Java. سنبدأ بالعمليات الأساسية، ثم ننتقل إلى مفاهيم أكثر تقدماً مثل مقارنة أنواع القوائم المختلفة (ArrayList و LinkedList). سأقدم لك أيضاً إرشادات لمساعدتك في اختيار التنفيذ الأنسب لـ List الذي يلائم موقفك. على الرغم من أن المعرفة الأساسية بلغة Java كافية لمتابعة هذا الدليل، إلا أن القسم الأخير يتطلب معرفة بهياكل البيانات الأساسية (Array، LinkedList) ومفهوم التعقيد الزمني (Big-O). إذا لم تكن على دراية بهذه المفاهيم، فلا تتردد في تخطي هذا القسم.
ما هي القوائم (Lists) في Java؟
القوائم هي مجموعات مرتبة من الكائنات (objects). تتشابه في هذا الجانب مع المتتاليات الرياضية، لكنها تختلف عن المجموعات (sets) التي لا تفرض ترتيباً معيناً. من الأمور المهمة التي يجب تذكرها بخصوص القوائم في Java أنها تسمح بالعناصر المكررة (duplicates) والعناصر الفارغة (null elements). هي أنواع مرجعية (reference types) أو كائنية (object types)، ومثل جميع الكائنات في Java، يتم تخزينها في الذاكرة الحرة (heap). في Java، تعتبر القائمة (List) واجهة (interface)، وهناك العديد من أنواع القوائم التي تطبق هذه الواجهة.

سأستخدم ArrayList في الأمثلة القليلة الأولى، لأنه النوع الأكثر شيوعاً من القوائم. ArrayList هو في الأساس مصفوفة قابلة لتغيير الحجم (resizable array). في معظم الحالات، يُفضل استخدام ArrayList على المصفوفات العادية (regular arrays) نظراً لما توفره من دوال مفيدة عديدة. كانت الميزة الوحيدة للمصفوفات هي حجمها الثابت (بعدم تخصيص مساحة أكبر مما تحتاج)، ولكن القوائم تدعم أيضاً الأحجام الثابتة الآن.
كيفية إنشاء قائمة (List) في Java
كفى حديثاً، دعنا نبدأ بإنشاء قائمتنا.
import java.util.ArrayList;
import java.util.List;
public class CreateArrayList {
public static void main (String[] args) {
ArrayList<Integer> list0 = new ArrayList<>();
// Makes use of polymorphism
List<Integer> list = new ArrayList<Integer>();
// Local variable with "var" keyword, Java 10
var list2 = new ArrayList<Integer>();
}
}
في الأقواس الزاوية (<>)، نحدد نوع الكائنات التي سنقوم بتخزينها. تذكر أن النوع داخل الأقواس يجب أن يكون نوع كائن (object type) وليس نوعاً أولياً (primitive type). لذلك، يجب علينا استخدام أغلفة الكائنات (object wrappers) مثل فئة Integer بدلاً من int، و Double بدلاً من double، وهكذا. هناك العديد من الطرق لإنشاء ArrayList، ولكنني قدمت ثلاث طرق شائعة في المقتطف أعلاه. الطريقة الأولى هي بإنشاء الكائن من فئة ArrayList الملموسة (concrete class) عن طريق تحديد ArrayList على الجانب الأيسر من التعيين. المقتطف الثاني يستخدم تعدد الأشكال (polymorphism) باستخدام List على الجانب الأيسر. هذا يجعل التعيين غير مرتبط بشكل وثيق بفئة ArrayList ويسمح لنا بتعيين أنواع أخرى من القوائم والتبديل إلى تنفيذ List مختلف بسهولة. الطريقة الثالثة هي طريقة Java 10 لإنشاء المتغيرات المحلية باستخدام الكلمة المفتاحية var. يقوم المترجم (compiler) بتفسير نوع المتغير عن طريق فحص الجانب الأيمن. يمكننا أن نرى أن جميع التعيينات تؤدي إلى نفس النوع:
System.out.println(list0.getClass());
System.out.println(list.getClass());
System.out.println(list2.getClass());
الناتج:
class java . util . ArrayList
class java . util . ArrayList
class java . util . ArrayList
يمكننا أيضاً تحديد السعة الأولية للقائمة.
List<Integer> list = new ArrayList<>( 20 );
هذا مفيد لأنه كلما امتلأت القائمة وحاولت إضافة عنصر آخر، يتم نسخ القائمة الحالية إلى قائمة جديدة بسعة مضاعفة للقائمة السابقة. يحدث كل هذا خلف الكواليس. هذه العملية تجعل تعقيدنا الزمني (complexity) هو O(n)، لذلك نرغب في تجنبها. السعة الافتراضية هي 10، لذا إذا كنت تعلم أنك ستخزن المزيد من العناصر، يجب عليك تحديد السعة الأولية.
إضافة وتحديث عناصر القائمة في Java
لإضافة عناصر إلى القائمة، يمكننا استخدام الدالة add(). يمكننا أيضاً تحديد فهرس (index) العنصر الجديد، ولكن كن حذراً عند القيام بذلك لأنه قد يؤدي إلى استثناء IndexOutOfBoundsException.
import java.util.ArrayList;
public class AddElement {
public static void main (String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("hello");
list.add(1, "world");
System.out.println(list);
}
}
الناتج:
[hello, world]
يمكننا استخدام الدالة set() لتحديث عنصر.
list.set( 1 , "from the otherside" );
System.out.println(list);
الناتج:
[hello, world]
[hello, from the otherside]
استرداد وحذف عناصر القائمة في Java
لاسترداد عنصر من القائمة، يمكنك استخدام الدالة get() وتوفير فهرس العنصر الذي تريد الحصول عليه.
import java.util.ArrayList;
import java.util.List;
public class GetElement {
public static void main (String[] args) {
List<String> list = new ArrayList<String>();
list.add("hello");
list.add("freeCodeCamp");
System.out.println(list.get(1));
}
}
الناتج:
freeCodeCamp
تعقيد هذه العملية على ArrayList هو O(1) نظراً لأنها تستخدم مصفوفة وصول عشوائي عادية (random access array) في الخلفية. لإزالة عنصر من ArrayList، تُستخدم الدالة remove().
list.remove( 0 );
هذا يزيل العنصر في الفهرس 0، وهو "hello" في هذا المثال. يمكننا أيضاً استدعاء الدالة remove() مع عنصر للبحث عنه وإزالته. تذكر أنها تزيل فقط أول ظهور للعنصر إذا كان موجوداً.
public static void main (String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("freeCodeCamp");
list.add("freeCodeCamp");
list.remove("freeCodeCamp");
System.out.println(list);
}
الناتج:
[hello, freeCodeCamp]
لإزالة جميع التكرارات، يمكننا استخدام الدالة removeAll() بنفس الطريقة. توجد هذه الدوال داخل واجهة List، لذا فإن جميع تطبيقات List تحتوي عليها (سواء كانت ArrayList أو LinkedList أو Vector).
الحصول على حجم القائمة في Java
للحصول على طول القائمة، أو عدد العناصر، يمكننا استخدام الدالة size().
import java.util.ArrayList;
import java.util.List;
public class GetSize {
public static void main (String[] args) {
List<String> list = new ArrayList<>();
list.add("Welcome");
list.add("to my post");
System.out.println(list.size());
}
}
الناتج:
2
القوائم ثنائية الأبعاد في Java
من الممكن إنشاء قوائم ثنائية الأبعاد، على غرار المصفوفات ثنائية الأبعاد (2D arrays).
ArrayList<ArrayList<Integer>> listOfLists = new ArrayList<>();
نستخدم هذا التركيب لإنشاء قائمة من القوائم، وكل قائمة داخلية تخزن أعداداً صحيحة. لكننا لم نقم بتهيئة القوائم الداخلية بعد. نحتاج إلى إنشائها ووضعها في هذه القائمة بأنفسنا:
int numberOfLists = 3 ;
for ( int i = 0 ; i < numberOfLists; i++) {
listOfLists.add( new ArrayList<>());
}
أقوم بتهيئة قوائمي الداخلية، وأضيف 3 قوائم في هذه الحالة. يمكنني أيضاً إضافة قوائم لاحقاً إذا احتجت إلى ذلك. الآن يمكننا إضافة عناصر إلى قوائمنا الداخلية. لإضافة عنصر، نحتاج أولاً إلى الحصول على مرجع (reference) للقائمة الداخلية. على سبيل المثال، لنفترض أننا نريد إضافة عنصر إلى القائمة الأولى. نحتاج إلى الحصول على القائمة الأولى، ثم الإضافة إليها.
listOfLists.get( 0 ).add( 1 );
إليك مثال لك. حاول تخمين مخرجات مقطع الكود التالي:
public static void main (String[] args) {
ArrayList<ArrayList<Integer>> listOfLists = new ArrayList<>();
System.out.println(listOfLists);
int numberOfLists = 3 ;
for ( int i = 0 ; i < numberOfLists; i++) {
listOfLists.add( new ArrayList<>());
}
System.out.println(listOfLists);
listOfLists.get( 0 ).add( 1 );
listOfLists.get( 1 ).add( 2 );
listOfLists.get( 2 ).add( 0 , 3 );
System.out.println(listOfLists);
}
الناتج:
[]
[[], [], []]
[[ 1 ], [ 2 ], [ 3 ]]
لاحظ أنه من الممكن طباعة القوائم مباشرة (على عكس المصفوفات العادية) لأنها تتجاوز الدالة toString().
دوال مفيدة أخرى في Java List API
هناك بعض الدوال والاختصارات الأخرى المفيدة التي تستخدم بشكل متكرر. في هذا القسم، أريد أن أجعلك على دراية ببعضها لتسهيل عملك مع القوائم.
إنشاء قائمة بعناصر مسبقة في Java
من الممكن إنشاء القائمة وتعبئتها ببعض العناصر في سطر واحد. هناك طريقتان للقيام بذلك. الطريقة القديمة هي كالتالي:
public static void main (String[] args) {
List<String> list = Arrays.asList("freeCodeCamp", "let's", "create");
}
يجب أن تكون حذراً بشأن شيء واحد عند استخدام هذه الطريقة: الدالة Arrays.asList() تُرجع قائمة غير قابلة للتعديل (immutable list). لذا إذا حاولت إضافة أو إزالة عناصر بعد إنشاء الكائن، ستحصل على استثناء UnsupportedOperationException. قد تميل إلى استخدام الكلمة المفتاحية final لجعل القائمة غير قابلة للتعديل ولكنها لن تعمل كما هو متوقع. إنها تضمن فقط أن مرجع الكائن لا يتغير – ولا تهتم بما يحدث داخل الكائن. لذلك تسمح بالإدراج والإزالة.
final List<String> list2 = new ArrayList<>();
list2.add("erinc.io is the best blog ever!");
System.out.println(list2);
الناتج:
[erinc.io is the best blog ever!]
الآن دعنا نلقي نظرة على الطريقة الحديثة للقيام بذلك:
ArrayList<String> friends = new ArrayList<>(List.of( "Gulbike" , "Sinem" , "Mete" ));
تم شحن الدالة List.of() مع Java 9. تُرجع هذه الدالة أيضاً قائمة غير قابلة للتعديل، ولكن يمكننا تمريرها إلى مُنشئ (constructor) ArrayList لإنشاء قائمة قابلة للتعديل (mutable list) بهذه العناصر. يمكننا إضافة وإزالة العناصر إلى هذه القائمة دون أي مشاكل.
إنشاء قائمة تحتوي على N نسخة من عنصر معين
توفر Java دالة تسمى nCopies() وهي مفيدة بشكل خاص لاختبار الأداء (benchmarking). يمكنك ملء مصفوفة بأي عدد من العناصر في سطر واحد.
public class NCopies {
public static void main (String[] args) {
List<String> list = Collections.nCopies( 10 , "HELLO" );
System.out.println(list);
}
}
الناتج:
[HELLO, HELLO, HELLO, HELLO, HELLO, HELLO, HELLO, HELLO, HELLO, HELLO]
استنساخ قائمة (Clone a List) في Java
كما ذكرنا سابقاً، القوائم هي أنواع مرجعية (reference types)، لذا فإن قواعد التمرير بالمرجع (passing by reference) تنطبق عليها.
public static void main (String[] args) {
List<String> list1 = new ArrayList<String>();
list1.add("Hello");
List<String> list2 = list1;
list2.add(" World");
System.out.println(list1);
System.out.println(list2);
}
الناتج:
[Hello, World]
[Hello, World]
يحتوي المتغير list1 على مرجع للقائمة. عندما نعيّنه إلى list2، فإنه يشير أيضاً إلى نفس الكائن. إذا لم نرغب في تغيير القائمة الأصلية، يمكننا استنساخ القائمة.
ArrayList<String> list3 = (ArrayList<String>) list1.clone();
list3.add(" Of Java");
System.out.println(list1);
System.out.println(list3);
الناتج:
[Hello, World]
[Hello, World, Of Java]
بما أننا استنسخنا list1، فإن list3 يحتوي على مرجع لنسخته في هذه الحالة. وبالتالي تظل list1 دون تغيير.
تحويل قائمة إلى مصفوفة في Java
أحياناً تحتاج إلى تحويل قائمتك إلى مصفوفة لتمريرها إلى دالة تقبل مصفوفة. يمكنك استخدام الكود التالي لتحقيق ذلك:
List<Integer> list = new ArrayList<>(List.of( 1 , 2 ));
Integer[] toArray = list.toArray( new Integer[ 0 ]);
تحتاج إلى تمرير مصفوفة، والدالة toArray() تُرجع تلك المصفوفة بعد ملئها بعناصر القائمة.
فرز قائمة (Sort a List) في Java
لفرز قائمة، يمكننا استخدام Collections.sort(). تقوم بالفرز بترتيب تصاعدي افتراضياً ولكن يمكنك أيضاً تمرير مقارن (comparator) للفرز بمنطق مخصص.
List<Integer> toBeSorted = new ArrayList<>(List.of( 3 , 2 , 4 , 1 ,- 2 ));
Collections.sort(toBeSorted);
System.out.println(toBeSorted);
الناتج:
[ -2 , 1 , 2 , 3 , 4 ]
كيف تختار نوع القائمة الأنسب لاستخدامك؟
قبل إنهاء هذه المقالة، أرغب في تقديم مقارنة موجزة للأداء بين تطبيقات القوائم المختلفة حتى تتمكن من اختيار الأنسب لحالة استخدامك. سنقارن ArrayList و LinkedList و Vector. لكل منها مزايا وعيوب، لذا تأكد من مراعاة السياق المحدد قبل اتخاذ قرارك.
مقارنة بين Java ArrayList و LinkedList
إليك مقارنة لأوقات التشغيل من حيث التعقيد الخوارزمي (algorithmic complexity).
ArrayList |
LinkedList |
|
|---|---|---|
GET(index) |
O(1) |
O(n) |
GET from Start or End |
O(1) |
O(1) |
ADD |
O(1), إذا كانت القائمة ممتلئة O(n) |
O(1) |
ADD(index) |
O(n) |
O(1) |
Remove(index) |
O(n) |
O(1) |
Search and Remove |
O(n) |
O(n) |
بشكل عام، عملية get أسرع بكثير في ArrayList، بينما عمليتا add و remove أسرع في LinkedList. يستخدم ArrayList مصفوفة في الخلفية، وكلما تم إزالة عنصر، تحتاج عناصر المصفوفة إلى الإزاحة (وهي عملية O(n)). اختيار هياكل البيانات مهمة معقدة ولا توجد وصفة تنطبق على كل موقف. ومع ذلك، سأحاول تقديم بعض الإرشادات لمساعدتك في اتخاذ هذا القرار بسهولة أكبر:
- إذا كنت تخطط لإجراء المزيد من عمليات
getوaddبخلافremove، فاستخدمArrayListلأن عمليةgetمكلفة للغاية فيLinkedList. تذكر أن الإدراج هوO(1)فقط إذا قمت باستدعائه دون تحديد الفهرس والإضافة إلى نهاية القائمة. - إذا كنت ستقوم بإزالة العناصر و/أو الإدراج في المنتصف (وليس في النهاية) بشكل متكرر، يمكنك التفكير في التحول إلى
LinkedListلأن هذه العمليات مكلفة فيArrayList. - تذكر أنه إذا كنت تصل إلى العناصر بشكل تسلسلي (باستخدام
iterator)، فلن تواجه فقداناً في الأداء معLinkedListأثناء الحصول على العناصر.

مصدر المقارنة: programcreek
مقارنة بين Java ArrayList و Vector
Vector مشابه جداً لـ ArrayList. إذا كنت قادماً من خلفية C++، فقد تميل إلى استخدام Vector، ولكن حالة استخدامه مختلفة قليلاً عن C++. تحتوي دوال Vector على الكلمة المفتاحية synchronized، لذا يضمن Vector الأمان على مستوى الخيوط (thread safety) بينما لا يضمنه ArrayList. قد تفضل Vector على ArrayList في البرمجة متعددة الخيوط (multithreaded programming) أو يمكنك استخدام ArrayList والتعامل مع المزامنة بنفسك. في برنامج أحادي الخيط (single-threaded program)، من الأفضل التمسك بـ ArrayList لأن الأمان على مستوى الخيوط يأتي بتكلفة أداء.
الخلاصة
في هذا المقال، حاولت تقديم نظرة عامة على List API في Java. لقد تعلمنا كيفية استخدام الدوال الأساسية، وتناولنا أيضاً بعض الحيل المتقدمة لتسهيل حياتنا البرمجية. كما أجرينا مقارنة بين ArrayList و LinkedList و Vector، وهو موضوع يُطرح بشكل شائع في المقابلات التقنية. شكراً لك على تخصيص وقت لقراءة المقال كاملاً وآمل أن يكون مفيداً. يمكنك الوصول إلى الكود كاملاً من هذا المستودع. إذا كنت مهتماً بقراءة المزيد من المقالات المشابهة، يمكنك الاشتراك في القائمة البريدية لمدونتي لتلقي إشعارات عند نشر مقال جديد.
الخلاصة التقنية
تُعد واجهة List في Java حجر الزاوية في التعامل مع المجموعات المرتبة من الكائنات، وتوفر مرونة وقوة للمطورين. الفهم العميق للفروقات بين تطبيقاتها الشائعة مثل ArrayList و LinkedList، لا سيما من حيث التعقيد الزمني (Big-O) لعمليات الإضافة، الحذف، والاسترداد، أمر بالغ الأهمية لتحسين أداء التطبيقات. بينما يتألق ArrayList في سرعة الوصول العشوائي (O(1)) بفضل طبيعته القائمة على المصفوفات، فإن LinkedList يوفر كفاءة عالية (O(1)) في عمليات الإدراج والحذف في المنتصف، مما يجعله الخيار الأمثل للسيناريوهات التي تتطلب تعديلات متكررة على بنية القائمة. أما Vector، فرغم توفيره للأمان على مستوى الخيوط، إلا أن تكلفته في الأداء تجعله أقل تفضيلاً في البيئات أحادية الخيط. اختيار التنفيذ الصحيح يعتمد بشكل مباشر على نمط الاستخدام المتوقع للقائمة، مما يؤكد على أهمية التحليل الدقيق لمتطلبات المشروع.