Home› Java› Complete Guide
Complete Guide

Complete Java Tutorial

Java is one of the most in-demand programming languages in the world — and for good reason. It powers Android apps, enterprise backends, and systems that handle millions of requests a day. This guide covers all 257 Java topics on TheCodeForge, from your first Hello World to advanced concurrency and Spring Boot.

Learning Roadmap
Beginner → Learn Java syntax, OOP basics and how the JVM works
Intermediate → Explore collections, streams, exception handling and design patterns
Advanced → Master concurrency, JVM internals, Spring Boot and microservices
257
Topics
73
Beginner
125
Intermediate
59
Advanced
Jump to section
Java Basics (13)Control Flow (9)Arrays (8)OOP Concepts (16)Strings (15)Exception Handling (20)Collections (21)Java 8+ Features (16)Multithreading (10)Java I/O (8)Advanced Java (29)Spring Boot (21)Hibernate & JPA (7)Build Tools (5)Concurrency (6)Spring Security (4)Spring Cloud (9)Messaging (7)Microservices Patterns (3)Deployment (3)Performance (1)Production (3)Interview (4)Basics (1)Android (5)Exceptions (7)Spring (5)JVM (1)

Java's reputation lags its reality by about a decade. The language people describe — verbose, ceremonious, stuck on version 8 — is not the language you would write today. Records removed most DTO boilerplate, sealed interfaces and pattern matching made exhaustive domain modelling pleasant, text blocks fixed embedded SQL and JSON, and virtual threads changed the economics of blocking I/O so thoroughly that a thread per request is viable again.

What has not changed is where Java engineers actually lose time, and it is a consistent list: collections behaviour that depends on equals and hashCode being right, concurrency where visibility and atomicity get conflated, memory problems that are not all heap problems, and Spring proxies behaving in ways the annotation does not advertise.

Collections: the contract that quietly loses your data

The most consequential rule in everyday Java is that equals and hashCode must agree. Override one without the other and hash-based collections stop working — not with an exception, but by silently failing to find entries you just inserted, because lookup goes to a different bucket.

The second-order version of the same rule: never mutate a field that participates in hashCode while the object is a key in a HashMap or an element of a HashSet. The object is now in the wrong bucket and is effectively invisible — including to remove, which is how collections develop entries that cannot be deleted.

NeedUseWhy not the obvious choice
Lookup by keyHashMapTreeMap costs O(log n) and you rarely need the ordering
Keys in sorted order or range queriesTreeMapHashMap gives no ordering guarantee at all
Insertion order preservedLinkedHashMapIt also gives you an LRU cache for free via removeEldestEntry
Frequent insert or remove at both endsArrayDequeLinkedList is almost always slower in practice — cache locality beats the complexity table
Concurrent mapConcurrentHashMapCollections.synchronizedMap locks the whole map and compound operations are still not atomic
A small fixed setEnumSet / EnumMapBitset-backed and dramatically faster than a HashSet of enums

Concurrency: visibility and atomicity are different problems

volatile guarantees that a write becomes visible to other threads and restricts reordering around it. It does not make an operation atomic, which is why count++ on a volatile field is still a race — it is a read, an add and a write, and two threads can interleave between them.

Getting this distinction wrong produces the worst category of bug: one that passes every test, appears under load weeks later, and cannot be reproduced on demand. The practical guidance is to reach for the concurrency utilities rather than for primitives — AtomicInteger, ConcurrentHashMap, ExecutorService — because they encode the correct memory semantics for you.

java
// Visible, but not atomic - still a race under contention
private volatile int count;
public void inc() { count++; }

// Atomic and visible
private final AtomicInteger count = new AtomicInteger();
public void inc() { count.incrementAndGet(); }

// Compound operations on a concurrent map are not atomic by default
if (!map.containsKey(k)) map.put(k, compute(k));   // two threads can both enter
map.computeIfAbsent(k, this::compute);             // atomic, computed once

// Virtual threads: blocking is cheap again (Java 21+)
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
    ids.forEach(id -> exec.submit(() -> blockingFetch(id)));
}   // thousands of concurrent blocking calls, a handful of OS threads
In practiceVirtual threads change the default advice of the last decade. Reactive frameworks were largely adopted to avoid blocking a limited thread pool; with virtual threads, straightforward blocking code scales to very high concurrency and is far easier to read and debug.

