Java Core Interview Questions & Answers
Tipico Intermediate Developer Position - Comprehensive
Guide
1. Object-Oriented Programming Fundamentals
Q1: What is inheritance and why do we use it?
Answer: Inheritance is a mechanism where a new class (child/subclass) acquires properties
and behaviors from an existing class (parent/superclass). It enables code reusability and
establishes an "is-a" relationship.
Why use it:
Code reusability: Common functionality in parent class
Extensibility: Add new features without modifying existing code
Polymorphism: Treat objects of different classes through common interface
Maintainability: Changes in parent automatically reflect in children
Example:
class Animal {
void eat() { [Link]("Eating..."); }
}
class Dog extends Animal {
void bark() { [Link]("Barking..."); }
}
Q2: What is the difference between an interface and an abstract class?
Answer:
Aspect Interface Abstract Class
All abstract (pre-Java Can have concrete
Methods
8) methods
Multiple
Supported Not supported
Inheritance
Can have instance
Fields public static final only
variables
Access Modifiers public only Any access modifier
Constructor No constructor Can have constructor
Use Case Define contract Partial implementation
Table 1: Interface vs Abstract Class comparison
When to use:
Interface: Define behavior contract for unrelated classes
Abstract class: Share common code among related classes
Q3: Explain method overloading vs method overriding
Answer:
Method Overloading (Compile-time polymorphism):
Same method name, different parameters (type, number, or order)
Within the same class
Return type can differ
Resolved at compile time
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
}
Method Overriding (Runtime polymorphism):
Same method signature in parent and child
Inheritance relationship required
Return type must be same or covariant
Resolved at runtime
class Animal {
void sound() { [Link]("Animal sound"); }
}
class Dog extends Animal {
@Override
void sound() { [Link]("Bark"); }
}
Q4: What is polymorphism? Give real-world examples
Answer: Polymorphism means "many forms" - the ability of an object to take multiple
forms. Java supports two types:
Compile-time Polymorphism (Method Overloading):
Same method name handles different parameter types.
Runtime Polymorphism (Method Overriding):
Parent reference can hold child objects and invoke overridden methods.
Real-world example:
// Payment system with polymorphism
interface Payment {
void processPayment(double amount);
}
class CreditCardPayment implements Payment {
public void processPayment(double amount) {
// Process via credit card gateway
}
}
class PayPalPayment implements Payment {
public void processPayment(double amount) {
// Process via PayPal API
}
}
// Usage
Payment payment = new CreditCardPayment();
[Link](100.0); // Calls appropriate implementation
Q5: When would you use abstract classes over interfaces?
Answer: Use abstract classes when:
1. Sharing code: Multiple related classes share common implementation
2. State management: Need instance variables with different access modifiers
3. Constructor logic: Require initialization code for subclasses
4. Partial implementation: Want to provide some default behavior
5. Access control: Need protected or private members
Example scenario: Building a game with different character types that share common
attributes (health, position) but have different abilities.
2. SOLID Principles & Design Patterns
Q6: How does Single Responsibility Principle (SRP) improve code
maintainability?
Answer: SRP states that a class should have only one reason to change - one responsibility.
Benefits:
Easier testing: Each class has focused, testable behavior
Lower coupling: Classes don't depend on unrelated functionality
Better readability: Clear purpose for each class
Simplified debugging: Bugs isolated to specific responsibility
Easier refactoring: Changes affect minimal code surface
Example violation:
// BAD: Multiple responsibilities
class User {
void saveToDatabase() { }
void sendEmail() { }
void generateReport() { }
}
SRP solution:
// GOOD: Single responsibility per class
class User { /* user data */ }
class UserRepository { void save(User user) { } }
class EmailService { void send(User user) { } }
class ReportGenerator { void generate(User user) { } }
Q7: Explain Open/Closed Principle with a real example
Answer: OCP states that software entities should be open for extension but closed for
modification.
Meaning: Add new functionality by extending existing code, not changing it.
Example:
// BAD: Must modify class to add new shapes
class AreaCalculator {
double calculate(Object shape) {
if (shape instanceof Circle) {
// calculate circle area
} else if (shape instanceof Rectangle) {
// calculate rectangle area
}
// Adding Triangle requires modifying this method
}
}
// GOOD: Open for extension
interface Shape {
double area();
}
class Circle implements Shape {
public double area() { return [Link] * radius * radius; }
}
class Rectangle implements Shape {
public double area() { return width * height; }
}
class Triangle implements Shape {
public double area() { return 0.5 * base * height; }
}
// Adding new shape doesn't require modifying existing code
Q8: When would you use composition over inheritance?
Answer: Favor composition when:
1. "Has-a" relationship rather than "is-a"
2. Flexible behavior changes at runtime
3. Avoiding tight coupling of inheritance hierarchy
4. Multiple behaviors from different sources (Java doesn't support multiple
inheritance)
5. Testing isolation: Easier to mock composed dependencies
Example:
// Composition approach
class Engine {
void start() { }
}
class Car {
private Engine engine; // Car HAS-A Engine
Car() {
[Link] = new Engine();
}
void start() {
[Link]();
}
Advantages: Can swap Engine implementations, test Car independently, change behavior
at runtime.
Q9: Design a logging system following SOLID principles
Answer:
// Single Responsibility: Each logger has one purpose
interface Logger {
void log(String message);
}
// Open/Closed: Can add new loggers without modifying existing
class FileLogger implements Logger {
public void log(String message) {
// Write to file
}
}
class ConsoleLogger implements Logger {
public void log(String message) {
[Link](message);
}
}
class DatabaseLogger implements Logger {
public void log(String message) {
// Write to database
}
}
// Dependency Inversion: Depend on abstraction
class Application {
private Logger logger;
// Inject dependency
Application(Logger logger) {
[Link] = logger;
}
void process() {
[Link]("Processing...");
}
// Usage
Logger logger = new FileLogger();
Application app = new Application(logger);
SOLID principles applied:
SRP : Each logger handles one output type
OCP : Add new loggers without changing existing code
LSP : Any Logger implementation can replace another
ISP : Single focused interface, no unused methods
DIP : Application depends on Logger abstraction, not concrete implementations
3. Exception Handling
Q10: What's the difference between final, finally, and finalize?
Answer:
Keyword Purpose Usage
Immutability Variables: cannot
modifier reassign; Methods:
final cannot override;
Classes: cannot
extend
Exception Code that always
handling block executes after try-
finally
catch, regardless of
exception
Garbage Called by GC before
collection object deletion
finalize()
method (deprecated since
Java 9)
Table 2: final, finally, finalize comparison
Examples:
// final
final int x = 10;
// x = 20; // Compilation error
// finally
try {
// risky code
} catch (Exception e) {
// handle
} finally {
// always executes (e.g., close resources)
}
// finalize (deprecated - use try-with-resources instead)
@Override
protected void finalize() throws Throwable {
// cleanup before GC
}
Q11: Can finally block prevent exception from being thrown?
Answer: Yes, finally block can prevent exception propagation in specific cases:
Case 1 : Finally block returns a value
int method() {
try {
throw new Exception("Error");
} finally {
return 42; // Suppresses exception
}
}
Case 2 : Finally block throws different exception
void method() throws IOException {
try {
throw new RuntimeException("First");
} finally {
throw new IOException("Second"); // Replaces first exception
}
}
Best practice: Avoid return statements in finally blocks - they suppress exceptions and
make debugging difficult.
Q12: When should you create custom exceptions?
Answer: Create custom exceptions when:
1. Domain-specific errors: Business logic violations (e.g., InsufficientFundsException)
2. Clearer error messages: Provide context-specific information
3. Exception handling strategy: Different handling for different business scenarios
4. API design: Clearer contract for callers
Example:
public class InsufficientFundsException extends Exception {
private double balance;
private double withdrawAmount;
public InsufficientFundsException(double balance, double withdrawAmount) {
super("Insufficient funds: balance=" + balance + ", requested=" + withdrawAm
[Link] = balance;
[Link] = withdrawAmount;
}
public double getShortfall() {
return withdrawAmount - balance;
}
}
// Usage
if (balance < amount) {
throw new InsufficientFundsException(balance, amount);
}
Q13: What's the difference between throw and throws?
Answer:
Aspect throw throws
Actually throw Declare possible
Purpose
exception exceptions
Location Inside method body Method signature
Throw exception List exception types
Usage
instance
One exception per Multiple exceptions
Count
statement allowed
Runtime or checked Checked exceptions
Type
only
Table 3: throw vs throws comparison
Example:
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(balance, amount);
}
}
4. Collections Framework
Q14: How does HashMap work internally?
Answer: HashMap uses hashing to store key-value pairs efficiently.
Internal Structure:
Array of buckets (default size: 16)
Each bucket can hold multiple entries (linked list or tree structure)
Load factor: 0.75 (rehashing when 75% full)
Key Operations:
1. PUT operation:
Calculate hashCode() of key
Apply hash function to determine bucket index: index = hash(key) & (n-1)
If bucket empty, store entry
If bucket occupied (collision), use equals() to check if key exists
If key exists, replace value; otherwise add to bucket
2. GET operation:
Calculate hashCode() of key
Find bucket using hash function
Traverse bucket entries using equals() to find matching key
Return associated value
Collision Handling:
Java 7 and earlier: Linked list in bucket
Java 8+ : Linked list converts to balanced tree when bucket size exceeds 8 entries
(improves worst-case from O(n) to O(log n))
Rehashing: When size exceeds threshold (capacity × load factor), HashMap doubles
capacity and redistributes all entries.
Q15: What's the difference between HashMap, LinkedHashMap, and
TreeMap?
Answer:
Feature HashMap LinkedHashMap TreeMap
No order Insertion order Sorted
Order
(natural/comparator)
O(1) O(1) average O(log n)
Performance
average
Null Keys 1 allowed 1 allowed Not allowed
Use Case Fast lookups Order matters Sorted iteration
Lowest Medium Highest (tree)
Memory (doubly-linked
list)
Table 4: Map implementations comparison
When to use:
HashMap: Default choice for key-value storage, fastest performance
LinkedHashMap: When insertion/access order preservation needed (LRU cache)
TreeMap: When sorted keys required, range operations (subMap, headMap)
Q16: What triggers HashMap rehashing and what's the performance
impact?
Answer: Rehashing occurs when the number of entries exceeds the threshold.
Threshold calculation: capacity × load factor
Default capacity: 16
Default load factor: 0.75
Initial threshold: 16 × 0.75 = 12 entries
Rehashing process:
1. Create new array with double capacity (16 → 32 → 64...)
2. Recalculate hash for all existing entries
3. Redistribute entries to new buckets
4. Old array becomes eligible for garbage collection
Performance impact:
During rehashing: O(n) operation, temporarily slower
After rehashing: Improved performance due to fewer collisions
Trade-off: Memory (larger array) vs speed (fewer collisions)
Optimization: If you know expected size, set initial capacity to avoid multiple rehashings:
// For 1000 entries: 1000 / 0.75 ≈ 1334, use next power of 2
Map<String, String> map = new HashMap<>(2048);
Q17: Why is ArrayList preferred over Vector?
Answer:
Feature ArrayList Vector
Synchronization Not synchronized Synchronized
Slower (synchronized
Performance Faster (no locks)
overhead)
Growth 50% increase 100% doubling
Legacy Modern (Java 1.2) Legacy (Java 1.0)
Manual sync
Thread Safety Thread-safe by default
needed
Table 5: ArrayList vs Vector comparison
Why ArrayList is preferred:
Performance: No synchronization overhead for single-threaded scenarios
Better control: Explicitly synchronize only when needed
Efficient growth: 50% growth wastes less memory than 100% doubling
Modern approach: Use [Link]() or CopyOnWriteArrayList for
thread safety
When to use Vector: Almost never in modern code. Use ArrayList with explicit
synchronization if needed.
Q18: When should you use LinkedList over ArrayList?
Answer: Use LinkedList when:
1. Frequent insertions/deletions at beginning or middle: O(1) after positioning vs
ArrayList's O(n)
2. Queue/Deque operations: Implements Deque interface for both ends
3. Memory pattern: Many small additions/removals vs batch operations
Performance comparison:
Operation ArrayList LinkedList
Access by index O(1) O(n)
Add at end O(1) amortized O(1)
Add at beginning O(n) O(1)
Add in middle O(n) O(1) after positioning
Remove from end O(1) O(1)
Remove from beginning O(n) O(1)
Table 6: ArrayList vs LinkedList performance
In practice: ArrayList is preferred for most use cases due to:
Better cache locality (contiguous memory)
Lower memory overhead (no node pointers)
Faster iteration
Use LinkedList only when: Implementing queue/deque, frequent head insertions, or
maintaining insertion-deletion heavy lists.
Q19: How do you make a HashMap thread-safe?
Answer: Three approaches:
Approach 1: [Link]()
Map<String, String> map = [Link](new HashMap<>());
Synchronizes all methods
Performance bottleneck: locks entire map
Approach 2: ConcurrentHashMap (RECOMMENDED)
Map<String, String> map = new ConcurrentHashMap<>();
Segment-level locking (better concurrency)
Null keys/values not allowed
Better performance for concurrent reads/writes
Approach 3: Explicit synchronization
Map<String, String> map = new HashMap<>();
synchronized(map) {
[Link](key, value);
}
Manual control but error-prone
Best practice: Use ConcurrentHashMap for high-concurrency scenarios,
[Link]() for simple thread-safety needs.
5. Java Generics
Q20: What are bounded type parameters? Give an example
Answer: Bounded type parameters restrict the types that can be used as generic
arguments.
Upper Bounded (extends):
// T must be Number or its subclass
public <T extends Number> double sum(List<T> numbers) {
return [Link]()
.mapToDouble(Number::doubleValue)
.sum();
}
// Works with Integer, Double, Float, etc.
sum([Link](1, 2, 3)); // Integer
sum([Link](1.5, 2.5, 3.5)); // Double
Multiple bounds:
// T must implement both Comparable and Serializable
public <T extends Comparable<T> & Serializable> void sort(List<T> list) {
[Link](list);
}
Why use bounded types:
Access methods of bound type (e.g., [Link]())
Enforce type constraints at compile time
Enable more specific generic algorithms
Q21: Explain the difference between wildcard types
Answer:
Wildcard Syntax Use Case
<?> Unknown type, read-
Unbounded
only
<? extends Type> Read from structure
Upper Bounded
(producer)
<? super Type> Write to structure
Lower Bounded
(consumer)
Table 7: Wildcard types comparison
Examples:
// Unbounded: accept any type
void printList(List<?> list) {
for (Object obj : list) {
[Link](obj);
}
}
// Upper bounded: read from list
double sumNumbers(List<? extends Number> numbers) {
return [Link]()
.mapToDouble(Number::doubleValue)
.sum();
}
// Can call with List<Integer>, List<Double>, etc.
// Lower bounded: add to list
void addNumbers(List<? super Integer> list) {
[Link](42);
[Link](100);
}
// Can call with List<Integer>, List<Number>, List<Object>
Q22: What is the PECS principle?
Answer: PECS = Producer Extends, Consumer Super
Rule:
Producer (reading from): Use <? extends T> - you get objects out
Consumer (writing to): Use <? super T> - you put objects in
Explanation:
// Producer: generates/provides data
public void processNumbers(List<? extends Number> producer) {
for (Number n : producer) {
// READ from list - safe
[Link]([Link]());
}
// [Link](42); // COMPILE ERROR - can't write
}
// Consumer: accepts/consumes data
public void addIntegers(List<? super Integer> consumer) {
[Link](42); // WRITE to list - safe
[Link](100);
// Integer x = [Link](0); // COMPILE ERROR - don't know exact type
}
Memory aid:
Extends = Extract (read)
Super = Store (write)
Real-world example: [Link](List<? super T> dest, List<? extends T> src) - source
produces, destination consumes.
Q23: Can you instantiate a generic type?
Answer: No, you cannot directly instantiate a generic type due to type erasure.
Why not:
public class Box<T> {
// COMPILE ERROR: Cannot instantiate generic type
// T instance = new T();
}
Type erasure removes generic type information at runtime, so JVM doesn't know what T
actually is.
Workarounds:
Approach 1: Pass Class object
public class Box<T> {
private T instance;
public Box(Class<T> clazz) throws Exception {
instance = [Link]().newInstance();
}
}
// Usage
Box<String> box = new Box<>([Link]);
Approach 2: Factory pattern
public interface Factory<T> {
T create();
}
public class Box<T> {
private T instance;
public Box(Factory<T> factory) {
instance = [Link]();
}
Q24: What is type erasure in Java Generics?
Answer: Type erasure is the process where the compiler removes all generic type
information after compilation.
Process:
1. Replace type parameters with bounds (or Object if unbounded)
2. Insert type casts where needed
3. Generate bridge methods for polymorphism
Before compilation:
List<String> list = new ArrayList<String>();
[Link]("Hello");
String s = [Link](0);
After type erasure (bytecode equivalent):
List list = new ArrayList();
[Link]("Hello");
String s = (String) [Link](0); // Cast inserted by compiler
Implications:
Cannot use instanceof with generic types: obj instanceof List<String> invalid
Cannot create generic arrays: new T[10] invalid
No runtime type information about generics
Same bytecode for List<String> and List<Integer>
Why erasure: Backward compatibility with pre-generics Java code (Java 1.4 and earlier).
6. Multithreading & Concurrency
Q25: What's the difference between process and thread?
Answer:
Aspect Process Thread
Independent program Lightweight unit
Definition
execution within process
Separate memory Shared memory with
Memory
space other threads
IPC (slow, complex) Direct memory access
Communication
(fast)
Creation Cost Expensive Inexpensive
Context Switch Slower Faster
Isolation Fully isolated Share code, data, files
Table 8: Process vs Thread comparison
Key point: Multiple threads within a process share the same memory space, enabling fast
communication but requiring synchronization for thread safety.
Q26: How does thread synchronization work?
Answer: Synchronization ensures only one thread accesses shared resources at a time
using monitor locks (intrinsic locks).
Mechanism:
Each object in Java has an associated monitor lock. When a thread enters a synchronized
block/method:
1. Thread acquires the monitor lock
2. Executes synchronized code
3. Releases lock upon exit (normal or exception)
4. Other threads wait until lock is released
Synchronized method:
public synchronized void increment() {
count++; // Only one thread at a time
}
Synchronized block (more granular control):
public void increment() {
synchronized(this) {
count++;
}
}
Class-level synchronization:
public static synchronized void staticMethod() {
// Locks on Class object, not instance
}
Q27: What's the difference between synchronized method and
synchronized block?
Answer:
Aspect Synchronized Method Synchronized Block
Lock Scope Entire method Specific code section
this (instance method) Explicitly specified
Lock Object
or Class (static)
Granularity Coarse-grained Fine-grained
Lower (locks entire Higher (minimal
Performance
method) locking)
Less flexible Can lock on different
Flexibility
objects
Table 9: Synchronized method vs block comparison
Example:
// Synchronized method - locks entire method
public synchronized void process() {
// Line 1-10: non-critical code
// Line 11-12: critical section
// Line 13-20: non-critical code
}
// Synchronized block - locks only critical section
public void process() {
// Line 1-10: non-critical code (no lock)
synchronized(this) {
// Line 11-12: critical section (locked)
}
// Line 13-20: non-critical code (no lock)
}
Best practice: Use synchronized blocks to minimize lock duration and improve
concurrency.
Q28: What causes deadlock and how do you prevent it?
Answer: Deadlock occurs when two or more threads are blocked forever, each waiting for
a lock held by another.
Classic example:
// Thread 1
synchronized(lockA) {
synchronized(lockB) {
// work
}
}
// Thread 2
synchronized(lockB) { // Acquires lockB
synchronized(lockA) { // Waits for lockA (held by Thread 1)
// work
}
}
// Thread 1 waits for lockB, Thread 2 waits for lockA → DEADLOCK
Four necessary conditions for deadlock:
1. Mutual exclusion: Resources cannot be shared
2. Hold and wait: Thread holds resource while waiting for another
3. No preemption: Resources cannot be forcibly taken
4. Circular wait: Circular chain of threads waiting for resources
Prevention strategies:
1. Lock ordering: Always acquire locks in same order
2. Lock timeout: Use tryLock() with timeout instead of blocking
3. Avoid nested locks: Minimize situations where thread holds multiple locks
4. Use concurrent collections: ConcurrentHashMap, CopyOnWriteArrayList
Corrected example:
// Both threads acquire locks in same order
synchronized(lockA) {
synchronized(lockB) {
// work safely
}
}
Q29: Explain volatile keyword and when to use it
Answer: volatile is a keyword ensuring visibility of variable changes across threads.
Guarantees:
1. Visibility: Writes to volatile variable immediately visible to all threads
2. Ordering: Prevents compiler/CPU from reordering instructions around volatile
access
3. NOT Atomic: Does not guarantee atomicity for compound operations
Memory semantics:
Write to volatile → flushes to main memory
Read from volatile → reads from main memory (not CPU cache)
When to use:
Flag variables controlling thread execution
Status indicators that multiple threads check
Single writer, multiple readers scenarios
Simple state that doesn't require atomic operations
Example:
public class TaskRunner {
private volatile boolean running = true; // Visibility guarantee
public void run() {
while (running) { // Other threads see updates immediately
// perform work
}
}
public void stop() {
running = false; // Change visible to all threads
}
}
Don't use for: Compound operations like count++ (use AtomicInteger or synchronized
instead).
Q30: What's the difference between volatile and synchronized?
Answer:
Feature volatile synchronized
Visibility Yes Yes
Atomicity No Yes
Mutual Exclusion No Yes
Performance Faster (no locking) Slower (lock overhead)
Scope Variables only Methods and blocks
Blocking Never blocks Can block threads
Use Case Flags, status Critical sections
Table 10: volatile vs synchronized comparison
Key difference:
volatile: Lightweight visibility mechanism for single variables
synchronized: Full mutual exclusion with atomicity guarantees
Example showing the difference:
// volatile - NOT safe for increment
private volatile int count = 0;
public void increment() {
count++; // NOT ATOMIC: read, increment, write
}
// synchronized - safe for increment
private int count = 0;
public synchronized void increment() {
count++; // ATOMIC: only one thread at a time
}
Q31: What is AtomicInteger and how does it differ from volatile?
Answer: AtomicInteger provides atomic operations on integer values without explicit
locking using Compare-And-Swap (CAS) algorithm.
Key features:
Lock-free atomic operations
Methods: get(), set(), incrementAndGet(), compareAndSet()
Better performance than synchronized for simple operations
Thread-safe without blocking
Comparison:
Feature volatile int AtomicInteger
Visibility Yes Yes
No (compound Yes (all operations)
Atomicity
operations)
increment() Not atomic Atomic
compareAndSet Not available Available
Performance Fastest Fast (CAS, no locks)
Table 11: volatile vs AtomicInteger comparison
Example:
// volatile - UNSAFE for counter
private volatile int counter = 0;
public void increment() {
counter++; // Race condition possible
}
// AtomicInteger - SAFE for counter
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
[Link](); // Atomic operation
}
// Advanced usage
int oldValue = [Link]();
int newValue = oldValue + 5;
boolean success = [Link](oldValue, newValue);
When to use:
volatile: Simple flags, status variables (no compound operations)
AtomicInteger: Counters, sequence generators, lock-free algorithms
Q32: How does ExecutorService work? Give implementation example
Answer: ExecutorService manages thread pool execution, abstracting thread creation
and lifecycle management.
Benefits:
Thread reuse (avoid creation overhead)
Resource management (limit concurrent threads)
Task queuing and scheduling
Graceful shutdown
Common implementations:
// Fixed thread pool: reuses fixed number of threads
ExecutorService executor = [Link](5);
// Cached thread pool: creates threads as needed, reuses idle threads
ExecutorService executor = [Link]();
// Single thread executor: sequential task execution
ExecutorService executor = [Link]();
// Scheduled executor: delayed/periodic tasks
ScheduledExecutorService scheduler = [Link](3);
Implementation example:
public class TaskProcessor {
private ExecutorService executor = [Link](10);
public void processTasks(List<Task> tasks) {
for (Task task : tasks) {
[Link](() -> {
[Link]();
});
}
}
public void shutdown() {
[Link](); // Stop accepting new tasks
try {
if () {
[Link](); // Force shutdown
}
} catch (InterruptedException e) {
[Link]();
}
}
}
Callable and Future:
// Submit Callable that returns result
Future<Integer> future = [Link](() -> {
return 42;
});
// Get result (blocks until complete)
Integer result = [Link]();
Q33: What is thread starvation?
Answer: Thread starvation occurs when a thread cannot gain regular access to shared
resources and is unable to make progress.
Causes:
1. Low priority threads: High-priority threads consistently preempt low-priority ones
2. Unfair locks: Some threads repeatedly acquire locks while others wait
3. Long-running synchronized blocks: Block resources for extended periods
4. Insufficient thread pool size: More tasks than threads available
Example scenario:
// Thread pool with 2 threads
ExecutorService executor = [Link](2);
// Submit 10 long-running tasks
for (int i = 0; i < 10; i++) {
[Link](() -> {
while(true) { } // Infinite loop - starves other tasks
});
}
// Tasks 3-10 never execute - starved by first 2 tasks
Prevention:
Use fair locks: new ReentrantLock(true)
Avoid long-running synchronized sections
Set appropriate thread priorities
Size thread pools correctly for workload
Implement timeout mechanisms
7. Memory Management & Garbage Collection
Q34: Explain garbage collection in Java
Answer: Garbage Collection (GC) is automatic memory management that reclaims
memory occupied by unreachable objects.
How it works:
1. Marking: Identify reachable objects starting from GC roots (static variables, active
threads, JNI references)
2. Sweeping: Remove unmarked (unreachable) objects
3. Compacting: Move surviving objects together to reduce fragmentation
Generational hypothesis: Most objects die young, so GC divides heap into generations.
Heap generations:
Young Generation: New objects allocated here
Eden space: Initial allocation
Survivor spaces (S0, S1): Objects surviving Minor GC
Old Generation (Tenured): Long-lived objects promoted here
Metaspace (Java 8+): Class metadata (replaced PermGen)
GC types:
Minor GC: Cleans young generation (frequent, fast)
Major GC: Cleans old generation (infrequent, slower)
Full GC: Cleans entire heap (rare, expensive)
Q35: What is the mark and sweep algorithm?
Answer: Mark and Sweep is a two-phase garbage collection algorithm.
Phase 1 - Marking:
1. Start from GC roots (stack references, static variables, active threads)
2. Traverse object graph following references
3. Mark all reachable objects
4. Unmarked objects are garbage
Phase 2 - Sweeping:
1. Scan entire heap
2. Reclaim memory occupied by unmarked objects
3. Add reclaimed memory to free list
Optional Phase 3 - Compacting:
1. Move surviving objects together
2. Eliminate fragmentation
3. Update references to moved objects
Visualization:
Before GC:
[Obj A]→[Obj B] [Obj C (unreachable)] [Obj D]→[Obj E]
After Marking:
[Obj A✓]→[Obj B✓] [Obj C] [Obj D✓]→[Obj E✓]
After Sweeping:
[Obj A][Obj B] [FREE] [Obj D][Obj E]
After Compacting:
[Obj A][Obj B][Obj D][Obj E][FREE SPACE]
Performance characteristics:
Pause time: Stops application threads during collection (Stop-The-World)
Efficiency: Depends on heap size and live object ratio
Q36: Can you force garbage collection? Should you?
Answer:
Can you?: You can request GC using [Link]() or [Link]().gc(), but it's only
a suggestion to the JVM, not a guarantee.
Should you?: Almost never.
Why not:
1. JVM knows better: Modern GCs optimize based on memory pressure and allocation
patterns
2. Performance degradation: Forcing GC at wrong time causes unnecessary pauses
3. No guarantee: JVM may ignore the request
4. Unpredictable timing: May trigger expensive Full GC
Rare valid cases:
Before critical low-latency operation
After loading large temporary data structures
Memory profiling and testing
Benchmarking (to establish baseline)
Best practice: Tune GC parameters and heap size instead of manually triggering GC. Trust
the JVM's GC algorithms.
Q37: What are weak references and when to use them?
Answer: Java provides four reference types with different garbage collection behaviors:
Reference Type GC Behavior Use Case
Never collected while Normal objects
Strong
reachable
Collected when Memory-
Soft
memory low sensitive caches
Collected in next GC Canonical
Weak
cycle mappings
Collected, cleanup Pre-finalization
Phantom
actions cleanup
Table 12: Java reference types
Weak Reference Example:
// WeakHashMap - canonical mapping
WeakHashMap<Image, Metadata> imageCache = new WeakHashMap<>();
Image img = new Image("[Link]");
[Link](img, metadata);
// When img = null and no other strong references exist,
// the entry is automatically removed from map during next GC
img = null;
[Link](); // Entry removed from WeakHashMap
Common use cases:
Caching: Objects can be reclaimed when memory needed
Listeners/Observers: Prevent memory leaks when observer not explicitly removed
Canonical mappings: Associate metadata with objects without preventing GC
SoftReference for cache:
SoftReference<byte[]> cache = new SoftReference<>(largeData);
byte[] data = [Link]();
if (data == null) {
// Recompute - GC cleared it due to memory pressure
}
Q38: Describe the G1 GC collector
Answer: G1 (Garbage First) is a low-latency garbage collector designed for large heaps
(>4GB) with predictable pause times.
Key features:
1. Region-based: Divides heap into equal-sized regions (1-32MB)
2. Generational: Still uses young/old generation concept
3. Concurrent marking: Marks objects while application runs
4. Incremental compaction: Compacts regions gradually
5. Predictable pauses: Target maximum pause time (e.g., 200ms)
How it works:
Tracks live data in each region
Prioritizes regions with most garbage (hence "Garbage First")
Collects regions with least live data first for maximum memory reclamation
Configuration:
-XX:+UseG1GC # Enable G1
-XX:MaxGCPauseMillis=200 # Target pause time
-XX:G1HeapRegionSize=16m # Region size
When to use:
Large heaps (>6GB)
Low-latency requirements (real-time systems, web applications)
Predictable pause times more important than maximum throughput
Advantages over Parallel GC:
Better pause time predictability
Concurrent processing (less stop-the-world)
Better for large heaps
8. Streams API (Java 8+)
Q39: What's the difference between intermediate and terminal
operations?
Answer:
Aspect Intermediate Terminal
Stream Non-stream
Return Type
result
Lazy (deferred) Eager (triggers
Execution
pipeline)
Count Multiple allowed One required
filter(), map(), collect(),
Examples sorted() forEach(),
count()
Purpose Transform stream Produce result
Table 13: Intermediate vs Terminal operations
Intermediate operations (return Stream):
filter(Predicate): Select elements matching condition
map(Function): Transform elements
flatMap(Function): Flatten nested structures
sorted(): Sort elements
distinct(): Remove duplicates
limit(n): Limit to n elements
skip(n): Skip first n elements
Terminal operations (produce result):
collect(Collector): Accumulate into collection
forEach(Consumer): Perform action on each element
reduce(BinaryOperator): Reduce to single value
count(): Count elements
anyMatch(), allMatch(), noneMatch(): Boolean tests
findFirst(), findAny(): Retrieve element
Example:
List<String> result = [Link]() // Source
.filter(s -> [Link]() > 3) // Intermediate
.map(String::toUpperCase) // Intermediate
.sorted() // Intermediate
.collect([Link]()); // Terminal - executes pipeline
Q40: Can streams be reused after terminal operation?
Answer: No, streams cannot be reused after a terminal operation is invoked.
Reason: Stream represents a one-time traversal of data. After terminal operation, stream is
consumed and closed.
Example:
Stream<String> stream = [Link]()
.filter(s -> [Link]("A"));
List<String> result1 = [Link]([Link]()); // OK
List<String> result2 = [Link]([Link]()); // IllegalStateException
// Error: stream has already been operated upon or closed
Solution: Create new stream for each operation:
List<String> result1 = [Link]()
.filter(s -> [Link]("A"))
.collect([Link]());
List<String> result2 = [Link]()
.filter(s -> [Link]("A"))
.collect([Link]());
Best practice: Don't store streams in variables for reuse. Create fresh streams from source
collections.
Q41: What is lazy evaluation in streams?
Answer: Lazy evaluation means intermediate operations don't execute until a terminal
operation is invoked.
How it works:
1. Intermediate operations return new stream and record the operation
2. No processing occurs until terminal operation called
3. When terminal operation invoked, entire pipeline executes element-by-element
Efficiency benefits:
Short-circuiting: Can stop processing early (findFirst(), anyMatch())
Optimization: JVM can optimize entire pipeline together
Avoid unnecessary work: Process only what's needed
Example:
List<Integer> numbers = [Link](1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// No execution yet - lazy
Stream<Integer> stream = [Link]()
.filter(n -> {
[Link]("Filtering: " + n);
return n > 5;
})
.map(n -> {
[Link]("Mapping: " + n);
return n * 2;
});
// Terminal operation triggers execution
Integer first = [Link]().get();
// Output: Only processes until first match found
// Filtering: 1
// Filtering: 2
// ...
// Filtering: 6
// Mapping: 6
// Result: 12 (stops early, doesn't process 7-10)
Without lazy evaluation, all filtering and mapping would occur even though we only need
first element.
Q42: Explain map() vs flatMap()
Answer:
map(): Transforms each element to another element (one-to-one mapping)
List<String> words = [Link]("hello", "world");
List<Integer> lengths = [Link]()
.map(String::length) // String → Integer
.collect([Link]());
// Result: [5, 5]
flatMap(): Transforms each element to a stream, then flattens all streams into one (one-to-
many mapping)
List<String> words = [Link]("hello", "world");
List<Character> chars = [Link]()
.flatMap(s -> [Link]()
.mapToObj(c -> (char) c))
.collect([Link]());
// Result: [h, e, l, l, o, w, o, r, l, d]
Visual difference:
map():
[1, 2, 3] → map(n → [n, n*10]) → [[1,10], [2,20], [3,30]] (nested)
flatMap():
[1, 2, 3] → flatMap(n → [n, n*10]) → [1, 10, 2, 20, 3, 30] (flattened)
Common use case - flattening nested collections:
List<List<Integer>> nested = [Link](
[Link](1, 2),
[Link](3, 4),
[Link](5, 6)
);
List<Integer> flat = [Link]()
.flatMap(Collection::stream)
.collect([Link]());
// Result: [1, 2, 3, 4, 5, 6]
Q43: How do you use Collectors to group results?
Answer: Collectors provide reduction operations to accumulate stream elements.
Common collectors:
toList/toSet/toMap:
List<String> list = [Link]([Link]());
Set<String> set = [Link]([Link]());
Map<Integer, String> map = [Link](
[Link](
String::length, // key mapper
s -> s // value mapper
)
);
groupingBy - Group by classifier:
List<Person> people = [Link](
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Charlie", 25)
);
// Group by age
Map<Integer, List<Person>> byAge = [Link]()
.collect([Link](Person::getAge));
// Result: {25=[Alice, Charlie], 30=[Bob]}
// Group and count
Map<Integer, Long> ageCount = [Link]()
.collect([Link](
Person::getAge,
[Link]()
));
// Result: {25=2, 30=1}
partitioningBy - Partition into true/false groups:
Map<Boolean, List<Integer>> partitioned = [Link]()
.collect([Link](n -> n > 5));
// Result: {false=[1,2,3,4,5], true=[6,7,8,9,10]}
joining - Concatenate strings:
String result = [Link]()
.collect([Link](", ", "[", "]"));
// Result: "[hello, world, java]"
Q44: Implement: Find first non-repeated character in a string
Answer:
Solution using Streams and LinkedHashMap (preserves insertion order):
public static Character findFirstNonRepeated(String str) {
Map<Character, Long> charCount = [Link]()
.mapToObj(c -> (char) c)
.collect([Link](
c -> c,
LinkedHashMap::new, // Preserve order
[Link]()
));
return [Link]().stream()
.filter(entry -> [Link]() == 1)
.map([Link]::getKey)
.findFirst()
.orElse(null);
}
// Test
findFirstNonRepeated("leetcode"); // Returns 'l'
findFirstNonRepeated("loveleetcode"); // Returns 'v'
Step-by-step:
1. Convert string to character stream
2. Group by character, count occurrences (LinkedHashMap preserves order)
3. Filter entries with count = 1
4. Return first match
Q45: When should you use parallel streams?
Answer: Use parallel streams when:
Good scenarios:
1. Large data sets (>10,000 elements for benefit to outweigh overhead)
2. CPU-intensive operations (complex calculations per element)
3. Stateless operations (no shared mutable state)
4. Associative operations (order doesn't matter)
Bad scenarios:
1. Small data sets: Overhead exceeds benefit
2. I/O operations: Threads blocked on I/O, not CPU-bound
3. Ordered operations: forEachOrdered() defeats parallelism
4. Shared mutable state: Race conditions
Example - Good use:
// CPU-intensive: calculate primes for large dataset
List<Integer> primes = [Link]()
.filter(n -> isPrime(n)) // CPU-intensive check
.collect([Link]());
Example - Bad use:
// I/O-bound: parallel doesn't help
List<String> content = [Link]()
.map(file -> readFile(file)) // Blocked on I/O
.collect([Link]());
Warning: Parallel streams use common ForkJoinPool. Blocking operations can starve
other parallel streams in application.
9. Immutable Objects
Q46: What is an immutable object?
Answer: An immutable object is an object whose state cannot be modified after creation.
All fields are effectively final.
Characteristics:
Cannot change state after construction
All fields are final
No setter methods
Thread-safe by nature
Safe to use as HashMap keys
Built-in examples: String, Integer, LocalDate, BigDecimal
Benefits:
Thread-safety: No synchronization needed
Simplified code: No defensive copying needed
Hashcode stability: Safe for hash-based collections
Caching: Can cache and reuse instances
Drawbacks:
More objects created (new instance for each "modification")
Higher memory usage for frequently changing data
Q47: How do you create an immutable class?
Answer: Follow these requirements:
1. Make the class final (prevent subclassing)
2. Make all fields private final
3. No setter methods
4. Initialize all fields via constructor
5. Perform deep copy for mutable objects
6. Return defensive copies of mutable fields
Implementation example:
public final class Person {
private final String name;
private final int age;
private final Date birthDate; // Date is mutable!
public Person(String name, int age, Date birthDate) {
[Link] = name;
[Link] = age;
// Deep copy to protect against external modification
[Link] = new Date([Link]());
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public Date getBirthDate() {
// Return defensive copy
return new Date([Link]());
}
}
Why each requirement:
final class: Prevents subclass from adding mutable state
private final fields: No direct access or reassignment
No setters: No way to modify after construction
Deep copy in constructor: Caller can't modify original after passing
Defensive copy in getter: Caller can't modify internal state
Q48: Why should immutable classes be final?
Answer: Making the class final prevents subclassing, which could break immutability.
Risk without final:
// Immutable class without final keyword
public class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
// Attacker creates mutable subclass
public class MutablePoint extends ImmutablePoint {
private int x;
private int y;
public MutablePoint(int x, int y) {
super(0, 0); // Dummy values
this.x = x;
this.y = y;
}
@Override
public int getX() { return x; }
@Override
public int getY() { return y; }
// Mutability added!
public void setX(int x) { this.x = x; }
public void setY(int y) { this.y = y; }
}
// Now "immutable" reference is actually mutable
ImmutablePoint point = new MutablePoint(5, 10);
((MutablePoint) point).setX(20); // Mutated!
Solution: Make class final:
public final class ImmutablePoint {
// Cannot be subclassed - immutability guaranteed
}
Q49: How to protect immutable class if it contains mutable objects?
Answer: Use defensive copying - create copies when receiving and returning mutable
objects.
Problem without protection:
// BAD: Mutable reference exposes internal state
public final class Person {
private final String name;
private final Date birthDate;
public Person(String name, Date birthDate) {
[Link] = name;
[Link] = birthDate; // Shares reference!
}
public Date getBirthDate() {
return birthDate; // Exposes internal mutable object!
}
// Caller can mutate internal state
Date date = new Date();
Person person = new Person("Alice", date);
[Link](0); // Modifies person's internal birthDate!
Date bd = [Link]();
[Link](0); // Also modifies internal state!
Solution with defensive copying:
// GOOD: Defensive copies protect immutability
public final class Person {
private final String name;
private final Date birthDate;
public Person(String name, Date birthDate) {
[Link] = name;
// Deep copy in constructor
[Link] = new Date([Link]());
}
public Date getBirthDate() {
// Return defensive copy
return new Date([Link]());
}
For collections:
public final class Team {
private final List<String> members;
public Team(List<String> members) {
// Deep copy
[Link] = new ArrayList<>(members);
}
public List<String> getMembers() {
// Return unmodifiable view
return [Link](members);
}
Q50: Why are immutable objects safe for multithreading?
Answer: Immutable objects are inherently thread-safe without synchronization because:
Key reasons:
1. No state changes: State cannot be modified after construction, so no race conditions
2. Visibility guaranteed: final fields have special memory semantics ensuring
visibility across threads
3. No synchronization needed: No locks, no performance overhead
4. Safe publication: Can be safely published to other threads without synchronization
Thread-safety without immutability (requires synchronization):
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // Requires synchronization
}
Thread-safety with immutability (no synchronization):
public final class ImmutableCounter {
private final int count;
public ImmutableCounter(int count) {
[Link] = count;
}
public ImmutableCounter increment() {
return new ImmutableCounter(count + 1); // No locks needed
}
Additional benefits:
No deadlock risk
No need for defensive copying in multithreaded context
Can be safely cached and shared
Simplified concurrent programming
Real-world usage: String class is immutable, allowing safe sharing across threads in Java
applications.
10. equals() & hashCode() Contract
Q51: What's the difference between equals() and hashCode()?
Answer:
equals():
Determines logical equality between objects
Compares object content/state
Used by collections to check if objects are equal
Default implementation: reference equality (this == obj)
hashCode():
Returns integer hash code for object
Used by hash-based collections (HashMap, HashSet)
Objects with same state should return same hash code
Default implementation: memory address-based
Relationship: They must maintain a contract for hash-based collections to work correctly.
Example:
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != [Link]()) return false;
Person person = (Person) obj;
return age == [Link] &&
[Link](name, [Link]);
}
@Override
public int hashCode() {
return [Link](name, age);
}
Q52: Explain the contract between equals() and hashCode()
Answer: The equals-hashCode contract has three rules:
Contract rules:
1. Consistency with equals: If [Link](b) returns true, then [Link]() must equal
[Link]()
2. Multiple invocations: Multiple hashCode() calls on same object must return same
value (if object unchanged)
3. Unequal objects: If [Link](b) returns false, hashCode() values may or may not
differ (but different values improve hash table performance)
Why this contract exists:
Hash-based collections use hashCode() to locate bucket, then equals() to verify equality.
Breaking the contract causes:
Duplicate entries in HashSet
HashMap unable to find values
Unpredictable collection behavior
Example of violation:
// BAD: equals() overridden but not hashCode()
public class Person {
private String name;
@Override
public boolean equals(Object obj) {
if (obj instanceof Person) {
return [Link](((Person) obj).name);
}
return false;
}
// hashCode() not overridden - uses default memory-based hash
}
// Consequence
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
Set<Person> set = new HashSet<>();
[Link](p1);
[Link](p2); // Both added despite being equal!
[Link]([Link]()); // 2 (should be 1)
Correct implementation: Always override both together.
Q53: What happens if you override equals() but not hashCode()?
Answer: Hash-based collections malfunction because the contract is violated.
Specific problems:
HashMap cannot find values:
Person p1 = new Person("Alice");
Map<Person, String> map = new HashMap<>();
[Link](p1, "Engineer");
Person p2 = new Person("Alice"); // Equal to p1 via equals()
String role = [Link](p2); // Returns null!
// Why: Different hashCode() → different bucket → value not found
HashSet allows duplicates:
Set<Person> set = new HashSet<>();
[Link](new Person("Alice"));
[Link](new Person("Alice")); // Should be rejected
[Link]([Link]()); // 2 (should be 1)
// Why: Different hashCode() → different buckets → both stored
Contains() fails:
List<Person> list = new ArrayList<>();
[Link](new Person("Alice"));
boolean contains = [Link](new Person("Alice")); // false!
// Why: equals() returns true but used with wrong hash bucket
Rule: If you override equals(), you must override hashCode() to maintain contract.
Q54: Show implementation of equals() and hashCode() for a custom class
Answer:
public class Employee {
private final int id;
private final String name;
private final String department;
public Employee(int id, String name, String department) {
[Link] = id;
[Link] = name;
[Link] = department;
}
@Override
public boolean equals(Object obj) {
// 1. Check reference equality (optimization)
if (this == obj) {
return true;
}
// 2. Check null and class type
if (obj == null || getClass() != [Link]()) {
return false;
}
// 3. Cast and compare fields
Employee employee = (Employee) obj;
return id == [Link] &&
[Link](name, [Link]) &&
[Link](department, [Link]);
}
@Override
public int hashCode() {
// Use [Link]() for consistent hashing
return [Link](id, name, department);
}
Key implementation points:
1. Reference check first: if (this == obj) return true - performance optimization
2. Null check: if (obj == null) return false
3. Type check: getClass() != [Link]() - ensures same runtime type
4. Field comparison: Compare all significant fields
5. Use [Link](): Handles null safely
6. Consistent hashCode(): Use same fields in both methods
Testing:
Employee e1 = new Employee(1, "Alice", "Engineering");
Employee e2 = new Employee(1, "Alice", "Engineering");
Employee e3 = new Employee(2, "Bob", "Sales");
[Link]([Link](e2)); // true
[Link]([Link]() == [Link]()); // true
[Link]([Link](e3)); // false
Q55: How does HashMap use equals() and hashCode()?
Answer: HashMap uses both methods in a two-step lookup process.
PUT operation:
[Link](key, value);
// Step 1: Calculate bucket index
int hash = [Link]();
int index = hash & (capacity - 1); // Determine bucket
// Step 2: Check if key exists in bucket
for (Entry entry : bucket[index]) {
if ([Link](key)) { // Use equals() to compare
[Link] = value; // Update existing
return;
}
}
// Key not found, add new entry to bucket
GET operation:
value = [Link](key);
// Step 1: Find bucket using hashCode()
int index = [Link]() & (capacity - 1);
// Step 2: Find entry in bucket using equals()
for (Entry entry : bucket[index]) {
if ([Link](key)) {
return [Link]; // Found!
}
}
return null; // Not found
Why both are needed:
hashCode(): Fast bucket location (O(1) average)
equals(): Accurate key comparison within bucket (handles collisions)
Performance impact of bad hashCode():
// BAD: All objects return same hash
@Override
public int hashCode() {
return 1; // All keys go to same bucket!
}
// HashMap degrades to O(n) - effectively a linked list
11. String Handling
Q56: Why is String immutable in Java?
Answer: Strings are immutable for several critical reasons:
1. String Pool optimization:
Literal strings stored in pool
Multiple references share same object
Saves memory for duplicate strings
Only safe if strings cannot be modified
2. Security:
Strings used for sensitive data (passwords, file paths, network connections)
Immutability prevents malicious modification
Example: If String were mutable, changing a filename could redirect file access
3. Thread-safety:
Immutable objects inherently thread-safe
No synchronization needed for String operations
Safe to share across threads
4. Hashcode caching:
String can cache hashCode (calculated once)
Safe because value never changes
Performance benefit for hash-based collections
5. Class loading:
Class names are strings
JVM depends on immutability for class loading security
Example showing benefits:
String s1 = "hello";
String s2 = "hello"; // Same object from pool
[Link](s1 == s2); // true (safe due to immutability)
// If String were mutable:
[Link]("world"); // Would affect s2 too!
Q57: What is String Pool and how does it optimize memory?
Answer: String Pool (String Constant Pool) is a special memory region in the Java heap
where string literals are stored.
How it works:
1. Literal creation: JVM checks if string exists in pool
2. If exists: Return reference to existing string
3. If not exists: Add new string to pool, return reference
4. Result: Multiple references to same literal share one object
Example:
String s1 = "hello"; // Created in pool
String s2 = "hello"; // Reuses s1 from pool
String s3 = "hello"; // Reuses s1 from pool
[Link](s1 == s2); // true (same object)
[Link](s1 == s3); // true (same object)
String s4 = new String("hello"); // Created in heap, not pool
[Link](s1 == s4); // false (different objects)
Memory optimization:
Without pool:
s1 → ["hello"] (5 bytes)
s2 → ["hello"] (5 bytes)
s3 → ["hello"] (5 bytes)
Total: 15 bytes
With pool:
s1 → ["hello"] (5 bytes)
s2 → ↑
s3 → ↑
Total: 5 bytes (10 bytes saved)
Location: String pool resides in heap memory (moved from PermGen to heap in Java 7).
Q58: What does the intern() method do?
Answer: intern() explicitly adds a string to the String Pool and returns the pooled
reference.
Behavior:
1. If string exists in pool: Return pooled reference
2. If not in pool: Add string to pool, return reference
Usage:
String s1 = new String("hello"); // Heap object
String s2 = [Link](); // Pool reference
String s3 = "hello"; // Pool reference
[Link](s1 == s2); // false (heap vs pool)
[Link](s2 == s3); // true (both from pool)
When to use intern():
Large number of duplicate strings
Memory optimization for string-heavy applications
Canonical string representation
When to avoid:
Strings are mostly unique (wastes pool space)
Short-lived strings (unnecessary overhead)
High-frequency string creation (pool lookup cost)
Real-world example:
// Processing large dataset with repeated values
List<String> countries = readMillionsOfRecords(); // Many duplicates
// Without intern: Millions of duplicate "USA", "Germany", etc.
// With intern: One pool entry per unique country
List<String> optimized = [Link]()
.map(String::intern)
.collect([Link]());
// Significant memory savings
Warning: Interned strings live until GC determines no references exist, so excessive
interning can cause memory issues.
Q59: Explain the difference between String created with literal vs new
operator
Answer:
String literal ("text"):
Created in String Pool
Reused if same literal exists
Memory efficient
Recommended approach
new String() constructor:
Created in heap memory
Always creates new object
Not pooled automatically
Memory inefficient
Example:
// Literal - String Pool
String s1 = "hello";
String s2 = "hello";
[Link](s1 == s2); // true (same pool object)
// new operator - Heap
String s3 = new String("hello");
String s4 = new String("hello");
[Link](s3 == s4); // false (different heap objects)
[Link](s1 == s3); // false (pool vs heap)
// Equality check
[Link]([Link](s3)); // true (same content)
Memory visualization:
String Pool:
"hello" ← s1, s2
Heap:
String object ("hello") ← s3
String object ("hello") ← s4
Intern bridge:
String s3 = new String("hello");
String s5 = [Link](); // Returns pool reference
[Link](s1 == s5); // true (both from pool)
Best practice: Use literals unless you specifically need a new heap object.
Q60: Are interned strings eligible for garbage collection?
Answer: Yes, interned strings can be garbage collected if no references exist.
Historical context:
Before Java 7 : String pool in PermGen (permanent generation) - rarely collected
Java 7+ : String pool moved to heap - eligible for regular GC
Garbage collection behavior:
String s1 = new String("temporary").intern();
// s1 references pooled "temporary"
s1 = null; // No more references to "temporary"
// At next GC, "temporary" can be removed from pool if no other references
String literals (compile-time constants) are different:
String s = "hello"; // Literal referenced by compiled class
// "hello" persists as long as class is loaded
Practical implications:
Safe to intern many strings without permanent memory leak
Pool size can grow and shrink based on usage
Don't worry about pool exhaustion in modern Java
When strings are NOT collected:
Literal strings in code (referenced by class metadata)
Interned strings with active references
Strings in long-lived collections
12. Volatile Keyword & Concurrency
Q61: What is volatile keyword and when to use it?
Answer: volatile ensures changes to a variable are immediately visible to all threads.
Problem without volatile:
public class Task {
private boolean running = true; // Cached in thread's CPU cache
public void run() {
while (running) { // May never see update from another thread
// work
}
}
public void stop() {
running = false; // Update might not be visible to run() thread
}
}
Solution with volatile:
public class Task {
private volatile boolean running = true; // Visibility guaranteed
public void run() {
while (running) { // Always sees latest value
// work
}
}
public void stop() {
running = false; // Immediately visible to all threads
}
When to use volatile:
1. Simple flags: Boolean flags controlling thread loops
2. Status indicators: Variables checked by multiple threads
3. Single writer, multiple readers: One thread writes, others read
4. No compound operations: Simple reads and writes only
When NOT to use:
Compound operations: count++ (use AtomicInteger)
Multiple variables that must update together (use synchronized)
Complex state changes (use locks)
Q62: Does volatile make variables atomic?
Answer: No, volatile only guarantees visibility, not atomicity for compound operations.
What volatile provides:
✅ Visibility: Changes visible across threads
✅ Ordering: Prevents instruction reordering
❌ Atomicity: Does NOT make compound operations atomic
Example - volatile is NOT enough:
private volatile int count = 0;
public void increment() {
count++; // NOT ATOMIC even with volatile
// Expands to: int temp = count; temp++; count = temp;
// Race condition between read and write
}
Thread interleaving example:
Thread 1: read count (0) → increment (1) → [paused]
Thread 2: read count (0) → increment (1) → write count (1)
Thread 1: [resumed] → write count (1)
Result: count = 1 (should be 2 - lost update!)
Solution for atomic operations:
// Option 1: AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
[Link](); // Atomic
}
// Option 2: synchronized
private int count = 0;
public synchronized void increment() {
count++; // Atomic due to mutual exclusion
}
When volatile alone is sufficient:
// Simple assignment - atomic operation
private volatile boolean flag = false;
public void setFlag(boolean value) {
flag = value; // Single write - atomic
}
Q63: Show real-world example using volatile as flag variable
Answer:
Scenario: Background task that can be stopped by main thread
public class DataProcessor implements Runnable {
private volatile boolean running = true;
private volatile boolean paused = false;
@Override
public void run() {
while (running) {
if (!paused) {
processNextBatch();
} else {
try {
[Link](100); // Wait while paused
} catch (InterruptedException e) {
[Link]().interrupt();
}
}
}
cleanup();
}
private void processNextBatch() {
// Process data
}
private void cleanup() {
// Release resources
}
// Control methods called from other threads
public void pause() {
paused = true; // Visible to run() thread
}
public void resume() {
paused = false; // Visible to run() thread
}
public void stop() {
running = false; // Visible to run() thread
}
// Usage
DataProcessor processor = new DataProcessor();
Thread thread = new Thread(processor);
[Link]();
// From main thread
[Link](5000);
[Link](); // Pause processing
[Link](2000);
[Link](); // Resume processing
[Link](5000);
[Link](); // Stop gracefully
[Link](); // Wait for completion
Why volatile is correct here:
Simple boolean flags
Single writer (control methods), single reader (run loop)
No compound operations
Visibility is the only requirement
Q64: When would you prefer volatile over synchronized?
Answer: Prefer volatile when all these conditions are met:
Use volatile when:
1. Read/write only: Simple reads and writes, no compound operations
2. Single writer: Only one thread modifies the variable
3. Independent updates: Variable updates don't depend on current value
4. Performance critical: Need lowest possible overhead
5. No atomicity needed: Don't need to lock multiple variables together
Volatile example:
public class Configuration {
private volatile boolean debugMode = false;
// Many threads read
public boolean isDebugMode() {
return debugMode; // Fast read, no lock
}
// One admin thread writes
public void setDebugMode(boolean mode) {
debugMode = mode; // Simple write, no lock
}
Synchronized needed when:
public class Counter {
private int count = 0; // volatile wouldn't help here
public synchronized void increment() {
count++; // Compound operation needs atomicity
}
public synchronized int getCount() {
return count;
}
Performance comparison:
volatile: Minimal overhead (memory barrier only)
synchronized: Lock acquisition/release overhead, thread contention
Rule of thumb: If you're tempted to write volatile int count++, use AtomicInteger or
synchronized instead.
Advanced Practice Questions
Q65: Implement thread-safe Singleton pattern using volatile
Answer:
Double-Checked Locking with volatile:
public class Singleton {
// volatile prevents instruction reordering
private static volatile Singleton instance;
private Singleton() {
// Private constructor
}
public static Singleton getInstance() {
if (instance == null) { // First check (no locking)
synchronized ([Link]) {
if (instance == null) { // Second check (with lock)
instance = new Singleton();
}
}
}
return instance;
}
}
Why volatile is critical:
Without volatile, object construction can be reordered:
Thread 1: Allocate memory → Assign reference → Initialize object
Thread 2: Sees reference != null → Returns half-initialized object!
With volatile, happens-before guarantee ensures full initialization before reference
assignment visible.
Alternative - Initialization-on-demand holder:
public class Singleton {
private Singleton() { }
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return [Link];
}
}
Thread-safe, lazy, no volatile or synchronization needed (relies on class initialization
guarantees).
Q66: Design a producer-consumer pattern using BlockingQueue
Answer:
import [Link].*;
public class ProducerConsumerExample {
private static final int CAPACITY = 10;
private static final BlockingQueue<Integer> queue =
new ArrayBlockingQueue<>(CAPACITY);
static class Producer implements Runnable {
@Override
public void run() {
try {
for (int i = 1; i <= 20; i++) {
[Link](i); // Blocks if queue full
[Link]("Produced: " + i);
[Link](100);
}
} catch (InterruptedException e) {
[Link]().interrupt();
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
Integer item = [Link](); // Blocks if queue empty
[Link]("Consumed: " + item);
[Link](200);
}
} catch (InterruptedException e) {
[Link]().interrupt();
}
}
}
public static void main(String[] args) {
ExecutorService executor = [Link](3);
[Link](new Producer());
[Link](new Consumer());
[Link](new Consumer());
// Let it run for 10 seconds
try {
[Link](10000);
} catch (InterruptedException e) {
[Link]();
}
[Link]();
}
Why BlockingQueue:
Thread-safe without explicit synchronization
Automatic blocking when queue full/empty
No need for wait/notify mechanism
Clean separation of producer and consumer logic
Q67: Implement a thread-safe lazy-initialized cache
Answer:
import [Link];
import [Link];
public class LazyCache<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
private final Function<K, V> loader;
public LazyCache(Function<K, V> loader) {
[Link] = loader;
}
public V get(K key) {
// computeIfAbsent is atomic
return [Link](key, k -> {
[Link]("Loading: " + k);
return [Link](k);
});
}
public void invalidate(K key) {
[Link](key);
}
public void clear() {
[Link]();
}
}
// Usage
LazyCache<Integer, String> userCache = new LazyCache<>(userId -> {
// Expensive database lookup
return [Link](userId);
});
// First call: loads from database
String user1 = [Link](123);
// Second call: returns cached value
String user2 = [Link](123);
Thread-safety features:
ConcurrentHashMap: Thread-safe without locking entire map
computeIfAbsent(): Atomic check-and-compute operation
Multiple threads can safely call get() concurrently
Only one thread computes value for each key
Q68: Explain how ConcurrentHashMap achieves thread-safety
Answer: ConcurrentHashMap uses segment-level locking (Java 7) and CAS operations
(Java 8+) for high-concurrency performance.
Java 8+ approach (current):
1. Lock-free reads: get() operations don't acquire locks
2. CAS for updates: Compare-And-Swap for atomic updates
3. Synchronized bins: Lock individual buckets only during write conflicts
4. Tree bins: Converts to tree structure when bin size exceeds 8
Key differences from synchronized HashMap:
Feature ConcurrentHashMap [Link]
Read locking Lock-free Locks entire map
Write locking Per-bin Locks entire map
Null keys/values Not allowed Allowed
Iterators Weakly consistent Fail-fast
Concurrency High Low
Table 14: ConcurrentHashMap vs synchronized HashMap
Performance:
// High contention scenario - many threads reading and writing
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Multiple threads can:
[Link]("key1", 1); // Thread 1 - locks only "key1" bin
[Link]("key2"); // Thread 2 - no lock needed
[Link]("key3", 3); // Thread 3 - locks different bin
// All operations proceed concurrently
Special atomic operations:
// Atomic put if absent
[Link](key, value);
// Atomic compute
[Link](key, (k, v) -> v == null ? 1 : v + 1);
// Atomic update
[Link](key, (k, v) -> v + 1);
Scenario-Based Questions
Q69: How would you implement a thread-safe rate limiter?
Answer:
Token bucket algorithm using AtomicInteger:
public class RateLimiter {
private final int maxTokens;
private final long refillIntervalMs;
private final AtomicInteger tokens;
private volatile long lastRefillTime;
public RateLimiter(int maxTokens, long refillIntervalMs) {
[Link] = maxTokens;
[Link] = refillIntervalMs;
[Link] = new AtomicInteger(maxTokens);
[Link] = [Link]();
}
public boolean tryAcquire() {
refillTokens();
return [Link](current ->
current > 0 ? current - 1 : current
) > 0;
}
private synchronized void refillTokens() {
long now = [Link]();
if (now - lastRefillTime >= refillIntervalMs) {
[Link](maxTokens);
lastRefillTime = now;
}
}
}
// Usage
RateLimiter limiter = new RateLimiter(10, 1000); // 10 requests per second
if ([Link]()) {
// Process request
} else {
// Reject - rate limit exceeded
}
Design choices:
AtomicInteger for lock-free token decrements
volatile for lastRefillTime visibility
synchronized on refillTokens to prevent race condition on refill
getAndUpdate() for atomic decrement with boundary check
Q70: Design a LRU cache using LinkedHashMap
Answer:
import [Link];
import [Link];
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
// Initial capacity, load factor, accessOrder=true
super(capacity, 0.75f, true);
[Link] = capacity;
}
@Override
protected boolean removeEldestEntry([Link]<K, V> eldest) {
// Remove oldest when size exceeds capacity
return size() > capacity;
}
}
// Usage
LRUCache<String, String> cache = new LRUCache<>(3);
[Link]("1", "One");
[Link]("2", "Two");
[Link]("3", "Three");
// Cache: {1=One, 2=Two, 3=Three}
[Link]("1"); // Access "1" - moves to end
// Cache: {2=Two, 3=Three, 1=One}
[Link]("4", "Four"); // Exceeds capacity - removes "2"
// Cache: {3=Three, 1=One, 4=Four}
Thread-safe version:
public class ThreadSafeLRUCache<K, V> {
private final Map<K, V> cache;
public ThreadSafeLRUCache(int capacity) {
cache = [Link](new LinkedHashMap<K, V>(
capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry([Link]<K, V> eldest) {
return size() > capacity;
}
});
}
public V get(K key) {
synchronized (cache) {
return [Link](key);
}
}
public void put(K key, V value) {
synchronized (cache) {
[Link](key, value);
}
}
}
Key features:
accessOrder=true: Reorders on access (LRU behavior)
removeEldestEntry(): Automatic eviction of oldest entry
Thread-safety via synchronization for production use
Q71: How would you prevent memory leaks in Java?
Answer: Common memory leak causes and prevention:
1. Unclosed resources:
// BAD
FileInputStream fis = new FileInputStream(file);
// If exception occurs, stream never closed
// GOOD: Try-with-resources
try (FileInputStream fis = new FileInputStream(file)) {
// Use stream
} // Automatically closed
2. Static collections:
// BAD: Static collection grows indefinitely
public class Cache {
private static final Map<String, Object> cache = new HashMap<>();
public static void add(String key, Object value) {
[Link](key, value); // Never removed!
}
}
// GOOD: Use weak references or eviction policy
private static final Map<String, Object> cache = new WeakHashMap<>();
// OR size-limited cache
private static final Map<String, Object> cache = new LRUCache<>(1000);
3. Forgotten listeners:
// BAD: Listener never removed
[Link](listener);
// Object can't be GC'd while button exists
// GOOD: Remove listener when done
[Link](listener);
// ALTERNATIVE: Use weak references
public class WeakListenerList {
private List<WeakReference<Listener>> listeners = new ArrayList<>();
}
4. Thread locals not cleaned:
// BAD
ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
[Link](connection);
// In thread pool, thread reused without cleanup
// GOOD
try {
[Link](connection);
// Use connection
} finally {
[Link](); // Clean up
}
5. Improper equals/hashCode:
// BAD: Mutable key in HashMap
Person key = new Person("Alice");
[Link](key, value);
[Link]("Bob"); // Changes hashCode!
[Link](key); // Returns null - can't find in new bucket
// Original entry leaks (unreachable but not GC'd)
// GOOD: Use immutable keys
Prevention checklist:
Close all resources (use try-with-resources)
Remove listeners when no longer needed
Clear ThreadLocal variables in finally blocks
Use weak references for caches
Limit static collection sizes
Use immutable objects as map keys
Profile with tools (VisualVM, JProfiler)
Coding Exercises for Practice
Exercise 1: Implement Custom ArrayList with Generics
public class CustomArrayList<E> {
private static final int DEFAULT_CAPACITY = 10;
private Object[] elements;
private int size = 0;
public CustomArrayList() {
elements = new Object[DEFAULT_CAPACITY];
}
public void add(E element) {
if (size == [Link]) {
resize();
}
elements[size++] = element;
}
@SuppressWarnings("unchecked")
public E get(int index) {
if (index >= size || index < 0) {
throw new IndexOutOfBoundsException();
}
return (E) elements[index];
}
public int size() {
return size;
}
private void resize() {
int newCapacity = [Link] + ([Link] >> 1); // 1.5x
elements = [Link](elements, newCapacity);
}
Exercise 2: Thread-Safe Bank Account
public class BankAccount {
private double balance;
private final Object lock = new Object();
public BankAccount(double initialBalance) {
[Link] = initialBalance;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
synchronized (lock) {
balance += amount;
}
}
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
synchronized (lock) {
if (balance < amount) {
throw new InsufficientFundsException(balance, amount);
}
balance -= amount;
}
}
public double getBalance() {
synchronized (lock) {
return balance;
}
}
public void transfer(BankAccount target, double amount)
throws InsufficientFundsException {
// Acquire locks in consistent order to prevent deadlock
Object lock1 = [Link](this) <
[Link](target) ? [Link] : [Link];
Object lock2 = lock1 == [Link] ? [Link] : [Link];
synchronized (lock1) {
synchronized (lock2) {
[Link](amount);
[Link](amount);
}
}
}
Exercise 3: Stream Processing - Employee Analytics
class Employee {
String name;
String department;
double salary;
// Constructor, getters
}
public class EmployeeAnalytics {
// Find average salary by department
public Map<String, Double> averageSalaryByDept(List<Employee> employees) {
return [Link]()
.collect([Link](
Employee::getDepartment,
[Link](Employee::getSalary)
));
}
// Find top 3 highest paid employees
public List<Employee> topThreeEarners(List<Employee> employees) {
return [Link]()
.sorted([Link](Employee::getSalary).reversed())
.limit(3)
.collect([Link]());
}
// Count employees per department
public Map<String, Long> employeeCountByDept(List<Employee> employees) {
return [Link]()
.collect([Link](
Employee::getDepartment,
[Link]()
));
}
// Get all unique departments
public Set<String> getAllDepartments(List<Employee> employees) {
return [Link]()
.map(Employee::getDepartment)
.collect([Link]());
}
// Find employees with salary above threshold, grouped by department
public Map<String, List<String>> highEarnersByDept(
List<Employee> employees, double threshold) {
return [Link]()
.filter(e -> [Link]() > threshold)
.collect([Link](
Employee::getDepartment,
[Link](
Employee::getName,
[Link]()
)
));
}
Final Tips for Tipico Interview Success
1. Code Quality: Write clean, readable code with meaningful variable names
2. Test Coverage: Aim for 90%+ unit test coverage on assessment tasks
3. Think Aloud: Verbalize your thought process during live coding
4. Ask Clarifying Questions: Understand requirements before coding
5. Time Management: Allocate 2 hours effectively across multiple problems
6. Edge Cases: Handle null inputs, empty collections, boundary conditions
7. Error Handling: Use appropriate exception handling, don't swallow exceptions
8. SOLID Principles: Demonstrate understanding in code structure
9. Concurrency Awareness: Show thread-safety considerations when relevant
10. Performance: Discuss time/space complexity of your solutions
Study Schedule Recommendation
Week 1-2 : Master OOP, SOLID, Collections (Q1-Q19, Q65-Q68)
Week 2-3 : Deep dive into Concurrency, Memory Management (Q25-Q38, Q69-Q71)
Week 3-4 : Generics, Streams, Immutability, String handling (Q20-Q24, Q39-Q45, Q46-Q60)
Week 4-5 : Mock interviews, coding exercises, weak area review
Daily routine:
Morning: Study 2-3 topics deeply (understand, not memorize)
Afternoon: Code implementations and unit tests
Evening: Practice explaining concepts verbally (2-3 minutes each)
Good luck with your Tipico interview preparation!