0% found this document useful (0 votes)
18 views106 pages

One Stop Solution Java, Springboot, SQL, Testing, Get

The document provides a comprehensive overview of Core Java Fundamentals, including key concepts such as JVM, JRE, and JDK, which are essential for understanding Java's platform independence. It covers data types, variables, operators, control flow statements, strings, arrays, exception handling, access modifiers, and the use of 'this' and 'super' keywords. Additionally, it explains the static keyword and its implications for class members, making it a foundational resource for Java developers.

Uploaded by

rawatabhi651
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
18 views106 pages

One Stop Solution Java, Springboot, SQL, Testing, Get

The document provides a comprehensive overview of Core Java Fundamentals, including key concepts such as JVM, JRE, and JDK, which are essential for understanding Java's platform independence. It covers data types, variables, operators, control flow statements, strings, arrays, exception handling, access modifiers, and the use of 'this' and 'super' keywords. Additionally, it explains the static keyword and its implications for class members, making it a foundational resource for Java developers.

Uploaded by

rawatabhi651
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 106

One stop solution – Java , SpringBoot , SQL , Testing , Git

Let's dive deep into "1. Core Java Fundamentals". This section covers the absolute basics of Java that
every developer, regardless of experience level, must know inside out.

1. Core Java Fundamentals: The Building Blocks of Java

This section forms the foundation of all Java development. A strong grasp here demonstrates your
understanding of how Java works at a fundamental level.

1.1. JVM, JRE, JDK

These three acronyms are often confused but are distinct and crucial for understanding Java's "Write
Once, Run Anywhere" philosophy.

• JVM (Java Virtual Machine):

o What it is: The heart of Java's platform independence. It's an abstract machine that
provides a runtime environment in which Java bytecode can be executed.

o Function: It takes the .class (bytecode) files generated by the Java compiler and
converts them into machine-specific code that the underlying operating system
(Windows, macOS, Linux, etc.) can understand and execute.

o Key Responsibilities:

▪ Loading: Loads .class files.

▪ Verification: Verifies the bytecode for security and correctness.

▪ Execution: Executes the bytecode.

▪ Runtime Environment: Provides the memory areas (Heap, Stack, Method


Area, PC Registers, Native Method Stacks) necessary for program execution.

▪ Garbage Collection: Manages memory automatically.

▪ JIT (Just-In-Time) Compiler: A component of the JVM that compiles


frequently executed bytecode segments into native machine code at runtime
for performance optimization.

o Platform Dependent: While Java source code is platform-independent, the JVM itself
is platform-dependent. You need a specific JVM implementation for each operating
system (e.g., a Windows JVM, a Linux JVM).

• JRE (Java Runtime Environment):

o What it is: A software package that provides the minimum requirements for running
a Java application. It contains the JVM along with the necessary Java class libraries
(like java.lang, java.util, java.io, etc.) and supporting files.

o Function: If you only want to run Java applications (not develop them), the JRE is
sufficient. It contains everything needed to execute compiled Java code.
o Contents: JVM + Java Standard Library (classes).

• JDK (Java Development Kit):

o What it is: A complete software development environment for developing,


compiling, and running Java applications.

o Function: It includes everything in the JRE (JVM + Java Standard Library) PLUS
development tools like:

▪ javac (Java Compiler): Translates Java source code (.java files) into bytecode
(.class files).

▪ java (Java Launcher): Used to launch Java applications.

▪ javadoc: Generates API documentation from source code comments.

▪ jar: Archives Java classes and resources into a single JAR file.

▪ Debugger, Profiler, etc.

o Contents: JRE + Development Tools.

o When to use: If you are a Java developer, you need the JDK.

• "Write Once, Run Anywhere" Principle: This principle is achieved because the Java compiler
(javac) converts your .java source code into platform-agnostic bytecode (.class files). This
bytecode can then be executed by any JVM, regardless of the underlying operating system.
The JVM acts as a translator, making the bytecode executable on the specific machine.

1.2. Data Types

Java is a strongly typed language, meaning every variable must be declared with a data type.

• Primitive Data Types:

o Store direct values.

o Are fixed in size and performance.

o Integer types:

▪ byte: 8-bit signed integer (-128 to 127)

▪ short: 16-bit signed integer (-32,768 to 32,767)

▪ int: 32-bit signed integer (most commonly used)

▪ long: 64-bit signed integer (for very large numbers, append 'L' or 'l')

o Floating-point types:

▪ float: 32-bit single-precision (append 'F' or 'f')

▪ double: 64-bit double-precision (default for decimal numbers)

o Character type:
▪ char: 16-bit Unicode character (can hold any character, including special
symbols)

o Boolean type:

▪ boolean: Represents true or false.

• Object (Reference) Data Types:

o Do not store values directly but store references (memory addresses) to objects.

o Examples: String, Array, Scanner, Integer, custom class objects (e.g., MyClass).

o Default value is null.

o Occupy space in the Heap memory.

1.3. Variables

Variables are containers for storing data values.

• Local Variables:

o Declared inside a method, constructor, or block.

o Accessible only within that method, constructor, or block.

o Must be initialized before use. No default value.

o Stored on the Stack memory.

• Instance Variables (Non-static Fields):

o Declared inside a class but outside any method, constructor, or block.

o Belong to an instance (object) of the class.

o Each object has its own copy of instance variables.

o Default values are assigned if not explicitly initialized (e.g., 0 for numeric, false for
boolean, null for objects).

o Stored on the Heap memory.

• Static Variables (Class Variables):

o Declared inside a class but outside any method, constructor, or block, using the static
keyword.

o Belong to the class itself, not to any specific object.

o Only one copy exists for the entire class, shared by all instances.

o Can be accessed directly using the class name (e.g., ClassName.staticVariable).

o Default values are assigned.

o Stored in the Method Area (part of Heap in modern JVMs).

• final Variables:
o A variable declared final can be assigned a value only once. Its value cannot be
changed after initialization.

o For primitive types, final means the value itself is constant.

o For reference types, final means the reference itself cannot be changed to point to
another object. The contents of the object it points to can still be modified (unless
the object itself is immutable, like String).

1.4. Operators

Symbols that perform operations on variables and values.

• Arithmetic Operators: +, -, *, /, % (modulus)

• Relational Operators: == (equality), != (not equal), <, >, <=, >= (return boolean)

• Logical Operators: && (logical AND), || (logical OR), ! (logical NOT)

• Bitwise Operators: &, |, ^, ~, <<, >>, >>> (operate on individual bits)

• Assignment Operators: =, +=, -=, *=, /=, %=, etc.

• Unary Operators: +, -, ++ (increment), -- (decrement)

• Ternary Operator (Conditional Operator): condition ? valueIfTrue : valueIfFalse; (shorthand


for if-else)

• instanceof Operator: Checks if an object is an instance of a particular class or interface.

• Operator Precedence and Associativity: Important for evaluating expressions correctly. (e.g.,
multiplication before addition).

1.5. Control Flow Statements

Statements that control the order in which instructions are executed.

• Conditional Statements:

o if-else: Executes a block of code if a condition is true, otherwise executes another


block.

o if-else if-else: Multiple conditions.

o switch: Allows a variable to be tested for equality against a list of values. Good for
multiple choice scenarios. (break is crucial to prevent fall-through).

• Looping Statements:

o for loop: Executes a block of code a specific number of times. Ideal when you know
the number of iterations.

o Enhanced for loop (for-each): Iterates over arrays or collections without explicitly
managing indices. Read-only access.

o while loop: Executes a block of code repeatedly as long as a condition is true. The
condition is checked before each iteration.
o do-while loop: Executes a block of code at least once, and then repeatedly as long as
a condition is true. The condition is checked after the first iteration.

• Branching Statements:

o break: Terminates the nearest enclosing loop or switch statement.

o continue: Skips the current iteration of a loop and proceeds to the next iteration.

o return: Exits the current method and optionally returns a value.

1.6. Strings

In Java, String is a class, not a primitive type. Strings are sequences of characters.

• String (Immutable):

o String objects are immutable, meaning once created, their content cannot be
changed.

o Any operation that appears to modify a String (e.g., concatenation) actually creates a
new String object.

o This immutability makes String objects thread-safe and suitable for use as keys in
HashMaps.

o String Pool: Java maintains a "String Pool" in the Heap to optimize memory usage.
When you create a String literal (e.g., String s = "hello";), the JVM first checks if
"hello" already exists in the pool. If it does, it returns a reference to the existing
object; otherwise, it creates a new one and puts it in the pool.

o Using new String("hello") always creates a new object in the Heap, even if "hello" is
in the String Pool.

• StringBuilder (Mutable, Not Thread-Safe):

o Used when you need to perform many modifications to a string.

o Provides methods like append(), insert(), delete(), reverse().

o More efficient for string manipulation than String concatenation because it avoids
creating many intermediate String objects.

o Not thread-safe: If multiple threads modify a StringBuilder concurrently, it can lead


to inconsistent results.

• StringBuffer (Mutable, Thread-Safe):

o Similar to StringBuilder in functionality (mutable).

o Thread-safe: All its public methods are synchronized. This makes it suitable for multi-
threaded environments, but it comes with a performance overhead compared to
StringBuilder.

o When to use: In a single-threaded environment, StringBuilder is preferred for


performance. In a multi-threaded environment where multiple threads might modify
the same string, StringBuffer is safer.
1.7. Arrays

A container object that holds a fixed number of values of a single data type.

• Declaration: dataType[] arrayName; or dataType arrayName[];

• Initialization:

o int[] numbers = new int[5]; (creates an array of 5 integers, initialized to 0)

o String[] names = {"Alice", "Bob", "Charlie"}; (initializes with values)

• Accessing Elements: Using index (0-based): arrayName[index].

• length property: arrayName.length gives the size of the array.

• Multidimensional Arrays: Arrays of arrays (e.g., int[][] matrix = new int[3][3];).

• Limitations: Fixed size once created. Cannot grow or shrink dynamically.

1.8. Wrapper Classes

Java's primitive data types are not objects. Wrapper classes provide object representations for
primitive types.

• Purpose:

o To allow primitives to be treated as objects (e.g., when used in Java Collections


Framework, which only stores objects).

o To provide utility methods (e.g., Integer.parseInt(), Double.toString()).

o To support null values (primitives cannot be null).

• Examples:

o byte -> Byte

o short -> Short

o int -> Integer

o long -> Long

o float -> Float

o double -> Double

o char -> Character

o boolean -> Boolean

• Autoboxing and Unboxing (Java 5+):

o Autoboxing: Automatic conversion of a primitive type to its corresponding wrapper


class object.

▪ Integer i = 10; (automatically converts int 10 to Integer object)


o Unboxing: Automatic conversion of a wrapper class object to its corresponding
primitive type.

▪ int j = i; (automatically converts Integer object i to int primitive j)

o This simplifies code by allowing you to mix primitives and wrapper objects in many
contexts.

1.9. Exception Handling

A robust way to handle runtime errors in a program. Prevents program termination and allows for
graceful recovery.

• try block: Contains the code that might throw an exception.

• catch block: Catches specific types of exceptions thrown in the try block. You can have
multiple catch blocks for different exception types (most specific first).

• finally block: Always executes, regardless of whether an exception occurred or was caught.
Used for cleanup code (e.g., closing resources like file streams, database connections).

• throw keyword: Used to explicitly throw an exception from your code.

• throws keyword: Used in a method signature to declare that a method might throw one or
more specified checked exceptions. The caller of the method must then either handle or re-
declare these exceptions.

• Checked vs. Unchecked Exceptions:

o Checked Exceptions:

▪ Must be handled explicitly by the programmer (either by a try-catch block or


by declaring throws in the method signature).

▪ Occur at compile time.

▪ Examples: IOException, SQLException, ClassNotFoundException.

▪ They typically represent external problems that your application can


anticipate and recover from.

o Unchecked Exceptions (Runtime Exceptions):

▪ Do not need to be handled explicitly.

▪ Occur at runtime.

▪ Examples: NullPointerException, ArrayIndexOutOfBoundsException,


ArithmeticException.

▪ They typically represent programming errors that should be fixed rather than
caught (e.g., dividing by zero, accessing an invalid array index).

▪ Error (e.g., OutOfMemoryError, StackOverflowError) are also unchecked and


typically indicate severe problems that the application cannot recover from.

• Custom Exceptions: You can create your own exception classes by extending Exception (for
checked) or RuntimeException (for unchecked).
1.10. Access Modifiers

Keywords that set the accessibility (visibility) of classes, fields, methods, and constructors.

• public: Accessible from anywhere (within the same class, same package, subclasses, and
different packages). Most permissive.

• private: Accessible only within the same class. Most restrictive. Used for encapsulation.

• protected: Accessible within the same package and by subclasses (even if in different
packages).

• Default (No keyword): Accessible only within the same package. Often called "package-
private".

1.11. this and super Keywords

• this keyword:

o Refers to the current object (the object on which the method is invoked).

o Uses:

▪ To differentiate between instance variables and local variables if they have


the same name (this.variable = variable;).

▪ To invoke the current class's constructor from another constructor in the


same class (this(); or this(args);).

▪ To pass the current object as an argument to a method.

▪ To return the current object from a method.

• super keyword:

o Refers to the immediate parent class object.

o Uses:

▪ To call the parent class's constructor (super(); or super(args); must be the


first statement in a child class constructor).

▪ To invoke the parent class's method if it's overridden in the child class
(super.methodName();).

▪ To access parent class's instance variables if they are hidden by a child class's
variables of the same name (super.variableName;).

1.12. static Keyword

A non-access modifier applicable to fields, methods, blocks, and nested classes. It makes a member
belong to the class itself rather than to any specific instance.

• static variable:

o Also known as a class variable.

o Shared by all instances of the class. Only one copy exists.


o Initialized once when the class is loaded.

o Can be accessed directly by the class name (e.g., ClassName.staticVariable).

• static method:

o Belongs to the class, not an object.

o Can be called directly using the class name (e.g., ClassName.staticMethod();).

o Cannot access non-static (instance) variables or non-static methods directly


because they belong to an object which might not exist when the static method is
called.

o Can only access other static members (variables and methods).

o Cannot use this or super keywords.

o Can a static method be overridden? No. Method overriding is a runtime


polymorphism concept that applies to instance methods associated with objects.
Static methods are resolved at compile time based on the reference type, not the
actual object type. This is called "method hiding" if a subclass defines a static
method with the same signature.

• static block:

o Used to initialize static variables.

o Executes only once when the class is loaded into memory.

o Can have multiple static blocks; they execute in the order they appear.

• static nested class (Nested Static Class):

o A static class defined inside another class.

o Does not require an outer class instance to be created.

o Can access only static members of the outer class.

1.13. final Keyword

A non-access modifier used to restrict or prevent changes.

• final variable:

o The value of a final variable can be assigned only once. It becomes a constant.

o Must be initialized at the time of declaration, in a constructor (for instance final


variables), or in a static block (for static final variables).

o For primitive types, the value itself is constant.

o For reference types, the reference cannot be changed to point to another object, but
the object's internal state (if mutable) can still be modified.

• final method:

o A final method cannot be overridden by subclasses.


o Used when you want to ensure that a method's implementation remains consistent
across all subclasses.

• final class:

o A final class cannot be subclassed (inherited from).

o Used to prevent inheritance and ensure security or immutability (e.g., String class is
final).

1.14. Garbage Collection

Automatic memory management in Java.

• What it is: A process that automatically frees up memory occupied by objects that are no
longer reachable (referenced) by the running program.

• JVM Component: It's a daemon thread running in the background of the JVM.

• Advantages:

o Reduces memory leaks.

o Frees developers from manual memory management (like malloc()/free() in C++).

• How it works (Simplified):

o Identifies unreachable objects.

o Reclaims the memory occupied by them.

• System.gc() / Runtime.getRuntime().gc(): These methods are hints to the JVM to run the
garbage collector, but there's no guarantee it will run immediately. JVM decides when to run
GC based on its algorithms and memory needs.

• Memory Areas (Heap is where objects live):

o Heap: Where all objects (instance variables, arrays) are allocated. It's divided into
generations (Young Generation, Old Generation).

o Stack: Where method calls, local variables, and primitive data types are stored. Each
thread has its own stack.

o Method Area (PermGen/Metaspace): Stores class structures, method data, static


variables, etc.

o PC Registers: Stores the address of the next instruction to be executed for each
thread.

o Native Method Stacks: For native methods written in other languages (C/C++).

• Garbage Collection Algorithms (Basic understanding):

o Mark and Sweep: Marks reachable objects, then sweeps away unreachable ones.

o Copying: Copies live objects from one memory area to another, leaving dead objects
behind.
o Mark-Compact: Marks live objects, then compacts them to one end of the heap,
reducing fragmentation.

o Generational GC: Divides the heap into generations (Young, Old) assuming most
objects die young. Different algorithms are used for different generations (e.g.,
Copying for Young, Mark-Compact for Old). Common GCs include Serial, Parallel,
CMS, G1, ZGC, Shenandoah.

1.15. Serialization and Deserialization

• Serialization:

o The process of converting an object's state into a byte stream.

o This byte stream can then be stored in a file, transmitted across a network, or stored
in a database.

o Allows objects to persist or be transferred.

o To make an object serializable, its class must implement the java.io.Serializable


interface (a marker interface, no methods to implement).

o ObjectOutputStream is used for serialization.

• Deserialization:

o The reverse process: converting a byte stream back into a Java object in memory.

o ObjectInputStream is used for deserialization.

• transient keyword:

o If you don't want a particular field of an object to be serialized, declare it as


transient.

o When an object is deserialized, transient fields will be initialized to their default


values (e.g., 0 for int, null for objects).

1.16. Generics (Java 5+)

• Purpose: To provide type safety at compile time and eliminate the need for casting, reducing
runtime errors (ClassCastException).

• Example without Generics: List list = new ArrayList(); list.add("hello"); list.add(123); String s
= (String) list.get(0); Integer i = (Integer) list.get(1); (Here, you have to cast and there's no
compile-time check if you add wrong types).

• Example with Generics: List<String> list = new ArrayList<>(); list.add("hello"); //


list.add(123); // Compile-time error String s = list.get(0); (No casting needed, type safety).

• Type Erasure:

