Java is a statically typed, compiled, object-oriented language that runs on the Java Virtual Machine (JVM). It powers enterprise backends, Android apps, and large-scale distributed systems. Its philosophy: write once, run anywhere.

Prerequisites#

  • Programming Logic (OOP concepts, data structures)
  • Basic understanding of compilation vs interpretation

1. JVM Architecture#

flowchart TD
    A[".java source"] --> B["javac compiler"]
    B --> C[".class bytecode"]
    C --> D["Class Loader"]
    D --> E["JVM"]
    
    subgraph JVM
        E --> F["Bytecode Verifier"]
        F --> G["Interpreter"]
        F --> H["JIT Compiler"]
        G --> I["Execution"]
        H --> I
    end

Key Components#

ComponentRole
javacCompiles .java → .class (bytecode)
Class LoaderLoads classes into memory on demand
Bytecode VerifierEnsures bytecode is safe (no illegal memory access)
InterpreterExecutes bytecode line by line
JIT CompilerCompiles hot paths to native machine code at runtime

Why the JVM Matters#

  • Platform independence — bytecode runs on any OS with a JVM
  • Performance — JIT compilation approaches native speed
  • Memory safety — no pointer arithmetic, automatic bounds checking
  • Ecosystem — Kotlin, Scala, Groovy, Clojure all run on the JVM

Key Takeaway#

Java’s compilation model (source → bytecode → JIT → native) gives it both portability and performance. The JVM is not just a Java runtime — it’s a platform.


2. Syntax, Types, and Operators#

Primitive Types#

TypeSizeRangeDefault
byte8 bits-128 to 1270
short16 bits-32,768 to 32,7670
int32 bits-2³¹ to 2³¹-10
long64 bits-2⁶³ to 2⁶³-10L
float32 bitsIEEE 7540.0f
double64 bitsIEEE 7540.0d
char16 bitsUnicode (UTF-16)‘\u0000’
boolean1 bit*true/falsefalse

*JVM implementation-dependent; often stored as int.

Reference Types#

Everything that isn’t a primitive is a reference type — objects allocated on the heap, accessed via references (not pointers).

String name = "Alice";     // reference to String object
int[] numbers = {1, 2, 3}; // reference to array object
List<String> items = new ArrayList<>(); // reference to ArrayList

var (Local Variable Type Inference)#

// Java 10+
var list = new ArrayList<String>();  // inferred as ArrayList<String>
var count = 0;                       // inferred as int
// Only for local variables — not fields, parameters, or return types

Operators#

// Arithmetic: +, -, *, /, %
// Comparison: ==, !=, <, >, <=, >=
// Logical: &&, ||, !
// Bitwise: &, |, ^, ~, <<, >>, >>>
// Assignment: =, +=, -=, *=, /=

// == vs .equals()
String a = new String("hello");
String b = new String("hello");
a == b        // false — different objects
a.equals(b)   // true — same content

Key Takeaway#

Java’s type system is strict — no implicit narrowing, no unsigned types (until recent additions), and a clear split between primitives (stack) and objects (heap). This rigidity prevents entire classes of bugs.


3. Object-Oriented Programming#

Classes and Objects#

public class Vehicle {
    // Fields
    private String make;
    private int year;
    private double mileage;
    
    // Constructor
    public Vehicle(String make, int year) {
        this.make = make;
        this.year = year;
        this.mileage = 0.0;
    }
    
    // Methods
    public void drive(double km) {
        if (km < 0) throw new IllegalArgumentException("Distance must be positive");
        this.mileage += km;
    }
    
    // Getter
    public double getMileage() {
        return mileage;
    }
    
    @Override
    public String toString() {
        return String.format("%d %s (%.1f km)", year, make, mileage);
    }
}

Interfaces#

Define a contract without implementation:

public interface Drawable {
    void draw(Canvas canvas);
    
    // Default method (Java 8+)
    default void drawWithBorder(Canvas canvas) {
        drawBorder(canvas);
        draw(canvas);
    }
    
    // Static method
    static Drawable empty() {
        return canvas -> {}; // lambda
    }
}

Abstract Classes#

Partial implementation — can have state and constructors:

public abstract class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    // Concrete method
    public String getColor() { return color; }
    
    // Abstract — subclasses must implement
    public abstract double area();
    public abstract double perimeter();
}

public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    
    @Override
    public double area() { return Math.PI * radius * radius; }
    
    @Override
    public double perimeter() { return 2 * Math.PI * radius; }
}

Interface vs Abstract Class#

