Arrays in Java
An array in Java is a data structure that allows you to store multiple values of the same type in a single variable. Arrays are objects in Java, meaning they inherit
methods from the Object class. Here's a quick overview:
Key Features of Arrays
Fixed Size: Once an array is created, its size cannot be changed.
Indexed:Elements are accessed by their numerical index. The first element is at index 0, the second at index 1, and so on.
Contiguous Memory Allocation When we use arrays of primitive types, the elements are stored in contiguous locations. For non primitive types, references of
items are stored at contiguous locations.
Homogeneous: All elements in an array must be of the same data type.
Efficient: Arrays provide fast access to elements using their index.
When to Use an Array?
When you know the number of elements in advance.
When you need fast, random access to elements by index.
For storing primitive data or objects where the size is static.
String Class in Java
Key Features of the String Class
Immutability: Strings are immutable, which means any modification creates a new object rather than altering the original.
Stored in String Pool: String literals are stored in a special memory area called the String Pool for memory efficiency.
Final Class: The String class is declared as final, so it cannot be subclassed.
Special Notes
Strings are thread-safe because of their immutability.
For mutable strings, you can use StringBuilder or StringBuffer.
Why are they immutable?
Security: Strings are used for sensitive data like network connections, file paths, and usernames. Immutability prevents them from being changed by unauthorized
code.
Thread-Safety: Immutable objects can be shared across multiple threads without synchronization because their state cannot change.
String Comparison: == vs .equals()
This is a classic interview question and a common source of bugs.
== Operator: Compares object references. It checks if two reference variables point to the exact same memory object.
.equals() Method: Compares the actual content (sequence of characters) of the Strings.
Mutable Alternatives: StringBuilder and StringBuffer
Since Strings are immutable, operations like concatenation in a loop are inefficient, as they create many temporary objects. For such scenarios, use mutable
alternatives.
StringBuilder (Introduced in Java 5)
Not thread-safe. (Faster)
Use this when you are working in a single thread.
StringBuffer
Thread-safe. (Slower due to synchronization)
Use this when you are modifying a string from multiple threads.
StringBuilder Class in Java
It offers similar functionality to StringBuffer, but without thread safety.
StringBuilder is not synchronized, making it faster and more efficient than StringBuffer in single-threaded applications.
Use StringBuffer only when thread safety is needed; otherwise, prefer StringBuilder for better performance.
Performance: Faster than String for operations like concatenation, insertion, or deletion.
Thread-Unsafety: Unlike StringBuffer, StringBuilder is not synchronized, making it unsuitable for multi-threaded environments.
Disadvantages of the StringBuilder class are listed below:
It is not synchronized, making it unsuitable for use in multi-threaded environments.
If not used properly, StringBuilder may allocate excess memory, especially if the initial capacity is set too large.
For multi-threaded scenarios, you must handle synchronization manually, unlike StringBuffer.
When to Use StringBuilder
Use StringBuilder when you need to perform multiple string manipulations in a single-threaded environment.
For multi-threaded environments, consider using StringBuffer instead, as it is thread-safe.
StringBuffer Class in Java
The key features of StringBuffer class are listed below:
Unlike String, we can modify the content of the StringBuffer without creating a new object.
All methods of StringBuffer are synchronized, making it safe to use in multithreaded environments.
Ideal for scenarios with frequent modifications like append, insert, delete, or replace operations.
Thread Safety
StringBuffer is synchronized, making it thread-safe. This means multiple threads cannot access it simultaneously, ensuring safe operations in a multi-threaded
environment.
StringBuffer is synchronized, making it suitable for use in multi-threaded environments
ArrayList in Java
Key Features of ArrayList
Dynamic Size: Automatically resizes when elements are added or removed.
Index-Based Access: Allows random access to elements using an index,just like array.
Allows Null Values: Can store null values.
Generic Support: Can specify the type of elements it holds (e.g., ArrayList<String>).
Not Synchronized: It is not thread-safe by default, but you can make it synchronized using [Link]().
Allows Duplicates: Duplicate elements are allowed.
Maintains Insertion Order: Elements are stored in the order they are inserted.
Slower than arrays for primitive types due to boxing/unboxing.
For thread-safe alternatives, consider using Vector or CopyOnWriteArrayList.
Flexible and easy to use compared to arrays.
Disadvantages
Slower than Arrays: Slower for certain operations like inserting elements in the middle
Increased Memory Usage: Requires more memory than arrays
Not Thread-Safe: Multiple threads may cause data corruption
Performance Degradation: Performance may degrade with a large number of elements
LinkedList in Java
It represents a linear collection of elements where each element points to the next one in the sequence.
Dynamic Size: LinkedList grows or shrinks dynamically at runtime.
Maintains Insertion Order: Elements are stored in the order they are added.
Allows Duplicates: Duplicate elements are [Link] Elements: Allows null elements
Not Synchronized: By default, LinkedList is not thread-safe.
Non-Contiguous Memory: Elements are not stored in contiguous memory locations.
Efficient Insertion/Deletion: Adding or removing elements at the beginning or middle is faster compared to ArrayList.
Advantages
Efficient for frequent insertions and deletions.
Can be used as a stack, queue, or deque due to its implementation of Deque.
Disadvantages
Higher memory usage compared to arrays due to storage of node pointers.
Slower random access (O(n)) compared to arrays (O(1)).
When to Use LinkedList
When you need frequent insertions and deletions.
When memory allocation in contiguous blocks is not feasible.
Frequent insertions/deletions at beginning or middle
Implementing stacks, queues, or deques
Sequential access is sufficient
Use ArrayList when:
Frequent random access is needed
Mostly adding/removing at the end
Memory overhead is a concern
Note: LinkedList nodes cannot be accessed directly by index; elements must be accessed by traversing from the head.
HashSet in Java
HashSet in Java implements the Set interface of the Collections Framework. It is used to store the unique elements, and it doesn't maintain any specific order of
elements.
Interface : Implements Set
Duplicate : Not Allowed
Order : No guaranteed order
Null : One null element is allowed
Performance : O(1) for add, remove, contains (on average)
Synchronized : Use [Link](new HashSet<>()) for thread-safe access.
Custom Objects : Must override hashCode() and equals()
Uses HashMap internally which is implementation of hash table data structure.
Also implements Serializable and Cloneable interfaces.
HashSet is not thread-safe. To make it thread-safe, synchronization is needed externally.
When to Use HashSet ?
When you need to prevent duplicates.
When you don't care about the order of elements.
When you need fast lookups, insertions, and deletions. It's significantly faster for contains() checks than a List (which is O(n)).
When to Avoid HashSet ?
When you need to maintain insertion order (use LinkedHashSet).
When you need elements sorted (use TreeSet).
Critical Override: hashCode() and equals()
When you store custom objects (like your own Employee, Student, etc.) in a HashSet, you MUST override the hashCode() and equals() methods in that class.
If you don't override them, Java uses the default implementations from the Object class, which are based on memory addresses. This will lead to unexpected
behavior, as two logically equal objects might be treated as different.
HashSet in Java :
Internal Working: The Magic of Hashing
The "Hash" in HashSet is the key to its performance. Here's how it works:
Key Features of HashSet
Unique Elements: It does not allow duplicate elements.
No Order Guarantee: The elements are not stored in any specific order (insertion order is not maintained).
Allows Null: It permits a single null element.
Fast Operations: Provides constant-time performance for basic operations like add, remove, and contains (on average).
HashCode: When you add an object to a HashSet, the hashCode() method of the object is called to compute a hash value (an integer).
Buckets: This hash value is used to determine a "bucket" or a specific location in an internal array (often called a table) where the element should be stored.
equals() for Duplicates: If two objects have the same hash code (a hash collision), the HashSet then uses the equals() method to check if they are truly identical.
If equals() returns true, it's a duplicate, and the element is not added.
If equals() returns false, the new element is stored in the same bucket, typically as part of a linked list or a tree (in modern Java, it converts to a tree for
performance if a bucket gets too large).
This mechanism is why HashSet offers constant time performance (O(1)) for basic operations like add, remove, contains, and size, assuming the hash function
distributes the elements properly among the buckets.
When to Use HashSet
When you need to store unique elements.
When the order of elements is not important.
When you need fast lookups, insertions, and deletions.
LinkedHashSet in Java
A LinkedHashSet is a hybrid data structure that combines the best features of a HashSet and a LinkedList.
Key Characteristics
Performance: Slightly slower than HashSet due to the overhead of maintaining the linked list.
No Duplicate Elements: Like all Set implementations, it does not allow duplicate elements.
Maintain Insertion Order: This is its defining feature. When you iterate over a LinkedHashSet, the elements are returned in the exact order in which they were first
inserted.
Allow only One null Element: You can add a single null value to a LinkedHashSet.
Not Synchronized: It is not thread-safe. If multiple threads access a LinkedHashSet concurrently and at least one thread modifies it, it must be synchronized
externally.
Performance: It provides predictable, constant-time performance O(1) for basic operations like add, remove, contains, and size, assuming the hash function
disperses elements properly. It's slightly slower than a HashSet due to the overhead of maintaining the linked list, but faster than a TreeSet.
When to Use LinkedHashSet
When you need a collection that ensures unique elements.
When the order of insertion is important.
For scenarios where performance is less critical than maintaining order.
TreeSet in Java
TreeSet is a powerful collection class in Java that implements the NavigableSet interface and provides a sorted set implementation.
Key Features
Sorted Collection: Elements are stored in sorted order (natural ordering or custom comparator)
No Duplicates: Like all Set implementations, it doesn't allow duplicate elements
Does not allow null values. From JDK 7 onward, inserting null throws NullPointerException.
Backed by TreeMap: Internally uses a Red-Black tree (self-balancing binary search tree)
Fast Operations: Most operations (add, remove, contains) take O(log n) time
TreeSet is not synchronized. it must be synchronized using [Link]().
When to Use TreeSet
✅ When you need elements in sorted order
✅ When you need frequent range queries or navigation operations
✅ When you need a collection without duplicates that maintains order
❌ When insertion order is important (use LinkedHashSet instead)
❌ When you need constant-time performance for basic operations (use HashSet instead)
Note:
A class must implement Comparable (or provide a Comparator) to be stored in a TreeSet.
Built-in classes like String, wrapper classes, etc., already implement Comparable
PriorityQueue in Java
A PriorityQueue in Java is a special type of queue where elements are processed based on their priority, not just their order of insertion (First-In-First-Out). The
element with the highest priority is always at the head of the queue and is the next one to be removed when you call poll() or remove().
Think of it like a hospital emergency room: patients are seen based on the severity of their condition, not on who arrived first.
Key Characteristics
Ordering: The elements are ordered either by their natural ordering (if they implement Comparable) or by a Comparator provided at queue construction time.
Dynamic Size: It is an unbounded queue, but its capacity grows dynamically as needed.
No Indexed Access: You cannot access elements by their index (like in a List). You can only peek at the head.
No null Elements: PriorityQueue does not permit null elements.
Not Thread-Safe: It is not synchronized. For multi-threaded environments, use PriorityBlockingQueue.
Head of the Queue: The "head" of the queue is the least element according to the specified ordering. For a min-heap (default), it's the smallest element; for a max-
heap, it's the largest.
Underlying Data Structure: It is implemented using a binary heap, which makes its operations very efficient.
PriorityQueue in Java
To use a PriorityQueue with custom objects, you must either:
Make the class implement Comparable, or Provide a Comparator when creating the queue.
Important Points to Remember
Iteration Order is Not Sorted: If you iterate over the queue using a for loop or an iterator, the elements will not be in priority order. The only guarantee is that
peek(), poll(), and remove() act on the head element.
Performance: The add and poll methods are very efficient (O(log n)) due to the heap implementation. However, remove(Object) and contains(Object) are linear
time (O(n)) operations, as they may need to scan the entire heap.
When to Use a PriorityQueue
Task Scheduling: Executing tasks based on their priority level.
Dijkstra's Algorithm: For finding the shortest path in a graph.
Huffman Coding: For building the compression tree.
K-th Largest/Smallest Element: Efficiently finding the top K elements in a stream of data.
Simulation Systems: Where events need to be processed in a priority-based order.
ArrayDeque in Java
ArrayDeque (which stands for Array Double-Ended Queue) is a resizable-array implementation of the Deque interface. In simple terms, it's a collection that allows
you to add, remove, and access elements from both ends (head and tail) with high efficiency.
The "Array" part of its name means it uses an internal array to store elements, and it dynamically resizes this array as needed, much like an ArrayList.
Key Characteristics
Double-Ended: You can add/remove from both the front (head) and the back (tail).
Resizable Array: Grows as needed, so you don't have to specify a capacity (though you can for optimization).
No Capacity Restrictions: Unlike a fixed-size queue, it grows automatically.
Faster than LinkedList: For use as a stack or queue, it's generally faster than LinkedList because it's not a linked node structure and has better locality of
reference.{ Fast insertion and removal at both ends (better than LinkedList for this purpose)}.
Not Thread-Safe: It is not synchronized. If multiple threads access it concurrently, it must be synchronized externally.
No Null Elements: ArrayDeque does not permit null elements. Attempting to insert null will throw a NullPointerException.
Implements Deque: Since it implements the Deque interface, it can be used as a LIFO Stack (Last-In-First-Out) or a FIFO Queue (First-In-First-Out).
ArrayDeque in Java
Common Use Case Shortcuts:
When using it as a FIFO Queue, the methods add(e), offer(e), remove(), poll(), element(), and peek() are available. They are equivalent to the Last methods for
insertion and the First methods for removal.
add(e) == addLast(e)
remove() == removeFirst()
When using it as a LIFO Stack, the methods push(e), pop(), and peek() are available.
push(e) == addFirst(e)
pop() == removeFirst()
When to Use ArrayDeque?
As a Stack: It is the recommended class to use instead of the legacy Stack class. The Stack class is synchronized and has poor performance.
As a Queue: It's an excellent choice for a single-threaded queue when you don't need the blocking features of LinkedBlockingQueue.
As a Deque: Whenever you need the flexibility of adding/removing from both ends.
Rule of Thumb: If you need a stack, queue, or deque, choose ArrayDeque. If you need a general-purpose list with index-based access or you need to frequently
add/remove from the middle of the list, choose LinkedList.
HashMap in Java
HashMap is a part of Java's Collections Framework that implements the Map interface. It stores key-value pairs and allows null values and the null key.
Key Characteristics
Key-Value Pair Storage: Each entry in a HashMap consists of a unique key and a corresponding value.
Unique Keys: Keys must be unique, but values can be duplicated.
Null Values: HashMap allows one null key and multiple null values.
Unordered: The elements in a HashMap are not stored in any specific order.
Unsynchronized: Not thread-safe (use CncurrentHashMap for thread safety)
O(1) time complexity for basic operations (get, put,insertion, deletion, and retrieval) on average
Backed by array: Uses buckets (array of linked lists/red-black trees)
How HashMap Works Internally
Hashing: Uses hashCode() method to determine bucket location
Buckets: Array of nodes (linked lists or trees)
Collision Resolution: Uses chaining (linked lists convert to trees when threshold reached)
Load Factor: When to resize (default 0.75 = 75% full)
Resizing: Doubles capacity when load factor exceeded
When to Use HashMap
Use HashMap when:
Need fast key-based access
Don't care about order
Frequent insertions and lookups
Consider alternatives when:
Need ordering: Use LinkedHashMap or TreeMap
Need thread safety: Use ConcurrentHashMap
Need sorted keys: Use TreeMap
LinkedHashMap in Java
LinkedHashMap is a hash table and linked list implementation of the Map interface with predictable iteration order.
Key Characteristics
Insertion Order: By default, maintains the order in which elements were inserted
Access Order: Can be configured to maintain access order (LRU behavior)
Performance: Similar to HashMap (O(1) for basic operations)
Thread-unsafe: Like HashMap, requires external synchronization for thread safety
Key-Value Pairs: Stores data as key-value pairs, just like other Map implementations.
Performance: Similar to HashMap, but slightly slower due to the overhead of maintaining the linked list.
Null Values: Allows one null key and multiple null values.
When to Use LinkedHashMap
When you need a Map with predictable iteration order.
When you want to preserve the order of elements as they were inserted.
Advantages
Predictable iteration order.
Easy to use when insertion order matters.
Disadvantages
Slightly more memory usage compared to HashMap due to the linked list overhead.
TreeMap in Java
TreeMap is a Red-Black tree based implementation of the Map interface that maintains ascending order of keys.
Key Features
Sorted: Keys are stored in sorted order (natural ordering or custom comparator)
No duplicate keys: Like all Map implementations
Not synchronized: Needs external synchronization for thread safety
O(log n) time complexity: For basic operations (put, get, remove)
No Duplicates: Keys must be unique, but values can be duplicated.
Null Handling:
Keys: TreeMap does not allow null keys.
Values: It allows multiple null values.
When to Use TreeMap
Use TreeMap when:
You need sorted iteration
You frequently need range operations
Memory is not a primary concern
Use HashMap when:
You need better performance O(1) vs O(log n)
Order doesn't matter
Memory efficiency is important