skip to content

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.

HashSetmapHashMapentries: key → value"pear""apple""fig""kiwi"ObjectPRESENTstatic final, onefor every HashSet
a 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

add =
added so far: pear, apple, fig, banana, kiwi
HashSetbucket order. looks random, isn't, can change after a resize.
0banana1apple2pear3kiwi4fig
LinkedHashSetinsertion order, kept by a linked list.
0pear1apple2fig3banana4kiwi
TreeSetsorted by compareTo. capitals sort before lowercase.
0apple1banana2fig3kiwi4pear
add words and watch where they land in each set. the 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.
Main.java
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());
}
output
[banana, apple, pear, kiwi, fig]
[pear, apple, fig, banana, kiwi]
[apple, banana, fig, kiwi, pear]
false 5
HashSetLinkedHashSetTreeSet
built onHashMapLinkedHashMapTreeMap
ordernone to rely oninsertionsorted
add / contains / removeO(1) averageO(1) averageO(log n)
extrasgetFirst, getLast, reversed (sequenced)first, floor, ceiling, headSet, tailSet...
decides "duplicate" withhashCode + equalshashCode + equalscompareTo / Comparator

that last row matters more than it looks.

predict the output
java
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:

Main.java
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);
}
output
[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.

days =
EnumSet.
long elements (bit 63 … bit 0, the top 48 are always 0 for a 7-constant enum)
0…0
15
14
13
12
11
10
9
8
7
SUN
6
SAT
5
FRI
4
THU
3
WED
2
TUE
1
MON
0
elements = 34set = [TUE, SAT]contains(FRI) = (elements & 1L << 4) != 0 → false
what EnumSet just did
EnumSet<Day> days = EnumSet.of(TUE, SAT); two bits set: 1 and 5. elements = 2 + 32 = 34.
a 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.

Main.java
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));
}
output
[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.

keylines
  • HashSet = HashMap with dummy values. TreeSet = TreeMap. LinkedHashSet = LinkedHashMap.
  • hash sets decide duplicates with equals + hashCode, tree sets with compareTo.
  • addAll / retainAll / removeAll are union / intersection / difference.
  • for enum elements, EnumSet: one long, one bit per constant.