Frequently asked Java Interview questions
-
The main differences between JVM, JRE and JDK are listed below,
JVM: Java Virtual Machine(JVM) is an abstract machine that provides a runtime environment for the execution of Java ByteCode. i.e, It is part of Java Runtime Environment(JRE) and responsible for converting bytecode into machine-readable code. JVM is platform dependent but it interprets the bytecode which is platform independent. As a result, Java applications to run on different platforms and operating systems.
JRE: Java Runtime Environment (JRE) is an installation package that provides Java Virtual Machine (JVM), class libraries and other components to run the Java program or application on any machine.
JDK: Java Development Kit (JDK) is a software development kit used to develop and execute Java programs. It includes tools for developing, monitoring and debugging java programs, along with JRE to execute those respective programs.
The pictorial representation of these three components looks as follows,
-
Java is platform-independent language because java programs are compiled to a bytecode that can be run on any device or OS which has a Java Virtual Machine(JVM). This is possible due to JVM's "Write Once, Run Anywhere"(WORA) capability. Due to this reason, you can write a Java program on one platform such as Windows machine and then run it on a different platform such as macOS or Linux machine without making any modifications to the code.
Below diagram explains the platform independence feature of Java,
-
JVM(Java Virtual Machine) is an abstract runtime engine to run java applications. It is part of Java Runtime Environment(JRE) to execute the bytecode generated by java compilers. JVM brings WORA(Write Once Read Anywhere) behavior in which you can write java program on one system and expected to run on any java enabled systems without any modifications. Upon compiling .java file to .class file(contains byte code), .class file goes into several steps described by JVM.
JVM consists of three main components:
- Class Loader subsystem
- Runtime data area/ memory area
- Execution engine
Let's understand the JVM architecture which includes the above three components,
-
Class Loader Subsystem: The class loaders are responsible to load the class files dynamically into JVM during the runtime. The first class that loaded into the memory is usually the class that contains
mainmethod.This subsystem performs three main activities.-
Loading: As part of this step, the class loader will read the .class file and generate the corresponding bytecode. The following important information of the class file will be saved in method area of JVM.
- The Fully Qualified Class Name(FQCN) of the loaded class.
- Immediate parent class.
- Variables, method and modifier information
- Whether .class is related to Class, Interface or Enum.
JVM also creates an object of Class type(available from java.lang package) to represent the file in the heap memory. But it will be created only on the very first time when the class file is loaded into JVM. This object is helpful for the developers to get class level information.
-
Linking: Linking is the process of taking a class or interface and combining it into the runtime state of the Java virtual machine, preparing it for execution. It involves the following steps:
-
Verification: This phase ensure the correctness of
.classfile by checking whether the file is properly formatted or not, and the code generated by a valid compiler or not. If the validation is failed, you will receivejava.lang.VerifyErrorthrough ByteCodeVerifier. -
Preparation: In this step, JVM allocates memory for all static variables within classes/interfaces and assign them with default values.
-
Resolution: In this step, all symbolic references to classes or interfaces are replaced with their actual memory locations. This transformation is known as Dynamic Linking.
-
-
Initialization: This is the last phase of class loading, where all static variables are replaced with their original values and static block gets executed.
-
There are three built-in classloaders available in Java:
-
Bootstrap ClassLoader: This is a root class loader and typically represented as null. It loads the core java libraries located in <JAVA_HOME>/jmods folder like
java.lang,java.utiletc. This class loader is written in native code unlike other class loaders. -
Platform ClassLoader: This class loader is responsible for loading platform classes such as Java SE platform APIs, their implementation classes and JDK-specific run-time classes. For example, the platform classes such as
java.net.http,java.sql,org.graalvm.compile(JDK Specific). -
System/Application classLoader: This class loader is responsible for loading all the classes configured from the classpath. The classpath can contain classes from directories, JAR files etc.
The above class loaders follows a class loader hierarchy ensuring a delegation mechanism. That means, if the class is loaded and its not found by application classloader, the request is delegated upwards to the platform application class loader. But even it is not found by platform class loader then further delegate upwards to Bootstrap class loader.
Note: Apart from the above built-in class loaders, you can also create your own user defined class loader for ensuring application independence. For example, Tomcat webserver use this approach(WebAppClassLoader) to ensure that different web applications to load and isolate classes/jars.
-
Runtime data area/ memory area:
The runtime data areas are mainly divided into five components,
-
Method: The method area stores below class level information,
- ClassLoader reference
- Runtime constant pool
- Field data such as name, type, modifiers, attributes etc of each field.
- Method data such as name, return type, modifiers, attributes, parameter types etc of each field.
- Constructor data such as parameter types and their order for each constructor.
This is a shared resource and only one method area is available per JVM.
-
Heap: This is the runtime data area in which all the objects and their respective instance variables are stored. The heap is created upon JVM startup and there is only one heap per JVM.
-
Stack: Each thread which is created in JVM, a separate runtime-stack is also created at the same time. The local variables, method calls and intermediate results are stored in this stack area. But if the thread requires a larger stack size than what is available, JVM throws
StackOverflowerror.
For each method call, a stack frame is created in the stack memory area and it is going to be destroyed upon method execution gets completed. Each stack frame is further divided into three sections.
- Local variables: For each frame, all the local variables and their values are stored in an array format.
- Operand stack: In each frame, the variables and operators required for mathematical operations are stored in LIFO stack.
- Frame data: This area holds all symbolic references related to constant pool resolution and method returned data. It is also used to store a reference to the exception table which has all the information related to catch block in case of exceptions.
Note: Stack area is not a shared resource.
-
PC Registers: JVM supports multiple threads at the same time and each thread has separate PC registers. These registers are used to store address of the currently executed JVM instruction. After completion of current instruction, PC register is going to be updated with next instruction.
-
Native method stack: This stack is used to hold the information related to native methods written in C, C++ and Assembly.
-
-
Execution engine: The execution engine is responsible for executing the bytecode that is loaded into the JVM. It communicates with various runtime data memories of JVM during the execution. Once the program is finished executing, the memory in the runtime data area will be released.
It contains three main components for executing the class files.
-
Interpreter: The interpreter reads and executes the bytecode instructions line by line to convert into respective machine language instructions. The main advantage of interpreter is that it is very quick to load and execute the code in a faster pace. But whenever it executes the same code blocks again and again, the interpreter executes repeatedly. So the interpreter cannot be optimized the runtime in case of repeated code execution.
-
JIT compiler: Just-In-Time Compiler(JIT Compiler) is introduced to overcome the disadvantages of interpreter, especially with repeated code execution. JIT compiler can remember the repeated code block segments and they will stored as native code in the cache. Whenever JIT compiler reads the same code block again, it will use the native code stored in the cache.
JIT compiler has further divided into the following components,
-
Intermediate Code Generator: It generates intermediate code.
-
Code Optimizer: It optimizes the intermediate code for better performance.
-
Target Code Generator: It converts intermediate code into native machine code.
-
Profiler: This is used to identify Hotspots(i.e, repeatedly executing methods) and replace hotspots with respective native code.
-
Garbage Collector: Garbage Collector(GC) is responsible to collect and remove unreferenced objects from the heap area. This memory management feature is used to reclaim the unused memory and makes free space for newer objects. This happens in two phases:
- Mark: In this step, GC identifies the unused objects in memory
- Sweep: As part of this step, GC removes the objects identified in the previous phase.
-
-
Java Native Interface (JNI) : JNI is used as a bridge between java method calls and native libraries written in C, C++ etc. Sometimes it is required to invoke native code for specific underlined hardeware.
-
Native Method Libraries : These are collection of native libraries(C/C++/Assembly) which are required by execution engine. These libraries loaded through JNI and represented in the form of
.dllor.sofiles.
-
Java has many important features which makes it unique among the popular programming languages. Some of those features are:
-
Simple: Java is a simple programming language and easy to understand without any complexities unlike in the prior programming languages. Some of the reasons for simplicity are: 1. It is based on C++ which makes it easy for new developers to learn it quickly. 2. Java doens't support pointers(unlike C or C++) considering its complexity and security vulnerabilities. Also, operator overloading doesn't exist in Java. 3. There is no need to remove unreferenced objects because it will be taken care by Automatic Garbage Collection.
-
Object-oriented: Java is an Object Oriented programming(OOPS) language, which means everything in Java is represented with different types of classes and objects. This OOPS is a methodology that simplifies software development and maintenance by following the below features.
- Abstraction
- Encapsulation
- Inheritance
- Polymorphism
-
Portable: Java is portable because the bytecode generated on one machine can be taken to any platform for the execution.
-
Platform Independent: The java compiler converts the source code to bytecode and then JVM executes the bytecode to produce the output. If you compile a java program on specific operating system(let's say Windows), it can run on any other platform(Windows, Linux or Mac) with that platform specific JVM. This is the reason why we call Java as a platform-independent language.
-
Robust: Java is robust with many safe guards to ensure reliable code. It has the below safe guards,
- It is capable of handling unexpected termination of a program through exception handling feature.
- It provides strong memory management through Garbage Collector from JVM, which collects all the unused variables and objects to free up the memory space. Whereas in prior programming languages, programmers are solely responsible for allocating and deallocating memory spaces.
-
Secured: Java is extremely safe due to various features listed below,
- There are no explicit pointers. So you cannot access out-of-bound array. If you still try to access an array, it will show an
ArrayIndexOutOfBoundException. So it is impossible to exploit in Java with security flaws like stack corruption or buffer overflow. - The programs runs in an secured environment(JVM) that is independent of operating system(OS).
- Java has bytecode verifier that checks the code blocks for any illegal code that violates the access right to objects.
- There are no explicit pointers. So you cannot access out-of-bound array. If you still try to access an array, it will show an
-
Distributed: Java supports the creation of distributed applications by using Remote Method Invocation(RMI) and Enterprise Java Beans. The java programs are easily distributed on one or more systems through internet connection.
-
Multi-threaded: Java supports multithreading feature, which allows concurrent execution of several parts of a program for maximum utilization of the CPU. This is possible through multiple threads running the program simultaneously.
-
Compiled and Interpreted: It provides both compilation and interpretation of programs. The programs are compiled by compiler first and the generated bytecode is going to be interpreted.
-
High performance: Java bytecode is close to native code, so it runs more quickly than other conventionally interpreted programming languages. Ofcourse, it is not as fast as compiled languages like C or C++. JVM also uses JIT compiler which enables high performance through below features.
-
Method inlining
-
Dead code elimination
-
Null check elimination
-
Constant folding
-
-
The
main()method acts as a starting point for JVM to start the execution of a Java program. i.e, JVM doesn't execute the code if there is no main method in the code. The signature of main method should follow specific pattern for the JVM to recognize it as a entry point. Since it is an important method of Java, you need to have proper understanding of its signature in detail.The signature of main method for displaying "Hello World" looks like below,
public static void main(String[] args) { System.out.println("Hello World"); }
When
java.exeparses the command line, it generates a new String array and invokes the main() method. Let's describe each word in the statement,-
Public: The public access modifier is accessible everywhere and it enables JVM can invoke it from outside the class as it is not present in the current class. Otherwise, you will receive an error saying "Main method not found in class".
-
Static: This static keyword is used to make
main()method as a class related method so that JVM can invoke it without instantiating the class. It is also helpful to avoid unnecessary memory usage with object just for calling main method. -
Void: The void keyword is used to specify that a method doesn’t return anything. Since main() method doesn't return anything, its return type is void. Once the main method terminates, the java program terminates as well.
-
Main: The "main" method name is an identifier that the JVM looks for as the starting point of the Java program.
-
String[] args: This is an array of strings which stores arguments passed by command line while starting a program.
Note: You can create any number of main methods through overloading features. i.e, Multiple main methods can be created with different parameters.
-
-
String constant pool is a special storage space inside the heap memory area where string literals are stored. It is also known as String pool or String Intern Pool. This is privately maintained by String class and it is empty by default. Whenever you create a new string object, JVM checks for presence of string object in the string pool. If the string value is already present, the same object reference is shared with the variable, else a new string is created in the pool with the variable reference stored in stack area.
-
Strings are immutable, that means the contents of string objects can't be modified after their creation. i.e, When you try to alter the string, it creates a new string. Due to this behavior, the internal state of a string remains the same throughout the execution of the program. This characteristic of immutability helps with the benefits of caching, security, synchronization, and performance.
-
String is an immutable class to represent sequence of characters. Java also provided mutable version of String class through StringBuffer and StringBuilder. Even though both these classes are mutable, there are many differences between StringBuffer and StringBuilder.
Some of the major differences in a tabular form:
StringBuffer StringBuilder StringBuffer was introduced in Java 1.0 version StringBuilder was introduced in Java 1.5 version StringBuffer is synchronized(thread safe). That means two or more threads cannot call the methods of StringBuffer simultaneously StringBuilder is non-synchronized(not thread safe). That means two or more threads can call the methods of StringBuilder simultaneously. Due to Synchronization, StringBuffer is slower than StringBuilder StringBuilder is faster than StringBuffer because there won't be any preliminary checks for multiple threads -
Since both
equals()andhashCode()methods are available in Object class(i.e, Java.lang.object), every java class gets the default implementation of equals and hashCode methods. These methods work together to verify if two objects have the same value or not.1. equals: The
equals()method is used to compare equality of two Objects. By default their implementation compares the identities of the object. The Object class definedequals()method as follows,public boolean equals(Object obj) { return (this == obj); }
There are some rules need to be followed to use equals method:
- Reflexive: For any object x,
x.equals(x)should returntrue. - Symmetric: For any two objects x and y,
x.equals(y)should returntrueif and only ify.equals(x)returnstrue. - Transitive: For multiple objects x, y, and z,
if x.equals(y)returnstrueandy.equals(z)returnstrue, thenx.equals(z)should returntrue. - Consistent: For any two objects x and y, multiple invocations of
x.equals(y)should return same result(trueorfalse), unless any of the object properties is modified that is being used in the equals() method implementation.
2. hashCode: The
hashCode()method returns the integer hash code value of the object. This method must be overridden in every class which overridesequals()method. The general contract of hashCode is:- While execution of the application, the multiple invocations of
hashCode()on the object should return the same integer value, unless the object property used in theequals()method is being modified. - During the multiple executions of the application, the object's hashCode can change.
- If two objects are equal based on
equals()method, then their object's hash code must be same. - If two objects are unequal based on
equals()method, their hash code value may or may not be equal.
Together, the implementation of
equals()andhashCode()should follow these rules.- If
x.equals(y)is true, thenx.hashCode() === y.hashCode()should always be true. - If
x.hashCode() === y.hashCode()is true, then it doesn't required to bex.equals(y)always true.
- Reflexive: For any object x,
-
An exception is an event that interrupts the normal flow the program execution. There are two types of exceptions,
- Checked exceptions
- Unchecked exceptions
The hierarchical structure of exceptions is categorized as shown below,
The exceptions which are checked at compile time are known as checked exceptions. If some code inside a method throws this checked exception, then either you need to handle(or caught) the exception using try/catch block or declare in the method signature using thows keyword.
Some of the common checked exceptions are,
- IOException
- FileNotFoundException
- ClassNotFoundException
- InterruptedException
- SQLException
- ParseException
For example, the following class has two methods which throws checked(
FileNotFoundException) exceptions from FileInputStream constructor. This is due to accessing input file which doesn't exist. You can handle them either using try/catch block or declaring thows keyword in the method signature.import java.io.*; class MyCheckedExceptions { private void methodWithTryCatch() { File file = new File("non_existing_file.txt"); try { FileInputStream stream = new FileInputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); } } private void methodWithThrows() throws FileNotFoundException { File file = new File("non_existing_file.txt"); FileInputStream stream = new FileInputStream(file); } }
Whereas the exceptions which are not checked at compile time are known as unchecked exceptions. All the exceptions under Error and RuntimeException comes under unchecked exceptions.
Some of the common unchecked exceptions are,
- NullPointerException
- ArrayIndexOutOfBoundsException
- ArithmeticException
- NumberFormatException
- IllegalThreadStateException
For example, the following method doesn't have any errors during compile time. But it will throw
ArithmeticExceptionduring runtime because of division by zero.class MyUncheckedException { private void divideByZero() { int a = 1; int b = 0; int result = a / b; } }
-
Wrapper classes provides the mechanism to convert primitive types into object types and vice versa. Since Java is an object-oriented programming language, and many APIs and libraries in Java require objects instead primitive types. For example, data structures in collection framework, utility classes from
java.utils.*,cloning process, Serialization, Synchronization etc require object type.The following table represent primitives and their respective wrapper classes.
Primitive data type Wrapper class byte Byte short Short int Integer float Float double Double long Long boolean Boolean char Character You can use
.valueOfmethods from wrapper classes to convert primitive to object type, and the same way wrapper class methods likeintValue,doubleValueetc used to convert wrapper class to primitive type. But this manual process can be automated.- autoboxing
The automatic conversion of primitive to object(corresponding wrapper class) is known as autoboxing. The following example demonstrate how the conversion works,
public class AutoboxingDemo{ public static void main(String args[]){ int num = 10; Integer x = Integer.valueOf(num); // Converting primitive int into Integer explicitly Integer y = num; // Autoboxing without explicit conversion System.out.println("x = "+ x + " y " + y); } }
- unboxing The automatic conversion of wrapper class to primitive is known as unboxing. The following example demonstrate both manual and automatic conversion,
public class UnboxingDemo{ public static void main(String args[]){ Integer num = new Integer(3); int x = num.intValue(); //Converting Integer to int explicitly int y = num; //Unboxing without manual conversion System.out.println("x ="+ x + "y = "+y); } }
Note: These autoboxing and unboxing features are available from JDK1.5 version onwards.
-
Java is not a fully object-oriented programming language because of two main reasons.
-
It supports primitive data types like int, byte, long, short, etc., which are not objects. Even though you can convert primitive data types to objects using wrapper classes, it does not make Java a pure object-oriented language. Because these objects are not originally associated with Java and internally it uses operations like Unboxing and Autoboxing. That means even you use wrapper classes, under the hood it uses primitive types for the calculation.
-
Both static variables and methods can be accessed without using an object. Instead, they are associated with classes, which is against to the object-oriented principle where everything should be an object.
-
-
Both Abstract class and interface are used to define contracts in object-oriented programming. But there are some key differences between them as shown below,
Abstract class Interface The abstractkeyword is used to declare abstract class and it can be extended usingextendskeywordThe interfacekeyword is used to declare interface and it can extend another interface only.The abstract class contains abstract and non-abstract methods Interface can have abstract methods only. But it is possible to have default and static methods from Java8 onwards It supports final, non-final, static and non-static variables It supports only final and static variables. It doens't support multiple inheritance Interface supports multiple inheritance It can have access modifiers such as public, protected, and private for their methods and properties It can only have public access Note: Both Abstract class and interfaces cannot be instantiated.
-
Marker interfaces are interfaces that don't have any fields, methods, or constants inside of it. They are also known as empty interfaces or tag interfaces. Examples of marker interface are Serializable, Cloneable and Remote interface. The purpose of marker interfaces are to provide run-time type information about an object to JVM and Compiler. They are mainly used in API development and in frameworks like Spring to provide additional information to the class.
Some of the commonly used built-in marker interfaces are:
- Cloneable: It is available in
java.langpackage. If we try to clone an object that doesn’t implement this marker interface, the JVM throws aCloneNotSupportedException. i.e, This marker interface is used as a signal/indicator to the JVM that it is legal to call theObject.clone()method. - Serializable: It is available in
java.iopackage. When we try to write an object to output stream usingObjectOutputStream.writeObject()method without implementing this marker interface, JVM throws an exception namedNotSerializableException. - Remote: It is available in
java.rmipackage. A remote object can be accessed from another machine if and only if the class has implemented this marker interface. Otherwise, it is going to throw exception namedRemoteException.
For example, the cloneable marker interface looks like below,
public interface Cloneable { // nothing here }
and the following student class implements Cloneable interface to make a copy of student's object.
import java.lang.*; class Student implements cloneable { String name = null; int age = 0; Student(String name, int age) { this.name = name; this.age = age; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) { Student s1 = new Student("Sudheer", 30); //Create clone of s1 object try { Student s2 = s1.clone(); } catch (Exception e) { System.out.println(s2.toString()); } } }
There are also alternatives for marker interfaces.
- Internal flags: They can be used in place of marker interface to indicate any specific operation.
- Annotations: They are used as tags to represent the metadata attached to classes, interfaces, or methods to indicate additional information required by JVM.
- Cloneable: It is available in
-
Collections in Java is a unified framework that provides architecture for storing and manipulating a group of objects.
The
java.util.*package contains the following classes and interfaces for Collections framework. -
Both Vector and ArrayList use dynamically resizable array as their internal data structure and implement the List interface. But there are couple of differences between them as listed below,
ArrayList Vector This data structure is part of Collection framework, introduced in JDK 1.2 This is a legacy class The capacity increment of ArrayList is 50% if the number of elements exceeds the current size The capacity increment of Vector is 100% if the number of elements exceeds the current size It is not synchronized by default. That means multiple threads can access it concurrently. It is synchronized by default. That means only one thread can access it at a time. It is quite fast because it is not synchronized It is quite slow due to synchronization ArrayList uses the Iterator interface to traverse over the elements. Vector uses both the Iterator and Enumeration interfaces to traverse over the elements. -
The
finalize()is a method from the Object class used to perform cleanup activity before destroying any object. The method is invoked by garbage collector for cleanup activities like closing the resources associated with an object(database connection or network connection). This process is known as finalization and it helps JVM for in-memory optimization.It is a protected method of Object class with syntax as shown below,
protected void finalize() throws Throwable{}
Since Object is the superclass of all java classes, the finalize method is available for every java class. That's why garbage collector can call the
finalize()method on any java object.This method has empty implementation inside Object class. If your class has clean-up activities, you need to override this method. The following example demonstrate how to override the finalize method and call the method explicitly.
import java.lang.*; public class finalizeDemo { protected void finalize() throws Throwable { try { System.out.println("Inside finalize() method of finalizeDemo class"); } catch (Throwable e) { throw e; } finally { System.out.println("Calling finalize method of the Object(super) class"); super.finalize(); } } public static void main(String[] args) throws Throwable { finalizeDemo fd = new finalizeDemo(); fd.finalize(); } }
The statement
super.finalize()is called inside finally block to ensure its execution even in the case of exceptions.Note: The garbage collector invokes the
finalize()method only once on any object. -
Java provides Comparable and Comparator interfaces for sorting the collection of objects, but they serve different purposes and are used in different scenarios.
Comparable: The Comparable interface is capable of comparing an object with another object of same type. The class which needs to compare its instances has to implement
java.lang.Comparableinterface. It can be used to provide single way of sorting. That means that the objects can be sorted based on a single data member. For example, Employee object can be sorted based on single attributes like id, name, age etc.If the class implements Comparable interface, it knows how to sort the objects because the same class itself has implemented compareTo method. This type of sorting is called default or natural sorting. The class can compare its object by overriding compareTo method with syntax as shown below,
class T implements Comparable<T> { @Override public int compareTo(T t) { // comparison logic goes here } }
For example, group of employees sorted based on their name attribute using Comparable interfaces as follows
import java.util.*; class Employee implements Comparable<Employee> { private final String id; private final String name; private final int age; public Employee(String id, String name, int age) { this.id = id; this.name = name; this.age = age; } @Override public int compareTo(final Employee employee) { return this.name.compareTo(employee.name); } public String getId() { return this.id; } public String getName() { return this.name; } public int getAge() { return this.age; } @Override public String toString() { return String.format("ID: %s | Name: %s | Age: %d", id, name, age); } } public class Main { public static void main(String[] args) { List<Employee> employees = new ArrayList<>(); employees.add(new Employee("3", "John", 32)); employees.add(new Employee("1", "James", 27)); employees.add(new Employee("2", "Jackson", 22)); Collections.sort(employees); employees.forEach(employee -> System.out.println(employee.toString())); } }
Comparator:
The comparator is a functional interface used to sort objects and it provides multiple sorting sequence in which objects sorted based on multiple data members. This interface is available as part of
java.utilpackage. For example, Employee object can be sorted based on multiple attributes like id, name, age etc.The comparator is used for custom ordering where the class is not aware about the sorting logic. The comparator can be created using a lambda expression that accept two objects of given type and return an integer value(i.e, 1, 0, -1) based on ordering with syntax as shown below,
Comparator<T> comparator = (T t1, T t2) -> { //comparison logic }
You can create multiple separate classes (which implement Comparator interface) to compare by different members.The Comparator provides the compare() method which can be overridden to customize the comparison logic. For example, group of employees sorted based on id and age data members even though Employee class overridden the compareTo method.
public static void sortById(List<Employee> employees) { Comparator<Employee> idComparator = (Employee e1, Employee e2) -> { return e1.getId().compareTo(e2.getId()); }; employees.sort(idComparator); } public static void sortByAge(List<Employee> employees) { Comparator<Employee> ageComparator = (Employee e1, Employee e2) -> { return e1.getAge() - e2.getAge(); }; employees.sort(ageComparator); }
In the above example, idComparator and ageComparator have been created to perform custom sorting. These comparators needs to be passed as an argument to sort method.
Note: It is preferred to use lambda expression to create Comparator because it is a functional interface(exactly one abstract method).
An inner class is a class that is defined within another class. Inner classes are used to logically group classes that are only used in one place, increase encapsulation, and make code more readable and maintainable. Java supports four types of inner classes:
- Member Inner Class (Non-static nested class): A class defined inside another class without the static modifier. It has access to all members of the outer class, including private members.
class Outer {
private int data = 30;
class Inner {
void display() {
System.out.println("Data: " + data);
}
}
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.display();
}
}- Static Nested Class: A static class defined inside another class. It can only access static members of the outer class.
class Outer {
static int data = 30;
static class StaticNested {
void display() {
System.out.println("Data: " + data);
}
}
public static void main(String[] args) {
Outer.StaticNested nested = new Outer.StaticNested();
nested.display();
}
}- Local Inner Class: A class defined within a method. It can access local variables that are final or effectively final.
class Outer {
void display() {
final int num = 23;
class LocalInner {
void print() {
System.out.println("Number: " + num);
}
}
LocalInner inner = new LocalInner();
inner.print();
}
}- Anonymous Inner Class: A class without a name, used to instantiate objects with certain extras such as overriding methods.
abstract class Animal {
abstract void sound();
}
class Test {
public static void main(String[] args) {
Animal dog = new Animal() {
void sound() {
System.out.println("Bark");
}
};
dog.sound();
}
}The terms final, finally, and finalize() in Java look similar but serve completely different purposes.
-
final → It is a modifier used for variables, methods, and classes.
- A
finalvariable cannot be reassigned once initialized. - A
finalmethod cannot be overridden in subclasses. - A
finalclass cannot be extended.
- A
-
finally → It is a block used in exception handling to execute important code such as closing files, releasing connections, etc.
- The
finallyblock executes regardless of whether an exception occurs or not.
- The
-
finalize() → It is a method defined in the
Objectclass that the Garbage Collector calls before destroying an object.- It is rarely used now because garbage collection timing is unpredictable.
Example:
public class FinalExample {
final int VALUE = 100;
public final void display() {
System.out.println("Final method");
}
public static void main(String[] args) {
try {
System.out.println("Inside try block");
} finally {
System.out.println("Inside finally block");
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("Finalize method called");
}
} ```
21. ### What is the difference between '==' and equals() method?
Both **==** and **.equals()** methods are used to compare objects, but they work in different ways:
1. **== (Reference comparison):** The `==` operator compares the memory addresses (references) of two objects. It returns `true` if both references point to the same object in memory.
2. **.equals() (Content comparison):** The `.equals()` method compares the actual content or values of two objects. By default, it behaves like `==`, but it can be overridden to provide custom comparison logic.
```java
public class ComparisonExample {
public static void main(String[] args) {
String s1 = new String("Hello");
String s2 = new String("Hello");
String s3 = s1;
// == compares references
System.out.println(s1 == s2); // false (different objects)
System.out.println(s1 == s3); // true (same reference)
// equals() compares content
System.out.println(s1.equals(s2)); // true (same content)
System.out.println(s1.equals(s3)); // true (same content)
}
}
```
| == Operator | equals() Method |
|-------------|-----------------|
| Compares object references | Compares object content |
| Cannot be overridden | Can be overridden |
| Works with primitives and objects | Works only with objects |
| Returns true if same memory location | Returns true if content is same |
**[⬆ Back to Top](#table-of-contents)**
22. ### What is method overloading and method overriding?
Method overloading and method overriding are two important concepts in Java that enable polymorphism.
**Method Overloading (Compile-time Polymorphism):**
Method overloading occurs when multiple methods in the same class have the same name but different parameters (different number, type, or order of parameters). The return type can be the same or different.
```java
class Calculator {
// Method with two int parameters
int add(int a, int b) {
return a + b;
}
// Overloaded method with three int parameters
int add(int a, int b, int c) {
return a + b + c;
}
// Overloaded method with double parameters
double add(double a, double b) {
return a + b;
}
}
```
**Method Overriding (Runtime Polymorphism):**
Method overriding occurs when a subclass provides a specific implementation of a method that is already defined in its parent class. The method must have the same name, return type, and parameters.
```java
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Cat meows");
}
}
```
| Method Overloading | Method Overriding |
|--------------------|-------------------|
| Same method name, different parameters | Same method name, same parameters |
| Occurs within the same class | Occurs between parent and child class |
| Compile-time polymorphism | Runtime polymorphism |
| Return type can be different | Return type must be same or covariant |
| Access modifier can be any | Cannot reduce access modifier |
**[⬆ Back to Top](#table-of-contents)**
23. ### What is the difference between HashMap and Hashtable?
Both HashMap and Hashtable are used to store key-value pairs in Java, but they have several important differences:
| HashMap | Hashtable |
|---------|-----------|
| Not synchronized (not thread-safe) | Synchronized (thread-safe) |
| Allows one null key and multiple null values | Does not allow null keys or values |
| Introduced in Java 1.2 | Legacy class from Java 1.0 |
| Faster due to no synchronization | Slower due to synchronization overhead |
| Iterator is fail-fast | Enumerator is not fail-fast |
| Extends AbstractMap class | Extends Dictionary class |
| Preferred for single-threaded applications | Can be used in multi-threaded applications |
```java
import java.util.*;
public class MapComparison {
public static void main(String[] args) {
// HashMap example
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("One", 1);
hashMap.put(null, 0); // Allows null key
hashMap.put("Two", null); // Allows null value
// Hashtable example
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("One", 1);
// hashtable.put(null, 0); // Throws NullPointerException
// hashtable.put("Two", null); // Throws NullPointerException
}
}
```
**Note:** For thread-safe operations, prefer `ConcurrentHashMap` over `Hashtable` as it provides better performance through segment-level locking.
**[⬆ Back to Top](#table-of-contents)**
24. ### What is the difference between ArrayList and LinkedList?
Both ArrayList and LinkedList implement the List interface but differ in their internal implementation and performance characteristics.
| ArrayList | LinkedList |
|-----------|------------|
| Uses dynamic array internally | Uses doubly linked list internally |
| Better for storing and accessing data (O(1) for get) | Better for manipulating data (O(1) for add/remove at ends) |
| Slower manipulation as shifting is required | Faster manipulation, no shifting needed |
| Implements only List interface | Implements List and Deque interfaces |
| More memory efficient | More memory overhead (stores prev/next references) |
| Good for random access | Good for sequential access |
```java
import java.util.*;
public class ListComparison {
public static void main(String[] args) {
// ArrayList - better for random access
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.add("B");
arrayList.get(0); // O(1) - fast random access
// LinkedList - better for frequent insertions/deletions
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("A");
linkedList.addFirst("B"); // O(1) - fast insertion at beginning
linkedList.removeLast(); // O(1) - fast removal at end
}
}
```
**When to use:**
- Use **ArrayList** when you need frequent access to elements by index
- Use **LinkedList** when you need frequent insertions/deletions, especially at the beginning or middle of the list
**[⬆ Back to Top](#table-of-contents)**
25. ### What is Java Reflection API?
Java Reflection API is a powerful feature that allows programs to examine and modify the behavior of classes, interfaces, methods, and fields at runtime. It is part of the `java.lang.reflect` package.
**Key capabilities of Reflection:**
1. Examine class properties at runtime
2. Create objects dynamically
3. Invoke methods at runtime
4. Access and modify private fields
5. Get information about constructors, methods, and fields
```java
import java.lang.reflect.*;
class Person {
private String name;
public int age;
public Person() {}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void privateMethod() {
System.out.println("Private method called");
}
public void display() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// Get Class object
Class<?> clazz = Person.class;
// Get class name
System.out.println("Class: " + clazz.getName());
// Get all declared methods
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("Method: " + method.getName());
}
// Create instance dynamically
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Person person = (Person) constructor.newInstance("John", 25);
// Access private field
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(person, "Jane");
// Invoke method
Method displayMethod = clazz.getMethod("display");
displayMethod.invoke(person);
}
}
```
**Use cases:** Frameworks like Spring, Hibernate, JUnit use Reflection extensively for dependency injection, ORM mapping, and test execution.
**[⬆ Back to Top](#table-of-contents)**
26. ### What are the different types of memory areas allocated by JVM?
JVM allocates memory in different areas for efficient program execution. The main memory areas are:
1. **Method Area (Metaspace):**
- Stores class-level data like class structures, method data, and runtime constant pool
- Shared among all threads
- Created during JVM startup
2. **Heap Area:**
- Stores all objects and their instance variables
- Shared among all threads
- Managed by Garbage Collector
- Divided into Young Generation (Eden, Survivor spaces) and Old Generation
3. **Stack Area:**
- Each thread has its own stack
- Stores local variables, method calls, and partial results
- Contains stack frames for each method invocation
- Memory is automatically allocated/deallocated
4. **PC (Program Counter) Registers:**
- Each thread has its own PC register
- Stores address of currently executing JVM instruction
- Updated after each instruction execution
5. **Native Method Stack:**
- Contains native method information
- Each thread has its own native method stack
- Used for methods written in languages other than Java (C, C++)
```
JVM Memory Structure:
┌─────────────────────────────────────────────┐
│ Method Area (Metaspace) │
│ (Class data, Method data, Constant pool) │
├─────────────────────────────────────────────┤
│ Heap Area │
│ ┌─────────────────┬─────────────────────┐ │
│ │ Young Generation│ Old Generation │ │
│ │ (Eden,Survivor) │ │ │
│ └─────────────────┴─────────────────────┘ │
├─────────────────────────────────────────────┤
│ Stack │ Stack │ Stack │ ... (per thread)│
├─────────────────────────────────────────────┤
│ PC Reg│ PC Reg │ PC Reg │ ... (per thread)│
├─────────────────────────────────────────────┤
│ Native│ Native │ Native │ ... (per thread)│
└─────────────────────────────────────────────┘
```
**[⬆ Back to Top](#table-of-contents)**
27. ### What is the difference between throw and throws?
Both `throw` and `throws` are used in exception handling but serve different purposes.
**throw:**
- Used to explicitly throw an exception
- Used inside a method body
- Can throw only one exception at a time
- Followed by an exception instance
**throws:**
- Used to declare exceptions that a method might throw
- Used in method signature
- Can declare multiple exceptions
- Followed by exception class names
```java
import java.io.*;
public class ThrowVsThrows {
// Using throws - declares that method may throw an exception
public void readFile(String filename) throws FileNotFoundException, IOException {
FileReader file = new FileReader(filename);
BufferedReader reader = new BufferedReader(file);
String line = reader.readLine();
reader.close();
}
// Using throw - explicitly throws an exception
public void validateAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
if (age < 18) {
throw new ArithmeticException("Not eligible to vote");
}
System.out.println("Eligible to vote");
}
// Combining throw and throws
public void processData(String data) throws CustomException {
if (data == null) {
throw new CustomException("Data cannot be null");
}
// Process data
}
}
class CustomException extends Exception {
public CustomException(String message) {
super(message);
}
}
```
| throw | throws |
|-------|--------|
| Used to throw an exception | Used to declare an exception |
| Inside method body | In method signature |
| Cannot throw multiple exceptions | Can declare multiple exceptions |
| Followed by instance | Followed by class name |
| Checked exceptions must be caught | Propagates exception to caller |
**[⬆ Back to Top](#table-of-contents)**
28. ### What is a singleton class and how to create one?
A Singleton class is a design pattern that ensures only one instance of a class is created throughout the application lifecycle. It provides a global point of access to that instance.
**Key characteristics:**
- Private constructor to prevent direct instantiation
- Static method to get the single instance
- Static variable to hold the single instance
**Different ways to create Singleton:**
1. **Eager Initialization:**
```java
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
```
2. **Lazy Initialization:**
```java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
```
3. **Thread-Safe Singleton (Double-Checked Locking):**
```java
public class ThreadSafeSingleton {
private static volatile ThreadSafeSingleton instance;
private ThreadSafeSingleton() {}
public static ThreadSafeSingleton getInstance() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
}
```
4. **Bill Pugh Singleton (Best Practice):**
```java
public class BillPughSingleton {
private BillPughSingleton() {}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
```
**[⬆ Back to Top](#table-of-contents)**
29. ### What are Java 8 Stream API features?
Java 8 Stream API provides a functional approach to process collections of objects. Streams support sequential and parallel operations on data.
**Key Features:**
1. **Lazy Evaluation:** Intermediate operations are not executed until a terminal operation is invoked
2. **Pipelining:** Operations can be chained together
3. **Internal Iteration:** Stream handles iteration internally
4. **Parallel Processing:** Easy parallel execution with `parallelStream()`
**Common Stream Operations:**
```java
import java.util.*;
import java.util.stream.*;
public class StreamDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter - filters elements based on condition
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// Result: [2, 4, 6, 8, 10]
// map - transforms elements
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
// Result: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// reduce - combines elements
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
// Result: 55
// sorted - sorts elements
List<Integer> sorted = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
// distinct - removes duplicates
List<Integer> unique = Arrays.asList(1, 1, 2, 2, 3).stream()
.distinct()
.collect(Collectors.toList());
// Result: [1, 2, 3]
// limit and skip
List<Integer> limited = numbers.stream()
.skip(2)
.limit(5)
.collect(Collectors.toList());
// Result: [3, 4, 5, 6, 7]
// anyMatch, allMatch, noneMatch
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);
boolean allPositive = numbers.stream().allMatch(n -> n > 0);
// findFirst, findAny
Optional<Integer> first = numbers.stream().findFirst();
// count
long count = numbers.stream().filter(n -> n > 5).count();
// forEach
numbers.stream().forEach(System.out::println);
}
}
```
**[⬆ Back to Top](#table-of-contents)**
30. ### What is the difference between fail-fast and fail-safe iterators?
Iterators in Java can be categorized as fail-fast or fail-safe based on their behavior when the underlying collection is modified during iteration.
**Fail-Fast Iterator:**
- Throws `ConcurrentModificationException` if the collection is modified during iteration
- Works on the original collection
- Examples: ArrayList, HashMap, HashSet iterators
- Uses `modCount` to detect modifications
```java
import java.util.*;
public class FailFastExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("B")) {
list.remove(element); // Throws ConcurrentModificationException
}
}
}
}
```
**Fail-Safe Iterator:**
- Does not throw exception if collection is modified during iteration
- Works on a clone/copy of the collection
- Examples: CopyOnWriteArrayList, ConcurrentHashMap iterators
- More memory overhead due to copying
```java
import java.util.concurrent.*;
import java.util.*;
public class FailSafeExample {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("B")) {
list.remove(element); // No exception thrown
}
}
System.out.println(list); // [A, C]
}
}
```
| Fail-Fast | Fail-Safe |
|-----------|-----------|
| Throws ConcurrentModificationException | No exception thrown |
| Works on original collection | Works on copy of collection |
| Less memory overhead | More memory overhead |
| ArrayList, HashMap, HashSet | CopyOnWriteArrayList, ConcurrentHashMap |
**[⬆ Back to Top](#table-of-contents)**
31. ### What is the difference between process and thread?
A process and a thread are both units of execution, but they differ significantly in their characteristics and usage.
**Process:**
- Independent executing program with its own memory space
- Contains at least one thread (main thread)
- Has its own address space, heap, stack, and system resources
- Processes are isolated from each other
- Inter-process communication (IPC) is expensive
**Thread:**
- Lightweight unit of execution within a process
- Shares memory space with other threads in the same process
- Has its own stack but shares heap with other threads
- Threads within a process can communicate directly
- Context switching between threads is faster
```java
public class ProcessVsThread {
public static void main(String[] args) {
// Creating a new process
try {
ProcessBuilder pb = new ProcessBuilder("notepad.exe");
Process process = pb.start(); // Creates a new process
} catch (Exception e) {
e.printStackTrace();
}
// Creating a new thread
Thread thread = new Thread(() -> {
System.out.println("Thread running: " + Thread.currentThread().getName());
});
thread.start(); // Creates a new thread within the same process
}
}
```
| Process | Thread |
|---------|--------|
| Heavy weight | Light weight |
| Own memory space | Shared memory space |
| Slower context switching | Faster context switching |
| Isolated from other processes | Can communicate with other threads |
| More resource intensive | Less resource intensive |
| Process crash doesn't affect others | Thread crash can affect entire process |
**[⬆ Back to Top](#table-of-contents)**
32. ### What are the different ways to create a thread in Java?
There are multiple ways to create a thread in Java:
**1. Extending Thread class:**
```java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
// Usage
MyThread thread = new MyThread();
thread.start();
```
**2. Implementing Runnable interface:**
```java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running: " + Thread.currentThread().getName());
}
}
// Usage
Thread thread = new Thread(new MyRunnable());
thread.start();
```
**3. Using Lambda expression (Java 8+):**
```java
Thread thread = new Thread(() -> {
System.out.println("Lambda thread running");
});
thread.start();
```
**4. Implementing Callable interface (returns result):**
```java
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 42;
}
}
// Usage
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
Integer result = future.get(); // Gets the result
executor.shutdown();
```
**5. Using ExecutorService:**
```java
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.execute(() -> {
System.out.println("Task executed by: " + Thread.currentThread().getName());
});
executor.submit(() -> {
return "Task completed";
});
executor.shutdown();
```
| Method | Returns Value | Can Extend Other Class | Exception Handling |
|--------|--------------|------------------------|-------------------|
| Thread class | No | No | Unchecked only |
| Runnable | No | Yes | Unchecked only |
| Callable | Yes | Yes | Checked and Unchecked |
**[⬆ Back to Top](#table-of-contents)**
33. ### What is synchronization in Java?
Synchronization is a mechanism that ensures only one thread can access a shared resource at a time. It prevents race conditions and ensures thread safety.
**Types of Synchronization:**
**1. Synchronized Method:**
```java
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
```
**2. Synchronized Block:**
```java
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
}
```
**3. Static Synchronization:**
```java
class StaticCounter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}
```
**Example demonstrating the need for synchronization:**
```java
class BankAccount {
private int balance = 1000;
// Without synchronization - race condition possible
public void withdraw(int amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " is withdrawing");
balance -= amount;
System.out.println("Balance after withdrawal: " + balance);
}
}
// With synchronization - thread safe
public synchronized void safeWithdraw(int amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " is withdrawing");
balance -= amount;
System.out.println("Balance after withdrawal: " + balance);
}
}
}
```
**Key Points:**
- Every object in Java has an intrinsic lock (monitor)
- Only one thread can hold the lock at a time
- Synchronization can cause performance overhead
- Prefer synchronized blocks over synchronized methods for fine-grained control
**[⬆ Back to Top](#table-of-contents)**
34. ### What is deadlock and how to avoid it?
A deadlock is a situation where two or more threads are blocked forever, each waiting for the other to release a lock.
**Deadlock Example:**
```java
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread 1: Holding lock1 and lock2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread 2: Holding lock2 and lock1");
}
}
});
thread1.start();
thread2.start();
}
}
```
**How to Avoid Deadlock:**
**1. Lock Ordering - Always acquire locks in the same order:**
```java
// Fixed version - both threads acquire locks in same order
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
synchronized (lock2) {
// work
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock1) { // Same order as thread1
synchronized (lock2) {
// work
}
}
});
```
**2. Use tryLock with timeout:**
```java
import java.util.concurrent.locks.*;
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
public void safeMethod() {
boolean gotLock1 = false;
boolean gotLock2 = false;
try {
gotLock1 = lock1.tryLock(1, TimeUnit.SECONDS);
gotLock2 = lock2.tryLock(1, TimeUnit.SECONDS);
if (gotLock1 && gotLock2) {
// Do work
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (gotLock1) lock1.unlock();
if (gotLock2) lock2.unlock();
}
}
```
**3. Avoid nested locks**
**4. Use lock timeout**
**5. Avoid holding locks for long periods**
**[⬆ Back to Top](#table-of-contents)**
35. ### What is the volatile keyword in Java?
The `volatile` keyword in Java is used to indicate that a variable's value may be modified by different threads. It ensures visibility of changes across threads and prevents caching of the variable.
**Key Features:**
1. **Visibility:** Changes made by one thread are immediately visible to other threads
2. **Atomicity:** Read and write operations on volatile variables are atomic (for primitives)
3. **No Caching:** The variable is always read from main memory, not from CPU cache
```java
public class VolatileExample {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// Do work
}
System.out.println("Stopped");
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
Thread worker = new Thread(() -> example.run());
worker.start();
Thread.sleep(1000);
example.stop(); // This change will be visible to the worker thread
}
}
```
**Without volatile:**
```java
// Without volatile, the worker thread might never see the change
// because it may read from its local cache
private boolean running = true; // Thread may cache this value
```
**Volatile vs Synchronized:**
| volatile | synchronized |
|----------|--------------|
| Only ensures visibility | Ensures visibility and atomicity |
| No blocking | Causes blocking |
| Cannot be used for compound operations | Can protect compound operations |
| Less overhead | More overhead |
**Note:** Volatile is not suitable for compound operations like `count++`. Use `AtomicInteger` or `synchronized` for such cases.
**[⬆ Back to Top](#table-of-contents)**
36. ### What is the transient keyword in Java?
The `transient` keyword in Java is used to indicate that a field should not be serialized when the object is converted to a byte stream.
**Use Cases:**
- Sensitive data like passwords
- Calculated/derived fields
- Non-serializable fields
- Thread-local data
```java
import java.io.*;
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // Will not be serialized
private transient int loginCount; // Will not be serialized
public User(String username, String password) {
this.username = username;
this.password = password;
this.loginCount = 0;
}
@Override
public String toString() {
return "User{username='" + username + "', password='" + password +
"', loginCount=" + loginCount + "}";
}
}
public class TransientDemo {
public static void main(String[] args) {
User user = new User("john", "secret123");
user.loginCount = 5;
System.out.println("Before serialization: " + user);
// Serialize
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.ser"))) {
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
// Deserialize
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.ser"))) {
User deserializedUser = (User) ois.readObject();
System.out.println("After deserialization: " + deserializedUser);
// Output: User{username='john', password='null', loginCount=0}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
```
**Key Points:**
- Transient fields get default values after deserialization (null for objects, 0 for numbers, false for boolean)
- Static fields are not serialized regardless of transient keyword
- Can be combined with custom readObject/writeObject for complex scenarios
**[⬆ Back to Top](#table-of-contents)**
37. ### What is serialization and deserialization?
**Serialization** is the process of converting an object's state into a byte stream. **Deserialization** is the reverse process - converting a byte stream back into an object.
**Why Serialization?**
- Persist object state to file/database
- Send objects over network
- Deep copy objects
- Cache objects
```java
import java.io.*;
class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int id;
private transient double salary; // Not serialized
public Employee(String name, int id, double salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
@Override
public String toString() {
return "Employee{name='" + name + "', id=" + id + ", salary=" + salary + "}";
}
}
public class SerializationDemo {
public static void main(String[] args) {
Employee emp = new Employee("John", 101, 50000);
// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("employee.ser"))) {
oos.writeObject(emp);
System.out.println("Object serialized successfully");
} catch (IOException e) {
e.printStackTrace();
}
// Deserialization
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("employee.ser"))) {
Employee deserializedEmp = (Employee) ois.readObject();
System.out.println("Deserialized: " + deserializedEmp);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
```
**Custom Serialization:**
```java
class CustomSerializable implements Serializable {
private String data;
private transient String sensitiveData;
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
// Encrypt and write sensitive data
oos.writeObject(encrypt(sensitiveData));
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// Decrypt sensitive data
sensitiveData = decrypt((String) ois.readObject());
}
private String encrypt(String data) { return data; } // Placeholder
private String decrypt(String data) { return data; } // Placeholder
}
```
**Important:** Always declare `serialVersionUID` to maintain compatibility during deserialization.
**[⬆ Back to Top](#table-of-contents)**
38. ### What are functional interfaces in Java?
A functional interface is an interface that contains exactly one abstract method. They are the foundation for lambda expressions in Java 8.
**Key Characteristics:**
- Has exactly one abstract method
- Can have multiple default and static methods
- Can be annotated with `@FunctionalInterface`
- Used as target types for lambda expressions
```java
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
// Default method - allowed
default void print(String msg) {
System.out.println(msg);
}
// Static method - allowed
static void info() {
System.out.println("Calculator interface");
}
}
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
// Using lambda expression
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println("Sum: " + add.calculate(5, 3)); // 8
System.out.println("Product: " + multiply.calculate(5, 3)); // 15
}
}
```
**Built-in Functional Interfaces (java.util.function):**
```java
import java.util.function.*;
public class BuiltInFunctionalInterfaces {
public static void main(String[] args) {
// Predicate - takes input, returns boolean
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
// Function - takes input, returns output
Function<String, Integer> length = s -> s.length();
System.out.println(length.apply("Hello")); // 5
// Consumer - takes input, returns nothing
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello"); // prints Hello
// Supplier - takes nothing, returns output
Supplier<Double> random = () -> Math.random();
System.out.println(random.get()); // random number
// BiFunction - takes two inputs, returns output
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(add.apply(5, 3)); // 8
// UnaryOperator - takes input, returns same type
UnaryOperator<Integer> square = n -> n * n;
System.out.println(square.apply(5)); // 25
// BinaryOperator - takes two inputs of same type, returns same type
BinaryOperator<Integer> max = (a, b) -> a > b ? a : b;
System.out.println(max.apply(5, 3)); // 5
}
}
```
**[⬆ Back to Top](#table-of-contents)**
39. ### What are lambda expressions in Java?
Lambda expressions are anonymous functions that provide a concise way to implement functional interfaces. Introduced in Java 8, they enable functional programming in Java.
**Syntax:**
```
(parameters) -> expression
(parameters) -> { statements; }
```
**Examples:**
```java
import java.util.*;
import java.util.function.*;
public class LambdaExamples {
public static void main(String[] args) {
// No parameters
Runnable runnable = () -> System.out.println("Hello Lambda!");
runnable.run();
// One parameter (parentheses optional)
Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("Hello");
// Multiple parameters
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(add.apply(5, 3));
// With type declarations
BiFunction<Integer, Integer, Integer> multiply =
(Integer a, Integer b) -> a * b;
// Multiple statements (braces required)
BiFunction<Integer, Integer, Integer> calculate = (a, b) -> {
int sum = a + b;
int product = a * b;
return sum + product;
};
// With Collections
List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice");
// forEach with lambda
names.forEach(name -> System.out.println(name));
// Method reference (shorthand for lambda)
names.forEach(System.out::println);
// Sorting with lambda
names.sort((s1, s2) -> s1.compareTo(s2));
// Using Comparator method reference
names.sort(String::compareTo);
// Filtering with streams
List<String> filtered = names.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.toList());
// Mapping with streams
List<Integer> lengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
}
}
```
**Method References:**
```java
// Static method reference
Function<String, Integer> parseInt = Integer::parseInt;
// Instance method reference
String str = "Hello";
Supplier<Integer> length = str::length;
// Constructor reference
Supplier<ArrayList<String>> listSupplier = ArrayList::new;
```
**Benefits:**
- Concise and readable code
- Enables functional programming
- Better support for parallel processing
- Reduced boilerplate code
**[⬆ Back to Top](#table-of-contents)**
40. ### What is Optional class in Java 8?
The `Optional` class is a container object that may or may not contain a non-null value. It helps avoid `NullPointerException` and makes null handling more explicit.
**Creating Optional:**
```java
import java.util.Optional;
public class OptionalDemo {
public static void main(String[] args) {
// Creating Optional
Optional<String> empty = Optional.empty();
Optional<String> name = Optional.of("John");
Optional<String> nullable = Optional.ofNullable(null);
// Checking if value present
System.out.println(name.isPresent()); // true
System.out.println(empty.isPresent()); // false
System.out.println(empty.isEmpty()); // true (Java 11+)
// Getting value
String value = name.get(); // Returns "John"
// String error = empty.get(); // Throws NoSuchElementException
// Safe access with orElse
String defaultName = empty.orElse("Unknown"); // "Unknown"
// orElseGet - lazy evaluation
String lazyDefault = empty.orElseGet(() -> computeDefault());
// orElseThrow
String required = name.orElseThrow(() ->
new IllegalArgumentException("Name is required"));
// ifPresent - execute if value exists
name.ifPresent(n -> System.out.println("Name: " + n));
// ifPresentOrElse (Java 9+)
name.ifPresentOrElse(
n -> System.out.println("Found: " + n),
() -> System.out.println("Not found")
);
}
static String computeDefault() {
return "Computed Default";
}
}
```
**Optional with Streams:**
```java
public class OptionalStreams {
public static void main(String[] args) {
Optional<String> name = Optional.of(" John ");
// map - transform value
Optional<Integer> length = name.map(String::length);
// Chaining operations
String result = name
.map(String::trim)
.map(String::toUpperCase)
.orElse("UNKNOWN");
System.out.println(result); // "JOHN"
// filter - conditional
Optional<String> filtered = name
.filter(n -> n.trim().length() > 3);
// flatMap - for nested Optionals
Optional<Optional<String>> nested = Optional.of(Optional.of("Hello"));
Optional<String> flat = nested.flatMap(o -> o);
}
}
```
**Best Practices:**
```java
class UserService {
// Return Optional for methods that may not find a result
public Optional<User> findById(int id) {
User user = database.find(id);
return Optional.ofNullable(user);
}
// Don't use Optional for fields or parameters
// Bad: private Optional<String> name;
// Good: private String name; // can be null
// Don't use Optional.get() without checking
// Bad: optional.get()
// Good: optional.orElse(default) or optional.ifPresent(...)
}
```
**[⬆ Back to Top](#table-of-contents)**
<!-- QUESTIONS_END -->


