Привет! В прошлой лекции мы познакомились с классом
ArrayList, а также научились совершать наиболее распространенные операции с ним.
Коротко: удалить элемент из
ArrayList проще всего методом
remove(): он умеет удалять и по индексу (
remove(1)), и по ссылке на объект (
remove(cat)), и сам сдвигает остальные элементы, не оставляя «дыр». Но удалять элементы
во время перебора списка в цикле for-each нельзя (будет
ConcurrentModificationException): для этого используют итератор и его метод
remove().
Кроме того, мы выделили достаточно много отличий ArrayList от обычного массива.
Кратко
- В обычном массиве при удалении остается «дыра» (обнуленная ячейка), которую нужно вручную закрывать сдвигом.
- В
ArrayList есть метод remove(): он удаляет элемент и сам сдвигает остальные, без «дыр».
remove(index) удаляет по номеру, remove(object) по ссылке на объект.
- Нельзя одновременно перебирать коллекцию и изменять ее (удалять или добавлять) в цикле for-each: будет ошибка
ConcurrentModificationException.
- Чтобы безопасно удалять во время перебора, используют
Iterator и его метод remove().
- У итератора 3 метода:
hasNext(), next(), remove().
Удаление в обычном массиве
Теперь рассмотрим удаление элемента из списка 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 классов, с которыми мы захотим использовать массивы, нам придется в каждом из них писать такой же метод с точно такой же логикой. Это вообще провал -_-
Метод remove() в ArrayList
Но в классе 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='Пушок'}]
Это может быть очень удобно, если не хочется всегда держать в голове индекс нужного объекта.
Соберем способы удаления из
ArrayList в таблицу:
| Способ | Что делает | Когда использовать |
remove(index) | удаляет элемент по индексу | когда известен номер элемента |
remove(object) | удаляет по ссылке на объект | когда известен сам объект, а не его индекс |
Iterator.remove() | безопасно удаляет во время перебора | при удалении внутри цикла по коллекции |
Удаление во время перебора: ConcurrentModificationException
С обычным удалением вроде разобрались. Теперь давай представим такую ситуацию: мы хотим перебрать наш список элементов и удалить кота
с определенным именем.
Используем для этого специальный оператор цикла
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. Она возникает именно тогда, когда мы пытаемся нарушить правило и изменить список во время итерации по нему.
Итератор (Iterator) для безопасного удаления
В 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. Точнее, почти все.
В следующей лекции мы заглянем во "внутренности" этого класса, и посмотрим, что же там происходит во время совершения операций. Это подробно разобрано в статье
Работа ArrayList в картинках. Подробнее об итераторе можно почитать и в
документации Oracle (Interface Iterator).
До встречи!
Читайте также
Вопросы и ответы
Как удалить элемент из ArrayList?
Методом
remove():
remove(index) удаляет по индексу,
remove(object) по ссылке на объект. В отличие от массива, ArrayList сам сдвигает остальные элементы и не оставляет «дыр».
Чем удаление из ArrayList лучше, чем из массива?
В массиве можно только обнулить ячейку (остается «дыра») и вручную сдвигать элементы. ArrayList делает это автоматически методом
remove().
Что такое ConcurrentModificationException?
Это ошибка, которая возникает, если менять коллекцию (удалять или добавлять элементы) во время ее перебора в цикле for-each. Одновременно итерировать и изменять коллекцию нельзя.
Как удалить элемент во время перебора списка?
Через
Iterator: получить итератор методом
iterator(), перебирать список через
hasNext() и
next(), а удалять текущий элемент методом итератора
remove().
Что такое Iterator в Java?
Это объект для безопасного прохода по коллекции. У него три метода:
hasNext() (есть ли следующий элемент),
next() (вернуть следующий),
remove() (удалить последний возвращенный элемент).
Чем remove() у ArrayList отличается от remove() у итератора?
ArrayList.remove() удаляет по индексу или объекту, но не работает во время перебора в for-each.
Iterator.remove() безопасно удаляет последний возвращенный методом
next() элемент прямо во время итерации.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