Search code examples
javacollectionsiteratorconcurrentmodification

Removing items from a list while iterating over it


I've seen many questions regarding this topics but none of the answers that really quite my usecase (Unless I've interpreted it wrong.). Is it possible to remove an item from a list while an iterator is iterating over it?

What I'm trying to achieve is having a queue with an audioplayer. The iterator iterates over the queue and blocks while playing the song. Songs can be added to or deleted from the queue while it is already playing.

I've tried the above idea and received an exception ConcurrentModificationException . I've also read that it's a bad practice to mutate a collection while an iterator is iterating over it. I'm hoping someone can point me into the right direction on how to properly mutate a list from a method call while iterating over it.


Solution

  • The best solution is not to maintain the currently playing song resp. the next song to play via an Iterator. Instead, you can create a specialized list which knows how to adapt this pointer on modifications.

    Such a class could look like

    class SongList extends AbstractList<Song> implements RandomAccess {
        final List<Song> backend = new ArrayList<>();
        int currentSong = -1;
    
        SongList() {}
        SongList(Collection<? extends Song> c) {
            backend.addAll(c);
        }
        // mandatory query methods
    
        @Override public int size() {
            return backend.size();
        }
        @Override public Song get(int index) {
            return backend.get(index);
        }
    
        // the "iterator"
        public Song nextSong() {
            if(++currentSong < size()) {
                return get(currentSong);
            }
            currentSong = -1;
            return null;
        }
    
        // modifying methods, which will adapt the pointer
    
        @Override public void add(int index, Song element) {
            backend.add(index, element);
            if(index <= currentSong) currentSong++;
        }
        @Override public Song remove(int index) {
            final Song removed = backend.remove(index);
            if(index <= currentSong) currentSong--;
            return removed;
        }
    
        @Override
        public boolean addAll(int index, Collection<? extends Song> c) {
            int old = size();
            backend.addAll(index, c);
            if(index <= currentSong) currentSong += size() - old;
            return true;
        }
    
        @Override protected void removeRange(int fromIndex, int toIndex) {
            backend.subList(fromIndex, toIndex).clear();
            if(fromIndex <= currentSong)
                currentSong = Math.max(fromIndex - 1, currentSong - toIndex + fromIndex);
        }
    
        // this will not change the pointer
    
        @Override public Song set(int index, Song element) {
            return backend.set(index, element);
        }
    
        // query methods overridden for performance
    
        @Override public boolean contains(Object o) {
            return backend.contains(o);
        }
        @Override public int indexOf(Object o) {
            return backend.indexOf(o);
        }
        @Override public Spliterator<Song> spliterator() {
            return backend.spliterator();
        }
        @Override public void forEach(Consumer<? super Song> action) {
            backend.forEach(action);
        }
        @Override public Object[] toArray() {
            return backend.toArray();
        }
        @Override public <T> T[] toArray(T[] a) {
            return backend.toArray(a);
        }
        @Override public String toString() {
            return backend.toString();
        }
    }
    

    AbstractList is specifically designed to provide the collection operations atop a few methods, so we only need to implement size() and get(int) to have a readable list and by providing add(int, Song), remove(int), and set(int, Song) we did already everything needed to support all modification operations. The other methods are only provided to improve the performance, the inherited methods would also work.

    The list supports a single pointer to a current play position, which can be iterated via nextSong(). When reaching the end, it will return null and reset the pointer, so that the next query will start again. The add and remove methods will adapt the pointer such that an already played song won’t be played again (unless restarting the entire list).

    set based modifications do not adapt the pointer, which implies that nothing meaningful will happen when you sort the list, some policies are imaginable, but at least when the list has duplicates, no perfect behavior exists. When comparing with other player software, no-one seems to expect perfect behavior when the list is turned up-side-down while playing. At least, there will never be an exception.