Abstract: LinkedHashSet implements the SequencedCollection interface, allowing us to addLast(), addFirst() and even reverse the collection. This sometimes causes some surprises.
Welcome to the 324th edition of The Java(tm) Specialists' Newsletter. May is always a busy month for training courses and conferences. I had to pick and choose which countries I would visit, so here is the list: Germany (JAX), England (Devoxx UK), Bulgaria (jPrime), and Poland (Geecon). Please do say "hi" if you see me at any of these events.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
A few weeks ago, I was solving an exercise I had written for my Java Concurrency Teardown Course on LinkedBlockingQueue and Deque. Since the LinkedBlockingDeque is also a SequencedCollection, I included a short description of the new Java 21 interface in the course, together with an exercise. I thought I discovered a bug in the LinkedHashSet, but after some reflection, I realized it was a feature instead.
Let's first take an ArrayList, add some elements, reverse it, then addFirst() and addLast(), and print out the original list. For example:
public class SequencedArrayListDemo { public static void main(String... args) { var list = new ArrayList<String>(); Collections.addAll(list, "one", "two", "three"); var reversed = list.reversed(); reversed.addFirst("first"); reversed.addLast("last"); System.out.println("list = " + list); } }
The output is, as expected, list = [last, one, two, three, first]
.
The reversed() method returns a view on the list that is reversed, but it is
still the underlying ArrayList. Thus when we call addFirst() on the reversed,
it's the same as calling addLast() on the ArrayList.
So far, so good.
Let's up the ante, by using add()
on both the list and the
reversed view:
public class SequencedArrayListMysteryDemo { public static void main(String... args) { var list = new ArrayList<String>(); Collections.addAll(list, "one", "two", "three"); var reversed = list.reversed(); list.add("list.add()"); reversed.add("reversed.add()"); System.out.println("list = " + list); } }
The add() method on the ArrayList adds at the end, and not surprisingly, the
add() method on the reversed view, adds at the end of the view, thus the
front of the ArrayList: list = [reversed.add(), one, two, three, list.add()]
.
Let's do the same with the LinkedHashSet:
public class SequencedLinkedHashSetDemo { public static void main(String... args) { var set = new LinkedHashSet<String>(); Collections.addAll(set, "one", "two", "three"); var reversed = set.reversed(); reversed.addFirst("first"); reversed.addLast("last"); System.out.println("set = " + set); } }
The result is the same as with the ArrayList: set = [last, one, two, three, first]
. And why wouldn't it be?
However, now we get to the mystery:
public class SequencedLinkedHashSetMysteryDemo { public static void main(String... args) { var set = new LinkedHashSet<String>(); Collections.addAll(set, "one", "two", "three"); var reversed = set.reversed(); set.add("set.add()"); reversed.add("reversed.add()"); System.out.println("set = " + set); } }
This time, the answer is surprising: set = [one, two, three, set.add(), reversed.add()]
.
Why is reversed.add() adding the value at the end of the LinkedHashSet, instead of
the beginning like in our
previous demo?
Before reporting a bug in the JDK, I usually try fix it first, and then report it. I was a bit nervous about this one, since the SequencedCollections were added by Stuart Marks and he is incredibly fastidious. Thus the behaviour I was seeing was probably as intended. My idea was to simply let add() call addLast(). However, there is a fundamental difference between the two methods. The plain add() will only add the item if it is not in the set already, returning true if the set was changed. addLast(), on the other hand, will always add it last, even if it was in the set already. For example:
public class LinkedHashSetAddVsAddLast { public static void main(String... args) { var set = new LinkedHashSet<String>(); set.add("A"); System.out.println(set); // [A] set.add("B"); System.out.println(set); // [A, B] set.add("C"); System.out.println(set); // [A, B, C] set.add("A"); // no change to set System.out.println(set); // [A, B, C] set.addLast("A"); System.out.println(set); // [B, C, A] } }
And as I suspected, this is indeed the intended behaviour. When we add() to a reversed LinkedHashSet, it will add the item to the underlying set using its submission rules, which will typically be in insertion order. We can override that order with addLast() and addFirst().
That's it from me for today. Enjoy labour day tomorrow - we are going for lunch in a remote Cretan village with friends. It will be amazing, with incredible food and even better company. Looking forward to it.
Kind regards
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.