Java Tutorial
🔍

Java LinkedHashSet

Java LinkedHashSet

java.util.LinkedHashSet<E> solves a very specific problem that HashSet cannot: it maintains the order in which elements were inserted while still rejecting duplicates and providing O(1) contains(). When you need a collection that is simultaneously unique, fast for membership checks, and predictable in its iteration order, LinkedHashSet is the correct default.

The distinction matters in production more than beginners expect. API responses, audit logs, and deduplication pipelines where the "first seen" order of items is meaningful — these all need LinkedHashSet, not HashSet.

What Is Java LinkedHashSet?

LinkedHashSet<E> is a concrete class in java.util that extends HashSet<E> and implements Set<E>. It adds one guarantee that HashSet cannot provide: iteration always produces elements in the order they were first inserted. Duplicates are still rejected, contains() is still O(1) average, and add() is still O(1) average — the only cost over HashSet is a small memory overhead per element for the linked list that tracks insertion order.

The diagram below shows where LinkedHashSet sits in the Collections hierarchy and how it compares to the other two Set implementations.

java.lang.Iterable<E>
    └── java.util.Collection<E>
            └── java.util.Set<E>               ← uniqueness contract
                    ├── java.util.HashSet<E>    ← hash table, NO order guarantee
                    │       └── LinkedHashSet   ← extends HashSet, INSERTION ORDER
                    └── java.util.SortedSet<E>
                            └── NavigableSet<E>
                                    └── TreeSet ← Red-Black tree, SORTED ORDER

KEY FACTS:
  Package    : java.util
  Since      : Java 1.4
  Extends    : HashSet<E>
  Backed by  : LinkedHashMap<E, Object> internally
  Ordering   : insertion order — elements iterate in the order they were first added
  Null       : allows exactly one null element
  Duplicates : rejected — add() returns false for existing elements
  Thread     : NOT thread-safe
  Initial capacity: 16 buckets (default)
  Load factor     : 0.75

The Three Set Implementations — Key Differences

FEATURE COMPARISON:

  Feature                HashSet          LinkedHashSet       TreeSet
  ---------------------- ---------------- ------------------- ------------------
  Ordering               None             Insertion order     Natural/Comparator
  add() / contains()     O(1) average     O(1) average        O(log n)
  Backed by              HashMap          LinkedHashMap        TreeMap
  Memory per element     Moderate         Slightly higher      Higher (tree node)
  Null allowed           Yes (1)          Yes (1)             No
  floor/ceiling/headSet  No               No                  Yes
  Best for               Pure speed       Order + uniqueness  Sorted unique set

When to Use LinkedHashSet

The decision between the three Set implementations follows a clear priority order.

CHOOSE LinkedHashSet WHEN:
  1. Elements must be unique AND iteration order must match insertion order
     — e.g., deduplicated list of recently visited pages, ordered unique tags
  2. Producing deterministic output from a Set for display, logging, or API responses
     — HashSet iteration order is undefined and can change between JVM runs
  3. "First seen wins" deduplication where the position of first occurrence matters
     — e.g., first unique city in a stream of user locations

CHOOSE HashSet INSTEAD WHEN:
  - Order is completely irrelevant AND maximum speed is the priority
  - HashSet is marginally faster than LinkedHashSet — no linked list to update

CHOOSE TreeSet INSTEAD WHEN:
  - Sorted order is required (ascending or via Comparator)
  - Range operations are needed: headSet(), tailSet(), floor(), ceiling()
  - The cost of O(log n) per operation is acceptable for sorted access

CHOOSE ArrayList or LinkedHashMap INSTEAD WHEN:
  - Duplicates are needed alongside ordered access (use ArrayList)
  - Key-value pairs are needed with insertion order (use LinkedHashMap)

A pattern that appears in production code:
  Use HashSet during processing (fastest contains and add),
  then wrap in new LinkedHashSet<>(hashSet) when deterministic output is needed.
  This costs one O(n) conversion instead of paying the LinkedHashSet overhead for
  the entire processing phase.

How LinkedHashSet Works Internally

LinkedHashSet is implemented as a thin wrapper around LinkedHashMap — just like HashSet wraps HashMap. The crucial difference is the backing map.

HASHSET internals:
  HashMap<E, Object> map
  — hash table only, no ordering

LINKEDHASHSET internals:
  LinkedHashMap<E, Object> map
  — hash table PLUS a doubly-linked list threading all entries in insertion order

Every add(e) call maps to:
  map.put(e, PRESENT)
  LinkedHashMap.put() does two things:
    1. Adds the key to the hash table bucket (same as HashMap)
    2. Appends the entry to the tail of the linked list

