Index

Sets in Detail

Java Collections

5.1 HashSet vs LinkedHashSet vs TreeSet

Java provides three commonly used implementations of the Set interface: HashSet, LinkedHashSet, and TreeSet. While they all guarantee uniqueness (no duplicate elements), they differ in their internal data structures and ordering behaviors. Understanding these differences is key to choosing the right implementation for your use case.

HashSet: Unordered and Fast

Example:

Set<String> hashSet = new HashSet<>();
hashSet.add("Banana");
hashSet.add("Apple");
hashSet.add("Cherry");
System.out.println(hashSet); // Output may vary: [Banana, Apple, Cherry]

HashSet is ideal when you don’t care about order and need fast access or lookup.

LinkedHashSet: Insertion-Order Preservation

Example:

Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Banana");
linkedHashSet.add("Apple");
linkedHashSet.add("Cherry");
System.out.println(linkedHashSet); // Output: [Banana, Apple, Cherry]

Use LinkedHashSet when you need predictable iteration order without sorting.

TreeSet: Sorted by Natural Order or Comparator

Example:

Set<String> treeSet = new TreeSet<>();
treeSet.add("Banana");
treeSet.add("Apple");
treeSet.add("Cherry");
System.out.println(treeSet); // Output: [Apple, Banana, Cherry]

Use TreeSet when elements need to be automatically sorted and you can tolerate slower performance.

Comparison Summary Table

Feature HashSet LinkedHashSet TreeSet
Internal Structure Hash Table Hash Table + Linked List Red-Black Tree
Order Guarantee None Insertion Order Sorted Order
Performance (Avg) O(1) O(1) O(log n)
Allows Nulls? Yes (1 null) Yes (1 null) Yes (if null-safe comparator not used)
Requires Comparable No No Yes (or custom Comparator)
Maintains Duplicates No No No

Ordering Diagram Example

Given input elements in the order: "Banana", "Apple", "Cherry":

HashSet       → [Apple, Banana, Cherry] ← Order is arbitrary
LinkedHashSet → [Banana, Apple, Cherry] ← Preserves insertion order
TreeSet       → [Apple, Banana, Cherry] ← Sorted (alphabetically)

Conclusion

Choosing the right Set implementation can significantly improve performance and maintainability based on your specific data handling needs.

Index

5.2 Handling Duplicates and Ordering

The primary feature of a Set in Java is uniqueness—a Set cannot contain duplicate elements. Unlike List, which allows duplicates and maintains element positions, Set ensures that only one instance of any object (as defined by equality) exists in the collection.

How Duplicates Are Detected

The uniqueness contract in Set relies on two key methods:

When an element is added, the Set:

  1. Computes the element's hash code.
  2. Looks in the corresponding hash bucket.
  3. Uses equals() to compare against existing elements.
  4. Rejects the addition if an equal element is found.

Example: Duplicate Handling with HashSet

Set<String> names = new HashSet<>();
names.add("Alice");
names.add("Bob");
names.add("Alice"); // Duplicate

System.out.println(names); // Output: [Alice, Bob]

Even though "Alice" was added twice, HashSet only retains one copy because String overrides equals() and hashCode() properly.

Ordering Behavior

Each Set implementation has different behavior regarding the order of iteration:

Set Type Maintains Order? Description
HashSet ❌ No order guarantee Elements are unordered and may appear randomly.
LinkedHashSet ✅ Insertion order Preserves the order in which elements were added.
TreeSet ✅ Sorted order Maintains elements in natural or custom order.

Examples of Ordering Differences

HashSet:

Set<String> hashSet = new HashSet<>();
hashSet.add("Banana");
hashSet.add("Apple");
hashSet.add("Cherry");

System.out.println("HashSet: " + hashSet);
// Output: Order is unpredictable, e.g., [Apple, Cherry, Banana]

LinkedHashSet:

Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Banana");
linkedHashSet.add("Apple");
linkedHashSet.add("Cherry");

System.out.println("LinkedHashSet: " + linkedHashSet);
// Output: [Banana, Apple, Cherry]

TreeSet:

Set<String> treeSet = new TreeSet<>();
treeSet.add("Banana");
treeSet.add("Apple");
treeSet.add("Cherry");

System.out.println("TreeSet: " + treeSet);
// Output: [Apple, Banana, Cherry] — sorted alphabetically

Summary

Understanding how Sets handle uniqueness and order helps avoid surprises in your application’s logic, especially when processing user input, maintaining catalogs, or implementing caches.

Index

5.3 Using Comparable and Comparator with Sets

TreeSet is a powerful Set implementation that maintains its elements in sorted order. Unlike HashSet or LinkedHashSet, which depend on hashCode() and equals() for uniqueness and ordering, TreeSet uses comparisons to determine element position and uniqueness. This comparison is performed using either the Comparable interface or a Comparator.

