Core Java Interview Questions: Complete Guide with Answers and Examples

Search for a command to run...

No comments yet. Be the first to comment.
A step-by-step developer's guide to base values, positional notation, and number conversions with practical examples in Java.

Create professional invoices with PDF and PNG export, built-in UPI payment QR codes, multiple workspaces, and complete local privacy. No signup required.

The clean, body-backed middle ground between GET and POST has finally arrived.

What is it, why it breaks, how to fix it, and when you actually need it in production.

Everything Developers Need to Know About Scrum, Kanban, and Surviving the Tech Industry

Ashutosh Writes
112 posts
Ashutosh Writes is a tech-focused blog where practical learning connects with real-world development. It features clear, engaging articles on web development, Python, Java, artificial intelligence, and modern software engineering. From hands-on project tutorials and coding guides to AI concepts and development insights, the blog is designed to simplify complex topics and help developers learn, build, and grow at every stage of their journey.
On this page
Mastering Core Java is essential for cracking technical interviews at top tech companies. Whether you’re preparing for coding rounds, design discussions, or scenario-based questions, having a solid grasp of Java fundamentals, memory management, multithreading, collections, and design patterns can set you apart.
This comprehensive guide covers all the essential Core Java interview questions in 2025, with clear explanations, comparisons, and code examples. From OOP principles to Java 8 features, JVM internals, and practical real-world scenarios, you’ll get a complete overview of the concepts that interviewers frequently test.
Use this guide to boost your Java knowledge, strengthen problem-solving skills, and confidently tackle interviews, making sure you’re well-prepared for both theoretical and hands-on coding rounds.
OOP (Object-Oriented Programming) is based on four main principles:
Encapsulation – Bundling data and methods that operate on that data into a single unit (class).
Example: Private fields with public getters/setters.
Abstraction – Hiding internal implementation details and exposing only essential features.
Example: Abstract classes and interfaces.
Inheritance – Reusing properties and methods from an existing class into a new class.
Example: class Dog extends Animal
Polymorphism – The ability of objects to take multiple forms.
Example: Method overriding where the same method behaves differently in different subclasses.
These principles together make code modular, extensible, and maintainable.
| Feature | Abstraction | Encapsulation |
| Purpose | Focuses on hiding implementation details | Focuses on bundling data and behavior |
| Achieved By | Abstract classes and interfaces | Access modifiers (private, public, etc.) |
| Concerned With | Design level | Implementation level |
| Example | Hiding database connection details behind an interface | Making class variables private and exposing getters/setters |
In short:
Abstraction hides what is done, Encapsulation hides how it’s done.
Inheritance allows one class to inherit properties and behaviors from another, promoting code reuse.
In Java:
class Parent {
void greet() { System.out.println("Hello!"); }
}
class Child extends Parent {
void greetChild() { System.out.println("Hi from Child!"); }
}
Here, Child inherits the greet() method from Parent.
Types of inheritance in Java:
Single
Multilevel
Hierarchical
Note: Java does not support multiple inheritance with classes to avoid ambiguity, but supports it with interfaces.
Polymorphism allows an object to behave differently based on context.
Types:
Compile-time polymorphism (Method Overloading):
Same method name, different parameter lists.
Resolved at compile-time.
void show(int a) {}
void show(String b) {}
Runtime polymorphism (Method Overriding):
Subclass provides a new implementation of a parent method.
Resolved at runtime using dynamic method dispatch.
Polymorphism improves flexibility and scalability in OOP systems.
| Feature | Method Overloading | Method Overriding |
| When | Compile-time | Runtime |
| Class Relationship | Same class | Parent-child classes |
| Parameters | Must differ in type/number/order | Must be same |
| Return Type | Can differ | Must be same or covariant |
| Static Binding | Yes | No (uses dynamic binding) |
Example:
// Overloading
void add(int a, int b) {}
void add(double a, double b) {}
// Overriding
class A {
void show() {}
}
class B extends A {
void show() {}
}
No.
Static methods are bound at compile-time (using class reference), not runtime (using object).
If a subclass defines a static method with the same name as the parent class’s static method, it hides the parent method — it’s not overriding.
class Parent {
static void greet() {}
}
class Child extends Parent {
static void greet() {}
} // Method hiding
No.
Constructors are not inherited, so overriding doesn’t apply.
However, constructors can be overloaded — a class can have multiple constructors with different parameter lists.
class Person {
Person() {}
Person(String name) {}
}
Interfaces are preferred when you want to define a contract that multiple unrelated classes can implement.
Key reasons:
Java supports multiple interface inheritance, but not multiple class inheritance.
Promotes loose coupling — implementation can vary independently.
Ideal for defining APIs, services, or capabilities (e.g., Comparable, Runnable).
Example:
A class can implements Serializable, Comparable but can only extends one abstract class.
| Feature | Interface | Abstract Class |
| Inheritance | Multiple allowed | Single allowed |
| Contains | Abstract methods + constants (Java 8+: default & static methods too) | Can have abstract & concrete methods |
| Variables | public static final by default | Can be instance variables |
| Constructor | Not allowed | Allowed |
| Use Case | When defining behavior/contract | When defining base class with partial implementation |
In modern Java, interfaces are often used to define capabilities, while abstract classes define common base logic.
No, both cannot be instantiated directly.
They serve as blueprints for subclasses or implementing classes.
However, you can create:
Runnable r = new Runnable() {
public void run() { System.out.println("Running..."); }
};
Here, an anonymous implementation of the interface is created.
The JVM (Java Virtual Machine) is an abstract machine that executes Java bytecode.
It provides a runtime environment for Java applications and is platform-dependent.
Key responsibilities:
Loading: Uses the ClassLoader to load class files.
Verifying: Ensures bytecode security.
Executing: Uses the Just-In-Time (JIT) compiler to convert bytecode to native code.
Memory Management: Allocates and manages heap, stack, and garbage collection.
In short:
👉 Java code → compiled to bytecode → executed by JVM → runs on any OS (platform independence).
| Component | Full Form | Purpose |
| JVM | Java Virtual Machine | Executes Java bytecode |
| JRE | Java Runtime Environment | Contains JVM + libraries required to run Java apps |
| JDK | Java Development Kit | Contains JRE + development tools (compiler, debugger, etc.) |
Relationship:
👉 JDK = JRE + development tools
👉 JRE = JVM + libraries
main() is the entry point of any standalone Java application.
Signature:
public static void main(String[] args)
Breakdown:
public: Accessible by JVM.
static: JVM can call it without creating an object.
void: Doesn’t return any value.
String[] args: Accepts command-line arguments.
Without main(), the program doesn’t have a starting point.
If main() is not static, the JVM cannot invoke it directly because no object of the class exists yet.
This leads to a runtime error like:
Error: Main method is not static in class MainClass
JVM needs main() to be static so that it can call it without object instantiation.
In modern Java (Java 7+), no — every standalone program needs a main() method.
Earlier (in applets or servlet containers), Java classes could be run without main() because:
Applets start with the init() method.
Servlets are loaded and managed by a container (init(), service(), destroy()).
For normal console applications → main() is mandatory.
The Java Memory Model (JMM) defines how Java threads interact through memory — how variables are read/written, and how visibility is ensured in concurrent execution.
It divides memory into:
Heap: Stores objects and their instance variables (shared across threads).
Stack: Each thread has its own stack storing method calls and local variables.
Method Area (Metaspace in Java 8+): Stores class-level metadata like method definitions and constant pool.
PC Register: Holds address of current executing instruction.
Native Method Stack: Used for native (non-Java) code execution.
The JMM ensures visibility, ordering, and atomicity across threads using keywords like volatile, synchronized, and final.
| Aspect | Heap | Stack |
| Stores | Objects, instance variables | Local variables, method calls |
| Shared? | Shared among all threads | Each thread has its own stack |
| Lifetime | Exists until object is garbage collected | Exists until method completes |
| Managed By | Garbage Collector | JVM automatically manages |
| Access Speed | Slower | Faster |
Example:
int x = 10; // stored in stack
Person p = new Person(); // object p in heap, reference in stack
PermGen (Permanent Generation):
Used before Java 8 to store class metadata, static variables, and interned strings.
Had a fixed size, which could cause OutOfMemoryError: PermGen space.
Metaspace (Java 8+):
Replaced PermGen. It stores class metadata in native memory (not heap).
Grows dynamically as needed, reducing memory errors.
✅ In short: Metaspace is a more flexible, dynamic replacement for PermGen.
Garbage Collection (GC) is an automatic memory management process that reclaims memory from objects no longer reachable by any reference.
How it works:
JVM identifies unreachable objects (not referenced anywhere).
GC frees that memory space.
Memory is reused for new objects.
GC Algorithms:
Serial GC (single-threaded, small apps)
Parallel GC (multi-threaded)
G1 GC (Garbage First) – default in Java 9+, low pause time collector.
Phases:
Mark: Identify live objects.
Sweep/Compact: Remove dead objects and defragment memory.
Developers can trigger GC via System.gc(), but JVM decides the actual execution time.
finalize() is a method defined in the Object class:
protected void finalize() throws Throwable
It’s called by the Garbage Collector before reclaiming an object’s memory — a last chance to release resources.
However:
Execution is not guaranteed or timely.
It might never be called if the program exits before GC runs.
✅ Since Java 9, finalize() is deprecated.
Modern alternatives:
Use try-with-resources for closing streams.
Implement AutoCloseable for cleanup logic.
Java provides four access levels to control visibility of classes, methods, and variables.
| Modifier | Within Class | Within Package | Subclass (Other Package) | Outside Package |
| private | ✅ | ❌ | ❌ | ❌ |
| default (no modifier) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
Summary:
private → Most restrictive; used for encapsulation.
default → Accessible within the same package.
protected → Visible to subclasses even if they’re in different packages.
public → Accessible from anywhere.
Example:
public class Person {
private int age;
protected String name;
public void greet() {}
}
Top-level (outer) classes cannot be private — they must be either public or default.
However, inner classes can be private.
Example:
class Outer {
private class Inner { // ✅ allowed
void show() { System.out.println("Inner"); }
}
}
If a top-level class were private, no other class (even in the same package) could access it — defeating the purpose of reusability.
A static block in Java is used for class-level initialization.
It runs once, when the class is first loaded into memory (before any constructor or object creation).
Example:
class DatabaseConnection {
static {
System.out.println("Initializing DB connection...");
}
}
Use cases:
Initialize static variables.
Load configurations.
Perform one-time setup (e.g., registering JDBC drivers).
Order of execution:
Static variables.
Static blocks (in order of appearance).
Constructors (when object is created).
| Type | Checked Exception | Unchecked Exception |
| Inheritance | Subclass of Exception (excluding RuntimeException) | Subclass of RuntimeException |
| Checked at | Compile-time | Runtime |
| Handling Required? | Must be handled or declared using throws | Optional |
| Examples | IOException, SQLException, FileNotFoundException | NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException |
Key point:
Checked exceptions represent recoverable errors, while unchecked ones indicate programming bugs (e.g., null access).
| Keyword | throw | throws |
| Purpose | Used to actually throw an exception | Declares that a method may throw exceptions |
| Usage Place | Inside method body | In method signature |
| Follows | Single exception instance | One or more exception classes |
| Example | throw new IOException("File not found"); | void readFile() throws IOException {} |
Example:
void readFile() throws IOException {
throw new IOException("Error reading file");
}
No ❌
A try block must be followed by at least one of the following:
A catch block
A finally block
However, try-finally without catch is valid:
try {
System.out.println("Try block");
} finally {
System.out.println("Cleanup code");
}
This ensures cleanup executes even if no exception occurs.
The finally block is used for resource cleanup — it executes always, regardless of whether an exception occurs or not.
Example:
try {
FileInputStream fis = new FileInputStream("data.txt");
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("Closing resources...");
}
✅ Executed:
After try or catch block
Even if return is used inside try or catch
❌ Not executed if JVM exits via System.exit(0) or power failure.
Modern alternative:
Use try-with-resources (Java 7+) to auto-close resources.
A custom exception allows developers to define domain-specific error types.
Steps to create:
Extend Exception (for checked) or RuntimeException (for unchecked).
Add constructors for custom messages.
Example:
class InvalidAgeException extends Exception {
public InvalidAgeException(String msg) {
super(msg);
}
}
class Validator {
void validate(int age) throws InvalidAgeException {
if (age < 18)
throw new InvalidAgeException("Age must be >= 18");
}
}
Use cases:
Validation (e.g., invalid user input)
Business logic constraints
Domain-specific error reporting (e.g., InsufficientBalanceException)
| Feature | List | Set | Map |
| Stores | Ordered collection of elements | Unique elements (no duplicates) | Key-value pairs |
| Duplicates | Allowed | Not allowed | Keys: not allowed, Values: allowed |
| Order | Maintains insertion order | Depends on implementation | Depends on implementation |
| Implementations | ArrayList, LinkedList, Vector | HashSet, LinkedHashSet, TreeSet | HashMap, TreeMap, LinkedHashMap |
Example:
List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
Map<Integer, String> map = new HashMap<>();
| Aspect | ArrayList | LinkedList |
| Data Structure | Dynamic array | Doubly linked list |
| Access Time | O(1) for index-based access | O(n) traversal required |
| Insertion/Deletion (middle) | O(n) | O(1) if node reference known |
| Memory Usage | Less (compact) | More (extra node references) |
| Use Case | Random access | Frequent insertions/deletions |
Example:
Use ArrayList when reads are frequent.
Use LinkedList when insertions/removals are frequent.
| Feature | HashMap | Hashtable |
| Thread-Safety | Not synchronized | Synchronized |
| Null Keys/Values | Allows one null key, multiple null values | Doesn’t allow nulls |
| Performance | Faster (no locking) | Slower (locks entire table) |
| Introduced In | Java 1.2 | Java 1.0 |
| Preferred? | Yes, in modern Java | Legacy class |
✅ For thread-safe alternatives, use ConcurrentHashMap instead of Hashtable.
HashMap stores data in buckets using a hashing mechanism.
Process:
Key’s hashCode() is computed.
The hash is mapped to an index in the bucket array ((n - 1) & hash).
Each bucket holds a linked list or tree (Java 8+).
On put():
If key exists → value replaced.
If not → new node added.
On get():
equals().Optimization (Java 8+):
Load Factor: Defines how full the HashMap can get before resizing (default = 0.75).
Threshold: capacity × loadFactor.
When the number of entries exceeds this threshold, HashMap resizes (doubles its capacity).
Example:
HashMap<String, Integer> map = new HashMap<>(16, 0.75f);
✅ Resizing improves performance but comes with a cost — it’s best to initialize maps with an estimated size to minimize rehashing.
| Type | Behavior | Example Collections |
| Fail-Fast | Throws ConcurrentModificationException if the collection is modified while iterating | ArrayList, HashMap |
| Fail-Safe | Works on a clone or snapshot of the collection | ConcurrentHashMap, CopyOnWriteArrayList |
Example:
for (Integer i : list) {
list.add(10); // ❌ throws ConcurrentModificationException
}
✅ Use fail-safe collections in concurrent environments.
ConcurrentHashMap is a thread-safe alternative to HashMap that provides high concurrency with minimal locking.
How it differs:
No full-table lock (uses segment-level locking or CAS in Java 8+).
Null keys/values are not allowed.
Iterators are fail-safe (operate on snapshot).
Performs better than Hashtable under multithreading.
Example:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
| Feature | HashMap | TreeMap |
| Order | No ordering | Sorted (natural/comparator) |
| Implementation | Hash table | Red-Black tree |
| Null Keys | Allows one | Doesn’t allow null key |
| Performance | O(1) average | O(log n) |
| Use Case | Fast lookups | Sorted data retrieval |
Example:
Map<Integer, String> map = new TreeMap<>();
map.put(2, "B");
map.put(1, "A"); // Automatically sorted by key
LinkedHashMap maintains insertion order or access order of entries.
Internally:
It extends HashMap and adds a doubly-linked list to preserve order.
Example:
Map<Integer, String> map = new LinkedHashMap<>();
map.put(1, "A");
map.put(2, "B");
✅ Useful when you need predictable iteration order or LRU caching (using removeEldestEntry).
| Feature | ArrayList | Vector |
| Synchronization | Not synchronized | Synchronized |
| Performance | Faster | Slower (locks every method) |
| Introduced In | Java 1.2 | Java 1.0 (legacy) |
| Growth | Grows by 50% | Grows by 100% (doubles) |
✅ Modern Java avoids Vector.
Use ArrayList for single-threaded cases, or Collections.synchronizedList() if synchronization is needed.
| Feature | Process | Thread |
| Definition | Independent program in execution | Smallest unit of a process |
| Memory | Has its own memory space | Shares memory with other threads of same process |
| Communication | Inter-process communication is complex | Easier via shared objects |
| Failure Impact | One process crash doesn’t affect others | Thread crash may affect whole process |
| Example | Running two Java programs | Two threads in same Java program |
In short: Threads share the same heap, enabling lightweight multitasking within a single process.
There are three main ways:
Extend Thread class
class MyThread extends Thread {
public void run() {
System.out.println("Thread running...");
}
}
new MyThread().start();
Implement Runnable interface
class MyTask implements Runnable {
public void run() {
System.out.println("Running via Runnable");
}
}
new Thread(new MyTask()).start();
Use ExecutorService or Callable (preferred in modern Java)
ExecutorService service = Executors.newFixedThreadPool(2);
service.submit(() -> System.out.println("Running in pool"));
✅ Best practice: Use ExecutorService to manage threads efficiently.
| Feature | Thread | Runnable |
| Inheritance | Requires extending Thread | Can be implemented along with other interfaces |
| Code Reuse | Less flexible | More flexible (no multiple inheritance issue) |
| Preferred | For small/simple use | Runnable preferred for real-world apps |
Example:
If your class already extends another class, use Runnable since Java doesn’t support multiple inheritance.
A thread in Java passes through these five states:
New – Created but not started (new Thread()).
Runnable – Ready to run, waiting for CPU (start() called).
Running – Currently executing.
Blocked/Waiting – Waiting for a resource or another thread.
Terminated – Execution completed or stopped.
Example:
Thread t = new Thread(() -> {});
t.start(); // Runnable
| Method | Defined In | Releases Lock? | Purpose |
| wait() | Object class | ✅ Yes | Waits until notified (notify() / notifyAll()) |
| sleep() | Thread class | ❌ No | Pauses execution for given time |
| join() | Thread class | ❌ No | Waits for another thread to finish |
Example:
Thread t = new Thread(() -> System.out.println("Task"));
t.start();
t.join(); // waits for t to finish
The synchronized keyword ensures mutual exclusion — only one thread can access a block/method at a time for a given object.
Types:
Synchronized method
synchronized void increment() {
count++;
}
Synchronized block
void increment() {
synchronized(this) {
count++;
}
}
Benefits: Prevents race conditions.
Downside: Can reduce performance due to locking.
A deadlock occurs when two or more threads are waiting for each other’s locks indefinitely.
Example:
synchronized (obj1) {
synchronized (obj2) { ... }
}
Another thread might lock obj2 first, then wait for obj1.
Prevention:
Acquire locks in a consistent order.
Use tryLock() with timeout (ReentrantLock).
Avoid nested locks where possible.
The volatile keyword ensures that a variable’s value is always read from main memory, not from a thread’s local cache.
Example:
volatile boolean flag = true;
If one thread changes flag, other threads immediately see the updated value.
Important:
Ensures visibility, not atomicity.
For compound operations (e.g., count++), use synchronization or AtomicInteger.
| Aspect | Thread | ExecutorService |
| Creation | Manually create and start threads | Manages thread pool automatically |
| Reusability | One-time use | Threads are reused |
| Performance | Higher overhead | More efficient for multiple tasks |
| Introduced | Java 1.0 | Java 5 (java.util.concurrent) |
Example:
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(() -> System.out.println("Task executed"));
executor.shutdown();
✅ Best practice: Always use ExecutorService for managing multiple threads efficiently.
Thread safety means an object or code segment behaves correctly when accessed by multiple threads simultaneously.
Ways to achieve:
Use immutable objects
Use synchronized blocks
Use atomic classes (AtomicInteger, AtomicReference)
Use concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList)
Example:
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
✅ Thread-safe code avoids race conditions and data inconsistency.
Java 8 introduced functional programming concepts and several performance improvements.
Key Features:
Lambda Expressions
Functional Interfaces
Stream API
Optional class
Method References
Default and Static methods in Interfaces
Date and Time API (java.time)
Parallel Streams
A lambda expression provides a concise way to represent anonymous functions.
Syntax:
(parameter) -> expression
Example:
List<String> names = Arrays.asList("John", "Jane", "Max");
names.forEach(name -> System.out.println(name));
Before Java 8:
for (String name : names)
System.out.println(name);
✅ Advantages:
Reduces boilerplate code
Enables functional programming
Works well with Streams and Collections
A functional interface is an interface that contains exactly one abstract method.
It can have default or static methods as well.
Example:
@FunctionalInterface
interface Calculator {
int add(int a, int b);
}
Built-in Functional Interfaces (in java.util.function):
Predicate<T> → returns boolean
Function<T, R> → transforms T to R
Consumer<T> → accepts and performs action
Supplier<T> → returns value without input
Example:
Predicate<Integer> isEven = x -> x % 2 == 0;
System.out.println(isEven.test(4)); // true
The Stream API is used to process collections of data in a declarative and functional style.
Example:
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = nums.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(squares);
Key Operations:
Intermediate → filter(), map(), sorted()
Terminal → collect(), forEach(), count(), reduce()
✅ Streams don’t modify the original collection.
| Method | Purpose | Example |
| map() | Transforms each element | [1,2,3] → [1,4,9] |
| flatMap() | Flattens nested streams | [[1,2],[3,4]] → [1,2,3,4] |
Example:
List<List<Integer>> list = Arrays.asList(Arrays.asList(1,2), Arrays.asList(3,4));
list.stream().flatMap(Collection::stream).forEach(System.out::println);
Optional<T> is a container that may or may not hold a non-null value.
It helps avoid NullPointerException.
Example:
Optional<String> name = Optional.ofNullable(getName());
System.out.println(name.orElse("Unknown"));
Common methods:
isPresent()
orElse()
orElseGet()
orElseThrow()
map() and flatMap()
✅ Always use Optional for return types, not fields.
Method references provide a shorthand for calling existing methods using ::.
Example:
list.forEach(System.out::println);
Types:
Static method → ClassName::staticMethod
Instance method → object::instanceMethod
Constructor → ClassName::new
Example:
Supplier<List<String>> supplier = ArrayList::new;
Default methods: Provide a method body inside an interface.
Example:
interface Vehicle {
default void start() {
System.out.println("Vehicle started");
}
}
Static methods in interfaces belong to the interface itself:
interface Utils {
static void show() {
System.out.println("Static method in interface");
}
}
✅ These features were introduced to maintain backward compatibility when new methods were added to interfaces like List and Map.
Java 8 introduced a modern, immutable, thread-safe Date-Time API.
Key Classes:
LocalDate, LocalTime, LocalDateTime
ZonedDateTime
Period, Duration
DateTimeFormatter
Example:
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
System.out.println(tomorrow);
✅ No more java.util.Date and SimpleDateFormat pain!
Parallel streams allow data to be processed in multiple threads automatically.
Example:
list.parallelStream()
.filter(x -> x > 10)
.forEach(System.out::println);
Note:
Use parallel streams for CPU-intensive operations.
Avoid for IO-bound or small datasets (due to overhead).
| Feature | String | StringBuilder | StringBuffer |
| Mutability | Immutable | Mutable | Mutable |
| Thread-safety | Not thread-safe | Not thread-safe | Thread-safe (synchronized) |
| Performance | Slower (creates new objects) | Faster (no synchronization) | Slightly slower (synchronization overhead) |
| Use Case | When data doesn’t change | Single-threaded string manipulations | Multi-threaded string manipulations |
Example:
String s = "Hello";
s.concat(" World"); // New object created
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // Modified in place
✅ For most use cases → prefer StringBuilder (fast, flexible).
Strings are stored in a special area of heap memory called the String Constant Pool (SCP).
When you create a string literal, e.g., "Java", it’s interned — meaning:
If "Java" already exists in the pool, the same reference is reused.
If not, it’s added to the pool.
Example:
String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2); // true (same reference)
✅ String literals are interned;new String("Java") creates a new object on the heap (not in SCP).
The String Constant Pool (SCP) is part of the heap memory (since Java 7+).
It’s a cache that stores unique string literals to improve performance and save memory.
Example:
String a = "Test";
String b = "Test";
System.out.println(a == b); // true (both refer to same object)
If created using new:
String c = new String("Test");
System.out.println(a == c); // false (different objects)
✅ You can manually add strings to the pool using intern():
String d = c.intern();
System.out.println(a == d); // true
Reasons:
Security: Used in sensitive contexts (ClassLoader, File paths, URLs).
Thread-safety: Immutable objects can be shared safely between threads.
Caching: Hash code of a string is cached; immutability ensures it’s consistent.
String Pool Optimization: Same string literals can be reused safely.
Example:
String s = "Java";
s.concat(" Rocks"); // creates a new string
✅ Once created, the value of a String object cannot be changed.
The equals() method compares the contents (values) of two strings,
while the == operator compares references.
Example:
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1 == s2); // false (different objects)
System.out.println(s1.equals(s2)); // true (same content)
✅ String overrides equals() and hashCode() from Object class to compare actual text.
hashCode() work in Strings?The hash code for a string is computed based on its characters:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
Because strings are immutable, their hash code is cached for performance.
✅ Used extensively in collections like HashMap and HashSet.
substring(), split(), and join() work?substring(begin, end)
Returns part of the string.
Creates a new String (does not modify original).
Example:
String s = "developer";
System.out.println(s.substring(0, 3)); // "dev"
split(regex)
String s = "a,b,c";
String[] arr = s.split(",");
String.join(delimiter, elements...)
String result = String.join("-", "Java", "Python", "C++");
System.out.println(result); // "Java-Python-C++"
Interning ensures that identical strings share the same reference in the String Pool.
Example:
String s1 = new String("Java");
String s2 = s1.intern();
String s3 = "Java";
System.out.println(s2 == s3); // true
✅ Improves memory efficiency, especially when there are many repeated strings.
append() – concatenates data
insert() – inserts at position
delete() – removes substring
reverse() – reverses content
capacity() – returns current buffer capacity
Example:
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World").reverse();
System.out.println(sb); // "dlroW olleH"
Not directly.
However, you can simulate mutability using:
StringBuilder or StringBuffer
Reflection (not recommended)
Creating a wrapper class with a mutable reference internally
| Keyword | Purpose | Example |
| final | Restricts modification | final int x = 10; or final class MyClass |
| finally | Block to execute after try/catch | try {…} finally {…} |
| finalize() | Called by GC before object is destroyed | protected void finalize() {…} |
Summary:
final → compile-time constant, prevents inheritance/overriding
finally → runtime block for cleanup
finalize() → deprecated method, used for cleanup before garbage collection
Static members belong to the class, not instances.
Static variable: Shared across all objects
Static method: Can be called without an object
Static block: Executes once at class loading
Example:
class Demo {
static int count;
static { System.out.println("Class loaded"); }
static void show() { System.out.println("Static method"); }
}
Demo.show();
System.out.println(Demo.count);
✅ Use static for memory efficiency and shared state.
Transient is a keyword used in serialization.
Fields marked as transient are not serialized.
Example:
class User implements Serializable {
private String name;
private transient String password; // won’t be saved
}
✅ Useful for sensitive data like passwords or temporary fields.
Ensures visibility of changes across threads.
Guarantees that reads/writes go directly to main memory.
Does not guarantee atomicity.
Example:
volatile boolean running = true;
while (running) {
/* do work */
}
Without volatile, one thread may never see the updated value.
| Keyword | Purpose | Example |
| this | Refers to current object | this.name = name; |
| super | Refers to parent class | super.toString(); |
this() → calls current class constructor
super() → calls parent class constructor
Both must be the first statement in constructor when used
abstract: Cannot instantiate class, may contain abstract methods
synchronized: Ensures mutual exclusion for threads
native: Method implemented in platform-specific code (C/C++)
strictfp: Ensures floating-point calculations are platform-independent
default (in interfaces): Provides default method implementation
var (Java 10+): Type inference for local variables
An inner class is a class defined within another class.
They allow logical grouping of classes and access to private members of the outer class.
Types of inner classes:
Member (non-static) inner class
Static nested class
Local inner class
Anonymous inner class
A static nested class is declared with the static keyword.
Unlike member inner classes, it does not have access to instance variables of the outer class.
Can be instantiated without an object of the outer class.
Example:
class Outer {
static class Nested {
void show() { System.out.println("Static nested class"); }
}
}
Outer.Nested nested = new Outer.Nested();
nested.show();
✅ Useful for grouping classes logically and reducing namespace pollution.
| Type | Definition | Scope | Example |
| Member inner class | Defined at class level (non-static) | Can access outer class members | class Outer { class Inner {} } |
| Static nested class | Defined with static keyword | Only static members of outer class | Outer.Nested nested = new Outer.Nested(); |
| Local inner class | Defined inside a method | Only visible within method | void method() { class Local {} } |
| Anonymous inner class | No class name, used for instant implementation | Usually for implementing interfaces or extending classes | Runnable r = new Runnable() { public void run() {} }; |
Key points:
Member inner classes hold a reference to outer class.
Static nested classes don’t hold outer class reference, so memory footprint is smaller.
Anonymous classes are great for event handlers and callbacks.
Example of anonymous inner class:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Anonymous Runnable");
}
};
new Thread(r).start();
When a class is only relevant to its outer class.
For callbacks, listeners, or adapters.
To encapsulate helper classes without exposing them publicly.
To access outer class private members without getters/setters.
✅ Correct use of inner classes improves code readability, encapsulation, and design.
SOLID is an acronym for five design principles that make code more maintainable, scalable, and testable:
| Principle | Description |
| S – Single Responsibility Principle (SRP) | A class should have only one reason to change. Each class should focus on a single functionality. |
| O – Open/Closed Principle (OCP) | Classes should be open for extension, closed for modification. |
| L – Liskov Substitution Principle (LSP) | Subclasses should be substitutable for their parent classes without affecting program correctness. |
| I – Interface Segregation Principle (ISP) | Clients should not be forced to depend on methods they don’t use. Prefer multiple small interfaces over one large interface. |
| D – Dependency Inversion Principle (DIP) | High-level modules should not depend on low-level modules. Both should depend on abstractions. |
✅ These principles are key to writing clean and scalable Java applications.
The Singleton pattern ensures a class has only one instance and provides a global access point.
Implementation (Thread-safe, lazy initialization):
class Singleton {
private static volatile Singleton instance;
private Singleton() {} // private constructor
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
✅ Use cases:
Logger
Configuration manager
Thread pool manager
Steps to make a class immutable:
Declare class as final.
Make all fields private and final.
No setters.
Return deep copies of mutable objects.
Example:
final class Employee {
private final String name;
private final int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
✅ Immutable objects are thread-safe and prevent unintended state changes.
The Factory pattern provides a way to create objects without exposing instantiation logic.
Example:
interface Shape { void draw(); }
class Circle implements Shape {
public void draw() { System.out.println("Circle"); }
}
class Square implements Shape {
public void draw() { System.out.println("Square"); }
}
class ShapeFactory {
public static Shape getShape(String type) {
if (type.equalsIgnoreCase("circle")) return new Circle();
if (type.equalsIgnoreCase("square")) return new Square();
throw new IllegalArgumentException("Unknown shape");
}
}
// Usage
Shape s = ShapeFactory.getShape("circle");
s.draw();
✅ Use when object creation logic is complex or depends on conditions.
Dependency Injection is a design pattern where an object receives its dependencies from external sources rather than creating them internally.
Types of DI:
Constructor injection – dependencies passed via constructor
Setter injection – dependencies set via setters
Interface injection – using an interface to inject dependencies (less common)
Example (Constructor DI):
class Service {
private final Repository repo;
public Service(Repository repo) { this.repo = repo; }
}
Repository repo = new Repository();
Service service = new Service(repo);
✅ Benefits:
Promotes loose coupling
Easier unit testing
Improves maintainability
Steps in Java program execution:
Compilation: .java source code is compiled into .class bytecode using javac.
Class loading: JVM loads the .class files into memory using ClassLoaders.
Bytecode verification: Ensures code is valid and doesn’t violate JVM constraints.
Execution: Just-In-Time (JIT) compiler converts bytecode into native machine code for performance.
Memory allocation: Objects are created in the heap; references stored in stack.
Garbage Collection: Unreferenced objects are cleaned automatically.
✅ Key point: JVM allows platform independence by abstracting underlying OS and hardware.
Class loading is handled by the JVM in three phases:
Loading: Loads .class file into memory using ClassLoader.
Linking:
Verification: Ensures bytecode integrity
Preparation: Allocates memory for static variables
Resolution: Resolves symbolic references
Initialization: Executes static blocks and initializes static fields.
ClassLoader loads Java classes into JVM at runtime.
Types of ClassLoaders:
Bootstrap ClassLoader: Loads core Java classes (rt.jar)
Extension ClassLoader: Loads JDK extension libraries (lib/ext)
Application ClassLoader: Loads classes from classpath (your code)
Custom ClassLoader: Can be created for dynamic class loading
✅ ClassLoader ensures lazy loading — classes are loaded only when needed.
Java code is compiled into bytecode (.class file), not native machine code.
JVM interprets bytecode or uses JIT to convert to native code.
Same bytecode can run on any platform with a compatible JVM.
Key principle: Write Once, Run Anywhere (WORA).
Memory management:
Use proper data structures (ArrayList vs LinkedList)
Minimize object creation, reuse objects
Multithreading: Use ExecutorService, parallel streams wisely
Collections: Use Concurrent collections for thread safety
Garbage Collection tuning: Adjust heap size and GC algorithm
String handling: Use StringBuilder instead of String concatenation in loops
Profiling: Use tools like VisualVM, JConsole to detect bottlenecks
Automatic memory management by JVM
Removes unreachable objects from heap
Common collectors: Serial, Parallel, CMS, G1
GC events: Minor GC (young generation), Major GC (old generation)
Finalize() is called before GC (deprecated in Java 9+)
✅ Modern JVM uses generational GC for efficiency.
| Memory Area | Purpose | Example |
| Heap | Stores objects and arrays | new Employee() |
| Stack | Stores method call frames, local variables | int x = 10; |
Stack memory is LIFO, automatically cleaned.
Heap memory is shared, cleaned by GC.
PermGen (Java 7 and below): Stores class metadata; fixed size → risk of OutOfMemoryError.
Metaspace (Java 8+): Dynamically resizable; stored in native memory → reduces class loading errors.
Serialization: Converts a Java object into a byte stream to save to disk or send over a network.
Deserialization: Converts the byte stream back into a Java object.
Example:
// Serialization
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.obj"));
oos.writeObject(employee);
// Deserialization
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.obj"));
Employee e = (Employee) ois.readObject();
Notes:
Use transient keyword for fields that shouldn’t be serialized.
Serializable classes must implement Serializable.
An interface without methods used to mark a class with special behavior.
Examples: Serializable, Cloneable, Remote.
Purpose: Provides metadata to JVM or frameworks.
Example:
String s = "Java";
if (s instanceof String) {
System.out.println("s is a String");
}
✅ Always returns true for null checks: null instanceof String → false.
Reflection allows inspecting and manipulating classes, methods, and fields at runtime.
Can be used to:
Get class info (Class<?> clazz = obj.getClass();)
Access private fields/methods
Dynamically create instances
Example:
Class<?> clazz = Class.forName("java.util.ArrayList");
Object obj = clazz.getDeclaredConstructor().newInstance();
✅ Useful for frameworks like Spring, Hibernate, and testing tools.
Autoboxing: Automatic conversion from primitive to wrapper class
int x = 10;
Integer y = x; // autoboxing
Unboxing: Wrapper class → primitive
Integer a = 20;
int b = a; // unboxing
enum is a special class representing a fixed set of constants.
Can have fields, methods, and constructors.
Example:
enum Day { MONDAY, TUESDAY, WEDNESDAY }
Day today = Day.MONDAY;
✅ Useful for type safety instead of string constants.
No, private methods are not visible to subclasses.
They are class-specific and cannot be overridden, but they can be redeclared in a subclass.
main:public static void main(String[] args) { }
public static void main(int[] args) { }
Yes, used in Singleton pattern or factory methods.
Prevents direct instantiation from outside the class.
Metadata for classes, methods, fields.
Examples: @Override, @Deprecated, @FunctionalInterface
Custom annotation example:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Test {
}
JavaBean: A reusable class following:
Private fields
Public getters and setters
No-arg constructor
Serializable
✅ Used for encapsulation and frameworks like JSP, JSF, and Spring.
var name = "Ashutosh"; // inferred as String
Introduced in Java 14 as immutable data carriers.
Automatically generate constructor, getters, equals, hashCode, toString.
record Employee(String name, int age) { }
Employee e = new Employee("John", 25);
✅ Great for DTOs and value objects.
Learn more about records here.
| Type | When occurs | Example |
| Compile-time error | During compilation | Syntax error, missing semicolon |
| Runtime error | During program execution | NullPointerException, ArrayIndexOutOfBounds |
Scenario: You have a large dataset and need frequent lookups.
Approach:
Choose the right data structure:
HashMap for O(1) key-value lookup.
TreeMap if sorted order is required (O(log n) lookup).
HashSet for unique element search.
Indexing: Precompute indices for frequent queries.
Use streams with parallel processing if data is very large.
Avoid unnecessary object creation: Use primitives or immutable objects.
Memory considerations: If memory is tight, consider using compressed data structures or disk-based solutions.
Example:
Map<String, Employee> employeeMap = new HashMap<>();
for(Employee e: employees) {
employeeMap.put(e.getId(), e);
}
// Lookup by ID is now O(1)
Scenario: Multiple threads need to access/update shared data.
Approach:
Synchronized blocks/methods:
synchronized(this) {
// critical section
}
Concurrent Collections: Use ConcurrentHashMap, CopyOnWriteArrayList.
Locks: Use ReentrantLock for fine-grained control.
Atomic Variables: Use AtomicInteger, AtomicReference for atomic operations.
Avoid deadlocks: Always acquire locks in consistent order.
✅ Modern Java encourages lock-free structures and immutability where possible.
Steps:
Identify symptoms: High heap usage, OutOfMemoryError, slow performance.
Analyze heap dumps: Use tools like VisualVM, Eclipse MAT, JProfiler.
Check references: Look for unreleased objects, static collections, caches.
Fix common causes:
Remove unused listeners or callbacks
Use WeakReference where needed
Clear collections properly
Test thoroughly: Run under load to confirm memory usage stabilizes.
Scenario: High-concurrency application.
Approach:
Prefer immutable objects to avoid synchronization overhead.
Use concurrent collections (ConcurrentHashMap, ConcurrentLinkedQueue) instead of synchronized versions.
Minimize synchronized blocks to only critical sections.
Use atomic variables for counters or flags.
Consider ReadWriteLock if reads dominate writes.
✅ Key: Balance thread safety vs performance.
Example Answer:
“In a recent project, we had a list of employees and needed to filter those in a specific department, sort by salary, and collect names. Using Java 8 Streams and lambdas, I could do it in one readable statement instead of nested loops:
List<String> names = employees.stream() .filter(e -> e.getDepartment().equals("Engineering")) .sorted(Comparator.comparing(Employee::getSalary).reversed()) .map(Employee::getName) .collect(Collectors.toList());This approach reduced boilerplate code, improved readability, and was easy to maintain.”
This guide covers all essential Core Java concepts, coding patterns, and real-world scenarios needed to excel in Java interviews in 2025. Use it to strengthen fundamentals, improve problem-solving, and confidently tackle interviews.