STRUCTURE AFTER:
  set.add("C"); set.add("A"); set.add("B"); set.add("A"); // duplicate

  HASH TABLE:
    bucket[2]: Entry("C") → null
    bucket[7]: Entry("A") → null
    bucket[4]: Entry("B") → null

  DOUBLY-LINKED LIST (tracks insertion order):
    HEAD → [Entry("C")] ↔ [Entry("A")] ↔ [Entry("B")] → null
                                                               TAIL

  Iteration traverses the linked list — always C, A, B.
  "A" duplicate was rejected: map.put("A", PRESENT) returned PRESENT (old value),
  so add() returned false and NO new entry was appended to the linked list.

  set.size()  → 3  (C, A, B)
  iteration   → C, A, B  (insertion order preserved)

The diagram below shows the full internal structure for a LinkedHashSet with four elements.

LINKEDHASHSET WITH ELEMENTS ["Mumbai", "Delhi", "Chennai", "Bengaluru"]:

  HASH TABLE (array of buckets):
    bucket[3]:  Entry("Mumbai")    → null
    bucket[7]:  Entry("Delhi")     → null
    bucket[1]:  Entry("Chennai")   → Entry("Bengaluru") → null  (collision)
    ...rest:    null

  LINKED LIST (insertion order):
    HEAD
     ↓
    [before=HEAD | "Mumbai"    | after=Delhi]
         ↓
    [before=Mumbai | "Delhi"   | after=Chennai]
         ↓
    [before=Delhi | "Chennai"  | after=Bengaluru]
         ↓
    [before=Chennai | "Bengaluru" | after=TAIL]
     ↓
    TAIL

  Each Entry has FOUR references:
    hash, key, value (PRESENT)  ← HashMap fields
    before, after               ← LinkedHashMap linked-list fields

  Iterator traverses before→after pointers — O(1) per step, O(n) total.
  Result: Mumbai, Delhi, Chennai, Bengaluru — always, regardless of bucket layout.

Core Operations with Examples

add() — Insertion Order Guaranteed

Java
1// File: LinkedHashSetAddDemo.java 2 3import java.util.HashSet; 4import java.util.LinkedHashSet; 5import java.util.Set; 6 7public class LinkedHashSetAddDemo { 8 9 public static void main(String[] args) { 10 11 LinkedHashSet<String> visitedPages = new LinkedHashSet<>(); 12 13 // add() returns true for new elements, false for duplicates 14 System.out.println("=== add() return values ==="); 15 System.out.println("add(/home) : " + visitedPages.add("/home")); // true 16 System.out.println("add(/products) : " + visitedPages.add("/products")); // true 17 System.out.println("add(/cart) : " + visitedPages.add("/cart")); // true 18 System.out.println("add(/home) : " + visitedPages.add("/home")); // false — duplicate 19 System.out.println("add(/checkout) : " + visitedPages.add("/checkout")); // true 20 System.out.println("add(/products) : " + visitedPages.add("/products")); // false — duplicate 21 System.out.println("add(/confirmation): " + visitedPages.add("/confirmation"));// true 22 23 System.out.println("\nInsertion order preserved: " + visitedPages); 24 System.out.println("Size (unique only): " + visitedPages.size()); // 5, not 7 25 26 System.out.println(); 27 28 // Contrast with HashSet — same elements, different iteration order 29 Set<String> hashSet = new HashSet<>(visitedPages); 30 System.out.println("=== Iteration order comparison ==="); 31 System.out.println("LinkedHashSet (insertion order): " + visitedPages); 32 System.out.println("HashSet (no order) : " + hashSet); 33 34 System.out.println(); 35 36 // null is allowed — one null element 37 LinkedHashSet<String> withNull = new LinkedHashSet<>(); 38 withNull.add("first"); 39 withNull.add(null); 40 withNull.add("third"); 41 withNull.add(null); // duplicate null — rejected 42 System.out.println("=== Null element ==="); 43 System.out.println("Set with null: " + withNull); // [first, null, third] 44 System.out.println("null at its insertion position: index 1"); 45 } 46}
Output:
=== add() return values ===
add(/home)        : true
add(/products)    : true
add(/cart)        : true
add(/home)        : false — duplicate
add(/checkout)    : true
add(/products)    : false — duplicate
add(/confirmation): true

Insertion order preserved: [/home, /products, /cart, /checkout, /confirmation]
Size (unique only): 5

=== Iteration order comparison ===
LinkedHashSet (insertion order): [/home, /products, /cart, /checkout, /confirmation]
HashSet       (no order)       : [/cart, /home, /checkout, /products, /confirmation]

