How did the Collections Framework Evolve in Java? #
Early Collections
- Early classes for grouping objects included Dictionary, Vector, Stack, and Properties.
- These classes had an ad-hoc design.
- There was no consistent set of interfaces.
Collections Framework
- The Collections Framework was introduced in Java 1.2.
- It had two main goals:
- Unify collection handling through standard interfaces and implementations
- Improve performance, flexibility, and ease of use
Evolution
- Over time, the framework evolved with new features:
- Java 5: Generics, enhanced for-each loop, concurrent collections (e.g.,
ConcurrentHashMap
) - Java 8: Stream API,
forEach
, lambda expressions - Java 9: Factory methods for immutable collections (
List.of()
,Set.of()
,Map.of()
) - Java 21: Sequenced collections for consistent ordering across types
- Java 5: Generics, enhanced for-each loop, concurrent collections (e.g.,
Why are Collections needed in Java? #
Collections in Java provide a flexible and efficient way to store, retrieve, and manipulate data.
Limitations of Arrays:
- Fixed Size – Cannot grow or shrink dynamically.
- Manual Operations – Searching, adding, or removing elements requires extra logic
- Lack of Built-in Methods – No built-in sorting, searching, or advanced data structures.
How Collections Solve These Problems:
- Dynamic Resizing – Can grow or shrink as needed.
- Predefined Methods – Supports adding, removing, sorting, and searching elements easily.
- Rich Data Structures – Includes Lists, Sets, Maps, and Queues for different needs.
Example – Array vs. Collection
String[] names = new String[3];
names[0] = "Alice";
names[1] = "Bob";
names[2] = "Charlie";
// Adding a new name?
//❌ Requires creating a new array.
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add("David"); // ✅ No need to resize manually
Can you give a 10,000 feet overview of interfaces in collection framework? #
- Collection is at the root of the hierarchy (add element, remove element, ..)
- SequencedCollection provides well-defined order (add first, add last, ..)
- List stores ordered elements, allows duplicates, supports index (add element e at index 3)
- Set holds unique elements; no duplicates allowed
- SortedSet maintains elements in sorted order
- NavigableSet adds search/navigation methods
- Queue is used for FIFO processing of elements
- Deque is a double-ended queue (
SequencedCollection
)
- Map stores key-value pairs; keys must be unique (Does not inherit from
Collection
interface)- SortedMap maintains elements in sorted order
- NavigableMap adds search/navigation methods
What are the main interfaces in the Collections Framework? #
NOTE: This is provided as a quick reference. We will not have a video discussing this question. We will discuss all the interfaces in videos for subsequent questions
Name | Description | Important Operations | Extends |
---|---|---|---|
Collection | Root interface for collections hierarchy. | add(e) , remove(e) , size() , iterator() , contains(e) , .. |
Iterable |
Sequenced Collection | A collection that has a well-defined encounter order, that supports operations at both ends, and that is reversible. | Inherited + addFirst(E e) , addLast(E e) , getFirst() , getLast() , removeFirst() , removeLast() , reversed() , .. |
Collection |
List | Ordered collection allowing duplicates. Access using index (positioned). | Inherited + get(int index) , set(int index, E element) , void add(int index, E element) , remove(int index) , indexOf(Object element) ,... |
Sequenced Collection |
Set | Collection of unique elements (NO duplicates) | Inherited Methods | Collection |
Sequenced Set | A collection that is both a Sequenced Collection and a Set |
`Inherited Methods | Sequenced Collection , Set |
SortedSet | Set maintaining elements in sorted order | first() , last() , subSet() , headSet() , tailSet() |
Set , Sequenced Set |
NavigableSet | Sorted set with navigation methods (find closest match elements) | lower(e) , higher(e) , floor(e) , ceiling(e) |
SortedSet |
Queue | Collection that follows FIFO order. | offer(e) , poll() , peek() |
Collection |
Deque | Double-ended queue allowing insertions/removals at both ends | Queue + SequencedCollection |
Queue , Sequenced Collection |
Blocking Queue | Thread-safe queue that blocks when full (on put() ) or empty (on take() ) |
put(e) , take() , offer(e, timeout) , poll(timeout) |
Queue |
Map | Stores key-value pairs | put(k,v) , get(k) , remove(k) |
(DOES NOT extend Collection) |
Sequenced Map | A Map that has a well-defined encounter order, that supports operations at both ends, and that is reversible. | reversed() , firstEntry() , lastEntry() , pollFirstEntry() , pollLastEntry() , putFirst(k, v) , putLast(k, v) , sequencedKeySet() , sequencedValues() , sequencedEntrySet() |
Map |
SortedMap | Maintains keys in sorted order | firstKey() , lastKey() , subMap() , headMap() , tailMap() |
Sequenced Map |
NavigableMap | SortedMap with navigation methods |
lowerKey(k) , higherKey(k) ,lowerEntry(k) , higherEntry(k) |
Sorted Map |
What are the key methods in the Collection interface? #
Collection is root interface of the collection framework!
Key Methods
add(E element)
: Adds an element to the collection.remove(Object element)
: Removes an element from the collection.size()
: Returns the number of elements in the collection.isEmpty()
: Checks if the collection is empty.clear()
: Removes all elements from the collection.contains(Object element)
: Checks if an element exists in the collection.containsAll(Collection<?> c)
: Checks if all elements in another collection exist.addAll(Collection<? extends E> c)
: Adds all elements from another collection.removeAll(Collection<?> c)
: Removes all matching elements.retainAll(Collection<?> c)
: Keeps only elements present in another collection.iterator()
: Returns an iterator to traverse elements.stream()
: Returns a stream using the elements in the collectionparallelStream()
: Returns a parallel stream using the elements in the collection
interface Collection<E> extends Iterable<E> {
boolean add(E element);
boolean remove(Object element);
int size();
boolean isEmpty();
void clear();
boolean contains(Object element);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
Iterator<E> iterator();
Stream<E> stream();
Stream<E> parallelStream();
//...
//...
}
What are the key methods in the SequencedCollection interface? #
Introduced in JDK 21, SequencedCollection extends Collection Interface. It represents a collection that has a well-defined encounter order, that supports operations at both ends, and that is reversible.
Key Methods
addFirst(E e)
: Adds an element at the beginning of the collection.addLast(E e)
: Adds an element at the end of the collection.getFirst()
: Retrieves the first element of the collection.getLast()
: Retrieves the last element of the collection.removeFirst()
: Removes and returns the first element of the collection.removeLast()
: Removes and returns the last element of the collection.reversed()
: Returns a view of the collection with elements in reverse order.
public interface SequencedCollection<E> extends Collection<E> {
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
SequencedCollection<E> reversed();
}
What are the key methods in the List interface? #
- Extension of SequencedCollection
List
extendsSequencedCollection
, inheriting all its methods.- Provides additional methods for position-based operations.
- A list may have duplicate elements.
- Insertion Order is Maintained
- Maintains the insertion order of elements.
- Elements are stored in the order they are added.
- Inserting Elements
- Use
add(int position, E element)
to insert at a specific position. - Elements added without position go to the end.
- Use
Example:
List<Integer> numbers = new ArrayList<>();
// Insertion Order is Maintained
numbers.add(10); // index 0
numbers.add(20); // index 1
numbers.add(30); // index 2
System.out.println(numbers); // [10, 20, 30]
// Inserting Elements - position-based operation
numbers.add(1, 15); // insert 15 at index 1
System.out.println(numbers); // [10, 15, 20, 30]
// Duplicates allowed
numbers.add(20); // add duplicate
System.out.println(numbers); // [10, 15, 20, 30, 20]
// Fast lookup using index (O(1))
System.out.println(numbers.get(2)); // 20
interface List<E> extends SequencedCollection<E> {
boolean addAll(int index, Collection<? extends E> c);
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object element);
int lastIndexOf(Object element);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);
static <E> List<E> of(); //Static methods added in JDK 9
}
How do you decide which List implementation to use? #
📌 1. ArrayList – Best for Fast Random Access
Key Characteristics:
- Uses a dynamic array internally
- Maintains insertion order of elements
- Supports indexed access to elements
When to Use:
- Fast element access via
get(index)
- Insertions/removals mostly at the end
- You don’t need thread safety
When to Avoid:
- Frequent insertions or deletions in the middle
- Large shifts in elements (reallocation is expensive)
📌 2. LinkedList – Best for Frequent Insertions/Deletions
Key Characteristics:
- Uses a doubly linked list internally
- Each element has references to the next and previous elements
When to Use:
- Frequent insertions or deletions in the middle
When to Avoid:
- Fast random access (
get(index)
) is needed - Memory overhead is a concern (extra references per element)
📌 3. Vector – Best for Thread-Safe Lists
Key Characteristics:
- Uses a dynamic array like
ArrayList
- All methods are synchronized (thread-safe)
- Slower than
ArrayList
due to synchronization overhead
When to Use:
- Multiple threads access the list simultaneously
- Working with legacy code that already uses
Vector
When to Avoid:
- Thread safety is not required (
ArrayList
is faster) - Performance is important (synchronization adds overhead)
📌 4. CopyOnWriteArrayList – Best for Read-Mostly, Thread-Safe Scenarios
Key Characteristics:
- Thread-safe variant of
ArrayList
- Creates a new copy of the array on each write (add/remove)
- Safe to iterate even during concurrent modifications
When to Use:
- Concurrent, read-heavy scenarios
When to Avoid:
- Frequent writes or large lists (copying is expensive)
How do you choose the right approach to sort a List? #
📌 1. Using Collections.sort()
– Best for Simple Sorting
- Uses natural ordering (e.g., alphabetical for Strings, ascending for numbers).
- Modifies the original list in place.
When to Use:
- Sorting a list of Strings or primitive types.
- Sorting objects that implement
Comparable
.
Example:
List<String> numbers = new ArrayList<>();
numbers.add("one");
numbers.add("two");
numbers.add("three");
numbers.add("four");
Collections.sort(numbers);
System.out.println(numbers); // [four, one, three, two]
📌 2. Using Comparable
– Best for Natural Ordering
- Implement
Comparable<T>
in the class. - Used when objects have a default sorting order (e.g., sorting players by runs).
When to Use:
- When an object has a natural ordering (e.g., sorting Cricketers by runs).
- If the class controls its sorting logic.
Example:
class Cricketer implements Comparable<Cricketer> {
int runs;
String name;
public Cricketer(String name, int runs) {
this.name = name;
this.runs = runs;
}
@Override
public int compareTo(Cricketer that) {
return Integer.compare(this.runs, that.runs);
}
@Override
public String toString() {
return name + " " + runs;
}
}
List<Cricketer> players = Arrays.asList(
new Cricketer("Virat", 12200),
new Cricketer("Rohit", 11600),
new Cricketer("Gill", 2400),
new Cricketer("Dhoni", 10700)
);
// Sort using Comparable (ascending by runs)
Collections.sort(players);
📌 3. Using Comparator
– Best for Custom Sorting
- Implement
Comparator<T>
separately from the class. - Allows sorting by different criteria (e.g., descending order).
When to Use:
- When multiple sorting orders are needed (e.g., sorting by name, then by runs).
- When modifying the original class is not possible.
Example:
import java.util.Comparator;
class DescendingSorter implements Comparator<Cricketer> {
@Override
public int compare(Cricketer c1, Cricketer c2) {
return Integer.compare(c2.runs, c1.runs);
}
}
List<Cricketer> players = Arrays.asList(
new Cricketer("Virat", 12200),
new Cricketer("Rohit", 11600),
new Cricketer("Gill", 2400),
new Cricketer("Dhoni", 10700)
);
// Sort using Comparator (descending by runs)
Collections.sort(players, new DescendingSorter());
📌 4. Comparison of Sorting Approaches
Approach | When to Use | Modifies Original Class? |
---|---|---|
Collections.sort() |
Simple sorting of Strings/numbers | No |
Comparable |
When objects have a default sorting order | Yes (compareTo() ) |
Comparator |
For custom sorting orders | No |
What are the key interfaces associated with Set operations? #
- Set – Extends
Collection
interface. Does not allow duplicates. - SequencedSet - A collection that is both a
Sequenced Collection
and aSet
. Represents a collection of unique elements with a well-defined order. - SortedSet – Extends
SequencedSet
and maintains sorted order. Provides methods likefirst()
,last()
,subSet()
,headSet()
,tailSet()
- NavigableSet – Extends
SortedSet
and provides navigation methods likelower()
,higher()
,floor()
,ceiling()
.
//Set - NO DUPLICATES
Set<String> simpleSet = new HashSet<>();
simpleSet.add("Apple");
simpleSet.add("Banana");
simpleSet.add("Apple"); // Ignored
// Set: [Banana, Apple]
System.out.println("Set: " + simpleSet);
// SequencedSet: SET + ORDER
SequencedSet<String> sequencedSet = new LinkedHashSet<>();
sequencedSet.add("One");
sequencedSet.add("Two");
sequencedSet.add("Three");
// No output, modifies set
sequencedSet.addFirst("Zero"); // Java 21+
// No output, modifies set
sequencedSet.addLast("Four"); // Java 21+
// SeqSet: [Zero, One, Two, Three, Four]
System.out.println("SeqSet: " + sequencedSet);
// First: Zero
System.out.println("First: " + sequencedSet.getFirst());
// Last: Four
System.out.println("Last: " + sequencedSet.getLast());
// SortedSet: SEQUENCED SET + SORTED ORDER
SortedSet<Integer> sortedSet = new TreeSet<>();
sortedSet.add(30);
sortedSet.add(10);
sortedSet.add(20);
// Sorted: [10, 20, 30]
System.out.println("Sorted: " + sortedSet);
// First: 10
System.out.println("First: " + sortedSet.first());
System.out.println("First: " + sortedSet.getFirst());
// Last: 30
System.out.println("Last: " + sortedSet.last());
System.out.println("Last: " + sortedSet.getLast());
// HeadSet: [10]
System.out.println("HeadSet: " + sortedSet.headSet(20));
// TailSet: [20, 30]
System.out.println("TailSet: " + sortedSet.tailSet(20));
// SubSet: [10, 20]
System.out.println("SubSet: " + sortedSet.subSet(10, 30));
//NavigableSet - SORTED SET + NAVIGATION
NavigableSet<Integer> navSet = new TreeSet<>();
navSet.add(10);
navSet.add(20);
navSet.add(30);
// NavSet: [10, 20, 30]
System.out.println("NavSet: " + navSet);
// Lower: 10
System.out.println("Lower: " + navSet.lower(20));
// Floor: 20
System.out.println("Floor: " + navSet.floor(20));
// Higher: 30
System.out.println("Higher: " + navSet.higher(20));
// Ceiling: 20
System.out.println("Ceiling: " + navSet.ceiling(20));
interface Set<E> extends Collection<E> {
// No duplicate elements allowed
// No ordering guaranteed
}
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed();
void addFirst(E e); //From SequencedCollection
void addLast(E e); //From SequencedCollection
E getFirst(); //From SequencedCollection
E getLast(); //From SequencedCollection
E removeFirst(); //From SequencedCollection
E removeLast(); //From SequencedCollection
}
interface SortedSet<E> extends SequencedSet<E> {
SortedSet<E> subSet(E start, E end); // start to end-1
SortedSet<E> headSet(E end); // < end
SortedSet<E> tailSet(E start); // >= start
E first();
E last();
}
interface NavigableSet<E> extends SortedSet<E> {
E lower(E e); //largest x such that x < e
E floor(E e); //largest x such that x <= e
E ceiling(E e);//smallest x such that x >= e
E higher(E e); //smallest x such that x > e
E pollFirst(); //return and remove first element
E pollLast(); //return and remove last element
}
How do you decide which
Set
implementation to use? #
📌 1. HashSet – Best for Fast Lookups
Key Characteristics:
- Unordered collection
- Does not allow duplicate elements
When to Use:
- Fast lookups and uniqueness are priorities
- You don’t care about the order of elements
When to Avoid:
- You need to preserve insertion or sorted order
- You need thread safety
📌 2. LinkedHashSet – Best for Maintaining Insertion Order
Key Characteristics:
- Extends
HashSet
- Maintains insertion order
- Slightly slower than
HashSet
When to Use:
- You want predictable iteration order
- Need fast operations + ordering
When to Avoid:
- Order doesn’t matter (
HashSet
is faster) - You need sorting (
TreeSet
is more appropriate) - You need thread safety
📌 3. TreeSet – Best for Sorted Elements
Key Characteristics:
- Implements
NavigableSet
- Maintains elements in natural or custom sort order
- Backed by a Red-Black Tree
When to Use:
- You need sorted elements
- You need Range-based queries (subSet, headSet, tailSet)
When to Avoid:
- Sorting is unnecessary (
HashSet
is faster) - You need thread safety
TreeSet<Integer> set = new TreeSet<>();
set.add(55); set.add(25); set.add(35);
set.add(5); set.add(45);
System.out.println(set); // [5, 25, 35, 45, 55]
System.out.println(set.first()); // 5
System.out.println(set.last()); // 55
System.out.println(set.subSet(10, 50)); // [25, 35, 45]
System.out.println(set.headSet(35)); // [5, 25]
System.out.println(set.tailSet(35)); // [35, 45, 55]
System.out.println(set.lower(25)); // 5
System.out.println(set.floor(25)); // 25
System.out.println(set.higher(25)); // 35
System.out.println(set.ceiling(25)); // 25
📌 4. CopyOnWriteArraySet – Best for Read-Mostly, Thread-Safe Scenarios
Key Characteristics:
- Thread-safe Set implementation
- All write operations (add/remove) create a new copy
When to Use:
- Many reads, few writes
- You need thread-safety
When to Avoid:
- Frequent writes or large data sets (copying is expensive)
📌 5. ConcurrentSkipListSet – Best for Concurrent & Sorted Access
Key Characteristics:
- Thread-safe version of
TreeSet
- Backed by a concurrent skip list
- Maintains sorted order
When to Use:
- Multi-threaded applications needing sorted data
When to Avoid:
- Single-threaded scenarios (overhead is unnecessary)
What are the main interfaces related to Queue? #
Interface | Description | Key Methods | Extends |
---|---|---|---|
Queue | Collection that holds elements for processing in FIFO order | offer(e) , poll() , peek() |
Collection |
Deque | Double-ended queue allowing insertion/removal from both ends | addFirst(e) , removeLast() , peekFirst() |
Queue ,SequencedCollection |
BlockingQueue | Thread-safe queue that blocks when full/empty | put(e) , take() , poll(timeout) |
Queue |
Queue Interface
- Extends the
Collection
interface. - Used for holding elements in order for processing.
- Key Methods:
peek()
: Retrieves the head without removal.poll()
: Retrieves and removes the head.
// Queue example using LinkedList
Queue<String> queue = new LinkedList<>();
queue.offer("A");
queue.offer("B");
// A (peek without remove)
System.out.println(queue.peek());
// A (remove and return)
System.out.println(queue.poll());
// B (now at head)
System.out.println(queue.peek());
interface Queue<E> extends Collection<E> {
// inserts element, exception if full
boolean add(E e);
// inserts element, returns false if full
boolean offer(E e);
// removes head, exception if empty
E remove();
// removes head, null if empty
E poll();
// gets head, exception if empty
E element();
// gets head, null if empty
E peek();
}
Deque Interface
- Extends
Queue
&SequencedCollection
interfaces - Supports insertion and removal from both ends.
// Deque example using ArrayDeque
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("B");
deque.addLast("C");
deque.addFirst("A");
// A
System.out.println(deque.peekFirst());
// C
System.out.println(deque.peekLast());
// A (remove front)
System.out.println(deque.removeFirst());
// C (remove end)
System.out.println(deque.removeLast());
public interface Deque<E>
extends Queue<E>, SequencedCollection<E> {
// add to front, exception if full
void addFirst(E e);
// add to end, exception if full
void addLast(E e);
// add to front, false if full
boolean offerFirst(E e);
// add to end, false if full
boolean offerLast(E e);
// remove from front, exception if empty
E removeFirst();
// remove from end, exception if empty
E removeLast();
// remove from front, null if empty
E pollFirst();
// remove from end, null if empty
E pollLast();
// get front, exception if empty
E getFirst();
// get end, exception if empty
E getLast();
// get front, null if empty
E peekFirst();
// get end, null if empty
E peekLast();
// remove first match
boolean removeFirstOccurrence(Object o);
// remove last match
boolean removeLastOccurrence(Object o);
}
BlockingQueue Interface
- A thread-safe queue designed for use in producer-consumer scenarios
- Blocks:
- On
put(e)
if the queue is full - On
take()
if the queue is empty
- On
// BlockingQueue example with ArrayBlockingQueue
// Queue with capacity 2
BlockingQueue<String> bq =
new ArrayBlockingQueue<>(2);
//Produce elements
bq.put("A");
bq.put("B");
// The queue is now full
// The next line will block if uncommented
// bq.put("C");
// Consume one element
System.out.println(bq.take()); // A
// Add another element (now space is available)
bq.put("C");
// Consume remaining elements
System.out.println(bq.take()); // B
System.out.println(bq.take()); // C
interface BlockingQueue<E> extends Queue<E> {
// inserts, waits if full
void put(E e) throws InterruptedException;
// inserts with timeout, waits if full
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
// removes, waits if empty
E take() throws InterruptedException;
// removes with timeout, waits if empty
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
// remaining capacity in queue
int remainingCapacity();
// removes all into given collection
int drainTo(Collection<? super E> c);
// removes up to maxElements into given collection
int drainTo(Collection<? super E> c, int maxElements);
}
What are the important implementations of Queue? #
Name | Description | Extends | Implements |
---|---|---|---|
PriorityQueue | Queue where elements are sorted by priority | AbstractQueue |
Queue |
ArrayDeque | Resizable array-based Deque |
AbstractCollection |
Deque |
ArrayBlockingQueue | Array-backed blocking queue (Fixed Size) | AbstractQueue |
BlockingQueue |
LinkedBlockingQueue | Linked-list blocking queue | AbstractQueue |
BlockingQueue |
ConcurrentLinkedQueue | Lock-free thread-safe queue | AbstractQueue |
Queue |
ConcurrentLinkedDeque | Lock-free thread-safe deque | AbstractCollection |
Deque |
What are the key interfaces related to Map? #
Here are the Map-related interfaces in Java:
1. Map Interface
- What? A collection that stores key-value pairs.
- Why? Allows quick lookup and modification of values using keys.
- Key Characteristics:
- Does not extend
Collection
. - Keys are unique, but values can be duplicate.
- Uses
Entry
objects to store key-value pairs.
- Does not extend
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 25);
map.put("Bob", 30);
System.out.println(map.get("Alice")); // Output: 25
Important Methods:
Method | Description |
---|---|
put(K key, V value) |
Adds a key-value pair. |
get(Object key) |
Retrieves the value for a given key. |
remove(Object key) |
Removes a key-value pair. |
containsKey(Object key) |
Checks if a key exists. |
containsValue(Object value) |
Checks if a value exists. |
keySet() |
Returns all keys. |
values() |
Returns all values. |
entrySet() |
Returns all key-value pairs. |
2. SequencedMap Interface
- What? A
Map
that maintains a well-defined sequence of entries - Why? Allows consistent iteration order and reverse traversal
Key Characteristics:
- Extends
Map
- Maintains the insertion or access order (depending on implementation)
- Adds methods to get/remove first and last entries
- Useful when both ordering and key-based access are important
SequencedMap<String, Integer> seqMap = new LinkedHashMap<>();
seqMap.put("A", 1);
seqMap.put("B", 2);
seqMap.put("C", 3);
System.out.println(seqMap.firstEntry()); // A=1
System.out.println(seqMap.lastEntry()); // C=3
Important Methods:
Method | Description |
---|---|
firstEntry() |
Returns the first entry in sequence |
lastEntry() |
Returns the last entry in sequence |
pollFirstEntry() |
Removes and returns the first entry |
pollLastEntry() |
Removes and returns the last entry |
reversed() |
Returns a reversed view of the map |
putFirst(K key, V value) |
Inserts entry at the beginning of sequence |
putLast(K key, V value) |
Inserts entry at the end of sequence |
3. SortedMap Interface
- What? A
Map
that maintains keys in sorted order. - Why? Useful when sorting keys automatically.
- Key Characteristics:
- Extends
SequencedMap
. - Keys are sorted in natural order or by a
Comparator
.
- Extends
SortedMap<Integer, String> sortedMap = new TreeMap<>();
sortedMap.put(3, "C");
sortedMap.put(1, "A");
sortedMap.put(2, "B");
System.out.println(sortedMap); // Output: {1=A, 2=B, 3=C}
Important Methods:
Method | Description |
---|---|
firstKey() |
Returns the first (lowest) key. |
lastKey() |
Returns the last (highest) key. |
subMap(fromKey, toKey) |
Returns a portion of the map. |
headMap(toKey) |
Returns view of map with keys < toKey |
tailMap(fromKey) |
Returns view of map with keys ≥ fromKey |
4. NavigableMap Interface
- What? An extension of
SortedMap
with navigation methods. - Why? Provides methods to find the closest matches for keys.
- Key Characteristics:
- Allows floor, ceiling, higher, and lower key lookups.
- Supports reverse order traversal.
NavigableMap<Integer, String> navMap = new TreeMap<>();
navMap.put(1, "A");
navMap.put(3, "C");
navMap.put(5, "E");
System.out.println(navMap.floorKey(4)); // Output: 3
System.out.println(navMap.ceilingKey(4)); // Output: 5
Important Methods:
Method | Description |
---|---|
floorKey(K key) |
Returns the largest key ≤ given key |
ceilingKey(K key) |
Returns the smallest key ≥ given key |
lowerKey(K key) |
Returns the largest key < given key |
higherKey(K key) |
Returns the smallest key > given key |
floorEntry(K key) |
Returns entry with the largest key ≤ given key |
ceilingEntry(K key) |
Returns entry with the smallest key ≥ given key |
lowerEntry(K key) |
Returns entry with the largest key < given key |
higherEntry(K key) |
Returns entry with the smallest key > given key |
descendingMap() |
Returns a reverse-order view of the map |
What are the key implementations of Map Interface? #
Name | Description | Extends | Implements |
---|---|---|---|
HashMap | Unordered key-value store (NOT thread safe) | AbstractMap | Map |
LinkedHashMap | Map with insertion order (NOT thread safe) | HashMap | SequencedMap |
TreeMap | Sorted key-value store (Red-Black tree)(NOT thread safe) | AbstractMap | NavigableMap |
ConcurrentHashMap | Thread-safe map with high concurrency | AbstractMap | ConcurrentMap (Map) |
ConcurrentSkipListMap | Thread-safe, sorted map (Keys are sorted) | AbstractMap | ConcurrentNavigableMap (NavigableMap) |
What is the purpose of the Collections class? #
The Collections
class provides utility methods for working with collections:
Feature | Method | Purpose |
---|---|---|
Sorting | sort(List) |
Sorts a list in natural order. |
Searching | binarySearch(List, key) |
Finds an element in a sorted list. |
Thread Safety | synchronizedList(List) |
Creates a thread-safe list. |
Unmodifiable Collections | unmodifiableList(List) |
Makes a list read-only. |
Finding Min/Max | min(Collection) , max(Collection) |
Gets the smallest or largest element. |
Shuffling/Reversing | shuffle(List) , reverse(List) |
Randomizes or reverses order. |
import java.util.*;
public class CollectionsExample {
public static void main(String[] args) {
// Create a list of numbers
List<Integer> numbers
= new ArrayList<>(Arrays.asList(10, 5, 20, 15));
// Sort the list
Collections.sort(numbers);
// Output: [5, 10, 15, 20]
System.out.println("Sorted List: " + numbers);
// Find the minimum and maximum values
int min = Collections.min(numbers);
int max = Collections.max(numbers);
// Output: Min: 5, Max: 20
System.out.println("Min: " + min + ", Max: " + max);
// Reverse the list
Collections.reverse(numbers);
// Output: [20, 15, 10, 5]
System.out.println("Reversed List: " + numbers);
// Shuffle the list
Collections.shuffle(numbers);
// Output: Random order
System.out.println("Shuffled List: " + numbers);
// Perform binary search (List must be sorted first)
Collections.sort(numbers);
int index = Collections
.binarySearch(numbers, 15);
// Output: Index position of 15
System.out.println("Index of 15: " + index);
// Make the list unmodifiable
List<Integer> unmodifiableList
= Collections.unmodifiableList(numbers);
System.out.println(
"Unmodifiable List: " + unmodifiableList);
// Uncommenting below line
// will throw UnsupportedOperationException
// unmodifiableList.add(25);
}
}
How do Synchronized and Concurrent Collections differ? #
Feature | Synchronized Collections | Concurrent Collections |
---|---|---|
Synchronization Method | Uses synchronized methods & blocks | Uses modern concurrency mechanisms like locks and non-blocking algorithms |
Performance | Slower due to full synchronization | Faster as multiple threads can work concurrently |
Thread Safety | Only one thread can access the collection at a time | Multiple threads can safely modify the collection |
Examples | Vector , Hashtable , Collections. synchronizedList() |
ConcurrentHashMap , CopyOnWriteArrayList , ConcurrentLinkedQueue |
Best Use Case | When multiple threads rarely modify the collection | When multiple threads frequently modify the collection |
What is the CopyOnWrite approach in Concurrent Collections? #
- What? A technique where modifications create a new copy of the collection instead of modifying the existing one.
- Why? Ensures thread safety without synchronization during read operations.
- How?
- Stores values in an immutable internal array.
- When modified, a new array is created with the updated data.
- Read operations are fast and do not block, only write operations are synchronized.
- Best Use Case: When reads outnumber writes
- Example Implementations:
CopyOnWriteArrayList
,CopyOnWriteArraySet
.
What is the Compare-And-Swap (CAS) approach? #
- What? A low-level, non-blocking synchronization technique used for thread-safe updates
- Why? Avoids locking, improving performance in high-concurrency scenarios
- How it works:
- Read the current value (expected value)
- Compute the new value
- Compare the current value with the expected value
- If they match, update to the new value
- If not, retries until it succeeds or exits (in very rare cases)
- Used In:
ConcurrentLinkedQueue
,AtomicInteger
etc..
In what scenarios does a Java Collection throw UnsupportedOperationException? #
- What? Thrown when an operation is not supported by a collection.
- Fixed Size Lists
- Example:
Arrays.asList()
- Operations that change the size of a list, like add()/remove(), throw
UnsupportedOperationException
- Operations that do not change the size of the list, like set(), are allowed
- Example:
- Unmodifiable Collections
- Example:
List.of()
,Set.of()
,Collections.unmodifiableList()
- All mutation operations (like
add()
,remove()
,set()
, clear()) throw
UnsupportedOperationException`
- Example:
Example Code:
List<String> list = Arrays.asList("A", "B");
list.add("C"); // Throws UnsupportedOperationException
List<String> immutable = List.of("X", "Y");
immutable.remove(0); // Throws UnsupportedOperationException
What is the difference between Fail-Fast and Fail-Safe Iterators? #
1. What is a Fail-Fast Iterator?
- What? Throws
ConcurrentModificationException
if the collection is modified during iteration. - Where? Used in non-thread-safe collections like
ArrayList
,HashMap
, etc. - When? Happens when a structural modification (add/remove) occurs outside the iterator.
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class FailFast {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
System.out.println(map.get(iterator.next()));
// Throws ConcurrentModificationException
map.put("key4", "value4");
}
}
}
2. What is a Fail-Safe Iterator?
- What? Does not throw exceptions on modification.
- Where? Used in concurrent collections (
ConcurrentHashMap
,CopyOnWriteArrayList
). - Why? Works on a separate copy of the data, ensuring safe iteration.
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
public class FailSafe {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map
= new ConcurrentHashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
map.put("key3", "value3");
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
System.out.println(map.get(iterator.next()));
map.put("key4", "value4"); // No Exception
}
}
}
3. Fail-Fast vs Fail-Safe
Feature | Fail-Fast | Fail-Safe |
---|---|---|
Modification | Throws Concurrent Modification Exception |
No exception |
Thread Safety | Not safe | Thread-safe |
Works On | Direct collection | A copy of the collection |
Examples | ArrayList , HashMap |
ConcurrentHashMap , CopyOnWriteArrayList |
What are some best practices for using Collections in Java? #
1. Choose the Right Collection Type
- Invest time to decide the right List, Map, Queue, Set or Map to use
2. Set Initial Capacity to Reduce Resizing
- Prevents frequent resizing and improves performance.
// Avoids unnecessary resizing
Map<String, Integer> map = new HashMap<>(100);
3. Use the Right Data Structure for Thread Safety
- Use ConcurrentHashMap instead of
HashMap
in multi-threaded environments. - Use CopyOnWriteArrayList for read-heavy operations with rare writes.
4. Use Immutable Collections When Possible
- Prevents accidental modification and improves safety.
- Use
Collections.unmodifiableList()
orList.of()
.
// Immutable List
List<String> names =
List.of("Alice", "Bob", "Charlie");
5. Prefer Iterators or Streams Over Manual Loops
- Improves readability and avoids
IndexOutOfBoundsException
.
// More readable than a for-loop
list.forEach(System.out::println);
Why are Sequenced Collections introduced? #
- Problem: Getting/Modifying first/last elements is tricky
- Solution: Java 21 added
SequencedCollection
interface
Sequenced Collection Interfaces
interface SequencedCollection<E> extends Collection<E> {
void addFirst(E element);
void addLast(E element);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
SequencedCollection<E> reversed();
}
interface SequencedSet<E>
extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed();
}
interface SequencedMap<K, V> extends Map<K, V> {
SequencedMap<K, V> reversed();
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K, V>> sequencedEntrySet();
V putFirst(K key, V value);
V putLast(K key, V value);
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry();
Entry<K, V> pollLastEntry();
}
Example: ArrayList
var courses = new ArrayList<>();
courses.add("Java");
courses.addFirst("Python");
courses.addLast("JavaScript");
System.out.println(
courses.getFirst()); // Python
System.out.println(
courses.getLast()); // JavaScript
System.out.println(
courses.reversed()); // [JavaScript, Java, Python]
Example: LinkedHashSet
var courses = new LinkedHashSet<>(
List.of("Spring", "AWS", "Azure"));
courses.addFirst("Java");
courses.addLast("Kotlin");
System.out.println(
courses.getFirst()); // Java
System.out.println(
courses.getLast()); // Kotlin
System.out.println(
courses.reversed()); // [Kotlin, Azure, AWS, Spring, Java]
Example: LinkedHashMap
var courses = new LinkedHashMap<>();
courses.put(1, "Spring");
courses.put(2, "Spring Boot");
courses.put(3, "Spring AI");
courses.putFirst(0, "Java");
courses.putLast(4, "Kotlin");
//{4=Kotlin, 3=Spring AI, 2=Spring Boot, 1=Spring, 0=Java}
System.out.println(courses.reversed());
//[0, 1, 2, 3, 4]
System.out.println(courses.sequencedKeySet());
//[Java, Spring, Spring Boot, Spring AI, Kotlin]
System.out.println(courses.sequencedValues());
//0=Java
System.out.println(courses.firstEntry());
//4=Kotlin
System.out.println(courses.lastEntry());
//0=Java (Removes and returns the first key-value pair)
System.out.println(courses.pollFirstEntry());
//4=Kotlin
System.out.println(courses.pollLastEntry());