FeatureInterfaceAbstract Class
Multiple inheritanceYesNo
State (fields)Constants onlyYes
ConstructorsNoYes
Access modifiersPublic only (pre-Java 9)Any
Use whenDefining capabilitySharing base implementation

Polymorphism#

List<Shape> shapes = List.of(
    new Circle("red", 5),
    new Rectangle("blue", 3, 4)
);

for (Shape shape : shapes) {
    // Calls the correct area() based on actual type
    System.out.println(shape.area());
}

Key Takeaway#

Java enforces OOP discipline — everything lives in a class, access is controlled via modifiers, and polymorphism is the primary extension mechanism. Prefer composition over inheritance; use interfaces to define contracts.


4. Generics and Type Erasure#

Why Generics#

Without generics, collections store Object and require casting:

// Pre-generics (unsafe)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // cast required, ClassCastException possible

// With generics (type-safe)
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // no cast needed

Generic Classes and Methods#

public class Pair<A, B> {
    private final A first;
    private final B second;
    
    public Pair(A first, B second) {
        this.first = first;
        this.second = second;
    }
    
    public A getFirst() { return first; }
    public B getSecond() { return second; }
}

// Generic method
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

Bounded Type Parameters#

// Upper bound — T must be Number or subclass
public <T extends Number> double sum(List<T> numbers) {
    return numbers.stream().mapToDouble(Number::doubleValue).sum();
}

// Multiple bounds
public <T extends Serializable & Comparable<T>> void process(T item) { ... }

Wildcards#

// Upper bounded — read-only (producer)
void printAll(List<? extends Shape> shapes) {
    for (Shape s : shapes) System.out.println(s.area());
}

// Lower bounded — write-only (consumer)
void addCircles(List<? super Circle> list) {
    list.add(new Circle("red", 5));
}

// PECS: Producer Extends, Consumer Super

Type Erasure#

At runtime, generic type information is erased. List<String> and List<Integer> are both just List. This means:

  • No new T() or new T[]
  • No instanceof with generic types
  • No overloading by generic parameter alone

Key Takeaway#

Generics provide compile-time type safety without runtime overhead (due to erasure). Use PECS to decide between extends and super wildcards.


5. Collections Framework#

flowchart TD
    I[Iterable] --> C[Collection]
    C --> L[List]
    C --> S[Set]
    C --> Q[Queue]
    
    L --> AL[ArrayList]
    L --> LL[LinkedList]
    
    S --> HS[HashSet]
    S --> TS[TreeSet]
    S --> LHS[LinkedHashSet]
    
    Q --> PQ[PriorityQueue]
    Q --> D[Deque]
    D --> AD[ArrayDeque]
    
    M[Map] --> HM[HashMap]
    M --> TM[TreeMap]
    M --> LHM[LinkedHashMap]

Choosing the Right Collection#

NeedUseWhy
Indexed accessArrayListO(1) get by index
Frequent insert/removeLinkedListO(1) at known position
Unique elementsHashSetO(1) contains
Sorted unique elementsTreeSetO(log n), maintains order
Key-value pairsHashMapO(1) get/put
Sorted key-valueTreeMapO(log n), sorted keys
Insertion orderLinkedHashMapO(1) + preserves order
FIFO queueArrayDequeFaster than LinkedList
Priority queuePriorityQueueMin-heap by default

Immutable Collections (Java 9+)#

List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> primes = Set.of(2, 3, 5, 7, 11);
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87);
// All throw UnsupportedOperationException on modification

Key Takeaway#

Know the performance characteristics of each collection. Default to ArrayList for lists, HashMap for maps, HashSet for sets. Switch only when you have a specific reason.


6. Exception Handling#

Exception Hierarchy#

flowchart TD
    T[Throwable] --> E[Exception]
    T --> Er[Error]
    E --> RE[RuntimeException]
    E --> CE["Checked Exceptions"]
    
    RE --> NPE[NullPointerException]
    RE --> IAE[IllegalArgumentException]
    RE --> ISE[IllegalStateException]
    RE --> CCE[ClassCastException]
    
    CE --> IOE[IOException]
    CE --> SQLE[SQLException]
    
    Er --> OOM[OutOfMemoryError]
    Er --> SOE[StackOverflowError]

Checked vs Unchecked#

TypeMust Catch/Declare?ExamplesUse For
CheckedYesIOException, SQLExceptionRecoverable conditions
Unchecked (Runtime)NoNullPointerException, IllegalArgumentExceptionProgramming errors
ErrorNo (don’t catch)OutOfMemoryErrorJVM failures

Try-with-Resources#