=== Null element ===
Set with null: [first, null, third]
null at its insertion position: index 1

contains(), remove(), and Bulk Operations

Java
1// File: LinkedHashSetOperationsDemo.java 2 3import java.util.LinkedHashSet; 4import java.util.Set; 5 6public class LinkedHashSetOperationsDemo { 7 8 public static void main(String[] args) { 9 10 LinkedHashSet<String> activeFeatures = new LinkedHashSet<>(); 11 activeFeatures.add("DarkMode"); activeFeatures.add("BetaSearch"); 12 activeFeatures.add("NewCheckout"); activeFeatures.add("LiveTracking"); 13 activeFeatures.add("AIRecommendations"); 14 15 System.out.println("=== contains() — O(1) average ==="); 16 System.out.println("contains(DarkMode) : " + activeFeatures.contains("DarkMode")); // true 17 System.out.println("contains(ChatSupport): " + activeFeatures.contains("ChatSupport")); // false 18 19 System.out.println("\n=== remove() — preserves order of remaining ==="); 20 System.out.println("Before : " + activeFeatures); 21 activeFeatures.remove("BetaSearch"); 22 System.out.println("After remove(BetaSearch): " + activeFeatures); 23 // Order of remaining elements is unchanged 24 25 System.out.println("\n=== removeIf() — insertion order preserved ==="); 26 activeFeatures.removeIf(feature -> feature.startsWith("New")); 27 System.out.println("After removeIf(starts with New): " + activeFeatures); 28 29 System.out.println(); 30 31 // Set operations — intersection, difference, union — order preserved on result 32 LinkedHashSet<String> userPermissions = new LinkedHashSet<>( 33 Set.of("READ", "WRITE", "DELETE", "ADMIN", "EXPORT") 34 ); 35 LinkedHashSet<String> rolePermissions = new LinkedHashSet<>( 36 Set.of("READ", "WRITE", "COMMENT") 37 ); 38 39 // Intersection — what both sets share 40 LinkedHashSet<String> granted = new LinkedHashSet<>(userPermissions); 41 granted.retainAll(rolePermissions); // keeps only elements in BOTH 42 System.out.println("=== Set operations ==="); 43 System.out.println("User permissions: " + userPermissions); 44 System.out.println("Role permissions: " + rolePermissions); 45 System.out.println("Granted (intersection): " + granted); 46 47 // Difference — permissions user has but role does not 48 LinkedHashSet<String> exclusive = new LinkedHashSet<>(userPermissions); 49 exclusive.removeAll(rolePermissions); 50 System.out.println("Exclusive to user : " + exclusive); 51 52 // Union — all permissions 53 LinkedHashSet<String> allPerms = new LinkedHashSet<>(userPermissions); 54 allPerms.addAll(rolePermissions); // duplicates ignored 55 System.out.println("Union (all) : " + allPerms); 56 } 57}
Output:
=== contains() — O(1) average ===
contains(DarkMode)   : true
contains(ChatSupport): false

=== remove() — preserves order of remaining ===
Before     : [DarkMode, BetaSearch, NewCheckout, LiveTracking, AIRecommendations]
After remove(BetaSearch): [DarkMode, NewCheckout, LiveTracking, AIRecommendations]

=== removeIf() — insertion order preserved ===
After removeIf(starts with New): [DarkMode, LiveTracking, AIRecommendations]

=== Set operations ===
User permissions: [READ, WRITE, DELETE, ADMIN, EXPORT]
Role permissions: [READ, WRITE, COMMENT]
Granted (intersection): [READ, WRITE]
Exclusive to user     : [DELETE, ADMIN, EXPORT]
Union (all)           : [READ, WRITE, DELETE, ADMIN, EXPORT, COMMENT]

Deduplication While Preserving First-Occurrence Order

This is the most common production use case for LinkedHashSet — converting a list with duplicates into a list of unique elements in the order they first appeared.

