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.

SyntaxSet<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:

  • LinkedHashSet keeps insertion order. Building one from a list is a clean way to remove duplicates while keeping the first occurrence of each.
  • TreeSet keeps elements sorted and adds range methods such as first, ceiling and headSet. Operations cost O(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 hashCode strands the element: contains fails and remove does nothing.
  • A comparator that disagrees with equals. A TreeSet built with String.CASE_INSENSITIVE_ORDER treats "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 HashSet has no first element. Use a LinkedHashSet or TreeSet if 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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.

Example · java
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: 1

Case-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.

Example · java
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

  • Be the first to comment on this lesson.