Вітання! Минулої лекції ми познайомабося з класом 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("Томас");
cats[1] = new Cat("Бегемот");
cats[2] = new Cat("Філіп Маркович");
cats[1] = null;
System.out.println(Arrays.toString(cats));
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
Висновок:
[Cat{name='Томас'}, null, Cat{name='Фабопп Маркович'}]
Але при обнуленні в масиві залишається "дірка". Адже ми видаляємо не комірку, а тільки її вміст. Уяви, що буде, якщо у нас масив з 50 котів, 17 з яких ми видалабо таким способом. У нас буде масив з 17-ма дірками, і йди слідкуй за ними! Пам'ятати пам'ять номера порожніх осередків, куди можна записувати нові значення - неможливо. Один раз помабошся — і перезапишеш осередок із потрібним посиланням на об'єкт. Є, звичайно, можливість зробити трохи акуратніше: після видалення зрушити елементи масиву до початку, так, щоб "дірка" виявилася наприкінці:
public static void main(String[] args) {
Cat[] cats = new Cat[4];
cats[0] = new Cat("Томас");
cats[1] = new Cat("Бегемот");
cats[2] = new Cat("Філіп Маркович");
cats[3] = new Cat("Пушок");
cats[1] = null;
for (int i = 2; i < cats.length-1; i++) {
//переміщаємо елементи до початку, щоб порожній осередок опинився в кінці
cats[i-1] = cats[i];
cats[i] = null;
}
System.out.println(Arrays.toString(cats));
}
Висновок:
[Cat{name='Томас'}, Cat{name='Фабопп Маркович'}, Cat{name='Пушок'}, null]
Тепер ніби виглядає краще, але це навряд чи можна назвати стабільним рішенням. Як мінімум тому, що нам доведеться щоразу писати цей код руками, коли ми видалятимемо елемент з масиву! Поганий варіант. Можна було б піти іншим шляхом і створити окремий метод:
public void deleteCat(Cat[] cats, int indexToDelete) {
//... видаляємо кота за індексом і зрушуємо елементи
}
Але від цього користі теж мало: цей метод вміє працювати тільки з об'єктами Cat
, а з іншими не вміє. Тобто якщо в програмі буде ще 100 класів, з якими ми захочемо використовувати масиви, нам доведеться в кожному з них писати такий же метод із такою самою логікою. Це взагалі провал -_- Але в класі ArrayList цю проблему успішно вирішено! У ньому реалізовано спеціальний метод для видалення елементів remove()
.
public static void main(String[] args) {
ArrayList<Cat> cats = new ArrayList<>();
Cat thomas = new Cat("Томас");
Cat behemoth = new Cat("Бегемот");
Cat philipp = new Cat("Філіп Маркович");
Cat pushok = new Cat("Пушок");
cats.add(thomas);
cats.add(behemoth);
cats.add(philipp);
cats.add(pushok);
System.out.println(cats.toString());
cats.remove(1);
System.out.println(cats.toString());
}
Ми передали метод індекс нашого об'єкта, і він був видалений (так само як у масиві). Метод remove()
має дві особливості. По-перше , він не залишає "дір". У ньому вже реалізовано логіку зсуву елементів при видаленні елемента з середини, яку ми раніше писали руками. Подивися висновок попереднього коду в консоль:
[Cat{name='Томас'}, Cat{name='Бегемот'}, Cat{name='Фабопп Маркович'}, Cat{name='Пушок'}]
[Cat{name='Томас'}, Cat{name='Фабопп Маркович'}, Cat{name='Пушок'}]
Ми видалабо з середини одного кота, і інші були пересунуті так, щоб не залишалося прогалин. По-друге , він може видаляти об'єкт не тільки за індексом (як звичайний масив), але і за посиланням на об'єкт :
public static void main(String[] args) {
ArrayList<Cat> cats = new ArrayList<>();
Cat thomas = new Cat("Томас");
Cat behemoth = new Cat("Бегемот");
Cat philipp = new Cat("Філіп Маркович");
Cat pushok = new Cat("Пушок");
cats.add(thomas);
cats.add(behemoth);
cats.add(philipp);
cats.add(pushok);
System.out.println(cats.toString());
cats.remove(philipp);
System.out.println(cats.toString());
}
Висновок:
[Cat{name='Томас'}, Cat{name='Бегемот'}, Cat{name='Фабопп Маркович'}, Cat{name='Пушок'}]
[Cat{name='Томас'}, Cat{name='Бегемот'}, Cat{name='Пушок'}]
Це може бути дуже зручно, якщо не хочеться завжди пам'ятати індекс потрібного об'єкта. Зі звичайним видаленням начебто розібралися. Тепер дамо уявимо таку ситуацію: ми хочемо перебрати наш список елементів та видалити кота з певним ім'ям. Використовуємо для цього спеціальний оператор циклу for
- for each
. З ним можна ознайомитися докладніше у цій лекції .
public static void main(String[] args) {
ArrayList<Cat> cats = new ArrayList<>();
Cat thomas = new Cat("Томас");
Cat behemoth = new Cat("Бегемот");
Cat philipp = new Cat("Філіп Маркович");
Cat pushok = new Cat("Пушок");
cats.add(thomas);
cats.add(behemoth);
cats.add(philipp);
cats.add(pushok);
for (Cat cat: cats) {
if (cat.name.equals("Бегемот")) {
cats.remove(cat);
}
}
System.out.println(cats);
}
Начебто код виглядає цілком логічно. Однак результат може тебе дуже здивувати:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
at java.util.ArrayList$Itr.next(ArrayList.java:831)
at Cat.main(Cat.java:25)
Якась помилка, причому незрозуміло, з чого раптом вона виникла. У цьому вся процесі є низка нюансів, із якими треба розібратися. Загальне правило, яке потрібно запам'ятати: Не можна проводити одночасно ітерацію (перебір) колекції та зміну її елементів. Так-так, саме зміна, а не лише видалення. Якщо ти спробуєш у нашому коді замінити видалення кота на вставку нових, результат буде той самий:
for (Cat cat: cats) {
cats.add(new Cat("Сейлем Сейберхеген"));
}
System.out.println(cats);
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
at java.util.ArrayList$Itr.next(ArrayList.java:831)
at Cat.main(Cat.java:25)
Ми змінабо одну операцію на іншу, але результат не змінився: та сама помилка ConcurrentModificationException
. Вона виникає саме тоді, коли ми намагаємося порушити правило та змінити список під час ітерації щодо нього. У Java для видалення елементів під час перебору необхідно використовувати особливий об'єкт — ітератор (клас Iterator
). Клас Iterator
відповідає за безпечний прохід за списком елементів. Він досить простий, оскільки має всього 3 методи:
hasNext()
— повертаєtrue
чиfalse
залежно від того, чи є у списку наступний елемент, чи ми вже дійшли до останнього.next()
- Повертає наступний елемент спискуremove()
- Видаляє елемент зі списку
Iterator<Cat> catIterator = cats.iterator();//Створюємо ітератор
while(catIterator.hasNext()) {//До тих пір, поки в списку є елементи
Cat nextCat = catIterator.next();//отримуємо наступний елемент
System.out.println(nextCat);//виводимо його в консоль
}
Висновок:
Cat{name='Томас'}
Cat{name='Бегемот'}
Cat{name='Фабопп Маркович'}
Cat{name='Пушок'}
Як бачиш, у класі ArrayList
вже реалізовано спеціальний метод для створення ітератора iterator()
. Крім того, зверніть увагу, що при створенні ітератора ми вказуємо клас об'єктів, з якими він повинен працювати ( <Cat>
). Зрештою, ми легко вирішуємо наше споконвічне завдання за допомогою ітератора. Наприклад, видалимо кота з ім'ям "Філіп Маркович":
Iterator<Cat> catIterator = cats.iterator();//Створюємо ітератор
while(catIterator.hasNext()) {//До тих пір, поки в списку є елементи
Cat nextCat = catIterator.next();//отримуємо наступний елемент
if (nextCat.name.equals("Філіп Маркович")) {
catIterator.remove();//видаляємо кота з потрібним ім'ям
}
}
System.out.println(cats);
Висновок:
[Cat{name='Томас'}, Cat{name='Бегемот'}, Cat{name='Пушок'}]
Можливо, ти помітив, що ми не вказували ні індекс елемента, ні ім'я змінної-посилання в методі ітератора remove()
! Ітератор розумніший, ніж може здатися: метод remove()
видаляє останній елемент, який повернуто ітератором. Як бачиш, він спрацював саме так, як потрібно :) Ось в принципі все, що тобі потрібно знати про видалення елементів з ArrayList
. Точніше – майже все. У наступній лекції ми заглянемо у “нутрощі” цього класу, і подивимося, що там відбувається під час здійснення операцій :) До зустрічі!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