Java
1// File: LinkedHashSetDeduplicationDemo.java 2 3import java.util.ArrayList; 4import java.util.Arrays; 5import java.util.LinkedHashSet; 6import java.util.List; 7 8public class LinkedHashSetDeduplicationDemo { 9 10 public static void main(String[] args) { 11 12 // Deduplication — first occurrence order preserved 13 List<String> rawSearchTerms = Arrays.asList( 14 "laptop", "laptop bag", "laptop", "laptop stand", 15 "laptop cooling pad", "laptop bag", "laptop", "laptop sleeve" 16 ); 17 System.out.println("Raw (with duplicates): " + rawSearchTerms); 18 19 // new LinkedHashSet<>(list) — O(n) pass, first occurrences in order 20 LinkedHashSet<String> uniqueTerms = new LinkedHashSet<>(rawSearchTerms); 21 System.out.println("Deduplicated (first-seen order): " + uniqueTerms); 22 23 // Convert back to List if needed 24 List<String> deduplicatedList = new ArrayList<>(uniqueTerms); 25 System.out.println("Back to List: " + deduplicatedList); 26 27 System.out.println(); 28 29 // Comparison: HashSet deduplication loses order 30 java.util.HashSet<String> hashDedup = new java.util.HashSet<>(rawSearchTerms); 31 System.out.println("HashSet dedup (no order) : " + hashDedup); 32 System.out.println("LinkedHashSet dedup (ordered) : " + uniqueTerms); 33 34 System.out.println(); 35 36 // Streaming deduplication with Collectors.toCollection 37 List<Integer> scores = Arrays.asList(92, 85, 92, 78, 85, 100, 78, 95); 38 System.out.println("Raw scores: " + scores); 39 40 LinkedHashSet<Integer> uniqueScores = scores.stream() 41 .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); 42 System.out.println("Unique scores (first-seen order): " + uniqueScores); 43 44 // distinct() in streams produces encounter order (source order for List) 45 List<Integer> distinctList = scores.stream() 46 .distinct() 47 .collect(java.util.stream.Collectors.toList()); 48 System.out.println("Stream distinct (same result) : " + distinctList); 49 } 50}
Output:
Raw (with duplicates): [laptop, laptop bag, laptop, laptop stand, laptop cooling pad, laptop bag, laptop, laptop sleeve]
Deduplicated (first-seen order): [laptop, laptop bag, laptop stand, laptop cooling pad, laptop sleeve]
Back to List: [laptop, laptop bag, laptop stand, laptop cooling pad, laptop sleeve]

HashSet dedup (no order)       : [laptop stand, laptop bag, laptop, laptop cooling pad, laptop sleeve]
LinkedHashSet dedup (ordered)  : [laptop, laptop bag, laptop stand, laptop cooling pad, laptop sleeve]

Raw scores: [92, 85, 92, 78, 85, 100, 78, 95]
Unique scores (first-seen order): [92, 85, 78, 100, 95]
Stream distinct (same result)   : [92, 85, 78, 100, 95]

Real-World Example — Razorpay Payment Method Selector

A payment gateway like Razorpay tracks which payment methods a user has used, in the order they were first used. The most recently used methods should appear at the top of the selector — but each method must appear only once, and the order must reflect when the user first used each method. LinkedHashSet solves this in a single data structure.

