Java Interview Questions
💡 Click Show Answer to generate an AI-powered answer instantly.
Explain final keyword in Java.
The `final` keyword in Java is a non-access modifier that restricts modification, overriding, or inheritance, depending on where it's applied. It signifies immutability or a lack of extensibility.
Purpose of `final`
The final keyword in Java is used to define an entity that cannot be reassigned (for variables), overridden (for methods), or inherited (for classes). It's a powerful tool for ensuring immutability and enforcing design constraints, helping to create more robust and secure code.
Applying `final`
final can be applied to variables, methods, and classes, each with a distinct effect.
`final` Variables
When applied to a variable, final makes it a constant, meaning its value can only be assigned once. For primitive types, this means the value itself cannot change. For reference types, it means the reference itself cannot be reassigned to point to a different object, although the internal state of the referenced object can still be modified (unless the object itself is immutable).
public class FinalVariableExample {
// Compile-time constant
private final int MAX_VALUE = 100;
// Blank final variable, must be initialized in constructor
private final String API_KEY;
public FinalVariableExample(String apiKey) {
this.API_KEY = apiKey; // Assigned once
// MAX_VALUE = 200; // Compile-time error: cannot assign a value to final variable MAX_VALUE
}
public void demonstrate() {
final int localConstant = 50;
// localConstant = 60; // Compile-time error
System.out.println("Max Value: " + MAX_VALUE);
System.out.println("API Key: " + API_KEY);
final StringBuilder sb = new StringBuilder("Hello");
// sb = new StringBuilder("World"); // Compile-time error: cannot assign a value to final variable sb
sb.append(" World"); // This is allowed, the object's state can change
System.out.println("StringBuilder: " + sb);
}
}
`final` Methods
A final method cannot be overridden by any subclass. This is useful for preventing unwanted modifications to core logic, ensuring that a method's implementation remains consistent across the class hierarchy. It's often used in design patterns or utility classes where specific behavior should not be altered.
class Parent {
public final void display() {
System.out.println("This is a final method in Parent.");
}
public void greet() {
System.out.println("Hello from Parent!");
}
}
class Child extends Parent {
// @Override
// public void display() { // Compile-time error: cannot override final method from Parent
// System.out.println("This is a display method in Child.");
// }
@Override
public void greet() { // Allowed, greet is not final
System.out.println("Hello from Child!");
}
}
`final` Classes
When a class is declared as final, it cannot be extended or subclassed. This means no other class can inherit from it. final classes are inherently immutable (if their fields are also final and properly handled) and are often used for security reasons, to prevent malicious subclasses, or to create utility classes with fixed behavior, like String, Integer, and other wrapper classes in Java's standard library.
final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// No setters, ensuring immutability
}
// class AnotherPoint extends ImmutablePoint { // Compile-time error: cannot inherit from final ImmutablePoint
// // ...
// }
Summary
finalvariables: Can be assigned only once, making them constants. For reference types, the reference cannot change, but the object's state might (unless the object itself is immutable).finalmethods: Cannot be overridden by subclasses, ensuring consistent behavior.finalclasses: Cannot be extended or inherited, preventing modification of their behavior through subclassing.- Used for immutability, security, and enforcing design constraints.
What is the difference between ArrayList and LinkedList?
ArrayList and LinkedList are two fundamental implementations of the List interface in Java's Collections Framework. While both serve to store an ordered sequence of elements, they differ significantly in their internal data structures and, consequently, their performance characteristics for various operations. Understanding these differences is crucial for selecting the appropriate list implementation for a given use case.
Fundamental Differences in Internal Structure
The core distinction between ArrayList and LinkedList lies in how they store and manage elements. ArrayList uses a dynamic array (a resizable array) internally, providing quick access to elements via index. LinkedList, on the other hand, implements a doubly-linked list, where each element (node) stores the data along with references to the previous and next nodes in the sequence.
| Feature | ArrayList | LinkedList |
|---|---|---|
| Internal Data Structure | Resizable array | Doubly-linked list |
| Random Access (get(index)) | O(1) (constant time) | O(n) (linear time, sequential traversal) |
| Insertion/Deletion (middle) | O(n) (requires shifting elements) | O(1) (after finding position), overall O(n) to find position |
| Insertion/Deletion (end) | Amortized O(1) (resizing can be O(n)) | O(1) |
| Memory Overhead | Less (stores only data), but may waste space due to capacity management | More (each node stores data + two pointers) |
| Implementation of Interfaces | Implements `RandomAccess` interface | Does not implement `RandomAccess`, also implements `Deque` |
| When Best Suited | Frequent random access, fewer insertions/deletions in middle | Frequent insertions/deletions, especially in middle or at ends |
ArrayList Characteristics
- Array-based: Elements are stored in a contiguous memory block.
- Fast Random Access: Retrieving an element by its index (
get(index)) is very efficient (O(1)) because the memory address can be directly calculated. - Slow Middle Operations: Adding or removing elements from the middle of the list is slow (O(n)) as it requires shifting all subsequent elements.
- Amortized Constant Time Addition at End: Adding elements to the end is typically fast (amortized O(1)), but occasional resizing operations (when the underlying array capacity is exceeded) can be O(n) as elements are copied to a new, larger array.
- Memory Efficiency: Generally uses less memory per element as it doesn't store pointers for each element, although capacity management might lead to some wasted space.
LinkedList Characteristics
- Node-based: Elements are stored in nodes, with each node containing the data and references to the previous and next nodes.
- Slow Random Access: Accessing an element by index (
get(index)) is slow (O(n)) because the list must be traversed sequentially from the beginning or end to reach the target index. - Fast Middle Operations: Adding or removing elements from the middle of the list is very efficient (O(1)) *once the position is found*, as it only involves updating a few pointers. However, finding the position itself is O(n).
- Fast End Operations: Adding or removing elements from the beginning or end (
addFirst(),removeLast()) is O(1) as the head/tail pointers are directly accessible. - Memory Overhead: Uses more memory per element due to the storage of two pointers (next and previous) in each node.
- Deque Implementation: Implements the
Dequeinterface, allowing it to be used as a double-ended queue (queue or stack).
When to Choose Which?
- Choose ArrayList when:
- You need frequent random access to elements by index (
get(index)). - You primarily add or remove elements at the end of the list.
- You have a relatively stable list where elements are rarely inserted or deleted from the middle.
- Memory efficiency for data storage (less pointer overhead) is a higher priority.
- Choose LinkedList when:
- You need frequent insertions or deletions of elements, especially in the middle of the list.
- You mainly iterate through the list sequentially and perform operations based on the current position (e.g., using an
Iterator). - You need to use it as a queue or a stack, leveraging its
Dequecapabilities (addFirst,removeLast, etc.). - The overhead of storing pointers for each element is acceptable for the performance gains in modification operations.
What is the difference between HashMap and Hashtable?
HashMap and Hashtable are two classes in Java's Collections Framework used for storing key-value pairs. While both implement the Map interface and serve a similar purpose, they possess distinct characteristics that are crucial to understand for effective application development, especially regarding thread-safety, null handling, and performance.
Core Distinctions
The main differences between HashMap and Hashtable center around their thread-safety mechanisms, how they handle null keys and values, their performance characteristics, and their historical context within the Java platform. These factors heavily influence which implementation should be chosen for a given scenario.
Detailed Comparison Points
- Synchronization: Hashtable is synchronized, meaning it is thread-safe and can be used directly in multi-threaded environments without external synchronization. HashMap is non-synchronized, making it not inherently thread-safe and suitable for single-threaded environments or when external synchronization (e.g., using
Collections.synchronizedMap()) is explicitly managed. - Null Keys and Values: HashMap allows one null key and multiple null values. Hashtable, on the other hand, does not allow any null key or null value; attempting to insert them will result in a
NullPointerException. - Performance: Due to the overhead of synchronization, Hashtable is generally slower than HashMap. HashMap offers superior performance in single-threaded scenarios where thread-safety is not a concern or is handled externally.
- Inheritance: Hashtable is a legacy class that extends
Dictionary(an obsolete class) and implements theMapinterface. HashMap is part of the newer Java Collections Framework, extendingAbstractMapand implementing theMapinterface. - Fail-Fast Iterators: HashMap's iterators (obtained via
keySet().iterator(),entrySet().iterator(),values().iterator()) are fail-fast, throwing aConcurrentModificationExceptionif the map is structurally modified during iteration. Hashtable'sEnumerationis not fail-fast, but itsIterator(if obtained from itsMapviews) is fail-fast. - Java Version: Hashtable was introduced in Java 1.0 as part of the original Java API. HashMap was introduced in Java 1.2 as part of the new Java Collections Framework.
Summary Table
| Feature | HashMap | Hashtable |
|---|---|---|
| Synchronization | Not synchronized (not thread-safe) | Synchronized (thread-safe) |
| Null Keys/Values | Allows one null key and multiple null values | Does not allow null keys or null values |
| Performance | Faster (no synchronization overhead) | Slower (due to synchronization overhead) |
| Inheritance | Extends `AbstractMap`, implements `Map` | Extends `Dictionary`, implements `Map` |
| Fail-Fast Iterator | Yes | `Enumeration` is not; `Iterator` is. |
| Legacy Status | Not a legacy class | Legacy class (from Java 1.0) |
| Preferred Usage | Single-threaded environments or when external synchronization is applied | Multi-threaded environments (though `ConcurrentHashMap` is generally preferred for new code) |
Explain the Java Collections Framework.
The Java Collections Framework (JCF) is a set of interfaces and classes that provides a unified architecture for representing and manipulating collections of objects. It offers powerful, high-performance implementations of common data structures, simplifying data management in Java applications.
What is the Java Collections Framework?
The JCF provides a rich set of interfaces (like List, Set, Queue, Map) and their implementations (like ArrayList, HashSet, LinkedList, HashMap) along with algorithms to perform common operations on these collections. Its primary goal is to improve the reusability, performance, and interoperability of data structures.
Core Interfaces
The framework is built around a few fundamental interfaces that define the types of collections. These interfaces are polymorphic, allowing manipulation of different implementations in a uniform manner.
- Collection: The root interface in the collection hierarchy. It defines basic operations that all collections support.
- List: An ordered collection (also known as a sequence). Elements can be accessed by their integer index. Allows duplicates.
- Set: A collection that contains no duplicate elements. It models the mathematical set abstraction.
- Queue: A collection designed for holding elements prior to processing. Besides basic Collection operations, queues provide additional insertion, extraction, and inspection operations.
- Map: An object that maps keys to values. A Map cannot contain duplicate keys; each key can map to at most one value. (Note: Map is not a true 'Collection' but is integral to the JCF).
The Collection Interface
This is the super-interface for all collections (except Map). It defines common methods like add(), remove(), contains(), size(), isEmpty(), and toArray().
The List Interface
Lists maintain the insertion order of elements and allow duplicate elements. Elements can be accessed by index. Common implementations include:
- ArrayList: Implemented using a dynamic array. Good for random access, slower for insertions/deletions in the middle.
- LinkedList: Implemented using a doubly linked list. Good for insertions/deletions, slower for random access.
- Vector: A thread-safe, synchronized version of ArrayList (legacy).
The Set Interface
Sets store unique elements and do not guarantee any specific order. Common implementations include:
- HashSet: Stores elements in a hash table. Provides constant-time performance for basic operations (add, remove, contains). Does not maintain order.
- LinkedHashSet: Maintains a doubly-linked list running through its elements, preserving insertion order.
- TreeSet: Stores elements in a Red-Black tree. Elements are sorted in their natural order or by a custom Comparator.
The Queue Interface
Queues typically operate in a First-In-First-Out (FIFO) manner. Common implementations include:
- LinkedList: Can be used as a Queue (implements both List and Deque interfaces).
- PriorityQueue: Elements are ordered according to their natural ordering or by a Comparator. Not strictly FIFO.
- ArrayDeque: A resizable-array implementation of the Deque interface, usable as both FIFO queues and LIFO stacks.
The Map Interface
Maps store key-value pairs, where each key is unique. Common implementations include:
- HashMap: Stores key-value pairs in a hash table. Provides constant-time performance for basic operations. Does not maintain order.
- LinkedHashMap: Maintains a doubly-linked list through its entries, preserving insertion order.
- TreeMap: Stores key-value pairs in a Red-Black tree. Keys are sorted in their natural order or by a custom Comparator.
- Hashtable: A synchronized, legacy version of HashMap.
Key Benefits
- Reduced programming effort: Developers don't need to write custom data structure implementations.
- Increased performance: Highly optimized, high-performance implementations are provided.
- Interoperability: Collections can be passed between different APIs seamlessly.
- Reduced effort to learn new APIs: By standardizing collection types.
- Increased software quality: Well-tested and robust implementations contribute to fewer bugs.
Example Usage: ArrayList
import java.util.ArrayList;
import java.util.List;
public class CollectionsExample {
public static void main(String[] args) {
// Create a List of Strings using ArrayList
List<String> names = new ArrayList<>();
// Add elements to the list
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add("Bob"); // Lists allow duplicates
System.out.println("Names: " + names);
// Access elements by index
System.out.println("Second name: " + names.get(1));
// Remove an element
names.remove("Bob"); // Removes the first occurrence of "Bob"
System.out.println("Names after removing Bob: " + names);
// Iterate over the list
System.out.println("Iterating through names:");
for (String name : names) {
System.out.println(" - " + name);
}
}
}
This example demonstrates basic operations on an ArrayList, including adding elements, accessing by index, removing elements, and iterating through the collection. It highlights how the JCF provides a straightforward way to handle collections of objects.
What is a Set in Java and its implementations?
In Java, a Set is an interface that belongs to the Java Collections Framework. It extends the Collection interface and represents a collection that does not allow duplicate elements. As a mathematical set, it models the concept of a unique group of items.
What is a Set?
A Set is an unordered collection of unique objects. Unlike a List, it does not maintain any specific order of elements, and you cannot access elements by index. Attempting to add a duplicate element to a Set will simply be ignored, and the Set will remain unchanged. It allows at most one null element.
Key Characteristics of a Set
- No duplicate elements: Each element in a Set must be unique.
- No guaranteed order: Elements are not stored in any specific order (except for LinkedHashSet and TreeSet).
- Permits at most one null element.
- Not synchronized: Set implementations are generally not thread-safe by default (use Collections.synchronizedSet() for thread safety).
- Provides basic collection operations: add, remove, contains, size, isEmpty, clear, etc.
Common Set Implementations
HashSet
HashSet is the most common implementation of the Set interface. It stores elements by using a hash table for storage. This offers constant-time performance for the basic operations (add, remove, contains, size) assuming the hash function disperses the elements properly. HashSet does not guarantee any order of elements.
import java.util.HashSet;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
Set<String> names = new HashSet<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add("Alice"); // Duplicate, will be ignored
System.out.println("Names in HashSet: " + names); // Order not guaranteed
System.out.println("Contains Bob? " + names.contains("Bob"));
}
}
LinkedHashSet
LinkedHashSet is an ordered version of HashSet. It maintains a doubly-linked list running through all of its entries. This means that elements are stored in the order in which they were inserted (insertion order). It offers performance almost identical to HashSet but with the added cost of maintaining the linked list.
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetExample {
public static void main(String[] args) {
Set<String> names = new LinkedHashSet<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add("Alice"); // Duplicate, will be ignored
System.out.println("Names in LinkedHashSet: " + names); // Insertion order maintained
}
}
TreeSet
TreeSet stores its elements in a sorted order. It implements the NavigableSet interface and is backed by a TreeMap. Elements are stored in ascending order according to their natural ordering, or by a Comparator provided at set creation time. Its performance for basic operations (add, remove, contains) is O(log n) because it's based on a balanced binary search tree (Red-Black tree). TreeSet does not permit null elements (unless a custom Comparator allows it, which is generally not recommended as it can lead to issues with natural ordering).
import java.util.TreeSet;
import java.util.Set;
public class TreeSetExample {
public static void main(String[] args) {
Set<String> names = new TreeSet<>();
names.add("Charlie");
names.add("Alice");
names.add("Bob");
names.add("Alice"); // Duplicate, will be ignored
System.out.println("Names in TreeSet: " + names); // Natural sorted order
// TreeSet does not allow null (will throw NullPointerException)
// names.add(null);
}
}
Choosing the Right Set Implementation
| Implementation | Order | Performance (avg) | Allows nulls? |
|---|---|---|---|
| HashSet | No guaranteed order | O(1) | Yes (one) |
| LinkedHashSet | Insertion order | O(1) | Yes (one) |
| TreeSet | Natural / Custom sorted order | O(log n) | No |
What is the difference between List and Set?
What is a Thread in Java?
What is the difference between Runnable and Thread?
In Java, both `Runnable` and `Thread` are fundamental concepts used for implementing multithreading. While both achieve concurrency, they represent different approaches and have distinct characteristics regarding how a task is defined and executed.
The `Thread` Class
The java.lang.Thread class is a concrete implementation of a thread. When you extend the Thread class, your class itself becomes a thread, and you define the thread's execution logic by overriding its run() method. To start the thread, you create an instance of your custom Thread class and call its start() method.
class MyThread extends Thread {
public void run() {
System.out.println("Thread created by extending Thread class. ID: " + Thread.currentThread().getId());
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // Invokes run() method
}
}
Key Points of `Thread` Class
- Class-based approach: Your class *is* a thread, meaning the task and the thread itself are combined.
- Limited to single inheritance: Java does not support multiple inheritance, so if your class already extends another class, you cannot extend
Thread. - Tightly coupled: The task (defined in
run()) and the mechanism for execution (theThreadobject) are tightly bound. - Each instance of your custom
Threadclass creates a new thread.
The `Runnable` Interface
The java.lang.Runnable interface defines a single method, run(), which contains the code to be executed by a thread. When you implement Runnable, your class defines a task that can be executed by a Thread object. You create an instance of your Runnable class, then pass it to the constructor of a Thread object, and finally call start() on the Thread object.
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread created by implementing Runnable interface. ID: " + Thread.currentThread().getId());
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread t1 = new Thread(runnable); // Pass runnable object to Thread constructor
t1.start(); // Invokes run() method of MyRunnable
// You can also reuse the runnable object for multiple threads:
Thread t2 = new Thread(runnable);
t2.start();
}
}
Key Points of `Runnable` Interface
- Interface-based approach: Your class *defines* a task for a thread, separating the task from the thread object itself.
- Supports multiple inheritance indirectly: Since it's an interface, your class can implement
Runnableand still extend another class. - Loose coupling: Promotes better design by separating the task (what to run) from the thread (how to run it).
- Resource sharing: A single
Runnableobject can be shared among multipleThreadinstances, allowing for easier sharing of common data. - Recommended approach: Generally preferred in Java for multithreading due to its flexibility and better design principles.
Comparative Table
| Feature | `Thread` Class | `Runnable` Interface |
|---|---|---|
| Type | Class (`java.lang.Thread`) | Interface (`java.lang.Runnable`) |
| Implementation | Extend `Thread` class | Implement `Runnable` interface |
| Task Definition | Override `run()` method directly in the extended class. | Implement `run()` method in the implementing class. |
| Inheritance | Limited (cannot extend another class) | Flexible (can extend another class and implement `Runnable`) |
| Coupling | Tightly coupled (task and thread are one entity) | Loosely coupled (task is separate from the thread executor) |
| Resource Sharing | More complex if multiple threads of the *same* custom `Thread` class instance need to share state (requires static or external shared objects). | Easier to share a single `Runnable` instance among multiple `Thread` objects. |
| Usage Scenario | When the class *is* inherently a thread and no other inheritance is needed. | When the class defines a task that can be executed by a thread, promoting modularity and reuse. |
| Recommendation | Less preferred in most modern Java applications. | Generally preferred and considered best practice in Java. |
Explain synchronized keyword in Java.
`synchronized` is a crucial keyword in Java used for controlling access to shared resources by multiple threads, ensuring thread safety and preventing data corruption in concurrent programming environments. It achieves this by providing a mechanism for mutual exclusion.
What is `synchronized`?
The synchronized keyword in Java is primarily used to achieve thread safety. When multiple threads try to access and modify shared resources simultaneously, it can lead to inconsistent or corrupted data. synchronized provides a way to enforce mutual exclusion, meaning only one thread can access a synchronized block or method at any given time for a specific object, thus preventing race conditions.
How `synchronized` Works: Intrinsic Locks (Monitors)
Every object in Java has an associated monitor (also known as an intrinsic lock). When a thread enters a synchronized method or block, it attempts to acquire the monitor of the object on which the synchronization is performed. If the monitor is available, the thread acquires it and proceeds. If another thread already holds the monitor, the current thread blocks and waits until the monitor is released. Once the thread exits the synchronized block/method (either normally or due to an exception), the monitor is released.
Applying `synchronized`
1. Synchronized Methods
When synchronized is applied to an instance method, the lock acquired is the intrinsic lock of the current instance (this). All calls to synchronized instance methods on the same object will be mutually exclusive.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
When synchronized is applied to a static method, the lock acquired is the intrinsic lock of the Class object itself (e.g., Counter.class). This means all calls to synchronized static methods for that class will be mutually exclusive across all instances.
class StaticCounter {
private static int staticCount = 0;
public static synchronized void incrementStatic() {
staticCount++;
}
public static synchronized int getStaticCount() {
return staticCount;
}
}
2. Synchronized Blocks
Synchronized blocks provide more fine-grained control over locking. You can specify any object as the lock. This is useful when you only need to protect a small part of a method, or when you want to use a different lock object than this or the Class object.
class DataProcessor {
private Object lock = new Object();
private int data = 0;
public void process() {
// ... some non-critical operations ...
synchronized (lock) {
// Critical section: only one thread can execute this at a time
data++;
}
// ... other non-critical operations ...
}
}
Key Characteristics and Guarantees
- Mutual Exclusion: Only one thread can execute a synchronized method or block on a given object at a time.
- Visibility: When a thread releases a lock, all writes made by that thread are guaranteed to be visible to any other thread that subsequently acquires the same lock. This addresses the 'memory visibility problem'.
- Atomicity: It ensures that operations within the synchronized block/method are performed as a single, indivisible unit.
- Happens-Before Relationship: Entering a synchronized block/method establishes a happens-before relationship with any subsequent execution of a synchronized block/method on the same monitor.
Potential Issues: Deadlock
While synchronized is powerful, its misuse can lead to deadlocks. A deadlock occurs when two or more threads are blocked indefinitely, each waiting for the other to release a resource (lock) that it needs.
Best Practices
- Synchronize on private final objects: This prevents other code from acquiring the lock for unrelated purposes, potentially causing deadlocks or performance issues.
- Keep synchronized blocks small: Only protect the critical section of code that truly needs thread safety. Over-synchronization can lead to performance bottlenecks.
- Avoid synchronizing on
thisorStringliterals: Synchronizing onthiscan expose your internal lock, allowing external code to block your object. Synchronizing onStringliterals can lead to unintended locking as Java internsStringliterals, meaning multiple seemingly different strings might refer to the same object. - Consider
java.util.concurrentutilities: For more complex concurrency scenarios, classes likeReentrantLock,Semaphore,CountDownLatch, and atomic variables (e.g.,AtomicInteger) from thejava.util.concurrentpackage often provide more flexibility and better performance thansynchronized.