10 Java Collections Framework Interview Questions and Answers
Prepare for your Java interview with our guide on the Java Collections Framework, featuring common questions and detailed answers.
Prepare for your Java interview with our guide on the Java Collections Framework, featuring common questions and detailed answers.
The Java Collections Framework is a cornerstone of Java programming, providing a set of classes and interfaces that manage groups of objects with ease and efficiency. This framework is essential for tasks such as data storage, retrieval, manipulation, and aggregation, making it a critical component for any Java developer to master. Its versatility and robustness are why it is a frequent topic in technical interviews.
This article offers a curated selection of interview questions focused on the Java Collections Framework. By working through these questions, you will deepen your understanding of key concepts and be better prepared to demonstrate your expertise in interviews.
ArrayList and LinkedList are both part of the Java Collections Framework, but they have different performance characteristics due to their underlying data structures.
ArrayList is backed by a dynamic array, providing fast random access to elements (O(1) time complexity for get operations). However, inserting or deleting elements, especially in the middle, can be slow (O(n) time complexity) due to the need to shift elements.
LinkedList is implemented as a doubly-linked list, allowing fast insertions and deletions (O(1) time complexity) with a reference to the node. However, accessing elements by index is slower (O(n) time complexity) because it requires traversing the list.
A HashMap in Java stores key-value pairs using hashing. When a key-value pair is added, the key is hashed to generate an index, determining where the pair will be stored in an internal array called a bucket.
Key components of a HashMap:
To sort a List of custom objects by a specific field, use the Comparator interface with the Collections.sort() method. The Comparator interface allows you to define the order based on a specific field.
Example:
import java.util.*; class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name + " - " + age; } } class AgeComparator implements Comparator<Person> { @Override public int compare(Person p1, Person p2) { return Integer.compare(p1.age, p2.age); } } public class Main { public static void main(String[] args) { List<Person> people = new ArrayList<>(); people.add(new Person("Alice", 30)); people.add(new Person("Bob", 25)); people.add(new Person("Charlie", 35)); Collections.sort(people, new AgeComparator()); for (Person person : people) { System.out.println(person); } } }
In this example, the Person class has two fields: name and age. The AgeComparator class implements the Comparator interface and overrides the compare() method to sort the Person objects by age.
The Java Collections Framework provides several implementations of the Set interface, including HashSet, TreeSet, and LinkedHashSet. Each has distinct characteristics and use cases.
An LRU (Least Recently Used) cache discards the least recently used items first when it reaches its capacity. This is useful in scenarios where you want to limit memory usage and ensure that the most recently accessed items are retained.
In Java, the LRU cache can be implemented using the LinkedHashMap class. By overriding the removeEldestEntry
method, you can specify the maximum capacity of the cache and automatically remove the least recently used entry when the cache exceeds this capacity.
import java.util.LinkedHashMap; import java.util.Map; public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(3); cache.put(1, "one"); cache.put(2, "two"); cache.put(3, "three"); cache.get(1); cache.put(4, "four"); System.out.println(cache); } }
In this example, the LRUCache class extends LinkedHashMap and overrides the removeEldestEntry
method to ensure that the cache does not exceed the specified capacity.
The Comparable interface defines the natural ordering of objects with the compareTo() method. The Comparator interface allows for custom ordering with the compare() method.
Example:
import java.util.*; class Student implements Comparable<Student> { String name; int age; Student(String name, int age) { this.name = name; this.age = age; } @Override public int compareTo(Student other) { return this.age - other.age; } } class NameComparator implements Comparator<Student> { @Override public int compare(Student s1, Student s2) { return s1.name.compareTo(s2.name); } } public class Main { public static void main(String[] args) { List<Student> students = new ArrayList<>(); students.add(new Student("Alice", 22)); students.add(new Student("Bob", 20)); students.add(new Student("Charlie", 21)); Collections.sort(students); // Natural ordering by age System.out.println("Sorted by age:"); for (Student s : students) { System.out.println(s.name + " " + s.age); } Collections.sort(students, new NameComparator()); // Custom ordering by name System.out.println("Sorted by name:"); for (Student s : students) { System.out.println(s.name + " " + s.age); } } }
ConcurrentHashMap and HashMap serve different purposes and have different characteristics.
HashMap is not thread-safe, meaning that if multiple threads access it concurrently and at least one modifies it structurally, it must be synchronized externally.
ConcurrentHashMap is designed for concurrent access, allowing multiple threads to read and write without external synchronization. It achieves this by dividing the map into segments, each of which can be locked independently.
Example:
import java.util.concurrent.ConcurrentHashMap; import java.util.HashMap; public class MapExample { public static void main(String[] args) { // HashMap example HashMap<Integer, String> hashMap = new HashMap<>(); hashMap.put(1, "One"); hashMap.put(2, "Two"); // ConcurrentHashMap example ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>(); concurrentHashMap.put(1, "One"); concurrentHashMap.put(2, "Two"); // Demonstrating concurrent access Runnable task = () -> { for (int i = 0; i < 10; i++) { concurrentHashMap.put(i, "Value " + i); } }; Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); } }
import java.util.WeakHashMap; public class WeakHashMapExample { public static void main(String[] args) { WeakHashMap<String, String> map = new WeakHashMap<>(); String key = new String("key"); String value = "value"; map.put(key, value); System.out.println("Map before GC: " + map); key = null; // Remove strong reference to the key System.gc(); // Suggest garbage collection // Wait for a moment to let GC do its work try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Map after GC: " + map); } }
In this example, we create a WeakHashMap and add an entry with a key and value. By setting the key to null and suggesting garbage collection, we allow the key to be garbage collected. After garbage collection, the entry is removed from the WeakHashMap.
The Stream API in Java provides a modern way to process collections of objects. It allows for functional-style operations on streams of elements, which can be sequences of data from collections like lists, sets, and maps. The Stream API integrates with the Collections Framework by providing methods to convert collections into streams and perform operations on them.
For example, you can convert a list into a stream and then use various stream operations to process the elements:
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StreamExample { public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); List<String> filteredNames = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList()); System.out.println(filteredNames); // Output: [Alice] } }
In this example, the stream()
method is called on the list to create a stream. The filter()
method is then used to retain only the elements that start with the letter “A”. Finally, the collect()
method is used to convert the stream back into a list.
When implementing a custom collection in Java, several considerations should be made: