Java Set Interface

In the last lesson you learned about Java Vector. Now we move to a different shape of collection called the Set. Where a List keeps duplicates, a Set throws away repeats and keeps only one of each value, so it is the tool when you need a collection of unique things.

πŸ€” The problem a Set solves

You are collecting newsletter signups. The same person clicks signup three times. In a List you now have the same email three times, so you send three copies later. That looks broken.

You could check β€œis this email already in my List?” before every add, but that check gets slow as the list grows, and it is easy to forget. You want a collection that refuses duplicates on its own.

That is a Set. A Set is a Collection that never holds the same element twice. You add freely, and it keeps only the unique values.

🧩 A Set is a Collection with no duplicates

A Set implements the Collection interface, so it has the methods you know: add, remove, contains, size, and isEmpty. The one rule it adds is simple. No element can appear twice. This example adds some names, including a repeat, and prints what survives.

import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> names = new HashSet<>();
names.add("Alex");
names.add("Riya");
names.add("Arjun");
names.add("Alex"); // duplicate, ignored
System.out.println(names);
System.out.println("Size: " + names.size());
}
}

What happens:

  • We add Alex, Riya, Arjun, then Alex again.
  • The Set already has Alex, so the second one is dropped.
  • The size is 3, not 4.

The duplicate just disappears, with no check from you. Notice the output order is not the add order. We come back to that.

Output

[Arjun, Alex, Riya]
Size: 3

πŸ” add returns false when the element is already there

The add method returns a boolean. It returns true if the element was new and got added, and false if it was already present. This example uses that return value to detect a duplicate.

import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> emails = new HashSet<>();
boolean first = emails.add("a@mail.com");
boolean second = emails.add("a@mail.com"); // same email again
System.out.println("First add worked: " + first);
System.out.println("Second add worked: " + second);
System.out.println(emails);
}
}

Read the lines:

  • The first add returns true, because the email was new.
  • The second add returns false, because it was already there.
  • The Set still has one copy.

This is a clean way to answer β€œwas this already signed up?” in one call. The false tells you it was a repeat.

Output

First add worked: true
Second add worked: false
[a@mail.com]

🚫 No index access, you iterate instead

A List lets you say list.get(0). A Set does not. There is no index access, no get(0), no set(2, x). A Set is about membership, not position. It answers β€œis this value in here?”, not β€œwhat is the third item?”.

So to read everything in a Set, you loop over it. This example walks through every element with a for-each loop.

import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> cities = new HashSet<>();
cities.add("Paris");
cities.add("Tokyo");
cities.add("Cairo");
// βœ… no get(index); loop instead
for (String city : cities) {
System.out.println(city);
}
// check membership, which a Set is great at
System.out.println("Has Tokyo? " + cities.contains("Tokyo"));
}
}

The walkthrough:

  • The for-each loop visits every element.
  • There is no number index, just each value in turn.
  • contains("Tokyo") answers the membership question fast.

So a Set trades index access for fast membership checks. That contains check is the main reason people reach for a Set.

Output

Paris
Cairo
Tokyo
Has Tokyo? true

πŸ—‚οΈ The three main implementations

Set is an interface, so you pick one of three built-in implementations. They differ in one thing: the order they keep elements in.

  • HashSet keeps no order at all. Fastest for add, remove, and contains.
  • LinkedHashSet keeps elements in insertion order. A little slower than HashSet, but predictable.
  • TreeSet keeps elements sorted in natural order. The slowest, because it stays sorted as you add.

Let’s see each in detail.

⚑ HashSet, no order and fastest

HashSet is the default Set you reach for. It promises no order, so elements can print in any arrangement. In return, it is the fastest at the core jobs. This example shows the print order has nothing to do with the add order.

import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<Integer> numbers = new HashSet<>();
numbers.add(50);
numbers.add(10);
numbers.add(30);
numbers.add(20);
// order is not the add order, and not sorted either
System.out.println(numbers);
System.out.println("Contains 30? " + numbers.contains(30));
}
}

Notice the result:

  • We added 50, 10, 30, 20.
  • The print shows a different, internal order.
  • contains(30) is still instant.

So HashSet output not matching your order is by design. Use it when order does not matter and you want speed.

Output

[50, 20, 30, 10]
Contains 30? true

HashSet order is not your order

Never rely on the order of a HashSet. It can change between runs and between Java versions. If you print one and it happens to look sorted, that is luck, not a promise. Need a fixed order? Use LinkedHashSet or TreeSet.

πŸ“₯ LinkedHashSet, keeps insertion order

LinkedHashSet works like HashSet but adds one promise. It remembers the order you inserted elements and gives them back in that order. Great when you want unique items and tidy, predictable output. This example shows the elements come back in add order.

import java.util.LinkedHashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> steps = new LinkedHashSet<>();
steps.add("Sign up");
steps.add("Verify email");
steps.add("Set password");
steps.add("Sign up"); // duplicate, ignored
// βœ… comes back in the order we added
System.out.println(steps);
System.out.println("Size: " + steps.size());
}
}

What to see:

  • Elements print in the exact order we added them.
  • The duplicate Sign up is still dropped, like any Set.
  • The order is reliable across runs.

So LinkedHashSet gives the no-duplicates rule plus a stable order. The small cost is a little extra memory and time.

Output

[Sign up, Verify email, Set password]
Size: 3

🌳 TreeSet, keeps elements sorted

TreeSet keeps its elements sorted at all times. As you add, it slots each element into its correct spot. So when you loop or print, everything comes out in natural order. This example adds numbers out of order and prints them sorted.

import java.util.Set;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
Set<Integer> scores = new TreeSet<>();
scores.add(50);
scores.add(10);
scores.add(30);
scores.add(20);
// βœ… always sorted, no matter the add order
System.out.println(scores);
}
}

Read the result:

  • We added 50, 10, 30, 20.
  • TreeSet prints them as 10, 20, 30, 50.
  • The sorting is automatic and always kept.

TreeSet is the pick when you need a unique, sorted collection. The trade-off is speed, since sorting on every add is more work than HashSet does.

Output

[10, 20, 30, 50]

TreeSet needs comparable elements

A TreeSet must know how to order its elements. Built-in types like String and Integer already know their natural order. For your own classes, the class must implement Comparable, or you pass a Comparator to the TreeSet, otherwise it throws a ClassCastException at runtime.

🧬 Uniqueness for custom objects needs equals and hashCode

So far we used Strings and numbers, where β€œsame value” is obvious. But how does a Set know two Person objects are the same person?

By default, Java treats two different objects as different even if their fields match. To make a Set see them as equal, your class must override two methods:

  • equals decides if two objects count as the same.
  • hashCode gives matching objects the same bucket number so the Set can find them.

For HashSet and LinkedHashSet you need both. This example defines both, so duplicates are caught.

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
// βœ… two people are equal if name and age match
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person other = (Person) o;
return age == other.age && Objects.equals(name, other.name);
}
// βœ… equal objects must return the same hash
public int hashCode() {
return Objects.hash(name, age);
}
public String toString() {
return name + "(" + age + ")";
}
}
public class Main {
public static void main(String[] args) {
Set<Person> people = new HashSet<>();
people.add(new Person("Alex", 30));
people.add(new Person("Alex", 30)); // same data, treated as duplicate
System.out.println(people);
System.out.println("Size: " + people.size());
}
}

Walk through it:

  • We add two Person objects with the same name and age.
  • Because equals and hashCode both look at name and age, the Set sees them as the same.
  • The second one is dropped, so the size is 1.

Without these two methods, the Set would treat them as two different people and the size would be 2. So for custom objects, equals and hashCode are what make uniqueness work.

Output

[Alex(30)]
Size: 1

βž• Set operations: union, intersection, difference

Sets shine when you combine them, just like in math. Three methods you already have on any collection do this:

  • addAll gives the union, everything from both sets.
  • retainAll gives the intersection, only the items in both.
  • removeAll gives the difference, items in one set but not the other.

This example builds two sets and shows all three operations.

import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3, 4));
Set<Integer> b = new HashSet<>(Set.of(3, 4, 5, 6));
// union: everything in either set
Set<Integer> union = new HashSet<>(a);
union.addAll(b);
System.out.println("Union: " + union);
// intersection: only items in both
Set<Integer> common = new HashSet<>(a);
common.retainAll(b);
System.out.println("Intersection: " + common);
// difference: in a but not in b
Set<Integer> onlyA = new HashSet<>(a);
onlyA.removeAll(b);
System.out.println("Difference: " + onlyA);
}
}

