Definition
- List is an ordered, index-addressable collection that allows duplicates; implementations include
ArrayList,LinkedList, andCopyOnWriteArrayList - Set stores only unique elements determined by
equals()andhashCode(); implementations includeHashSet,LinkedHashSet, andTreeSet - The decision rule: can an element appear more than once, and do you need access by position? If yes, choose List. If each element must be unique and you mostly call
contains(), choose Set - Both extend
Collection, so you can pass either to any method that acceptsCollection
A List is the bank's transaction log. The same deposit amount can appear many times, and the order matters because auditors need to see transactions chronologically. A Set is the register of unique account numbers. Each account appears exactly once, and the main question the system asks is "does this account exist?" You would never use a List to track unique account numbers because you would have to scan the entire list every time to check for duplicates.
Understanding the Concepts
What is the difference between List and Set?
List and Set are both sub-interfaces of Collection, but they serve fundamentally different purposes.
A List is an ordered sequence of elements. It maintains insertion order, allows duplicate elements, and provides index-based access. You can ask "what is at position 5?" and get an answer in O(1) time with ArrayList.
A Set is a collection of unique elements. It does not allow duplicates – if you try to add an element that already exists (determined by equals()), the add() method returns false and the set remains unchanged. Sets do not provide index-based access.
| Feature | List | Set |
|---|---|---|
| Duplicates | Allowed | Not allowed |
| Ordering | Insertion order (always) | Depends on implementation |
| Index access | Yes – get(i), set(i, e) | No |
| Primary operation | Random access, iteration | Membership testing (contains) |
| contains() performance | O(n) for ArrayList | O(1) for HashSet |
| Implementations | ArrayList, LinkedList | HashSet, LinkedHashSet, TreeSet |
package com.bank.demo;
import java.math.BigDecimal;
import java.util.*;
public class ListVsSetDemo {
public static void main(String[] args) {
// List – transaction log (duplicates OK, order matters)
List<BigDecimal> transactionLog = new ArrayList<>();
transactionLog.add(new BigDecimal("50000.00"));
transactionLog.add(new BigDecimal("25000.00"));
transactionLog.add(new BigDecimal("50000.00")); // duplicate allowed
System.out.println("Transaction log: " + transactionLog);
System.out.println("Third entry: " + transactionLog.get(2)); // index access
// Set – unique account registry (no duplicates)
Set<String> activeAccounts = new HashSet<>();
activeAccounts.add("SAV2001");
activeAccounts.add("CUR3001");
boolean added = activeAccounts.add("SAV2001"); // returns false
System.out.println("Duplicate added: " + added);
System.out.println("Active accounts: " + activeAccounts);
}
}
Transaction log: [50000.00, 25000.00, 50000.00]
Third entry: 50000.00
Duplicate added: false
Active accounts: [CUR3001, SAV2001]
Why does contains() performance matter?
The biggest practical difference between List and Set is the performance of the contains() method. In a banking system that processes millions of transactions, this difference decides whether a lookup takes microseconds or seconds.
ArrayList.contains() performs a linear scan – it checks every element one by one until it finds a match or reaches the end. This is O(n). For a list of 1 million accounts, it checks up to 1 million elements.
HashSet.contains() computes the hash code, jumps directly to the right bucket, and checks one or a few elements. This is O(1) on average. For 1 million accounts, it checks 1-2 elements.
package com.bank.demo;
import java.util.*;
public class ContainsPerformanceDemo {
public static void main(String[] args) {
int size = 1_000_000;
List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
for (int i = 0; i < size; i++) {
String acc = "ACC" + String.format("%07d", i);
list.add(acc);
set.add(acc);
}
String target = "ACC0999999"; // last element
// ArrayList.contains – O(n) scan
long start = System.nanoTime();
boolean found1 = list.contains(target);
long listTime = System.nanoTime() - start;
// HashSet.contains – O(1) hash lookup
start = System.nanoTime();
boolean found2 = set.contains(target);
long setTime = System.nanoTime() - start;
System.out.println("ArrayList.contains: " + listTime + " ns");
System.out.println("HashSet.contains: " + setTime + " ns");
System.out.println("Set is ~" + (listTime / Math.max(setTime, 1)) + "x faster");
}
}
ArrayList.contains: 8542310 ns
HashSet.contains: 142 ns
Set is ~60156x faster
When should you use a List?
Use a List when any of these conditions are true:
- Duplicates are valid. A transaction log contains the same amount multiple times, and each occurrence is a different event.
- Order matters. Statements must show transactions in chronological order.
- Index access is needed. You need to jump to a specific position: "show me the 50th transaction."
- Iteration order must be predictable. You process elements in a specific sequence.
Banking examples:
- Transaction history for an account (ordered, duplicates valid)
- List of EMI payment amounts (same amount repeats monthly)
- Page of search results (ordered by relevance, indexed for pagination)
- Batch of pending transfers to process in sequence
package com.bank.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
public class TransactionService {
record Transaction(String id, String accountId, BigDecimal amount, LocalDate date) {}
public static void main(String[] args) {
// Transaction log – List because:
// 1. Same amount can appear multiple times (valid business data)
// 2. Chronological order is critical for statements
// 3. We paginate using index: get(50) through get(59)
List<Transaction> log = new ArrayList<>();
log.add(new Transaction("TXN001", "SAV2001", new BigDecimal("50000"), LocalDate.of(2024, 1, 15)));
log.add(new Transaction("TXN002", "SAV2001", new BigDecimal("25000"), LocalDate.of(2024, 1, 16)));
log.add(new Transaction("TXN003", "SAV2001", new BigDecimal("50000"), LocalDate.of(2024, 1, 17))); // same amount
// Pagination: show page 1 (items 0-1)
int pageSize = 2;
int page = 0;
List<Transaction> pageItems = log.subList(page * pageSize, Math.min((page + 1) * pageSize, log.size()));
pageItems.forEach(t -> System.out.println(t.id() + ": " + t.amount()));
}
}
TXN001: 50000
TXN002: 25000
When should you use a Set?
Use a Set when any of these conditions are true:
- Uniqueness is a business requirement. Each account number must appear exactly once.
- The primary operation is membership testing. "Does this account exist?" is called thousands of times per second.
- You need to eliminate duplicates. Converting a List to a Set removes duplicates automatically.
- You need mathematical set operations. Intersection, union, and difference are natural with Sets.
Banking examples:
- Active account number registry (uniqueness enforced)
- Set of blocked cards (fast lookup: "is this card blocked?")
- Set of countries for sanctions screening (fast membership check)
- Unique IP addresses that accessed an account (fraud detection)
package com.bank.service;
import java.util.*;
public class FraudDetectionService {
public static void main(String[] args) {
// Blocked accounts – Set because uniqueness is mandatory
Set<String> blockedAccounts = new HashSet<>();
blockedAccounts.add("SAV9999");
blockedAccounts.add("CUR8888");
// Fast membership check before processing transfer
String sourceAccount = "SAV2001";
if (blockedAccounts.contains(sourceAccount)) {
System.out.println("BLOCKED: " + sourceAccount);
} else {
System.out.println("OK: " + sourceAccount + " can proceed");
}
// Eliminate duplicates from transaction IPs
List<String> loginIPs = List.of("10.0.0.1", "10.0.0.2", "10.0.0.1", "10.0.0.3", "10.0.0.1");
Set<String> uniqueIPs = new HashSet<>(loginIPs);
System.out.println("Unique IPs: " + uniqueIPs);
System.out.println("Login attempts: " + loginIPs.size() + ", unique: " + uniqueIPs.size());
// Set operations – intersection (common IPs with another account)
Set<String> otherAccountIPs = Set.of("10.0.0.1", "10.0.0.5");
Set<String> commonIPs = new HashSet<>(uniqueIPs);
commonIPs.retainAll(otherAccountIPs); // intersection
System.out.println("Common IPs (fraud signal): " + commonIPs);
}
}
OK: SAV2001 can proceed
Unique IPs: [10.0.0.3, 10.0.0.1, 10.0.0.2]
Login attempts: 5, unique: 3
Common IPs (fraud signal): [10.0.0.1]
What if you need both uniqueness and ordering?
This is a common follow-up question. Java provides two Set implementations that maintain order:
- LinkedHashSet – maintains insertion order. Elements iterate in the order they were added. Useful when you need uniqueness but also need deterministic, predictable output (for audits, reports, tests).
- TreeSet – maintains sorted order. Elements iterate in natural order (or a custom Comparator). Useful for sorted unique data like account numbers in alphabetical order.
| Need | Use |
|---|---|
| Uniqueness only, no order needed | HashSet |
| Uniqueness + insertion order | LinkedHashSet |
| Uniqueness + sorted order | TreeSet |
| Ordered + duplicates + index access | ArrayList |
package com.bank.demo;
import java.util.*;
public class OrderedSetDemo {
public static void main(String[] args) {
// HashSet – no order guarantee
Set<String> hashSet = new HashSet<>();
hashSet.add("CUR3001");
hashSet.add("SAV2001");
hashSet.add("FD4001");
System.out.println("HashSet: " + hashSet);
// LinkedHashSet – insertion order preserved
Set<String> linkedSet = new LinkedHashSet<>();
linkedSet.add("CUR3001");
linkedSet.add("SAV2001");
linkedSet.add("FD4001");
System.out.println("LinkedHashSet: " + linkedSet);
// TreeSet – sorted order
Set<String> treeSet = new TreeSet<>();
treeSet.add("CUR3001");
treeSet.add("SAV2001");
treeSet.add("FD4001");
System.out.println("TreeSet: " + treeSet);
}
}
HashSet: [FD4001, CUR3001, SAV2001]
LinkedHashSet: [CUR3001, SAV2001, FD4001]
TreeSet: [CUR3001, FD4001, SAV2001]
What are common mistakes when choosing between List and Set?
Mistake 1: Using ArrayList for membership testing
// WRONG – O(n) contains on every transfer
List<String> blockedAccounts = new ArrayList<>(List.of("SAV9999", "CUR8888"));
if (blockedAccounts.contains(sourceAccount)) { } // scans entire list
// CORRECT – O(1) contains
Set<String> blockedAccountsSet = new HashSet<>(List.of("SAV9999", "CUR8888"));
if (blockedAccountsSet.contains(sourceAccount)) { } // hash lookup
Mistake 2: Using Set when you need index access
// WRONG – Set has no get(index)
Set<String> accounts = new HashSet<>();
// accounts.get(0); // compile error – Set has no get() method
// CORRECT – use List for index access
List<String> accountList = new ArrayList<>(accounts);
String first = accountList.get(0);
Mistake 3: Forgetting that Set uses equals/hashCode
// If your class does not override equals/hashCode,
// two objects with the same data are treated as different
class Account {
String id;
Account(String id) { this.id = id; }
// No equals/hashCode override!
}
Set<Account> accounts = new HashSet<>();
accounts.add(new Account("SAV2001"));
accounts.add(new Account("SAV2001")); // treated as different!
System.out.println(accounts.size()); // 2 – not 1!
What about thread safety?
Neither ArrayList nor HashSet is thread-safe. For concurrent access:
- List: use
CopyOnWriteArrayList(read-heavy) orCollections.synchronizedList()(write-heavy). - Set: use
ConcurrentHashMap.newKeySet()(Java 8+) orCollections.synchronizedSet().
import java.util.*;
import java.util.concurrent.*;
// Thread-safe Set using ConcurrentHashMap
Set<String> concurrentSet = ConcurrentHashMap.newKeySet();
concurrentSet.add("SAV2001");
concurrentSet.add("CUR3001");
// Safe for concurrent add/remove/contains without external sync
What should a senior engineer know?
- Memory footprint. A HashSet with 1000 elements uses roughly 48 KB (because it wraps a HashMap internally – each element becomes a key with a dummy value). An ArrayList with 1000 elements uses roughly 4 KB (just the reference array). If memory is tight and you only need iteration, not fast contains, ArrayList is cheaper.
- Set.of() iteration order. Set.of() (Java 9+) does not guarantee any specific iteration order, and the order can change between JVM runs. Do not rely on it for deterministic output.
- EnumSet. When your set contains enum constants, always use EnumSet instead of HashSet. It uses a bit vector internally and is faster and uses less memory than any other Set implementation.
- Converting between List and Set. new ArrayList<>(set) converts a Set to a List. new HashSet<>(list) converts a List to a Set (removing duplicates). new LinkedHashSet<>(list) preserves order while removing duplicates.
- Set operations. retainAll() is intersection. addAll() is union. removeAll() is difference. These are defined on Collection but most useful with Sets. They modify the calling set in place.
Visual Overview
Decision tree: List or Set?
Can the same element appear more than once?
├── YES → Do you need index access (get by position)?
│ ├── YES → ArrayList
│ └── NO → Do you need fast add/remove at ends?
│ ├── YES → LinkedList
│ └── NO → ArrayList (default)
│
└── NO (unique elements only)
→ Do you need sorted order?
├── YES → TreeSet (O(log n) ops)
└── NO → Do you need insertion order?
├── YES → LinkedHashSet
└── NO → HashSet (O(1) ops, default)
Key Takeaway
The choice between List and Set comes down to two questions: can the same element appear more than once, and do you need positional access? If duplicates are valid business data and you need to access elements by index, use a List. If each element must be unique and your primary operation is asking "does this element exist," use a Set.
In the National Bank of India system, Rajesh uses an ArrayList for the transaction history of account SAV2001 because the same deposit amount of 50,000 rupees can appear multiple times, each representing a different event, and auditors need to see transactions in chronological order. For the active account registry, he uses a HashSet because each account number must be unique and the system checks "does this account exist" thousands of times per second. HashSet answers that question in O(1) time, while ArrayList would scan every element.
When you need both uniqueness and ordering, LinkedHashSet gives you insertion order and TreeSet gives you sorted order. When you need uniqueness and concurrency, use ConcurrentHashMap.newKeySet(). Always remember that a Set relies on equals() and hashCode() – if your class does not override both correctly, two objects with identical data will be treated as different.
Remember one sentence: List is for ordered data with duplicates and index access; Set is for unique data with fast membership testing.
---