o Generics are primarily a compile-time feature in Java.

o During compilation, generic type information is "erased" and replaced with raw
types (e.g., List<String> becomes List).
o This ensures backward compatibility with older Java versions that didn't support
generics.

o Because of type erasure, you cannot:

▪ Create instances of type parameters (e.g., new T()).

▪ Use instanceof with type parameters (e.g., obj instanceof T).

▪ Create arrays of type parameters (e.g., new T[size]).

• Wildcards (?): Used in generic code to relax the restrictions on a variable's type parameter.

o Unbounded Wildcard (<?>): Represents any type. List<?> can hold a list of any type.

o Upper Bounded Wildcard (<? extends T>): Represents T or any subclass of T. You can
read from such a list, but you cannot add elements (except null) because you don't
know the exact type. (PECS principle: Producer extends).

o Lower Bounded Wildcard (<? super T>): Represents T or any superclass of T. You can
add elements of type T or its subtypes to such a list, but you can only read objects as
Object type. (PECS principle: Consumer super).

1.17. Java 8 Features (Crucial!)

These significantly changed how Java code is written and are almost always a topic of discussion.

• Lambda Expressions:

o What they are: A concise way to represent an anonymous function (a function


without a name).

o Syntax: (parameters) -> expression or (parameters) -> { statements; }

o Use Case: Primarily used to implement functional interfaces. They simplify code for
callbacks and functional programming constructs.

o Example: Collections.sort(list, (s1, s2) -> s1.compareTo(s2));

• Functional Interfaces:

o An interface with exactly one abstract method.

o Annotated with @FunctionalInterface (optional, but good practice).

o Examples: Runnable, Callable, Comparator, Consumer, Predicate, Function, Supplier.

o Lambda expressions are instances of functional interfaces.

• Stream API:

o What it is: A sequence of elements that supports sequential and parallel aggregate
operations.

o Declarative Style: Allows you to express what you want to do (e.g., filter, map,
collect) rather than how (iterating manually).
o Non-terminal (Intermediate) Operations: Return a new stream. They are lazy and
don't process the data until a terminal operation is called. Examples: filter(), map(),
flatMap(), sorted(), distinct(), peek().

o Terminal Operations: Produce a result or a side-effect, and close the stream.


Examples: forEach(), collect(), reduce(), count(), min(), max(), anyMatch(), allMatch(),
noneMatch(), findFirst(), findAny().

o Source -> Zero or more Intermediate Operations -> One Terminal Operation

o No modification of original data source. Streams are designed to be immutable.

• Default Methods in Interfaces:

o What they are: Methods with an implementation provided directly within an


interface, prefixed with the default keyword.

o Purpose: Allows adding new methods to existing interfaces without breaking


backward compatibility for classes that implement that interface.

o Example: interface MyInterface { default void myDefaultMethod() {


System.out.println("Default implementation"); } }

• Optional Class:

o What it is: A container object that may or may not contain a non-null value.

o Purpose: To provide a clear way to represent the absence of a value, helping to avoid
NullPointerException (a common runtime error).

o Methods: isPresent(), isEmpty(), get(), orElse(), orElseThrow(), ifPresent().

o Example: Optional<String> name = Optional.ofNullable(getName());


name.ifPresent(System.out::println); String result = name.orElse("Default Name");

• Date and Time API (java.time package):

o Replacement for: The old java.util.Date and java.util.Calendar classes (which had
issues with thread-safety, mutability, and design flaws).

o Key Classes:

▪ LocalDate: Date without time and timezone (e.g., 2025-07-13).

▪ LocalTime: Time without date and timezone (e.g., 13:02:53).

▪ LocalDateTime: Date and time without timezone (e.g., 2025-07-


13T13:02:53).

▪ Instant: A point in time on the timeline (machine-readable, often used for


timestamps).

▪ ZonedDateTime: Date, time, and timezone.

▪ Duration: Represents a quantity of time in seconds and nanoseconds (e.g.,


difference between two Instants).
▪ Period: Represents a quantity of time in years, months, and days (e.g.,
difference between two LocalDates).

o Features: Immutable, thread-safe, clear API.

This detailed breakdown of "Core Java Fundamentals" should give you a very strong foundation for
your interview. Remember to not just know definitions but also how and when to use these
concepts and be able to provide small code examples.

You got it! Let's dive deep into "2. Object-Oriented Programming (OOP)". This is arguably the most
fundamental paradigm for Java and understanding its principles is crucial for designing robust,
maintainable, and scalable applications.

2. Object-Oriented Programming (OOP): The Foundation of Java Design

OOP is a programming paradigm based on the concept of "objects," which can contain data and
code: data in the form of fields (attributes or properties), and code in the form of procedures
(methods). The primary goal of OOP is to increase the flexibility and maintainability of programs.

2.1. Four Pillars of OOPs

These are the core principles that define object-oriented programming. You should be able to explain
each thoroughly, provide real-world analogies, and demonstrate them with code.

a. Encapsulation

• Concept: The binding of data (attributes/fields) and the methods that operate on that data
into a single unit (a class), and keeping them safe from outside interference and misuse. It's
about "data hiding" or "information hiding."

• How it's achieved in Java:

o Declaring instance variables as private: This prevents direct access from outside the
class.

o Providing public getter and setter methods: These "accessor" and "mutator"
methods are the controlled ways to read and modify the private data. You can add
validation logic within setters.

• Benefits:

o Data Security: Protects data from unauthorized access.

o Flexibility/Maintainability: Allows changes to the internal implementation of a class


without affecting external code that uses it (as long as the public interface, i.e.,
getters/setters, remains consistent).

o Modularity: Makes classes self-contained and easier to reuse.

o Control over Data: You can enforce business rules or validation logic when data is
set.

• Example:
Java