The idea behind each step:

  • We copy a first so the original sets stay unchanged.
  • union.addAll(b) merges both, and duplicates collapse to one.
  • common.retainAll(b) keeps only values in both, here 3 and 4.
  • onlyA.removeAll(b) strips out anything b has, leaving 1 and 2.

So β€œwho is in both groups?” and β€œwho is only in the first?” each become a one-liner.

Output

Union: [1, 2, 3, 4, 5, 6]
Intersection: [3, 4]
Difference: [1, 2]

🎯 Program to the Set interface

Declare your variable with the Set type, not the concrete class. So Set<String> names = new HashSet<>();, not HashSet<String> names = new HashSet<>();.

import java.util.LinkedHashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
// βœ… variable typed as the Set interface
Set<String> tags = new LinkedHashSet<>();
tags.add("java");
tags.add("coding");
System.out.println(tags);
}
}

Why this matters:

  • Your code depends on Set behavior, not one specific class.
  • Want sorting later? Change only the new TreeSet<>() part.
  • The rest keeps working, because it only ever called Set methods.

So programming to the interface lets you swap HashSet for LinkedHashSet or TreeSet in one place.

⚠️ Common Mistakes

A few Set mix-ups come up a lot.

Expecting order from a HashSet. People assume items print in add order. HashSet makes no such promise.

// ❌ assuming HashSet keeps your order
Set<String> s = new HashSet<>();
s.add("first");
s.add("second");
// printing this may NOT show first, then second
// βœ… need insertion order? use LinkedHashSet
Set<String> ordered = new LinkedHashSet<>();

If you need a known order, pick LinkedHashSet for insertion order or TreeSet for sorted order.

Custom objects without equals and hashCode. Without these, a HashSet will not catch duplicates, because it falls back to comparing object identity.

// ❌ no equals/hashCode: two "same" objects both get stored
class Point { int x, y; }
// βœ… override both so equal points count as one
// (see the Person example above)

For any custom type going into a Set, write both methods.

Expecting index access. A Set has no get(index). Code like set.get(0) will not compile.

// ❌ Sets have no index access
// String first = mySet.get(0); // does not compile
// βœ… loop, or convert to a List if you truly need positions
for (String item : mySet) {
System.out.println(item);
}

If you genuinely need positions, you probably want a List, not a Set.

βœ… Best Practices

Build these habits and Sets will serve you well.

  • Use HashSet by default. It is the fastest and fits most β€œI just need unique items” cases.
  • Use LinkedHashSet when order matters. It keeps insertion order with only a small cost.
  • Use TreeSet when you need sorting. It keeps everything in natural order automatically.
  • Always pair equals and hashCode for custom objects. Both, not just one, for hash-based Sets.
  • Program to the Set interface. Declare Set<T> so you can swap the implementation later.
  • Reach for Sets for membership checks. contains on a Set is far faster than scanning a List.

🧩 What You’ve Learned

Nicely done. Let’s recap the Set interface in plain words.

  • βœ… A Set is a Collection that holds no duplicates, keeping only one of each value.
  • βœ… add returns false when the element is already there, so you can detect repeats.
  • βœ… A Set has no index access. You iterate over it instead of using get(0).
  • βœ… HashSet keeps no order and is fastest, LinkedHashSet keeps insertion order, TreeSet keeps elements sorted.
  • βœ… For custom objects, uniqueness relies on equals and hashCode.
  • βœ… Set operations: addAll for union, retainAll for intersection, removeAll for difference.
  • βœ… Program to the Set interface so you can swap implementations freely.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is the defining rule of a Set?

    Why: A Set is a Collection that never stores the same element twice. Uniqueness is its core rule.

  2. 2

    What does add return when the element is already in the Set?

    Why: add returns false when the element was already present, because nothing changed.

  3. 3

    Which implementation keeps elements in sorted order?

    Why: TreeSet keeps elements in natural sorted order at all times. HashSet has no order and LinkedHashSet keeps insertion order.

  4. 4

    What does retainAll do to a set?

    Why: retainAll keeps only the elements that also appear in the other set, which is the intersection.

πŸš€ What’s Next?

Now you understand the Set idea: unique elements, no duplicates, and three implementations to choose from. Next we zoom into the most common one. Let’s see how HashSet works inside, why it is so fast, and when to use it.

Java HashSet

Share & Connect