Java
1// File: PaymentMethod.java 2 3import java.util.Objects; 4 5public class PaymentMethod { 6 7 private final String methodId; 8 private final String type; // UPI, CARD, NETBANKING, WALLET 9 private final String label; // display name 10 private final String provider; // GPay, PhonePe, HDFC, etc. 11 12 public PaymentMethod(String methodId, String type, String label, String provider) { 13 this.methodId = methodId; 14 this.type = type; 15 this.label = label; 16 this.provider = provider; 17 } 18 19 public String getMethodId() { return methodId; } 20 public String getType() { return type; } 21 public String getLabel() { return label; } 22 public String getProvider() { return provider; } 23 24 // Identity is methodId — same payment method regardless of display label 25 @Override 26 public boolean equals(Object obj) { 27 if (!(obj instanceof PaymentMethod other)) return false; 28 return Objects.equals(this.methodId, other.methodId); 29 } 30 31 @Override 32 public int hashCode() { 33 return Objects.hash(methodId); 34 } 35 36 @Override 37 public String toString() { 38 return String.format("%-12s %-10s %s", type, provider, label); 39 } 40}
Java
1// File: PaymentMethodSelector.java 2 3import java.util.ArrayList; 4import java.util.LinkedHashSet; 5import java.util.List; 6 7public class PaymentMethodSelector { 8 9 // LinkedHashSet: unique methods, insertion order = order first used 10 private final LinkedHashSet<PaymentMethod> usedMethods = new LinkedHashSet<>(); 11 12 // All available methods in the system 13 private final List<PaymentMethod> allMethods = new ArrayList<>(); 14 15 public void registerAvailableMethod(PaymentMethod method) { 16 allMethods.add(method); 17 } 18 19 // Called when user successfully pays with a method 20 public void recordUsage(PaymentMethod method) { 21 boolean isNew = usedMethods.add(method); 22 System.out.println(" " + (isNew ? "FIRST USE " : "REPEAT USE ") + ": " + method); 23 } 24 25 // Returns payment options: recently used first, then remaining available 26 public List<PaymentMethod> getOrderedOptions() { 27 List<PaymentMethod> options = new ArrayList<>(usedMethods); // used methods first 28 29 // Append available methods not yet used 30 for (PaymentMethod available : allMethods) { 31 if (!usedMethods.contains(available)) { // O(1) check 32 options.add(available); 33 } 34 } 35 return options; 36 } 37 38 public void displaySelector() { 39 System.out.println("\n" + "=".repeat(58)); 40 System.out.println(" RAZORPAY — SELECT PAYMENT METHOD"); 41 System.out.println("=".repeat(58)); 42 System.out.println(" -- Recently Used --"); 43 int i = 1; 44 for (PaymentMethod m : usedMethods) { 45 System.out.printf(" %d. %s%n", i++, m); 46 } 47 System.out.println(" -- Other Options --"); 48 for (PaymentMethod m : allMethods) { 49 if (!usedMethods.contains(m)) { 50 System.out.printf(" %d. %s%n", i++, m); 51 } 52 } 53 System.out.println("=".repeat(58)); 54 } 55 56 public static void main(String[] args) { 57 58 PaymentMethodSelector selector = new PaymentMethodSelector(); 59 60 // Register all available payment methods 61 PaymentMethod gpay = new PaymentMethod("UPI-GPAY", "UPI", "Google Pay", "GPay"); 62 PaymentMethod phonepe = new PaymentMethod("UPI-PPE", "UPI", "PhonePe", "PhonePe"); 63 PaymentMethod hdfc = new PaymentMethod("CARD-HDFC", "CARD", "HDFC Debit", "HDFC"); 64 PaymentMethod icici = new PaymentMethod("CARD-ICICI", "CARD", "ICICI Credit", "ICICI"); 65 PaymentMethod paytm = new PaymentMethod("WALLET-PTM", "WALLET", "Paytm Wallet", "Paytm"); 66 PaymentMethod netbank = new PaymentMethod("NB-SBI", "NETBANKING", "SBI NetBanking","SBI"); 67 68 selector.registerAvailableMethod(gpay); 69 selector.registerAvailableMethod(phonepe); 70 selector.registerAvailableMethod(hdfc); 71 selector.registerAvailableMethod(icici); 72 selector.registerAvailableMethod(paytm); 73 selector.registerAvailableMethod(netbank); 74 75 System.out.println("--- User transaction history ---"); 76 selector.recordUsage(hdfc); // first use — added to LinkedHashSet 77 selector.recordUsage(gpay); // first use 78 selector.recordUsage(hdfc); // repeat use — LinkedHashSet rejects, position unchanged 79 selector.recordUsage(paytm); // first use 80 selector.recordUsage(gpay); // repeat use — rejected, GPay stays at position 2 81 selector.recordUsage(hdfc); // repeat use again 82 83 selector.displaySelector(); 84 85 System.out.println("\n--- User adds new method ---"); 86 selector.recordUsage(icici); // first use — appended after paytm 87 selector.displaySelector(); 88 } 89}
Output:
--- User transaction history ---
  FIRST USE  : CARD       HDFC       HDFC Debit
  FIRST USE  : UPI        GPay       Google Pay
  REPEAT USE : CARD       HDFC       HDFC Debit
  FIRST USE  : WALLET     Paytm      Paytm Wallet
  REPEAT USE : UPI        GPay       Google Pay
  REPEAT USE : CARD       HDFC       HDFC Debit

==========================================================
  RAZORPAY — SELECT PAYMENT METHOD
==========================================================
  -- Recently Used --
  1. CARD       HDFC       HDFC Debit
  2. UPI        GPay       Google Pay
  3. WALLET     Paytm      Paytm Wallet
  -- Other Options --
  4. UPI        PhonePe    PhonePe
  5. CARD       ICICI      ICICI Credit
  6. NETBANKING SBI        SBI NetBanking
==========================================================

--- User adds new method ---
  FIRST USE  : CARD       ICICI      ICICI Credit

==========================================================
  RAZORPAY — SELECT PAYMENT METHOD
==========================================================
  -- Recently Used --
  1. CARD       HDFC       HDFC Debit
  2. UPI        GPay       Google Pay
  3. WALLET     Paytm      Paytm Wallet
  4. CARD       ICICI      ICICI Credit
  -- Other Options --
  5. UPI        PhonePe    PhonePe
  6. NETBANKING SBI        SBI NetBanking
==========================================================

