■ JAVA
Interview Preparation Guide
Complete Reference · Beginner to Advanced
JDK/JRE/JVM OOPs Collections Strings
Exception Handling Multithreading Java 8 Advanced Java
Theory + Code Examples | Every Topic Covered
Java Interview Guide · Page 1
Table of Contents
JDK, JRE & JVM
01 Architecture & Platform Independence
Data Types & Variables
02 Primitives, Casting, Variable Types
OOP Concepts
03 4 Pillars, Abstract Class, Interface, Diamond Problem
Classes & Objects
04 this keyword, Object vs Reference
Constructors
05 Types, Overloading, this(), super()
Strings
06 Immutability, String Pool, StringBuilder
Collections Framework
07 List, Set, Map, HashMap Internals
Exception Handling
08 Checked/Unchecked, Custom Exceptions
Multithreading
09 Thread, Sync, Race Condition, Deadlock
Java 8 Features
10 Lambda, Streams, Functional Interfaces
Advanced Java
11 Generics, Reflection, Serialization, Design Patterns
Tricky / Coding Questions
12 final vs finally, Heap vs Stack, GC, Cloning
Java Interview Guide · Page 2
■ SECTION 01 — JDK, JRE & JVM
Q1. What is JDK?
JDK (Java Development Kit) is a full-featured software development kit for Java. It includes the JRE plus
development tools like the javac compiler, javadoc, jdb debugger, jar utility, and more. When you install the JDK,
you get everything needed to write, compile, and run Java programs. The JDK is platform-specific — different
versions exist for Windows, macOS, and Linux. It is used by developers to develop Java applications. The JDK
contains the JRE inside it, so installing JDK is sufficient to both develop and run Java programs.
// Compile with JDK compiler // javac [Link] --> produces [Link] // java
HelloWorld --> runs the .class file
Q2. What is JRE?
JRE (Java Runtime Environment) is the environment required to run already-compiled Java programs (.class files).
It contains the JVM, core libraries, and supporting files but does NOT include development tools like the compiler.
End users who only need to run Java applications install the JRE. It provides the runtime libraries that Java
programs depend on at execution time. JRE acts as a translator between Java bytecode and the host operating
system, providing the runtime environment necessary for execution.
■ JRE = JVM + Libraries. JDK = JRE + Dev Tools.
Q3. What is JVM?
JVM (Java Virtual Machine) is the core engine of Java. It is an abstract computing machine that reads and
executes Java bytecode (.class files). JVM converts bytecode into machine-specific instructions using the
Just-In-Time (JIT) compiler, enabling platform independence. JVM manages memory through the Garbage
Collector, handles class loading via the ClassLoader, and provides runtime security. JVM is platform-dependent
(there is a different JVM for each OS), but bytecode is platform-independent, which is why Java is 'write once, run
anywhere'.
■ JVM is platform-dependent; bytecode is platform-independent.
Q4. Why is Java platform-independent?
Java achieves platform independence through bytecode. When you compile Java source code, the Java compiler
(javac) converts it into bytecode, not native machine code. Bytecode is an intermediate, platform-neutral
representation stored in .class files. Any machine with a JVM installed can execute this bytecode regardless of the
underlying OS or hardware. The JVM on each platform understands how to convert bytecode into native machine
instructions. This is the foundation of Java's 'Write Once, Run Anywhere' (WORA) philosophy.
Source Code (.java) |-- javac compiler --> Bytecode (.class) [Platform-Independent] |-- JVM
(platform-specific) --> Machine Code (runs on that OS/hardware)
Q5. What is Bytecode?
Bytecode is the compiled form of Java source code. It is an intermediate code that is neither source code nor
machine code. The Java compiler (javac) translates .java files into .class files containing bytecode. Bytecode
consists of compact numeric instructions that the JVM understands. It is designed to be platform-independent —
the same .class file can be run on any machine with a compatible JVM. Bytecode is more efficient than interpreting
source code directly and enables JIT compilation for performance.
■ Bytecode enables portability; JIT compilation gives performance.
Java Interview Guide · Page 3
Q6. What is JVM Architecture?
JVM architecture has three main components: (1) Class Loader Subsystem — loads, links, and initializes .class
files. (2) Runtime Data Areas — Method Area (class metadata), Heap (objects), Stack (method calls/local vars),
PC Register (current instruction pointer), Native Method Stack. (3) Execution Engine — Interpreter (executes
bytecode line by line), JIT Compiler (compiles hot methods to native code for speed), and Garbage Collector
(reclaims unused heap memory). The JVM also uses a Native Method Interface (JNI) to interact with native OS
libraries.
■ Remember: ClassLoader → Runtime Areas → Execution Engine.
Q7. What is the role of the compiler?
The Java compiler (javac) translates human-readable Java source code (.java files) into platform-independent
bytecode (.class files). The compiler performs syntax checking, type checking, and semantic analysis. It detects
compile-time errors like syntax mistakes, type mismatches, and undeclared variables. The compiler does NOT
execute code — it only translates it. The output (.class file) is then handed to the JVM for execution. The compiler
also generates symbol tables used during runtime by reflection and debugging tools.
// Compile: javac [Link] --> [Link] // Run: java MyClass (JVM loads and executes
[Link])
Java Interview Guide · Page 4
■ SECTION 02 — Data Types & Variables
Q1. What are primitive data types?
Java has 8 primitive data types: byte (8-bit, -128 to 127), short (16-bit), int (32-bit, most common integer), long
(64-bit, suffix L), float (32-bit decimal, suffix f), double (64-bit decimal, default for decimals), char (16-bit Unicode
character), boolean (true/false). Primitives store values directly in stack memory and are not objects. They have no
methods. Each has a corresponding Wrapper class (int → Integer, char → Character, etc.) that allows them to be
used as objects.
int age = 25; double price = 99.99; boolean isValid = true; char grade = 'A'; long bigNum =
9876543210L; byte b = 100;
■ 8 primitives: byte, short, int, long, float, double, char, boolean.
Q2. Difference between primitive and non-primitive data types?
Primitive types (int, char, boolean, etc.) hold actual values directly in memory (stack). They are predefined by Java
and not objects — they have no methods. Non-primitive types (String, arrays, classes, interfaces) hold references
(memory addresses) pointing to objects stored in the heap. Non-primitives can be null; primitives cannot.
Non-primitive types have methods and are more flexible but use more memory. Wrapper classes like Integer,
Double, and Boolean are the non-primitive counterparts of primitives, enabling use in Collections.
int x = 10; // primitive - value stored directly String s = "Hello"; // non-primitive -
reference to heap object Integer n = 42; // wrapper class (non-primitive) int[] arr = {1,2,3};
// array - non-primitive
Q3. What is type casting?
Type casting is the process of converting a variable from one data type to another. Java supports two forms:
widening (implicit) and narrowing (explicit). Casting is needed when you want to store a value of one type into a
variable of another type. In widening, no data is lost and Java handles it automatically. In narrowing, data may be
lost (e.g., truncation of decimal part) and requires an explicit cast by the programmer. Type casting also applies to
object references (upcasting and downcasting in OOP).
double d = 9.99; int i = (int) d; // Narrowing: i = 9 (decimal truncated) int x = 100; double y
= x; // Widening: y = 100.0 (automatic)
Q4. Difference between implicit and explicit casting?
Implicit casting (widening conversion) happens automatically when converting a smaller data type to a larger one.
No data loss occurs. Order: byte → short → int → long → float → double. The compiler handles it without any
instruction from the programmer. Explicit casting (narrowing conversion) must be done manually by the
programmer using a cast operator (type). It happens when converting a larger type to a smaller type and may
result in data loss or precision reduction. ClassCastException can occur at runtime if object casting fails.
// Implicit (Widening) int a = 50; double d = a; // automatic, d = 50.0 // Explicit (Narrowing)
double pi = 3.14159; int approx = (int) pi; // must cast, approx = 3
■ Widening is safe; narrowing may lose data.
Q5. Types of variables in Java?
Java has three types of variables: (1) Local Variables — declared inside a method or block, exist only during that
method's execution, stored on the stack, no default value (must be initialized before use). (2) Instance Variables —
declared inside a class but outside methods, each object gets its own copy, stored on the heap, have default
values (0, null, false). (3) Static Variables (Class Variables) — declared with the static keyword, shared across all
instances of the class, loaded once into the Method Area when the class is loaded.
Java Interview Guide · Page 5
class Example { static int count = 0; // static variable String name; // instance variable void
show() { int x = 10; // local variable [Link](x); } }
Java Interview Guide · Page 6
■ SECTION 03 — OOP Concepts
Q1. What is OOP?
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around data
(objects) rather than functions and logic. An object is an instance of a class that bundles data (fields) and
behaviour (methods) together. OOP promotes code reuse through inheritance, modularity through encapsulation,
flexibility through polymorphism, and simplicity through abstraction. Java is an OOP language (almost — it has
primitives and static methods too). OOP makes programs easier to design, understand, maintain, and extend.
■ OOP = Organizing code around objects that have state + behaviour.
Q2. What are the four pillars of OOP?
The four pillars are: (1) Abstraction — hiding complex implementation details and exposing only essential features
(using abstract classes and interfaces). (2) Encapsulation — wrapping data and methods into a class and
restricting direct access via access modifiers (private fields + public getters/setters). (3) Inheritance — allowing a
child class to acquire properties and behaviors of a parent class using 'extends', promoting code reuse. (4)
Polymorphism — the ability of an object to take many forms; method overloading (compile-time) and overriding
(runtime).
■ Remember: A-E-I-P → Abstraction, Encapsulation, Inheritance, Polymorphism.
Q3. What is Abstraction?
Abstraction means hiding the internal implementation and showing only the necessary details to the user. It
reduces complexity by focusing on what an object does rather than how it does it. In Java, abstraction is achieved
through abstract classes (partially abstract) and interfaces (fully abstract until Java 8). For example, when you
drive a car, you use the steering wheel and pedals without knowing the engine's internal mechanics — that is
abstraction. Abstract methods must be implemented by subclasses.
abstract class Shape { abstract double area(); // no implementation here } class Circle extends
Shape { double r; Circle(double r) { this.r = r; } double area() { return [Link] * r * r; } //
implementation }
Q4. What is Encapsulation?
Encapsulation is the process of binding data (variables) and methods that operate on that data into a single unit
(class), and restricting direct access to some of the object's components. It is achieved by declaring fields as
private and providing public getter and setter methods to control access. Encapsulation protects data from
unauthorized modification, improves maintainability, and achieves data hiding. It also allows adding validation logic
in setters. Classes like [Link] are good examples of encapsulation — internal array details are hidden.
class BankAccount { private double balance; public double getBalance() { return balance; }
public void deposit(double amount) { if (amount > 0) balance += amount; } }
Q5. What is Inheritance?
Inheritance is a mechanism where a child class (subclass) acquires the properties and behaviors of a parent class
(superclass) using the 'extends' keyword. It promotes code reuse — common code is written once in the parent
and reused in children. Java supports single inheritance (a class can extend only one class) and multilevel
inheritance (A extends B extends C). The child class can use inherited methods as-is, override them, or add new
ones. The 'super' keyword accesses the parent class's members.
class Animal { void eat() { [Link]("Eating..."); } } class Dog extends Animal {
void bark() { [Link]("Barking!"); } } // Dog inherits eat() from Animal Dog d = new
Dog(); [Link](); // inherited [Link](); // own method
Java Interview Guide · Page 7
Q6. What is Polymorphism?
Polymorphism means 'many forms' — one action can behave differently based on context. Java supports two
types: (1) Compile-time polymorphism (Method Overloading) — same method name, different parameters,
resolved at compile time. (2) Runtime polymorphism (Method Overriding) — subclass provides a specific
implementation of a method defined in the superclass, resolved at runtime via dynamic method dispatch.
Polymorphism enables writing flexible, extensible code where one interface can work with many implementations.
// Overloading (Compile-time) int add(int a, int b) { return a + b; } double add(double a,
double b) { return a + b; } // Overriding (Runtime) class Cat extends Animal { void sound() {
[Link]("Meow"); } } Animal a = new Cat(); [Link](); // prints "Meow" -- runtime
decision
Q7. Difference between abstraction and encapsulation?
Abstraction focuses on 'what' an object does — it hides implementation complexity and exposes a clean interface.
Encapsulation focuses on 'how' data is protected — it hides data from direct access by binding it with methods.
Abstraction is a design concept (achieved via abstract classes/interfaces); encapsulation is an implementation
concept (achieved via access modifiers). Abstraction is about hiding complexity at the design level; encapsulation
is about hiding data at the implementation level. Both work together but serve different purposes.
■ Abstraction = hide complexity | Encapsulation = hide data.
Q8. Difference between abstract class and interface?
Abstract class: can have abstract and non-abstract methods, can have constructors, can have instance variables
with any access modifier, supports single inheritance (a class extends one abstract class). Interface: all methods
are public and abstract by default (Java 8 allows default/static methods), no constructors, all variables are public
static final, supports multiple inheritance (a class can implement multiple interfaces). Use abstract class for 'is-a'
with shared state; use interface for 'can-do' behavior contracts.
abstract class Vehicle { // abstract class String brand; abstract void start(); void stop() {
[Link]("Stopped"); } } interface Flyable { // interface void fly(); // implicitly
public abstract } class FlyingCar extends Vehicle implements Flyable { void start() {
[Link]("Started!"); } public void fly() { [Link]("Flying!"); } }
Q9. Why doesn't Java support multiple inheritance?
Java does not allow a class to extend multiple classes to avoid the Diamond Problem — an ambiguity that arises
when two parent classes have the same method and the child class doesn't know which one to inherit. This leads
to ambiguous behavior and makes code difficult to maintain. Java solves this by allowing a class to extend only
one class (single inheritance) but implement multiple interfaces. Since interfaces (before Java 8) had no method
bodies, there was no ambiguity. Java 8's default methods reintroduced the problem and require explicit overriding
to resolve it.
// This is NOT allowed in Java: // class C extends A, B { } -- causes Diamond Problem // But
this IS allowed: interface A { default void show() { [Link]("A"); } } interface B {
default void show() { [Link]("B"); } } class C implements A, B { public void show()
{ [Link](); } // must resolve }
Q10. What is the Diamond Problem?
The Diamond Problem occurs in multiple inheritance when two parent classes (B and C) both inherit from the same
class (A) and override one of A's methods. If a class D inherits from both B and C, it becomes ambiguous which
version of the method D should use — B's or C's — forming a diamond shape in the inheritance hierarchy. This
leads to undefined behavior. Java avoids this by not allowing multiple class inheritance. With interfaces and default
methods in Java 8, the programmer must explicitly override the conflicting method to resolve the ambiguity.
// Diamond structure: // A // / \ // B C // \ / // D -- which show() to use? interface A {
default void show() { [Link]("A"); } } interface B extends A { default void show()
{ [Link]("B"); } } interface C extends A { default void show() {
Java Interview Guide · Page 8
[Link]("C"); } } class D implements B, C { public void show() { [Link](); }
// explicit resolution }
Java Interview Guide · Page 9
■■ SECTION 04 — Classes & Objects
Q1. What is a class?
A class is a blueprint or template that defines the structure and behaviour shared by all objects of that type. It
specifies what data (fields/variables) and operations (methods) an object of that class will have. A class does not
consume memory by itself — it is just a design. Think of a class as an architectural plan for a house; the plan itself
is not a house, but you can build many houses from it. Classes can contain fields, methods, constructors, nested
classes, and blocks. In Java, every program is written inside a class.
class Car { String brand; // field int speed; void accelerate() { // method speed += 10;
[Link]("Speed: " + speed); } }
Q2. What is an object?
An object is a real-world instance of a class. It is created using the 'new' keyword, which allocates memory on the
heap and calls the constructor. An object has state (values of its fields), behaviour (methods it can perform), and
identity (unique memory address). Every object is independent — changing one object does not affect another.
When you create an object, memory is allocated for all instance variables. Multiple objects can exist from the same
class, each with their own state.
Car myCar = new Car(); [Link] = "Toyota"; [Link] = 0; [Link](); // Speed:
10 Car yourCar = new Car(); // separate object with own state [Link] = "Honda";
Q3. Difference between class and object?
A class is a logical construct (blueprint) defined at compile time — it exists in source code and defines structure. An
object is a physical construct (instance) created at runtime — it occupies actual memory in the heap. A class is
declared once; many objects can be created from it. A class has no memory allocation by itself (except for static
members); an object takes memory when created with 'new'. The class defines what data and methods exist; the
object holds actual data values and can invoke those methods.
■ Class : Object = Blueprint : House
Q4. What is the 'this' keyword?
The 'this' keyword in Java refers to the current instance of the class — the object on which the current method or
constructor is being invoked. It is used to: (1) disambiguate between instance variables and parameters with the
same name, (2) call another constructor in the same class using this() (constructor chaining), (3) pass the current
object as an argument to another method, (4) return the current object from a method. 'this' cannot be used in
static methods because static methods belong to the class, not any instance.
class Person { String name; int age; Person(String name, int age) { [Link] = name; //
disambiguate [Link] = age; } Person getInstance() { return this; // return current object } }
Q5. What is the difference between object and reference?
An object is the actual data structure created in heap memory when 'new' is called. A reference is a variable that
holds the memory address (pointer) of the object in the heap; it does not contain the object itself. Multiple
references can point to the same object. If you assign one reference to another, both variables point to the same
object — modifying through one reference affects the other. Setting a reference to null does not destroy the object
immediately; it just removes that reference. The garbage collector reclaims the object when no references point to
it.
Car c1 = new Car(); // c1 is reference; object in heap Car c2 = c1; // c2 also points to SAME
object [Link] = "Ford"; // changes reflected via c1 too [Link]([Link]); //
"Ford" c1 = null; // c1 no longer references object // object still alive because c2 references
it
Java Interview Guide · Page 10
Java Interview Guide · Page 11
■■ SECTION 05 — Constructors
Q1. What is a constructor?
A constructor is a special method used to initialize objects. It is called automatically when an object is created with
'new'. A constructor has the same name as the class and has no return type (not even void). Its purpose is to set
initial values for the object's fields. If you don't define a constructor, Java provides a default no-argument
constructor automatically. Constructors can be overloaded. They cannot be abstract, static, final, or synchronized.
Constructors are not inherited, though you can call the parent's constructor using super().
class Student { String name; int roll; Student(String name, int roll) { // constructor
[Link] = name; [Link] = roll; } } Student s = new Student("Alice", 1); // constructor
called
Q2. Types of constructors?
Java has three types of constructors: (1) Default Constructor — provided by the compiler if no constructor is
defined; it has no parameters and sets fields to default values (0, null, false). (2) No-argument Constructor —
explicitly defined by the programmer with no parameters; used to set custom default values. (3) Parameterized
Constructor — takes parameters to initialize fields with specific values at object creation. Constructors can be
overloaded (same class, different parameter lists), allowing flexible object creation.
class Box { int length, width; Box() { length = 1; width = 1; } // no-arg Box(int l, int w) {
// parameterized length = l; width = w; } } Box b1 = new Box(); Box b2 = new Box(5, 3);
Q3. Difference between constructor and method?
A constructor has the same name as the class, has no return type, is called automatically at object creation, cannot
be called directly by name, cannot be overridden, and is used for initialization. A method has any name different
from the class (usually), has a return type (or void), is called explicitly by the programmer, can be called multiple
times, can be overridden, and is used to perform operations on the object. Constructors are not inherited; methods
are. Both can be overloaded.
■ Constructor = initialize | Method = operate. No return type vs explicit return type.
Q4. What is constructor overloading?
Constructor overloading means defining multiple constructors in the same class with different parameter lists
(different number, types, or order of parameters). This provides multiple ways to create objects with different initial
states. The compiler selects the appropriate constructor based on the arguments passed. Constructor overloading
improves flexibility and usability of a class. It is a form of compile-time polymorphism. All overloaded constructors
can call each other using this() to avoid code duplication.
class Rectangle { int w, h; Rectangle() { w = 1; h = 1; } // default Rectangle(int s) { w = s;
h = s; } // square Rectangle(int w, int h) { this.w=w; this.h=h; } // full } Rectangle r1 = new
Rectangle(); Rectangle r2 = new Rectangle(5); Rectangle r3 = new Rectangle(4, 6);
Q5. What is this()?
this() is used inside a constructor to call another constructor of the same class — this is called constructor
chaining. It must be the first statement in the constructor. It avoids code duplication when multiple constructors
share initialization logic. You can pass arguments to select which overloaded constructor to call. You cannot use
both this() and super() in the same constructor since both must be the first statement. this() is purely a
constructor-to-constructor call within the same class.
class Employee { String name; int age; String dept; Employee(String name) { this(name, 25); //
calls next constructor } Employee(String name, int age) { this(name, age, "IT"); // calls full
constructor } Employee(String name, int age, String dept) { [Link] = name; [Link] = age;
Java Interview Guide · Page 12
[Link] = dept; } }
Q6. What is super()?
super() is used inside a subclass constructor to call the constructor of the parent class. It must be the first
statement in the constructor. If you don't explicitly call super(), Java implicitly inserts super() (calling the parent's
no-arg constructor). If the parent has no no-arg constructor, you MUST explicitly call super(args) or compilation
fails. super() is used to reuse the parent's initialization logic and ensure proper object construction when
inheritance is used. super can also be used to call parent class methods or access parent fields.
class Animal { String name; Animal(String name) { [Link] = name; } } class Dog extends
Animal { String breed; Dog(String name, String breed) { super(name); // calls Animal
constructor [Link] = breed; } } Dog d = new Dog("Rex", "Labrador");
Q7. Can constructors be overridden?
No, constructors cannot be overridden. Overriding requires that a method with the same name and signature exists
in a subclass. Constructors are not inherited by subclasses — each class has its own constructors. Even if a
subclass defines a constructor with the same signature as the parent, it is not overriding; it is just a separate
constructor. Since constructors are not inherited, there is nothing to override. They can only be called from a
subclass using super(). Constructors can be overloaded (same class, different params) but not overridden.
■ Constructors: can be overloaded (YES) | overridden (NO) | inherited (NO).
Java Interview Guide · Page 13
■ SECTION 06 — Strings (Very Important)
Q1. What is String?
In Java, String is a class in [Link] package that represents a sequence of characters. It is not a primitive type.
Strings are objects stored in a special memory area called the String Pool (inside the heap). String is one of the
most widely used classes in Java. It provides a rich API — length(), charAt(), substring(), indexOf(), equals(),
compareTo(), etc. Strings in Java are immutable — once created, their content cannot be changed. Every string
literal in Java is an instance of the String class.
String s1 = "Hello"; // String literal (uses pool) String s2 = new String("Hello"); // new
object on heap [Link]([Link]()); // 5 [Link]([Link]()); //
HELLO
Q2. Why is String immutable?
String is immutable in Java for several important reasons: (1) Security — Strings are used for sensitive data like
passwords, file paths, and network connections; immutability prevents accidental modification. (2) String Pool
efficiency — the JVM can safely share String literals in the pool because they can't be changed. (3) Thread safety
— immutable objects are inherently thread-safe; no synchronization is needed. (4) Hashing — String's hashCode
is cached after first computation since the value never changes, making it efficient as HashMap keys. Any
'modification' creates a new String object.
String s = "Hello"; s = s + " World"; // creates NEW String; original "Hello" unchanged // s
now points to "Hello World" // "Hello" still exists in String pool
Q3. What is String Pool?
String Pool (also called String Intern Pool or String Constant Pool) is a special storage area inside the Java heap
where String literals are cached. When you create a string literal like String s = "Hello", Java first checks the pool. If
"Hello" already exists there, it returns the same reference (no new object created). If not, it creates a new entry.
This mechanism saves memory and improves performance. String objects created with 'new String(...)' always go
to the heap, not the pool — use intern() to add them to the pool explicitly.
String a = "Java"; // pool String b = "Java"; // returns same pool reference String c = new
String("Java"); // new heap object [Link](a == b); // true (same pool ref)
[Link](a == c); // false (different objects) [Link]([Link](c)); //
true (same content)
Q4. Difference between String, StringBuilder, and StringBuffer?
String is immutable — every modification creates a new object. Not suitable for heavy concatenation. StringBuilder
is mutable, not synchronized (not thread-safe), and faster — use in single-threaded scenarios for frequent string
manipulation. StringBuffer is mutable and synchronized (thread-safe) — use in multi-threaded scenarios.
Performance order: StringBuilder > StringBuffer > String (for modification). All three have similar APIs (append,
insert, delete, reverse). Prefer StringBuilder for most cases unless thread-safety is needed.
// String - immutable String s = "Hello"; s += " World"; // creates new object // StringBuilder
- mutable, faster StringBuilder sb = new StringBuilder("Hello"); [Link](" World"); //
modifies in-place // StringBuffer - thread-safe StringBuffer sbf = new StringBuffer("Hello");
[Link](" World"); // synchronized append
■ Interview tip: String=immutable, SB=mutable+fast, SBF=mutable+threadsafe.
Java Interview Guide · Page 14
Q5. Difference between == and equals()?
'==' is the reference equality operator — it checks whether two variables point to the same memory location (same
object). For primitives, it compares values. For objects, it compares references. 'equals()' is a method defined in
the Object class, intended to compare the logical content/value of objects. String overrides equals() to compare
character sequences. You should always use equals() to compare String content, never ==, because two separate
String objects with the same content will have different references.
String a = "Hello"; String b = "Hello"; String c = new String("Hello"); a == b; // true (same
pool object) a == c; // false (different heap objects) [Link](c); // true (same content)
■ Always use .equals() for String comparison, not ==.
Q6. How many ways can you create a String?
There are two main ways: (1) String literal — String s = "Hello"; — Java looks in the String Pool; if found, returns
existing reference; otherwise creates new entry in pool. (2) Using 'new' keyword — String s = new String("Hello");
— always creates a new object on the heap, even if the pool already has "Hello". Additional ways:
[Link](42) converts primitives to String; char array — new String(char[]) — String from character array;
StringBuilder/StringBuffer .toString().
String s1 = "Hello"; // literal String s2 = new String("Hello"); // new heap object String s3 =
[Link](42); // "42" char[] ch = {'H','i'}; String s4 = new String(ch); // "Hi"
StringBuilder sb = new StringBuilder("Hey"); String s5 = [Link](); // "Hey"
Java Interview Guide · Page 15
■ SECTION 07 — Collections Framework
Q1. What is Collection Framework?
The Java Collections Framework (JCF) is a unified architecture for storing and manipulating groups of objects. It
provides ready-to-use data structure implementations (List, Set, Queue, Map) with algorithms (sort, search,
shuffle). It reduces programming effort and increases performance. The core interfaces are: Collection (root for
List, Set, Queue) and Map (separate hierarchy). Key implementations: ArrayList, LinkedList, HashSet, TreeSet,
HashMap, TreeMap, PriorityQueue, etc. The [Link] package contains all collection classes.
■ Hierarchy: Collection → List/Set/Queue | Map is separate.
Q2. Difference between List, Set, and Map?
List is an ordered collection that allows duplicates. Elements have index positions (0-based). Implementations:
ArrayList, LinkedList, Vector. Set is an unordered collection that does NOT allow duplicates. No index access.
Implementations: HashSet, LinkedHashSet, TreeSet. Map stores key-value pairs; keys are unique, values can be
duplicate. Not a sub-interface of Collection. Implementations: HashMap, TreeMap, LinkedHashMap, Hashtable.
List<String> list = new ArrayList<>(); [Link]("a"); [Link]("a"); // duplicates allowed
Set<String> set = new HashSet<>(); [Link]("a"); [Link]("a"); // duplicate ignored, size=1
Map<String,Integer> map = new HashMap<>(); [Link]("age", 25); // key-value pairs
Q3. What is ArrayList?
ArrayList is a resizable array implementation of the List interface. It stores elements in a dynamic array that grows
automatically when capacity is exceeded (by 50% in Java). It allows duplicate elements and maintains insertion
order. It provides O(1) access by index (get/set) but O(n) for insertion/deletion at arbitrary positions. ArrayList is not
synchronized (not thread-safe). Use [Link]() or CopyOnWriteArrayList for thread safety.
Default initial capacity is 10.
ArrayList<Integer> list = new ArrayList<>(); [Link](10); [Link](20); [Link](30);
[Link](1, 15); // insert at index 1 [Link](0); // remove at index 0
[Link](list); // [15, 20, 30] [Link]([Link](1)); // 20
Q4. Difference between ArrayList and LinkedList?
ArrayList uses a dynamic array internally — fast random access O(1), slow insert/delete in middle O(n) due to
shifting. LinkedList uses a doubly-linked list — O(1) insert/delete at beginning/end, O(n) for random access (no
index). ArrayList is better for read-heavy operations; LinkedList is better for frequent insert/delete. ArrayList wastes
some memory due to capacity management; LinkedList uses more memory per element (stores two extra
pointers). LinkedList also implements Deque, so it can act as a queue/stack.
// Use ArrayList for frequent access: ArrayList<String> al = new ArrayList<>(); [Link](5); //
O(1) // Use LinkedList for frequent insert/delete: LinkedList<String> ll = new LinkedList<>();
[Link]("X"); // O(1)
Q5. What is HashSet?
HashSet is a Set implementation backed by a HashMap. It stores unique elements with no guaranteed order. It
allows one null element. Operations (add, remove, contains) are O(1) average case. Elements are stored based on
their hashCode. HashSet doesn't maintain insertion order (use LinkedHashSet for that) and doesn't sort elements
(use TreeSet for sorted order). Internally, elements are stored as keys in a HashMap with a dummy value
(PRESENT). Two elements are considered equal if equals() returns true and hashCodes match.
HashSet<String> set = new HashSet<>(); [Link]("Apple"); [Link]("Banana"); [Link]("Apple");
[Link](set); // [Apple, Banana] (no dup)
[Link]([Link]("Apple")); // true
Java Interview Guide · Page 16
Q6. Difference between HashSet and TreeSet?
HashSet stores elements in a hash table — no ordering guaranteed, O(1) operations. TreeSet stores elements in a
Red-Black Tree — always sorted in natural order (or custom Comparator), O(log n) operations. HashSet allows
one null; TreeSet throws NullPointerException if null is added (can't compare null). Use HashSet for fast operations
with no order needed; use TreeSet when sorted iteration is required.
HashSet<Integer> hs = new HashSet<>([Link](3,1,2,1)); [Link](hs); // [1, 2,
3] or any order TreeSet<Integer> ts = new TreeSet<>([Link](3,1,2,1));
[Link](ts); // [1, 2, 3] always sorted
Q7. What is HashMap?
HashMap is a key-value store (Map implementation) that uses hashing for O(1) average case put/get. It allows one
null key and multiple null values. It does not guarantee insertion order (use LinkedHashMap for that). It is not
synchronized (not thread-safe; use ConcurrentHashMap). Default initial capacity is 16 with load factor 0.75 — it
rehashes (doubles) when 75% full. Keys must implement hashCode() and equals() consistently. HashMap
internally uses an array of linked lists (or trees in Java 8+) called buckets.
HashMap<String, Integer> map = new HashMap<>(); [Link]("Alice", 90); [Link]("Bob", 85);
[Link]("Alice", 95); // updates existing key [Link]([Link]("Alice")); // 95
[Link]([Link]("Bob")); // true for ([Link]<String,Integer> e :
[Link]()) [Link]([Link]() + " = " + [Link]());
Q8. How does HashMap work internally?
HashMap uses an array of Node (bucket) objects. When you call put(key, value): (1) hashCode() is called on the
key; (2) the hash is processed (bit manipulation) to find the bucket index (hash & (n-1)); (3) if bucket is empty, the
node is stored; (4) if bucket has entries (collision), Java 7 uses linked list chaining; Java 8+ converts to a balanced
Red-Black Tree when a bucket has 8+ entries (TREEIFY_THRESHOLD) for O(log n) worst case instead of O(n).
When the load factor (0.75) is exceeded, the array is doubled and all entries are rehashed.
// Simplified internal flow: // put("name", "Alice") // 1. hash = "name".hashCode() --> some
int // 2. index = hash & (16-1) = 7 (for example) // 3. table[7] --> store Node("name",
"Alice") // If table[7] already has entries --> chain/tree
■ Java 8+: Linked list becomes Red-Black Tree at 8+ entries per bucket.
Q9. What is a collision in HashMap?
A collision in HashMap occurs when two different keys produce the same bucket index (after hash computation).
This happens because many keys map to a limited number of buckets. Collisions are handled by chaining —
multiple entries are stored in the same bucket as a linked list (or tree). get() then walks the list calling equals() to
find the exact key. Too many collisions degrade performance from O(1) to O(n) (or O(log n) with trees). Using good
hashCode() implementations and proper load factor management minimizes collisions.
// "Aa" and "BB" have same hashCode in Java! [Link]("Aa".hashCode()); // 2112
[Link]("BB".hashCode()); // 2112 -- collision! // Both go to same bucket; equals()
used to distinguish
Q10. Difference between HashMap and Hashtable?
HashMap: not synchronized (not thread-safe), allows one null key and null values, faster, introduced in Java 1.2,
part of Collections framework, use with ConcurrentHashMap for thread safety. Hashtable: synchronized
(thread-safe at method level), does NOT allow null keys or values (throws NullPointerException), slower due to
synchronization overhead, legacy class from Java 1.0, obsolete — prefer ConcurrentHashMap in multi-threaded
code.
// HashMap -- fast, not thread-safe HashMap<String,String> hm = new HashMap<>(); [Link](null,
"value"); // OK // Hashtable -- thread-safe but slow Hashtable<String,String> ht = new
Hashtable<>(); // [Link](null, "x"); // throws NullPointerException
Java Interview Guide · Page 17
■ Modern code: use ConcurrentHashMap instead of Hashtable.
Q11. Difference between Comparable and Comparator?
Comparable ([Link]) is implemented by the class itself in the compareTo() method — it defines the 'natural
ordering' of objects. Only one natural ordering per class. Comparator ([Link]) is a separate class/lambda that
defines an external ordering via compare(). Multiple Comparators can be defined for the same class (sort by name,
then by age, etc.). Use Comparable when the class has one obvious natural order (e.g., Integer, String). Use
Comparator for custom, alternative, or multiple sorting strategies.
// Comparable class Student implements Comparable<Student> { int age; public int
compareTo(Student o) { return [Link] - [Link]; } } // Comparator (lambda) List<Student> list =
...; [Link]((a, b) -> [Link]([Link]));
Q12. What is Iterator?
Iterator is an interface in [Link] that provides a way to traverse (iterate) elements of a Collection one by one. It
has three methods: hasNext() (returns true if more elements exist), next() (returns the next element), and remove()
(removes the last element returned by next()). Iterator is obtained from any Collection using the iterator() method. It
follows the fail-fast principle — throws ConcurrentModificationException if the collection is structurally modified
during iteration (except via the iterator's own remove()). For-each loop internally uses Iterator.
List<String> names = new ArrayList<>([Link]("A","B","C")); Iterator<String> it =
[Link](); while ([Link]()) { String name = [Link](); if ([Link]("B"))
[Link](); // safe removal } [Link](names); // [A, C]
Java Interview Guide · Page 18
■■ SECTION 08 — Exception Handling
Q1. What is an exception?
An exception is an abnormal event that disrupts the normal flow of a program's execution. In Java, exceptions are
objects — they are instances of classes that extend [Link]. When an error condition occurs, an
exception object is 'thrown'. If not handled, the program terminates abnormally. Exceptions carry information about
the error: type, message, and stack trace. Java's exception handling mechanism (try-catch-finally) allows programs
to gracefully handle errors and continue or shut down cleanly.
// Exception in action: int[] arr = {1, 2, 3}; [Link](arr[5]); // throws
ArrayIndexOutOfBoundsException!
Q2. Difference between Error and Exception?
Both Error and Exception extend [Link]. Error represents serious problems that a reasonable
application should not try to catch — they are usually caused by the JVM environment (OutOfMemoryError,
StackOverflowError, VirtualMachineError). Applications typically cannot recover from Errors. Exception represents
conditions that a program can catch and handle — like FileNotFoundException, NullPointerException, etc.
Exception is further divided into checked (compile-time) and unchecked (runtime) exceptions.
■ Error = JVM/system problem (don't catch) | Exception = program problem (handle).
Q3. What is exception handling?
Exception handling is a mechanism to handle runtime errors in a controlled way, maintaining the normal flow of the
program. Java provides five keywords for this: try (block of code to monitor for exceptions), catch (block to handle
specific exceptions), finally (block that always executes, for cleanup), throw (to explicitly throw an exception),
throws (to declare that a method might throw an exception). Exception handling separates error-handling code
from regular code, making programs more robust and readable.
try { int result = 10 / 0; // ArithmeticException } catch (ArithmeticException e) {
[Link]("Error: " + [Link]()); } finally { [Link]("This always
runs!"); }
Q4. What is try-catch-finally?
try block contains the code that might throw an exception. If an exception occurs, execution jumps to the matching
catch block. catch block specifies the exception type to handle and the recovery logic. Multiple catch blocks can
handle different exception types. finally block always executes regardless of whether an exception occurred or was
caught — it is used for cleanup operations like closing files, database connections, etc. Even if a return statement
is in try or catch, finally still executes (except on [Link]()).
try { FileReader f = new FileReader("[Link]"); // read file... } catch (FileNotFoundException
e) { [Link]("File not found: " + [Link]()); } catch (IOException e) {
[Link]("IO Error: " + [Link]()); } finally { [Link]("Closing
resources..."); // always runs }
Q5. Difference between checked and unchecked exceptions?
Checked exceptions are exceptions that the compiler forces you to handle (with try-catch) or declare (with throws).
They are subclasses of Exception but not RuntimeException. Examples: IOException, FileNotFoundException,
SQLException. Unchecked exceptions (Runtime Exceptions) are NOT checked at compile time. They are
subclasses of RuntimeException. Examples: NullPointerException, ArrayIndexOutOfBoundsException,
ArithmeticException. You may handle them but the compiler doesn't force you to. Errors are also unchecked.
// Checked - compiler forces handling void read() throws IOException { ... } // OR try { new
FileReader("[Link]"); } catch (FileNotFoundException e) { ... } // Unchecked - compiler silent,
Java Interview Guide · Page 19
but crashes at runtime String s = null; [Link](); // NullPointerException -- unchecked
■ Checked = must handle | Unchecked = optional handling.
Q6. What is throw?
The 'throw' keyword is used to explicitly throw an exception from a method or block of code. You can throw any
object that is an instance of Throwable (Exception or Error). It is used when you want to deliberately raise an
exception based on certain conditions — for example, validating input data. When throw is executed, the current
method's execution stops and the exception propagates up the call stack until caught. You typically throw custom
exceptions to indicate specific error conditions in your application.
void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("Invalid
age: " + age); } [Link] = age; } // setAge(-5) throws IllegalArgumentException
Q7. What is throws?
The 'throws' keyword is used in a method signature to declare that the method might throw one or more checked
exceptions. It informs the caller that they must handle (try-catch) or propagate (throws) those exceptions. It does
not throw the exception itself — that's done by 'throw'. Multiple exceptions can be declared: void myMethod()
throws IOException, SQLException. throws is mandatory for checked exceptions; optional for unchecked
(RuntimeException). It is part of the method contract.
// Method declares it might throw IOException void readFile(String path) throws IOException {
FileReader fr = new FileReader(path); // may throw // ... } // Caller must handle it: try {
readFile("[Link]"); } catch (IOException e) { [Link]("Caught: " +
[Link]()); }
Q8. What is NullPointerException?
NullPointerException (NPE) is the most common runtime exception in Java. It occurs when you try to use a
reference variable that is null (doesn't point to any object) — such as calling a method, accessing a field, or using
an array element on a null reference. It is an unchecked exception (subclass of RuntimeException). Common
causes: forgetting to initialize an object, a method returning null unexpectedly, or accessing a chained method call
where an intermediate result is null. Java 14+ provides 'helpful NullPointerExceptions' with exact location info.
String s = null; [Link](); // NullPointerException! // Prevention: if (s != null) {
[Link]([Link]()); } // Or use Optional (Java 8+):
[Link](s).ifPresent(str -> [Link]([Link]()));
Q9. How do you create a custom exception?
To create a custom exception, extend Exception (for checked) or RuntimeException (for unchecked) and optionally
add custom fields and constructors. Custom exceptions make code more readable by using domain-specific
exception names. They can carry additional context (error codes, field names, etc.). Always call the parent
constructor using super(message) to properly set the exception message accessible via getMessage().
// Custom checked exception class InsufficientFundsException extends Exception { private double
amount; public InsufficientFundsException(double amount) { super("Insufficient funds. Short
by: " + amount); [Link] = amount; } public double getAmount() { return amount; } } //
Usage: void withdraw(double amt) throws InsufficientFundsException { if (amt > balance) throw
new InsufficientFundsException(amt - balance); }
Java Interview Guide · Page 20
■ SECTION 09 — Multithreading
Q1. What is a thread?
A thread is the smallest unit of execution within a process. It is a lightweight sub-process that runs independently
but shares the process's memory space (heap, method area). Each thread has its own program counter (PC),
stack, and local variables. Java supports multithreading natively via the [Link] class and
[Link] interface. Threads enable concurrent execution of multiple tasks within a single program — for
example, a web server handling multiple requests simultaneously.
class MyThread extends Thread { public void run() { [Link]("Thread running: " +
getName()); } } MyThread t = new MyThread(); [Link](); // starts thread, calls run() in new
thread
Q2. What is multithreading?
Multithreading is the ability of a program to execute multiple threads concurrently, allowing better utilization of CPU
resources. Each thread runs seemingly in parallel — on a multi-core CPU they may run truly in parallel; on a single
core, they time-slice. Benefits: improved application responsiveness (UI thread stays responsive while background
tasks run), better CPU utilization, faster execution of tasks that can be parallelized. Java supports multithreading
through Thread class, Runnable, Callable, Executor framework, and concurrent utilities in [Link].
■ Use ExecutorService over raw Thread management in production code.
Q3. Difference between process and thread?
A process is a program in execution — it has its own independent memory space (code, data, heap, stack).
Processes are heavyweight; creating/switching between processes is expensive. A thread is a unit of execution
within a process — it shares the process's heap and method area but has its own stack. Threads are lightweight;
creating and context-switching threads is much cheaper. Communication between processes (IPC) is complex;
threads communicate easily through shared memory. A process crash doesn't affect other processes; a thread
crash can crash the whole process.
■ Process = isolated program | Thread = execution unit inside process.
Q4. How do you create a thread?
Two primary ways: (1) Extend Thread class — override run() method, create instance, call start(). (2) Implement
Runnable interface — implement run(), pass to Thread constructor, call start(). Prefer Runnable because it allows
the class to extend another class (Java's single inheritance). In Java 8+, use lambdas with Runnable. Java 5+ also
offers Callable (like Runnable but can return a result and throw checked exceptions) and ExecutorService for
thread pool management.
// Method 1: Extend Thread class T extends Thread { public void run() {
[Link]("T1"); } } new T().start(); // Method 2: Implement Runnable (preferred) new
Thread(() -> [Link]("T2")).start(); // Method 3: ExecutorService ExecutorService
es = [Link](3); [Link](() -> [Link]("T3"));
[Link]();
Q5. Difference between start() and run()?
start() creates a new thread of execution and then calls run() in that new thread — this is what actually achieves
multithreading. start() can only be called once per Thread instance; calling it again throws
IllegalThreadStateException. run() is just a normal method — calling run() directly executes the code in the
CURRENT thread, not a new thread. No new thread is created. This is a common interview trap — calling
[Link]() instead of [Link]() results in sequential (single-threaded) execution.
Java Interview Guide · Page 21
Thread t = new Thread(() -> [Link]([Link]().getName())); [Link]();
// NEW thread -- prints: Thread-0 t = new Thread(() ->
[Link]([Link]().getName())); [Link](); // SAME thread -- prints: main
(no new thread!)
■ Always use start() for multithreading; run() is just a regular method call.
Q6. What is synchronization?
Synchronization is a mechanism that ensures only one thread at a time can execute a synchronized block or
method, preventing thread interference and data inconsistency. In Java, every object has an intrinsic lock
(monitor). When a thread enters a synchronized block/method, it acquires the lock; other threads attempting to
enter must wait until the lock is released. Synchronization can be applied to methods (synchronized keyword on
method) or blocks (synchronized(object) {}). It prevents race conditions but may introduce deadlocks and reduces
performance.
class Counter { private int count = 0; synchronized void increment() { // only one thread at a
time count++; } // OR use sync block: void decrement() { synchronized(this) { count--; } } }
Q7. What is thread safety?
Thread safety means a class or method can be used correctly by multiple threads simultaneously without causing
data corruption or unexpected behavior. A class is thread-safe if its internal state remains consistent when
accessed by multiple threads concurrently. Techniques to achieve thread safety: (1) Synchronization — using
synchronized keyword or locks. (2) Atomic classes — AtomicInteger, AtomicLong ([Link]). (3)
Volatile keyword — ensures visibility of changes across threads. (4) Immutable objects — since they can't change,
they're inherently thread-safe. (5) Thread-local storage.
// Not thread-safe: int count = 0; count++; // read-modify-write is not atomic! // Thread-safe
with AtomicInteger: AtomicInteger count = new AtomicInteger(0); [Link](); //
atomic operation
Q8. What is race condition?
A race condition occurs when two or more threads access shared data concurrently and the final result depends on
the timing/order of execution. The outcome is unpredictable and non-deterministic. Classic example: two threads
each read count=5, both increment to 6, both write 6 — but the correct answer is 7. Race conditions are caused by
non-atomic operations on shared mutable state without proper synchronization. They are hard to reproduce and
debug. Prevention: synchronization, atomic operations, or using thread-safe data structures.
// Race condition example: class Counter { int count = 0; void increment() { count++; } // NOT
atomic: read, modify, write } // Thread1 and Thread2 both call increment simultaneously //
Expected: count=2, but might get count=1 due to race!
Q9. What is deadlock?
Deadlock is a situation where two or more threads are waiting forever for each other to release locks — forming a
circular dependency. Thread A holds Lock 1 and waits for Lock 2; Thread B holds Lock 2 and waits for Lock 1.
Neither can proceed. Prevention strategies: always acquire locks in the same order, use tryLock() with timeout,
minimize synchronized block scope, use lock-free data structures, or use higher-level concurrency utilities
(ReentrantLock). Deadlocks can be detected using thread dump analysis.
// Deadlock scenario: // Thread 1: synchronized(lockA) { synchronized(lockB) {...} } // Thread
2: synchronized(lockB) { synchronized(lockA) {...} } // Prevention: always lock in same order:
// Thread 1: synchronized(lockA) { synchronized(lockB) {...} } // Thread 2: synchronized(lockA)
{ synchronized(lockB) {...} }
■ 4 Conditions for Deadlock: Mutual Exclusion, Hold&Wait;, No Preemption, Circular Wait.
Java Interview Guide · Page 22
■ SECTION 10 — Java 8 Features
Q1. What is Lambda Expression?
A lambda expression is a concise way to represent an anonymous function (a method without a name). It
implements a functional interface (an interface with exactly one abstract method). Syntax: (parameters) ->
expression. Lambdas enable functional programming in Java, making code more concise and readable —
especially with Collections and Streams. They eliminate the need for verbose anonymous inner classes. Lambda
expressions don't have their own 'this' — they capture the enclosing scope's 'this'.
// Before Java 8: Runnable r = new Runnable() { public void run() {
[Link]("Running"); } }; // With Lambda: Runnable r = () ->
[Link]("Running"); // With Comparator: [Link]((a, b) -> [Link](b));
Q2. What is Functional Interface?
A functional interface is an interface that has exactly one abstract method (SAM — Single Abstract Method). It can
have default and static methods. The @FunctionalInterface annotation is optional but recommended — it causes a
compile error if you accidentally add a second abstract method. Lambdas implement functional interfaces. Built-in
functional interfaces in [Link]: Predicate (T→boolean), Function (T→R), Consumer (T→void), Supplier
(→T), BiFunction, etc.
@FunctionalInterface interface MathOperation { int operate(int a, int b); } MathOperation add =
(a, b) -> a + b; MathOperation mul = (a, b) -> a * b; [Link]([Link](5, 3)); //
8 [Link]([Link](5, 3)); // 15
Q3. What is Streams API?
The Streams API ([Link]) provides a functional-style way to process collections of objects. A Stream is a
sequence of elements supporting sequential and parallel aggregate operations. Streams don't store data — they
work on the source collection. Streams are lazy — intermediate operations (filter, map, sorted) are not executed
until a terminal operation (collect, forEach, count, reduce) is called. Streams can be sequential (default) or parallel
(parallelStream()). Once a stream is consumed, it cannot be reused.
List<String> names = [Link]("Alice","Bob","Charlie","Ann"); List<String> result =
[Link]() .filter(n -> [Link]("A")) // intermediate .map(String::toUpperCase) //
intermediate .sorted() // intermediate .collect([Link]()); // terminal
[Link](result); // [ALICE, ANN]
Q4. Difference between Collection and Stream?
Collection is a data structure that stores elements in memory — all elements are computed and stored eagerly.
You can iterate it multiple times. Collection is the source of data. Stream is a pipeline for processing data from a
Collection — elements are computed lazily on demand. A Stream can only be traversed once (then it's exhausted).
Collection is for data storage; Stream is for data processing. Collections support CRUD operations; Streams
support only read operations (they don't modify the source).
■ Collection = stores data | Stream = processes data lazily.
Q5. What is filter()?
filter() is an intermediate Stream operation that tests each element against a Predicate (boolean condition) and
passes only matching elements to the next stage. It doesn't modify the original collection. Signature: Stream
filter(Predicate predicate). Multiple filters can be chained. filter() is lazy — it doesn't execute until a terminal
operation is called. Under the hood, it creates a new stream that iterates the source and applies the predicate.
List<Integer> nums = [Link](1, 2, 3, 4, 5, 6, 7, 8); List<Integer> evens = [Link]()
.filter(n -> n % 2 == 0) .collect([Link]()); [Link](evens); // [2, 4, 6,
Java Interview Guide · Page 23
8]
Q6. What is map()?
map() is an intermediate Stream operation that transforms each element in the stream by applying a Function —
mapping each element to a new value (possibly of a different type). Signature: Stream map(Function mapper). It's
a 1-to-1 transformation — each element produces exactly one output element. Common uses: extracting a field
from objects, converting types, applying transformations. Specialized variants: mapToInt(), mapToDouble(),
mapToLong() return primitive streams for efficiency.
List<String> names = [Link]("alice", "bob", "charlie"); List<String> upper =
[Link]() .map(String::toUpperCase) .collect([Link]());
[Link](upper); // [ALICE, BOB, CHARLIE] // Map objects to a field: List<Integer>
lengths = [Link]() .map(String::length) .collect([Link]()); // [5, 3, 7]
Q7. What is reduce()?
reduce() is a terminal Stream operation that combines all elements into a single result by repeatedly applying a
BinaryOperator. It 'folds' the stream into one value. Signature: Optional reduce(BinaryOperator) or T reduce(T
identity, BinaryOperator). The identity value is the initial value used in the reduction. Examples: sum, product, max,
min, string concatenation. reduce() is the foundation of aggregate operations — sum(), count(), average() are
implemented using it internally.
List<Integer> nums = [Link](1, 2, 3, 4, 5); // Sum using reduce int sum = [Link]()
.reduce(0, (a, b) -> a + b); [Link](sum); // 15 // Or with method reference: int
product = [Link]() .reduce(1, Integer::sum); // sums with initial 1
Q8. What is Method Reference?
Method reference is a shorthand notation for a lambda expression that calls an existing method. Syntax:
ClassName::methodName. Types: (1) Static method reference — Math::sqrt, (2) Instance method reference on a
specific object — obj::toString, (3) Instance method reference on an arbitrary object of a type —
String::toUpperCase, (4) Constructor reference — ArrayList::new. Method references make code more readable
and concise when the lambda simply delegates to an existing method.
// Lambda vs Method Reference: [Link](s -> [Link](s)); // lambda
[Link]([Link]::println); // method ref // Various types:
[Link]().map(Math::sqrt); // static method ref [Link]().map(String::length); //
instance method ref [Link](Random::new); // constructor ref
Q9. What is Predicate?
Predicate is a built-in functional interface in [Link] that represents a boolean-valued function of one
argument. Its abstract method is: boolean test(T t). Used heavily with [Link](). Predicates can be composed
using and(), or(), negate() methods to build complex conditions. [Link]() (Java 11) creates a negated
predicate. BiPredicate tests two arguments.
Predicate<String> isLong = s -> [Link]() > 4; Predicate<String> startsA = s ->
[Link]("A"); List<String> names = [Link]("Alice","Bob","Andrew","Jo"); // Compose
predicates: [Link]() .filter([Link](startsA)) // long AND starts with A
.forEach([Link]::println); // prints: Alice, Andrew
Q10. What is Consumer?
Consumer is a functional interface in [Link] that represents an operation that accepts one argument and
returns no result (void). Its abstract method is: void accept(T t). Used for operations that have side effects —
printing, saving to DB, sending email. The andThen() method chains multiple consumers. forEach() in Streams and
Collections takes a Consumer. BiConsumer accepts two arguments.
Consumer<String> printer = s -> [Link](s); Consumer<String> upper = s ->
[Link]([Link]()); // Chain consumers: Consumer<String> both =
Java Interview Guide · Page 24
[Link](upper); [Link]("hello"); // prints: hello // HELLO // With forEach:
[Link]("a","b","c").forEach([Link]::println);
Java Interview Guide · Page 25
■ SECTION 11 — Advanced Java
Q1. What are Generics?
Generics enable classes, interfaces, and methods to operate on types as parameters, providing compile-time type
safety and eliminating the need for casting. Syntax: class Box { T value; }. Type parameters (T, E, K, V) are
replaced with actual types at compile time. Benefits: catch type errors at compile time rather than runtime,
eliminate explicit casting, enable writing reusable type-safe code. Collections framework heavily uses Generics —
ArrayList ensures only Strings are stored.
// Without Generics (error-prone): List list = new ArrayList(); [Link]("Hello"); String s =
(String) [Link](0); // cast needed // With Generics (type-safe): List<String> list = new
ArrayList<>(); [Link]("Hello"); String s = [Link](0); // no cast needed // Generic class:
class Pair<K, V> { K key; V value; Pair(K k, V v) { key=k; value=v; } }
Q2. What is Type Erasure?
Type erasure is the process by which the Java compiler removes all generic type information after compilation. At
runtime, generic types like List and List are both just List (they have no type parameter info in the bytecode). The
compiler inserts casts automatically where needed. This maintains backward compatibility with pre-Java 5 code.
Consequence: you cannot create arrays of generic types, use instanceof with generic types, or access the type
parameter T at runtime. This is why [Link] doesn't exist.
// At compile time: List<String> // At runtime (after erasure): just List // This causes type
erasure limitation: // List<String>[] arr = new List<String>[5]; // COMPILE ERROR // You can't
do this at runtime: void method(T item) { // [Link] -- not allowed due to type erasure! }
■ Type erasure = generics are compile-time only; runtime sees raw types.
Q3. What are Annotations?
Annotations are metadata tags that provide additional information about code to the compiler or runtime. They
don't directly affect program execution but are used by compilers and tools. Built-in annotations: @Override
(compiler checks you're actually overriding), @Deprecated (marks outdated code), @SuppressWarnings,
@FunctionalInterface. Meta-annotations define annotation behavior: @Retention (how long annotation info is
kept), @Target (where annotation can be applied), @Inherited, @Documented. Frameworks like Spring and
Hibernate heavily rely on annotations for configuration.
@Override public String toString() { return "Custom"; } // compiler checks @Deprecated void
oldMethod() {} // warns callers it's outdated // Custom annotation: @interface Author { String
name(); String date(); } @Author(name="Alice", date="2024-01-15") class MyClass {}
Q4. What is Reflection API?
The Reflection API ([Link]) allows a program to inspect and manipulate classes, methods, fields, and
constructors at runtime — even private ones. You can load classes dynamically, create instances without knowing
the class at compile time, invoke methods by name, and inspect annotations. Used by frameworks (Spring,
Hibernate, JUnit) to provide dependency injection, ORM mapping, and test discovery. It comes with performance
overhead and breaks encapsulation, so use cautiously.
Class<?> clazz = [Link]("[Link]"); // Get all methods: Method[] methods =
[Link](); for (Method m : methods) [Link]([Link]()); //
Create instance dynamically: Object obj = [Link]().newInstance(); //
Invoke method: Method addMethod = [Link]("add", [Link]); [Link](obj,
"Hello");
Java Interview Guide · Page 26
Q5. What is Serialization?
Serialization is the process of converting an object's state into a byte stream so it can be saved to a file, database,
or transmitted over a network. In Java, a class must implement the [Link] marker interface (no
methods) to be serializable. The JVM uses ObjectOutputStream to write and ObjectInputStream to read. Fields
marked transient are NOT serialized (useful for sensitive data like passwords). The serialVersionUID field is used
to verify version compatibility during deserialization.
class Person implements Serializable { private static final long serialVersionUID = 1L; String
name; transient String password; // NOT serialized } // Serialize: ObjectOutputStream oos = new
ObjectOutputStream( new FileOutputStream("[Link]")); [Link](new Person("Alice",
"secret")); // Deserialize: ObjectInputStream ois = new ObjectInputStream( new
FileInputStream("[Link]")); Person p = (Person) [Link]();
Q6. What is Deserialization?
Deserialization is the reverse of serialization — converting a byte stream back into an object in memory. It reads
the serialized data using ObjectInputStream and reconstructs the object. During deserialization, the constructor is
NOT called — the JVM directly restores the state from the stream. If the class has changed since serialization
(different serialVersionUID), an InvalidClassException is thrown. Deserialization from untrusted sources is a
security risk — it can lead to code injection attacks. Always validate deserialized data.
// Deserialize: try (ObjectInputStream ois = new ObjectInputStream( new
FileInputStream("[Link]"))) { Person p = (Person) [Link]();
[Link]([Link]); // "Alice" [Link]([Link]); // null (transient) }
catch (ClassNotFoundException | IOException e) { [Link](); }
Q7. What is Singleton Design Pattern?
Singleton is a creational design pattern that ensures only ONE instance of a class is ever created, and provides a
global access point to it. Use cases: database connections, logging, configuration managers, thread pools.
Implementation steps: (1) private constructor (prevent external instantiation), (2) private static instance variable, (3)
public static getInstance() method (lazy or eager initialization). Thread-safe singleton uses double-checked locking
or enum.
class Singleton { private static volatile Singleton instance; private Singleton() {} // private
constructor public static Singleton getInstance() { if (instance == null) { synchronized
([Link]) { if (instance == null) // double-check instance = new Singleton(); } }
return instance; } } // Best practice: use enum singleton enum EnumSingleton { INSTANCE; }
Q8. What is Factory Pattern?
Factory Method is a creational design pattern that defines an interface for creating objects but lets subclasses
decide which class to instantiate. It decouples object creation from the client code — the client asks for an object
without knowing the exact class. This promotes loose coupling and easy extensibility (adding new product types
without modifying existing code). The 'factory' is a method/class that returns objects of a common
interface/supertype. Widely used in Java: [Link](), [Link](), JDBC
DriverManager.
interface Shape { void draw(); } class Circle implements Shape { public void draw() {
[Link]("Drawing Circle"); } } class Square implements Shape { public void draw() {
[Link]("Drawing Square"); } } class ShapeFactory { public static Shape
create(String type) { return switch(type) { case "circle" -> new Circle(); case "square" -> new
Square(); default -> throw new IllegalArgumentException(type); }; } } Shape s =
[Link]("circle"); [Link](); // Drawing Circle
Java Interview Guide · Page 27
■ SECTION 12 — Tricky / Coding Questions
Q1. Difference between == and equals()?
'==' checks reference equality — whether two variables point to the same object in memory. For primitives, it
compares values directly. 'equals()' is a method defined in Object, designed to compare logical/content equality.
String, Integer, List override equals() for content comparison. For custom classes, override equals() (and
hashCode()) to define what 'equal' means for your objects. Default equals() in Object class behaves like ==
(reference comparison).
Integer a = 127; Integer b = 127; [Link](a == b); // true (Integer cache) Integer x
= 200; Integer y = 200; [Link](x == y); // false (no cache above 127)
[Link]([Link](y)); // true (same value)
■ Always override both equals() AND hashCode() together (contract requirement).
Q2. Difference between final, finally, and finalize()?
final is a keyword: applied to variables (constant, can't reassign), methods (can't override), or classes (can't
extend). finally is a try-catch block that always executes — used for cleanup (closing streams, releasing
resources), even if an exception is thrown or a return is reached. finalize() is a method in Object class called by the
Garbage Collector just before an object is garbage collected — it's deprecated in Java 9+ and unreliable; use
try-with-resources or Cleaner instead.
// final: final int MAX = 100; // MAX = 200; -- error! // finally: try { riskyOp(); } catch
(Exception e) { handle(); } finally { cleanup(); } // always runs // finalize (deprecated):
@Override protected void finalize() throws Throwable { [Link]("GC about to collect
me"); }
■ final=keyword | finally=block | finalize()=method. Three totally different things!
Q3. Can we overload the main method?
Yes, you can overload the main method in Java — you can define multiple methods named 'main' with different
parameter lists. However, the JVM only uses the standard entry point: public static void main(String[] args) to start
the program. The overloaded main methods are treated as ordinary methods and can only be called explicitly from
code. Overloading main is legal but serves no special purpose in terms of program entry.
class Test { public static void main(String[] args) { [Link]("JVM entry: " +
[Link]); main(42); // calling overloaded version } public static void main(int x) { //
overloaded [Link]("Overloaded main: " + x); } }
Q4. Can we override a static method?
No, static methods cannot be overridden in Java. Static methods belong to the class, not to instances. If a subclass
defines a static method with the same signature as the parent's static method, it is called method hiding (not
overriding). The version called depends on the reference type, not the actual object type — unlike true runtime
polymorphism. So if Animal has a static sound() and Dog redefines it, [Link]() calls Animal's version even if
the variable holds a Dog.
class Animal { static void sound() { [Link]("Animal sound"); } } class Dog extends
Animal { static void sound() { [Link]("Dog sound"); } // hiding } Animal a = new
Dog(); [Link](); // "Animal sound" -- not Dog's (method hiding!) Dog d = new Dog(); [Link]();
// "Dog sound"
■ Static method: HIDING (not overriding). Runtime polymorphism doesn't apply.
Java Interview Guide · Page 28
Q5. Can we override a private method?
No, private methods cannot be overridden. Private methods are not visible to subclasses — they are not inherited.
If a subclass defines a method with the same name and signature as a private method in the parent, it is simply a
new independent method (not an override). There is no runtime polymorphism for private methods. Since they
aren't visible outside the class, they can't participate in method dispatch. @Override annotation would cause a
compile error if used on such a method.
class Parent { private void show() { [Link]("Parent"); } void test() { show(); } //
calls Parent's show() } class Child extends Parent { void show() { [Link]("Child");
} // NEW method } new Child().test(); // still prints "Parent"! // Because [Link]() always
calls Parent's private show()
Q6. Can we inherit a final class?
No, a final class cannot be extended (subclassed). Declaring a class as final prevents inheritance, ensuring the
class behavior cannot be modified by subclassing. This is used for security (String, Integer, all wrapper classes are
final), immutability, and design enforcement. If you try to extend a final class, you get a compile error: 'cannot
inherit from final ClassName'. Similarly, final methods cannot be overridden in subclasses (but they can be
inherited and used).
final class ImmutablePoint { final int x, y; ImmutablePoint(int x, int y) { this.x=x; this.y=y;
} } // class ExtendedPoint extends ImmutablePoint {} // COMPILE ERROR! // String is final: //
class MyString extends String {} // COMPILE ERROR!
Q7. What happens when an object becomes null?
When an object becomes null (no references point to it), it becomes eligible for garbage collection. The object still
exists in memory until the GC runs and reclaims the memory. Setting a reference to null does NOT immediately
destroy the object. If multiple references point to an object, the object only becomes eligible for GC when ALL
references are null. Accessing a null reference throws NullPointerException. You can hint for GC using
[Link]() but it's not guaranteed to run.
MyObject obj = new MyObject(); // created in heap obj = null; // reference removed; object is
now GC eligible // But: MyObject a = new MyObject(); MyObject b = a; // both point to same
object a = null; // b still holds reference, NOT GC eligible yet b = null; // NOW it's GC
eligible
■ GC collects objects only when no reference (root-reachable) points to them.
Q8. Difference between Heap and Stack memory?
Stack is a thread-specific, LIFO structure storing method call frames, local variables, and references. Each thread
has its own stack. Memory is automatically allocated and freed as methods are called/return. Stack size is smaller
and has faster access. StackOverflowError occurs when it's full (deep recursion). Heap is the shared memory area
where all objects are stored. Memory is managed by the Garbage Collector. Heap has more space but slower
access. OutOfMemoryError occurs when heap is exhausted.
void method() { int x = 10; // x stored in STACK (local var) String s = "Hi"; // s (reference)
in STACK // "Hi" object in HEAP MyObj obj = new MyObj(); // obj reference in STACK // MyObj
object in HEAP } // x, s, obj references popped from stack when method exits
■ Stack = fast, small, automatic | Heap = large, shared, GC-managed.
Q9. What is Garbage Collection?
Garbage Collection (GC) is the automatic process of reclaiming heap memory occupied by objects that are no
longer referenced by any part of the program. Java's GC eliminates manual memory management (no
free()/delete). The GC runs in the background, identifies unreachable objects, and frees their memory. JVM has
several GC algorithms: Serial GC, Parallel GC, G1 GC (default in Java 9+), ZGC, Shenandoah. Objects go through
generations (Young, Old, Permanent/Metaspace) to optimize GC performance.
Java Interview Guide · Page 29
[Link](); // suggests GC to run (not guaranteed) // Objects become GC-eligible: MyObject obj
= new MyObject(); obj = null; // eligible for GC // Or when out of scope: void method() {
MyObject temp = new MyObject(); } // temp out of scope --> GC eligible
■ finalize() is called before GC — but it's deprecated in Java 9+. Use Cleaner.
Q10. What is Object Cloning?
Object cloning creates an exact copy of an object. To clone an object in Java, the class must implement the
Cloneable marker interface and override [Link]() method. The default clone() performs a shallow copy — it
copies field values directly. For object fields, only the reference is copied (not the object itself). Deep copy requires
manually copying all nested objects. CloneNotSupportedException is thrown if Cloneable is not implemented.
Alternative: copy constructors or serialization-based deep copy.
class Person implements Cloneable { String name; int[] scores; @Override protected Object
clone() throws CloneNotSupportedException { Person p = (Person) [Link](); // shallow copy
[Link] = [Link](); // deep copy array return p; } } Person p1 = new Person(); Person p2
= (Person) [Link]();
Q11. Shallow Copy vs Deep Copy?
Shallow copy creates a new object and copies field values — for primitive fields, values are copied; for reference
fields, only the reference (memory address) is copied, not the referenced object itself. Both original and copy share
the same nested objects. Modifying a nested object through the copy affects the original. Deep copy creates a new
object and recursively copies all nested objects as well — original and copy are completely independent. Deep
copy is more expensive but safer when nested mutable objects exist.
// Shallow copy -- shares nested object: Address addr = new Address("NYC"); Person p1 = new
Person("Alice", addr); Person p2 = (Person) [Link](); // shallow [Link] = "LA"; //
affects p1 too! // Deep copy -- fully independent: Person p3 = new Person([Link], new
Address([Link])); [Link] = "Paris"; // does NOT affect p1
■ For safety with mutable nested objects, always use deep copy.
Q12. Why is Java not 100% object-oriented?
Java is not purely object-oriented because it has 8 primitive data types (int, char, boolean, byte, short, long, float,
double) that are NOT objects. They don't inherit from Object, have no methods, and don't participate in OOP
features. A purely OO language (like Smalltalk or Ruby) treats everything as an object. Additionally, Java has static
methods and variables that belong to the class rather than instances, which is not purely OOP. Wrapper classes
(Integer, Character, etc.) provide object wrappers for primitives, but the primitives themselves are not objects.
int x = 5; // NOT an object -- primitive! Integer y = 5; // IS an object -- wrapper class //
Primitives have NO methods: // [Link]() -- compile error! // Wrapper has methods: String s
= [Link](); // "5" // Autoboxing bridges the gap: Integer z = x; // autoboxing: int ->
Integer automatically
■ 8 primitives + static = Java is OOP, not purely OOP.
Q13. Difference between final, finally, and finalize()? (Summary)
This is a top interview question. final is a non-access modifier keyword with three uses: (1) final variable = constant
(can't be reassigned), (2) final method = can't be overridden in subclass, (3) final class = can't be extended. finally
is a code block in exception handling that ALWAYS executes after try/catch (for guaranteed cleanup). finalize() is
an instance method in [Link] that the GC calls just before collecting an object. It's deprecated since Java
9 — use try-with-resources or [Link] for cleanup.
final int LIMIT = 100; // final variable final class String { } // final class (can't extend)
try { process(); } catch (Exception e) { log(e); } finally { [Link](); } // always
executes @Deprecated protected void finalize() { // called before GC [Link](); }
Java Interview Guide · Page 30
Q14. Why are Strings immutable in Java? (Deep Dive)
String immutability in Java is a deliberate design decision with multiple benefits: (1) String Pool — the JVM can
safely cache and reuse String literals because no one can modify them; sharing is safe. (2) Security — Strings are
used as parameters for network connections, file paths, class loading, and database queries; mutability would be a
security hole. (3) Thread Safety — immutable objects are automatically thread-safe; multiple threads can share a
String without synchronization. (4) Consistent HashCode — String's hashCode is computed once and cached;
since value never changes, HashMap performance is reliable. (5) Class Loading — class names passed to
ClassLoader are Strings; mutability could allow class-switching attacks.
String s = "Hello"; [Link](" World"); // returns NEW String, original unchanged
[Link](s); // still "Hello" String result = [Link](" World");
[Link](result); // "Hello World" // Internally, String stores chars in a private
final char[]: // private final char[] value; -- final + private = immutable
■ String immutability is not accidental — it serves security, performance, and safety goals.
Java Interview Guide · Page 31
■ You've Got This! ■
✔ Practice coding every question — reading is not enough.
✔ Explain answers in simple words — interviewers love clarity.
✔ Always link OOP answers to real-world examples.
✔ For Collections, know internal implementations (HashMap especially).
✔ Java 8 features (Lambda, Streams) are asked in EVERY modern interview.
✔ Know the difference between == and .equals() — it's a classic trap.
Best of luck with your Java Interview! ■
Java Interview Guide · Page 32