Привет! В прошлой лекции мы познакомились с классом 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];//перемещаем элементы к началу, чтобы пустая ячейка оказалась в конце
   }

   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='Пушок'}] Возможно ты заметил, что мы не указывали ни индекс элемента, ни имя переменной-ссылки в методе итератора remove()! Итератор умнее, чем может показаться: метод remove() удаляет последний элемент, который был возвращен итератором. Как видишь, он сработал именно так, как было нужно :) Вот в принципе все, что тебе нужно знать об удалении элементов из ArrayList. Точнее — почти все. В следующей лекции мы заглянем во “внутренности” этого класса, и посмотрим, что же там происходит во время совершения операций :) До встречи!