LinkedHashSet handles the two requirements simultaneously: no duplicate payment method can appear in the "Recently Used" list, and methods appear in exactly the order they were first used. A HashSet would solve uniqueness but break the ordering. An ArrayList would preserve order but require manual duplicate checking.

Performance Considerations

OperationHashSetLinkedHashSetTreeSet
add(e)O(1) avgO(1) avgO(log n)
remove(e)O(1) avgO(1) avgO(log n)
contains(e)O(1) avgO(1) avgO(log n)
IterationO(n + capacity)O(n)O(n)
Memory per element~48 bytes (Node)~64 bytes (Node + 2 pointers)~56 bytes (TreeNode)
first() / last()Not availableNot availableO(log n)
floor() / ceiling()Not availableNot availableO(log n)

Iteration difference: LinkedHashSet iterates in O(n) by following linked-list before/after pointers — it does not scan empty buckets. HashSet iterates in O(n + capacity) because it scans all bucket slots including empty ones. For a HashSet pre-sized to 10,000 that holds 5 elements, iteration touches 10,000 slots. LinkedHashSet touches only the 5 elements.

Memory cost: Each entry in LinkedHashSet carries two extra references compared to HashSet — the before and after pointers for the doubly-linked list. On a 64-bit JVM with compressed references, this is approximately 8 extra bytes per entry. For most applications this is negligible. For extremely large sets in memory-constrained environments, HashSet is the leaner choice.

Thread safety: LinkedHashSet is not thread-safe. For concurrent ordered-set access, there is no direct concurrent equivalent. The common workaround is Collections.synchronizedSet(new LinkedHashSet<>()) or, for high-concurrency, use ConcurrentHashMap.newKeySet() (which provides no ordering) and manage ordering separately.

Best Practices

Declare the variable as Set<E> or LinkedHashSet<E> based on whether ordered iteration is part of the contract. If a method signature uses Set<E>, callers can pass any Set implementation. If ordered iteration is required by the caller — for display, for deterministic test output, for serialisation — declare LinkedHashSet<E> explicitly so the ordering guarantee is visible in the API. Using Set<E> for a variable that must preserve insertion order hides a contract that callers may rely on.

Use new LinkedHashSet<>(collection) for order-preserving deduplication. It processes the entire collection in one O(n) pass, preserves the first occurrence of each element, and is cleaner than manual loop-based deduplication. new ArrayList<>(new LinkedHashSet<>(list)) is the idiomatic one-liner for converting a list with duplicates to a deduplicated list in first-occurrence order.

Prefer LinkedHashSet over HashSet when writing unit tests that compare set contents. Tests that assert assertEquals(expectedSet, actualSet) with HashSet may produce confusing failure messages — the order of elements in the string representation is non-deterministic. LinkedHashSet produces consistent string output, making test failures readable. Wrap the actual result in new LinkedHashSet<>(actual) in assertions where order matters.

Do not use LinkedHashSet as a substitute for List when duplicates are intentional. LinkedHashSet is a Set — it rejects duplicates. If the business logic needs to count occurrences, maintain position including duplicates, or allow the same element at multiple positions, use a List. LinkedHashSet is only correct when uniqueness and ordered iteration are both required simultaneously.

Common Mistakes

Mistake 1 — Expecting Re-insertion to Update Position

Java
1LinkedHashSet<String> recentItems = new LinkedHashSet<>(); 2recentItems.add("Item-A"); // position 0 3recentItems.add("Item-B"); // position 1 4recentItems.add("Item-C"); // position 2 5 6// WRONG assumption: re-adding "Item-A" moves it to the end 7recentItems.add("Item-A"); // add() returns false — Item-A stays at position 0! 8 9System.out.println(recentItems); // [Item-A, Item-B, Item-C] — unchanged 10 11// CORRECT for LRU-style "move to end on re-access": 12recentItems.remove("Item-A"); 13recentItems.add("Item-A"); // now at position 2 (end) 14System.out.println(recentItems); // [Item-B, Item-C, Item-A]

Mistake 2 — Using LinkedHashSet When TreeSet Is Needed for Sorted Order

Java
1// WRONG for sorted output — LinkedHashSet preserves insertion order, NOT sorted order 2LinkedHashSet<String> cities = new LinkedHashSet<>(); 3cities.add("Pune"); cities.add("Mumbai"); cities.add("Ahmedabad"); 4System.out.println("LinkedHashSet: " + cities); // [Pune, Mumbai, Ahmedabad] — insertion order 5 6// CORRECT for alphabetically sorted unique cities 7java.util.TreeSet<String> sortedCities = new java.util.TreeSet<>(); 8sortedCities.add("Pune"); sortedCities.add("Mumbai"); sortedCities.add("Ahmedabad"); 9System.out.println("TreeSet : " + sortedCities); // [Ahmedabad, Mumbai, Pune]

