sets
HashSet (a HashMap in disguise), TreeSet, and EnumSet (a bit vector).
- chapter
- chapter 06.07
- reading time
- 6 min read
a set is a map that only cares about keys. so java builds them out of maps: a HashSet is a HashMap whose values are all one dummy object, LinkedHashSet is a LinkedHashMap, TreeSet is a TreeMap.
the odd one out is EnumSet: a bit vector. one long, one bit per enum constant.
the picture
you know how a set's whole job is "no duplicates"? a map already does that for its keys. so instead of writing a second hash table, the JDK authors wrapped the first one.
HashSet opened up. one field, a HashMap. your elements are its keys, and every value points at the same PRESENT object. there's exactly one of those in the whole JVM.that's not a simplification, it's the source. HashSet.java in jdk 25 has static final Object PRESENT = new Object(), and add(e) is literally map.put(e, PRESENT) == null. contains is containsKey, remove is the map's remove, and iterating the set iterates the map's key set.
which means everything from the HashMap chapter applies, word for word: hashing, buckets, the 0.75 resize, treeified buckets, the equals/hashCode contract, and the not-quite-random iteration order. the price of the shortcut is memory: each element costs a full map entry (32 bytes on a typical 64-bit JVM) even though the value is always the same.
three sets, three orders
HashSet order here is computed with the same model as the HashMap lab, so it matches what java prints. add Apple to see compareTo put capitals first. add a word twice and add returns false.void main() {
List<String> words = List.of("pear", "apple", "fig", "banana", "kiwi", "apple");
Set<String> hash = new HashSet<>(words);
Set<String> linked = new LinkedHashSet<>(words);
Set<String> tree = new TreeSet<>(words);
IO.println(hash);
IO.println(linked);
IO.println(tree);
IO.println(hash.add("fig") + " " + hash.size());
}[banana, apple, pear, kiwi, fig] [pear, apple, fig, banana, kiwi] [apple, banana, fig, kiwi, pear] false 5
HashSet | LinkedHashSet | TreeSet | |
|---|---|---|---|
| built on | HashMap | LinkedHashMap | TreeMap |
| order | none to rely on | insertion | sorted |
add / contains / remove | O(1) average | O(1) average | O(log n) |
| extras | getFirst, getLast, reversed (sequenced) | first, floor, ceiling, headSet, tailSet... | |
| decides "duplicate" with | hashCode + equals | hashCode + equals | compareTo / Comparator |
that last row matters more than it looks.
class P {
final int x;
P(int x) { this.x = x; }
}
record Q(int x) {}
void main() {
Set<P> ps = new HashSet<>(List.of(new P(1), new P(1)));
Set<Q> qs = new HashSet<>(List.of(new Q(1), new Q(1)));
IO.println(ps.size() + " " + qs.size());
}set algebra
the bulk methods from Collection are the classic set operations. they modify the set you call them on, so copy first if you need the original:
void main() {
Set<String> a = new TreeSet<>(List.of("x", "y", "z"));
Set<String> b = Set.of("y", "z", "w");
Set<String> union = new TreeSet<>(a); union.addAll(b);
Set<String> inter = new TreeSet<>(a); inter.retainAll(b);
Set<String> diff = new TreeSet<>(a); diff.removeAll(b);
IO.println(union + " " + inter + " " + diff);
}[w, x, y, z] [y, z] [x]
addAll is union, retainAll is intersection, removeAll is difference, and containsAll is "is a subset of". (Set.of gives you an immutable set: no nulls, no duplicates allowed even at creation. more in immutable collections.)
EnumSet: a set that's a number
when the elements are constants of one enum, java can do something much better than hashing. every enum constant has an ordinal(), its position (0, 1, 2...). so a set of them is just "which positions are in", which is a row of bits.
Day enum with seven constants. toggle days and watch the bits in elements flip. the factory methods are single bit-twiddling expressions, copied from RegularEnumSet.java.for an enum with up to 64 constants, EnumSet is a RegularEnumSet with one long field. add is an OR, remove is an AND-NOT, contains is an AND, size() is Long.bitCount, and addAll between two enum sets is a single |. no hashing, no boxing, no per-element objects, and iteration is in ordinal order (declaration order). enums with more than 64 constants get a JumboEnumSet, an array of longs, same idea.
import static java.time.DayOfWeek.*;
void main() {
Set<DayOfWeek> weekend = EnumSet.of(SATURDAY, SUNDAY);
Set<DayOfWeek> workdays = EnumSet.complementOf(EnumSet.copyOf(weekend));
IO.println(weekend + " " + workdays.size() + " " + workdays.contains(FRIDAY));
IO.println(EnumSet.range(TUESDAY, THURSDAY));
}[SATURDAY, SUNDAY] 5 true [TUESDAY, WEDNESDAY, THURSDAY]
its sibling EnumMap is the same trick for maps: an array indexed by ordinal, so get is an array read.
HashSet=HashMapwith dummy values.TreeSet=TreeMap.LinkedHashSet=LinkedHashMap.- hash sets decide duplicates with
equals+hashCode, tree sets withcompareTo. addAll/retainAll/removeAllare union / intersection / difference.- for enum elements,
EnumSet: onelong, one bit per constant.