🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!
В этой статье мы разберёмся, какие есть способы удаления элементов из ArrayList
в Java и в чём между ними разница.
Удаление элементов из обычного массива в Java — не самый удобный процесс. Поскольку сам элемент удалить нельзя, остаётся только “обнулить” его значение — то есть присвоить ему null
:
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 + '\'' +
'}';
}
}
Run CodeЧто получим в результате:
[Cat{name='Thomas'}, null, Cat{name='Lionel Messi'}]
Однако установка значения null
лишь очищает содержимое ячейки, но не удаляет саму позицию в массиве. По сути, остаётся “дыра”.
Представьте массив из 50 объектов типа Cat, из которого «удалили» 17 элементов, присвоив им null
. В результате остаётся 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];
}
System.out.println(Arrays.toString(cats));
}
Run CodeРезультат:
[Cat{name='Thomas'}, Cat{name='Fluffy'}, Cat{name='Fluffy'}, null]
Такое решение выглядит несколько лучше, однако назвать его надежным нельзя. Хотя бы потому, что при каждом удалении элемента из массива требуется вручную прописывать одинаковую последовательность действий.
Это плохой вариант.
Гораздо лучше — реализовать отдельный метод:
public void deleteCat(Cat[] cats, int indexToDelete) {
}
Run CodeОднако и такой метод малоэффективен: он будет работать только с объектами класса Cat
, и не подойдёт для других типов.
То есть если в программе есть ещё сотня других классов, с которыми мы хотим работать через массивы, придётся в каждом из них писать точно такой же метод с одинаковой логикой.
Эту задачу успешно решает класс 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());
}
Run CodeМы передаем индекс объекта в метод, и он его удаляет — всё как с массивом.
Метод 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());
}
Run CodeВывод:
[Cat{name='Thomas'}, Cat{name='Behemoth'}, Cat{name='Lionel Messi'}, Cat{name='Fluffy'}]
[Cat{name='Thomas'}, 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);
}
Run CodeКод кажется совершенно логичным. Но результат может удивить:
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("Salem Saberhagen"));
}
System.out.println(cats);
Run CodeException 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
отвечает за безопасную итерацию по списку элементов.
Это довольно просто, потому что у него всего три метода:
hasNext()
— возвращает true или false, в зависимости от того, есть ли ещё элементы в списке или мы уже дошли до конца.next()
— возвращает следующий элемент в списке.remove()
— удаляет элемент из списка.
Как видите, итератор как раз то, что нам нужно, и при этом он довольно прост в использовании.
Допустим, нам нужно проверить, есть ли следующий элемент в списке, и показать его, если он есть:
Iterator<Cat> catIterator = cats.iterator();
while(catIterator.hasNext()) {
Cat nextCat = catIterator.next();
System.out.println(nextCat);
}
Run CodeРезультат:
Cat{name='Thomas'}
Cat{name='Behemoth'}
Cat{name='Lionel Messi'}
Cat{name='Fluffy'}
Как видите, в ArrayList
уже есть специальный метод для создания итератора: iterator()
.
Также стоит заметить, что при создании итератора мы указываем класс объектов, с которыми он будет работать ( <Cat>
).
Итог: итератор идеально справляется с поставленной задачей.
Например, удалим кота по имени “Lionel Messi”:
Iterator<Cat> catIterator = cats.iterator();
while(catIterator.hasNext()) {
Cat nextCat = catIterator.next();
if (nextCat.name.equals("Lionel Messi")) {
catIterator.remove();
}
}
System.out.println(cats);
Run CodeВывод:
[Cat{name='Thomas'}, Cat{name='Behemoth'}, Cat{name='Fluffy'}]
Вы, возможно, заметили, что мы не указывали ни индекс, ни имя в методе remove()
итератора.
Итератор гораздо умнее, чем может показаться: remove()
удаляет последний элемент, который был возвращён итератором. Как видите, он сделал именно то, что мы от него хотели.
В принципе, это всё, что нужно знать об удалении элементов из ArrayList
. Ну, почти всё.
Перевод статьи «How to remove an element from ArrayList in Java?».