// Automatically closes resources (implements AutoCloseable)
try (var reader = new BufferedReader(new FileReader("data.txt"));
     var writer = new BufferedWriter(new FileWriter("out.txt"))) {
    
    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line.toUpperCase());
        writer.newLine();
    }
} catch (IOException e) {
    logger.error("File processing failed", e);
}
// reader and writer are closed here, even if exception occurred

Key Takeaway#

Use checked exceptions for recoverable conditions the caller should handle. Use unchecked exceptions for programming errors (precondition violations). Always use try-with-resources for AutoCloseable objects.


7. Concurrency#

Thread Lifecycle#

stateDiagram-v2
    [*] --> New: Thread created
    New --> Runnable: start()
    Runnable --> Running: Scheduler picks
    Running --> Runnable: yield/preempt
    Running --> Blocked: synchronized (waiting for lock)
    Running --> Waiting: wait()/join()/park()
    Running --> TimedWaiting: sleep()/wait(timeout)
    Blocked --> Runnable: Lock acquired
    Waiting --> Runnable: notify()/unpark()
    TimedWaiting --> Runnable: Timeout/notify
    Running --> Terminated: run() completes

Creating Threads#

// Modern approach: ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(4);

Future<String> future = executor.submit(() -> {
    // runs in thread pool
    return fetchData();
});

String result = future.get(); // blocks until complete
executor.shutdown();

Synchronization#

public class Counter {
    private int count = 0;
    
    // synchronized method — only one thread at a time
    public synchronized void increment() {
        count++;
    }
    
    // Or use explicit locks for more control
    private final ReentrantLock lock = new ReentrantLock();
    
    public void incrementWithLock() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

CompletableFuture (Java 8+)#

CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> fetchUser(userId))          // async
    .thenApply(user -> user.getEmail())            // transform
    .thenCompose(email -> sendEmail(email))        // chain async
    .exceptionally(ex -> {
        logger.error("Failed", ex);
        return "fallback";
    });

Virtual Threads (Java 21+)#

// Lightweight threads — millions possible
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = urls.stream()
        .map(url -> executor.submit(() -> fetch(url)))
        .toList();
    
    for (var future : futures) {
        System.out.println(future.get());
    }
}

Virtual threads are cheap (no OS thread per virtual thread), making the “thread per request” model viable again without the overhead.

Key Takeaway#

Java’s concurrency model evolved from raw threads → ExecutorService → CompletableFuture → Virtual Threads. Use virtual threads for I/O-bound work (Java 21+), thread pools for CPU-bound work, and CompletableFuture for async composition.


8. Streams and Functional Programming#

Lambda Expressions#

// Before lambdas
Comparator<String> comp = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
};

// With lambdas
Comparator<String> comp = (a, b) -> a.length() - b.length();

// Method reference
Comparator<String> comp = Comparator.comparingInt(String::length);

Stream API#

List<String> result = employees.stream()
    .filter(e -> e.getSalary() > 50000)       // filter
    .sorted(Comparator.comparing(Employee::getName)) // sort
    .map(Employee::getName)                    // transform
    .distinct()                                // deduplicate
    .limit(10)                                 // take first 10
    .collect(Collectors.toList());             // terminal operation

// Reduction
int totalSalary = employees.stream()
    .mapToInt(Employee::getSalary)
    .sum();

// Grouping
Map<Department, List<Employee>> byDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

Stream Characteristics#

  • Lazy — intermediate operations don’t execute until a terminal operation is called
  • Non-reusable — a stream can only be consumed once
  • Optionally parallel — .parallelStream() for multi-threaded processing

Optional#

Optional<User> user = findUser(id);

// Safe access
String name = user
    .map(User::getName)
    .orElse("Unknown");

// Throw if absent
User u = user.orElseThrow(() -> new NotFoundException("User " + id));

// Never do: user.get() without checking isPresent()

Key Takeaway#

Streams bring functional programming to Java — declarative data transformation pipelines. Use them for collection processing; avoid for simple iterations where a for-loop is clearer.


9. Memory Management and Garbage Collection#

JVM Memory Model#

flowchart TD
    subgraph JVM Memory
        subgraph Heap
            YG["Young Generation"]
            OG["Old Generation"]
        end
        subgraph Non-Heap
            MS["Metaspace (class metadata)"]
            CS["Code Cache (JIT compiled)"]
        end
        ST["Thread Stacks (per thread)"]
    end
    
    YG --> |"Objects survive GC"| OG

Garbage Collection Basics#

  1. Mark — identify all reachable objects from GC roots (stack variables, static fields)
  2. Sweep — reclaim memory from unreachable objects
  3. Compact — defragment heap (optional, depends on collector)