Mistake 3 — Mutating an Element's hashCode-Relevant Field After Insertion

Java
1import java.util.LinkedHashSet; 2import java.util.Objects; 3 4// Same contract as HashSet — mutating a field used in hashCode() loses the element 5class Tag { 6 String name; 7 Tag(String n) { this.name = n; } 8 9 @Override public boolean equals(Object o) { return o instanceof Tag t && name.equals(t.name); } 10 @Override public int hashCode() { return Objects.hash(name); } 11 @Override public String toString() { return name; } 12} 13 14LinkedHashSet<Tag> tags = new LinkedHashSet<>(); 15Tag t = new Tag("java"); 16tags.add(t); 17 18t.name = "kotlin"; // bucket index changes — element lost in wrong bucket 19 20System.out.println(tags.contains(t)); // false — t now hashes to a different bucket 21System.out.println(tags.size()); // 1 — entry exists but is unreachable 22// Use immutable fields in equals/hashCode to avoid this

Mistake 4 — Using LinkedHashSet for Thread-Safe Ordered Sets Without Synchronisation

Java
1// WRONG — LinkedHashSet is not thread-safe 2LinkedHashSet<String> sharedSet = new LinkedHashSet<>(); 3// Thread 1 adds elements, Thread 2 iterates — can corrupt the linked list 4 5// CORRECT — synchronise external to the set 6Set<String> syncSet = java.util.Collections.synchronizedSet(new LinkedHashSet<>()); 7// External synchronisation required during iteration: 8synchronized (syncSet) { 9 for (String item : syncSet) { 10 process(item); 11 } 12}

Interview Questions

Q1. What is LinkedHashSet in Java and how does it differ from HashSet?

LinkedHashSet extends HashSet and adds one guarantee: iteration always produces elements in the order they were inserted. HashSet makes no ordering guarantee — elements can iterate in any order, and that order may change after rehashing. Internally, LinkedHashSet is backed by LinkedHashMap instead of HashMap. LinkedHashMap maintains a doubly-linked list threading all entries in insertion order alongside the hash table. This linked list is what LinkedHashSet's iterator traverses, giving O(n) iteration with predictable order. Both HashSet and LinkedHashSet provide O(1) average add(), remove(), and contains().

Q2. What is the internal structure of LinkedHashSet?

LinkedHashSet is backed by a LinkedHashMap<E, Object> where each element becomes a key and a shared static PRESENT dummy object is the value. LinkedHashMap extends HashMap and maintains a doubly-linked list of all entries using before and after pointers on each Entry. When add(element) is called, LinkedHashMap.put(element, PRESENT) adds the entry to both the hash table bucket and the tail of the doubly-linked list. Iteration follows the linked list from head to tail, producing insertion order. Removal updates both the hash table and the linked list pointers in O(1).

Q3. Does re-inserting an existing element change its position in LinkedHashSet?

No. add(element) on an already-present element returns false and makes no structural change — the element stays at its original insertion position. If you need the element to move to the end (as in an LRU-style structure), you must explicitly remove(element) first and then add(element) again. This is a common misconception and a common interview follow-up question — the answer is: LinkedHashSet preserves the position of the first insertion, not the most recent access.

Q4. Why does LinkedHashSet iterate faster than HashSet for full traversals?

HashSet iteration is O(n + capacity) because it scans all bucket slots in the backing HashMap array, including empty slots. For a HashSet with capacity 10,000 but only 5 elements, iteration examines 10,005 slots. LinkedHashSet iteration is O(n) because it follows the doubly-linked list pointers — it touches only the n live entries, skipping all empty buckets entirely. For sets with low load relative to capacity, LinkedHashSet's iterator is measurably faster despite the slightly higher per-entry memory cost.

Q5. When would you choose LinkedHashSet over a List for storing unique ordered elements?

Use LinkedHashSet when contains() or remove() is called frequently and the number of elements is large enough that O(n) list-scan would be slow. LinkedHashSet.contains() is O(1); ArrayList.contains() is O(n). For displaying a deduplicated stream of events in arrival order, or checking membership in a "visited" set while maintaining traversal order, LinkedHashSet handles both in one data structure. Use ArrayList when duplicates are intentional or when index-based access (get(i)) is needed — LinkedHashSet has no get(index) method.

Q6. How does LinkedHashSet handle the equals() and hashCode() contract?

