As you know, ConcurrentModificationException has nothing to do with multithreading. This crap arises when we try to modify the collection while iterating over it. As usual, this has historical roots: collections and iterators appeared in Java 1.2, in those days there was no way to avoid using the iterator explicitly when traversing the collection, so the proposal to change the collection using iterator methods did not look absolutely terrible:
Iterator iterator = collection.iterator(); while (iterator.hasNext()) { Object element = iterator.next(); if (iDontLikeThisElement(element)) { iterator.remove(); } }
Not yet looked. But there were no other options. Later in the fifth java, a foreach loop appears, and the use of iterators becomes mostly implicit:
for (E element : collection) { if (iDonLikeThisElement(element)) { collection.remove(element);
“Whatever you want! Use explicit iterators, dear customers, and do not protrude ”- probably the developers of Java platform, working on the top five, thought something like that.
')
In the sixth java package appears competition. Now you can do this:
Set<String> set = Collections.newSetFromMap(new ConcurrentHashMap<>());
And get a set that does not throw ConcurrentModificationException. But again, the happiness is not quite complete:
- Usually we do not need multithreading at all.
- Null is not supported as elements, neither keys, nor values. Oh well, to be honest.
- The order of the elements is not defined and can change - this is much worse. Those. if we run through the elements and do some calculation with loss of accuracy, then we may face unpleasant surprises and different results on the same data sets, which, say, is not always good. There are also tasks where it is desirable to preserve the original data order. Well, here such things also have a place to be:
set.add("aaa"); set.add("bbb"); for (String s : set) { set.add("ddd"); System.out.println(s); } Conclusionaaa bbb ddd
| set.add("aaa"); set.add("bbb"); for (String s : set) { set.add("ccc"); System.out.println(s); } Conclusionaaa bbb
|
So now we will make our own collection with a clear order. And so, what we want to get:
- Within one thread, you can add and remove items at any time without any exceptions. And of course for the constant time.
- You can store nulls if you suddenly want to.
- Items are bypassed in the order in which they were added.
All this is easily achieved with a slightly modified bidirectional list:
- Removing an element we will not reset the link to the next one, i.e. if the iterator is on this element, then it can go further.
- At the end of the list we place a fake element that turns into a real one when something is added to the list. Those. even having reached the end of the list, the iterator does not rest on null and can continue to work if a new item appears in the collection. Further in the code this fake element is called placeholder.
Look at the picture.

- In the beginning we have the elements A, B, C, D.
- Then elements C and D are deleted.
- A new element E is added.
It can be noted that if at the time of deletions we had an iterator pointing to element C, then moving further along the links it would get to the newly added element E. If there was no iterator, then nothing prevents the garbage collector from freeing the memory from the deleted elements.
Well, for constant access time, we obviously need a hashmap:
import java.util.AbstractSet; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; public class LinkedSet<E> extends AbstractSet<E> { private static class LinkedElement<E> { E value; boolean exists; LinkedElement<E> prev; LinkedElement<E> next; } private Map<E, LinkedElement<E>> map = new HashMap<>(); private LinkedElement<E> placeholder = new LinkedElement<>(); private LinkedElement<E> head = placeholder; @Override public boolean isEmpty() { return head == placeholder; } @Override public int size() { return map.size(); } @Override public boolean contains(Object o) { return map.containsKey(o); }
Add item:
@Override public boolean add(E e) { LinkedElement<E> element = map.putIfAbsent(e, placeholder); if (element != null) { return false; } element = placeholder; element.exists = true; element.value = e; placeholder = new LinkedElement<>(); placeholder.prev = element; element.next = placeholder; return true; }
Uninstall:
@Override public boolean remove(Object o) { LinkedElement<E> removedElement = map.remove(o); if (removedElement == null) { return false; } removeElementFromLinkedList(removedElement); return true; } private void removeElementFromLinkedList(LinkedElement<E> element) { element.exists = false; element.value = null; element.next.prev = element.prev; if (element.prev != null) { element.prev.next = element.next; element.prev = null; } else { head = element.next; } }
Iterator:
@Override public Iterator<E> iterator() { return new ElementIterator(); } private class ElementIterator implements Iterator<E> { LinkedElement<E> next = head; LinkedElement<E> current = null; LinkedElement<E> findNext() { LinkedElement<E> n = next; while (!n.exists && n.next != null) { next = n = n.next; } return n; } @Override public boolean hasNext() { return findNext().exists; } @Override public E next() { LinkedElement<E> n = findNext(); if (!n.exists) { throw new NoSuchElementException(); } current = n; next = n.next; return n.value; } @Override public void remove() { if (current == null) { throw new IllegalStateException(); } if (map.remove(current.value, current)) { removeElementFromLinkedList(current); } else { throw new NoSuchElementException(); } } }
Now you can do this:
Set<Integer> set = new LinkedSet<>();
It is clear that LinkedMap can be constructed in the same way. But in general, that's all, another bike is ready. Why did not Link LinkedHashMap and LinkedHashSet library work in this way? Who knows, it is possible for the javists to envy the javascriptists.