GC Algorithms#

CollectorUse CaseCharacteristics
G1 (default)General purposeLow pause, good throughput
ZGCLow latencySub-millisecond pauses, large heaps
ShenandoahLow latencyConcurrent compaction
Parallel GCThroughputLonger pauses, maximum throughput

Common Memory Issues#

  • Memory leak — objects referenced but never used (e.g., growing collections, unclosed resources)
  • OutOfMemoryError — heap exhausted
  • GC thrashing — too much time in GC, too little in application

Key Takeaway#

You don’t manage memory manually in Java, but you must understand GC to diagnose performance issues. Avoid creating unnecessary objects in hot paths, close resources, and profile with tools like VisualVM or JFR.


10. Build Tools#

Maven#

<!-- pom.xml -->
<project>
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.2.0</version>
        </dependency>
    </dependencies>
</project>
mvn clean compile    # compile
mvn test             # run tests
mvn package          # build JAR/WAR
mvn install          # install to local repo

Gradle#

// build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
./gradlew build      # compile + test + package
./gradlew test       # run tests
./gradlew bootRun    # run Spring Boot app

Maven vs Gradle#

AspectMavenGradle
ConfigXML (verbose)Groovy/Kotlin DSL
SpeedSlowerIncremental + cache
FlexibilityConvention-heavyHighly customizable
EcosystemLarger plugin baseGrowing

Key Takeaway#

Maven is the safe default for enterprise Java. Gradle is faster and more flexible. Both solve the same problem: dependency management, compilation, testing, and packaging.


11. Testing#

JUnit 5#

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    
    private Calculator calc;
    
    @BeforeEach
    void setUp() {
        calc = new Calculator();
    }
    
    @Test
    @DisplayName("Addition of two positive numbers")
    void testAdd() {
        assertEquals(5, calc.add(2, 3));
    }
    
    @Test
    void testDivideByZero() {
        assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
    }
    
    @ParameterizedTest
    @CsvSource({"1,1,2", "0,0,0", "-1,1,0"})
    void testAddParameterized(int a, int b, int expected) {
        assertEquals(expected, calc.add(a, b));
    }
}

Mockito#

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository repository;
    
    @InjectMocks
    private UserService service;
    
    @Test
    void findUser_returnsUser_whenExists() {
        var user = new User(1L, "Alice");
        when(repository.findById(1L)).thenReturn(Optional.of(user));
        
        var result = service.findUser(1L);
        
        assertEquals("Alice", result.getName());
        verify(repository).findById(1L);
    }
}

Key Takeaway#

JUnit 5 + Mockito is the standard Java testing stack. Test behavior, not implementation. Mock external dependencies (databases, APIs), not internal collaborators.


12. Enterprise Patterns: Spring Basics#

Dependency Injection#

// Interface
public interface NotificationService {
    void send(String message, String recipient);
}

// Implementation
@Service
public class EmailNotificationService implements NotificationService {
    @Override
    public void send(String message, String recipient) {
        // send email
    }
}

// Consumer — Spring injects the implementation
@RestController
public class OrderController {
    private final NotificationService notifications;
    
    // Constructor injection (preferred)
    public OrderController(NotificationService notifications) {
        this.notifications = notifications;
    }
    
    @PostMapping("/orders")
    public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
        Order order = processOrder(request);
        notifications.send("Order confirmed", order.getEmail());
        return ResponseEntity.ok(order);
    }
}

Spring Boot Application#

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
# application.yml
server:
  port: 8080
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: ${DB_USER}
    password: ${DB_PASSWORD}

Key Takeaway#

Spring Boot eliminates boilerplate for enterprise Java. Dependency injection decouples components, making code testable and modular. Convention over configuration — Spring provides sensible defaults.


Summary#

TopicCore Concept
JVMBytecode + JIT = portability + performance
TypesStatic, strong, primitive vs reference
OOPInterfaces for contracts, classes for implementation
GenericsCompile-time safety, erased at runtime
CollectionsRight structure for right access pattern
ExceptionsChecked for recovery, unchecked for bugs
ConcurrencyVirtual threads (modern) or CompletableFuture
StreamsDeclarative collection processing
MemoryGC handles it, but understand the model
BuildMaven (safe) or Gradle (fast)
TestingJUnit 5 + Mockito
SpringDI + convention over configuration

Java is verbose by design — it optimizes for readability and maintainability in large teams over brevity. Modern Java (17+) is significantly less verbose than legacy Java, with records, sealed classes, pattern matching, and virtual threads.