أهلاً! في الدرس الأخير، تعرفنا على الفصل
ArrayList
، وتعلمنا كيفية إجراء العمليات الأكثر شيوعًا مع هذا الفصل. بالإضافة إلى ذلك، أشرنا إلى عدة اختلافات بين ArrayList
المصفوفة العادية والمصفوفة العادية. لكننا تجاوزنا موضوعًا واحدًا، وهو كيفية حذف العناصر من ملف ArrayList
. سنناقش ذلك الآن. لقد ذكرنا بالفعل أن حذف العناصر من مصفوفة عادية ليس أمرًا مريحًا للغاية. نظرًا لأننا لا نستطيع حذف العنصر نفسه، فيمكننا فقط "إزالة" قيمته (ضبطها على قيمة خالية):
public class Cat {
private String name;
public Cat(String name) {
this.name = name;
}
public static void main(String[] args) {
Cat[] cats = new Cat[3];
cats[0] = new Cat("Thomas");
cats[1] = new Cat("Behemoth");
cats[2] = new Cat("Lionel Messi");
cats[1] = null;
System.out.println(Arrays.toString(cats));
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
الإخراج: [Cat{name='Thomas'}, null, Cat{name='Lionel Messi'}] لكن تعيين عنصر مصفوفة على القيمة null يترك "فجوة". لم نقم بإزالة الموضع في المصفوفة، بل محتوياتها فقط. تخيل ماذا سيحدث إذا كان لدينا مجموعة مكونة من 50 قطة وقمنا بإزالة 17 منها بهذه الطريقة. سيكون لدينا مصفوفة بها 17 حفرة. فقط حاول تتبعهم! من غير الواقعي أن تتوقع أن تتذكر عدد الخلايا الفارغة التي يمكنك كتابة قيم جديدة فيها. إذا قمت بخطأ واحد، فسوف تقوم بالكتابة فوق مرجع الكائن الذي تريده. هناك، بالطبع، طريقة للقيام بذلك بعناية أكبر: بعد إزالة عنصر، انقل العناصر إلى مقدمة المصفوفة لوضع "الفتحة" في النهاية:
public static void main(String[] args) {
Cat[] cats = new Cat[4];
cats[0] = new Cat("Thomas");
cats[1] = new Cat("Behemoth");
cats[2] = new Cat("Lionel Messi");
cats[2] = new Cat("Fluffy");
cats[1] = null;
for (int i = 2; i < cats.length-1; i++) {
cats [i-1] = cats [i];// Move the elements to the front of the array, so the empty position is at the end
}
System.out.println(Arrays.toString(cats));
}
الإخراج: [Cat{name='Thomas'}, Cat{name='Fluffy'}, Cat{name='Fluffy'}, null] يبدو هذا أفضل، لكن من الصعب وصفه بالحل القوي. إذا لم يكن هناك سبب آخر سوى حقيقة أنه يتعين علينا كتابة هذا الرمز في كل مرة نقوم فيها بحذف عنصر من مصفوفة! هذا خيار سيء. يمكننا أن نذهب بطريقة أخرى وننشئ طريقة منفصلة:
public void deleteCat(Cat[] cats, int indexToDelete) {
//...delete the cat corresponding to the index and move the elements
}
ولكن هذا أيضًا قليل الفائدة: هذه الطريقة يمكن أن تعمل فقط مع Cat
الكائنات، وليس الأنواع الأخرى. بمعنى آخر، إذا كان لدى البرنامج 100 فئة أخرى نريد استخدامها مع المصفوفات، فسيتعين علينا كتابة نفس الطريقة بنفس المنطق تمامًا في كل منها. هذه كارثة كاملة -_- لكن ArrayList
الفصل يحل هذه المشكلة! يطبق طريقة خاصة لإزالة العناصر:remove()
public static void main(String[] args) {
ArrayList<Cat> cats = new ArrayList<>();
Cat thomas = new Cat("Thomas");
Cat behemoth = new Cat("Behemoth");
Cat lionel = new Cat("Lionel Messi");
Cat fluffy = new Cat ("Fluffy");
cats.add(thomas);
cats.add(behemoth);
cats.add(lionel);
cats.add(fluffy);
System.out.println(cats.toString());
cats.remove(1);
System.out.println(cats.toString());
}
نقوم بتمرير فهرس كائننا إلى الطريقة التي تحذفه (تمامًا كما هو الحال في المصفوفة). تحتوي الطريقة remove()
على ميزتين خاصتين. أولا، لا يترك "الثقوب". إنه يطبق بالفعل المنطق اللازم لتبديل العناصر عند إزالة عنصر من المنتصف، وهو ما كتبناه بأنفسنا سابقًا. انظر إلى الإخراج من الكود السابق:
[Cat{name='Thomas'}, Cat{name='Behemoth'}, Cat{name='Lionel Messi'}, Cat{name='Fluffy'}]
[Cat{name='Thomas'}, Cat{name='Lionel Messi'}, Cat{name='Fluffy'}]
قمنا بإزالة قطة واحدة من المنتصف، وتم نقل الباقي حتى لا تكون هناك مساحات فارغة. ثانيًا ، يمكنه حذف الكائنات ليس فقط عن طريق الفهرس (مثل المصفوفة العادية)، ولكن أيضًا عن طريق المرجع :
public static void main(String[] args) {
ArrayList<Cat> cats = new ArrayList<>();
Cat thomas = new Cat("Thomas");
Cat behemoth = new Cat("Behemoth");
Cat lionel = new Cat("Lionel Messi");
Cat fluffy = new Cat ("Fluffy");
cats.add(thomas);
cats.add(behemoth);
cats.add(lionel);
cats.add(fluffy);
System.out.println(cats.toString());
cats.remove(lionel);
System.out.println(cats.toString());
}
الإخراج: [قطة {اسم='توماس'}، قطة{اسم='بيهيموث'}، قطة{اسم='ليونيل ميسي'}، قطة{اسم='رقيق'}] [قطة{اسم='توماس'}، Cat{name='Behemoth'}, Cat{name='Fluffy'}] يمكن أن يكون هذا مناسبًا جدًا إذا كنت لا تريد دائمًا تتبع فهرس الكائن المطلوب. يبدو أننا اكتشفنا الحذف العادي. الآن دعونا نتخيل هذا الموقف: نريد تكرار قائمتنا وإزالة قطة باسم محدد . للقيام بذلك، سنستخدم for
حلقة سريعة (وتسمى أيضًا حلقة for-each)، والتي تعرفنا عليها في دروس ريشي:
public static void main(String[] args) {
ArrayList<Cat> cats = new ArrayList<>();
Cat thomas = new Cat("Thomas");
Cat behemoth = new Cat("Behemoth");
Cat lionel = new Cat("Lionel Messi");
Cat fluffy = new Cat ("Fluffy");
cats.add(thomas);
cats.add(behemoth);
cats.add(lionel);
cats.add(fluffy);
for (Cat cat: cats) {
if (cat.name.equals("Behemoth")) {
cats.remove(cat);
}
}
System.out.println(cats);
}
يبدو الرمز منطقيًا تمامًا. ولكن النتيجة قد تكون مفاجأة كبيرة: استثناء في مؤشر الترابط "الرئيسي" java.util.ConcurrentModificationException في java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) في java.util.ArrayList$Itr.next(ArrayList. java:831) في Cat.main(Cat.java:25) هناك نوع من الخطأ، وليس من الواضح سبب حدوثه. تتضمن هذه العملية عددًا من الفروق الدقيقة التي يجب معالجتها. إليك القاعدة العامة التي يجب أن تتذكرها: لا يمكنك التكرار على مجموعة وتغيير عناصرها في نفس الوقت. ونحن نعني أي نوع من التغيير، وليس مجرد الإزالة. إذا استبدلت عملية إزالة القطط بمحاولة إدخال قطط جديدة، فستكون النتيجة واحدة:
for (Cat cat: cats) {
cats.add(new Cat("Salem Saberhagen"));
}
System.out.println(cats);
استثناء في مؤشر الترابط "الرئيسي" java.util.ConcurrentModificationException في java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) في java.util.ArrayList$Itr.next(ArrayList.java:831) في Cat.main( Cat.java:25) قمنا بتغيير عملية إلى أخرى، لكن النتيجة لم تتغير: لقد حصلنا على نفس ConcurrentModificationException . ويحدث ذلك على وجه التحديد عندما نحاول كسر القاعدة المذكورة أعلاه عن طريق تغيير القائمة أثناء التكرار عليها. في Java، نحتاج إلى كائن خاص يسمى المكرر (Iterator
الفئة) لحذف العناصر أثناء التكرار على مجموعة. الفصلIterator
مسؤول عن التكرار الآمن لقائمة العناصر. الأمر بسيط جدًا، لأنه يحتوي على 3 طرق فقط:
hasNext()
- إرجاع صحيح أو خطأ، اعتمادًا على ما إذا كان هناك عنصر تالي في القائمة، أو أننا وصلنا بالفعل إلى العنصر الأخير.next()
- إرجاع العنصر التالي في القائمةremove()
- إزالة عنصر من القائمة
Iterator<Cat> catIterator = cats.iterator();// Create an iterator
while(catIterator.hasNext()) {// As long as there are elements in the list
Cat nextCat = catIterator.next();// Get the next element
System.out.println(nextCat);// Display it
}
الإخراج: Cat{name='Thomas'} Cat{name='Behemoth'} Cat{name='Lionel Messi'} Cat{name='Fluffy'} كما ترون، فقد طبق ArrayList
بالفعل طريقة خاصة لإنشاء مكرر : iterator()
. بالإضافة إلى ذلك، لاحظ أنه عندما نقوم بإنشاء مكرر، فإننا نحدد فئة الكائنات التي سيعمل معها ( <Cat>
). خلاصة القول هي أن المكرر يتعامل بسهولة مع مهمتنا الأصلية. على سبيل المثال، قم بإزالة القطة المسماة "ليونيل ميسي":
Iterator<Cat> catIterator = cats.iterator();// Create an iterator
while(catIterator.hasNext()) {// As long as there are elements in the list
Cat nextCat = catIterator.next();// Get the next element
if (nextCat.name.equals("Lionel Messi")) {
catIterator.remove();// Delete the cat with the specified name
}
}
System.out.println(cats);
الإخراج: [Cat{name='Thomas'}, Cat{name='Behemoth'}, Cat{name='Fluffy'}] ربما لاحظت أننا لم نحدد الفهرس أو الاسم في remove()
طريقة المكرر ! المكرِّر أكثر ذكاءً مما قد يبدو: فهو remove()
يزيل العنصر الأخير الذي أرجعه المكرِّر. كما ترون، فقد فعل ما أردناه أن يفعله :) من حيث المبدأ، هذا هو كل ما تحتاج إلى معرفته حول إزالة العناصر من ملف ArrayList
. حسنا، كل شيء تقريبا. في الدرس التالي، سننظر داخل هذا الفصل، ونرى ما يحدث هناك أثناء استدعاءات الأساليب المختلفة :) حتى ذلك الحين!
GO TO FULL VERSION