Memory: not every OutOfMemoryError is a heap problem

The message after OutOfMemoryError names the region, and the region determines the fix. Java heap space is the familiar one — too much retained, or a leak. Metaspace is class metadata, usually a classloader leak in an application server or repeated dynamic proxy generation. Unable to create native thread is an OS limit or thread leak, not heap at all. Direct buffer memory is off-heap NIO buffers, which -Xmx does not govern.

In containers the most common version is none of these: the JVM sized its heap against the container limit with no headroom, and the kernel's OOM killer terminated the process because total memory — heap plus metaspace plus thread stacks plus native — exceeded the cgroup limit. Nothing appears in the application log, which is the giveaway.

bash
# Container-aware sizing: leave headroom for non-heap memory
-XX:MaxRAMPercentage=70.0

# Capture evidence at the moment of failure, not after a restart
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps

# What is actually retained? Histogram of live objects.
jcmd <pid> GC.class_histogram | head -30
jcmd <pid> VM.native_memory summary     # needs -XX:NativeMemoryTracking=summary

Spring: proxies explain most of the surprises

Spring implements @Transactional, @Async, @Cacheable and the rest by wrapping your bean in a proxy. Everything surprising about those annotations follows from that one implementation detail.

The most important consequence: calling an annotated method from another method of the same class bypasses the proxy entirely, so the annotation does nothing. No warning, no error — the transaction simply does not start. It is the single most common real Spring bug, and the fix is to move the annotated method to another bean or to inject a self-reference deliberately.

The second consequence: by default only unchecked exceptions trigger a rollback. A checked exception escaping a @Transactional method commits the transaction unless you declare rollbackFor.

Frequently Asked Questions

Which Java version should I target?
Java 21 for new work — it is a long-term-support release with virtual threads, pattern matching for switch, records and sealed types all finalised. Java 17 remains a reasonable floor for compatibility. Staying on 8 now costs you real ergonomics and performance, and increasingly reads as a maintenance signal.
Do I still need to learn reactive programming?
Less urgently than two years ago. Virtual threads solve the problem reactive frameworks were most often adopted for — scaling blocking I/O — with far simpler code and readable stack traces. Reactive still earns its place for genuine stream processing with back-pressure, and for existing codebases built around it.
What is the difference between checked and unchecked exceptions?
Checked exceptions must be declared or handled and are intended for conditions a caller can recover from. Unchecked exceptions signal programming errors. In practice checked exceptions are used less than the original design intended, and many modern APIs avoid them — having a considered opinion on this is itself a common interview topic.
Why is my Spring @Transactional method not rolling back?
Three usual causes. A checked exception was thrown and the default only rolls back on unchecked ones — declare rollbackFor. The method was called from within the same bean, so the proxy was bypassed. Or the exception was caught inside the method and never propagated, so Spring never saw a failure at all.
Are streams slower than loops?
Slightly, for simple operations on small collections, because of the pipeline setup. The difference is rarely measurable in application code and is dwarfed by I/O. Use streams where they express intent more clearly; use loops where the logic is imperative. Parallel streams need real care — they use the common fork-join pool and can hurt more than they help.
How much of the JVM do I need to understand?
Enough to read a heap dump, distinguish the memory regions above, and explain what a stop-the-world pause is. That covers almost every production incident you will meet. Collector tuning flags are a specialist topic, and 'measure before tuning' is the correct instinct at any level.

Java Basics

Control Flow

Arrays

OOP Concepts

Strings

Exception Handling

Collections

Java 8+ Features

Multithreading

Java I/O

Advanced Java

Spring Boot

Hibernate & JPA

Build Tools

Concurrency

Spring Security

Spring Cloud

Messaging

Microservices Patterns

Deployment

Performance

Production

Interview

Basics

Android

Exceptions

Spring

JVM

Interview prep
→Java Interview Questions
Also Explore
DSA 197 tutorials → System Design 145 tutorials → C# / .NET 60 tutorials → Interview 77 tutorials → Mobile 20 tutorials → Testing 12 tutorials →
Start from the beginning

Every tutorial starts with a plain-English analogy — then real code, then interview questions.

Browse Java Tutorials →