New 120 Interview Questions on JAVA

Introduction
Welcome, young programmers, to the exciting world of Java! Whether you’re just starting your coding journey or have already dabbled in Java programming, this blog is here to help you sharpen your skills and prepare for interviews. We understand that you’re passionate about learning, so we’ve compiled a list of 170 interview questions that will challenge your knowledge and expand your understanding of Java. Get ready to dive into the fascinating world of Java programming and enhance your problem-solving abilities!
Basic Questions
1. What is Java?
Java is a popular object-oriented programming language that was developed by Sun Microsystems (now owned by Oracle Corporation) in the mid-1990s. It is known for its platform independence, which means that Java programs can run on any operating system or platform that has a Java Virtual Machine (JVM) installed. Java is widely used for developing a variety of applications, including web, desktop, mobile, and enterprise applications.
2. What are the key features of Java?
The key features of Java are:
- Platform Independence: Java programs can run on any platform with a compatible JVM.
- Object-Oriented Programming: Java follows an object-oriented approach, organizing code into reusable objects and classes.
- Garbage Collection: Java has automatic memory management, where the garbage collector frees up memory by removing unreferenced objects.
- Security: Java has built-in security features to protect against malicious code, such as sandboxing and bytecode verification.
- Portability: Java’s “write once, run anywhere” principle allows code to be developed on one platform and executed on another without recompilation.
- Exception Handling: Java provides a robust exception handling mechanism to handle and recover from runtime errors.
- Multi-threading: Java supports concurrent programming with built-in support for creating and managing multiple threads of execution.
- Rich API: Java provides a vast standard library (Java API) with a wide range of classes and methods for various programming tasks.
- High Performance: Java’s Just-In-Time (JIT) compiler and optimized runtime environment deliver high-performance execution.
3. Explain the concept of object-oriented programming.
Object-oriented programming (OOP) is a programming paradigm that organizes code into objects, which are instances of classes. It emphasizes the concept of objects, data encapsulation, and interactions between objects. OOP promotes modular design, code reuse, and easier maintenance by focusing on the following key concepts:
- Encapsulation: Bundling data and methods together in an object and hiding internal details, providing access only through well-defined interfaces.
- Inheritance: Defining new classes based on existing classes, inheriting their attributes and behaviors, and extending or modifying them as needed.
- Polymorphism: Treating objects of different classes as interchangeable based on their common interfaces, enabling flexibility and code reusability.
- Abstraction: Creating abstract classes and interfaces to define common behaviors and characteristics, without specifying implementation details.
- Association: Establishing relationships between objects to represent dependencies, collaborations, or ownerships.
- Composition: Building complex objects by combining simpler objects or components, forming a “has-a” relationship.
- Aggregation: Representing a whole-part relationship between objects, where parts can exist independently of the whole.
- Message Passing: Objects communicate with each other by sending and receiving messages, invoking methods or requesting services.
4. What are the principles of OOP and explain each briefly?
The principles of Object-Oriented Programming (OOP) are guidelines that help in designing and implementing effective object-oriented systems. They include:
- Encapsulation: Encapsulation refers to the bundling of data (attributes) and related behaviors (methods) into objects. It ensures that an object’s internal state is accessed and modified only through its defined interfaces, promoting data hiding and information security.
- Inheritance: Inheritance allows classes to inherit attributes and behaviors from other classes. It supports code reuse by creating a hierarchy of classes, where derived classes (subclasses) inherit from a base class (superclass). Inheritance promotes modularity and extensibility, enabling the creation of specialized classes while inheriting common features.
- Polymorphism: Polymorphism allows objects of different classes to be treated as interchangeable entities based on their common interface. It enables flexibility and code reuse by providing the ability to write code that can work with objects of different types. Polymorphism is typically achieved through method overriding and method overloading.
- Abstraction: Abstraction focuses on defining essential features and behaviors while hiding unnecessary details. It allows the creation of abstract classes and interfaces that provide a common interface for a set of related classes. Abstraction helps in managing complexity, promoting modularity and maintainability.
- Association: Association represents a relationship between two or more objects, where one object uses another or is related to it in some way. It can be a one-to-one, one-to-many, or many-to-many relationship. Associations are typically established through references or method parameters, enabling objects to collaborate and interact.
- Composition: Composition represents a “has-a” relationship between objects, where one object is composed of one or more other objects. The composed objects are part of the containing object’s structure and cannot exist independently. Composition allows building complex objects by combining simpler objects, promoting code reuse and modularity.
- Aggregation: Aggregation represents a whole-part relationship between objects, where parts can exist independently of the whole. It is a specialized form of composition. Aggregated objects have a weaker relationship with the containing object, and they can be shared among multiple containing objects.
5. Differentiate between JDK, JRE, and JVM.
Property | JDK (Java Development Kit) | JRE (Java Runtime Environment) | JVM (Java Virtual Machine) |
---|---|---|---|
Purpose | Development and compilation of Java programs | Running Java applications and applets | Executing Java bytecode and managing memory |
Components | Compiler, debugger, libraries, documentation, and development tools | Runtime libraries, Java Virtual Machine, and necessary files | Interpreter, Just-In-Time (JIT) compiler, garbage collector |
Includes | JRE (Java Runtime Environment) | Java class libraries and development tools | Runtime libraries, execution engine, memory management |
Usage | Required for Java development and compilation | Required to run Java applications | Required to execute Java bytecode |
Example | JDK 16 | JRE 16 | JVM (part of JRE or JDK) |
6. What is a Java Class and Object?
In Java, a class is a blueprint or template that defines the structure and behavior of objects. It contains member variables (attributes) and methods (behaviors) that define the characteristics and actions of objects created from the class. An object, on the other hand, is an instance of a class—a runtime entity that holds the state and behavior defined by the class.
Here’s an example of a Java class and object:
// Java Class: Person
public class Person {
// Member variables
private String name;
private int age;
// Constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Method
public void displayInfo() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
}
}
// Creating Objects
public class Main {
public static void main(String[] args) {
// Creating object of class Person
Person person1 = new Person("John Doe", 25);
// Accessing the object's method
person1.displayInfo();
}
}
In this example, the Person
class defines a blueprint for creating person objects. It has two member variables (name
and age
), a constructor to initialize those variables, and a method (displayInfo()
) to display the person’s information. In the Main
class, we create an object person1
of type Person
and call the displayInfo()
method to print the person’s information.
Output:
Name: John Doe
Age: 25
7. What are data types in Java?
In Java, data types specify the type and size of values that can be stored in variables. Java has two categories of data types:
- Primitive Data Types: These are basic data types provided by Java. There are eight primitive data types:
- byte: Represents a signed 8-bit integer.
- short: Represents a signed 16-bit integer.
- int: Represents a signed 32-bit integer.
- long: Represents a signed 64-bit integer.
- float: Represents a 32-bit floating-point number.
- double: Represents a 64-bit floating-point number.
- char: Represents a single 16-bit Unicode character
- boolean: Represents a boolean value (
true
orfalse
).
- Reference Data Types: These data types are derived from classes and can store references to objects. Examples of reference data types include:
- Class types: User-defined classes or pre-defined classes like
String
,ArrayList
, etc. - Array types: Arrays of elements of any type.
- Interface types: Interfaces implemented by classes.
- Enumeration types: Enumerated types defined using the
enum
keyword.
- Class types: User-defined classes or pre-defined classes like
8. Explain Java’s Exception Handling.
Exception handling in Java allows programmers to handle and manage runtime errors, known as exceptions, in a structured manner. It helps in gracefully handling exceptional situations and provides a mechanism for handling errors without terminating the program abruptly.
Java’s exception handling is based on the following keywords:
try
: Thetry
block encloses the code that may potentially throw an exception.catch
: Thecatch
block catches and handles the exceptions thrown by thetry
block.finally
: Thefinally
block contains code that is always executed, whether an exception occurs or not. It is typically used to release resources.throw
: Thethrow
statement is used to explicitly throw an exception from a block of code.throws
: Thethrows
clause is used in a method declaration to indicate that the method might throw one or more exceptions.
Here’s an example demonstrating exception handling in Java:
public class ExceptionExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Error: Division by zero");
} finally {
System.out.println("Finally block executed");
}
}
public static int divide(int num1, int num2) {
if (num2 == 0) {
throw new ArithmeticException("Division by zero");
}
return num1 / num2;
}
}
In this example, the divide
method attempts to divide num1
by num2
. If num2
is zero, an ArithmeticException
is explicitly thrown using the throw
statement. In the main
method, this exception is caught in the catch
block, which prints an error message. The finally
block always executes, irrespective of whether an exception occurs or not.
Output:
Error: Division by zero
Finally block executed
9. What are the differences between Checked Exception and Unchecked Exception?
Property | Checked Exception | Unchecked Exception |
---|---|---|
Checked at Compile-time? | Yes | No |
Handling requirement | Must be handled using try-catch or declared with throws | Not required to be caught or declared |
Inheritance | Extends Exception class | Extends RuntimeException or Error class |
Examples | IOException , SQLException | NullPointerException , ArrayIndexOutOfBoundsException |
Checked exceptions are checked at compile-time, meaning the compiler ensures that these exceptions are either caught using try-catch blocks or declared in the method signature using the throws
clause. They represent exceptional conditions that can be reasonably expected and handled.
10. Can we override static methods in Java?
No, we cannot override static methods in Java because static methods belong to the class, not the instance of the class. Inheritance allows for method overriding, but static methods are resolved at compile-time based on the reference type, not the actual object type.
Here’s an example illustrating the behavior of static methods in Java:
class Parent {
static void display() {
System.out.println("Parent's static method");
}
}
class Child extends Parent {
static void display() {
System.out.println("Child's static method");
}
}
public class Main {
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child();
parent.display(); // Output: Parent's static method
child.display(); // Output: Parent's static method
}
}
In this example, the Parent
class has a static method display()
, and the Child
class attempts to override it. However, when invoking the display()
method using a Parent
reference (parent
) or a Child
reference (child
), the output will be “Parent’s static method” in both cases. The static method is resolved based on the reference type, not the actual object type.
11. Can we override static methods in Java?
No, we cannot override static methods in Java because static methods belong to the class, not the instance of the class. Inheritance allows for method overriding, but static methods are resolved at compile-time based on the reference type, not the actual object type.
Here’s an example illustrating the behavior of static methods in Java:
class Parent {
static void display() {
System.out.println("Parent's static method");
}
}
class Child extends Parent {
static void display() {
System.out.println("Child's static method");
}
}
public class Main {
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child();
parent.display(); // Output: Parent's static method
child.display(); // Output: Parent's static method
}
}
In this example, the Parent
class has a static method display()
, and the Child
class attempts to override it. However, when invoking the display()
method using a Parent
reference (parent
) or a Child
reference (child
), the output will be “Parent’s static method” in both cases. The static method is resolved based on the reference type, not the actual object type.
12. What is the difference between abstract classes and interfaces?
Here is the difference between abstract classes and interfaces in tabular form:
Abstract Classes | Interfaces | |
---|---|---|
1. | Can have fields, constructors, and methods | Can only have constant fields and abstract methods |
2. | Can provide partial or complete method implementations | Only have method declarations without any implementation |
3. | Can have instance variables | Cannot have instance variables |
4. | Can extend only one class | Can extend multiple interfaces |
5. | Supports single inheritance | Supports multiple inheritance through interfaces |
6. | Can be used to create objects | Cannot be used to create objects directly |
7. | Can define constructors | Cannot define constructors |
8. | Can have non-abstract methods | Can only have abstract methods |
9. | Can be used to define common behavior among related classes | Can be used to define contracts for unrelated classes |
10. | Provides a way to achieve code reuse and polymorphism | Provides a way to achieve multiple inheritance and contract-based programming |
11. | Examples: Animal (abstract class with common animal behavior) | Examples: Comparable (defines comparison contract) |
13. What is a package in Java?
In Java, a package is a mechanism for organizing related classes, interfaces, and sub-packages. It helps in avoiding naming conflicts and provides better code organization and modularity. Packages can be considered as directories that hold related Java files.
Here’s an example illustrating the use of packages in Java:
// File: com.example.utilities.MathUtils.java
package com.example.utilities;
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
// File: com.example.Main.java
package com.example;
import com.example.utilities.MathUtils;
public class Main {
public static void main(String[] args) {
int result = MathUtils.add(5, 3);
System.out.println("Result: " + result);
}
}
In this example, the MathUtils
class is defined inside the com.example.utilities
package. The Main
class, located in the com.example
package, imports the MathUtils
class using the import
statement. The Main
class then uses the add()
method from the MathUtils
class to perform an addition operation.
14. What is a constructor in Java?
A constructor in Java is a special member method of a class that is used to initialize objects of that class. It has the same name as the class and does not have a return type. Constructors are called automatically when an object is created using the new
keyword.
Here’s an example demonstrating the use of a constructor:
public class Person {
private String name;
private int age;
// Constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getters and setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("John Doe", 25);
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
}
}
In this example, the Person
class has a constructor that takes two parameters (name
and age
) and initializes the corresponding member variables. In the Main
class, a Person
object is created using the constructor new Person("John Doe", 25)
. The object’s state is then accessed using getter methods to display the name and age.
Output:
Name: John Doe
Age: 25
15. What are the types of constructors in Java?
In Java, there are three types of constructors:
- Default Constructor: A default constructor is automatically provided by the compiler if no explicit constructors are defined in a class. It takes no arguments and has an empty body. Its purpose is to initialize the object with default values or perform other necessary setup.
- Parameterized Constructor: A parameterized constructor is a constructor that takes one or more parameters. It allows the initialization of objects with specific values passed as arguments during object creation. Parameterized constructors provide flexibility in initializing object state.
- Copy Constructor: A copy constructor is a constructor that takes an object of the same class as a parameter and creates a new object with the same state as the parameter object. It is used to create a copy of an existing object. Copy constructors are useful when you want to create a new object with the same values as an existing object.
16. Explain the concept of Inheritance in Java.
Inheritance is a fundamental concept in object-oriented programming that allows one class to inherit the properties (methods and variables) of another class. The class that is being inherited from is called the superclass or base class, and the class that inherits is called the subclass or derived class. Inheritance promotes code reuse, extensibility, and the creation of specialized classes.
Here’s an example illustrating the concept of inheritance in Java:
// Superclass
class Animal {
void eat() {
System.out.println("Animal is eating.");
}
}
// Subclass inheriting from Animal
class Dog extends Animal {
void bark() {
System.out.println("Dog is barking.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // Output: Animal is eating.
dog.bark(); // Output: Dog is barking.
}
}
In this example, the Animal
class is the superclass, and the Dog
class is the subclass that inherits from Animal
. The Dog
class inherits the eat()
method from Animal
and also defines its own method bark()
. The Main
class creates an object of the Dog
class and invokes both the inherited eat()
method and the bark()
method.
Output:
Animal is eating.
Dog is barking.
17. Explain Polymorphism in Java.
Polymorphism is a core concept in object-oriented programming that allows objects of different classes to be treated as objects of a common superclass or interface. It provides a way to perform different actions based on the type of the object being referred to. Polymorphism allows for code flexibility, extensibility, and reusability.
Here’s an example demonstrating polymorphism in Java:
// Superclass
class Animal {
void makeSound() {
System.out.println("Animal is making a sound.");
}
}
// Subclasses inheriting from Animal
class Dog extends Animal {
void makeSound() {
System.out.println("Dog is barking.");
}
}
class Cat extends Animal {
void makeSound() {
System.out.println("Cat is meowing.");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound(); // Output: Dog is barking.
animal2.makeSound(); // Output: Cat is meowing.
}
}
In this example, the Animal
class is the superclass, and the Dog
and Cat
classes are subclasses inheriting from Animal
. Each subclass overrides the makeSound()
method to provide its own implementation. In the Main
class, objects of Dog
and Cat
are created and assigned to Animal
references. When the makeSound()
method is invoked on these references, the appropriate subclass’s implementation is called based on the actual object type.
Output:
Dog is barking.
Cat is meowing.
18. What is Encapsulation?
Encapsulation is a principle of object-oriented programming that binds together the data (attributes) and methods (behaviors) that manipulate that data within a class. It hides the internal implementation details of an object and provides controlled access to the object’s state through well-defined interfaces. Encapsulation ensures data integrity, improves code maintainability, and promotes reusability.
Here’s an example illustrating encapsulation in Java:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0) {
this.age = age;
} else {
System.out.println("Invalid age");
}
}
}
In this example, the Person
class has private member variables name
and age
. To access these variables from outside the class, public getter and setter methods are provided (getName()
, setName()
, getAge()
, setAge()
). The getter methods allow read access to the private variables, and the setter methods allow controlled write access by performing validations, such as checking if the age is a non-negative value.
19. What is Multithreading in Java?
Multithreading in Java refers to the concurrent execution of multiple threads within a single program. A thread is an independent unit of execution that represents a sequence of instructions. Multithreading allows multiple threads to run concurrently, enabling efficient utilization of system resources and improving program responsiveness.
Here’s an example demonstrating multithreading in Java:
class MyThread extends Thread {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread: " + Thread.currentThread().getId() + ", Count: " + i);
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start();
thread2
.start();
}
}
In this example, a MyThread
class extends the Thread
class and overrides the run()
method. The run()
method contains the logic that will be executed by the thread. In the Main
class, two MyThread
objects (thread1
and thread2
) are created. The start()
method is called on each thread object, which initiates the execution of the run()
method in a separate thread.
Output (may vary):
Thread: 9, Count: 1
Thread: 10, Count: 1
Thread: 9, Count: 2
Thread: 10, Count: 2
Thread: 9, Count: 3
Thread: 10, Count: 3
Thread: 9, Count: 4
Thread: 10, Count: 4
Thread: 9, Count: 5
Thread: 10, Count: 5
20. Explain the concept of Synchronization in Java.
In Java, synchronization is a technique used to control access to shared resources or critical sections of code when multiple threads are involved. It prevents race conditions and thread interference that can lead to data inconsistencies or program errors. Synchronization ensures that only one thread can access a synchronized block or method at a time, allowing for thread-safe execution.
Here’s an example demonstrating synchronization in Java:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.decrement();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + counter.getCount());
}
}
In this example, the Counter
class has synchronized methods (increment()
, decrement()
, getCount()
) that access and modify the count
variable. Two threads (thread1
and thread2
) are created, each calling the increment()
and decrement()
methods respectively. The synchronized
keyword ensures that only one thread can access these methods at a time, preventing concurrent access and potential data inconsistencies. The join()
method is used to wait for both threads to complete before printing the final count.
Output:
Count: 0
21. Differentiate between ArrayList and LinkedList. Give a code example.
Property | ArrayList | LinkedList |
---|---|---|
Implementation | Implements List and RandomAccess interfaces. | Implements List and Deque interfaces. |
Underlying Data Structure | Uses a dynamically resizing array. | Uses a doubly-linked list. |
Access Time | Provides fast access to elements by index. | Provides slower access to elements by index (linear time). |
Insertion/Deletion Time | Slower for inserting/removing elements in the middle of the list (shifts elements). | Faster for inserting/removing elements anywhere in the list. |
Memory Overhead | Less memory overhead (no pointers or extra links). | More memory overhead (due to pointers and extra links). |
Iteration Performance | Faster iteration using indexed for-loops. | Slower iteration (requires traversing nodes). |
Use Cases | Suitable when frequent element access is required. | Suitable when frequent insertions/deletions are required. |
Example usage of ArrayList:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
System.out.println(list); // Output: [Apple, Banana, Orange]
}
}
Example usage of LinkedList:
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
System.out.println(list); // Output: [Apple, Banana, Orange]
}
}
Both examples demonstrate adding elements to the respective lists. The output is the same in both cases: [Apple, Banana, Orange]
.
Choose ArrayList
when frequent element access by index is required, while LinkedList
is more suitable for frequent insertions or deletions at any position in the list.
22. Explain HashMap in Java. Give a code example.
HashMap
is a commonly used implementation of the Map
interface in Java. It provides a way to store key-value pairs, where each key is unique. HashMap
uses a hash table data structure to store and retrieve elements efficiently. The keys are hashed to calculate the index where the corresponding value is stored, allowing for constant-time performance (on average) for basic operations such as get and put.
Here’s an example illustrating the use of HashMap
in Java:
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
// Create a HashMap
HashMap<String, Integer> map = new HashMap<>();
// Add key-value pairs
map.put("John", 25);
map.put("Emily", 30);
map.put("Adam", 28);
// Access values
int age = map.get("John");
System.out.println("John's age: " + age);
// Check if a key exists
boolean exists = map.containsKey("Emily");
System.out.println("Emily exists: " + exists);
// Remove a key-value pair
map.remove("Adam");
// Size of the HashMap
int size = map.size();
System.out.println("Size of map: " + size);
}
}
In this example, a HashMap
is created with String
keys and Integer
values. Key-value pairs are added using the put()
method. The value corresponding to a key is accessed using the get()
method. The containsKey()
method checks if a key exists. The remove()
method removes a key-value pair. The size()
method returns the size of the HashMap
.
Output:
John age: 25
Emily exists: true
Size of map: 2
23. Explain what is meant by Java collections.
In Java, collections refer to a group of objects or elements organized and managed in a unified manner. The Java Collections Framework provides a set of interfaces, classes, and algorithms to handle collections of objects efficiently. It offers a wide range of data structures, such as lists, sets, queues, maps, and more, along with operations to manipulate and process the data.
The Java Collections Framework includes the following key components:
Interface: Interfaces define contracts for different types of collections and provide a common set of methods to operate on them. Examples include `List`, `Set`, `Queue`, and `Map`.
Classes: Classes in the Java Collections Framework provide concrete implementations of the interfaces. Examples include `ArrayList`, `LinkedList`, `HashSet`, `HashMap`, and many more.
Algorithms: The framework includes various algorithms for sorting, searching, and manipulating collections. These algorithms can be applied to any collection that implements the appropriate interfaces.
24: Differentiate between equals() and ==.
The equals()
method and the ==
operator are used to compare objects in Java, but they have different behaviors.
The ==
operator checks for reference equality. It compares the memory addresses of the objects being compared. In other words, it checks if two object references are pointing to the same memory location.
The equals()
method, on the other hand, is used for content equality. It compares the actual content or values of the objects being compared. By default, the equals()
method in Java compares references, similar to the ==
operator. However, it can be overridden in classes to provide custom comparison logic based on the content of the objects.
Here’s an example to illustrate the difference:
String str1 = new String("Hello");
String str2 = new String("Hello");
String str3 = str1;
System.out.println(str1 == str2); // false, different memory addresses
System.out.println(str1 == str3); // true, same memory address
System.out.println(str1.equals(str2)); // true, content comparison
In the example above, str1
and str2
are two different String objects that contain the same content. The ==
operator returns false
because they are different objects with different memory addresses. However, the equals()
method returns true
because the content of the strings is the same.
25: What are Java access modifiers?
Java access modifiers are keywords used to control the visibility and accessibility of classes, methods, variables, and constructors in Java. There are four access modifiers in Java:
public
: Accessible from anywhere, both within and outside the class or package.protected
: Accessible within the same package or subclasses in different packages.private
: Accessible only within the same class.- Default (no modifier): Accessible within the same package.
Here’s an example demonstrating the use of access modifiers:
public class MyClass {
public int publicVar;
protected int protectedVar;
private int privateVar;
int defaultVar;
public void publicMethod() {
// Method code
}
protected void protectedMethod() {
// Method code
}
private void privateMethod() {
// Method code
}
void defaultMethod() {
// Method code
}
}
In the example above, we have a class MyClass
with variables and methods declared with different access modifiers. The publicVar
and publicMethod()
are accessible from anywhere. The protectedVar
and protectedMethod()
are accessible within the same package or by subclasses in different packages. The privateVar
and privateMethod()
are only accessible within the same class. The defaultVar
and defaultMethod()
are accessible within the same package.
26: What is method overloading and overriding? Give examples.
Method overloading and overriding are two different concepts related to methods in Java.
Method Overloading:
Method overloading is the ability to define multiple methods with the same name but with different parameter lists in the same class. The compiler determines which method to call based on the number, type, and order of the arguments passed during method invocation. Overloaded methods may or may not have different return types.
Here’s an example of method overloading:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
In the example above, the Calculator
class has three overloaded add()
methods. The first add()
method accepts two integers and returns an integer. The second add()
method accepts two doubles and returns a double. The third add()
method accepts three integers and returns an integer. The appropriate method is called based on the arguments provided during invocation.
Method Overriding:
Method overriding is the process of providing a different implementation for a method in a subclass that is already defined in its superclass. The method in the subclass must have the same name, return type, and parameter list as the method in the superclass. It allows the subclass to provide its own specific implementation of the inherited method.
Here’s an example of method overriding:
public class Vehicle {
public void drive() {
System.out.println("Vehicle is being driven.");
}
}
public class Car extends Vehicle {
@Override
public void drive() {
System.out.println("Car is being driven.");
}
}
In the example above, the Vehicle
class has a drive()
method. The Car
class extends Vehicle
and overrides the drive()
method to provide its own implementation. When invoking the drive()
method on an instance of the Car
class, the overridden method in the Car
class is called instead of the one in the Vehicle
class.
27: Explain Autoboxing and Unboxing in Java. Give examples.
Autoboxing and unboxing are features in Java that provide automatic conversion between primitive types and their corresponding wrapper classes.
Autoboxing:
Autoboxing is the automatic conversion of primitive types to their corresponding wrapper classes. It allows you to assign primitive values to wrapper class variables without manually creating instances of the wrapper classes.
Here’s an example of autoboxing:
int number = 42;
Integer wrapper = number; // Autoboxing: int to Integer
In the example above, the int
variable number
is automatically boxed into an Integer
object. This conversion is done implicitly by the Java compiler.
Unboxing:
Unboxing is the automatic conversion of wrapper class objects to their corresponding primitive types. It allows you to extract the primitive value from a wrapper class object without manually calling a getter method.
Here’s an example of unboxing:
Integer wrapper = 42;
int number = wrapper; // Unboxing: Integer to int
In the example above, the Integer
object wrapper
is automatically unboxed, and its value is assigned to the int
variable number
. This conversion is done implicitly by the Java compiler.
28. What is a Java Bean?
In Java, a Java Bean is a reusable software component that follows a set of conventions for its structure and behavior. It is a plain, serializable class that encapsulates state and provides getter and setter methods to access and modify the state. Java Beans are commonly used in Java frameworks, such as JavaBeans API and JavaServer Faces (JSF), for building modular and reusable components.
The Java Bean conventions include:
- The class should have a public default (no-argument) constructor.
- The class properties (instance variables) should be private with corresponding public getter and setter methods.
- The class should implement the
java.io.Serializable
interface for supporting object serialization.
29. What is a Java Servlet?
A Java Servlet is a Java-based technology used for developing server-side web applications. It is a server-side component that receives requests and generates responses based on those requests. Servlets run on a web server and follow the Java Servlet API, which provides a set of interfaces and classes for servlet development.
Key features of Java Servlets include:
- Handling HTTP requests and responses.
- Dynamic content generation for web applications.
- Session management for maintaining user state.
- Accessing and processing form data.
- Interacting with databases and other external resources.
30. Explain the concept of JDBC.
JDBC (Java Database Connectivity) is a Java API that allows Java programs to interact with relational databases. It provides a standard set of interfaces and classes for connecting to databases, executing SQL queries, and manipulating database data. JDBC enables developers to build database-driven applications in Java.
Key concepts in JDBC:
- Driver: A JDBC driver is a software component that provides the necessary functionality to connect to a specific database management system (DBMS). Different databases require different drivers.
- Connection: A JDBC connection represents a connection to a database. It is established using a database URL, username, and password. Connections are used to execute SQL queries and manage transactions.
- Statement: A JDBC statement is used to execute SQL queries or updates on a database. There are three types of statements:
Statement
,PreparedStatement
, andCallableStatement
. - ResultSet: A JDBC
ResultSet
represents the result of an executed query. It provides methods to retrieve and manipulate the data returned by the query. - Transaction: A JDBC transaction represents a sequence of database operations that are executed as a single unit. Transactions ensure data integrity and consistency by enforcing the ACID properties (Atomicity, Consistency, Isolation, Durability).
31. Explain what is JSP.
JSP (JavaServer Pages) is a Java-based technology used for building dynamic web pages. It is a server-side technology that allows Java code to be embedded within HTML pages. JSP files are processed by a web server that converts them into servlets before sending them to the client.
Key features of JSP:
- Dynamic Content: JSP enables the dynamic generation of HTML content by embedding Java code directly into HTML pages using special tags and expressions.
- Java API Support: JSP provides access to the entire Java API, including JDBC for database access, Servlet API for handling requests, and other Java classes.
- Reusable Components: JSP supports the creation and use of reusable components called JSP tags, which can be easily incorporated into JSP pages for modular development.
- Separation of Concerns: JSP promotes the separation of business logic (Java code) from presentation logic (HTML markup) by allowing them to be written separately but integrated within a single file.
32. Differentiate between fail-fast and fail-safe iterators.
Property | Fail-Fast Iterator | Fail-Safe Iterator |
---|---|---|
Behavior on Modification | Throws ConcurrentModificationException if the underlying collection is structurally modified during iteration. | Does not throw any exception if the underlying collection is modified during iteration. It operates on a cloned copy of the collection. |
Concurrent Modification | Detects concurrent modification by maintaining an internal modification count. | Does not detect concurrent modification and operates on a cloned copy of the collection, allowing for safe iteration. |
Performance | Generally faster and more efficient, as it avoids creating an additional copy of the collection. | Slower and less efficient due to the need to create a copy of the collection for iteration. |
Use Case | Suitable when it is desirable to detect and handle concurrent modification exceptions during iteration. | Suitable when it is necessary to avoid concurrent modification exceptions and ensure safe iteration. |
33. What are Java annotations?
Java annotations are a form of metadata that provide additional information and instructions to the compiler, runtime, or other tools. Annotations are defined using the @
symbol followed by the annotation name and can be applied to classes, methods, fields, and other program elements.
Here’s an example of a custom annotation in Java:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value() default "";
int count() default 0;
}
In this example, a custom annotation @MyAnnotation
is defined. The annotation has two elements: value()
of type String
and count()
of type int
. The @Retention
annotation specifies that the annotation should be retained at runtime. The @Target
annotation specifies that the annotation can be applied to methods.
34. Explain Java Generics. Give a code example.
Java Generics provide a way to create reusable code that can work with different types while providing compile-time type safety. Generics allow classes and methods to be parameterized with types, enabling the creation of generic algorithms and data structures.
Here’s an example of a generic class in Java:
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
In this example, the Box
class is a generic class that can hold an object of any type T
. The type parameter T
is specified within angle brackets (<>
) after the class name. The setContent()
method accepts an object of type T
as a parameter, and the getContent()
method returns an object of type T
. The actual type for T
is determined when an instance of Box
is created.
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String content = stringBox.getContent(); // No casting required
Box<Integer> intBox = new Box<>();
intBox.setContent(42);
int value = intBox.getContent(); // No casting required
In this usage example, a Box
object is created with a specific type argument (String
and Integer
). The setContent()
method is called with an object of the corresponding type, and the getContent()
method returns the object without requiring any casting.
35. What is the Java Memory Model?
The Java Memory Model defines the rules and guarantees for how threads interact through memory when accessing shared data. It specifies how changes made by one thread become visible to other threads and how memory operations are ordered and synchronized.
Key concepts in the Java Memory Model:
- Shared Memory: All threads in a Java program share the same memory, enabling communication through shared variables.
- Visibility: Changes made to shared variables by one thread should become visible to other threads. Without proper synchronization, changes made by one thread may not be immediately visible to other threads.
- Atomicity: Certain operations, such as reading or writing a reference or a 32-bit primitive variable, are atomic and guaranteed to occur as a single, indivisible operation.
- Ordering: The Java Memory Model defines rules for the ordering of memory operations, specifying when changes made by one thread are guaranteed to be seen by other threads.
- Synchronization: Synchronization mechanisms, such as
synchronized
blocks andvolatile
variables, are used to ensure memory visibility and ordering of operations.
36. What are the Java concurrency utilities?
The Java concurrency utilities provide a set of high-level classes and interfaces for writing concurrent and multithreaded programs. These utilities simplify the development of concurrent applications by offering thread management, synchronization, and coordination mechanisms.
Some important Java concurrency utilities include:
- Executor Framework: The
Executor
framework provides a higher-level abstraction for executing tasks asynchronously and managing thread pools. It includes interfaces likeExecutor
,ExecutorService
, and classes likeThreadPoolExecutor
andScheduledThreadPoolExecutor
. - Concurrent Collections: The
java.util.concurrent
package provides thread-safe implementations of common collection classes, such asConcurrentHashMap
,ConcurrentLinkedQueue
, andCopyOnWriteArrayList
, allowing for safe concurrent access without the need for external synchronization. - Atomic Variables: The
java.util.concurrent.atomic
package provides atomic classes, such asAtomicInteger
,AtomicLong
, andAtomicReference
, which offer atomic operations on variables without the need for explicit locking. - Locks and Conditions: The
java.util.concurrent.locks
package provides advanced lock classes, such asReentrantLock
,ReadWriteLock
, andCondition
, allowing for fine-grained control over thread synchronization and coordination. - CountdownLatch, CyclicBarrier, Semaphore: These classes provide synchronization and coordination mechanisms for managing the execution flow of multiple threads.
37. How does garbage collection work in Java?
Garbage collection in Java is the automatic process of reclaiming memory occupied by objects that are no longer reachable or in use by the program. The garbage collector is responsible for identifying and freeing up memory that is no longer needed, ensuring efficient memory management and preventing memory leaks.
The process of garbage collection involves the following steps:
- Marking: The garbage collector traverses the object graph starting from known root objects (e.g., static variables, thread stacks) and marks objects that are still reachable.
- Sweeping: The garbage collector identifies memory regions that are not marked as reachable and considers them as garbage. It reclaims the memory occupied by these objects.
- Compacting: In some garbage collection algorithms, the surviving objects are moved closer together to reduce memory fragmentation and improve memory locality.
Java’s garbage collection is managed by the JVM (Java Virtual Machine) and follows different garbage collection algorithms, such as the mark-and-sweep algorithm, generational garbage collection, and concurrent garbage collection.
The garbage collector runs automatically in the background, freeing memory when needed. However, developers can also manually request garbage collection using the System.gc()
method, although it doesn’t guarantee immediate collection.
38. What is reflection in Java?
Reflection in Java is a feature that allows a program to examine and manipulate its own structure, such as classes, methods, fields, and constructors, at runtime. It provides a way to dynamically access and modify the properties and behaviors of classes, without knowing their details at compile time.
With reflection, you can perform the following tasks:
- Inspect Class Information: Retrieve information about a class, such as its name, superclass, implemented interfaces, constructors, methods, and fields.
- Instantiate Objects Dynamically: Create instances of classes dynamically at runtime, even if their names are not known until then.
- Invoke Methods: Invoke methods on objects dynamically, including private methods that are not accessible by normal means.
- Access and Modify Fields: Get and set the values of fields dynamically, even if they are private or inaccessible by normal means.
39. What is the purpose of the ‘this’ keyword?
In Java, the this
keyword is a reference to the current instance of a class. It is used to refer to the object on which a method is being invoked or to access instance variables or methods within a class when there is a naming conflict between local variables and instance variables.
The this
keyword serves the following purposes:
- Accessing instance variables: It allows access to instance variables of the current object. This is particularly useful when the variable names clash with method parameters or local variables.
- Invoking another constructor: It can be used to invoke another constructor in the same class, using the constructor chaining mechanism.
Here’s an example illustrating the use of the this
keyword:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void introduce() {
System.out.println("Hello, my name is " + this.name);
}
public void changeName(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("John");
person.introduce(); // Output: Hello, my name is John
person.changeName("Alice");
person.introduce(); // Output: Hello, my name is Alice
}
}
In this example, the Person
class has an instance variable name
and methods introduce()
and changeName()
. The this
keyword is used to access the name
instance variable within the introduce()
method. It is also used to differentiate between the method parameter name
and the instance variable name
in the changeName()
method.
40. What is a lambda expression in Java?
In Java, a lambda expression is a concise way to represent an anonymous function—an implementation of a functional interface—that can be treated as a value. It provides a simplified syntax for writing code blocks that can be passed around as arguments or stored in variables.
Here’s an example of a lambda expression:
interface MathOperation {
int operate(int a, int b);
}
public class LambdaExample {
public static void main(String[] args) {
// Lambda expression as an implementation of the MathOperation interface
MathOperation addition = (a, b) -> a + b;
// Using the lambda expression
int result = addition.operate(5, 3);
System.out.println("Result: " + result); // Output: 8
}
}
In this example, we define a functional interface called MathOperation
with a single abstract method operate()
. We then use a lambda expression to implement this interface. The lambda expression (a, b) -> a + b
takes two integers a
and b
as input parameters and returns their sum.
By assigning the lambda expression to the addition
variable of type MathOperation
, we can treat it as an instance of the functional interface. We can then call the operate()
method on the addition
object, passing in arguments 5
and 3
, which will execute the lambda expression and return the result.
Lambda expressions are particularly useful in functional programming, stream processing, and when working with APIs that expect functional interfaces. They provide a more concise and expressive way to write code that deals with behaviors rather than explicit class implementations.
Intermediate Questions
1. What is the use of the “transient” keyword in Java?
The “transient” keyword in Java is used to indicate that a variable should not be serialized when an object is converted into a byte stream. When an object is serialized, all of its member variables are also serialized by default. However, if a variable is marked as transient, its value will not be persisted during serialization and will be set to its default value when the object is deserialized. This is useful when there are variables that don’t need to be persisted, such as temporary or derived values, or variables that contain sensitive information that should not be serialized.
Example:
public class MyClass implements Serializable {
private transient int transientValue;
private int regularValue;
// ...
}
2. How does the “volatile” keyword work in Java?
The “volatile” keyword in Java is used to indicate that a variable may be modified by multiple threads. It ensures that any read or write operation on the variable is directly performed on the main memory instead of using a thread’s local cache. When a variable is declared as volatile, it guarantees visibility and atomicity.
Visibility: Changes made to a volatile variable by one thread are immediately visible to other threads. It prevents thread caching of the variable’s value and ensures that all threads see the most up-to-date value.
Atomicity: Read and write operations on a volatile variable are atomic, which means they are indivisible and thread-safe. No other thread can observe an intermediate state of the variable during a volatile write or read operation.
Example:
public class MyClass {
private volatile int counter = 0;
public void increment() {
counter++;
}
// ...
}
In the example above, if the counter variable is not declared as volatile, multiple threads accessing the increment() method may observe stale values due to thread caching. However, with the volatile keyword, all threads will see the most recent value of the counter variable.
3. Explain the differences between String, StringBuilder, and StringBuffer.
Property | String | StringBuilder | StringBuffer |
---|---|---|---|
Mutability | Immutable | Mutable | Mutable |
Thread Safety | Immutable | Not Thread-safe | Thread-safe |
Performance | Fast | Fast | Slower |
Usage | General use, when immutability is required | Single-threaded environments, when mutability is required and thread-safety is not a concern | Multi-threaded environments, when mutability and thread-safety are required |
Operations | Creating a new string requires memory allocation and copying | Modifying existing string is efficient, no memory allocation required | Modifying existing string is efficient, no memory allocation required |
Example | String name = "John"; | StringBuilder sb = new StringBuilder(); | StringBuffer sb = new StringBuffer(); |
4. Can we create an Immutable object in Java? If yes, how?
Yes, we can create immutable objects in Java. To create an immutable object, we need to follow these guidelines:
- Declare the class as
final
to prevent subclassing. - Make all fields
private
andfinal
to ensure they cannot be modified after object creation. - Do not provide any setter methods to modify the fields.
- Make sure that mutable objects referenced by the class are also handled immutably (e.g., using defensive copies or ensuring they are also immutable).
- Provide a parameterized constructor to set the initial values of the fields.
- Avoid exposing any internal references that could be modified.
Example:
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
In the example above, the ImmutablePerson
class is declared as final
, and its fields (name
and age
) are declared as private
and final
. The class only provides getter methods to access the field values, but no setter methods. Once an ImmutablePerson
object is created, its state cannot be modified, ensuring immutability.
5. How can we make sure that a resource is always closed, regardless of whether an exception occurs or not?
In Java, we can use the try-with-resources
statement to ensure that a resource is always closed, even if an exception occurs. The try-with-resources
statement is used to automatically close resources that implement the AutoCloseable
interface.
The basic syntax of the try-with-resources
statement is as follows:
try (ResourceType resource1 = new ResourceType(); ResourceType resource2 = new ResourceType()) {
// Code that uses the resources
} catch (Exception e) {
// Exception handling
}
When the code execution enters the try
block, the resources are automatically initialized. After the execution of the try
block (whether normally or due to an exception), the resources’ close()
method is called automatically, releasing the associated system resources.
It is important to note that the resources declared within the try
statement must implement the AutoCloseable
interface, which includes classes like FileInputStream
, Connection
, or any custom resource that implements the interface.
Example:
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
// Process each line
}
} catch (IOException e) {
// Exception handling
}
In the example above, the BufferedReader
resource is automatically closed at the end of the try
block, regardless of whether an exception occurs or not.
6. How can you handle memory leaks in Java?
Memory leaks in Java can be handled by following these practices:
- Avoid unnecessary object creation: Create objects only when necessary and ensure proper object lifecycle management.
- Release resources explicitly: Close resources like file handles, database connections, or network sockets explicitly when they are no longer needed, using the
finally
block or thetry-with-resources
statement. - Use proper data structures: Choose appropriate data structures that automatically release resources when they are no longer referenced, such as
WeakHashMap
orSoftReference
. - Avoid excessive caching: Be cautious when using caches to avoid excessive memory consumption. Consider using cache eviction strategies or weak references to automatically remove unused objects from the cache.
- Avoid static references: Static references can keep objects alive for the entire lifetime of the application. Avoid using static fields or ensure they are properly cleared when no longer needed.
- Use profiling tools: Utilize memory profiling tools like Java VisualVM or Eclipse Memory Analyzer to identify memory leaks and analyze object retention.
- Monitor and tune garbage collection: Monitor garbage collection behavior and tune JVM settings, such as heap size or garbage collection algorithms, to optimize memory management.
7. How does the Java class loader work?
The Java class loader is responsible for loading classes into the Java Virtual Machine (JVM) at runtime. It locates and reads class files, verifies their integrity, and transforms them into a format that can be understood and executed by the JVM.
The class loader follows a hierarchical delegation model with three main types of class loaders:
- Bootstrap Class Loader: It is the parent of all class loaders and is responsible for loading core Java classes from the JDK’s rt.jar file. It is implemented in native code and is not written in Java.
- Extension Class Loader: It is the child of the bootstrap class loader and loads classes from the Java extension directories (
<JAVA_HOME>/lib/ext
). - Application Class Loader: Also known as the system class loader, it is the child of the extension class loader and loads classes from the application’s classpath, including user-defined classes.
When a class needs to be loaded, the class loader first checks if the class has already been loaded. If not, it delegates the loading process to its parent class loader. If the parent class loader cannot find the class, the class loader attempts to load it itself.
The class loader searches for class files in various locations, such as directories, JAR files, or network locations, depending on its implementation. Once a class is loaded, it is stored in the JVM’s method area, and its bytecode is executed by the JVM.
8. Explain the difference between overloading a method and overriding it.
Property | Method Overloading | Method Overriding |
---|---|---|
Defined in | Same class | Subclass |
Method signature | Must be different (different number or types of parameters) | Must be the same (same name and parameter types) |
Return type | Can be the same or different | Must be the same or a covariant type |
Usage | Provides multiple methods with the same name but different behaviors | Modifies the behavior of an inherited method |
Relationship | Happens within a single class | Happens between a superclass and its subclass |
Compiler resolution | Decided at compile-time based on the method signature | Decided at runtime based on the actual object type |
Annotation | No specific annotation | @Override annotation is used |
9. Can you explain how the hashCode() and equals() methods work?
The hashCode()
and equals()
methods are used for object comparison in Java.
hashCode()
method: ThehashCode()
method returns an integer value, called the hash code, that represents the object’s state. It is primarily used for efficient storage and retrieval in hash-based data structures likeHashMap
orHashSet
. ThehashCode()
method should be overridden when theequals()
method is overridden.equals()
method: Theequals()
method compares two objects for equality based on their content. By default, it checks whether two objects refer to the same memory location (reference equality). However, it is common to override theequals()
method to provide custom equality checks based on the object’s properties. When overridingequals()
, it is also necessary to override thehashCode()
method to maintain the contract between the two methods.
Example:
public class Person {
private String name;
private int age;
// Constructors, getters, and setters
@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);
}
}
In the example above, the equals()
method compares Person
objects based on their name
and age
fields. The hashCode()
method uses the Objects.hash()
method to calculate a hash code based on the same fields.
10. How does Java handle circular references?
Java uses garbage collection to handle circular references. Circular references occur when two or more objects reference each other directly or indirectly, forming a cycle. If these objects are no longer referenced by any other part of the program, they can become unreachable and occupy memory indefinitely.
Java’s garbage collector can identify and collect objects that are no longer reachable by following a reachability algorithm. This algorithm starts from a set of root objects (such as static variables, local variables on the stack, and other objects directly referenced by the program) and traverses the object graph, marking objects as reachable. Objects that are not marked as reachable are considered garbage and can be reclaimed.
Circularly referenced objects can still be garbage collected if they are no longer reachable from the root objects. The garbage collector detects cycles by utilizing reachability analysis. It tracks whether objects can be reached from the root objects, and if a cycle exists but none of the objects are reachable, the entire cycle can be garbage collected.
11. What is a deadlock? How can you avoid it?
A deadlock is a situation in concurrent programming where two or more threads are blocked forever, waiting for each other to release resources that they hold. It occurs when each thread holds a resource and waits for a resource held by another thread, resulting in a cyclic dependency.
To avoid deadlocks, the following strategies can be employed:
- Resource Ordering: Define a strict order for acquiring resources to avoid circular wait conditions. Ensure that threads always acquire resources in the same order.
- Lock Timeout: Implement a timeout mechanism for acquiring locks. If a lock cannot be acquired within a certain time, release the held resources and retry or take appropriate action.
- Avoidance of Nested Locking: Minimize the locking of multiple resources within the same block of code. If a thread already holds one lock, avoid acquiring another lock before releasing the first one.
- Lock Avoidance: Use lock-free algorithms or non-blocking synchronization mechanisms such as atomic variables, concurrent collections, or semaphores to eliminate the need for traditional locks.
- Deadlock Detection and Recovery: Implement mechanisms to detect deadlocks, such as cycle detection algorithms. Once detected, take actions to recover from the deadlock, such as releasing resources and retrying or notifying the user or administrator.
- Avoiding Unnecessary Resource Hold: Release resources as soon as they are no longer required, rather than holding them for an extended period. This reduces the chances of resource contention and potential deadlocks.
12. Explain the difference between “==” and equals() in Java.
Property | “==” Operator | equals() Method |
---|---|---|
Comparison Purpose | Tests for reference equality (address comparison) | Tests for object equality (content comparison) |
Applicable to | All data types (primitive and reference types) | Reference types (objects) |
Syntax | obj1 == obj2 | obj1.equals(obj2) |
Default Implementation | Compares the memory address of the objects | Checks if two objects have the same content (implementation varies based on class) |
Overriding Possible | Not applicable (final behavior) | Yes, the equals() method can be overridden in classes |
Example | int a = 5; int b = 5; a == b; | String str1 = "Hello"; String str2 = "Hello"; str1.equals(str2); |
13. Explain method resolution in terms of static and dynamic binding.
- Static Binding: Static binding, also known as early binding, occurs during compile-time and is based on the type of the reference variable. The compiler determines the method to be executed based on the reference type rather than the actual object type. Static methods, final methods, and private methods are resolved statically.
- Dynamic Binding: Dynamic binding, also known as late binding or runtime polymorphism, occurs during runtime and is based on the actual type of the object. The JVM determines the method to be executed based on the object’s type at runtime. Dynamic binding is applicable to overridden methods in inheritance hierarchies.
Example:
class Animal {
public void makeSound() {
System.out.println("Animal is making a sound");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat is meowing");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Cat();
animal.makeSound(); // Dynamic binding, Cat's makeSound() method is called
Animal anotherAnimal = new Animal();
anotherAnimal.makeSound(); // Static binding, Animal's makeSound() method is called
}
}
In the example above, the makeSound()
method is overridden in the Cat
class. When animal.makeSound()
is invoked, the actual type of the object (Cat
) is used to determine the method to be called, resulting in dynamic binding. When another Animal.makeSound()
is invoked, the reference type (Animal
) is used to determine the method to be called, resulting in static binding.
14. Describe how generics work in Java.
Generics in Java provide a way to define classes, interfaces, and methods that can work with different types while maintaining type safety at compile-time. They allow the use of placeholders (type parameters) that are replaced with actual types when objects are created or methods are invoked.
Generics provide the following benefits:
- Type Safety: Generics ensure that the appropriate types are used at compile-time, reducing the chances of runtime errors caused by incorrect type usage.
- Code Reusability: Generics enable the creation of generic algorithms and data structures that can be reused with different types, eliminating the need to write duplicate code.
- Compile-Time Checking: The compiler performs type checking and validation, ensuring that incompatible types are not used, which helps catch errors early in the development process.
Example:
public class Box<T> {
private T content;
public void put(T item) {
this.content = item;
}
public T take() {
return content;
}
}
In the example above, T
is a type parameter representing the type of the object stored in the Box
class. When an instance of Box
is created, the type parameter is specified, and all occurrences of T
within the class are replaced with the actual type. This allows the Box
class to work with different types in a type-safe manner.
15. What is the diamond problem? How does Java avoid it?
The diamond problem is a term used in the context of multiple inheritance when a class inherits from two or more classes that have a common superclass. It occurs when the common superclass defines a method that is overridden in the subclasses, leading to ambiguity in method resolution.
Java avoids the diamond problem by disallowing multiple inheritance of classes but allowing multiple inheritance of interfaces. In Java, a class can implement multiple interfaces, and if two or more interfaces define a method with the same signature, the implementing class is responsible for providing an implementation for that method.
16. Explain the difference between the Runnable and Callable interfaces.
Property | Runnable | Callable |
---|---|---|
Introduced in | Java 1.0 | Java 1.5 |
Return Type | void | A value of a specified type (<V> ) |
Exception Throwing | run() method cannot throw checked exceptions | call() method can throw checked exceptions |
Result Retrieval | No direct provision for retrieving a result | Returns a result via the Future interface |
Usage | Suitable for tasks that do not require a return value | Suitable for tasks that return a result or throw an exception |
Example | Runnable task = () -> { /* Task implementation */ }; | Callable<String> task = () -> { /* Task implementation */ return "result"; }; |
17. What are the differences between a HashMap and a Hashtable?
Property | HashMap | Hashtable |
---|---|---|
Introduced in | Java 1.2 | Java 1.0 |
Synchronization | Not synchronized | Synchronized |
Thread-Safety | Not thread-safe (requires external synchronization) | Thread-safe |
Null Keys and Values | Allows null keys and values | Doesn’t allow null keys or values |
Performance | Better performance (unsynchronized access) | Lower performance due to synchronization overhead |
Iterator Fail-Fast | Yes | Yes |
Extends | AbstractMap | Dictionary |
Example | Map<String, Integer> map = new HashMap<>(); | Hashtable<String, Integer> table = new Hashtable<>(); |
18. Can you describe the differences between a Set, a List, and a Map?
Property | Set | List | Map |
---|---|---|---|
Duplicates | Does not allow duplicate elements | Allows duplicate elements | Key-value pairs, keys are unique |
Ordering | No defined ordering | Ordered (by index) | No defined ordering |
Lookup Time | Constant time (O(1)) for contains() and add() | Linear time (O(n)) for contains() | Constant time (O(1)) for get() and put() |
Implementation Examples | HashSet, TreeSet | ArrayList, LinkedList | HashMap, TreeMap |
Example | Set<String> set = new HashSet<>(); | List<Integer> list = new ArrayList<>(); | Map<String, Integer> map = new HashMap<>(); |
19. Describe the Observer pattern and how it’s used in Java.
The Observer pattern is a behavioral design pattern that establishes a one-to-many dependency between objects. In this pattern, when the state of one object (known as the subject or observable) changes, all its dependents (known as observers) are automatically notified and updated.
In Java, the Observer pattern is commonly implemented using the java.util.Observer
interface and the java.util.Observable
class (though the latter has been deprecated since Java 9). The Observable
class represents the subject, and the Observer
interface represents the observers.
To use the Observer pattern in Java, the following steps are typically followed:
- Create the subject class that extends
Observable
and contains the state to be monitored. - Create observer classes that implement the
Observer
interface and define the behavior to be executed when notified. - Register the observers with the subject using the
addObserver()
method. - Implement the logic to notify the observers of state changes using the
setChanged()
andnotifyObservers()
methods of theObservable
class.
20. What are the different types of inner classes in Java?
In Java, there are four types of inner classes:
- Member Inner Class: A member inner class is defined within another class and has access to the enclosing class’s members, including private members. It is typically used when the inner class requires access to the state of the enclosing class.
- Local Inner Class: A local inner class is defined within a method or a block of code and has access to the variables and parameters of its enclosing scope. It is usually used when a specific functionality needs to be implemented locally and is not required outside the block.
- Anonymous Inner Class: An anonymous inner class is a type of local inner class that does not have a named class declaration. It is declared and instantiated simultaneously, usually to implement an interface or extend a class. It is commonly used for event handling or providing implementation on the fly.
- Static Nested Class: A static nested class is defined as a static member of an enclosing class. It is similar to a top-level class and does not have access to the instance variables or methods of the enclosing class unless they are static. It is used when the nested class does not require access to the enclosing class’s instance members.
26. Explain the differences between Java’s old date API and the new date and time API introduced in Java 8.
Property | Old Date API (java.util.Date and java.util.Calendar) | New Date and Time API (java.time package) |
---|---|---|
Design Flaws | Mutable and not thread-safe | Immutable and thread-safe |
Date Manipulation | Difficult and error-prone | Easy and intuitive, with methods like plus() , minus() , and with() |
Timezone Handling | Limited support for timezone handling | Comprehensive support for timezone handling |
API Naming and Usability | Inconsistent and sometimes confusing method names | Consistent and clear method names |
Concurrency Support | No explicit support for concurrency | Support for concurrency using immutable types |
Backward Compatibility | Lacks backward compatibility, making it challenging to migrate existing code | Designed for backward compatibility with methods to convert between APIs |
27. Describe how Java 8’s Optional class works and when it should be used.
Java 8 introduced the Optional
class as a container object that may or may not contain a non-null value. It is used to handle the absence of a value, reducing the occurrence of NullPointerException
and providing a more expressive and safer alternative.
The Optional
class offers methods to work with potentially absent values, such as isPresent()
, get()
, orElse()
, orElseGet()
, or
ElseThrow(
)
, and many more. These methods allow you to handle the presence or absence of a value without explicit null checks.
Optional
should be used in scenarios where a method may return null, indicating the absence of a value. Instead of returning null, the method can return an Optional
object. The calling code can then use methods like isPresent()
or orElse()
to handle the presence or absence of the value.
Example usage of Optional
:
Optional<String> optionalName = getName();
if (optionalName.isPresent()) {
String name = optionalName.get();
System.out.println("Name: " + name);
} else {
System.out.println("Name is absent");
}
In the example, getName()
method returns an Optional<String>
, indicating the possible absence of a name. The code uses isPresent()
to check if the value is present, and get()
to retrieve the value if present. The orElse()
method could be used to provide a default value in case the name is absent.
28. Explain Java memory model. What is the difference between stack and heap memory?
The Java memory model defines how the Java Virtual Machine (JVM) organizes memory during the execution of a program. It specifies the behavior of threads, how they access memory, and how memory is shared between threads.
The memory model consists of two main memory areas: the stack and the heap.
- Stack Memory: Each thread in a Java program has its own stack, which is used for storing method invocations, local variables, and references to objects. It is a LIFO (Last-In-First-Out) structure, where method calls are added and removed in a stack-like manner. Stack memory is fast and memory-efficient but limited in size.
- Heap Memory: The heap is a shared memory area used for dynamic memory allocation. It is the storage for objects and arrays created during runtime. Objects in the heap can be accessed from multiple threads, making it suitable for sharing data. The heap is managed by the JVM’s garbage collector, which automatically deallocates memory for objects that are no longer reachable.
The key differences between stack and heap memory are as follows:
Property | Stack Memory | Heap Memory |
---|---|---|
Allocation | Automatic allocation and deallocation based on method calls | Dynamic allocation and deallocation using new keyword |
Memory Management | Managed automatically by the JVM | Managed by the garbage collector |
Access | Faster access and deallocation through stack pointer | Slower access and deallocation |
Lifetime | Short-lived, local variables and method invocations | Long-lived, objects created and used throughout the program |
Concurrency | Each thread has its own stack | Shared among threads |
Size | Limited in size | Larger size, subject to available memory |
Examples | Method invocations, local variables | Objects, arrays, and data structures |
29. What is the use of Java’s Proxy class?
Java’s Proxy
class is used to create dynamic proxy objects that implement one or more interfaces at runtime. It is part of the Java Reflection API and is mainly used in scenarios such as creating mock objects, implementing interception or logging mechanisms, and building remote procedure call (RPC) frameworks.
To create a dynamic proxy object using the Proxy
class, you need to provide an interface and an invocation handler. The invocation handler is responsible for intercepting method calls made to the proxy object and providing custom behavior.
Here’s an example usage of the Proxy
class:
interface MyInterface {
void myMethod();
}
class MyInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method invocation");
Object result = method.invoke(proxy, args); // Invoke the method on the actual object
System.out.println("After method invocation");
return result;
}
}
public class Main {
public static void main(String[] args) {
MyInterface proxyObject = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class[]{MyInterface.class},
new MyInvocationHandler()
);
proxyObject.myMethod();
}
}
In the example, a dynamic proxy object is created using the Proxy.newProxyInstance()
method. The proxy object implements the MyInterface
interface and uses the MyInvocationHandler
as the invocation handler. The invocation handler intercepts method calls and provides custom behavior before and after the method invocation.
30. Describe the differences between processes and threads in Java.
Property | Processes | Threads |
---|---|---|
Resource Allocation | Each process has its own system resources | Threads share resources within a process |
Memory Isolation | Separate memory space for each process | Shared memory space within a process |
Creation Overhead | Higher creation and termination overhead | Lower creation and termination overhead |
Communication | Inter-process communication mechanisms required | Direct communication within a process |
Scheduling | Processes are scheduled independently | Threads are scheduled within a process |
Scalability | Less scalable due to higher resource consumption | More scalable as they require fewer resources |
Fault Isolation | One process failure does not affect others | Thread failure affects the entire process |
Context Switching | Higher cost of context switching between processes | Lower cost of context switching between threads |
Examples | Running multiple applications simultaneously | Concurrent execution within a single application |
Advanced Questions
1. Explain the JIT compiler in Java.
The Just-In-Time (JIT) compiler is a part of the Java Virtual Machine (JVM) that dynamically compiles Java bytecode into native machine code during runtime. When a Java program is executed, the JVM initially interprets the bytecode, but as the JIT compiler identifies frequently executed portions of code (hotspots), it compiles them into native code for direct execution by the processor. This compilation process optimizes the performance of the code by taking advantage of runtime information.
2. Explain the Java memory model and the role of the JVM in the garbage collection process.
The Java memory model defines how Java programs interact with memory. It specifies the rules and guarantees regarding the visibility of shared data and the ordering of operations in a multi-threaded environment.
The JVM manages memory allocation and deallocation, including garbage collection. Garbage collection is the process of automatically reclaiming memory occupied by objects that are no longer needed by the program. The JVM uses various garbage collection algorithms to identify and free memory occupied by unreachable objects.
3. How does a ConcurrentModificationException occur and how can you prevent it? give code example
A ConcurrentModificationException
occurs when a collection is modified concurrently while it’s being iterated using an iterator. It typically happens when an element is added, removed, or modified in the collection outside of the iterator’s methods.
To prevent a ConcurrentModificationException
, you can use a java.util.concurrent
collection like CopyOnWriteArrayList
or synchronize access to the collection using locks.
Example:
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
// Concurrent modification
for (String element : list) {
list.remove(element); // Throws ConcurrentModificationException
}
// Prevention using CopyOnWriteArrayList
List<String> safeList = new CopyOnWriteArrayList<>(list);
for (String element : safeList) {
safeList.remove(element); // No exception thrown
}
4. How can you create a memory leak in Java?
A memory leak occurs when objects that are no longer needed are not garbage collected and continue to occupy memory. Here’s an example of how a memory leak can be created in Java:
public class LeakyClass {
private static List<Integer> list = new ArrayList<>();
public void addToList(int value) {
list.add(value);
}
// Missing method to remove elements from the list
}
In the above example, the addToList
method adds elements to the list
but there’s no corresponding method to remove elements. As a result, the list keeps growing, consuming more memory over time.
5. Discuss the role of Java Classloader.
The Java Classloader is responsible for loading Java classes into the JVM at runtime. It locates and reads class files, verifies their integrity, and transforms them into Java bytecode that can be executed by the JVM. The Classloader follows a hierarchical delegation model, where each Classloader has a parent Classloader, and if a class is not found, it delegates the request to its parent.
Example:
public class ClassLoaderExample {
public static void main(String[] args) {
// Get the ClassLoader associated with this class
ClassLoader classLoader = ClassLoaderExample.class.getClassLoader();
// Load a class using the ClassLoader
try {
Class<?> loadedClass = classLoader.loadClass("com.example.MyClass");
System.out.println("Class loaded: " + loadedClass.getName());
} catch (ClassNotFoundException e) {
System.out.println("Class not found: " + e.getMessage());
}
}
}
In the above example, the ClassLoaderExample
class demonstrates how to load a class dynamically using the ClassLoader
.
6. What is the contract between equals() and hashCode() methods?
The contract between the equals()
and hashCode()
methods states that if two objects are equal according to the equals()
method, their hash codes must also be equal. Conversely, if two objects have the same hash code, it doesn’t necessarily mean they are equal.
Example:
public class Person {
private String name;
private int age;
// Constructors, getters, and setters
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Person person = (Person) obj;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
In the above example, the equals()
method compares the name
and age
fields for equality, while the hashCode()
method generates a hash code based on the same fields.
7. What are the changes in Memory Management in newer versions of Java, specifically Java 9 onwards?
In Java 9, a new memory management subsystem called “Garbage-First Garbage Collector” (G1 GC) was introduced as the default garbage collector. G1 GC aims to provide more predictable and lower-latency garbage collection by dividing the heap into multiple regions and performing concurrent garbage collection.
Additionally, Java 9 introduced the “Unified JVM Logging” framework, which provides a standardized logging system for JVM components, including memory management. This allows better analysis and monitoring of memory-related events.
8. Explain the working of the Java HashMap and its internal structure.
The HashMap
class in Java provides a data structure that allows efficient key-value mapping and retrieval. It internally uses an array of buckets and a linked list (or a balanced tree in Java 8+) to handle collisions. Here’s a simplified explanation of its working:
- When a key-value pair is added to the
HashMap
, thehashCode()
method of the key is called to determine the bucket location. - If the bucket is empty, the key-value pair is stored in that bucket.
- If the bucket is not empty, a linked list (or a balanced tree) is used to handle collisions. The new key-value pair is added to the list or tree.
- When retrieving a value based on a key, the
hashCode()
method is called to locate the bucket. If the bucket is not empty, theequals()
method is used to find the exact key-value pair.
Example:
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("John", 25);
hashMap.put("Emily", 30);
hashMap.put("Adam", 35);
System.out.println(hashMap.get("Emily")); // Output: 30
}
}
In the above example, a HashMap
is used to store age values for different names. The put()
method adds key-value pairs, and the get()
method retrieves the value associated with a specific key.
9. Discuss Java 8’s Stream API, its purpose, and usage.
Java 8 introduced the Stream API, which is a powerful functional programming feature used to process collections of data in a declarative and parallelizable manner. The Stream API allows operations such as filtering, mapping, reducing, and sorting to be performed on data in a concise and expressive way.
Streams in Java 8 have the following main characteristics:
- Streams are not data structures, but rather a pipeline of operations on a data source.
- Streams can be sequential or parallel, enabling easy parallelization of operations.
- Streams support lazy evaluation, which means operations are performed only when needed.
- Streams can be used with various data sources, including collections, arrays, and I/O channels.
Example:
import java.util.Arrays;
import java.util.List;
public class StreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n * 2)
.sum();
System.out.println(sum); // Output: 20
}
}
In the above example, the Stream API is used to calculate the sum of even numbers multiplied by 2 from a list of integers. The filter()
method filters out odd numbers, the mapToInt()
method doubles the even numbers, and the sum()
method calculates the sum of the resulting values.
10. Explain in detail how multithreading and concurrency works in Java.
Multithreading in Java allows concurrent execution of multiple threads within a single program. Each thread represents an independent flow of control, capable of performing tasks simultaneously. Concurrency refers to the ability to make progress on more than one task at a time.
In Java, multithreading and concurrency are achieved using the Thread
class or the Runnable
interface. The Thread
class represents a thread of execution, while the Runnable
interface provides a way to define the code to be executed by a thread.
Example:
public class ThreadExample {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable("Thread 1"));
Thread thread2 = new Thread(new MyRunnable("Thread 2"));
thread1.start();
thread2.start();
}
static class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + ": " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
In the above example, two threads are created using the Thread
class and the Runnable
interface. Each thread executes the run()
method, which prints a message with a number and sleeps for 500 milliseconds. The output shows the interleaved execution of the two threads.
11. How do you handle deadlock in Java?
Deadlock occurs when two or more threads are blocked forever, waiting for each other to release the resources they hold. To handle deadlock in Java, you can follow these general strategies:
- Avoidance: Ensure that resources are acquired in a consistent order to prevent circular dependencies.
- Detection: Use techniques like resource allocation graphs or thread dump analysis to identify and diagnose deadlocks.
- Prevention: Use higher-level synchronization constructs like
java.util.concurrent
classes, which handle resource acquisition and release automatically. - Recovery: Restarting the application or killing the affected threads can resolve a deadlock, but it should be used cautiously.
Example:
public class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object
resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Acquired resource 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 1: Acquired resource 2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Acquired resource 2");
synchronized (resource1) {
System.out.println("Thread 2: Acquired resource 1");
}
}
});
thread1.start();
thread2.start();
}
}
In the above example, two threads are created and each thread attempts to acquire the resources in a different order. This creates a potential deadlock situation where each thread holds one resource and waits for the other resource to be released.
12. What is the difference between CyclicBarrier and CountDownLatch classes in concurrency?
CyclicBarrier | CountDownLatch |
---|---|
Allows a set number of threads to wait for each other at a barrier | Allows one or more threads to wait for the completion of events |
Can be reused after the barrier is broken | Cannot be reused once the count reaches zero |
Threads are released simultaneously | Threads are released one by one in the order of countdown |
Requires all threads to reach the barrier before releasing them | Requires a fixed number of events to occur before releasing |
13. Discuss the differences between Executor Framework and ForkJoinPool.
The Executor Framework and ForkJoinPool are both part of the Java concurrency framework, but they have different purposes and use cases:
- Executor Framework: It provides a higher-level abstraction for managing and executing tasks asynchronously. It includes interfaces like
Executor
,ExecutorService
, andThreadPoolExecutor
. The Executor Framework is suitable for executing independent tasks or tasks that don’t require recursive divide-and-conquer processing.
Example:
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
executor.execute(new MyTask(i));
}
executor.shutdown();
In the above example, an ExecutorService
is created using Executors.newFixedThreadPool()
and tasks are submitted for execution using the execute()
method.
- ForkJoinPool: It is designed for executing recursive divide-and-conquer tasks that can be broken down into smaller subtasks. ForkJoinPool utilizes work-stealing algorithms, where idle threads steal work from other busy threads. It is particularly useful for parallelizing algorithms like divide-and-conquer, parallel sorting, and parallel recursive tasks.
Example:
ForkJoinPool forkJoinPool = new ForkJoinPool();
MyRecursiveTask task = new MyRecursiveTask(data);
Integer result = forkJoinPool.invoke(task);
In the above example, a ForkJoinPool
is created, and a MyRecursiveTask
is invoked using the invoke()
method to perform parallel processing on the given data.
14. How does the CopyOnWriteArrayList work in Java?
CopyOnWriteArrayList
is a concurrent collection class in Java that provides thread-safe access to a list. It achieves thread-safety by creating a new copy of the underlying array whenever a modification operation (e.g., add or remove) is performed. This copy-on-write behavior ensures that iteration over the list is always consistent and doesn’t throw ConcurrentModificationException
.
Example:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
list.add("C"); // Doesn't affect the ongoing iteration
}
}
}
In the above example, the CopyOnWriteArrayList
is created, and elements are added to it. During iteration, the list is modified by adding another element, but it doesn’t affect the ongoing iteration. The iteration completes without any ConcurrentModificationException
.
15. What is a classpath and how does it work?
The classpath is an environment variable or a command-line option in Java that specifies the locations where the Java runtime should look for classes and resources when executing a Java program.
When a Java program is compiled, the Java compiler converts the source code into bytecode (.class files), which are platform-independent representations of the code. During runtime, the Java Virtual Machine (JVM) needs to find and load these bytecode files along with any required libraries or resources.
The classpath defines the search path for the JVM to locate these files. It is a list of directories, JAR files, or ZIP files where the JVM should look for classes and resources. The JVM searches for the necessary bytecode files within the specified locations in the order they are listed.
By default, the JVM includes the current working directory (.) in the classpath. However, it’s common to set the classpath explicitly to include additional directories or external libraries.
The classpath can be set in different ways:
- Command-line option: You can use the
-cp
or-classpath
option when executing thejava
command to specify the classpath. For example:
java -cp path/to/classes:path/to/libraries MyClass
- Environment variable: You can set the
CLASSPATH
environment variable to specify the classpath. For example:
export CLASSPATH=/path/to/classes:/path/to/libraries
- Build tools or IDEs: Build tools like Maven or Gradle, and Integrated Development Environments (IDEs) like Eclipse or IntelliJ IDEA, provide mechanisms to manage the classpath for your projects. They automatically handle the classpath configuration based on project dependencies and build configurations.
16. Explain the concept of Reflection in Java.
Reflection in Java allows a program to examine and manipulate the structure and behavior of classes, interfaces, fields, methods, and constructors at runtime. It provides a way to inspect and modify objects, invoke methods dynamically, and create new instances of classes.
Example:
import java.lang.reflect.Method;
public class ReflectionExample {
public static void main(String[] args) throws Exception {
String className = "com.example.MyClass";
Class<?> myClass = Class.forName(className);
Method method = myClass.getMethod("sayHello", String.class);
Object instance = myClass.getDeclaredConstructor().newInstance();
method.invoke(instance, "John");
}
}
class MyClass {
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
}
In the above example, the ReflectionExample
class demonstrates how to use reflection to dynamically invoke a method. It loads the class “com.example.MyClass” at runtime, gets the sayHello
method, creates an instance of the class, and invokes the method with a parameter.
17. Explain how Java handles operator overloading.
Java does not support operator overloading like some other languages (e.g., C++). In Java, the behavior of operators is predefined for built-in types, and new operators cannot be defined for user-defined types.
However, Java does provide a limited form of operator overloading through the use of certain classes and interfaces. For example, the +
operator is overloaded for string concatenation, allowing concatenation of two strings using the +
operator.
Example:
String str1 = "Hello";
String str2 = " World";
String result = str1 + str2; // Concatenation using the + operator
System.out.println(result); // Output: Hello World
In the above example, the +
operator is used to concatenate two strings (str1
and str2
). The result is a new string that combines both values.
18. Discuss how Lambda Expressions work in Java and their use cases.
Lambda expressions in Java provide a concise way to express anonymous functions or function pointers. They enable the use of functional programming constructs in Java and make code more readable and expressive.
A lambda expression consists of a parameter list, an arrow (->
), and a body. It can be assigned to a functional interface, which is an interface with a single abstract method (SAM). The lambda expression provides an implementation for that method.
Example:
interface MathOperation {
int operate(int a, int b);
}
public class LambdaExample {
public static void main(String[] args) {
MathOperation add = (a, b) -> a + b;
MathOperation subtract = (a, b) -> a - b;
int result1 = add.operate(5, 3);
int result2 = subtract.operate(5, 3);
System.out.println(result1); // Output: 8
System.out.println(result2); // Output: 2
}
}
In the above example, the MathOperation
interface represents a binary mathematical operation. Lambda expressions are used to define two operations: addition and subtraction. The operate()
method is implemented using lambda expressions, and the results are printed.
19. How does Java handle multiple inheritances?
In Java, multiple inheritances (i.e., inheriting from multiple classes) are not allowed. This is done to avoid the “diamond problem” where the inheritance hierarchy becomes ambiguous.
However, Java supports multiple inheritances through interfaces. An interface can extend multiple interfaces, allowing a class to implement multiple interfaces and inherit their abstract methods.
Example:
interface Interface1 {
void method1();
}
interface Interface2 {
void method2();
}
class MyClass implements Interface1, Interface2 {
public void method1() {
System.out.println("Method 1 implementation");
}
public void method2() {
System.out.println("Method 2 implementation");
}
}
public class MultipleInheritanceExample {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.method1(); // Output: Method 1 implementation
obj.method2(); // Output: Method 2 implementation
}
}
20. Describe the difference between a normal servlet and a Spring MVC Servlet.
A normal servlet is a Java class that extends the javax.servlet.http.HttpServlet
class and handles HTTP requests and responses. It requires manual configuration in the web deployment descriptor (web.xml) or using annotations like @WebServlet
.
On the other hand, a Spring MVC servlet is a part of the Spring MVC framework, which provides a model-view-controller architecture for building web applications. The Spring MVC servlet is the front controller that receives HTTP requests, routes them to appropriate handler methods, and delegates to view resolvers for rendering the response.
Key differences between a normal servlet and a Spring MVC servlet are:
- Configuration: Normal servlets are configured using web.xml or annotations, while Spring MVC servlets are configured using Spring configuration files (e.g., applicationContext.xml) or Java-based configuration.
- Request Handling: In a normal servlet, you handle requests and responses directly within the servlet class. In Spring MVC, you define controller classes with handler methods that handle specific requests based on annotations like
@RequestMapping
. - Dependency Injection: Spring MVC promotes the use of dependency injection and inversion of control, allowing you to inject dependencies into controller classes. Normal servlets usually don’t have built-in dependency injection capabilities.
- Additional Features: Spring MVC provides additional features like declarative transaction management, validation, internationalization, view resolution, and integration with other Spring components.
21. What is Spring Boot and why is it important in Java?
Spring Boot is a framework built on top of the Spring Framework that simplifies the development of Java applications, particularly web applications. It provides a streamlined and opinionated approach to configure and run Spring applications with minimal effort and boilerplate code.
Key features and benefits of Spring Boot:
- Auto-configuration: Spring Boot automatically configures the application based on dependencies and sensible defaults, reducing manual configuration.
- Embedded Server: It includes an embedded server (e.g., Tomcat, Jetty) so that you can run the application as a standalone JAR file without needing to deploy it in a separate server.
- Starter Dependencies: Spring Boot provides starter dependencies that bring in all the necessary dependencies for common use cases (e.g., web applications, database connectivity) with predefined configurations.
- Actuator: Spring Boot Actuator provides endpoints and tools for monitoring and managing the application in production.
- Production-Ready: Spring Boot promotes best practices for production-ready applications, including metrics, health checks, security configurations, and externalized configuration.
22. Explain what a JVM bytecode is.
JVM bytecode is a low-level instruction set that the Java Virtual Machine (JVM) understands and executes. When a Java program is compiled, it is translated into platform-independent bytecode, which can be executed by any JVM implementation.
JVM bytecode is similar to machine code, but it’s designed to be platform-independent and portable. It represents the Java program’s instructions and data in a binary format.
The JVM executes bytecode by interpreting each instruction or by dynamically compiling it into native machine code using the Just-In-Time (JIT) compiler. This intermediate bytecode representation allows Java programs to be executed on any system with a compatible JVM.
23. Discuss the new features in Java 9/10/11.
Java 9, 10, and 11 introduced several new features and enhancements. Here are some key features in each version:
Java 9:
- Modular System (Project Jigsaw): Introduced the Java Platform Module System (JPMS) to modularize the JDK and applications, improving security, performance, and maintainability.
- Reactive Streams: Added the
Flow
API for reactive programming, providing standard interfaces for asynchronous stream processing. - JShell: Introduced an interactive Read-Eval-Print Loop (REPL) tool for experimenting with Java code snippets.
Java 10:
- Local Variable Type Inference: Added the
var
keyword for inferring the type of local variables, improving code readability. - Garbage Collector Interface: Introduced the
GarbageCollectorMXBean
interface for better control and monitoring of garbage collectors.
Java 11:
- HTTP Client: Added a new HTTP client API (
java.net.http.HttpClient
) for making HTTP requests and handling responses. - Local-Variable Syntax for Lambda Parameters: Enabled the use of
var
in lambda parameters for more concise code. - Launch Single-File Source-Code Programs: Introduced the ability to directly run single-file Java programs without explicit compilation.
24. How to use the Java Module System introduced in Java 9?
To use the Java Module System introduced in Java 9, you need to follow these steps:
- Create module-info.java: Create a
module-info.java
file in the source folder of your module. This file declares the module and specifies its dependencies and exported packages. - Compile with modules: When compiling the code, use the
--module-source-path
option to specify the source path and the--module
option to specify the main module. - Run with modules: When running the code, use the
--module-path
option to specify the module path containing the compiled modules and the--module
option to specify the main module.
Example:
Suppose you have a project with the following directory structure:
project
├── src
│ ├── com.example
│ │ ├── module-info.java
│ │ └── MyClass.java
│ └── module-info.java
In module-info.java
of the main module:
module main {
requires module1;
requires module2;
}
In module-info.java
of module1:
module module1 {
exports com.example.module1;
}
In module-info.java
of module2:
module module2 {
requires module1;
exports com.example.module2;
}
To compile and run the code:
javac --module-source-path src -d out $(find src -name "*.java")
java --module-path out --module main/com.example.MyClass
25. What are CompletableFuture in Java and where are they used?
CompletableFuture
is a class introduced in Java 8 that represents a future result of an asynchronous computation. It provides a way to perform operations on the result of an asynchronous task when it becomes available, without blocking the calling thread.
CompletableFuture
can be used to compose, combine, and handle the results of asynchronous tasks. It supports a wide range of operations, including chaining dependent tasks, combining multiple tasks, applying transformations, and handling exceptions.
CompletableFuture
is commonly used in scenarios where you need to perform non-blocking and parallel computations, such as making multiple remote API calls concurrently, handling callbacks in asynchronous event-driven programming, or implementing parallel algorithms.
Example:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenApply(String::toUpperCase);
future.thenAccept(System.out::println); // Output: HELLO WORLD
}
}
In the above example, a CompletableFuture
is created by using supplyAsync()
to perform a computation asynchronously. Then, thenApply()
methods are chained to apply transformations to the result. Finally, thenAccept()
is used to handle the result when it becomes available and print it.
26. Explain in detail about Java’s Optional API.
The Optional API in Java is introduced in Java 8 as a container object that may or may not contain a non-null value. It provides a way to handle the absence of a value and avoid null pointer exceptions.
The key features of the Optional API are:
Optional<T>
: The main class of the Optional API. It is a parameterized type that represents an optional value of typeT
.of()
andofNullable()
: Static factory methods to create instances ofOptional
.of()
creates anOptional
with a non-null value, whileofNullable()
creates anOptional
that may or may not have a value.isPresent()
: Method to check if theOptional
contains a value.get()
: Method to retrieve the value from theOptional
. It throws aNoSuchElementException
if theOptional
is empty.orElse()
andorElseGet()
: Methods to provide a default value when theOptional
is empty.orElse()
always evaluates the parameter, whileorElseGet()
lazily evaluates the parameter.map()
andflatMap()
: Methods to transform the value inside theOptional
. They allow applying functions to the value and return a newOptional
.filter()
: Method to conditionally transform the value based on a predicate. It returns anOptional
containing the value if the predicate is true, otherwise an emptyOptional
.
27. What is a metaspace in Java 8 and above?
Metaspace is a new memory space introduced in Java 8 as a replacement for the permanent generation (PermGen) space. It is a part of the JVM’s memory management system and is used to store class metadata, such as class definitions, bytecode, and method information.
Unlike the PermGen space, which had a fixed size and caused issues with class loading and unloading, Metaspace is dynamic and can grow or shrink based on the application’s needs. It is allocated from the native memory of the operating system.
Metaspace offers several advantages over PermGen, including:
- Automatic memory management: Metaspace is managed by the JVM’s garbage collector, removing the need for manual tuning of PermGen space.
- No more OutOfMemoryErrors: With Metaspace, classes and class metadata are stored in native memory, reducing the risk of running out of memory due to class loading.
- Improved performance: Metaspace provides faster class loading and unloading, resulting in improved application startup times.
28. What is the concept of String Pool in Java?
The String Pool in Java is a special memory area in the JVM where String literals are stored. It is a way to reuse immutable String objects, reducing memory usage and improving performance.
When a String literal is encountered in the code, the JVM checks if the String already exists in the String Pool. If it does, a reference to the existing String object is returned. If not, a new String object is created and added to the String Pool for future reuse.
The String Pool provides the following benefits:
- Memory efficiency: Since String literals are shared, multiple references to the same String literal point to the same object, reducing memory consumption.
- Performance optimization: String comparisons using
equals()
or==
can be optimized by comparing references, as the String Pool ensures that identical String literals share the same object.
29. Explain the Reactive programming in Java.
Reactive programming is a programming paradigm focused on building asynchronous, event-driven systems that are responsive, resilient, and scalable. It aims to handle and react to streams of data and events as they occur, rather than relying on traditional request-response patterns.
In Java, reactive programming is enabled by frameworks and libraries that provide Reactive Extensions (Rx) or Reactive Streams implementations. These libraries allow developers to compose and transform streams of data, apply backpressure to handle data flow, and handle errors and concurrency in a declarative and composable manner.
The key concepts in reactive programming are:
- Streams: Represent continuous sequences of data or events.
- Observables (or Publishers): Emit data or events over time.
- Subscribers (or Observers): Subscribe to observables and react to the emitted data or events.
- Operators: Perform transformations and manipulations on streams, such as filtering, mapping, and reducing.
30. How does the Garbage Collection work in Java and what are its types?
Garbage collection in Java is the automatic process of reclaiming memory occupied by objects that are no longer needed. The JVM’s garbage collector manages memory allocation and deallocation, allowing developers to focus on writing code rather than manual memory management.
The garbage collection process typically involves the following steps:
- Marking: The garbage collector starts from a set of root objects (e.g., static variables, local variables in active threads) and traverses the object graph, marking objects that are still reachable.
- Sweep/Compact: The garbage collector sweeps through the heap, freeing memory occupied by objects that were not marked as reachable. In some cases, it may also compact the remaining objects to reduce fragmentation.
- Finalization: Objects with finalizers are finalized during the garbage collection process. Finalization allows objects to perform cleanup actions before they are garbage collected.
Java provides different types of garbage collectors to suit different application needs:
- Serial Collector: Suitable for single-threaded applications or small-scale applications with low memory requirements.
- Parallel Collector: Performs garbage collection using multiple threads, leading to improved throughput.
- Concurrent Mark Sweep (CMS) Collector: Performs most of the garbage collection work concurrently with the application’s execution, reducing pauses.
- Garbage-First Garbage Collector (G1GC): Introduced in Java 9, it is designed for large heaps and provides predictable pause times by dividing the heap into regions and performing concurrent garbage collection.
Coding Questions
1. Reverse a String
public String reverseString(String str) {
char[] charArray = str.toCharArray();
int start = 0;
int end = str.length() - 1;
while (start < end) {
char temp = charArray[start];
charArray[start] = charArray[end];
charArray[end] = temp;
start++;
end--;
}
return new String(charArray);
}
2. Factorial
// Iterative approach
public int factorialIterative(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
// Recursive approach
public int factorialRecursive(int n) {
if (n == 0) {
return 1;
} else {
return n * factorialRecursive(n - 1);
}
}
3. Fibonacci Series
// Print Fibonacci series up to a given number
public void printFibonacciSeries(int n) {
int first = 0;
int second = 1;
System.out.print(first + " " + second + " ");
for (int i = 2; i < n; i++) {
int next = first + second;
System.out.print(next + " ");
first = second;
second = next;
}
}
// Return the nth number in the Fibonacci series
public int getNthFibonacciNumber(int n) {
if (n <= 1) {
return n;
}
int first = 0;
int second = 1;
for (int i = 2; i <= n; i++) {
int next = first + second;
first = second;
second = next;
}
return second;
}
4. Palindrome
// Check if a number is a palindrome
public boolean isPalindromeNumber(int number) {
int originalNumber = number;
int reversedNumber = 0;
while (number != 0) {
int remainder = number % 10;
reversedNumber = reversedNumber * 10 + remainder;
number /= 10;
}
return originalNumber == reversedNumber;
}
// Check if a string is a palindrome
public boolean isPalindromeString(String str) {
int start = 0;
int end = str.length() - 1;
while (start < end) {
if (str.charAt(start) != str.charAt(end)) {
return false;
}
start++;
end--;
}
return true;
}
5. Prime Number
public boolean isPrime(int number) {
if (number <= 1) {
return false;
}
for (int i = 2; i <= Math.sqrt(number); i++) {
if (number % i == 0) {
return false;
}
}
return true;
}
6. Array Sorting
import java.util.Arrays;
public void sortArray(int[] arr) {
Arrays.sort(arr);
}
7. Duplicate Elements in Array
import java.util.HashSet;
import java.util.Set;
public Set<Integer> findDuplicateElements(int[] arr) {
Set<Integer> duplicateElements = new HashSet<>();
Set<Integer> uniqueElements = new HashSet<>();
for (int num : arr
) {
if (!uniqueElements.add(num)) {
duplicateElements.add(num);
}
}
return duplicateElements;
}
8. Second Largest in Array
public int findSecondLargest(int[] arr) {
if (arr.length < 2) {
throw new IllegalArgumentException("Array must have at least 2 elements.");
}
int largest = Integer.MIN_VALUE;
int secondLargest = Integer.MIN_VALUE;
for (int num : arr) {
if (num > largest) {
secondLargest = largest;
largest = num;
} else if (num > secondLargest && num != largest) {
secondLargest = num;
}
}
return secondLargest;
}
9. Substrings of a String
public List<String> generateSubstrings(String str) {
List<String> substrings = new ArrayList<>();
for (int i = 0; i < str.length(); i++) {
for (int j = i + 1; j <= str.length(); j++) {
substrings.add(str.substring(i, j));
}
}
return substrings;
}
10. Armstrong Number
public boolean isArmstrongNumber(int number) {
int originalNumber = number;
int sum = 0;
int digits = String.valueOf(number).length();
while (number != 0) {
int remainder = number % 10;
sum += Math.pow(remainder, digits);
number /= 10;
}
return originalNumber == sum;
}
11. Matrix Operations
public int[][] matrixAddition(int[][] matrix1, int[][] matrix2) {
int rows = matrix1.length;
int columns = matrix1[0].length;
int[][] result = new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
result[i][j] = matrix1[i][j] + matrix2[i][j];
}
}
return result;
}
public int[][] matrixMultiplication(int[][] matrix1, int[][] matrix2) {
int rows1 = matrix1.length;
int columns1 = matrix1[0].length;
int columns2 = matrix2[0].length;
int[][] result = new int[rows1][columns2];
for (int i = 0; i < rows1; i++) {
for (int j = 0; j < columns2; j++) {
for (int k = 0; k < columns1; k++) {
result[i][j] += matrix1[i][k] * matrix2[k][j];
}
}
}
return result;
}
12. Anagrams
public boolean areAnagrams(String str1, String str2) {
if (str1.length() != str2.length()) {
return false;
}
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
Arrays.sort(chars1);
Arrays.sort(chars2);
return Arrays.equals(chars1, chars2);
}
13. Binary Search
public int binarySearch(int[] arr, int target) {
int low = 0;
int high = arr.length - 1;
while (low <= high)
{
int mid = low + (high - low) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1; // Target not found
}
14. Depth-First Search (DFS) or Breadth-First Search (BFS)
import java.util.*;
public void dfs(int[][] graph, int start) {
boolean[] visited = new boolean[graph.length];
Stack<Integer> stack = new Stack<>();
stack.push(start);
while (!stack.isEmpty()) {
int node = stack.pop();
if (!visited[node]) {
System.out.print(node + " ");
visited[node] = true;
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
stack.push(neighbor);
}
}
}
}
}
15. Tree Traversal
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
public void preOrderTraversal(TreeNode root) {
if (root != null) {
System.out.print(root.val + " ");
preOrderTraversal(root.left);
preOrderTraversal(root.right);
}
}
public void inOrderTraversal(TreeNode root) {
if (root != null) {
inOrderTraversal(root.left);
System.out.print(root.val + " ");
inOrderTraversal(root.right);
}
}
public void postOrderTraversal(TreeNode root) {
if (root != null) {
postOrderTraversal(root.left);
postOrderTraversal(root.right);
System.out.print(root.val + " ");
}
}
16. Binary Search Tree
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
class BinarySearchTree {
private TreeNode root;
public void insert(int val) {
root = insertNode(root, val);
}
private TreeNode insertNode(TreeNode node, int val) {
if (node == null) {
return new TreeNode(val);
}
if (val < node.val) {
node.left = insertNode(node.left, val);
} else if (val > node.val) {
node.right = insertNode(node.right, val);
}
return node;
}
public boolean search(int val) {
return searchNode(root, val);
}
private boolean searchNode(TreeNode node, int val) {
if (node == null) {
return false;
}
if (val == node.val) {
return true;
} else if (val < node.val) {
return searchNode(node.left, val);
} else {
return searchNode(node.right, val);
}
}
public void delete(int val) {
root = deleteNode(root, val);
}
private TreeNode deleteNode(TreeNode node, int val) {
if (node == null) {
return null;
}
if (val < node.val) {
node.left = deleteNode(node.left, val);
} else if (
val > node.val) {
node.right = deleteNode(node.right, val);
} else {
if (node.left == null) {
return node.right;
} else if (node.right == null) {
return node.left;
}
TreeNode minNode = findMin(node.right);
node.val = minNode.val;
node.right = deleteNode(node.right, minNode.val);
}
return node;
}
private TreeNode findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
}
17. Sorting Algorithms
public void mergeSort(int[] arr) {
if (arr.length <= 1) {
return;
}
int mid = arr.length / 2;
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
mergeSort(left);
mergeSort(right);
merge(left, right, arr);
}
private void merge(int[] left, int[] right, int[] arr) {
int i = 0;
int j = 0;
int k = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
arr[k++] = left[i++];
} else {
arr[k++] = right[j++];
}
}
while (i < left.length) {
arr[k++] = left[i++];
}
while (j < right.length) {
arr[k++] = right[j++];
}
}
18. Linked List Reversal
class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode reverseLinkedList(ListNode head) {
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}
19. Hash Table Implementation
class Entry {
int key;
int value;
Entry next;
public Entry(int key, int value) {
this.key = key;
this.value = value;
}
}
class HashTable {
private Entry[] buckets;
private int capacity;
private int size;
public HashTable(int capacity) {
this.capacity = capacity;
this.size = 0;
this.buckets = new Entry[capacity];
}
public void put(int key, int value) {
int index = hash(key);
Entry entry = buckets[index];
while (entry != null) {
if (entry.key == key) {
entry.value = value;
return;
}
entry = entry.next;
}
Entry newEntry = new Entry(key, value);
newEntry.next = buckets[index];
buckets[index] = newEntry;
size++;
}
public int get(int key) {
int index = hash(key);
Entry entry = buckets[index];
while (entry != null) {
if (entry.key == key) {
return entry.value;
}
entry = entry.next;
}
return -1; // Key not found
}
public void remove(int key) {
int index = hash(key);
Entry prev = null;
Entry entry = buckets[index];
while (entry !=