class BankAccount {

private String accountNumber; // Encapsulated data

private double balance; // Encapsulated data

public BankAccount(String accountNumber, double initialBalance) {

this.accountNumber = accountNumber;

if (initialBalance >= 0) { // Validation in constructor

this.balance = initialBalance;

} else {

this.balance = 0; // Or throw an exception

// Public getter for account number (read-only)

public String getAccountNumber() {

return accountNumber;

// Public getter for balance

public double getBalance() {

return balance;

// Public setter/mutator for deposit with validation

public void deposit(double amount) {

if (amount > 0) {

this.balance += amount;

System.out.println("Deposited " + amount + ". New balance: " + this.balance);

} else {

System.out.println("Deposit amount must be positive.");


}

// Public setter/mutator for withdrawal with validation

public void withdraw(double amount) {

if (amount > 0 && this.balance >= amount) {

this.balance -= amount;

System.out.println("Withdrew " + amount + ". New balance: " + this.balance);

} else {

System.out.println("Invalid withdrawal amount or insufficient balance.");

public class EncapsulationDemo {

public static void main(String[] args) {

BankAccount myAccount = new BankAccount("123456789", 1000.0);

// Cannot directly access myAccount.balance = -500; due to private access

System.out.println("Account Number: " + myAccount.getAccountNumber());

System.out.println("Current Balance: " + myAccount.getBalance());

myAccount.deposit(500);

myAccount.withdraw(200);

myAccount.withdraw(2000); // Will fail due to validation

b. Inheritance

• Concept: The mechanism by which one class acquires the properties (fields) and behaviors
(methods) of another class. It represents an "is-a" relationship (e.g., "A Car is-a Vehicle").

• Keywords:
o extends: Used by a class to inherit from another class.

o implements: Used by a class to implement an interface.

• Types of Inheritance (in terms of class hierarchy):

o Single Inheritance: A class inherits from only one direct parent class. (Java supports
this for classes).

o Multilevel Inheritance: A class inherits from a class, which in turn inherits from
another class (e.g., A -> B -> C).

o Hierarchical Inheritance: Multiple classes inherit from a single parent class.

o Multiple Inheritance: A class inheriting from multiple direct parent classes. Java
does NOT support multiple inheritance for classes to avoid the "diamond problem"
(ambiguity if parent classes have methods with same signature).

o Hybrid Inheritance: A mix of two or more types of inheritance.

• Benefits:

o Code Reusability: Child classes don't need to rewrite code already present in the
parent.

o Extensibility: New functionalities can be added by extending existing classes.

o Polymorphism: A key enabler for runtime polymorphism.

• Example:

Java

class Vehicle { // Parent class (Superclass)

String brand;

public Vehicle(String brand) {

this.brand = brand;

public void honk() {

System.out.println("Vehicle sound!");

public void displayBrand() {

System.out.println("Brand: " + brand);

}
}

class Car extends Vehicle { // Child class (Subclass) inherits from Vehicle

String model;

public Car(String brand, String model) {

super(brand); // Calls the parent class constructor

this.model = model;

// Method Overriding (runtime polymorphism)

@Override

public void honk() {

System.out.println("Car horn!");

public void drive() {

System.out.println(brand + " " + model + " is driving.");

class ElectricCar extends Car { // Multilevel inheritance

boolean autonomous;

public ElectricCar(String brand, String model, boolean autonomous) {

super(brand, model);

this.autonomous = autonomous;

@Override

public void honk() {


System.out.println("Silent electric car hum!");

public void charge() {

System.out.println(brand + " " + model + " is charging.");

public class InheritanceDemo {

public static void main(String[] args) {

Car myCar = new Car("Toyota", "Camry");

myCar.displayBrand();

myCar.honk(); // Calls Car's honk() due to overriding

myCar.drive();

ElectricCar tesla = new ElectricCar("Tesla", "Model S", true);

tesla.displayBrand(); // Inherited from Vehicle

tesla.honk(); // Calls ElectricCar's honk()

tesla.charge();

c. Polymorphism

• Concept: "Many forms." It refers to the ability of an object to take on many forms or the
ability of a variable to refer to objects of different types at different times. In Java, it primarily
means that a single interface can be used for different underlying forms (data types).

• Types of Polymorphism in Java:

1. Compile-time Polymorphism (Static Polymorphism / Method Overloading):

▪ Concept: Occurs when there are multiple methods in the same class with
the same name but different method signatures (different number of
parameters, different types of parameters, or different order of parameters).

▪ The compiler decides which method to call based on the arguments passed
at compile time.

▪ Example:
Java

class Calculator {

public int add(int a, int b) {

return a + b;

public double add(double a, double b) { // Overloaded method

return a + b;

public int add(int a, int b, int c) { // Overloaded method

return a + b + c;

// Usage:

Calculator calc = new Calculator();

calc.add(5, 10); // Calls int version

calc.add(5.0, 10.0); // Calls double version

calc.add(1, 2, 3); // Calls three-int version

2. Runtime Polymorphism (Dynamic Polymorphism / Method Overriding):

▪ Concept: Occurs when a subclass provides a specific implementation for a


method that is already defined in its superclass. The method signature
(name, return type, parameters) must be exactly the same.

▪ The decision of which method to call is made at runtime based on the actual
object type, not the reference type. This is also known as "Dynamic Method
Dispatch."

▪ Rules for Overriding:

▪ Method must have the same name, same parameters, and same
return type.

▪ Access modifier cannot be more restrictive than the overridden


method.

▪ final or static methods cannot be overridden.

▪ Constructors cannot be overridden.

▪ Example (refer to Vehicle and Car example in Inheritance):

Java
Vehicle v1 = new Vehicle("Generic");

Vehicle v2 = new Car("Honda", "Civic"); // Polymorphic reference

Vehicle v3 = new ElectricCar("BMW", "iX", false); // Polymorphic reference

v1.honk(); // Output: Vehicle sound!

v2.honk(); // Output: Car horn! (Runtime decision based on actual object Car)

v3.honk(); // Output: Silent electric car hum! (Runtime decision based on ElectricCar)

• Benefits:

o Flexibility: A single interface (method name) can be used to perform different


actions depending on the object.

o Code Reusability: Write generic code that can work with objects of different types,
as long as they share a common supertype.

o Maintainability: Easier to add new types without modifying existing code, just
extend the base class and override.

d. Abstraction

• Concept: The process of hiding the implementation details and showing only the essential
features of an object to the outside world. It's about "what" an object does, not "how" it
does it.

• How it's achieved in Java:

o Abstract Classes:

▪ A class declared with the abstract keyword.

▪ Cannot be instantiated (you cannot create objects of an abstract class).

▪ Can have both abstract methods (methods without an implementation,


marked abstract) and concrete (non-abstract) methods.

▪ Can have constructors (which are called by subclass constructors via super()).

▪ Can have instance variables and static variables.

▪ A subclass of an abstract class must either implement all its abstract


methods or also be declared abstract.

▪ Represents a partial implementation, serving as a base for specific


subclasses.

o Interfaces:

▪ A blueprint of a class. All methods in an interface are implicitly public


abstract (before Java 8).
▪ Can only declare constants (implicitly public static final) and abstract
methods.

▪ Cannot have constructors.

▪ Before Java 8, interfaces could only have abstract methods. From Java 8, they
can have default and static methods with implementations. From Java 9,
they can also have private methods.

▪ A class implements an interface using the implements keyword and must


provide implementations for all its abstract methods (unless the
implementing class is abstract itself).

▪ Represents a contract: any class implementing the interface guarantees to


provide the specified behaviors.

▪ Java supports multiple inheritance of interfaces (a class can implement


multiple interfaces).

• Abstract Class vs. Interface (Key Differences):

Feature Abstract Class Interface

Keyword abstract class interface

Instantiation Cannot be instantiated directly Cannot be instantiated directly

Abstract All methods were implicitly public


Can have zero or more
Methods abstract before Java 8.

From Java 8, can have default and


static methods.

From Java 9, can have private


methods.

Only default and static methods


Concrete Can have concrete methods (with
(and private in Java 9) can have
Methods body)
bodies.

Only public static final variables


Variables Can have instance and static variables
(constants)

Constructors Can have constructors Cannot have constructors


Feature Abstract Class Interface

A class implements multiple


Inheritance A class extends one abstract class.
interfaces.

Defines a contract for behavior.


Defines a template for subclasses with
Good for "can-do" relationships
some common implementation. Good
Purpose or specifying common
for "is-a" relationships where some
functionalities across unrelated
common behavior is shared.
classes.

• When to Use Which:

o Abstract Class: Use when you want to provide a base class with some common
functionality implemented, but also enforce that subclasses implement specific
methods. Ideal when you have a strong "is-a" relationship and want to share code.

o Interface: Use when you want to define a contract that multiple unrelated classes
can adhere to. Ideal for defining capabilities or behaviors that can be shared across
different class hierarchies.

• Example (Abstraction):

Java

// Abstract Class

abstract class Shape {

String name;

public Shape(String name) {

this.name = name;

// Abstract method - must be implemented by subclasses

public abstract double calculateArea();

// Concrete method - has an implementation

public void displayInfo() {

System.out.println("This is a " + name + " shape.");

}
}

class Circle extends Shape {

double radius;

public Circle(String name, double radius) {

super(name);

this.radius = radius;

@Override

public double calculateArea() {

return Math.PI * radius * radius;

class Rectangle extends Shape {

double length;

double width;

public Rectangle(String name, double length, double width) {

super(name);

this.length = length;

this.width = width;

@Override

public double calculateArea() {

return length * width;

}
// Interface

interface Drawable {

void draw(); // Abstract method (implicitly public abstract)

default void setColor(String color) { // Default method (Java 8+)

System.out.println("Setting color to " + color);

class Triangle implements Drawable {

@Override

public void draw() {

System.out.println("Drawing a triangle.");

public class AbstractionDemo {

public static void main(String[] args) {

// Shape myShape = new Shape("Generic"); // Compile-time error: Cannot instantiate abstract


class

Circle circle = new Circle("My Circle", 5.0);

circle.displayInfo();

System.out.println("Circle Area: " + circle.calculateArea());

Rectangle rectangle = new Rectangle("My Rectangle", 4.0, 6.0);

rectangle.displayInfo();

System.out.println("Rectangle Area: " + rectangle.calculateArea());

Triangle triangle = new Triangle();

triangle.draw();
triangle.setColor("Blue"); // Using default method from interface

2.2. Constructors

• Concept: A special type of method used to initialize objects. It's invoked automatically when
an object of a class is created using the new keyword.

• Rules:

o Must have the same name as the class.

o Has no return type (not even void).

o Cannot be static, final, abstract, or synchronized.

• Types of Constructors:

o Default Constructor: If you don't define any constructor, Java automatically provides
a public, no-argument default constructor. It initializes instance variables to their
default values.

o No-argument Constructor: A constructor explicitly defined by you that takes no


arguments.

o Parameterized Constructor: A constructor that takes arguments. Used to initialize


instance variables with specific values provided at object creation.

• Constructor Overloading: A class can have multiple constructors with the same name but
different parameter lists (just like method overloading).

• Constructor Chaining:

o Using this(): To call another constructor of the same class.

o Using super(): To call a constructor of the parent class. super() or super(args) must
be the first statement in a child class constructor.

• Example:

Java

class Dog {

String name;

int age;

// No-argument constructor

public Dog() {

this("Buddy", 3); // Calls the parameterized constructor


System.out.println("Dog object created with default values.");

// Parameterized constructor

public Dog(String name, int age) {

this.name = name;

this.age = age;

System.out.println("Dog object created: " + name + ", " + age + " years old.");

// Another overloaded constructor

public Dog(String name) {

this(name, 0); // Calls the parameterized constructor with default age

System.out.println("Dog object created with name only.");

public void bark() {

System.out.println(name + " says Woof!");

public class ConstructorDemo {

public static void main(String[] args) {

Dog dog1 = new Dog(); // Calls no-arg constructor

dog1.bark();

Dog dog2 = new Dog("Max", 5); // Calls parameterized constructor

dog2.bark();

Dog dog3 = new Dog("Lucy"); // Calls overloaded constructor

dog3.bark();
}

2.3. equals() and hashCode()

These methods (defined in java.lang.Object) are crucial when dealing with collections, especially
HashMap, HashSet, and Hashtable.

• equals() Method:

o Purpose: Used to compare two objects for equality of content (value equality), not
just reference equality (==).

o Default Implementation: In the Object class, equals() behaves like == (compares


memory addresses).

o Overriding: You should override equals() whenever you want to define what it
means for two objects of your class to be logically equal, based on their content.

o Contract (Important!): If you override equals(), you must also override hashCode() to
maintain the general contract for the Object.hashCode() method.

• hashCode() Method:

o Purpose: Returns an integer hash code value for the object. This hash code is
primarily used by hash-based collections (HashMap, HashSet, Hashtable) to
determine where to store and retrieve objects efficiently.

o Default Implementation: In Object class, hashCode() typically returns a unique


integer for each object based on its memory address.

o Contract:

1. If two objects are equals() according to the equals() method, then calling the
hashCode() method on each of the two objects must produce the same
integer result.

2. If two objects are not equals(), their hash codes do not have to be different.
However, different hash codes for unequal objects can improve the
performance of hash tables.

3. The hashCode() result must be consistent: if the object's state (used in


equals() comparisons) is not modified, hashCode() must return the same
value.

• Why override both?

o If you override equals() but not hashCode(), and you put objects of your class into a
HashSet or use them as keys in a HashMap:

▪ HashSet might store two "equal" objects at different locations because their
default hashCode() values (based on memory address) would be different,
leading to duplicate "equal" objects in a set.
▪ HashMap might not be able to retrieve a value you stored because the key
you use for retrieval (which is equals() to the original key) hashes to a
different bucket.

o HashMap first checks hashCode() to narrow down the search to a specific bucket,
then uses equals() to find the exact object within that bucket. If hashCode() values
differ for equal objects, the second equals() check will never happen.

• Example (Overriding equals() and hashCode()):

Java

class Employee {

int id;

String name;

public Employee(int id, String name) {

this.id = id;

this.name = name;

// Overriding equals()

@Override

public boolean equals(Object o) {

if (this == o) return true; // Same object reference

if (o == null || getClass() != o.getClass()) return false; // Null check, type check

Employee employee = (Employee) o; // Downcast

return id == employee.id && // Compare 'id' for equality

name.equals(employee.name); // Compare 'name' for equality

// Overriding hashCode() - essential if equals() is overridden

@Override

public int hashCode() {

int result = id;


result = 31 * result + name.hashCode(); // Prime number (31) for better distribution

return result;

@Override

public String toString() {

return "Employee{id=" + id + ", name='" + name + "'}";

public class EqualsHashCodeDemo {

public static void main(String[] args) {

Employee emp1 = new Employee(1, "Alice");

Employee emp2 = new Employee(1, "Alice"); // Logically equal to emp1

Employee emp3 = new Employee(2, "Bob");

System.out.println("emp1 == emp2: " + (emp1 == emp2)); // false (different objects)

System.out.println("emp1.equals(emp2): " + emp1.equals(emp2)); // true (after override)

System.out.println("Hash Code of emp1: " + emp1.hashCode());

System.out.println("Hash Code of emp2: " + emp2.hashCode());

System.out.println("Hash Code of emp3: " + emp3.hashCode());

// Demonstrate in HashSet

java.util.Set<Employee> employeeSet = new java.util.HashSet<>();

employeeSet.add(emp1);

employeeSet.add(emp2); // This will NOT be added if equals/hashCode are correctly overridden

System.out.println("Size of employeeSet: " + employeeSet.size()); // Should be 1

System.out.println("Set contains emp1: " + employeeSet.contains(emp1)); // true


System.out.println("Set contains emp2: " + employeeSet.contains(emp2)); // true (finds emp1
via hash & equals)

2.4. Composition vs. Inheritance

Two fundamental ways to establish relationships between classes.

• Inheritance (is-a relationship):

o As discussed, one class acquires properties and behaviors of another.

o Example: Car extends Vehicle (A Car is-a Vehicle).

o Pros: Code reuse, facilitates polymorphism.

o Cons: Tight coupling between parent and child classes, can lead to rigid hierarchies,
"fragile base class problem" (changes in base class can break subclasses).

• Composition (has-a relationship):

o A class contains an object of another class as one of its fields (instance variables).

o Instead of inheriting, you use an instance of another class to get its functionality.

o Example: Car has an Engine (A Car has-a Engine).

o Pros:

▪ Loose Coupling: Classes are less dependent on each other.

▪ Flexibility: You can change the component class at runtime or easily swap
implementations.

▪ High Reusability: The component class can be reused independently.

▪ Better Maintainability: Changes in one class are less likely to impact others.

o Cons: Can sometimes involve more initial setup (delegating calls).

• When to Prefer Composition over Inheritance:

o "Favor composition over inheritance" is a common design principle.

o Use inheritance when there's a strong, clear "is-a" relationship and you want to
leverage polymorphism and common base class implementations.

o Use composition when a class has or uses another class's functionality, but isn't
necessarily a specialized version of it. It offers greater flexibility and less coupling.

• Example:

Java

// Composition Example: Car has an Engine

class Engine {
public void start() {

System.out.println("Engine started.");

public void stop() {

System.out.println("Engine stopped.");

class VehicleComposition {

private Engine engine; // Vehicle has an Engine

public VehicleComposition() {

this.engine = new Engine(); // Engine is part of Vehicle

public void startVehicle() {

engine.start(); // Delegating the call

System.out.println("Vehicle started.");

public void stopVehicle() {

engine.stop();

System.out.println("Vehicle stopped.");

public class CompositionDemo {

public static void main(String[] args) {

VehicleComposition myVehicle = new VehicleComposition();

myVehicle.startVehicle();

myVehicle.stopVehicle();
}

2.5. Design Patterns (Basic knowledge for 3 years experience)

While a full deep dive into all design patterns isn't expected, knowing a few common ones and their
purpose demonstrates good design principles.

a. Singleton Pattern

• Purpose: Ensures that a class has only one instance and provides a global point of access to
that instance.

• Use Cases: Logging, configuration management, thread pools, database connection pools,
caching.

• Implementation (Common ways):

1. Lazy Initialization (Thread-safe with synchronized method/block):

Java

class Logger {

private static Logger instance; // Lazily initialized

private Logger() {} // Private constructor to prevent external instantiation

public static synchronized Logger getInstance() { // Thread-safe method

if (instance == null) {

instance = new Logger();

return instance;

public void log(String message) {

System.out.println("Log: " + message);

2. Eager Initialization (Thread-safe, simplest):

Java

class Configuration {

private static Configuration instance = new Configuration(); // Instantiated at class load time

private Configuration() {}
public static Configuration getInstance() {

return instance;

public void loadConfig() {

System.out.println("Configuration loaded.");

3. Double-Checked Locking (More performant thread-safe lazy):

Java

class ConnectionPool {

private static volatile ConnectionPool instance; // volatile for visibility

private ConnectionPool() {}

public static ConnectionPool getInstance() {

if (instance == null) { // First check: no need to synchronize if already created

synchronized (ConnectionPool.class) { // Synchronize on class object

if (instance == null) { // Second check: only create if still null

instance = new ConnectionPool();

return instance;

public void getConnection() { /* ... */ }

4. Static Inner Class (Bill Pugh Singleton - most common and recommended): This is
the most widely accepted approach as it combines lazy loading with thread safety
without explicit synchronization.

Java

class MySingleton {

private MySingleton() {}
// Static inner class. Not loaded until getInstance() is called.

private static class SingletonHelper {

private static final MySingleton INSTANCE = new MySingleton();

public static MySingleton getInstance() {

return SingletonHelper.INSTANCE;

public void doSomething() {

System.out.println("Singleton is doing something.");

5. Enum Singleton (Most robust, handles serialization and reflection attacks):

Java

public enum EnumSingleton {

INSTANCE; // The single instance

public void doSomething() {

System.out.println("Enum Singleton is doing something.");

// Usage: EnumSingleton.INSTANCE.doSomething();

b. Factory Method Pattern

• Purpose: Provides an interface for creating objects in a superclass, but allows subclasses to
alter the type of objects that will be created. It's about "deferring instantiation to
subclasses."

• Use Cases: When a class cannot anticipate the type of objects it needs to create, or when a
class wants its subclasses to specify the objects to be created.1

• Example:2

Java

// Product Interface
interface Notification {

void notifyUser();

// Concrete Products

class EmailNotification implements Notification {

@Override

public void notifyUser() {

System.out.println("Sending an email notification.");

class SMSNotification implements Notification {

@Override

public void notifyUser() {

System.out.println("Sending an SMS notification.");

// Creator Abstract Class (or Interface) with Factory Method

abstract class NotificationFactory {

public abstract Notification createNotification();

// Concrete Creators

class EmailNotificationFactory extends NotificationFactory {

@Override

public Notification createNotification() {

return new EmailNotification();

}
class SMSNotificationFactory extends NotificationFactory {

@Override

public Notification createNotification() {

return new SMSNotification();

public class FactoryMethodDemo {

public static void main(String[] args) {

NotificationFactory emailFactory = new EmailNotificationFactory();

Notification email = emailFactory.createNotification();

email.notifyUser();

NotificationFactory smsFactory = new SMSNotificationFactory();

Notification sms = smsFactory.createNotification();

sms.notifyUser();

c. Builder Pattern

• Purpose: Constructs a complex object step by step. It separates the construction of a


complex object from its representation, so the same construction process can create
different representations.

• Use Cases: When an object has many optional parameters, or when its construction involves
multiple steps or requires various configurations. Avoids "telescoping constructors."

• Example:

Java

class Pizza {

private String dough;

private String sauce;

private String cheese;

private boolean pepperoni;


private boolean mushrooms;

// Private constructor - only accessible by the Builder

private Pizza(PizzaBuilder builder) {

this.dough = builder.dough;

this.sauce = builder.sauce;

this.cheese = builder.cheese;

this.pepperoni = builder.pepperoni;

this.mushrooms = builder.mushrooms;

@Override

public String toString() {

return "Pizza [dough=" + dough + ", sauce=" + sauce + ", cheese=" + cheese +

", pepperoni=" + pepperoni + ", mushrooms=" + mushrooms + "]";

// Static nested Builder class

public static class PizzaBuilder {

private String dough;

private String sauce;

private String cheese;

private boolean pepperoni = false; // Default values

private boolean mushrooms = false; // Default values

public PizzaBuilder(String dough, String sauce, String cheese) {

this.dough = dough;

this.sauce = sauce;

this.cheese = cheese;

}
public PizzaBuilder addPepperoni(boolean pepperoni) {

this.pepperoni = pepperoni;

return this; // Return builder for method chaining

public PizzaBuilder addMushrooms(boolean mushrooms) {

this.mushrooms = mushrooms;

return this;

public Pizza build() {

return new Pizza(this); // Create Pizza object

public class BuilderPatternDemo {

public static void main(String[] args) {

Pizza veggiePizza = new Pizza.PizzaBuilder("Thin Crust", "Tomato", "Mozzarella")

.addMushrooms(true)

.build();

System.out.println(veggiePizza);

Pizza meatLovers = new Pizza.PizzaBuilder("Thick Crust", "BBQ", "Cheddar")

.addPepperoni(true)

.build();

System.out.println(meatLovers);

d. Observer Pattern
• Purpose: Defines a one-to-many dependency between objects so that when one object
changes state, all its dependents are notified and updated automatically.

• Use Cases: Event handling systems, real-time data updates (e.g., stock tickers), UI
components reacting to model changes.

• Components:

o Subject (Observable): The object that maintains a list of its dependents (observers)
and notifies them of state changes.

o Observer: The object that wants to be notified of changes in the subject.

• Example (Conceptual, using Java's built-in Observer/Observable is deprecated but good for
understanding; often custom interfaces are used):

Java

import java.util.ArrayList;

import java.util.List;

// Subject interface

interface Subject {

void addObserver(Observer o);

void removeObserver(Observer o);

void notifyObservers(String message);

// Concrete Subject

class NewsAgency implements Subject {

private List<Observer> observers = new ArrayList<>();

private String news;

public void setNews(String news) {

this.news = news;

notifyObservers(news); // Notify all subscribed observers

@Override
public void addObserver(Observer o) {

observers.add(o);

@Override

public void removeObserver(Observer o) {

observers.remove(o);

@Override

public void notifyObservers(String message) {

for (Observer observer : observers) {

observer.update(message);

// Observer interface

interface Observer {

void update(String news);

// Concrete Observers

class NewsChannel implements Observer {

private String channelName;

public NewsChannel(String channelName) {

this.channelName = channelName;

@Override
public void update(String news) {

System.out.println(channelName + " received news: " + news);

public class ObserverPatternDemo {

public static void main(String[] args) {

NewsAgency agency = new NewsAgency();

NewsChannel channel1 = new NewsChannel("Channel Alpha");

NewsChannel channel2 = new NewsChannel("Channel Beta");

NewsChannel channel3 = new NewsChannel("Channel Gamma");

agency.addObserver(channel1);

agency.addObserver(channel2);

agency.setNews("Breaking: New tech company launched!");

// Output:

// Channel Alpha received news: Breaking: New tech company launched!

// Channel Beta received news: Breaking: New tech company launched!

agency.removeObserver(channel1);

agency.addObserver(channel3);

agency.setNews("Market trends indicate growth.");

// Output:

// Channel Beta received news: Market trends indicate growth.

// Channel Gamma received news: Market trends indicate growth.

}
Mastering these OOP concepts is fundamental. Be ready to explain them theoretically, draw
diagrams if needed, and write simple code demonstrating each principle. Good luck!

Alright, let's deep dive into "3. Collections Framework". This is an absolutely critical topic for any
Java developer, especially one with 3 years of experience. You'll use these classes daily, and
interviewers often ask about their internal workings, performance characteristics, and thread-safety
aspects.

3. Collections Framework: Managing Groups of Objects

The Java Collections Framework (JCF) is a set of interfaces and classes that represent groups of
objects as a single unit. It provides a unified architecture for storing and manipulating collections of
objects.

3.1. Hierarchy

Understanding the hierarchy is key to understanding the relationships and shared behaviors among
different collection types.

• Iterable<E> (Top-level Interface):

o The root of the collection hierarchy.

o Defines the iterator() method, which returns an Iterator<E>.

o Enables the use of the enhanced for-each loop.

• Collection<E> (Root Interface of all Collections):

o Extends Iterable<E>.

o Represents a group of objects.

o Defines common behaviors for all collections: add(), remove(), contains(), size(),
isEmpty(), toArray(), clear(), etc.

o Sub-interfaces: List, Set, Queue.

• List<E> (Interface):

o Extends Collection<E>.

o Represents an ordered collection (elements have a specific sequence).

o Allows duplicate elements.

o Elements can be accessed by their integer index (0-based).

o Provides methods like get(int index), set(int index, E element), add(int index, E
element), remove(int index), indexOf(), lastIndexOf(), subList().

o Common Implementations: ArrayList, LinkedList, Vector (legacy), Stack (legacy).

• Set<E> (Interface):
o Extends Collection<E>.

o Represents a collection that contains no duplicate elements.

o Does not guarantee order (except for LinkedHashSet which maintains insertion
order, and TreeSet which maintains sorted order).

o The add() method returns true if the element was added (i.e., it was not a duplicate)
and false otherwise.

o Common Implementations: HashSet, LinkedHashSet, TreeSet.

• Queue<E> (Interface):

o Extends Collection<E>.

o Represents a collection designed for holding elements prior to processing.

o Typically orders elements in a FIFO (First-In, First-Out) manner, but other orderings
are possible (e.g., PriorityQueue).

o Provides specific methods for adding (offer), removing (poll, remove), and inspecting
(peek, element) elements from the "head" of the queue.

o Common Implementations: LinkedList (implements Queue), PriorityQueue,


ArrayDeque.

• Map<K, V> (Interface):

o Does NOT extend Collection<E>. It's a separate hierarchy.

o Represents a collection of key-value pairs.

o Each key is unique; a key maps to at most one value.

o Keys and values can be null (depending on implementation).

o Provides methods like put(K key, V value), get(Object key), remove(Object key),
containsKey(), containsValue(), keySet(), values(), entrySet().

o Common Implementations: HashMap, LinkedHashMap, TreeMap, Hashtable


(legacy).

3.2. List Implementations

• ArrayList:

o Internal Data Structure: Resizable array.

o Order: Maintains insertion order.

o Duplicates: Allows duplicates.

o Nulls: Allows null elements.

o Thread-Safety: Not thread-safe.

o Performance:
▪ get(index): O(1) - constant time, very fast due to direct index access.

▪ add(element) (at end): Amortized O(1) - usually fast, but can be O(N) if
resizing is needed (doubles its capacity).

▪ add(index, element) (in middle): O(N) - requires shifting elements.

▪ remove(index): O(N) - requires shifting elements.

▪ Iteration: Fast using for loop or enhanced for-each loop.

o Use Case: When you need frequent random access to elements and fewer
insertions/deletions in the middle.

• LinkedList:

o Internal Data Structure: Doubly-linked list.

o Order: Maintains insertion order.

o Duplicates: Allows duplicates.

o Nulls: Allows null elements.

o Thread-Safety: Not thread-safe.

o Performance:

▪ get(index): O(N) - requires traversing from head or tail.

▪ add(element) (at end/start): O(1).

▪ add(index, element) (in middle): O(N) to find the index, then O(1) for
insertion. Better than ArrayList if the current node is already known.

▪ remove(index): O(N) to find the index, then O(1) for removal.

▪ Iteration: Slower than ArrayList for direct index access, but fast using an
Iterator.

o Use Case: When you need frequent insertions and deletions at the ends or in the
middle, and less frequent random access. Also implements Queue and Deque
interfaces, making it suitable for queues, stacks, and double-ended queues.

• Vector (Legacy):

o Internal Data Structure: Resizable array, similar to ArrayList.

o Thread-Safety: Synchronized (thread-safe). All its methods are synchronized, which


comes with performance overhead.

o Performance: Similar to ArrayList but slower due to synchronization. Capacity


increments by doubling by default.

o Use Case: Rarely used in modern Java code unless thread-safety is strictly required
for all operations on a single list instance, and Collections.synchronizedList() or
CopyOnWriteArrayList are not viable. Generally, ArrayList with explicit
synchronization or concurrent collections are preferred.
• Stack (Legacy):

o Internal Data Structure: Extends Vector.

o Concept: LIFO (Last-In, First-Out) data structure.

o Methods: push() (add), pop() (remove and return top), peek() (return top without
removing), empty(), search().

o Thread-Safety: Synchronized (due to Vector inheritance).

o Use Case: Should be avoided. For LIFO stack functionality, use ArrayDeque which is
more efficient and provides better performance, or LinkedList as a Deque.

3.3. Set Implementations

• HashSet:

o Internal Data Structure: Uses a HashMap internally to store its elements (elements
are stored as keys in the underlying HashMap, with a dummy value).

o Order: No guaranteed order. Order can change over time.

o Duplicates: Stores only unique elements.

o Nulls: Allows one null element.

o Thread-Safety: Not thread-safe.

o Performance:

▪ add(), remove(), contains(): O(1) on average (constant time), assuming good


hash function distribution. In worst case (many collisions), can degrade to
O(N).

o Important: Relies on hashCode() and equals() methods of the stored objects for
uniqueness. If you override equals(), you must override hashCode().

o Use Case: When you need to store unique elements and the order doesn't matter,
and you need fast lookups.

• LinkedHashSet:

o Internal Data Structure: Uses a LinkedHashMap internally. Combines hashing with a


doubly-linked list.

o Order: Maintains insertion order (the order in which elements were added).

o Duplicates: Stores only unique elements.

o Nulls: Allows one null element.

o Thread-Safety: Not thread-safe.

o Performance: Slightly slower than HashSet for insertions and deletions due to
maintaining the linked list, but still O(1) on average. Iteration is faster than HashSet
because it follows the insertion order link.
o Use Case: When you need unique elements and also want to preserve the order of
insertion.

• TreeSet:

o Internal Data Structure: Uses a TreeMap internally (backed by a Red-Black tree, a


self-balancing binary search tree).

o Order: Stores elements in sorted (natural) order or by a custom Comparator.

o Duplicates: Stores only unique elements.

o Nulls: Does NOT allow null elements (throws NullPointerException on add if the set
is not empty or on first element if Comparator is used that cannot handle null).

o Thread-Safety: Not thread-safe.

o Performance:

▪ add(), remove(), contains(): O(log N) - logarithmic time, efficient for large


sets.

o Important: Elements must implement Comparable interface (for natural ordering) or


you must provide a Comparator in the TreeSet constructor.

o Use Case: When you need to store unique elements in a sorted order.

3.4. Map Implementations

• HashMap:

o Internal Data Structure: Array of linked lists (or Red-Black trees in Java 8+ for large
buckets).

o Order: No guaranteed order. Order can change over time.

o Keys/Values: Allows one null key and multiple null values.

o Thread-Safety: Not thread-safe.

o Performance:

▪ put(), get(), remove(): O(1) on average (constant time), assuming good hash
function for keys. In worst case (many collisions), can degrade to O(N).

o Important: Relies on hashCode() and equals() methods of the keys for uniqueness
and retrieval.

o Use Case: Most commonly used Map implementation when order is not important
and you need fast key-based lookups.

• LinkedHashMap:

o Internal Data Structure: HashMap + doubly-linked list running through its entries.

o Order: Maintains insertion order by default. Can also be configured for access order
(elements move to the end of the list when accessed, useful for LRU caches).
o Keys/Values: Allows one null key and multiple null values.

o Thread-Safety: Not thread-safe.

o Performance: Slightly slower than HashMap for additions and deletions due to
maintaining the linked list, but still O(1) on average. Iteration is faster than HashMap
because it follows the insertion/access order.

o Use Case: When you need a map that preserves the order of key-value pair insertion
(or access).

• TreeMap:

o Internal Data Structure: Red-Black tree.

o Order: Stores entries in sorted order based on the keys (natural order or custom
Comparator).

o Keys/Values: Does NOT allow null keys (throws NullPointerException). Allows


multiple null values.

o Thread-Safety: Not thread-safe.

o Performance:

▪ put(), get(), remove(): O(log N).

o Important: Keys must implement Comparable or you must provide a Comparator.

o Use Case: When you need a map where keys are stored in a sorted order, or when
you need to perform range queries (e.g., subMap()).

• Hashtable (Legacy):

o Internal Data Structure: Array of linked lists, similar to HashMap.

o Order: No guaranteed order.

o Keys/Values: Does NOT allow null keys or null values. Throws NullPointerException.

o Thread-Safety: Synchronized (thread-safe). All its methods are synchronized.

o Performance: Slower than HashMap due to synchronization overhead.

o Use Case: Largely superseded by ConcurrentHashMap for concurrent access or


HashMap with external synchronization if needed.

3.5. Concurrency in Collections

Thread-safety is a major concern when collections are accessed by multiple threads.

• Problem with non-thread-safe collections (ArrayList, HashMap, etc.) in multi-threaded


environments:

o Race Conditions: Multiple threads trying to modify the collection simultaneously can
lead to inconsistent state.
o ConcurrentModificationException: If one thread iterates over a collection while
another thread modifies it (add/remove), the iterator can become invalid, leading to
this runtime exception.

• Solutions for Thread-Safe Collections:

1. Collections.synchronizedList(), Collections.synchronizedSet(),
Collections.synchronizedMap():

▪ How it works: These are static factory methods that return a synchronized
(wrapper) view of a non-synchronized collection. They internally wrap the
methods of the underlying collection with synchronized blocks.

▪ Example: List<String> synchronizedList = Collections.synchronizedList(new


ArrayList<>());

▪ Drawbacks:

▪ Performance: Can be slow due to single lock for the entire collection
(high contention).

▪ External Synchronization for Iteration: You still need to manually


synchronize externally when iterating over these collections to
prevent ConcurrentModificationException.

Java

synchronized (synchronizedList) {

for (String item : synchronizedList) {

// ...

▪ Generally not recommended for high concurrency scenarios.

2. java.util.concurrent package (Concurrent Collections):

▪ Provides highly optimized, scalable, and thread-safe alternatives. These are


generally preferred for concurrent applications.

▪ ConcurrentHashMap:

▪ How it works: Uses a more fine-grained locking mechanism


(segment locking or node-level locking depending on Java version)
instead of a single global lock. This allows multiple threads to read
and write concurrently to different parts of the map without
blocking each other.

▪ Performance: Much better concurrency than Hashtable or


Collections.synchronizedMap().

▪ Nulls: Does not allow null keys or values.


▪ Iteration: Iterators are weakly consistent (fail-safe). They reflect the
state of the map at the time the iterator was created and will not
throw ConcurrentModificationException if the map is modified
concurrently.

▪ Use Case: Primary choice for thread-safe maps in high-concurrency


environments.

▪ CopyOnWriteArrayList and CopyOnWriteArraySet:

▪ How they work: When a modifying operation (add, set, remove) is


performed, a new copy of the underlying array is created. Reads
operate on the old, unmodified array.

▪ Performance: Excellent for concurrent read operations. Very poor


for frequent write operations due to the overhead of copying the
entire array.

▪ Use Case: When reads vastly outnumber writes. E.g., maintaining a


list of listeners in an event system. Iterators also fail-safe.

▪ BlockingQueue Interfaces (e.g., ArrayBlockingQueue,


LinkedBlockingQueue):

▪ Concept: Queues that support operations that wait for the queue to
become non-empty when retrieving an element, and wait for space
to become available when storing an element.

▪ Use Case: Producer-Consumer patterns, thread pools.

• Fail-Fast vs. Fail-Safe Iterators:

o Fail-Fast Iterators:

▪ Implemented by most non-concurrent collections (ArrayList, HashMap,


HashSet, etc.).

▪ They try to detect concurrent modifications (modifications to the underlying


collection by any other means than the iterator's own remove() method) and
throw a ConcurrentModificationException immediately.

▪ They are designed to "fail fast" to indicate potential bugs in multi-threaded


code.

o Fail-Safe Iterators:

▪ Implemented by concurrent collections (ConcurrentHashMap,


CopyOnWriteArrayList, etc.).

▪ They operate on a clone or snapshot of the collection at the time the iterator
was created.

▪ They do not throw ConcurrentModificationException if the original


collection is modified concurrently.
▪ The trade-off is that they might not reflect the very latest state of the
collection.

3.6. Comparable vs. Comparator

These interfaces are used for custom sorting of objects.

• Comparable<T> (Natural Ordering):

o Interface: public interface Comparable<T> { int compareTo(T o); }

o Purpose: Defines the natural or default sorting order for objects of a class.

o Implementation: The class whose objects you want to sort must implement this
interface.

o compareTo() method:

▪ Returns a negative integer, zero, or a positive integer as this object is less


than, equal to, or greater than the specified object.

▪ obj1.compareTo(obj2):

▪ < 0 if obj1 comes before obj2

▪ == 0 if obj1 is equal to obj2

▪ > 0 if obj1 comes after obj2

o Usage: Used by Collections.sort(List) for lists, and TreeSet/TreeMap constructors.

• Comparator<T> (Custom/External Ordering):

o Interface: public interface Comparator<T> { int compare(T o1, T o2); boolean


equals(Object obj); // Default method from Object }

o Purpose: Defines an alternative or custom sorting order for objects. Useful when:

▪ You don't want to modify the original class.

▪ You need multiple ways to sort the same objects (e.g., sort Employee by
name, id, salary).

▪ The objects do not implement Comparable.

o Implementation: You create a separate class that implements Comparator or use a


lambda expression (Java 8+).

o compare() method:

▪ Returns a negative integer, zero, or a positive integer as the first argument is


less than, equal to, or greater than the second.

▪ comparator.compare(obj1, obj2): Same return value logic as compareTo().

o Usage: Used by Collections.sort(List, Comparator), Arrays.sort(array, Comparator),


and TreeSet/TreeMap constructors that take a Comparator argument.
o Java 8+: Often implemented using lambda expressions for conciseness: (o1, o2) ->
o1.getName().compareTo(o2.getName())

• Example:

Java

import java.util.*;

class Person implements Comparable<Person> { // Implements Comparable for natural ordering by


age

String name;

int age;

public Person(String name, int age) {

this.name = name;

this.age = age;

@Override

public int compareTo(Person other) {

return Integer.compare(this.age, other.age); // Natural order: sort by age

@Override

public String toString() {

return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';

public class SortingDemo {

public static void main(String[] args) {

List<Person> people = new ArrayList<>();

people.add(new Person("Alice", 30));

people.add(new Person("Charlie", 25));


people.add(new Person("Bob", 35));

// 1. Sort using natural ordering (Comparable)

Collections.sort(people); // Requires Person to implement Comparable

System.out.println("Sorted by Age (Natural Order):");

people.forEach(System.out::println);

// Output: Charlie (25), Alice (30), Bob (35)

// 2. Sort using a custom Comparator (by Name)

Comparator<Person> nameComparator = (p1, p2) -> p1.name.compareTo(p2.name);

Collections.sort(people, nameComparator);

System.out.println("\nSorted by Name (Custom Comparator):");

people.forEach(System.out::println);

// Output: Alice (30), Bob (35), Charlie (25)

// 3. Using a TreeSet (natural order)

TreeSet<Person> sortedPeopleByAge = new TreeSet<>();

sortedPeopleByAge.add(new Person("David", 40));

sortedPeopleByAge.add(new Person("Eve", 20));

sortedPeopleByAge.add(new Person("Frank", 30));

System.out.println("\nTreeSet (Sorted by Age):");

sortedPeopleByAge.forEach(System.out::println);

// 4. Using a TreeMap (custom comparator for keys)

Map<Person, String> personMap = new TreeMap<>(nameComparator); // TreeMap uses


Comparator

personMap.put(new Person("Zara", 28), "Developer");

personMap.put(new Person("Yara", 32), "Manager");

personMap.put(new Person("Xavier", 25), "Analyst");

System.out.println("\nTreeMap (Sorted by Name):");

personMap.forEach((person, role) -> System.out.println(person + " -> " + role));


}

The Collections Framework is vast, but focusing on these core interfaces and their most common
implementations, their performance characteristics, and thread-safety concerns will make you well-
prepared for interview questions. Be ready to discuss specific scenarios for when to use ArrayList vs.
LinkedList, HashMap vs. TreeMap, and HashMap vs. ConcurrentHashMap.

Got it. Let's delve into "4. Multithreading and Concurrency" in detail. This is a crucial topic for a 3-
year experienced Java developer, as it directly relates to building high-performance, responsive, and
scalable applications. Interviewers will often ask about thread creation, synchronization, common
concurrency problems, and the java.util.concurrent package.

4. Multithreading and Concurrency: Building Concurrent Applications

Multithreading allows a program to execute multiple parts of its code concurrently within a single
process. Each independent part of execution is called a thread.

Concurrency is the ability of different parts of a program or system to execute independently or in


parallel.

4.1. Thread Basics

• What is a Thread?

o The smallest unit of execution within a process.1

o A single process can have multiple threads.2

o Threads share the same memory space (heap) of the process, but each has its own
call stack and PC (Program Counter) register.3

o Lightweight compared to processes (faster context switching).4

• Advantages of Multithreading:

o Responsiveness: UI applications remain responsive while long-running tasks execute


in the background.

o Resource Utilization: Efficiently utilizes CPU (especially multi-core processors) by


allowing tasks to run in parallel.5

o Faster Execution: Completes tasks quicker by dividing them among multiple


threads.6

o Better Resource Sharing: Threads within the same process can easily share data.7

• Disadvantages of Multithreading:

o Complexity: Harder to design, debug, and test concurrent applications due to race
conditions, deadlocks, etc.
o Overhead: Context switching between threads incurs some overhead.

o Resource Contention: Multiple threads accessing shared resources can lead to data
corruption or inconsistent states.8

• Creating Threads: In Java, there are two primary ways to create and run threads:9

1. Extending the Thread Class:

▪ Create a class that extends java.lang.Thread.

▪ Override the run() method with the code that the thread will execute.

▪ Create an instance of your class and call its start() method (which in turn
calls run() in a new thread).

▪ Limitation: Java does not support multiple inheritance, so your class cannot
extend any other class if you choose this approach.

Java

class MyThread extends Thread {

@Override

public void run() {

System.out.println("Thread extending Thread class is running: " +


Thread.currentThread().getName());

public class ThreadCreationDemo {

public static void main(String[] args) {

MyThread thread1 = new MyThread();

thread1.start(); // Starts a new thread, calls run()

2. Implementing the Runnable Interface: (Recommended approach)

▪ Create a class that implements java.lang.Runnable.

▪ Override the run() method.

▪ Create an instance of your Runnable class.

▪ Create a Thread object, passing your Runnable instance to its constructor.

▪ Call the start() method on the Thread object.


▪ Advantage: Your class can still extend another class while implementing
Runnable, offering more flexibility. This separates the task (what to run) from
the thread (how to run it).

Java

class MyRunnable implements Runnable {

@Override

public void run() {

System.out.println("Thread implementing Runnable is running: " +


Thread.currentThread().getName());

public class RunnableCreationDemo {

public static void main(String[] args) {

MyRunnable myRunnable = new MyRunnable();

Thread thread2 = new Thread(myRunnable, "WorkerThread-1"); // Give thread a name

thread2.start();

3. Implementing Callable and Future (Java 5+):

▪ Purpose: Runnable's run() method cannot return a value or throw checked


exceptions. Callable solves this.

▪ Callable's call() method returns a result and can throw exceptions.10

▪ Future represents the result of an asynchronous computation.11 It provides


methods to check if the computation is complete, wait for its completion,
and retrieve the result.12

▪ Requires an ExecutorService to execute Callable tasks.13

Java

import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {

private int number;


public MyCallable(int number) {

this.number = number;

@Override

public Integer call() throws Exception {

System.out.println("Callable task " + number + " running in thread: " +


Thread.currentThread().getName());

Thread.sleep(1000); // Simulate work

return number * number;

public class CallableFutureDemo {

public static void main(String[] args) throws InterruptedException, ExecutionException {

ExecutorService executor = Executors.newFixedThreadPool(2); // Create a thread pool

Future<Integer> future1 = executor.submit(new MyCallable(5));

Future<Integer> future2 = executor.submit(new MyCallable(10));

System.out.println("Result of task 1: " + future1.get()); // get() blocks until result is available

System.out.println("Result of task 2: " + future2.get());

executor.shutdown(); // Shut down the executor

• Thread Lifecycle: A thread goes through various states from its creation to its termination.

o NEW: A thread that has been created but not yet started (i.e., start() method not
called).

o RUNNABLE: A thread that is executing or ready to execute. It's waiting for its turn on
the CPU. It includes both "Running" and "Ready" states.
o BLOCKED: A thread that is waiting for a monitor lock (e.g., waiting to enter a
synchronized block/method) to access a shared resource.14

o WAITING: A thread that is indefinitely waiting for another thread to perform a


particular action (e.g., calling Object.wait(), Thread.join(), LockSupport.park()). It will
not return to the runnable state until another thread explicitly wakes it up.

o TIMED_WAITING: A thread that is waiting for another thread to perform an action


for a specified waiting time (e.g., Thread.sleep(long millis), Object.wait(long millis),
Thread.join(long millis), LockSupport.parkNanos(), LockSupport.parkUntil()). It will
return to the runnable state after the time expires or if woken up.

o TERMINATED: A thread that has finished its execution (its run() method has
completed or thrown an uncaught exception).

4.2. Synchronization

The process of controlling access to shared resources by multiple threads to prevent race conditions
and ensure data consistency.15

• Race Condition: Occurs when multiple threads try to access and modify a shared resource
simultaneously, and the final outcome depends on the unpredictable order of execution of
these threads.16

• How to achieve Synchronization:

1. synchronized Keyword:

▪ synchronized method:

▪ When applied to an instance method, it acquires the lock on the


object itself (i.e., this object) before executing the method. Only one
synchronized method of an object can be executed at a time by any
thread.

▪ When applied to a static method, it acquires the lock on the class


object (ClassName.class). Only one static synchronized method of a
class can be executed at a time by any thread.

▪ Java

▪ class Counter {

▪ private int count = 0;

▪ public synchronized void increment() { // Synchronized method

▪ count++;

▪ }

▪ public synchronized int getCount() {

▪ return count;

▪ }
▪ }

▪ synchronized block:

▪ Acquires a lock on a specified object (the monitor).

▪ Allows for more fine-grained control than synchronized methods.


You can synchronize only the critical section of code.

▪ Java

▪ class SharedResource {

▪ private int data = 0;

▪ private final Object lock = new Object(); // A dedicated lock


object

▪ public void increment() {

▪ synchronized (lock) { // Synchronized block on 'lock' object

▪ data++;

▪ }

▪ }

▪ public void updateData(int newValue) {

▪ synchronized (this) { // Synchronized on 'this' object

▪ this.data = newValue;

▪ }

▪ }

▪ }

▪ Key points about synchronized:

▪ Re-entrant: A thread that has acquired a lock can re-enter any


synchronized block or method that is guarded by the same lock.

▪ Intrinsic Lock (Monitor Lock): Every Java object has an intrinsic lock
associated with it.

2. volatile Keyword:

▪ Purpose: Ensures visibility of changes to variables across threads. It prevents


compilers/CPUs from caching variables locally and ensures that any write to
a volatile variable happens before any subsequent read of that variable by
another thread.
▪ Does NOT guarantee atomicity: volatile only ensures visibility, not that
operations like i++ (which are read-modify-write) are atomic. For atomicity,
you need synchronization or Atomic classes.

▪ Use Case: When one thread writes to a variable and other threads only read
that variable, and you need to ensure readers see the latest value. (e.g., a
flag to stop a thread).

▪ Java

▪ class VolatileExample {

▪ private volatile boolean running = true;

▪ public void start() {

▪ new Thread(() -> {

▪ while (running) {

▪ // Do work

▪ }

▪ System.out.println("Thread stopped.");

▪ }).start();

▪ }

▪ public void stop() {

▪ running = false; // Change will be immediately visible to other thread

▪ }

▪ }

3. wait(), notify(), notifyAll() Methods:

▪ From Object class: These methods are used for inter-thread communication
and must be called from within a synchronized block/method, on the object
whose intrinsic lock is held.

▪ wait(): Causes the current thread to release the lock on the object and go
into a WAITING or TIMED_WAITING state until another thread invokes
notify() or notifyAll() on the same object, or the specified timeout expires.

▪ notify(): Wakes up a single thread that is waiting on this object's monitor.17


Which thread gets woken up is non-deterministic.

▪ notifyAll(): Wakes up all threads that are waiting on this object's monitor.

▪ Typical Use Case: Producer-Consumer problem.


Java

class Message {

private String msg;

private boolean isEmpty = true;

public synchronized String take() { // Consumer method

while (isEmpty) {

try {

wait(); // Wait if no message

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

isEmpty = true;

notifyAll(); // Notify producer that it can put another message

return msg;

public synchronized void put(String msg) { // Producer method

while (!isEmpty) {

try {

wait(); // Wait if message is not consumed

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

this.msg = msg;

isEmpty = false;

notifyAll(); // Notify consumer that message is available

}
4.3. Thread Pools (ExecutorService)

• Problem: Creating a new thread for every task is expensive in terms of resource consumption
(memory, CPU cycles for context switching).

• Solution: Thread pools manage a pool of worker threads. Tasks are submitted to the pool,
and the pool's threads execute them.

• ExecutorService (Interface): The main interface for executing tasks asynchronously.18

• Executors (Utility Class): Provides static factory methods to create common ExecutorService
configurations.19

o newFixedThreadPool(int nThreads): Creates a thread pool with a fixed number of


threads.20 If more tasks are submitted than threads, they wait in a queue.

o newCachedThreadPool(): Creates a thread pool that creates new threads as needed,


but reuses existing ones when they become available.21 Good for applications with
many short-lived tasks.

o newSingleThreadExecutor(): Creates a single-threaded executor.22 Ensures tasks are


executed sequentially.

o newScheduledThreadPool(int corePoolSize): Creates a thread pool that can


schedule commands to run after a given delay, or to execute periodically.23

• Lifecycle: ExecutorService needs to be explicitly shut down using shutdown() (waits for
current tasks to complete) or shutdownNow() (attempts to stop all running tasks
immediately).24

• Benefits:

o Reduced Overhead: Reuses threads, avoiding the overhead of creating/destroying


threads.25

o Resource Management: Limits the number of concurrent threads, preventing


resource exhaustion.

o Improved Responsiveness: Tasks are picked up faster from the queue.

o Separation of Concerns: Decouples task submission from thread management.

• Example (See CallableFutureDemo above for basic usage).

4.4. Concurrency Utilities (java.util.concurrent)

This package introduced powerful, high-level concurrency constructs that are generally preferred
over low-level wait()/notify() or synchronized for complex scenarios, due to their ease of use,
robustness, and performance.26

• Lock Interface (e.g., ReentrantLock):

o Problem with synchronized: Limited flexibility (must release lock in the same block,
cannot interrupt a waiting thread, cannot try to acquire a lock without blocking,
cannot acquire multiple locks in specific order).
o ReentrantLock: An explicit lock implementation that provides more flexibility than
synchronized.

▪ lock(): Acquires the lock. Blocks if the lock is not available.

▪ unlock(): Releases the lock. Must be called in a finally block to ensure


release even if exceptions occur.

▪ tryLock(): Attempts to acquire the lock without blocking.27

▪ tryLock(long timeout, TimeUnit unit): Attempts to acquire the lock within a


specified timeout.28

▪ lockInterruptibly(): Acquires the lock unless the current thread is


interrupted.29

▪ Re-entrant: A thread can acquire the same lock multiple times.30

o Condition Interface: Can be obtained from a Lock (lock.newCondition()).31 It's an


alternative to wait()/notify() that provides more granular control over thread waiting
and notification (e.g., multiple wait sets for a single lock).

o ReentrantReadWriteLock: Allows multiple readers to access a resource concurrently,


but only one writer at a time.32 Ideal for read-heavy workloads.

• CountDownLatch:

o Purpose: A synchronization aid that allows one or more threads to wait until a set of
operations being performed in other threads completes.

o Mechanism: Initialized with a count. Threads call countDown() when their task is
complete.33 A thread waiting on the latch calls await() and blocks until the count
reaches zero.34

o Use Case: Start multiple threads, do some concurrent work, and then wait for all of
them to finish before proceeding.

• CyclicBarrier:

o Purpose: A synchronization aid that allows a set of threads to wait for each other to
reach a common barrier point.35

o Mechanism: Initialized with a number of parties. Threads call await() when they
reach the barrier. All threads block until all parties have called await().

o Reusability: The barrier can be reused once all threads have passed it.36

o Use Case: Useful for scenarios where a group of threads needs to perform a series of
operations in stages, with all threads needing to complete a stage before moving to
the next.

• Semaphore:

o Purpose: Controls access to a limited number of resources.


o Mechanism: Initialized with a number of permits. Threads acquire a permit
(acquire()) to access the resource and release it (release()) when done.37 If no
permits are available, threads block.

o Use Case: Limiting the number of concurrent database connections, limiting


concurrent requests to a specific service.

• BlockingQueue (e.g., ArrayBlockingQueue, LinkedBlockingQueue):

o Purpose: Queues that automatically handle synchronization when adding or


removing elements.

o Blocking Operations:

▪ put(E e): Waits if the queue is full.

▪ take(): Waits if the queue is empty.38

o Non-blocking variants: offer(), poll().

o Use Case: The quintessential solution for the Producer-Consumer problem.


Producers add items, consumers take items.

• Atomic Variables (e.g., AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference):

o Purpose: Provide atomic operations on single variables without using explicit locks.

o Mechanism: Use low-level, hardware-supported compare-and-swap (CAS)


operations.

o Benefits: More performant and scalable than using synchronized for simple variable
updates.

o Use Case: Atomic counters, flags, or references that need to be updated safely by
multiple threads.

4.5. Deadlock

• What it is: A situation where two or more threads are blocked indefinitely, each waiting for
the other to release a resource that it needs.39

• Conditions for Deadlock (Coffman Conditions - all four must be met):

1. Mutual Exclusion: At least one resource must be held in a non-sharable mode (e.g.,
a lock).

2. Hold and Wait: A thread holding at least one resource is waiting to acquire
additional resources held by other threads.40

3. No Preemption: Resources cannot be forcibly taken from a thread; they must be


released voluntarily.41

4. Circular Wait: A set of threads T1, T2, ..., Tn exists such that T1 is waiting for a
resource held by T2, T2 is waiting for a resource held by T3, and so on, with Tn
waiting for a resource held by T1.

• Example (Classic Deadlock):


Java

public class DeadlockExample {

private static Object lock1 = new Object();

private static Object lock2 = new Object();

public static void main(String[] args) {

// Thread 1: Tries to acquire lock1, then lock2

new Thread(() -> {

synchronized (lock1) {

System.out.println("Thread 1: Acquired lock1");

try { Thread.sleep(100); } catch (InterruptedException e) {}

System.out.println("Thread 1: Waiting for lock2...");

synchronized (lock2) { // Tries to acquire lock2 while holding lock1

System.out.println("Thread 1: Acquired lock2");

}, "Thread-1").start();

// Thread 2: Tries to acquire lock2, then lock1

new Thread(() -> {

synchronized (lock2) {

System.out.println("Thread 2: Acquired lock2");

try { Thread.sleep(100); } catch (InterruptedException e) {}

System.out.println("Thread 2: Waiting for lock1...");

synchronized (lock1) { // Tries to acquire lock1 while holding lock2

System.out.println("Thread 2: Acquired lock1");

}, "Thread-2").start();

}
• How to Prevent Deadlock:

o Break Circular Wait: The most common strategy. Establish a consistent ordering of
resource acquisition. If Thread 1 acquires Lock A then Lock B, Thread 2 should also
acquire Lock A then Lock B, never Lock B then Lock A.

o Avoid Hold and Wait: Acquire all necessary resources at once, or release all held
resources if additional ones cannot be acquired.

o Allow Preemption: If a thread cannot acquire a resource, it releases the ones it holds
and retries.

o Avoid Mutual Exclusion (if possible): Use concurrent data structures that don't
require explicit locking (e.g., ConcurrentHashMap).

• How to Detect Deadlock:

o Thread Dumps: Analyze thread dumps (jstack <pid>). They will show threads in
BLOCKED state, often with waiting for monitor entry on specific locks and holding
lock on others.

o Monitoring Tools: Profilers or APM (Application Performance Monitoring) tools can


detect deadlocks.42

• How to Resolve Deadlock: Usually involves fixing the code to prevent one of the four
conditions from being met, most commonly by ensuring a consistent lock acquisition order.

Multithreading and Concurrency are vast and complex topics. For a 3-year experienced developer,
the expectation is to not just know the basics, but also to understand the trade-offs of different
synchronization mechanisms, when to use the java.util.concurrent utilities, and how to identify and
prevent common concurrency issues like deadlocks. Practice writing concurrent code and debugging
it to solidify your understanding.

Got it! Let's dive deep into "5. Spring Boot & Microservices". This is a crucial area for a Java
developer with 3 years of experience, as Spring Boot is the de-facto standard for building modern
Java applications, especially in the context of microservices.

5. Spring Boot & Microservices: Modern Java Application Development

Spring Boot is an opinionated framework that simplifies the development of production-ready, stand-
alone Spring applications.1 It builds on top of the Spring Framework, making it easier to get Spring
applications up and running with minimal configuration.2 Microservices are an architectural style that
structures an application as a collection of loosely coupled, independently deployable services.3

5.1. Spring Core Concepts

Spring Boot is built on the foundation of the Spring Framework.4 Understanding these core concepts
is vital.

a. IoC (Inversion of Control) Container and Dependency Injection (DI)

• IoC (Inversion of Control):


o Concept: Instead of the developer manually creating and managing objects and their
dependencies, the Spring IoC container takes control of this process. The control of
object creation and lifecycle is inverted from the application code to the framework.

o How it works: Spring scans your application, identifies components, creates their
instances, and manages their lifecycle (creation, configuration, destruction).5

o Benefits: Reduces boilerplate code, promotes loose coupling, easier testing, and
promotes modularity.6

• Dependency Injection (DI):

o Concept: A specific implementation of IoC where the container "injects" (provides)


the dependencies (collaborating objects) that an object needs, rather than the
object creating or looking up its dependencies itself.7

o Analogy: Instead of a class A creating an instance of B (tight coupling), A declares


that it needs an instance of B, and Spring provides it.

o Types of Dependency Injection:

1. Constructor Injection (Recommended): Dependencies are provided through


the class constructor.

▪ Pros: Guarantees that required dependencies are available when the


object is created (immutable dependencies), makes testing easier,
clearer about required dependencies.8

▪ Example:

Java

@Service

public class UserService {

private final UserRepository userRepository; // Final for immutability

@Autowired // Optional from Spring Boot 2.x if only one constructor

public UserService(UserRepository userRepository) {

this.userRepository = userRepository;

// ... methods using userRepository

2. Setter Injection: Dependencies are provided through setter methods.

▪ Pros: Allows for optional dependencies, more flexible for re-


configuring objects after creation.9
▪ Cons: Object might be in an inconsistent state before all setters are
called. Not suitable for mandatory dependencies.

▪ Example:

Java

@Service

public class ProductService {

private ProductRepository productRepository;

@Autowired

public void setProductRepository(ProductRepository productRepository) {

this.productRepository = productRepository;

// ... methods using productRepository

3. Field Injection (Least Recommended but commonly seen): Dependencies are


injected directly into fields using @Autowired.

▪ Pros: Very concise, less code.10

▪ Cons:

▪ Makes objects harder to test without Spring context.

▪ Breaks encapsulation (private fields are directly accessed by


the framework).11

▪ Makes dependencies less obvious.

▪ Cannot declare fields as final.

▪ Example:

Java

@Service

public class OrderService {

@Autowired

private OrderRepository orderRepository; // Field injection

// ... methods using orderRepository

b. Bean Lifecycle
• Bean: An object that is instantiated, assembled, and managed by a Spring IoC container. They
are the backbone of your application and form the objects that are managed by the Spring
IoC container.12

• Lifecycle Stages (Simplified):

1. Instantiation: The container creates an instance of the bean.13

2. Populate Properties: Dependencies are injected (DI).14

3. Initialization:

▪ BeanPostProcessors are applied (e.g., @PostConstruct methods are called,


InitializingBean's afterPropertiesSet() method is called).

▪ Custom init-method is called (if configured).

4. Ready for Use: The bean is ready for use by the application.

5. Destruction:

▪ PreDestroy methods are called, DisposableBean's destroy() method is


called.15

▪ Custom destroy-method is called (if configured).

• Bean Scopes: Define how Spring container manages instances of a bean.

1. singleton (Default):

▪ Concept: Only one single instance of the bean is created per Spring IoC
container. This is the most common scope.

▪ Use Case: Stateless beans (e.g., services, repositories, controllers), shared


resources.

2. prototype:

▪ Concept: A new instance of the bean is created every time it is requested


from the container.

▪ Use Case: Stateful beans that need a fresh instance for each client.

3. Web-Aware Scopes (only in web applications):

▪ request: A new instance for each HTTP request.

▪ session: A new instance for each HTTP session.

▪ application: A new instance for the lifecycle of the ServletContext.

▪ websocket: A new instance for the lifecycle of a WebSocket.

c. Annotations

Spring Boot heavily relies on annotations for configuration and defining components.16
• @Component: A generic stereotype annotation indicating that an annotated class is a
"component" and should be managed by the Spring IoC container.17 It's a general-purpose
annotation for any Spring-managed component.

• Stereotype Annotations (specialized @Component):

o @Service: Indicates that an annotated class is a "service" component in the business


layer.18 It provides better readability and can be used for specific cross-cutting
concerns (e.g., transaction management).

o @Repository: Indicates that an annotated class is a "repository" component in the


data access layer.19 It enables automatic exception translation from technology-
specific exceptions (e.g., JDBC SQLException) to Spring's DataAccessException
hierarchy.20

o @Controller: Indicates that an annotated class is a "controller" component in the


web layer, typically handling incoming web requests and returning responses (often
views).21

o @RestController: A convenience annotation that combines @Controller and


@ResponseBody. 22Used for building RESTful web services that return data directly
(e.g., JSON/XML) rather than view names.

• @Autowired: Used for automatic dependency injection.23 Spring automatically finds a


matching bean by type and injects it.24

o Can be used on constructors, setter methods, fields, and even method parameters.25

• @Qualifier("beanName"): Used with @Autowired when there are multiple beans of the
same type.26 It helps specify exactly which bean to inject by its name.

• @Value("${property.name}"): Used to inject values from properties files


(application.properties, application.yml) or environment variables directly into fields or
method parameters.

• @Bean:

o Annotated on methods within a @Configuration class.

o Indicates that a method produces a bean to be managed by the Spring IoC container.
The method's return value will be registered as a Spring bean.

o The method name typically becomes the bean name.27

• @Configuration:

o Indicates that a class declares one or more @Bean methods.

o Spring processes these classes to generate bean definitions and service requests for
those beans at runtime.

o Often used for externalizing bean definitions or for complex configurations that
cannot be done with simple stereotype annotations.

5.2. Spring Boot


Spring Boot significantly streamlines Spring application development.28

• What is Spring Boot?

o An extension of the Spring Framework that simplifies the creation of stand-alone,


production-grade Spring applications.29

o Aims to get you up and running with minimal fuss by providing sensible defaults.30

• Advantages of Spring Boot:

o Auto-Configuration: Automatically configures your Spring application based on the


dependencies present in your classpath. For example, if spring-boot-starter-web is
present, it auto-configures Tomcat and Spring MVC. If spring-boot-starter-data-jpa
and a database driver are present, it configures DataSource and EntityManager.

o Embedded Servers: Comes with embedded Tomcat, Jetty, or Undertow servers.31


This means you can run your application as a simple JAR file without needing to
deploy it to an external web server.

o Starter Dependencies: Provides "starter" POMs (e.g., spring-boot-starter-web,


spring-boot-starter-data-jpa). These are convenient dependency descriptors that
bring in all necessary transitive dependencies for a particular functionality,
simplifying your pom.xml.

o Opinionated Defaults: Provides sensible default configurations, reducing the need


for explicit XML or Java-based configuration in many cases.32

o Production-Ready Features: Includes features like externalized configuration, health


checks, metrics, and tracing, making it easy to monitor and manage applications in
production.33

o No XML Configuration: Largely eliminates the need for verbose XML configurations;
prefers annotation-based and JavaConfig.

• Creating RESTful APIs with Spring Boot:

o Uses @RestController to define controllers that handle incoming HTTP requests and
return JSON/XML responses.34

o @RequestMapping: Maps HTTP requests to handler methods.35

▪ Can be used at class level (base path) and method level.

▪ @RequestMapping(value = "/users", method = RequestMethod.GET)

o Shorthand Annotations (Preferred):

▪ @GetMapping: For HTTP GET requests (read data).

▪ @PostMapping: For HTTP POST requests (create data).

▪ @PutMapping: For HTTP PUT requests (update/replace data).36

▪ @DeleteMapping: For HTTP DELETE requests (delete data).37

▪ @PatchMapping: For HTTP PATCH requests (partial update data).38


o @PathVariable: Binds a method parameter to a URI template variable (e.g.,
/users/{id}).39

o @RequestParam: Binds a method parameter to a web request parameter (query


parameter, e.g., /users?name=Alice).40

o @RequestBody: Maps the HTTP request body to a domain object (e.g., converting
JSON request body to a Java object).41 Requires a JSON/XML processing library like
Jackson.

o @ResponseBody: (Implicit in @RestController) Indicates that the return value of a


method should be bound directly to the web response body.42

o ResponseEntity<T>: Allows you to control the entire HTTP response (status code,
headers, body).43

• Basic Exception Handling in Spring Boot:

o @ResponseStatus: Annotate custom exception classes with @ResponseStatus to


automatically map them to specific HTTP status codes.44

o @ExceptionHandler: Methods annotated with this in a @Controller or


@RestController handle specific exceptions that occur within that controller.45

o @ControllerAdvice / @RestControllerAdvice:

▪ Global exception handlers. Methods in a class annotated with


@ControllerAdvice (or @RestControllerAdvice) and @ExceptionHandler can
handle exceptions from any controller in the application.

▪ This centralizes exception handling logic.

• application.properties/application.yml:

o Used for externalizing configuration.

o Allows you to define application-specific properties (e.g., server port, database


connection details, logging levels, custom properties).46

o application.yml is preferred for its cleaner, more readable YAML syntax.

o Spring Boot automatically loads these files. Profiles can be used (application-
dev.properties, application-prod.yml) for environment-specific configurations.47

5.3. Spring Data JPA/Hibernate

Spring Data JPA simplifies data access layer development by providing abstractions over JPA (Java
Persistence API) and Hibernate (a popular JPA implementation).48

• ORM (Object-Relational Mapping):

o Concept: A technique that maps objects in an object-oriented programming


language (like Java) to tables in a relational database. It abstracts away the
complexities of JDBC and SQL.

o Benefit: Allows developers to interact with the database using object-oriented


paradigms, rather than writing raw SQL.
• JPA (Java Persistence API):

o A specification (standard) for ORM in Java. It defines a set of interfaces and


annotations.

o Hibernate is one of the most popular implementations of the JPA specification.49

• Core Annotations for Entities:

o @Entity: Marks a plain old Java object (POJO) as a JPA entity, meaning it maps to a
table in the database.50

o @Table(name="table_name"): (Optional) Specifies the name of the database table


to which this entity is mapped.51 If omitted, the class name is used.

o @Id: Marks the primary key field of the entity.

o @GeneratedValue(strategy = GenerationType.IDENTITY): Configures how the


primary key value is generated. IDENTITY uses database auto-increment. Other
strategies include AUTO, SEQUENCE, TABLE.

o @Column(name="column_name"): (Optional) Specifies the database column name


for a field.52

o @OneToMany, @ManyToOne, @OneToOne, @ManyToMany: Annotations for


defining relationships between entities.53

• JpaRepository / CrudRepository:

o Spring Data JPA provides these interfaces that offer out-of-the-box CRUD (Create,
Read, Update, Delete) operations and query methods without writing any
implementation code.54

o You just declare an interface extending JpaRepository

o CrudRepository: Basic CRUD functionalities (save, findById, findAll, delete).55

o PagingAndSortingRepository: Extends CrudRepository and adds methods for


pagination and sorting.

o JpaRepository: Extends PagingAndSortingRepository and adds JPA-specific features


like flush(), saveAndFlush(), deleteInBatch(), and methods for batch operations. It
also integrates with JPA EntityManager.

• Derived Query Methods: Spring Data JPA can automatically generate queries from method
names in your repository interfaces.56

o findByFirstName(String firstName): Generates SELECT * FROM table WHERE


first_name = ?

o findByAgeGreaterThan(int age)

o findByEmailContainingIgnoreCase(String email)

o findByNameAndAge(String name, int age)


o You can also use @Query annotation for complex custom queries (JPQL or native
SQL).

• Lazy vs. Eager Loading:

o Applies to fetching strategies for associated entities (relationships like @OneToMany,


@ManyToOne).

o Lazy Loading (Default for @OneToMany, @ManyToMany):

▪ Associated data is not loaded from the database immediately when the main
entity is loaded.

▪ It's loaded only when it's explicitly accessed (e.g., when you call a getter
method on the collection).

▪ Pros: Efficient, loads only what's needed.

▪ Cons: Can lead to N+1 query problem if not managed properly (fetching child
entities one by one in a loop).

o Eager Loading (Default for @ManyToOne, @OneToOne):

▪ Associated data is loaded from the database immediately along with the
main entity.

▪ Pros: Data is available instantly.

▪ Cons: Can fetch unnecessary data, leading to performance issues if


relationships are deep or data is large.

o Recommendation: Prefer lazy loading and use explicit fetching strategies (e.g., JOIN
FETCH in JPQL queries or @EntityGraph) when you know you need the associated
data to avoid N+1 issues.

5.4. Microservices (Conceptual Understanding)

While "microservices" is an architectural style, Spring Boot is a primary tool for building them.57

• What are Microservices?

o An architectural approach where a large application is built as a suite of small,


independent services.58

o Each service:

▪ Runs in its own process.

▪ Communicates with other services through lightweight mechanisms (e.g.,


REST APIs, message queues).59

▪ Is independently deployable.

▪ Is built around a specific business capability.

▪ Can be developed, deployed, and scaled independently.

▪ Often uses its own data store (decentralized data management).


• Advantages:

o Scalability: Services can be scaled independently based on their load.60

o Resilience: Failure in one service doesn't necessarily bring down the entire
application.

o Technology Heterogeneity: Different services can use different programming


languages, databases, and frameworks best suited for their needs.

o Faster Development Cycles: Smaller, autonomous teams can work on services


independently.61

o Easier Maintenance: Smaller codebases are easier to understand and manage.

o Deployment Flexibility: Independent deployments mean faster release cycles.

• Disadvantages:

o Increased Complexity: Distributed systems are inherently more complex (network


latency, data consistency, distributed transactions, monitoring, debugging).62

o Operational Overhead: More services mean more deployments, more monitoring,


more infrastructure.

o Data Consistency: Maintaining data consistency across multiple independent


databases can be challenging.

o Inter-service Communication: Needs robust communication mechanisms.63

• Common Microservices Patterns:

o API Gateway:

▪ Concept: A single entry point for all client requests. It acts as a reverse proxy
that routes requests to the appropriate microservice.64

▪ Benefits: Request routing, authentication/authorization, rate limiting,


caching, load balancing, logging, request aggregation.

▪ Example: Spring Cloud Gateway, Netflix Zuul.

o Service Discovery:

▪ Concept: A mechanism for services to register themselves and for clients to


find instances of services.

▪ Why needed: In a microservices architecture, service instances are


dynamically created/destroyed, and their network locations change
frequently.65

▪ Types:

▪ Client-Side Discovery: Client queries a service registry (e.g., Eureka,


Consul) to find available service instances.66

▪ Server-Side Discovery: Load balancer queries the registry and routes


requests to instances.
▪ Example: Netflix Eureka, Apache ZooKeeper, HashiCorp Consul.

o Circuit Breaker:

▪ Concept: Prevents cascading failures in a distributed system. If a service is


repeatedly failing, the circuit breaker "trips" (opens) and stops sending
requests to that service for a period, allowing it to recover.67

▪ Example: Resilience4j, Netflix Hystrix (deprecated).

o Centralized Configuration:

▪ Concept: Externalizes application configuration (e.g., database URLs, API


keys) into a central repository.68

▪ Example: Spring Cloud Config Server, HashiCorp Vault.

o Load Balancing: Distributing incoming network traffic across multiple servers to


ensure no single server is overloaded.69 (Client-side with Ribbon/Spring Cloud
LoadBalancer, or server-side with Nginx/ELB).

• Basic Understanding of Communication Between Microservices:

o Synchronous Communication (REST/HTTP):

▪ Mechanism: One service makes a direct HTTP request (GET, POST, etc.) to
another service and waits for a response.

▪ Pros: Simple to implement, easy to debug, ubiquitous.

▪ Cons: Tightly coupled (services need to know each other's endpoints),


blocking (calling service waits), cascading failures.

o Asynchronous Communication (Messaging Queues):

▪ Mechanism: Services communicate by sending messages to and receiving


messages from a message broker (e.g., Kafka, RabbitMQ, ActiveMQ).

▪ Pros: Loose coupling, improved resilience (messages can be retried),


scalability, supports pub/sub patterns.

▪ Cons: Increased complexity, message ordering can be tricky, requires a


message broker infrastructure.

Spring Boot and Microservices are vast topics, but for a 3-year experienced role, you're expected to
have hands-on experience building REST APIs with Spring Boot, understanding how Spring manages
dependencies, interacting with databases using Spring Data JPA, and a good conceptual grasp of
microservices architecture and common patterns. Be prepared to discuss your project experiences
with these technologies!

Alright, let's break down "6. Database Technologies & ORM" in detail. This topic is fundamental for
any backend developer, and for someone with 3 years of experience, you're expected to have a solid
understanding of both relational databases and how Java applications interact with them, specifically
through JPA/Hibernate, which we briefly touched upon in the previous topic.

6. Database Technologies & ORM: Storing and Managing Data

This section covers the core concepts of databases, particularly relational databases, SQL, and the
crucial role of Object-Relational Mapping (ORM) tools like JPA and Hibernate in Java applications.

6.1. RDBMS Concepts & SQL

• RDBMS (Relational Database Management System):

o Concept: A type of database that stores and provides access to data points that are
related to one another. Data is organized into tables (relations), which consist of
rows (records) and columns (attributes).

o Key Characteristics:

▪ Structured Data: Data is organized in a predefined schema.

▪ Tables: Data stored in tables with rows and columns.

▪ Relationships: Relationships between tables are established using primary


keys (PK) and foreign keys (FK).

▪ Schema-on-Write: Schema must be defined before data can be inserted.

▪ ACID Properties: Guarantees data integrity and reliability for transactions.

o Examples: MySQL, PostgreSQL, Oracle, SQL Server, H2 (in-memory for testing).

• ACID Properties (for Transactions):

o Transaction: A single logical unit of work that either completes entirely or fails
entirely.

o Atomicity:

▪ Concept: A transaction is treated as a single, indivisible unit. Either all


operations within the transaction succeed, or none of them do. If any part of
the transaction fails, the entire transaction is rolled back to its initial state.

▪ Analogy: A money transfer from account A to B. It's either debit A and credit
B, or neither.

o Consistency:

▪ Concept: A transaction brings the database from one valid state to another
valid state. It ensures that all data integrity rules (e.g., foreign key
constraints, unique constraints, check constraints) are maintained before
and after the transaction.

▪ Analogy: After a money transfer, the total sum of money across accounts
remains the same.

o Isolation:
▪ Concept: Concurrent transactions do not interfere with each other. Each
transaction executes as if it were the only transaction running in the system.
The intermediate state of a transaction is not visible to other concurrent
transactions.

▪ Analogy: Multiple people can transfer money simultaneously, but each


transfer appears to happen in isolation, without seeing partial results of
others.

▪ Transaction Isolation Levels (Important for interview): Define the degree to


which one transaction's uncommitted changes are visible to other
concurrent transactions.

▪ Read Uncommitted (Dirty Read): A transaction can read data that


has been modified by another transaction but not yet committed.
Highly problematic due to dirty reads.

▪ Read Committed (Default in most databases like PostgreSQL, SQL


Server): A transaction can only read data that has been committed
by other transactions. Prevents dirty reads.

▪ Repeatable Read (Default in MySQL): A transaction sees only the


data that was committed before the transaction started. Any rows it
reads will appear the same if read again within the same
transaction. Prevents dirty reads and non-repeatable reads.
However, it can suffer from "phantom reads" (new rows inserted by
other transactions might appear).

▪ Serializable (Highest isolation, lowest concurrency): Full isolation.


Transactions are executed in a serial fashion, as if they were running
one after another. Prevents dirty reads, non-repeatable reads, and
phantom reads. Comes with significant performance overhead.

o Durability:

▪ Concept: Once a transaction has been committed, its changes are


permanently recorded in the database and survive system failures (e.g.,
power loss, crashes). Data is written to non-volatile storage.

▪ Analogy: Once a transfer is confirmed, even if the system crashes, the


change is permanent.

• SQL (Structured Query Language):

o The standard language for managing and manipulating relational databases.

o DML (Data Manipulation Language): For manipulating data within tables.

▪ SELECT: Retrieve data.

▪ INSERT: Add new rows.

▪ UPDATE: Modify existing rows.

▪ DELETE: Remove rows.


o DDL (Data Definition Language): For defining and managing database schema.

▪ CREATE: Create tables, databases, indexes, etc.

▪ ALTER: Modify table structure.

▪ DROP: Delete tables, databases.

▪ TRUNCATE: Remove all rows from a table (DDL, faster than DELETE, cannot
be rolled back).

o DCL (Data Control Language): For managing permissions and access.

▪ GRANT: Give users privileges.

▪ REVOKE: Remove users' privileges.

o TCL (Transaction Control Language): For managing transactions.

▪ COMMIT: Make changes permanent.

▪ ROLLBACK: Undo changes.

▪ SAVEPOINT: Set a point within a transaction to which you can roll back.

• Common SQL Constructs:

o JOIN (INNER, LEFT, RIGHT, FULL OUTER)

o WHERE clause

o GROUP BY clause with aggregate functions (COUNT, SUM, AVG, MIN, MAX)

o HAVING clause (filters GROUP BY results)

o ORDER BY clause

o LIMIT/OFFSET (for pagination)

o Subqueries, UNION, INTERSECT, EXCEPT.

• Indexes:

o Concept: A special lookup table that the database search engine can use to speed up
data retrieval operations. Like an index in a book.

o Types:

▪ Clustered Index: Determines the physical order of data storage in the table.
A table can have only one clustered index (often the Primary Key).

▪ Non-Clustered Index: A logical ordering of data. Stores a copy of the indexed


columns and pointers to the actual data rows. A table can have multiple non-
clustered indexes.

o Benefits: Significantly speeds up SELECT queries, especially with WHERE clauses,


JOINs, and ORDER BY clauses.

o Drawbacks:
▪ Increases storage space.

▪ Slows down INSERT, UPDATE, and DELETE operations because the index itself
must also be updated.

o When to Use: On columns frequently used in WHERE clauses, JOIN conditions,


ORDER BY clauses, or for enforcing uniqueness (UNIQUE constraint often implies an
index).

6.2. JDBC (Java Database Connectivity)

• Concept: A Java API that provides a standard way for Java applications to connect to and
interact with various relational databases.

• Architecture:

o Application: Your Java code.

o JDBC API: The interfaces and classes defined in java.sql and javax.sql.

o JDBC Driver Manager: Manages the various JDBC drivers.

o JDBC Driver: A vendor-specific implementation (e.g., MySQL Connector/J) that


translates JDBC API calls into the database's native protocol.

o Database: The actual relational database.

• Typical Steps to use JDBC:

1. Load the JDBC driver (Class.forName("com.mysql.cj.jdbc.Driver"); - usually implicit


now).

2. Establish a Connection (DriverManager.getConnection(url, username, password)).

3. Create a Statement or PreparedStatement.

▪ Statement: For simple, static SQL queries. Prone to SQL injection.

▪ PreparedStatement: (Highly Recommended) For parameterized queries.


Prevents SQL injection, improves performance for repeated queries.

4. Execute the query (executeQuery for SELECT, executeUpdate for


INSERT/UPDATE/DELETE).

5. Process the ResultSet (if it's a SELECT query).

6. Close resources (in finally blocks): ResultSet, Statement, Connection (in that order).

• Problem with JDBC: Verbose, requires lots of boilerplate code for common operations, error-
prone (forgetting to close resources). This led to the development of ORMs.

6.3. JPA (Java Persistence API)

• Concept: A specification for object-relational mapping (ORM) in Java. It defines how to


manage relational data in Java applications using an object-oriented approach. It's a standard
API for persistence.

• Key Features:
o Annotations: Uses annotations (e.g., @Entity, @Table, @Id) to map Java classes to
database tables and fields to columns.

o EntityManager: The primary interface for interacting with the persistence context (a
set of managed entities). Used for performing CRUD operations, finding entities, and
querying.

o JPQL (Java Persistence Query Language): An object-oriented query language defined


by JPA. It queries entities and their relationships, not directly database tables.

• Why JPA? It hides the complexity of JDBC, allowing developers to focus on business logic
rather than SQL.

6.4. Hibernate

• Concept: A powerful, open-source ORM framework that is a popular implementation of the


JPA specification. While it can be used standalone, it's most commonly used as the
underlying ORM provider for JPA in Spring applications.

• Features:

o Native SQL, HQL (Hibernate Query Language), Criteria API: Provides various ways to
query data. HQL is similar to JPQL but has some Hibernate-specific extensions.

o Caching (First-Level and Second-Level):

▪ First-Level Cache (Session Cache):

▪ Default, enabled by EntityManager (or Session in native Hibernate).

▪ Per-session cache. All entities loaded or persisted within a single


EntityManager instance are stored here.

▪ When you query an entity, Hibernate first checks the first-level


cache. If found, it returns the cached instance; otherwise, it fetches
from the database.

▪ Scope: Short-lived, associated with the current EntityManager


(transaction).

▪ Second-Level Cache (Shared Cache):

▪ Optional, needs to be explicitly enabled and configured (e.g.,


Ehcache, Redis).

▪ Shared across multiple EntityManager (or Session) instances.

▪ When an entity is loaded from the database, it's stored in the


second-level cache. Subsequent requests for the same entity from
different sessions can retrieve it from this cache without hitting the
database.

▪ Scope: Long-lived, shared across the entire application's


EntityManagerFactory.

▪ Types: Read-only, Read-write, Non-strict read-write, Transactional.


▪ When to Use: For data that is frequently read and rarely updated.

o Dirty Checking:

▪ Concept: Hibernate automatically detects changes made to managed


entities (entities loaded into the persistence context) without needing
explicit update() calls.

▪ How it works: When an entity is loaded, Hibernate stores a snapshot of its


state. Before flushing (synchronizing changes to the database), it compares
the current state of the entity with its snapshot. If changes are detected, an
UPDATE statement is automatically generated and executed.

▪ Benefits: Simplifies update operations, makes code cleaner.

6.5. N+1 Problem

• Concept: A common performance anti-pattern in ORM frameworks (like Hibernate/JPA). It


occurs when, to retrieve a collection of "parent" entities, an initial query is executed (1
query), and then for each parent entity, a separate query is executed to fetch its "child"
(associated) entities (N queries). This results in N+1 queries instead of a single, optimized
query.

• Scenario:

o Imagine Book and Author entities, where a Book has one Author (@ManyToOne) and
an Author can have multiple Books (@OneToMany).

o If you load N Author entities and then iterate through them to access their books
collection (which is lazy-loaded), Hibernate will execute N separate queries to fetch
the books for each author.

• Example (Conceptual):

Java

// N+1 problem likely if books collection is LAZY

List<Author> authors = authorRepository.findAll(); // 1 query to get N authors

for (Author author : authors) {

System.out.println(author.getName());

for (Book book : author.getBooks()) { // N queries, one for each author's books

System.out.println(" - " + book.getTitle());

• Solutions to N+1 Problem:

1. FetchType.EAGER (Use with Caution!):

▪ Can set the fetch type on the @OneToMany, @ManyToMany, @ManyToOne,


@OneToOne annotations to FetchType.EAGER.
▪ Problem: Causes immediate fetching of all associated data, leading to
potentially large result sets and performance issues if not all eager-fetched
data is needed. Not a general solution.

▪ Example: @OneToMany(mappedBy = "author", fetch = FetchType.EAGER)

2. JOIN FETCH in JPQL/HQL: (Most common and recommended for specific use cases)

▪ Explicitly tells the ORM to fetch the associated collection/entity in the same
query using a JOIN clause.

▪ Java

▪ // In your Spring Data JPA repository:

▪ @Query("SELECT a FROM Author a JOIN FETCH a.books")

▪ List<Author> findAllAuthorsWithBooks();

▪ Benefits: Fetches all necessary data in a single query, avoiding N+1.

▪ Drawbacks: Can lead to a Cartesian product if multiple collections are


eagerly fetched, which can bloat the result set.

3. @EntityGraph (Spring Data JPA specific):

▪ Provides a way to define a "fetch plan" for entities and their associated
relationships.

▪ You can specify which attributes of an entity graph (e.g., entity itself and its
associations) to fetch eagerly.

▪ Java

▪ // In your Spring Data JPA repository:

▪ @EntityGraph(attributePaths = {"books"})

▪ List<Author> findAll();

▪ Benefits: Cleaner than JOIN FETCH for simple graph fetching, often
preferred.

▪ Type: Can be FETCH (forces joins) or LOAD (allows lazy loading for unspecifed
attributes).

4. Batch Fetching (e.g., batch_size in Hibernate):

▪ A less common but effective strategy. Hibernate can fetch lazy


collections/entities in batches rather than one by one.

▪ Configure hibernate.default_batch_fetch_size or @BatchSize annotation on


relationships.

▪ Example: @BatchSize(size = 10) on a @OneToMany relationship. When


accessing the collection, instead of 1 query per parent, it might do 1 query
for every 10 parents. Reduces N to N/batch_size.
5. Separate Queries (if JOIN FETCH is not ideal):

▪ Sometimes, it's better to fetch entities and their related data in separate
queries if the relations are complex or the data is not always needed
together.

▪ Can use Maps to store and associate entities.

6. DTOs (Data Transfer Objects):

▪ Instead of fetching entire entities, fetch only the necessary data into a DTO.
This avoids loading associated lazy collections at all if you only need a subset
of data.

▪ Often combined with constructor-based projections in Spring Data JPA


(@Query("SELECT new com.example.dto.AuthorDto(a.id, a.name) FROM
Author a")).

6.6. Database Normalization & Denormalization

• Database Normalization:

o Concept: A systematic process of organizing the columns and tables of a relational


database to minimize data redundancy and improve data integrity. It involves
dividing large tables into smaller, more manageable tables and defining relationships
between them.

o Normal Forms (1NF, 2NF, 3NF, BCNF): A set of guidelines for database design.

▪ 1NF (First Normal Form): Each column must contain atomic (indivisible)
values. No repeating groups.

▪ 2NF (Second Normal Form): Must be in 1NF, and all non-key attributes must
be fully dependent on the primary key. (Eliminates partial dependencies).

▪ 3NF (Third Normal Form): Must be in 2NF, and all non-key attributes must
not depend on other non-key attributes (eliminate transitive dependencies).

▪ BCNF (Boyce-Codd Normal Form): A stronger version of 3NF, addresses


certain types of anomalies not covered by 3NF.

o Benefits:

▪ Reduces data redundancy (less storage, easier to maintain).

▪ Improves data integrity (avoids update, insert, delete anomalies).

▪ Makes the database schema more flexible and easier to modify.

o Drawbacks:

▪ Can lead to more tables and more complex JOIN operations, potentially
impacting read performance.

• Database Denormalization:
o Concept: The process of intentionally introducing redundancy into a database
schema, often by combining data from multiple tables into a single table. It's
typically done after a database has been normalized.

o Purpose: To improve read performance (especially for complex reports or frequently


accessed queries) by reducing the number of JOIN operations required.

o Trade-offs:

▪ Pros: Faster read queries, simpler queries.

▪ Cons: Increases data redundancy, potential for update/insert/delete


anomalies (data inconsistency), higher storage requirements.

o When to Use: When read performance is critical and outweighs the risks of data
redundancy and potential inconsistency, especially in data warehousing, reporting,
or heavily read-optimized applications. Careful management (e.g., using triggers or
batch processes to maintain consistency) is often required.

A strong understanding of these database concepts, particularly SQL, ACID properties, transaction
isolation levels, and ORM usage (JPA/Hibernate with Spring Data JPA), including performance
considerations like the N+1 problem, is essential for a seasoned Java developer. Be ready to discuss
specific examples from your projects.

Alright, let's break down "7. Testing" in detail. For a Java developer with 3 years of experience,
testing is not just a concept but a critical part of the daily development workflow. You're expected to
write effective tests, understand different testing levels, and use modern testing frameworks and
practices.

7. Testing: Ensuring Code Quality and Reliability

Testing is an integral part of the software development lifecycle, aimed at verifying that an
application functions as expected, meets requirements, and is free of defects. In modern Java
development, a robust testing strategy is crucial.

7.1. Unit Testing

• Concept: The smallest level of testing, where individual units or components of a software
are tested in isolation from the rest of the application.

o Unit: Typically a single class or even a single method.

• Purpose: To verify that each unit of the source code performs exactly as intended.

• Characteristics:

o Isolated: Units are tested independently, often using mocks or stubs to isolate them
from their dependencies (e.g., databases, external services, other complex classes).
o Fast: Unit tests should execute very quickly, allowing developers to run them
frequently.

o Automated: Typically automated and run as part of the build process.

o High Coverage: Aims for high code coverage to ensure most of the logic is tested.

• Frameworks/Tools:

o JUnit 5 (Jupiter, Platform, Vintage): The de-facto standard testing framework for
Java.

▪ @Test: Marks a method as a test method.

▪ @DisplayName: Provides a more readable name for the test.

▪ @BeforeEach / @AfterEach: Methods run before/after each test method in


a class.

▪ @BeforeAll / @AfterAll: Methods run once before/after all test methods in


a class (must be static).

▪ @Disabled: Skips a test.

▪ Assertions class: Provides static methods for asserting expected outcomes


(e.g., assertEquals, assertTrue, assertNotNull, assertThrows).

▪ Parameterized Tests (@ParameterizedTest, @ValueSource, @CsvSource,


@MethodSource): Run the same test method multiple times with different
input arguments.

o Mockito: A popular mocking framework for Java.

▪ Purpose: To create mock objects for dependencies that are too complex,
slow, or external to be used in a unit test.

▪ @Mock: Creates a mock object.

▪ @InjectMocks: Injects the mocks into the object under test.

▪ when().thenReturn(): Defines the behavior of a mocked method call.

▪ verify(): Verifies that a method on a mock object was called a certain


number of times or with specific arguments.

▪ any(), eq(): Argument matchers for when() and verify().

• Example (JUnit + Mockito):

Java

import org.junit.jupiter.api.Test;

import org.junit.jupiter.api.extension.ExtendWith;

import org.mockito.InjectMocks;

import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;

import static org.mockito.Mockito.when;

import static org.mockito.Mockito.verify;

import static org.mockito.Mockito.times;

// A simple service we want to test

class UserService {

private UserRepository userRepository; // Dependency

public UserService(UserRepository userRepository) {

this.userRepository = userRepository;

public String getUserFullName(Long id) {

User user = userRepository.findById(id);

if (user != null) {

return user.getFirstName() + " " + user.getLastName();

return null;

public void createUser(String firstName, String lastName) {

User newUser = new User(firstName, lastName);

userRepository.save(newUser);

// A dependency interface

interface UserRepository {
User findById(Long id);

void save(User user);

// A simple POJO

class User {

private String firstName;

private String lastName;

public User(String firstName, String lastName) {

this.firstName = firstName;

this.lastName = lastName;

public String getFirstName() { return firstName; }

public String getLastName() { return lastName; }

@ExtendWith(MockitoExtension.class) // Integrates Mockito with JUnit 5

class UserServiceTest {

@Mock // Creates a mock instance of UserRepository

private UserRepository userRepository;

@InjectMocks // Injects the mocked userRepository into userService

private UserService userService;

@Test

void testGetUserFullName_ExistingUser() {

// Define mock behavior: when findById(1L) is called, return a specific User object

when(userRepository.findById(1L)).thenReturn(new User("John", "Doe"));


String fullName = userService.getUserFullName(1L);

// Assertions

assertEquals("John Doe", fullName);

// Verify that findById was called exactly once with argument 1L

verify(userRepository, times(1)).findById(1L);

@Test

void testGetUserFullName_NonExistingUser() {

when(userRepository.findById(2L)).thenReturn(null);

String fullName = userService.getUserFullName(2L);

assertEquals(null, fullName);

verify(userRepository, times(1)).findById(2L);

@Test

void testCreateUser() {

userService.createUser("Jane", "Smith");

// Verify that save was called exactly once with any User object

verify(userRepository, times(1)).save(Mockito.any(User.class));

7.2. Integration Testing

• Concept: Tests the interaction between different components or layers of an application. It


verifies that modules or services work together correctly.
• Purpose: To find defects in the interfaces and interactions between integrated components.

• Characteristics:

o Less Isolated: Involves multiple components, possibly including real databases,


external services, or message queues.

o Slower: Can take longer to run due to external dependencies and setup.

o Automated: Still automated, often run less frequently than unit tests.

• Frameworks/Tools:

o Spring Boot Test: Provides excellent support for writing integration tests for Spring
Boot applications.

▪ @SpringBootTest: Loads the full Spring application context (or a slice of it).
Can be configured to run with a specific WebEnvironment (e.g.,
RANDOM_PORT).

▪ @Autowired: Can inject actual Spring beans from the context into the test.

▪ @DataJpaTest: Specifically for testing JPA repositories. It sets up an in-


memory database (e.g., H2) and auto-configures JPA components. Useful for
testing database interactions without loading the full web layer.

▪ @WebMvcTest: Specifically for testing Spring MVC controllers. It auto-


configures Spring MVC components but doesn't load the full application
context or data layer. Often used with MockMvc.

o MockMvc: Provided by Spring Test. Used for testing Spring MVC controllers without
starting a full HTTP server. It simulates HTTP requests and responses.

o Testcontainers: A powerful library that allows you to spin up lightweight, throwaway


instances of databases, message brokers, web browsers, or anything else that can
run in a Docker container for your tests. Ideal for true integration tests with real
dependencies.

o WireMock: A mock server for HTTP-based APIs. Useful for simulating external service
dependencies in integration tests.

• Example (Spring Boot Integration Test with MockMvc and @WebMvcTest):

Java

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;

import org.springframework.boot.test.mock.mockito.MockBean;

import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;


import static org.mockito.Mockito.when;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// Assume a simple controller

@RestController

class MyController {

private MyService myService; // Assume MyService is a dependency

public MyController(MyService myService) {

this.myService = myService;

@GetMapping("/hello")

public String hello() {

return myService.greet();

interface MyService { // Service dependency

String greet();

@WebMvcTest(MyController.class) // Tests only the web layer (MyController)

class MyControllerIntegrationTest {

@Autowired

private MockMvc mockMvc; // Used to simulate HTTP requests

@MockBean // Mocks MyService and adds it to the Spring context


private MyService myService;

@Test

void testHelloEndpoint() throws Exception {

// Define behavior for the mocked service

when(myService.greet()).thenReturn("Hello from service!");

mockMvc.perform(get("/hello")) // Perform a GET request to /hello

.andExpect(status().isOk()) // Expect HTTP 200 OK

.andExpect(content().string(containsString("Hello from service!"))); // Expect specific


content

7.3. Test Driven Development (TDD)

• Concept: A software development process where tests are written before the actual code
that implements the functionality. It's an iterative development approach.

• The "Red-Green-Refactor" Cycle:

1. Red (Write a failing test): Write a new unit test for a small piece of functionality that
doesn't exist yet. The test should fail.

2. Green (Make the test pass): Write just enough production code to make the failing
test pass. Don't worry about perfect design or optimization yet.

3. Refactor (Improve the code): Once the test passes, refactor the production code
(and potentially the test code) to improve its design, readability, and performance,
ensuring all tests still pass.

4. Repeat the cycle.

• Benefits:

o Forces Good Design: Encourages writing small, focused, testable units of code.

o Improved Code Quality: Leads to cleaner, more modular code with fewer bugs.

o Built-in Regression Suite: Every new feature adds to your safety net of tests.

o Reduces Debugging Time: Issues are caught early.

o Clearer Requirements: Writing tests first clarifies understanding of requirements.

o Confidence in Changes: Refactoring and adding features is less risky with a


comprehensive test suite.
• Drawbacks:

o Initial learning curve.

o Can feel slower initially (though faster in the long run).

o Requires discipline.

7.4. Mocking vs. Spying

Both Mockito features, used to control the behavior of dependencies in tests.

• Mocking (@Mock):

o Concept: Creates a completely fake object. It's a "dummy" implementation of an


interface or class that you programmatically control.

o Behavior: By default, mocks do nothing. You must explicitly define the behavior of
any method calls you expect on the mock using when().thenReturn() (stubbing).

o Use Case: When you want to isolate the code under test completely from its
dependencies, especially when dependencies are complex, external, or have side
effects. You're testing how the code under test interacts with its dependencies.

o Example: UserRepository in the Unit Testing example above. We don't want to hit a
real database; we just want to control what findById returns.

• Spying (@Spy or Mockito.spy()):

o Concept: Creates a partial mock or a "spy." It wraps a real object.

o Behavior: By default, a spy invokes the real methods of the actual object. You can
selectively override (stub) certain method calls if needed, while other methods will
still call the original implementation.

o Use Case: When you want to test a real object but need to mock only specific
methods of that object, or when you want to verify calls on a real object. This is
useful for legacy code or when the object itself has internal state you want to
preserve for some methods, while mocking others.

o Example: If you have a complex PaymentProcessor that performs many steps, and
you only want to mock the final sendToGateway() call while letting other calculation
methods run normally.

• Key Difference Summary:

o Mock: A completely fake object. You tell it what to do.

o Spy: A real object that you observe/partially override. It does its real work unless you
explicitly tell it otherwise.

• Example (Spying):

Java

import org.junit.jupiter.api.Test;

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Spy;

import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;

import static org.mockito.Mockito.when;

import static org.mockito.Mockito.verify;

import static org.mockito.Mockito.times;

class Calculator {

public int add(int a, int b) {

System.out.println("Real add method called.");

return a + b;

public int multiply(int a, int b) {

System.out.println("Real multiply method called.");

return a * b;

@ExtendWith(MockitoExtension.class)

class CalculatorTest {

@Spy // Spy on a real Calculator instance

private Calculator calculator; // Mockito will create a real instance and wrap it

@Test

void testAddWithSpy() {

int result = calculator.add(2, 3); // Calls the real add method

assertEquals(5, result);

verify(calculator, times(1)).add(2, 3); // Verify the real method was called

}
@Test

void testMultiplyWithSpyAndStubbing() {

// Stub the multiply method to return a specific value

when(calculator.multiply(2, 3)).thenReturn(100);

int result = calculator.multiply(2, 3); // Calls the stubbed behavior

assertEquals(100, result);

verify(calculator, times(1)).multiply(2, 3); // Verify it was called

7.5. Code Coverage

• Concept: A metric that measures the percentage of your application's source code that is
executed by your test suite. It helps identify untested areas of your code.

• Types of Coverage:

o Line Coverage: Percentage of executable lines that were executed.

o Branch Coverage / Decision Coverage: Percentage of if/else, switch, loops where all
branches (true/false paths) were executed.

o Method Coverage: Percentage of methods that were called.

o Class Coverage: Percentage of classes that were loaded.

• Tools:

o JaCoCo (Java Code Coverage): A popular open-source code coverage library for Java.
Integrates well with Maven/Gradle. Generates reports (HTML, XML, CSV) showing
coverage percentages and highlighting uncovered lines.

o SonarQube: A platform for continuous inspection of code quality and security.


Integrates with JaCoCo to display coverage metrics alongside other code quality
issues.

• Importance:

o Provides an indicator of how thoroughly your code is being tested.

o Helps identify "dead code" (unreachable code).

o Guides future testing efforts (focus on low-coverage areas).

• Limitations: High code coverage does not guarantee a bug-free application. It only tells you
what code was run, not if the code works correctly for all possible scenarios or edge cases.
It's a quantitative metric, not qualitative. You can have 100% coverage but still have logical
bugs if your assertions are weak or you miss edge cases in your tests.

For a 3-year experienced Java developer, you should be comfortable not only writing unit and
integration tests but also explaining their purpose, the trade-offs, and the tools involved. You should
be familiar with TDD principles and why it's beneficial. Understanding the nuances of mocking vs.
spying and the meaning of code coverage are also key indicators of a solid testing background.

Got it! Let's delve into "8. Version Control (Git)" in detail. For a Java developer (or any developer for
that matter) with 3 years of experience, a deep understanding of Git is non-negotiable. It's the
backbone of collaborative development and project management.

8. Version Control (Git): Managing Code Changes

Version Control Systems (VCS) are tools that help software teams manage changes to source code
over time. They keep track of every modification made to the code, allowing developers to revert to
previous versions, compare changes, and collaborate efficiently without overwriting each other's
work.

Git is by far the most popular distributed version control system (DVCS).

8.1. Why Version Control?

• Tracking Changes: Records who changed what, when, and why.

• Collaboration: Enables multiple developers to work on the same codebase simultaneously


without conflicts.

• Reversion: Easily revert to any previous state of the code.

• Branching & Merging: Facilitates parallel development of features and bug fixes without
affecting the main codebase.

• Backup & Disaster Recovery: The entire history is distributed, providing redundancy.

• Audit Trail: Provides a history for compliance and debugging.

8.2. Git Basics

• Distributed Version Control System (DVCS):

o Unlike centralized VCS (like SVN, Perforce) where there's a single central repository,
in a DVCS, every developer has a complete copy of the entire repository (including
its full history) on their local machine.

o Benefits:

▪ Offline Work: Developers can commit changes locally even without network
access.

▪ Faster Operations: Most operations (commits, diffs, Browse history) are


done locally, making them very fast.
▪ Resilience: No single point of failure; if the central server goes down,
development can continue using local copies.

▪ Flexibility: Easier to experiment with branches.

• Core Concepts:

o Repository (Repo): A .git directory at the root of your project that contains all the
history, commits, and other metadata for your project.

o Commit: A snapshot of your project's files at a specific point in time. Each commit
has a unique SHA-1 hash, a commit message, author, date, and a pointer to its
parent commit(s).

o Branch: A lightweight, movable pointer to a commit. Branches allow you to diverge


from the main line of development and work on new features or bug fixes in
isolation.

o Head: A pointer to the tip of the current branch.

o Index (Staging Area/Cache): An intermediate area between your working directory


and your repository. It's where you prepare the next commit. Files in the staging area
are the ones that will be included in the next commit.

o Working Directory: The actual files you are currently working on in your file system.

• Common Commands & Workflow:

1. git init: Initializes a new Git repository in the current directory.

2. git clone <repository_url>: Copies an existing remote repository to your local


machine.

3. git status: Shows the status of your working directory and staging area (which files
are modified, staged, or untracked).

4. git add <file(s)> / git add .: Adds changes from the working directory to the staging
area.

5. git commit -m "Your commit message": Records the staged changes to the
repository. The commit message should be descriptive.

6. git log: Displays the commit history.

▪ git log --oneline: Concise log.

▪ git log --graph --oneline --all: Visualizes branch history.

7. git diff: Shows changes between different states:

▪ git diff: Shows changes in the working directory not yet staged.

▪ git diff --staged: Shows changes in the staging area not yet committed.

▪ git diff <commit1> <commit2>: Shows difference between two commits.

8. git push: Uploads your local commits to a remote repository.


9. git pull: Fetches changes from a remote repository and merges them into your
current local branch. (Equivalent to git fetch followed by git merge).

10. git fetch: Downloads commits, files, and refs from a remote repository into your
local repository, but does not merge them into your working branch.

8.3. Branching Strategies

Branching is Git's superpower for parallel development.

• git branch:

o git branch: Lists all local branches.

o git branch <branch_name>: Creates a new branch.

o git branch -d <branch_name>: Deletes a local branch (only if merged).

o git branch -D <branch_name>: Forces deletion of a local branch (even if unmerged).

• git checkout <branch_name>: Switches to an existing branch.

• git checkout -b <new_branch_name>: Creates a new branch and switches to it. (Shorthand
for git branch <new_branch_name> then git checkout <new_branch_name>).

• git merge <branch_to_merge>: Integrates changes from a specified branch into the current
branch.

o Fast-Forward Merge: If the target branch has no new commits since the source
branch branched off, Git simply moves the pointer forward.

o Three-Way Merge: If both branches have diverged, Git creates a new "merge
commit" that combines the changes from both.

• git rebase <base_branch>:

o Concept: Rewrites commit history. It takes your commits from your current branch
and reapplies them one by one on top of the specified <base_branch>.

o Result: Creates a linear history, avoiding merge commits.

o When to Use: To incorporate changes from the main branch into your feature branch
before merging, making the history cleaner for a later fast-forward merge.

o Caution: Never rebase public/shared branches (like main or develop) because it


rewrites history, which can cause significant problems for other collaborators. Only
rebase your private feature branches.

• Common Strategies:

o Git Flow: A more complex, strict branching model with long-running branches
(master, develop) and supporting branches (feature, release, hotfix). Good for larger,
regulated projects.

o GitHub Flow / GitLab Flow: Simpler, lightweight strategies often centered around
main (or master) branch. Features are developed on short-lived branches, merged
into main, and then main is directly deployed. Preferred for continuous delivery and
smaller teams.

8.4. Handling Conflicts

• What is a Conflict?

o Occurs when Git tries to merge two branches, and the same line(s) of code (or the
same file) have been modified differently in both branches. Git cannot automatically
decide which change to keep.

• When Conflicts Happen:

o During git merge.

o During git rebase.

o During git pull (which performs a fetch and merge).

• Resolution Process:

1. Git will mark the conflicting sections in the file using special markers (<<<<<<<,
=======, >>>>>>>).

2. You manually edit the file to resolve the conflict (decide which version to keep, or
combine them).

3. After editing, git add <conflicted_file> to stage the resolved file.

4. git commit -m "Merge conflict resolved" (if merging) or git rebase --continue (if
rebasing).

• Example of Conflict Markers:

• <<<<<<< HEAD (or current branch)

• This is line A in my current branch.

• =======

• This is line A from the incoming branch.

• >>>>>>> feature/new-feature (or incoming branch)

You would edit this section to:

This is the combined and resolved line A.

8.5. git reset vs. git revert

Both commands are used to undo changes, but they do so in fundamentally different ways.

• git revert <commit_hash>:

o Concept: Creates a new commit that undoes the changes introduced by the specified
commit. It does not rewrite history.

o Effect: The original commit still exists in the history, but its effects are cancelled out
by the new revert commit.
o When to Use: When you need to undo changes on a public/shared branch (e.g.,
main, develop) because it preserves the history and doesn't affect other developers'
clones.

o Analogy: You made a mistake, so you wrote a new instruction to undo the mistake,
but the original instruction is still on the record.

• git reset <mode> <commit_hash>:

o Concept: Moves the HEAD pointer (and potentially the branch pointer and
index/working directory) to a specified commit. It effectively rewrites history by
discarding subsequent commits.

o Modes:

▪ --soft: Moves HEAD and the current branch pointer to the target commit.
The changes from the "undone" commits are kept in the staging area.

▪ --mixed (Default): Moves HEAD and the current branch pointer. The changes
are kept in the working directory (unstaged).

▪ --hard: Moves HEAD and the current branch pointer. Discards all changes
(from working directory, staging area, and commits) from the target commit
onwards. DANGEROUS! Use with extreme caution as data can be lost.

o When to Use: To undo changes on your local, private feature branches before you
have pushed them to a remote repository. It keeps your history clean and linear.

o Analogy: You erased the mistake from the record as if it never happened.

• Summary Table:

Feature git revert git reset

Does NOT rewrite history. Rewrites history. Discards subsequent


History
Adds a new commit. commits.

Safe for shared/public Dangerous on shared branches (causes


Safety
branches. divergence). Safe on private branches.

Undoes changes by adding an Moves branch pointer (and data depending on


Action
inverse commit. mode).

Undo changes publicly, keep


Goal Clean up private history, discard changes.
history.

8.6. git stash

• Concept: Temporarily saves changes that you don't want to commit immediately, allowing
you to switch branches or perform other operations without committing incomplete work.
• Use Case:

o You're working on a feature, and a high-priority bug needs fixing on main. You don't
want to commit incomplete work. Stash your changes, switch to main, fix the bug,
then come back to your feature branch and reapply the stash.

• Commands:

o git stash save "message" / git stash: Saves your current uncommitted changes (both
staged and unstaged) to a temporary stash. Your working directory becomes clean.

o git stash list: Shows a list of stashed changes.

o git stash apply: Applies the latest stash back to your working directory, but keeps the
stash in the stash list.

o git stash pop: Applies the latest stash and then removes it from the stash list. (More
common)

o git stash drop: Removes a specific stash from the stash list.

o git stash clear: Removes all stashes.

8.7. .gitignore

• Concept: A text file (.gitignore) placed in the root of your repository that tells Git which files
or directories to ignore and not track.

• Purpose: To prevent unnecessary or undesirable files from being committed to the


repository.

• Commonly Ignored Files/Directories:

o Compiled class files (.class, .jar, .war)

o IDE-specific files (e.g., .idea/ for IntelliJ, .vscode/ for VS Code)

o Build tool outputs (target/ for Maven, build/ for Gradle)

o Log files (*.log)

o Dependency directories (node_modules/, vendor/)

o Temporary files, configuration files with sensitive data (e.g., application-


dev.properties if it contains local DB credentials).

• Syntax:

o # for comments

o filename.txt: Ignores a specific file.

o *.log: Ignores all files ending with .log.

o /dir/: Ignores a directory and its contents.

o dir/: Ignores a directory anywhere in the repo.

o !file.txt: Excludes a previously ignored file (negation).


For a 3-year experienced developer, you should be able to perform all these Git operations
confidently, understand the implications of commands like rebase and reset, explain common
branching strategies, and effectively resolve merge conflicts. Git is a tool you'll use every day, so
proficiency is paramount.

No problem, let's dive into "9. Project Management Tools & Methodologies" in detail. For a 3-year
experienced Java developer, understanding these concepts isn't just about coding; it's about how
software projects are organized, delivered, and how teams collaborate effectively. You'll likely be part
of, or even lead, aspects of these processes.

9. Project Management Tools & Methodologies: Organizing Software Development

This topic covers the frameworks and tools used to plan, execute, and monitor software
development projects. The goal is to deliver high-quality software efficiently and effectively.

9.1. Agile Methodologies

Agile is an iterative and incremental approach to project management and software development
that helps teams deliver value to their customers faster and with fewer headaches. It emphasizes
flexibility, collaboration, and rapid response to change.

• Core Principles (from Agile Manifesto):

o Individuals and interactions over processes and tools.

o Working software over comprehensive documentation.

o Customer collaboration over contract negotiation.

o Responding to change over following a plan.

• Key Characteristics:

o Iterative & Incremental: Projects are broken into small, manageable iterations
(sprints/cycles) where a small piece of working software is delivered.

o Customer Collaboration: Continuous feedback from the customer is encouraged.

o Self-Organizing Teams: Teams are empowered to decide how to best achieve their
goals.

o Adaptive Planning: Plans are flexible and evolve as requirements become clearer.

o Continuous Improvement: Teams regularly reflect on their work and find ways to
improve.

• Common Agile Frameworks:

o Scrum:

▪ The most popular Agile framework. It provides a lightweight framework for


developing and delivering complex products.
▪ Key Roles:

▪ Product Owner: Represents the voice of the customer. Manages the


Product Backlog, ensures value, prioritizes features.

▪ Scrum Master: A servant-leader who facilitates the Scrum process,


removes impediments, and coaches the team on Agile principles.
Not a project manager or team lead in the traditional sense.

▪ Development Team: A self-organizing, cross-functional group


responsible for delivering the product increment. Typically 3-9
members.

▪ Key Events (Ceremonies):

▪ Sprint Planning: (Beginning of Sprint) Team selects items from the


Product Backlog to work on during the sprint and plans how to
achieve the Sprint Goal.

▪ Daily Scrum (Stand-up): (Daily, 15 mins) Team members answer:


"What did I do yesterday?", "What will I do today?", "Are there any
impediments?".

▪ Sprint Review: (End of Sprint) Inspects the Increment and adapts the
Product Backlog if needed. Stakeholders provide feedback.

▪ Sprint Retrospective: (End of Sprint) Team inspects how the last


sprint went with regard to individuals, interactions, processes, and
tools, and identifies improvements for the next sprint.

▪ Key Artifacts:

▪ Product Backlog: A prioritized, dynamic list of all known


requirements for the product. Managed by the Product Owner.

▪ Sprint Backlog: A subset of the Product Backlog selected for a sprint,


plus the plan for delivering the increment. Managed by the
Development Team.

▪ Increment (Potentially Shippable Product): The sum of all Product


Backlog items completed during a sprint and all previous sprints. It
must be in a "Done" state.

▪ Sprint: A time-boxed iteration (typically 1-4 weeks) during which a "Done,"


usable, and potentially releasable product Increment is created.

o Kanban:

▪ Focuses on visualizing workflow, limiting work in progress (WIP), and


maximizing flow.

▪ Key Principles:

▪ Visualize the workflow (Kanban board with columns like "To Do," "In
Progress," "Done").
▪ Limit Work in Progress (WIP) to prevent bottlenecks and ensure
focus.

▪ Manage flow (optimize the speed and smoothness of items moving


through the workflow).

▪ Make process policies explicit.

▪ Implement feedback loops.

▪ Improve collaboratively, evolve experimentally.

▪ Use Case: Ideal for maintenance teams, support teams, or workflows with
unpredictable demand where continuous flow is more important than fixed
iterations.

9.2. Scrum vs. Kanban (Key Differences)

Feature Scrum Kanban

Cadence Time-boxed sprints (e.g., 2-4 weeks) Continuous flow, no fixed iterations

Defined roles (Product Owner, Scrum Master,


Roles No prescribed roles, team-driven
Dev Team)

Artifacts Product Backlog, Sprint Backlog, Increment Kanban Board, WIP Limits, Flow metrics

No prescribed meetings, focus on ad-hoc


Meetings Daily Scrum, Sprint Planning, Review, Retro
sync

Change Changes discouraged within a sprint Changes can be introduced at any time

Deliver potentially shippable increment per Improve flow, reduce lead time, deliver
Goals
sprint continuously

WIP
Implicit (via Sprint Planning) Explicitly defined on the board
Limits

Export to Sheets

9.3. Project Management Tools

These tools facilitate the implementation of Agile and other methodologies, providing platforms for
task management, collaboration, and tracking.

• Jira:

o Type: Issue tracking and project management software.

o Features: Highly configurable for Scrum, Kanban, and custom workflows.

▪ Issue Types: Supports various issue types like Stories, Tasks, Bugs, Epics.

▪ Workflows: Customizable workflows to define the lifecycle of issues.


▪ Boards: Scrum Boards (sprints, burndown charts) and Kanban Boards (WIP
limits, swimlanes).

▪ Reporting: Dashboards, various agile reports (velocity, sprint reports).

▪ Integrations: Integrates with Git (Bitbucket, GitHub), CI/CD tools (Jenkins),


Confluence, etc.

o Use Case: Widely used in software development for tracking features, bugs, tasks,
and managing Agile projects.

• Trello:

o Type: Simple, visual project management tool based on Kanban boards.

o Features:

▪ Boards, Lists, Cards: Core elements. Boards represent projects, lists


represent stages, cards represent tasks.

▪ Checklists, Due Dates, Attachments: Basic task management features.

▪ Power-Ups: Integrations with other tools (e.g., Slack, Google Drive).

o Use Case: Excellent for small teams, personal task management, or simple projects
where visual organization and ease of use are priorities. Less suited for complex
enterprise-level projects requiring detailed reporting and strict workflows.

• Confluence:

o Type: Collaboration and knowledge management software, often used with Jira.

o Features:

▪ Wikis: Create and organize documentation, meeting notes, project


requirements, knowledge bases.

▪ Templates: Predefined templates for various documents (e.g., meeting


notes, retrospectives, technical specs).

▪ Integrations: Deep integration with Jira for linking documentation to issues.

▪ Versioning: Tracks changes to pages.

o Use Case: Ideal for creating and sharing project documentation, team wikis, decision
records, and knowledge management within an organization.

• Asana / Monday.com / ClickUp (and many others):

o General-purpose project management and work management tools that can often
be configured to support Agile methodologies. They offer varying levels of features,
complexity, and integrations, catering to different team sizes and needs.

9.4. Other Methodologies (Briefly Mentioned)

While Agile is dominant, it's good to be aware of others:

• Waterfall Model:
o Concept: A traditional, linear, sequential approach where each phase (Requirements,
Design, Implementation, Testing, Deployment, Maintenance) must be completed
before the next phase begins.

o Characteristics: Heavy documentation, rigid planning, difficult to adapt to changes.

o Use Case: Rarely used for complex software projects today, sometimes for small,
well-defined, low-risk projects.

• DevOps (Practice, not strictly a PM methodology):

o Concept: A set of practices that combines software development (Dev) and IT


operations (Ops) to shorten the systems development life cycle and provide
continuous delivery with high software quality.

o Emphasis: Automation, continuous integration, continuous delivery, continuous


monitoring, and collaboration between development and operations teams.

o Impact on PM: Fosters a culture of shared responsibility and faster feedback loops.

For a 3-year experienced developer, you should be able to articulate the advantages of Agile,
specifically Scrum, and discuss the roles, events, and artifacts. You should also be familiar with the
practical application of tools like Jira, Trello, and Confluence, having likely used them in your previous
roles. Understanding the "why" behind these methodologies and tools, and how they contribute to
successful software delivery, is as important as knowing their features.

You might also like