Natural Ordering with Comparable

If no custom comparator is provided, TreeSet relies on the natural ordering of elements — that is, the element class must implement the Comparable<T> interface and define the compareTo() method.

Example: Using Comparable in a Custom Class

import java.util.*;

class Person implements Comparable<Person> {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Natural ordering by age
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class TreeSetComparableExample {
    public static void main(String[] args) {
        Set<Person> people = new TreeSet<>();
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Charlie", 30)); // Treated as duplicate if compareTo returns 0

        System.out.println(people);
        // Output: [Bob (25), Alice (30)]
    }
}

Note: If compareTo() returns 0, the elements are considered equal by TreeSet, and the duplicate is not added, even if other fields differ.

Custom Ordering with Comparator

If you need multiple sort orders or the class does not implement Comparable, you can pass a Comparator to the TreeSet constructor.

Example: TreeSet with Comparator for Name

import java.util.*;

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class TreeSetComparatorExample {
    public static void main(String[] args) {
        // Sort by name instead of age
        Comparator<Person> nameComparator = Comparator.comparing(p -> p.name);

        Set<Person> people = new TreeSet<>(nameComparator);
        people.add(new Person("Alice", 30));
        people.add(new Person("Bob", 25));
        people.add(new Person("Alice", 40)); // Duplicate based on name

        System.out.println(people);
        // Output: [Alice (30), Bob (25)]
    }
}

Here, "Alice (40)" is not added because "Alice (30)" already exists and the comparator considers them equal based on the name.

Comparable vs Comparator

Feature Comparable Comparator
Interface method compareTo(T o) compare(T o1, T o2)
Where defined Inside the class Outside the class
Natural or custom order Natural Custom
Flexibility One sort order Multiple sort orders possible

Key Takeaways

Mastering Comparable and Comparator unlocks the full potential of TreeSet in your Java collection toolbox.

Index

5.4 Runnable Examples: Implementing sets with custom objects

In this section, we’ll look at how to use custom classes as elements in different Set implementations—HashSet, LinkedHashSet, and TreeSet. You’ll learn how to properly implement equals(), hashCode(), and Comparable, and how to use a custom Comparator to define ordering behavior.

Example: Custom Class for HashSet and LinkedHashSet

import java.util.*;

class Book {
    String title;
    String author;

    Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    // Required for HashSet/LinkedHashSet to detect duplicates
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Book)) return false;
        Book b = (Book) o;
        return title.equals(b.title) && author.equals(b.author);
    }

    @Override
    public int hashCode() {
        return Objects.hash(title, author);
    }

    @Override
    public String toString() {
        return "\"" + title + "\" by " + author;
    }
}

public class CustomSetDemo {
    public static void main(String[] args) {
        Set<Book> hashSet = new HashSet<>();
        Set<Book> linkedHashSet = new LinkedHashSet<>();

        Book b1 = new Book("Java Basics", "Alice");
        Book b2 = new Book("Advanced Java", "Bob");
        Book b3 = new Book("Java Basics", "Alice"); // Duplicate

        hashSet.add(b1);
        hashSet.add(b2);
        hashSet.add(b3); // Ignored due to equals() and hashCode()

        linkedHashSet.add(b1);
        linkedHashSet.add(b2);
        linkedHashSet.add(b3); // Ignored as well

        System.out.println("HashSet: " + hashSet);
        System.out.println("LinkedHashSet: " + linkedHashSet);
    }
}

Output:

HashSet: ["Java Basics" by Alice, "Advanced Java" by Bob]
LinkedHashSet: ["Java Basics" by Alice, "Advanced Java" by Bob]

Explanation: Even though b1 and b3 are different instances, HashSet and LinkedHashSet treat them as equal based on the overridden equals() and hashCode() methods.

Example: Custom Ordering in TreeSet with Comparator

import java.util.*;

class Student {
    String name;
    int score;

    Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return name + " (" + score + ")";
    }
}

public class TreeSetCustomComparator {
    public static void main(String[] args) {
        // Sort by score descending
        Comparator<Student> byScoreDesc = (s1, s2) -> Integer.compare(s2.score, s1.score);

        Set<Student> students = new TreeSet<>(byScoreDesc);

        students.add(new Student("Alice", 90));
        students.add(new Student("Bob", 85));
        students.add(new Student("Charlie", 90)); // Duplicate in sort key

        System.out.println("TreeSet with custom Comparator: " + students);
    }
}

Output:

TreeSet with custom Comparator: [Alice (90), Bob (85)]

Explanation: The two students with score 90 are considered equal by the comparator, so only the first is retained in the TreeSet.

Key Takeaways

These examples demonstrate how to use Java sets effectively with custom objects while maintaining correct behavior around uniqueness and ordering.

Index