All topics
Java Interview Questions & Answers
39 questions with detailed answers — for freshers and experienced candidates.
Want to actually learn Java?
Join a hands-on mini internship or training on iCampusLink and earn a certificate.
Explore programs →Fresher Level
Q1. What is the difference between JDK, JRE, and JVM?
The JVM (Java Virtual Machine) is an abstract machine that provides a runtime environment for executing Java bytecode. It's the core component. The JRE (Java Runtime Environment) is the JVM along with the core classes and supporting files required to run Java applications. It does not include development tools. The JDK (Java Development Kit) is a superset of JRE, containing the JRE, along with development tools like the Java compiler (javac), debugger, and documentation tools. Developers use the JDK to write, compile, and run Java programs, while end-users typically only need the JRE to execute them.
Q2. Explain the concept of "Pass by Value" in Java.
In Java, arguments are always passed by value. This means that when you pass a primitive data type (like int, char, boolean) to a method, a copy of its value is passed. Any changes made to the parameter inside the method will not affect the original variable. For objects, a copy of the *reference* to the object is passed. While the reference itself is passed by value (a copy is made), both the original and copied references point to the same object in memory. Therefore, changes made to the object's state through the parameter inside the method *will* affect the original object.
public class PassByValueDemo {
public static void main(String[] args) {
int x = 10;
modifyPrimitive(x);
System.out.println("Original int after method: " + x); // Output: 10
StringBuilder sb = new StringBuilder("Original");
modifyObject(sb);
System.out.println("Original object after method: " + sb); // Output: Original Modified
}
public static void modifyPrimitive(int num) {
num = 20; // Changes local copy, original 'x' is unaffected
}
public static void modifyObject(StringBuilder obj) {
obj.append(" Modified"); // Changes the object content, visible to original 'sb'
// obj = new StringBuilder("New Object"); // This would not affect the original reference 'sb'
}
}
Q3. What are the main principles of Object-Oriented Programming (OOP) in Java?
The four main principles of OOP are Encapsulation, Inheritance, Polymorphism, and Abstraction.
* **Encapsulation**: Bundling data (attributes) and methods (behaviors) that operate on the data into a single unit (class), and restricting direct access to some of the object's components.
* **Inheritance**: A mechanism where one class acquires the properties and behaviors of another class, promoting code reusability.
* **Polymorphism**: The ability of an object to take on many forms. In Java, this is achieved through method overloading (compile-time) and method overriding (runtime).
* **Abstraction**: Hiding complex implementation details and showing only essential features of the object. Achieved using abstract classes and interfaces.
Q4. Differentiate between `==` operator and `.equals()` method in Java.
The `==` operator is used for reference comparison (checking if two references point to the exact same object in memory) for non-primitive types, and value comparison for primitive types. The `.equals()` method, on the other hand, is used for content comparison. For primitive wrapper classes and `String` objects, `equals()` compares the actual content or value. If not overridden in a custom class, the default `Object.equals()` method behaves exactly like `==`, performing reference comparison. It's crucial to override `equals()` in custom classes if you want to define what equality means for objects of that class based on their state.
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false (different objects)
System.out.println(s1.equals(s2)); // true (same content)
Q5. What is method overloading and method overriding?
* **Method Overloading**: Occurs when a class has multiple methods with the same name but different parameter lists (different number of parameters, different types of parameters, or different order of parameter types). The return type can be different but is not sufficient to distinguish overloaded methods. It's a compile-time polymorphism.
* **Method Overriding**: Occurs when a subclass provides a specific implementation for a method that is already defined in its superclass. The method signature (name, parameter list, and return type) must be the same as in the superclass. It's a runtime polymorphism. The `@Override` annotation is often used to ensure correct overriding.
class Animal { void eat() {} }
class Dog extends Animal {
void eat(String food) {} // Overloading (different parameter list)
@Override void eat() {} // Overriding (same signature as superclass method)
}
Q6. Explain the purpose of the `final` keyword in Java.
The `final` keyword is a non-access modifier used for classes, methods, and variables.
* **Final Variable**: Once assigned, its value cannot be changed. For reference variables, the reference itself cannot be changed to point to another object, but the object's internal state can be modified if it's mutable.
* **Final Method**: Cannot be overridden by subclasses. This ensures that the method's implementation remains consistent across the inheritance hierarchy.
* **Final Class**: Cannot be subclassed (inherited from). This is often used for security reasons or to create immutable classes like `String`.
final int MAX_VALUE = 100;
// MAX_VALUE = 200; // Compile-time error: cannot assign a value to a final variable
final class ImmutableClass {
// This class cannot be extended
}
// class SubClass extends ImmutableClass {} // Compile-time error
class Parent { final void display() {} }
class Child extends Parent {
// @Override void display() {} // Compile-time error: cannot override final method
}
Q7. What is an abstraction and how is it achieved in Java?
Abstraction is the process of hiding the implementation details and showing only the essential features of an object or system. It focuses on "what" an object does rather than "how" it does it. In Java, abstraction is primarily achieved through two mechanisms:
* **Abstract Classes**: A class declared with the `abstract` keyword. It can have abstract methods (methods without an implementation) and concrete methods. An abstract class cannot be instantiated directly and must be subclassed.
* **Interfaces**: A blueprint of a class. It can contain method signatures (Java 8+ allows default and static methods) but no concrete method implementations (before Java 8). Classes implement interfaces to provide the implementation for the defined methods. Interfaces support multiple inheritance of type.
abstract class Shape {
abstract void draw(); // Abstract method
void printColor() { System.out.println("Color"); } // Concrete method
}
interface Drawable {
void draw(); // Implicitly public abstract
default void resize() { System.out.println("Resizing"); } // Java 8 default method
}
Q8. What is the difference between an `ArrayList` and a `LinkedList` in Java?
Both `ArrayList` and `LinkedList` implement the `List` interface but use different underlying data structures.
* **`ArrayList`**: Uses a dynamic array to store elements. It provides fast random access (O(1)) because elements can be accessed directly by index. Adding or removing elements in the middle requires shifting subsequent elements, leading to O(n) complexity. Adding elements at the end is usually O(1) amortized time, but can be O(n) if resizing is needed.
* **`LinkedList`**: Uses a doubly-linked list. Each element (node) stores a reference to the previous and next elements. It provides fast insertions and deletions (O(1)) once the position is found, as only pointers need to be updated. However, random access (getting an element by index) is slow (O(n)) because it requires traversing the list from the beginning or end.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
List<String> arrayList = new ArrayList<>(); // Good for random access, poor for middle insertions/deletions
List<String> linkedList = new LinkedList<>(); // Poor for random access, good for insertions/deletions at ends/middle
Q9. Explain the concept of Garbage Collection in Java.
Garbage Collection (GC) is an automatic memory management process in Java that reclaims memory occupied by objects that are no longer referenced by the program. Unlike languages like C++, developers don't explicitly deallocate memory. The JVM's garbage collector automatically identifies and frees up memory from "dead" objects, preventing memory leaks and simplifying memory management for developers. When an object becomes unreachable, it becomes eligible for garbage collection. The GC process runs as a low-priority background thread and typically involves marking reachable objects, then sweeping away the unreachable ones. Modern JVMs use sophisticated generational, concurrent, and parallel collectors (e.g., G1, ZGC) to optimize performance and minimize application pauses.
Q10. What is the purpose of the `static` keyword in Java?
The `static` keyword is used to create class-level members (variables and methods) rather than instance-level members.
* **Static Variable (Class Variable)**: Belongs to the class, not to any specific object. All instances of the class share the same static variable. It's initialized once when the class is loaded.
* **Static Method (Class Method)**: Belongs to the class and can be called directly using the class name, without creating an object. Static methods can only access static variables and other static methods directly; they cannot access instance variables or instance methods without an object reference.
* **Static Block**: Used to initialize static variables. It's executed exactly once when the class is loaded into memory.
* **Static Nested Class**: A nested class that is static. It doesn't require an outer class instance and can access only static members of the outer class.
class MyClass {
static int count = 0; // Static variable
static {
System.out.println("Static block executed.");
count = 10;
}
static void displayCount() { // Static method
System.out.println("Count: " + count);
}
class InnerClass { /* non-static nested class */ }
static class StaticNestedClass { /* static nested class */ }
}
// Usage:
// MyClass.displayCount(); // Calls static method without object
// System.out.println(MyClass.count); // Access static variable directly
Intermediate Level
Q1. How does Java achieve platform independence?
Java achieves platform independence through the use of the Java Virtual Machine (JVM) and bytecode. When Java source code (`.java` files) is compiled, it's not translated into machine-specific native code, but into an intermediate format called bytecode (`.class` files). This bytecode is a set of instructions for the JVM. The JVM acts as an interpreter and runtime environment that executes this bytecode. Since a JVM exists for various operating systems and hardware architectures, the same compiled bytecode can run on any platform that has a compatible JVM installed. This "write once, run anywhere" capability is a cornerstone of Java's design.
Q2. Explain the concept of checked and unchecked exceptions in Java.
Exceptions in Java are broadly categorized into checked and unchecked exceptions.
* **Checked Exceptions**: These are exceptions that the compiler forces you to handle. They typically represent situations that a well-written application should anticipate and recover from, such as `IOException`, `SQLException`, or `ClassNotFoundException`. You must either catch them using a `try-catch` block or declare them using the `throws` keyword in the method signature.
* **Unchecked Exceptions**: These are exceptions that the compiler does not force you to handle. They typically represent programming errors or unrecoverable conditions, such as `NullPointerException`, `ArrayIndexOutOfBoundsException`, or `ArithmeticException`. These are subclasses of `RuntimeException`. While you *can* catch them, it's generally considered bad practice as they indicate a bug that should be fixed rather than handled programmatically.
import java.io.FileReader;
import java.io.IOException;
class ExceptionDemo {
// Checked exception: Must be handled or declared
void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
// ... read from reader ...
reader.close();
}
// Unchecked exception: No compiler enforcement
void divide(int a, int b) {
if (b == 0) {
// Typically, fix the logic to avoid division by zero
// rather than catching ArithmeticException
}
int result = a / b; // ArithmeticException possible at runtime
System.out.println(result);
}
public static void main(String[] args) {
ExceptionDemo demo = new ExceptionDemo();
try {
demo.readFile("nonexistent.txt");
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
demo.divide(10, 0); // Will throw unchecked ArithmeticException
}
}
Q3. What are Wrapper Classes in Java and why are they used?
Wrapper classes in Java provide a way to use primitive data types (like `int`, `char`, `boolean`, `double`) as objects. For each primitive type, there's a corresponding wrapper class in the `java.lang` package (e.g., `Integer` for `int`, `Character` for `char`). They are used for several reasons:
1. **Collections Framework**: Java collections (like `ArrayList`, `HashMap`) store objects, not primitive types. Wrapper classes allow primitives to be stored in collections.
2. **Generics**: Generic types only work with objects.
3. **Null Values**: Wrapper class objects can hold `null`, whereas primitives cannot, which is useful for representing the absence of a value.
4. **Utility Methods**: Wrapper classes provide useful methods, such as converting strings to primitives (`Integer.parseInt()`) or primitives to strings (`Integer.toString()`).
Java provides autoboxing and unboxing features (since Java 5) to automatically convert between primitive types and their corresponding wrapper objects, simplifying their usage.
import java.util.ArrayList;
import java.util.List;
public class WrapperClassDemo {
public static void main(String[] args) {
// Autoboxing: int primitive to Integer object
Integer i = 100;
System.out.println("Autoboxed Integer: " + i);
// Unboxing: Integer object to int primitive
int j = i;
System.out.println("Unboxed int: " + j);
// Usage in Collections (requires objects)
List<Integer> numbers = new ArrayList<>();
numbers.add(50); // Autoboxing: 50 (int) -> new Integer(50)
int firstNum = numbers.get(0); // Unboxing: Integer(50) -> 50 (int)
System.out.println("Number from list: " + firstNum);
// Utility method
String str = "123";
int parsedInt = Integer.parseInt(str);
System.out.println("Parsed int: " + parsedInt);
}
}
Q4. Explain the concept of 'immutable' objects in Java with an example.
An immutable object is an object whose state cannot be changed after it is created. Once an immutable object is initialized, its internal data remains constant throughout its lifetime. `String` is the most common example of an immutable class in Java.
Key benefits of immutability include:
1. **Thread Safety**: Immutable objects are inherently thread-safe as their state cannot be modified by multiple threads concurrently, eliminating the need for synchronization.
2. **Security**: Useful for sensitive data where state should not change, preventing unexpected modifications.
3. **Cacheability**: Can be easily cached and reused, as their state won't change.
4. **Simplicity**: Easier to reason about and debug because their state is fixed.
To create an immutable class, you typically: declare the class `final`, make all fields `private` and `final`, do not provide setter methods, and ensure any mutable object fields are defensively copied in the constructor and getter methods.
final class MyImmutableClass {
private final int value;
private final String name;
private final MyMutableObject mutableObject; // Example of a mutable field
public MyImmutableClass(int value, String name, MyMutableObject mutableObject) {
this.value = value;
this.name = name;
// Defensive copy for mutable objects
this.mutableObject = new MyMutableObject(mutableObject.getData());
}
public int getValue() { return value; }
public String getName() { return name; }
public MyMutableObject getMutableObject() {
// Return a defensive copy to prevent external modification
return new MyMutableObject(this.mutableObject.getData());
}
}
class MyMutableObject {
private String data;
public MyMutableObject(String data) { this.data = data; }
public String getData() { return data; }
public void setData(String data) { this.data = data; }
}
Q5. What is the difference between `interface` and `abstract class`?
Both `interface` and `abstract class` are used to achieve abstraction, but they have key differences:
* **Methods**: An `interface` before Java 8 could only have abstract methods (public and abstract by default). Since Java 8, it can have default and static methods. An `abstract class` can have abstract and concrete (non-abstract) methods.
* **Variables**: `interface` variables are implicitly `public static final`. An `abstract class` can have any type of variables (static, non-static, final, non-final).
* **Constructors**: An `abstract class` can have constructors, but an `interface` cannot.
* **Inheritance/Implementation**: A class `implements` an `interface` and `extends` an `abstract class`. A class can implement multiple interfaces but can only extend one abstract class.
* **Access Modifiers**: Interface methods are implicitly `public`. Abstract class methods can have any access modifier (public, protected, private, default).
Interfaces are used to define a contract or capability that a class promises to fulfill, often representing a "has-a" relationship. Abstract classes are used to provide a common base implementation for related classes that share a common behavior, often representing an "is-a" relationship.
interface MyInterface {
int MY_CONSTANT = 100; // public static final by default
void doSomething(); // public abstract by default
default void log(String msg) { System.out.println(msg); }
}
abstract class MyAbstractClass {
String name; // Can have non-final instance variables
public MyAbstractClass(String name) { this.name = name; } // Can have constructor
abstract void doAnotherThing(); // Abstract method
void commonMethod() { System.out.println("Common implementation"); } // Concrete method
}
class ConcreteClass extends MyAbstractClass implements MyInterface {
public ConcreteClass(String name) { super(name); }
@Override public void doSomething() { System.out.println("Implementing interface method"); }
@Override public void doAnotherThing() { System.out.println("Implementing abstract class method"); }
}
Q6. Explain `hashCode()` and `equals()` contract in Java.
The `hashCode()` and `equals()` methods are defined in the `Object` class and are fundamental for correct object comparison and storage in hash-based collections (`HashMap`, `HashSet`, `Hashtable`). Their contract states:
1. **Consistency with equals**: If two objects are equal according to the `equals(Object)` method, then calling the `hashCode()` method on each of the two objects must produce the same integer result.
2. **Consistency within an object**: If an object is not modified, its `hashCode()` method must consistently return the same integer whenever it is invoked on that object.
3. **No requirement for unequal objects**: If two objects are not equal according to the `equals(Object)` method, their `hashCode()` values are not required to be different. However, distinct hash codes for unequal objects can significantly improve the performance of hash tables.
Failing to adhere to this contract can lead to unpredictable behavior, especially when using objects as keys in `HashMap` or elements in `HashSet`, where objects might not be found even if they logically exist. It's best practice to override both or neither, and IDEs often provide generation tools for these methods.
import java.util.Objects;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
public static void main(String[] args) {
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
Person p3 = new Person("Bob", 25);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true (due to contract)
System.out.println(p1.equals(p3)); // false
System.out.println(p1.hashCode() == p3.hashCode()); // false (good distribution)
}
}
Q7. What is the purpose of the `transient` keyword in Java?
The `transient` keyword in Java is used to mark a field of an object to indicate that it should not be serialized when the object is written to a persistent storage (like a file) or sent over a network. When a `transient` field is deserialized, its value will be initialized to the default value for its type (e.g., `null` for object references, `0` for `int`, `false` for `boolean`). This is useful for fields that contain sensitive information (like passwords) that shouldn't be persisted, derived data that can be recomputed, or resources that cannot be serialized directly (like `Thread` objects, `FileInputStream`s, or database connections). It helps to manage the state that needs to be persisted versus the state that doesn't.
import java.io.Serializable;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
class User implements Serializable {
String username;
transient String password; // This field will not be serialized
int age;
public User(String username, String password, int age) {
this.username = username;
this.password = password;
this.age = age;
}
public String toString() {
return "User{username='" + username + "', password='" + password + "', age=" + age + "}";
}
public static void main(String[] args) {
User user = new User("john.doe", "secret123", 30);
System.out.println("Before serialization: " + user);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("User object serialized.");
} catch (Exception e) {
e.printStackTrace();
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User deserializedUser = (User) ois.readObject();
System.out.println("After deserialization: " + deserializedUser); // password will be null
} catch (Exception e) {
e.printStackTrace();
}
}
}
Q8. How do you create and manage threads in Java?
In Java, threads can be created in two main ways:
1. **Extending the `Thread` class**: Create a class that extends `java.lang.Thread` and override its `run()` method. Then, create an instance of this class and call its `start()` method.
2. **Implementing the `Runnable` interface**: Create a class that implements `java.lang.Runnable` and override its `run()` method. Then, create a `Thread` object, passing an instance of your `Runnable` implementation to its constructor, and call the `Thread`'s `start()` method.
Implementing `Runnable` is generally preferred because it allows your class to extend another class (Java does not support multiple inheritance for classes), and promotes better separation of concerns (the task to be executed is separate from the thread that executes it).
Threads are managed using methods like `start()` (to begin execution), `join()` (to wait for a thread to complete), `sleep()` (to pause execution), `interrupt()` (to request a thread to stop), and increasingly, through the `java.util.concurrent` package (Executors, ThreadPools, `Callable` for returning results and throwing exceptions).
class MyRunnable implements Runnable {
private String taskName;
public MyRunnable(String taskName) { this.taskName = taskName; }
@Override
public void run() {
System.out.println(taskName + " is running in thread: " + Thread.currentThread().getName());
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println(taskName + " finished.");
}
}
class MyThread extends Thread {
private String taskName;
public MyThread(String taskName) { this.taskName = taskName; }
@Override
public void run() {
System.out.println(taskName + " is running in thread: " + Thread.currentThread().getName());
}
}
public class ThreadCreationDemo {
public static void main(String[] args) throws InterruptedException {
// Using Runnable interface
Thread thread1 = new Thread(new MyRunnable("Runnable Task"), "RunnableThread-1");
thread1.start();
// Extending Thread class
MyThread thread2 = new MyThread("Thread Class Task");
thread2.setName("MyThread-2");
thread2.start();
thread1.join(); // Wait for thread1 to finish
thread2.join(); // Wait for thread2 to finish
System.out.println("Main thread finished.");
}
}
Q9. Explain the concept of "synchronization" in multithreading.
Synchronization in Java is a mechanism used to control access to shared resources by multiple threads, preventing data inconsistency and race conditions. When multiple threads try to access and modify the same shared data concurrently, it can lead to incorrect results (e.g., a counter incremented by two threads might only increase by one due to interleaved operations).
Java provides the `synchronized` keyword for this purpose. It can be applied to methods or blocks of code, ensuring that only one thread can execute the synchronized code at a time for a given monitor object.
* **Synchronized Method**: When a method is declared `synchronized`, the thread executing it acquires an intrinsic lock (monitor) on the object (for instance methods) or the class (for static methods) before entering the method and releases it upon exit (either normal or abrupt). Only one thread can hold this lock at a time.
* **Synchronized Block**: Allows finer-grained control by specifying a monitor object on which to acquire the lock. Only one thread can execute the synchronized block at a time for the given monitor object. This is often preferred for synchronizing only the critical section of code.
Synchronization ensures atomicity (operations appear to happen instantaneously) and visibility (changes to shared variables are visible across threads) of shared data.
class Counter {
private int count = 0;
// Synchronized method: lock on 'this' (the Counter instance)
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " increments to: " + count);
}
// Synchronized block: lock on 'this'
public void decrement() {
synchronized (this) {
count--;
System.out.println(Thread.currentThread().getName() + " decrements to: " + count);
}
}
public int getCount() { return count; }
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
counter.decrement();
}
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount()); // Should be 0
}
}
Q10. What are Generics in Java and what are their benefits?
Generics, introduced in Java 5, allow you to write classes, interfaces, and methods that operate on objects of various types while providing compile-time type safety. Instead of working with `Object` and then casting, you can specify type parameters (e.g., `<T>`, `<E>`, `<K, V>`) when defining generic types or methods.
Benefits include:
1. **Type Safety**: Catches type mismatch errors at compile time rather than runtime, preventing `ClassCastException`s that might occur with raw types.
2. **Code Reusability**: You can write a single generic class/method that works with different types, eliminating the need for boilerplate code (e.g., `List<String>` and `List<Integer>` use the same `ArrayList` implementation).
3. **Elimination of Casts**: No need for explicit type casting when retrieving elements from generic collections, making code cleaner and safer.
4. **Improved Readability**: The code becomes more expressive and easier to understand by clearly indicating the types it operates on.
import java.util.ArrayList;
import java.util.List;
public class GenericsDemo {
public static void main(String[] args) {
// Without generics (raw type) - requires casting, prone to ClassCastException
List rawList = new ArrayList();
rawList.add("hello");
rawList.add(123); // Can add any object
String s1 = (String) rawList.get(0);
// String s2 = (String) rawList.get(1); // Runtime ClassCastException
// With generics - compile-time type safety, no casting needed
List<String> names = new ArrayList<>(); // Type parameter String
names.add("Alice");
names.add("Bob");
// names.add(123); // Compile-time error: type mismatch
String name = names.get(0); // No cast needed
System.out.println(name);
// Generic method example
printList(names);
}
public static <T> void printList(List<T> list) {
for (T element : list) {
System.out.println(element);
}
}
}
Q11. Explain the concept of Autoboxing and Unboxing in Java.
Autoboxing and unboxing are features introduced in Java 5 that simplify the conversion between primitive types and their corresponding wrapper classes.
* **Autoboxing**: The automatic conversion that the Java compiler makes between a primitive type and its corresponding wrapper class. For example, an `int` can be directly assigned to an `Integer` object, or passed to a method expecting an `Integer`.
* **Unboxing**: The automatic conversion that the Java compiler makes from a wrapper class object to its corresponding primitive type. For example, an `Integer` object can be directly assigned to an `int` variable, or passed to a method expecting an `int`.
These features reduce boilerplate code and make it easier to work with primitive types in contexts where objects are required (like collections, generics, or reflection). However, it's important to be aware of potential `NullPointerException` during unboxing if the wrapper object being unboxed is `null`.
import java.util.ArrayList;
import java.util.List;
public class AutoboxingUnboxingDemo {
public static void main(String[] args) {
// Autoboxing: int primitive to Integer object
Integer i = 100; // Compiler converts 100 (int) to new Integer(100)
System.out.println("Autoboxed Integer: " + i);
// Unboxing: Integer object to int primitive
int j = i; // Compiler converts i (Integer) to i.intValue() (int)
System.out.println("Unboxed int: " + j);
// Autoboxing in method calls
printInteger(200); // int 200 is autoboxed to Integer
// Unboxing with null can cause NullPointerException
Integer nullInteger = null;
try {
// int k = nullInteger; // This would throw NullPointerException at runtime
System.out.println("Attempting to unbox null...");
} catch (NullPointerException e) {
System.out.println("Caught NullPointerException during unboxing.");
}
// Autoboxing in collections
List<Integer> list = new ArrayList<>();
list.add(50); // Autoboxing: 50 (int) -> Integer(50)
int value = list.get(0); // Unboxing: Integer(50) -> 50 (int)
System.out.println("Value from list: " + value);
}
public static void printInteger(Integer num) {
System.out.println("Received Integer object: " + num);
}
}
Q12. What is the `try-with-resources` statement and its benefits?
The `try-with-resources` statement, introduced in Java 7, is a `try` statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it (e.g., `InputStream`, `OutputStream`, `Connection`, `Reader`, `Writer`). Any object that implements the `java.lang.AutoCloseable` interface can be used as a resource. The `try-with-resources` statement ensures that each resource is closed automatically at the end of the statement, regardless of whether the `try` block completes normally or abruptly (due to an exception).
Benefits include:
1. **Reduced Boilerplate**: Eliminates the need for explicit `finally` blocks to close resources, making code cleaner and more concise.
2. **Improved Reliability**: Guarantees that resources are always closed, even if exceptions occur, preventing resource leaks.
3. **Better Exception Handling**: If multiple exceptions occur (one in the `try` block and one during resource closing), the original exception from the `try` block is preserved, and subsequent exceptions from the `close()` method are suppressed. These suppressed exceptions can be retrieved using `Throwable.getSuppressed()`.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesDemo {
public static void main(String[] args) {
// Example of try-with-resources with a single resource
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
// Example with multiple resources (separated by semicolon)
try (FileReader fr = new FileReader("input.txt");
BufferedReader br = new BufferedReader(fr)) {
System.out.println("Reading from multiple resources...");
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error with multiple resources: " + e.getMessage());
}
}
}
Q13. Differentiate between `HashMap`, `LinkedHashMap`, and `TreeMap`.
All three implement the `Map` interface, storing key-value pairs, but differ in their internal structure, ordering guarantees, and performance characteristics:
* **`HashMap`**: Provides no guaranteed order of elements (neither insertion order nor sorted order). It stores elements based on their hash codes, offering O(1) average time complexity for `put`, `get`, `remove` operations. In the worst case (many hash collisions), performance can degrade to O(n). It allows one `null` key and multiple `null` values. It's generally the fastest option when order is not important.
* **`LinkedHashMap`**: Maintains insertion order (the order in which elements were added to the map) or access order (the order in which elements were last accessed, useful for LRU caches). It uses a hash table for efficient O(1) lookups and a doubly-linked list to maintain order. Its performance characteristics for `put`, `get`, `remove` are similar to `HashMap` (O(1) average).
* **`TreeMap`**: Stores elements in natural sorted order of keys or by a custom `Comparator` provided at creation. It's based on a Red-Black tree, providing O(log n) time complexity for `put`, `get`, `remove` operations. It does not allow `null` keys (as it needs to compare them) but allows `null` values. It also provides methods for navigating the map, like `firstKey()`, `lastKey()`, `headMap()`, `tailMap()`.
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class MapComparisonDemo {
public static void main(String[] args) {
System.out.println("HashMap (No guaranteed order):");
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("Apple", 1);
hashMap.put("Banana", 2);
hashMap.put("Cherry", 3);
System.out.println(hashMap); // Order may vary
System.out.println("\nLinkedHashMap (Insertion order):");
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("Apple", 1);
linkedHashMap.put("Banana", 2);
linkedHashMap.put("Cherry", 3);
System.out.println(linkedHashMap); // Preserves insertion order
System.out.println("\nTreeMap (Sorted order):");
Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("Apple", 1);
treeMap.put("Banana", 2);
treeMap.put("Cherry", 3);
System.out.println(treeMap); // Sorted by key
}
}
Q14. What are the key features introduced in Java 8?
Java 8 brought several significant features that revolutionized how Java is written, moving towards more functional programming paradigms:
1. **Lambda Expressions**: Provide a concise syntax for implementing functional interfaces, making code more readable for single-method interfaces. Enables functional programming.
2. **Stream API**: A powerful API for processing collections of objects in a declarative and functional style, supporting operations like `filter`, `map`, `reduce`, `forEach`, etc., which can be executed sequentially or in parallel.
3. **Default Methods in Interfaces**: Allows adding new methods to interfaces without breaking existing implementations, promoting backward compatibility and enabling interface evolution.
4. **`java.time` API (Date and Time API)**: A modern, immutable, and thread-safe API for handling dates and times, replacing the old, problematic `java.util.Date` and `Calendar` classes.
5. **Method References**: A shorthand for lambda expressions, referring to methods by their names, making code even more compact and readable when lambdas just call an existing method.
6. **`Optional` Class**: A container object that may or may not contain a non-null value, designed to combat `NullPointerException`s by forcing explicit handling of absent values.
7. **`CompletableFuture`**: Enhanced asynchronous programming, allowing complex asynchronous computations to be chained and composed.
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class Java8FeaturesDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 1. Lambda Expressions and Stream API
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A")) // Lambda in filter
.map(String::toUpperCase) // Method reference in map
.collect(Collectors.toList());
System.out.println("Filtered & Mapped: " + filteredNames); // Output: [ALICE]
// 2. ForEach with Lambda
names.forEach(name -> System.out.println("Hello, " + name));
// 3. Date and Time API
LocalDate today = LocalDate.now();
System.out.println("Today's date: " + today);
// 4. Optional Class
Optional<String> optionalName = Optional.ofNullable("Eve");
optionalName.ifPresent(n -> System.out.println("Optional value: " + n));
String defaultName = Optional.empty().orElse("Guest");
System.out.println("Default name: " + defaultName);
}
}
Q15. How does `try-catch-finally` work? What happens if an exception occurs in the `finally` block?
The `try-catch-finally` block is a fundamental construct in Java for handling exceptions and ensuring proper resource management.
* **`try` block**: Contains the code that might throw an exception. If an exception occurs here, execution immediately jumps to the appropriate `catch` block (if one exists).
* **`catch` block(s)**: Follows a `try` block and is used to handle specific types of exceptions. If an exception's type matches a `catch` block's declared exception type (or a superclass), that `catch` block is executed. Multiple `catch` blocks can be used to handle different exception types.
* **`finally` block**: This block is optional but, if present, its code is *always* executed, regardless of whether an exception occurred in the `try` block, was caught by a `catch` block, or the `try` block completed normally. It's typically used for cleanup operations, such as closing files, database connections, or network sockets.
**What happens if an exception occurs in the `finally` block?**
If an exception occurs within the `finally` block, it will override any pending exception from the `try` or `catch` block. The original exception will be lost, and the new exception thrown from the `finally` block will be propagated to the caller. This behavior can mask the root cause of the original problem, which is why it's generally advised to handle exceptions *within* the `finally` block itself (e.g., wrap resource closing in its own `try-catch`) or, even better, use the `try-with-resources` statement (introduced in Java 7), which automatically handles resource closing and correctly suppresses exceptions.
public class TryCatchFinallyDemo {
public static void main(String[] args) {
System.out.println("Scenario 1: No exception in try");
try {
System.out.println("Inside try block (no exception).");
} catch (ArithmeticException e) {
System.out.println("Inside catch block.");
} finally {
System.out.println("Inside finally block (no exception).");
}
System.out.println("\nScenario 2: Exception in try, caught by catch");
try {
System.out.println("Inside try block (throwing exception).");
int result = 10 / 0; // Throws ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Inside catch block: " + e.getMessage());
} finally {
System.out.println("Inside finally block (after catch).");
}
System.out.println("\nScenario 3: Exception in try, not caught, then exception in finally");
try {
System.out.println("Inside try block (throwing IOException).");
throw new java.io.IOException("Original IO Error");
} catch (ArithmeticException e) {
System.out.println("Inside catch block (won't execute).");
} finally {
System.out.println("Inside finally block (throwing new exception).");
// This new exception will mask the original IOException
throw new RuntimeException("Finally block error");
}
}
}
Q16. What is the purpose of the `volatile` keyword in Java?
The `volatile` keyword in Java is used to ensure visibility of changes to variables across threads and to prevent instruction reordering by the compiler and JVM. When a variable is declared `volatile`, two guarantees are made:
1. **Visibility**: Changes made by one thread to a `volatile` variable are immediately visible to all other threads. This prevents threads from working with stale, cached values of the variable in their local memory (CPU caches).
2. **Ordering**: It establishes a happens-before relationship, meaning that all writes to `volatile` fields happen-before any subsequent reads of the same `volatile` field. This prevents the compiler and JVM from reordering instructions around `volatile` reads/writes, ensuring that operations before a `volatile` write are visible, and operations after a `volatile` read only occur after the read.
`volatile` is weaker than `synchronized`. It only guarantees visibility and ordering, not atomicity. For atomic operations (like incrementing a counter), `synchronized` blocks/methods or classes from `java.util.concurrent.atomic` package (e.g., `AtomicInteger`) should be used, as `volatile` `count++` is not atomic.
It's typically used for status flags or small data items that are shared between threads where only one thread writes and other threads read the variable's value.
public class VolatileDemo {
private volatile boolean flag = false; // Ensures visibility of 'flag' changes
public void setFlag() {
flag = true; // Write to volatile variable
System.out.println(Thread.currentThread().getName() + " set flag to true.");
}
public void waitForFlag() {
System.out.println(Thread.currentThread().getName() + " waiting for flag to be true...");
// Loop until 'flag' becomes true. Without volatile, this might spin indefinitely
// if the compiler optimizes 'flag' to a local register or cache.
while (!flag) {
// busy-wait
}
System.out.println(Thread.currentThread().getName() + " detected flag is true!");
}
public static void main(String[] args) throws InterruptedException {
VolatileDemo demo = new VolatileDemo();
Thread readerThread = new Thread(demo::waitForFlag, "ReaderThread");
Thread writerThread = new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
demo.setFlag();
}, "WriterThread");
readerThread.start();
writerThread.start();
readerThread.join();
writerThread.join();
System.out.println("Main thread finished.");
}
}
Q17. How do you prevent a class from being serialized in Java?
There are two primary ways to prevent a class from being serialized in Java:
1. **Do not implement the `java.io.Serializable` interface**: This is the simplest and most common way. If a class does not implement `Serializable`, its objects cannot be serialized by Java's default serialization mechanism. If a superclass implements `Serializable`, but a subclass does not, the subclass's fields will not be serialized, but the superclass's serializable fields will be.
2. **Implement `readObject()` and `writeObject()` methods with `ObjectNotSerializableException`**: For a class that *does* implement `Serializable` (perhaps because a superclass does), you can explicitly prevent its serialization by providing custom `private void writeObject(ObjectOutputStream out) throws IOException` and `private void readObject(ObjectInputStream in) throws IOException` methods. Inside these methods, you would throw `java.io.ObjectNotSerializableException`. This gives you fine-grained control and can be useful if you only want to prevent serialization under certain conditions or for specific subclasses.
Additionally, marking individual fields as `transient` prevents only those specific fields from being serialized, allowing the rest of the object to be serialized.
import java.io.*;
// Method 1: Do not implement Serializable
class NonSerializableClass {
String data;
public NonSerializableClass(String data) { this.data = data; }
public String toString() { return "NonSerializableClass{data='" + data + "'}"; }
}
// Method 2: Implement Serializable, but throw exception in custom methods
class MyClassPreventSerialization implements Serializable {
private String secretData;
public MyClassPreventSerialization(String secretData) {
this.secretData = secretData;
}
private void writeObject(ObjectOutputStream out) throws IOException {
throw new NotSerializableException("This class is explicitly not serializable!");
}
private void readObject(ObjectInputStream in) throws IOException {
throw new NotSerializableException("This class is explicitly not deserializable!");
}
public String toString() { return "MyClassPreventSerialization{secretData='" + secretData + "'}"; }
}
public class PreventSerializationDemo {
public static void main(String[] args) {
// Demo for Method 1
NonSerializableClass obj1 = new NonSerializableClass("Sensitive Info");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj1.ser"))) {
oos.writeObject(obj1);
System.out.println("Attempted to serialize NonSerializableClass (will fail).");
} catch (NotSerializableException e) {
System.out.println("Caught expected error for NonSerializableClass: " + e.getMessage());
} catch (IOException e) {
e.printStackTrace();
}
// Demo for Method 2
MyClassPreventSerialization obj2 = new MyClassPreventSerialization("Another Secret");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("obj2.ser"))) {
oos.writeObject(obj2);
System.out.println("Attempted to serialize MyClassPreventSerialization (will fail).");
} catch (NotSerializableException e) {
System.out.println("Caught expected error for MyClassPreventSerialization: " + e.getMessage());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Advanced Level
Q1. Explain the internal working of a `HashMap` in Java.
`HashMap` internally uses an array of `Node` objects (or `Entry` objects in older Java versions) to store key-value pairs. Each index in this array is called a bucket.
When you `put(key, value)`:
1. **Hashing**: The `hashCode()` method of the key object is computed. This hash code is then processed through a hashing function (which often includes `XOR`ing with its higher bits to reduce collisions for keys with similar hash codes) to generate an index for the internal array.
2. **Bucket Placement**: The calculated index determines which bucket (array slot) the key-value pair will be stored in.
3. **Collision Handling**: If multiple keys hash to the same bucket (a collision occurs), the `Node`s are stored in a data structure at that array index. Historically, this was a linked list. In Java 8 and later, if a linked list at a bucket exceeds a certain threshold (default 8 nodes) and the table capacity is sufficiently large, it is converted into a balanced tree (specifically a Red-Black tree). This improves worst-case performance from O(N) to O(logN) during lookups and insertions in highly-collided buckets.
4. **Key Equality**: When adding a new entry, if a key already exists in that bucket (either in a linked list or tree), its `equals()` method is used to compare the new key with existing keys. If an equal key is found, its associated value is updated; otherwise, a new `Node` is added.
When you `get(key)`:
1. **Hashing and Bucket Retrieval**: The `hashCode()` of the key is computed, and the same hashing function is applied to find the corresponding bucket in the array.
2. **Traversal and Comparison**: The `equals()` method is then used to traverse the linked list (or tree) at that bucket to find the exact `Node` matching the key. Once found, its associated value is returned.
`HashMap`'s performance relies heavily on a good `hashCode()` implementation to distribute keys evenly across buckets and minimize collisions. Poor hash code distribution leads to more elements in linked lists/trees, degrading average-case performance towards O(N) for operations.
Q2. What is the difference between `CountDownLatch` and `CyclicBarrier`?
Both `CountDownLatch` and `CyclicBarrier` are synchronization aids in `java.util.concurrent` used to coordinate multiple threads, but they serve different purposes:
* **`CountDownLatch`**: A one-time event barrier. It allows one or more threads to wait until a set of operations being performed by other threads completes. You initialize it with a count. Threads call `countDown()` to decrement the count. A waiting thread calls `await()` and blocks until the count reaches zero. Once the count reaches zero, it cannot be reset; you'd need a new `CountDownLatch` for a subsequent event. It's typically used when one thread needs to wait for multiple other threads to finish their tasks before proceeding (e.g., a main thread waiting for worker threads to complete initialization).
* **`CyclicBarrier`**: A reusable barrier. It allows a set of threads to wait for each other to reach a common barrier point. Once all threads (or the specified number of parties) reach the barrier, they are released, and the barrier can be reused for another cycle. It can also execute an optional `Runnable` command (a 'barrier action') once all threads reach the barrier, useful for performing an action before releasing the threads. It's typically used when a group of threads needs to repeatedly wait for each other at a synchronization point before continuing to the next phase of a computation.
In essence, `CountDownLatch` is for "one-shot" waiting for multiple threads to finish (one-to-many or many-to-one synchronization), while `CyclicBarrier` is for multiple threads repeatedly waiting for each other at a synchronization point (many-to-many synchronization).
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class BarrierComparisonDemo {
public static void main(String[] args) throws InterruptedException {
// --- CountDownLatch Demo ---
System.out.println("\n--- CountDownLatch Demo ---");
CountDownLatch latch = new CountDownLatch(3); // Main thread waits for 3 tasks
ExecutorService executorForLatch = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
int taskId = i + 1;
executorForLatch.submit(() -> {
try { Thread.sleep((long) (Math.random() * 1000)); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Task " + taskId + " finished. Count: " + latch.getCount());
latch.countDown(); // Decrement count
});
}
latch.await(); // Main thread waits until count is zero
System.out.println("Main thread released by CountDownLatch. All tasks completed.");
executorForLatch.shutdown();
// --- CyclicBarrier Demo ---
System.out.println("\n--- CyclicBarrier Demo ---");
// Barrier action runs when all 3 parties arrive
Runnable barrierAction = () -> System.out.println("--- All parties arrived at barrier! ---");
CyclicBarrier barrier = new CyclicBarrier(3, barrierAction);
ExecutorService executorForBarrier = Executors.newFixedThreadPool(3);
for (int i = 0; i < 6; i++) { // 6 tasks, so barrier will be tripped twice
int participantId = i + 1;
executorForBarrier.submit(() -> {
try {
System.out.println("Participant " + participantId + " arrived at barrier.");
barrier.await(); // Wait for other participants
System.out.println("Participant " + participantId + " passed barrier.");
} catch (Exception e) {
System.err.println("Participant " + participantId + " interrupted: " + e.getMessage());
}
});
}
executorForBarrier.shutdown();
executorForBarrier.awaitTermination(1, java.util.concurrent.TimeUnit.SECONDS);
}
}
Q3. Explain the concept of thread pools and their benefits.
A thread pool is a collection of pre-initialized, reusable threads that can execute tasks. Instead of creating a new thread for each task (which is an expensive and resource-intensive operation), tasks are submitted to the thread pool, which assigns them to an available thread. If no threads are available, the task is typically queued. The `java.util.concurrent.Executors` framework provides factory methods to create various types of thread pools (e.g., `FixedThreadPool`, `CachedThreadPool`, `ScheduledThreadPool`, `SingleThreadExecutor`).
Benefits include:
1. **Reduced Overhead**: Creating and destroying threads for every task is expensive in terms of CPU and memory. Thread pools reuse existing threads, significantly reducing this overhead.
2. **Improved Performance**: Tasks are processed more efficiently as threads are readily available to pick up new work from the queue without waiting for thread creation.
3. **Resource Management**: Thread pools allow you to limit the number of concurrent threads, preventing system overload and resource exhaustion that can occur if too many threads are created simultaneously.
4. **Enhanced Responsiveness**: Tasks can start executing immediately if a thread is available, without the latency associated with new thread creation.
5. **Managed Lifecycle**: The `ExecutorService` (the interface typically used with thread pools) provides mechanisms for managing thread lifecycle, handling task queues, and defining task execution policies, simplifying concurrent application development.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) throws Exception {
// Create a fixed thread pool with 2 threads
ExecutorService executor = Executors.newFixedThreadPool(2);
// Submit a Runnable task
executor.submit(() -> {
System.out.println("Task 1 executed by: " + Thread.currentThread().getName());
try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
// Submit a Callable task (can return a result and throw exceptions)
Future<Integer> future = executor.submit(() -> {
System.out.println("Task 2 executed by: " + Thread.currentThread().getName());
try { Thread.sleep(300); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return 123;
});
// Submit another Runnable task (will be queued until a thread is free)
executor.submit(() -> {
System.out.println("Task 3 executed by: " + Thread.currentThread().getName());
});
System.out.println("Result from Task 2: " + future.get()); // Blocks until Task 2 completes
// Shut down the executor. It will stop accepting new tasks and finish existing ones.
executor.shutdown();
// Optionally, wait for all tasks to complete within a timeout
if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
System.err.println("Executor did not terminate in time.");
executor.shutdownNow(); // Force shutdown
}
System.out.println("All tasks submitted to thread pool.");
}
}
Q4. What is "Type Erasure" in Java Generics? What are its implications?
Type erasure is the process by which the Java compiler enforces type constraints at compile time and then discards (erases) the generic type information at runtime. This means that at runtime, a generic type like `List<String>` becomes just `List` (its raw type). All type parameters are replaced with their bounds (e.g., `T` becomes `Comparable` if `T extends Comparable`) or with `Object` if no explicit bounds are specified.
Implications of Type Erasure:
1. **No Runtime Type Information**: You cannot use `instanceof` or `new` with generic type parameters directly at runtime (e.g., `new T()`, `object instanceof List<String>` are compile-time errors). The JVM does not know `List<String>` from `List<Integer>`.
2. **Bridge Methods**: The compiler may generate synthetic bridge methods to handle polymorphism correctly when generic methods are overridden. These methods ensure that method calls resolve correctly to the most specific method, even with type erasure.
3. **Backward Compatibility**: Type erasure ensures backward compatibility with older Java versions (prior to Java 5) that didn't have generics. Existing code could still interact with new generic code using raw types.
4. **Array Creation Restrictions**: You cannot create arrays of parameterized types (e.g., `new List<String>[10]`) directly because the JVM needs to know the concrete type of elements for array creation and type checking, which is lost due to erasure.
5. **No Primitive Type Arguments**: Primitive types (like `int`, `boolean`) cannot be used as type arguments for generic types; they must be their wrapper types (e.g., `List<Integer>` instead of `List<int>`).
While it simplifies the JVM and allows backward compatibility, type erasure introduces limitations that developers must be aware of when working with generics.
import java.util.ArrayList;
import java.util.List;
public class TypeErasureDemo {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // Output: true
// At runtime, both are just 'java.util.ArrayList' due to type erasure.
// Cannot use instanceof with parameterized types (compile-time error)
// if (strings instanceof List<String>) { /* ... */ }
// Cannot create arrays of generic types (compile-time error)
// List<String>[] arrayOfLists = new List<String>[10];
}
// Bridge method example (conceptual, compiler generates this)
// interface GenericInterface<T> { void setValue(T value); }
// class StringImpl implements GenericInterface<String> {
// public void setValue(String value) { /* ... */ }
// // Compiler might generate a bridge method:
// // public void setValue(Object value) { setValue((String) value); }
// }
}
Q5. Explain the different types of references in Java (Strong, Weak, Soft, Phantom).
Java has different types of references, primarily managed by the `java.lang.ref` package, which interact differently with the garbage collector (GC) and offer more flexibility in memory management:
1. **Strong Reference**: This is the most common type of reference. If an object has at least one strong reference pointing to it, it is considered strongly reachable and is **not eligible for garbage collection**. An object becomes eligible for GC only when it's no longer strongly reachable.
2. **Soft Reference**: An object with only soft references (and no strong references) is eligible for GC if the JVM is low on memory. Soft references are commonly used for implementing memory-sensitive caches. The garbage collector is free to reclaim soft-reachable objects as needed, but it will typically try to keep them as long as memory is available.
3. **Weak Reference**: An object with only weak references (and no strong or soft references) is eligible for GC in the *next* garbage collection cycle. Weak references are often used for implementing canonicalized mappings (e.g., `WeakHashMap` where entries are automatically removed if the key is no longer strongly referenced) or to store metadata that doesn't prevent its target from being collected.
4. **Phantom Reference**: The weakest type of reference. An object with only phantom references (and no strong, soft, or weak references) is eligible for GC, but before its memory is actually reclaimed, it's enqueued into a `ReferenceQueue`. Phantom references cannot be used to retrieve the object (their `get()` method always returns `null`). They are primarily used for *post-mortem cleanup* actions, allowing you to perform cleanup operations on an object *after* it has been determined unreachable and its memory is about to be reclaimed. This is a safer alternative to finalizers.
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class ReferenceTypesDemo {
public static void main(String[] args) {
// 1. Strong Reference
String strongRef = new String("Strong"); // Object "Strong" is strongly reachable
System.out.println("Strong Reference: " + strongRef);
// 2. Soft Reference
SoftReference<String> softRef = new SoftReference<>(new String("Soft"));
System.out.println("Soft Reference (before GC): " + softRef.get());
// System.gc(); // Hint to GC (not guaranteed to run)
// If memory runs low, softRef.get() might return null
// 3. Weak Reference
WeakReference<String> weakRef = new WeakReference<>(new String("Weak"));
System.out.println("Weak Reference (before GC): " + weakRef.get());
System.gc(); // Request GC, likely clears weak reference
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Weak Reference (after GC): " + weakRef.get()); // Likely null
// 4. Phantom Reference (requires a ReferenceQueue for notification)
ReferenceQueue<String> refQueue = new ReferenceQueue<>();
String phantomTarget = new String("Phantom");
PhantomReference<String> phantomRef = new PhantomReference<>(phantomTarget, refQueue);
System.out.println("Phantom Reference: " + phantomRef.get()); // Always null
phantomTarget = null; // Remove strong reference to make object eligible for GC
System.gc();
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
// Check if the phantom reference has been enqueued (object has been finalized but not yet reclaimed)
if (refQueue.poll() == phantomRef) {
System.out.println("Phantom reference enqueued. Object is ready for post-mortem cleanup.");
} else {
System.out.println("Phantom reference not yet enqueued or object not collected.");
}
}
}
Q6. What is the difference between `CompletableFuture` and `Future`?
Both `Future` and `CompletableFuture` represent the result of an asynchronous computation, but `CompletableFuture` (introduced in Java 8) offers significantly more power and flexibility compared to the basic `Future` interface.
* **`Future`**: Represents a result that will be available at some point in the future. It provides methods like `get()` (which blocks until the result is available or an exception occurs), `isDone()` (to check completion status), `cancel()` (to attempt to cancel the task), and `isCancelled()`. Its primary limitation is that you cannot chain multiple asynchronous operations or react to the completion of a `Future` without blocking or polling. It's a pull-based API; you actively pull the result when you need it.
* **`CompletableFuture`**: Extends `Future` and implements `CompletionStage`. It allows you to define a pipeline of dependent asynchronous tasks, enabling non-blocking, reactive programming. You can chain operations (e.g., `thenApply()` for transformation, `thenAccept()` for consumption, `thenCompose()` for chaining `CompletableFuture`s, `thenCombine()` for combining results from two `CompletableFuture`s), handle exceptions declaratively, and compose multiple `CompletableFuture`s without blocking. It's a push-based API; it reacts to completion and pushes results down the pipeline. It also allows you to manually complete a computation (using `complete()` or `completeExceptionally()`).
In essence, `CompletableFuture` moves beyond just representing a result to providing a powerful framework for building complex asynchronous workflows in a non-blocking and functional style, addressing the composability limitations of `Future`.
import java.util.concurrent.*;
public class CompletableFutureVsFuture {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
// --- Using Future (blocking) ---
System.out.println("\n--- Using Future ---");
Future<String> future = executor.submit(() -> {
System.out.println("Future task running...");
Thread.sleep(500);
return "Hello from Future!";
});
System.out.println("Main thread doing other work...");
// String result = future.get(); // This line blocks the main thread
// System.out.println("Future result: " + result);
// --- Using CompletableFuture (non-blocking, reactive) ---
System.out.println("\n--- Using CompletableFuture ---");
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("CompletableFuture task running...");
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return "Hello from CompletableFuture!";
}, executor);
completableFuture
.thenApply(s -> s + " Processed.") // Chain a transformation
.thenAccept(s -> System.out.println("CompletableFuture result: " + s)) // Consume the result
.exceptionally(ex -> {
System.err.println("Error: " + ex.getMessage());
return null;
}); // Handle exceptions
System.out.println("Main thread doing other work while CompletableFuture processes...");
// Wait for CompletableFuture to complete (for demo purposes, not usually needed in reactive flows)
completableFuture.join();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
}
}
Q7. Describe the different memory areas of the JVM.
The JVM defines several memory areas for managing program execution, which are categorized into per-thread and shared memory areas:
**Per-Thread Memory Areas:**
1. **PC (Program Counter) Register**: Each JVM thread has its own PC register. It stores the address of the current instruction being executed by the thread. If the method being executed is native, the value of the PC register is undefined.
2. **JVM Stacks (Thread Stacks)**: Each JVM thread has its own private stack. It stores frames, which are created and destroyed with each method invocation. A frame holds local variables, operand stacks (used for intermediate computations), and partial results for method invocations. Stack memory is allocated and deallocated automatically as methods are called and return.
3. **Native Method Stacks**: Similar to JVM stacks, but they store information about native methods (methods written in languages other than Java, like C/C++) invoked by the Java application.
**Shared Memory Areas:**
1. **Heap Area**: This is the runtime data area from which memory for all class instances (objects) and arrays is allocated. It's shared among all JVM threads and is the primary area for garbage collection. The heap is typically divided into generations (Young, Old, Permanent/Metaspace) for efficient GC.
2. **Method Area**: Stores class-level data for each loaded class, such as its bytecode, runtime constant pool (including field and method data, static variables), and method code. It's shared among all threads. In Java 8+, the permanent generation (PermGen) was replaced by **Metaspace**, which typically resides in native memory, not the heap, and dynamically resizes.
Q8. What are functional interfaces and how are they used with Lambda expressions?
A functional interface is an interface that contains exactly one abstract method. It can have any number of default and static methods. The `@FunctionalInterface` annotation is optional but recommended to indicate that an interface is intended to be a functional interface and to allow the compiler to enforce the single abstract method rule.
Functional interfaces are the cornerstone of lambda expressions in Java 8. A lambda expression provides a concise way to implement the single abstract method of a functional interface. Instead of writing an anonymous inner class (which is verbose), you can use a compact syntax `(parameters) -> expression` or `(parameters) -> { statements; }` to directly provide the implementation of that single abstract method. This enables functional programming paradigms, making code more readable, concise, and expressive.
Java provides several built-in functional interfaces in the `java.util.function` package, such as `Predicate`, `Consumer`, `Function`, and `Supplier`.
import java.util.function.Predicate;
import java.util.function.Consumer;
// Custom functional interface (annotated for compile-time check)
@FunctionalInterface
interface MyCalculator {
int calculate(int a, int b);
// default void log(String msg) { System.out.println(msg); } // Default methods allowed
}
public class FunctionalInterfaceLambdaDemo {
public static void main(String[] args) {
// Using a custom functional interface with a lambda expression
MyCalculator adder = (x, y) -> x + y;
System.out.println("Sum: " + adder.calculate(5, 3)); // Output: Sum: 8
MyCalculator multiplier = (x, y) -> x * y;
System.out.println("Product: " + multiplier.calculate(5, 3)); // Output: Product: 15
// Using a built-in functional interface (Predicate) with lambda
Predicate<String> startsWithA = s -> s.startsWith("A");
System.out.println("'Apple' starts with A: " + startsWithA.test("Apple")); // Output: true
System.out.println("'Banana' starts with A: " + startsWithA.test("Banana")); // Output: false
// Using a built-in functional interface (Consumer) with lambda
Consumer<String> greeter = name -> System.out.println("Hello, " + name + "!");
greeter.accept("Alice"); // Output: Hello, Alice!
}
}
Q9. Explain the concept of `Stream` API in Java 8. How is it different from Collections?
The Java 8 Stream API provides a powerful and functional way to process sequences of elements. A `Stream` is a sequence of elements from a source (like a collection, array, or I/O channel) that supports aggregate operations (like filter, map, reduce, find, match) that can be executed either sequentially or in parallel. Streams are designed to enable a declarative style of data processing, where you describe *what* you want to achieve rather than *how* to achieve it.
Key differences from Collections:
1. **Processing vs. Storage**: Collections are primarily about storing, managing, and accessing groups of data. Streams are about processing data from a source, performing computations, and producing a result.
2. **Lazy Evaluation**: Stream operations are often lazy. Intermediate operations (like `filter`, `map`) are not executed immediately when called; they are chained together to form a pipeline. Actual processing only occurs when a terminal operation (like `forEach`, `sum`, `collect`, `count`) is invoked.
3. **Single Pass**: Streams can generally be traversed only once. Once a terminal operation is performed, the stream is "consumed" and cannot be reused. If you need to process the same data again, you must create a new stream from the source.
4. **No Storage**: Streams do not store elements themselves. They are views over a data source and compute elements on demand.
5. **Functional**: Streams encourage a functional style of programming with immutable data transformations. Operations produce new streams rather than modifying the underlying data source.
6. **Parallelism**: Streams can be easily converted to parallel streams (`parallelStream()`), allowing operations to be executed concurrently on multiple CPU cores with minimal code changes, which is beneficial for performance on large datasets.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamAPIDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Example 1: Filter even numbers, double them, and sum them
int sumOfDoubledEvens = numbers.stream()
.filter(n -> n % 2 == 0) // Intermediate operation: filters even numbers
.mapToInt(n -> n * 2) // Intermediate operation: doubles each number
.sum(); // Terminal operation: sums the results
System.out.println("Sum of doubled even numbers: " + sumOfDoubledEvens); // Output: 60 (2*2 + 4*2 + 6*2 + 8*2 + 10*2)
// Example 2: Collect names starting with 'A' into a new List
List<String> names = Arrays.asList("Alice", "Bob", "Anna", "Charlie", "Andrew");
List<String> namesStartingWithA = names.stream()
.filter(s -> s.startsWith("A"))
.collect(Collectors.toList()); // Terminal operation: collects to a List
System.out.println("Names starting with 'A': " + namesStartingWithA);
// Example 3: Parallel stream for potentially faster processing
long count = numbers.parallelStream()
.filter(n -> n > 5)
.count(); // Terminal operation
System.out.println("Count of numbers greater than 5: " + count);
}
}
Q10. What is the purpose of the `Optional` class in Java 8?
The `Optional` class, introduced in Java 8, is a container object that may or may not contain a non-null value. It's designed to provide a better, more explicit way to handle `null` values and prevent `NullPointerException`s, which are a common source of bugs in Java applications. Before `Optional`, developers often had to perform repetitive `null` checks, leading to verbose and error-prone code.
Instead of returning `null` from a method when a value might be absent, you can return an `Optional` instance (either `Optional.empty()` for no value or `Optional.of(value)`/`Optional.ofNullable(value)` for a present value). This forces the caller to explicitly consider the case where the value might not be present, promoting more robust, readable, and less error-prone code.
Key methods of `Optional` include:
* `isPresent()`: Returns `true` if a value is present, `false` otherwise.
* `isEmpty()`: (Java 11+) Returns `true` if no value is present, `false` otherwise.
* `get()`: Returns the value if present, otherwise throws `NoSuchElementException` (use with caution, only after `isPresent()` check).
* `orElse(T other)`: Returns the value if present, otherwise returns a specified default value.
* `orElseGet(Supplier<? extends T> other)`: Returns the value if present, otherwise invokes a `Supplier` to get a default value (lazy evaluation).
* `orElseThrow(Supplier<? extends X> exceptionSupplier)`: Returns the value if present, otherwise throws an exception produced by the supplier.
* `ifPresent(Consumer<? super T> consumer)`: If a value is present, performs the given action on the value.
* `map(Function<? super T, ? extends U> mapper)`: If a value is present, applies the mapping function to it and returns an `Optional` describing the result. Otherwise, returns an empty `Optional`.
* `filter(Predicate<? super T> predicate)`: If a value is present and matches the predicate, returns an `Optional` describing the value. Otherwise, returns an empty `Optional`.
Using `Optional` encourages a more functional and declarative style for handling potentially absent values, making the intent of the code clearer.
import java.util.Optional;
public class OptionalDemo {
public static Optional<String> findUserNameById(int id) {
if (id == 1) {
return Optional.of("Alice");
} else if (id == 2) {
return Optional.ofNullable(null); // Explicitly creating an empty Optional from null
} else {
return Optional.empty(); // No user found
}
}
public static void main(String[] args) {
// Case 1: Value is present
Optional<String> user1 = findUserNameById(1);
String name1 = user1.orElse("Default User");
System.out.println("User 1: " + name1); // Output: User 1: Alice
user1.ifPresent(s -> System.out.println("Hello, " + s + "!"));
// Case 2: Value is null (handled as empty Optional)
Optional<String> user2 = findUserNameById(2);
String name2 = user2.orElseGet(() -> "Generated Default");
System.out.println("User 2: " + name2); // Output: User 2: Generated Default
// Case 3: Value is absent
Optional<String> user3 = findUserNameById(3);
try {
String name3 = user3.orElseThrow(() -> new IllegalArgumentException("User not found!"));
System.out.println("User 3: " + name3);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage()); // Output: User not found!
}
// Chaining operations with map and filter
Optional<String> adminUser = Optional.of("Admin");
adminUser.filter(s -> s.equals("Admin"))
.map(String::toLowerCase)
.ifPresent(s -> System.out.println("Admin user found: " + s)); // Output: Admin user found: admin
Optional<String> guestUser = Optional.of("Guest");
guestUser.filter(s -> s.equals("Admin"))
.map(String::toLowerCase)
.ifPresent(s -> System.out.println("Admin user found: " + s)); // No output
}
}
Q11. What are the differences between `fail-fast` and `fail-safe` iterators in Java collections?
This distinction relates to how iterators behave when a collection is modified structurally while it is being iterated over by one or more threads.
* **Fail-Fast Iterators**: These iterators (e.g., those returned by non-thread-safe collections like `ArrayList`, `HashMap`, `HashSet`, `Vector`, `Hashtable`) throw a `ConcurrentModificationException` immediately if the underlying collection is structurally modified (e.g., adding, removing, or resizing elements) by any means other than the iterator's own `remove()` or `add()` methods, after the iterator has been created. They achieve this by maintaining an internal `modCount` variable (or similar mechanism) in the collection, which is incremented on structural changes. The iterator keeps a copy of this count, and on each operation, it checks if its copy matches the collection's current `modCount`. If they don't match, an exception is thrown. This behavior is primarily for detecting bugs early in single-threaded environments or when concurrent modification is unintended.
* **Fail-Safe Iterators**: These iterators (e.g., those returned by concurrent collections in `java.util.concurrent` like `CopyOnWriteArrayList`, `ConcurrentHashMap`, `CopyOnWriteArraySet`) do not throw `ConcurrentModificationException`. They operate on a snapshot or a copy of the collection's data taken at the time the iterator was created. This means that any subsequent structural changes to the original collection (additions, deletions) will *not* be reflected by the iterator. While they avoid exceptions, they might iterate over stale data, meaning they may not see the most up-to-date state of the collection. They are typically used in concurrent scenarios where avoiding exceptions is preferred over strict real-time consistency during iteration.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class FailFastFailSafeDemo {
public static void main(String[] args) {
// --- Fail-Fast Iterator Demo (ArrayList) ---
System.out.println("\n--- Fail-Fast Iterator Demo ---");
List<String> failFastList = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> failFastIterator = failFastList.iterator();
while (failFastIterator.hasNext()) {
String element = failFastIterator.next();
System.out.println("Reading fail-fast: " + element);
if (element.equals("B")) {
// This structural modification will cause ConcurrentModificationException
// if the iterator tries to access the next element
// failFastList.add("D"); // Throws ConcurrentModificationException
// failFastList.remove("A"); // Throws ConcurrentModificationException
// To remove safely using fail-fast iterator:
// failFastIterator.remove(); // This is allowed
}
}
// To demonstrate the exception, uncomment the failFastList.add("D") line above.
// try {
// for (String s : failFastList) { // Enhanced for loop uses iterator internally
// System.out.println("Reading in loop: " + s);
// if (s.equals("B")) failFastList.add("D");
// }
// } catch (java.util.ConcurrentModificationException e) {
// System.err.println("Caught expected ConcurrentModificationException: " + e.getMessage());
// }
// --- Fail-Safe Iterator Demo (CopyOnWriteArrayList) ---
System.out.println("\n--- Fail-Safe Iterator Demo ---");
List<String> failSafeList = new CopyOnWriteArrayList<>(Arrays.asList("X", "Y", "Z"));
Iterator<String> failSafeIterator = failSafeList.iterator();
while (failSafeIterator.hasNext()) {
String element = failSafeIterator.next();
System.out.println("Reading fail-safe: " + element);
if (element.equals("Y")) {
// This structural modification will NOT cause an exception
// but the added element 'W' will NOT be seen by this iterator instance.
failSafeList.add("W");
System.out.println("Added 'W' to fail-safe list.");
}
}
System.out.println("Fail-safe iteration finished.");
System.out.println("Original fail-safe list after iteration: " + failSafeList); // 'W' is present in the list
}
}
Q12. Explain the concept of JVM Class Loading. What are the three classloaders?
JVM Class Loading is the process of locating, loading, and linking classes into the JVM's runtime memory. It happens dynamically at runtime when a class is first referenced (e.g., when an object of that class is created, or a static method/field is accessed). The process involves three main steps:
1. **Loading**: Finds the `.class` file for a given class name and reads its binary data into the JVM.
2. **Linking**: Verifies the bytecode, prepares static fields for initialization, and resolves symbolic references.
3. **Initialization**: Executes the class's static initializers and static blocks.
The JVM uses a delegation-based hierarchy of classloaders to load classes:
1. **Bootstrap (Primordial) Classloader**: This is the highest-level classloader, built into the JVM and typically implemented in native code. It is responsible for loading the core Java API classes (e.g., `java.lang.Object`, `String`) from the Java runtime's internal libraries (`rt.jar` in older Java versions, now part of the `jimage` file in newer modular Java versions). It's the parent of all other classloaders.
2. **Extension Classloader**: Child of the Bootstrap Classloader. It loads classes from the Java extensions directory (`<JAVA_HOME>/lib/ext` in older versions, or any directory specified by the `java.ext.dirs` system property). This was used for optional packages and extensions.
3. **Application (System) Classloader**: Child of the Extension Classloader. It loads classes from the application's classpath, which is specified by the `-classpath` or `CLASSPATH` environment variable. This is typically the classloader that loads your application's classes and third-party libraries.
**Delegation Model**: When a classloader is asked to load a class, it first delegates the request to its parent classloader. If the parent can load the class, it does so. Only if the parent cannot find or load the class does the current classloader attempt to load it from its own search path. This model prevents duplicate class loading and ensures that core Java classes are always loaded by the trusted Bootstrap Classloader, maintaining security and consistency.
Prepared by iCampusLink. 39 Java interview questions.