Пользователь Professor Hans Noodles
Professor Hans Noodles
41 уровень

Удаление элемента из списка ArrayList

Статья из группы Java Developer
Привет! В прошлой лекции мы познакомились с классом ArrayList, а также научились совершать наиболее распространенные операции с ним. Кроме того, мы выделили достаточно много отличий ArrayList от обычного массива. Но одну из тем мы обошли стороной, а именно — удаление элемента из списка ArrayList. Ее мы сейчас и рассмотрим. Удаление элемента из списка ArrayList - 1Мы уже говорили, что удаление элементов в обычном массиве делается не очень удобно. Поскольку мы не можем удалить саму ячейку, нам остается только “обнулить” ее значение:

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='Пушок'}]
Это может быть очень удобно, если не хочется всегда держать в голове индекс нужного объекта. С обычным удалением вроде разобрались. Теперь давай представим такую ситуацию: мы хотим перебрать наш список элементов и удалить кота с определенным именем. Используем для этого специальный оператор цикла forfor 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='Пушок'}]
Удаление элемента из списка ArrayList - 2Возможно ты заметил, что мы не указывали ни индекс элемента, ни имя переменной-ссылки в методе итератора remove()! Итератор умнее, чем может показаться: метод remove() удаляет последний элемент, который был возвращен итератором. Как видишь, он сработал именно так, как было нужно :) Вот в принципе все, что тебе нужно знать об удалении элементов из ArrayList. Точнее — почти все. В следующей лекции мы заглянем во “внутренности” этого класса, и посмотрим, что же там происходит во время совершения операций :) До встречи!
Комментарии (207)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
Альфия 35 уровень, Санкт-Петербург
24 марта 2021
Какой-то бред: дальше в лекциях будет сказано, что for each и есть сокращенная запись итератора, а значит оба перебора идентичны и насколько я понимаю, удалить элемент не получится ни там, ни там. Удалять и добавлять элемент можно с помощью еще одного интерфейса ListIterator. Поправьте, если я не права.
Павел Гарматюк 17 уровень, Севастополь
14 марта 2021
Formidabb1e 11 уровень
28 февраля 2021
"Нельзя проводить одновременно итерацию (перебор) коллекции и изменение ее элементов." Вообще можно, используя обычный for(int i = 0; i < strCollection.size(); i++) {strCollection.remove(1); i--;} Просто при добавлении объекта i++, а при удалении i--
Артем 11 уровень, Москва
18 февраля 2021
Блин, как это все запомнить?(((
Cerca Trova 17 уровень, Киев
13 февраля 2021
for (int i = 2; i < cats.length-1; i++) { //перемещаем элементы к началу, чтобы пустая ячейка оказалась в конце cats[i-1] = cats[i]; cats[i] = null; } В примере ошибка! Мы не переместим все элементы к началу! Нужно убрать у cats.length-1 этот самый -1.
Anna 17 уровень, Минск
13 февраля 2021
"Нельзя проводить одновременно итерацию (перебор) коллекции и изменение ее элементов. Да-да, именно изменение, а не только удаление. Если ты попытаешься в нашем коде заменить удаление кота на вставку новых, результат будет тот же" НО итератор же не решает эту проблему!!!!! у него есть только метод для удаления! А как делать вставку или изменение элементов Set в переборе???????????
sasha 8 уровень
21 декабря 2020
Но почему выводится [ArrayList.Cat@1b6d3586, ArrayList.Cat@4554617c, ArrayList.Cat@74a14482, ArrayList.Cat@1540e19d].. И как вывести нормально? что то я пропустил. Извиняюсь)
Юрий 14 уровень, Ставрополь
7 декабря 2020
cats.removeIf(s -> s.name.equals("Филипп Маркович")); Метод removeIf () ArrayList используется для удаления всех элементов этого ArrayList, который удовлетворяет заданному фильтру предикатов, который передается в качестве параметра в метод. Ссылка
Станислав Бынеев 28 уровень, Москва
26 ноября 2020
Ставь власс, если Витя Телеграм уже запарил
Skanta 22 уровень, Ростов-на-Дону
11 ноября 2020
Для тех, кому лень листать комменты: в примере с "переставлением null" ошибка. Если тоже заметили, то молодцы! З.ы.: хорошим "звоночком" будет, если начинаете задавать вопрос "А зачем он уменьшил размер массива на -1?!"