Hi! In the last lesson, we got acquainted with the
ArrayList
class, and learned how to perform the most common operations with this class.
In addition, we pointed out several differences between an ArrayList
and an ordinary array.
But we skirted one topic, namely, how to delete elements from an ArrayList
.
We'll discuss that now.
We've already mentioned that deleting elements from an ordinary array is not very convenient.
Since we can't delete the element itself, we can only "zero out" (set to null) its value:
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 + '\'' +
'}';
}
}
Output:
[Cat{name='Thomas'}, null, Cat{name='Lionel Messi'}]
But setting an array element to null leaves a "hole". We haven't removed the position in the array, only its contents. Imagine what would happen if we had an array of 50 cats and removed 17 of them this way. We'll have an array with 17 holes. Just try keeping track of them! It's unrealistic to expect to remember the number of empty cells where you can write new values. If you make one mistake, you'll overwrite an object reference that you want.
There is, of course, a way to do this a little more carefully: after removing an element, move the elements to the front of the array to put the "hole" at the end:
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];// Move the elements to the front of the array, so the empty position is at the end
}
System.out.println(Arrays.toString(cats));
}
Output:
[Cat{name='Thomas'}, Cat{name='Fluffy'}, Cat{name='Fluffy'}, null]
This seems better, but it can hardly be called a robust solution. If for no other reason than the fact that we have to write this code every time when we delete an element from an array!
This is a bad option.
We could go another way and create a separate method:
public void deleteCat(Cat[] cats, int indexToDelete) {
//...delete the cat corresponding to the index and move the elements
}
But this is also of little use: this method can only work with Cat
objects, but not other types.
In other words, if a program has another 100 classes that we want to use with arrays, we'll have to write the same method with exactly the same logic in each of them. This is a total disaster -_-
But the ArrayList
class solves this problem! It implements a special method for removing elements: 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());
}
We pass our object's index to the method, which deletes it (just like in an array).
The remove()
method has two special features.
First, it does not leave "holes". It already implements the logic needed to shift elements when an element is removed from the middle, which we previously wrote ourselves.
Look at the output from the previous code:
[Cat{name='Thomas'}, Cat{name='Behemoth'}, Cat{name='Lionel Messi'}, Cat{name='Fluffy'}]
[Cat{name='Thomas'}, Cat{name='Lionel Messi'}, Cat{name='Fluffy'}]
We removed one cat from the middle, and the rest were moved so that there were no empty spaces.
Second, it can delete objects not only by index (like a normal array), but also by reference:
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());
}
Output:
[Cat{name='Thomas'}, Cat{name='Behemoth'}, Cat{name='Lionel Messi'}, Cat{name='Fluffy'}]
[Cat{name='Thomas'}, Cat{name='Behemoth'}, Cat{name='Fluffy'}]
This can be very convenient if you don't want to always keep track of the desired object's index.
It seems we've figured out ordinary deletion.
Now let's imagine this situation: we want to iterate over our list and remove a cat with a specific name.
To do this, we'll use a fast for
loop (also called a for-each loop), which we were introduced to in Rishi's lessons:
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);
}
The code looks perfectly logical. But the result might be a big surprise:
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)
There's some sort of an error, and it's unclear why it occurred.
This process involves a number of nuances that must be addressed.
Here's the general rule you need to remember:
You cannot simultaneously iterate over a collection and change its elements.
And we mean any sort of change, not merely removal. If you replace the cat removal with an attempt to insert new cats, the result will be the same:
for (Cat cat: cats) {
cats.add(new Cat("Salem Saberhagen"));
}
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)
We changed one operation to another, but the result didn't change: we get the same ConcurrentModificationException.
It occurs precisely when we try breaking the rule above by changing the list while iterating over it.
In Java, we need a special object called an iterator (Iterator
class) to delete items while iterating over a collection. The Iterator
class is responsible for safely iterating over the list of elements.
It is quite simple, since it has only 3 methods:
hasNext()
- returns true or false, depending on whether there is a next item in the list, or we have already reached the last one.next()
- returns the next item in the listremove()
- removes an item from the list
Iterator<Cat> catIterator = cats.iterator();// Create an iterator
while(catIterator.hasNext()) {// As long as there are elements in the list
Cat nextCat = catIterator.next();// Get the next element
System.out.println(nextCat);// Display it
}
Output:
Cat{name='Thomas'}
Cat{name='Behemoth'}
Cat{name='Lionel Messi'}
Cat{name='Fluffy'}
As you can see, the ArrayList
has already implemented a special method for creating an iterator: iterator()
.
Additionally, note that when we create an iterator, we specify the class of objects that it will work with (<Cat>
).
The bottom line is that an iterator easily handles our original task.
For example, remove the cat named "Lionel Messi":
Iterator<Cat> catIterator = cats.iterator();// Create an iterator
while(catIterator.hasNext()) {// As long as there are elements in the list
Cat nextCat = catIterator.next();// Get the next element
if (nextCat.name.equals("Lionel Messi")) {
catIterator.remove();// Delete the cat with the specified name
}
}
System.out.println(cats);
Output:
[Cat{name='Thomas'}, Cat{name='Behemoth'}, Cat{name='Fluffy'}]
You may have noticed that we didn't specify either the index or the name in the iterator's remove()
method!
The iterator is smarter than it may appear: remove()
removes the last element returned by the iterator. As you can see, it did just what we wanted it to do :)
In principle, this is everything you need to know about removing elements from an ArrayList
. Well, almost everything.
In the next lesson, we'll look inside this class, and see what happens there during various method calls :)
Until then!
GO TO FULL VERSION