Identically to HashSet — because it inherits the same HashMap-based lookup mechanism through LinkedHashMap. Two elements that are equal by equals() must produce the same hashCode(). If hashCode() is not overridden alongside equals(), two logically equal objects land in different buckets, and the set stores both — silently violating uniqueness. The linked list in LinkedHashMap does not help with this — it only tracks insertion order of keys that the hash table has already accepted. Correct equals() and hashCode() implementation is as critical for LinkedHashSet as it is for HashSet.

FAQs

Does LinkedHashSet maintain sorted order?

No. LinkedHashSet maintains insertion order — the order elements were first added. For sorted order (alphabetical or numeric), use TreeSet. For insertion order with uniqueness, use LinkedHashSet. These are distinct guarantees. A LinkedHashSet populated with {"B", "A", "C"} iterates as B, A, C. A TreeSet with the same elements iterates as A, B, C.

How do you convert a List with duplicates to a List with unique elements in first-occurrence order?

new ArrayList<>(new LinkedHashSet<>(originalList)) is the idiomatic one-liner. new LinkedHashSet<>(originalList) deduplicates in one O(n) pass, preserving first-occurrence order. Wrapping in new ArrayList<>() gives back a list with index access. Alternatively, originalList.stream().distinct().collect(Collectors.toList()) achieves the same result for List sources.

What is the memory difference between HashSet and LinkedHashSet?

Each entry in LinkedHashSet carries two additional references compared to HashSet: the before and after pointers for the doubly-linked list. On a 64-bit JVM with compressed references, this adds approximately 8 bytes per entry. For a set of 1,000,000 elements, the overhead difference is roughly 8 MB. For most applications this is negligible; for memory-constrained services with very large sets, HashSet is the leaner choice.

Is LinkedHashSet thread-safe?

No. LinkedHashSet is not thread-safe. Concurrent modification from multiple threads can corrupt both the hash table and the doubly-linked list. For thread-safe ordered set access, wrap it: Collections.synchronizedSet(new LinkedHashSet<>()). All iteration must be done inside a synchronized block on the wrapper. For high-concurrency scenarios, there is no direct concurrent LinkedHashSet equivalent — consider ConcurrentHashMap.newKeySet() (unordered, faster) or managing a separate ordering structure alongside a concurrent set.

Can LinkedHashSet be used as a queue with unique elements?

Yes — this is a practical use case. LinkedHashSet supports add() at the tail (end), iteration in insertion order, and iterator.remove() for removal during traversal. For a queue where each item should be processed at most once, LinkedHashSet prevents re-queuing the same item and guarantees FIFO processing order. To remove and process the first element: Iterator<E> it = set.iterator(); E first = it.next(); it.remove();. For high-throughput concurrent scenarios, LinkedBlockingQueue combined with a Set is more appropriate.

How does LinkedHashSet compare to a List for tracking visited items in order?

A List preserves insertion order and allows contains(), but contains() is O(n) — scanning every element. For a visited-items tracker that checks membership on every incoming item, O(n) per check produces O(n²) total. LinkedHashSet.contains() is O(1) average. Use LinkedHashSet when both "have I seen this before?" checks and ordered traversal are needed on the same collection. Use a List only when duplicates are intentional or index-based access is required.

Summary

LinkedHashSet<E> is the correct Set implementation when two requirements must be satisfied simultaneously: no duplicate elements and a predictable iteration order that matches insertion sequence. Backed by LinkedHashMap, it maintains a doubly-linked list of all entries alongside the hash table. add(), remove(), and contains() are O(1) average — identical to HashSet. The only cost is slightly more memory per entry and a marginally slower add() due to linked-list pointer maintenance.

The canonical use case: order-preserving deduplication. new LinkedHashSet<>(list) deduplicates a list in one pass, keeping the first occurrence of each element in position. new ArrayList<>(new LinkedHashSet<>(list)) gets back a list.

For interviews: explain the LinkedHashMap backing and the doubly-linked before/after pointers, clarify that re-insertion does not move an element's position, describe the O(n) iteration advantage over HashSet due to linked-list traversal versus bucket scanning, and distinguish insertion order (LinkedHashSet) from sorted order (TreeSet).

What to Read Next

TopicLink
How HashSet provides the faster unordered alternative that LinkedHashSet extendsJava HashSet →
How LinkedHashMap provides the insertion-order hash table that backs LinkedHashSetJava HashMap →
How TreeSet provides sorted unique elements as an alternative to LinkedHashSetJava TreeMap →
How the Collections Framework organises all Set implementations in one hierarchyJava Collections Framework →
How Generics make LinkedHashSet type-safe for any element type at compile timeJava Generics →
Java LinkedHashSet | DevStackFlow