Set and HashSet
Keep unique values with HashSet, get fast contains checks, combine sets for permission logic and choose LinkedHashSet or TreeSet when order matters.
Set<String> seen = new HashSet<>();
boolean added = seen.add("[email protected]"); // false if already present
boolean present = seen.contains("x");
Set<String> ordered = new LinkedHashSet<>(list);
Set<String> sorted = new TreeSet<>(list);A Set holds each value at most once. Whether two values are the same is decided by equals and hashCode for HashSet, or by the comparator for TreeSet. Sets answer one question very quickly: is this value present?
HashSet
HashSet is backed by a HashMap whose keys are your elements. add, remove and contains take constant time on average, compared with a full scan for List.contains. add returns false when the element was already there, which gives you duplicate detection without a separate check.
Iteration order is unspecified. If you print a set, return it as JSON or compare output in a test, pick an implementation with a defined order:
LinkedHashSetkeeps insertion order. Building one from a list is a clean way to remove duplicates while keeping the first occurrence of each.TreeSetkeeps elements sorted and adds range methods such asfirst,ceilingandheadSet. Operations costO(log n).
Set operations
Union, intersection and difference are addAll, retainAll and removeAll. They modify the set you call them on, so copy first when the original must survive. containsAll checks a subset. Together they express permission rules plainly: a request is allowed when the granted scopes contain all required scopes, and the difference tells you which are missing.
Mistakes people make
- No equals and hashCode. A plain class inherits identity equality, so two objects with the same fields are both kept. Records, strings and enums behave as you expect.
- Mutating an element after adding it. As with map keys, changing a field used in
hashCodestrands the element:containsfails andremovedoes nothing. - A comparator that disagrees with equals. A
TreeSetbuilt withString.CASE_INSENSITIVE_ORDERtreats"Ada"and"ada"as the same element. That is useful for usernames, and surprising when you did not mean it. - Expecting the first element. A
HashSethas no first element. Use aLinkedHashSetorTreeSetif you need one.
Back-end angle
Sets fit role and scope checks, de-duplicating email addresses during a bulk import, recording idempotency keys already processed, and turning an allow list or deny list into fast lookups before filtering a large batch.
Example
When to use it
- Reject a bulk user import row whose email address was already seen earlier in the same file.
- Return 403 with the list of missing scopes when a token's granted scopes do not contain all required ones.
- Skip webhook deliveries whose idempotency key is already in the processed set.
More examples
Scope checks with set operations
Copying before removeAll and retainAll keeps the original sets intact. Listing the missing scopes makes a 403 response useful to the client developer.
void main() {
Set<String> granted = new TreeSet<>(List.of("orders:read", "orders:write", "users:read"));
Set<String> required = Set.of("orders:read", "orders:refund");
IO.println("allowed: " + granted.containsAll(required));
Set<String> missing = new TreeSet<>(required);
missing.removeAll(granted);
IO.println("missing: " + missing);
Set<String> common = new TreeSet<>(granted);
common.retainAll(required);
IO.println("common: " + common);
IO.println("granted unchanged: " + granted);
}
// Output:
// allowed: false
// missing: [orders:refund]
// common: [orders:read]
// granted unchanged: [orders:read, orders:write, users:read]Classes need equals and hashCode, records have them
Without equals and hashCode, two TagClass objects are only equal to themselves, so the set keeps both. A record generates both methods from its components.
class TagClass {
final String name;
TagClass(String name) {
this.name = name;
}
}
record Tag(String name) {}
void main() {
Set<TagClass> classes = new HashSet<>();
classes.add(new TagClass("java"));
classes.add(new TagClass("java"));
Set<Tag> records = new HashSet<>();
records.add(new Tag("java"));
records.add(new Tag("java"));
IO.println("class without equals: " + classes.size());
IO.println("record: " + records.size());
}
// Output:
// class without equals: 2
// record: 1Case-insensitive usernames with TreeSet
TreeSet uses the comparator, not equals, to decide what counts as a duplicate. The first spelling added is the one kept.
void main() {
Set<String> usernames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
IO.println("add Ada: " + usernames.add("Ada"));
IO.println("add ada: " + usernames.add("ada"));
IO.println("add Linus: " + usernames.add("Linus"));
IO.println(usernames);
IO.println("contains ADA: " + usernames.contains("ADA"));
}
// Output:
// add Ada: true
// add ada: false
// add Linus: true
// [Ada, Linus]
// contains ADA: true
Discussion