Before You Start
Thirty years old and still the backbone of enterprise software, Android, and half the world's backend systems. These two lessons explain why, and get a JDK installed.
What is Java?
James Gosling's 1995 promise — write once, run anywhere — still the deal today
Java was built at Sun Microsystems under James Gosling with one central bet: compile to bytecode that runs unmodified on any machine with a JVM. Three decades and one Oracle acquisition later, that bet is why Java still runs most of the world's banking systems, all of Android, and a huge share of backend services you use daily without knowing it.
Java's origin at Sun Microsystems, the write-once-run-anywhere bytecode model, the six-month release cadence since Java 10, and where Java 25 LTS sits in that timeline.
Setup and JDK
JDK, JRE, JVM — three acronyms that mean genuinely different things
The JDK is what you install to write Java; the JRE is a runtime-only subset most people no longer install separately; the JVM is the actual virtual machine both of them ship. Getting this straight up front avoids a surprising amount of confused troubleshooting later.
Installing a JDK (25 LTS), JAVA_HOME, javac and java from the command line, and IDE setup.
Language Fundamentals
The syntax you will use in nearly every file: variables, control flow, methods, strings, and the standard library's biggest wins. Five lessons before objects properly begin.
Variables and Types
8 primitive types, String, autoboxing, and var inference
Java is statically and strongly typed, with exactly 8 primitives and everything else an object. Autoboxing quietly converts between the two, which is convenient until it silently costs you performance or triggers a subtle bug — both worth understanding before you rely on either.
The 8 primitive types, String basics, autoboxing/unboxing, type casting, and var's local type inference.
Control Flow
if/for/while are familiar; the switch expression is the modern rewrite
Java's basic control flow is standard C-family syntax you likely already know. What is worth real attention is the modern switch *expression* — arrow-form, no fall-through, and (with pattern matching) exhaustive — which is a genuinely different construct from the old switch statement, not just new syntax.
if/else, for/while/do-while, enhanced for, the classic switch statement, and the modern switch expression.
Methods
No standalone functions — every method lives inside a class
Unlike Python, JavaScript, or Go, Java has no free-floating functions: every method belongs to a class, static or instance. Overloading — the same name with different parameter signatures — is resolved at compile time based on the declared types you pass, not the runtime type.
Method syntax, parameters and return types, static vs instance methods, overloading, and varargs.
String Handling
Strings are immutable and pooled — why == on strings is a classic bug
Java interns string literals into a shared pool, so two literals with the same text can be the same object — which is exactly why comparing strings with <code>==</code> sometimes appears to work and sometimes doesn't. <code>.equals()</code> is the only correct comparison, always.
String immutability, the string pool, .equals() vs ==, StringBuilder, text blocks, and common String methods.
Standard Library
Three decades of APIs — including real replacements for old, flawed ones
java.util, java.time, java.util.Optional and friends represent thirty years of accretion, some of it excellent and some of it superseded by its own successor (java.time replacing the notoriously broken java.util.Date, for instance). Knowing which era an API is from tells you whether to reach for it.
java.time (LocalDate/LocalDateTime/Duration), java.util.Objects, Math, formatting, and legacy APIs superseded by better ones.
Object-Oriented Java — Java's Defining Discipline
Java is fundamentally an object-oriented language, and this is the stage where that identity is built up piece by piece — then the last lesson ties every piece together into one realistic class hierarchy. Eight lessons, the heart of the course.
Classes and Objects
A class is a blueprint; new allocates an actual object on the heap
Every Java program is built from classes and the objects instantiated from them. This lesson is the foundation the rest of the stage builds on: fields, constructors, the `this` reference, and the distinction between a class (compile-time) and an object (runtime, on the heap).
Class declarations, fields, constructors, the this keyword, object creation, and access modifiers.
Inheritance and Polymorphism
extends creates is-a; the runtime object decides which override runs
<code>extends</code> establishes an is-a relationship between classes, and polymorphism means the *actual* object at runtime — not the declared reference type — decides which overridden method executes. This dynamic dispatch is what makes interfaces and abstract classes actually useful.
extends, method overriding vs overloading, super, dynamic dispatch, and the Object class's role as universal superclass.
Interfaces and Abstract Classes
Implement many interfaces, extend only one abstract class
A class can implement any number of interfaces but extend only one abstract (or concrete) class — Java's answer to the diamond problem, and the actual reason interfaces exist as a separate construct from abstract classes rather than a redundant one.
Interface declarations, default and static interface methods, abstract classes, and when to choose one over the other.
Enums
A Java enum is a real class — fields, constructors, methods, per-constant bodies
Unlike C-style enums that are just named integers, a Java enum is a full class: it can carry fields, have a constructor, define methods, and even give individual constants their own method bodies. This makes enums a genuine modeling tool, not just a label.
Enum declarations, enum constructors and fields, methods on enums, per-constant bodies, and EnumMap/EnumSet.
Records
One line replaces the constructor, getters, equals, hashCode, and toString
A record is an immutable data carrier: declare the fields once and the compiler generates the constructor, accessors, <code>equals</code>, <code>hashCode</code>, and <code>toString</code> for you. It is Java's answer to the boilerplate that made simple data classes tedious for two decades.
Record syntax, compact constructors, validation in a compact constructor, and records implementing interfaces.
Sealed Classes
sealed names every permitted subclass — finalized in Java 17
A <code>sealed</code> class or interface lists exactly which classes are allowed to extend or implement it, closing off the hierarchy to anything the author didn't explicitly permit. This is what makes exhaustive pattern matching possible in the next lesson.
sealed, permits, non-sealed subclasses, and how sealed hierarchies enable exhaustive pattern matching.
Pattern Matching
instanceof used to require a separate cast — pattern matching removes it
Before pattern matching, <code>instanceof</code> told you a type but still required a separate, redundant cast to use it. Pattern matching for instanceof and switch collapses the check and the binding into one expression, and combined with sealed types, switch can be checked for exhaustiveness.
Pattern matching for instanceof, record patterns, pattern matching in switch, guards, and exhaustiveness with sealed types.
Object-Oriented Java, Tied Together
Classes, interfaces, records, sealed types and enums in one realistic hierarchy
This lesson is the synthesis for the stage: the individual pieces you just met — classes, access modifiers, abstract classes, interfaces, inheritance, polymorphism, records, sealed classes, and enums — combined into one realistic class hierarchy, including the builder pattern for constructing complex objects cleanly. Read it once each piece is familiar on its own.
Classes, access modifiers, abstract classes, interfaces, inheritance, polymorphism, records, sealed classes, the builder pattern, and enums together.
Collections, Generics & Functional Style
How Java stores and processes data at scale, and the functional-style API layered on top of it since Java 8. Four lessons.
Collections and Generics
ArrayList, HashMap, generics, wildcards, and type erasure
The Collections Framework — ArrayList, HashMap, HashSet, LinkedList, PriorityQueue, ArrayDeque — is what you reach for constantly, and generics are what make it type-safe. Type erasure is the one genuinely surprising part: generic type information does not exist at runtime, only at compile time.
ArrayList, HashMap, HashSet, LinkedList, PriorityQueue, ArrayDeque, generic classes and methods, wildcards, and type erasure.
Exception Handling
Checked vs unchecked, try-with-resources, and exception chaining
Java's checked-exception system is genuinely unusual among mainstream languages: the compiler forces you to declare or handle certain exceptions. try-with-resources then guarantees cleanup automatically for anything implementing AutoCloseable, which is what makes it the modern default over manual finally blocks.
try-catch-finally, checked vs unchecked exceptions, try-with-resources, custom exceptions, multi-catch, and exception chaining.
Streams and Lambdas
Lambdas, method references, and Collectors turn loops into pipelines
The Stream API, introduced in Java 8, lets you describe a data transformation as a declarative pipeline — filter, map, collect — instead of a hand-written loop with mutable accumulator state. Lambdas and method references are the syntax that makes passing behavior around finally lightweight in Java.
Lambda syntax, functional interfaces, method references, the Stream API, Collectors and groupingBy, and parallel streams.
Optional and Null Safety
Optional<T> makes the possibility of absence part of the type
<code>Optional<T></code> forces a caller to explicitly handle the case where a value might be absent, rather than discovering a <code>NullPointerException</code> at runtime. It is not a universal null replacement — using it correctly (return types, not fields or parameters) is the actual skill.
Optional.of/ofNullable/empty, map/filter/orElse chains, and the conventions around where Optional should and shouldn't appear.
Concurrency and I/O
Reading and writing data, and running work in parallel — including virtual threads, Java's biggest concurrency shift in a decade. Four lessons.
Virtual Threads
Millions of lightweight threads instead of thousands — finalized in Java 21
Virtual threads, the product of years of work under Project Loom, let you write simple blocking-style code that scales to millions of concurrent threads instead of the low thousands a platform thread's OS-level cost previously allowed. This is arguably the biggest Java concurrency shift since java.util.concurrent itself.
Platform threads vs virtual threads, Thread.ofVirtual(), structured concurrency, and where virtual threads change idiomatic server code.
IO and NIO
java.io still works; java.nio.file's Path/Files is what modern code uses
The original stream-based <code>java.io</code> package is still functional, but <code>java.nio.file</code> — with its <code>Path</code> and <code>Files</code> APIs — is what current code actually reaches for: simpler file operations, better error handling, and directory-walking support java.io never had.
InputStream/OutputStream/Reader/Writer basics, Path and Files, reading/writing files, and directory walking with NIO.2.
Reflection
java.lang.reflect inspects classes at runtime — slow, powerful, and everywhere
Reflection lets code inspect and manipulate classes, methods, and fields it didn't know about at compile time — the mechanism quietly powering Spring's dependency injection and JUnit's test discovery. It is genuinely slow relative to normal calls, which is why frameworks cache what they discover.
Class objects, inspecting methods/fields/constructors at runtime, invoking reflectively, and reflection's role in frameworks.
Concurrency, Tied Together
Thread, synchronized, ExecutorService, virtual threads and the Java Memory Model
This lesson is the synthesis for the stage: threads and the classic synchronization primitives, the higher-level <code>ExecutorService</code> abstraction, virtual threads from the previous lesson, <code>CompletableFuture</code> for composing async work, and the Java Memory Model that underpins why any of it is actually safe.
Thread and Runnable, synchronized and volatile, AtomicInteger, ExecutorService, CompletableFuture, and the Java Memory Model.
Tooling, Modules and Internals
What surrounds the code: build tools, testing, documentation, and what actually happens inside the JVM while your program runs. Seven lessons to close out the course.
Packages and Modules
Packages organize a JAR; modules (Java 9+) control what it exposes
Packages have organized Java code since 1.0, but the module system introduced in Java 9 goes further: a <code>module-info.java</code> file declares exactly which packages a JAR exposes to the outside world, closing off internal APIs that used to be reachable by accident.
Package declarations and imports, the module system, module-info.java, exports/requires, and encapsulation benefits.
Build Tools
Maven declares dependencies in XML; Gradle uses a real programming language
Maven and Gradle both manage the same underlying dependency repositories but take opposite philosophies: Maven's declarative XML convention-over-configuration versus Gradle's Groovy/Kotlin DSL that is genuinely programmable. Knowing both is close to a job requirement in real Java work.
pom.xml structure, Maven's build lifecycle, Gradle's build.gradle(.kts), dependency management, and choosing between them.
Testing
JUnit is the standard — now on its 6th major generation, requiring Java 17+
JUnit remains the de facto Java testing standard, and JUnit 6 (current as of late 2025) requires Java 17 as a baseline. This lesson covers writing and organizing real tests, not just the assertion syntax — fixtures, parameterized tests, and mocking with Mockito.
JUnit 6 annotations and assertions, parameterized tests, test lifecycle, and mocking with Mockito.
Javadoc
A specially formatted comment becomes real, browsable HTML documentation
The <code>javadoc</code> tool generates browsable HTML API documentation directly from specially formatted <code>/** ... */</code> comments in your source — the same mechanism behind every official Java API doc page you have ever read.
Javadoc comment syntax, @param/@return/@throws tags, generating HTML docs, and documentation conventions.
Annotations
@Override does nothing at runtime — metadata read by the compiler and tools
Annotations attach metadata to code that the compiler, build tools, or frameworks read and act on — they are not executable code themselves. <code>@Override</code> is purely a compiler safety check; custom annotations are how frameworks like Spring and JUnit discover what to wire up.
Built-in annotations (@Override, @Deprecated, @FunctionalInterface), writing custom annotations, retention policies, and how frameworks use them.
JVM Internals
The JVM interprets bytecode, then compiles hot paths to native code live
The JVM starts by interpreting bytecode and, as it observes which code paths run most often, compiles those specific hot paths to native machine code via the JIT compiler — a genuinely different execution model from an ahead-of-time compiled language, and the reason long-running Java processes often warm up.
Bytecode and class loading, the interpreter, JIT compilation and hot-path optimization, and HotSpot's tiered compilation.
Garbage Collection
Several genuinely different collectors — the default has changed more than once
Java has shipped multiple, architecturally different garbage collectors over its history, and which one is the default has changed more than once as tradeoffs between throughput, pause time, and memory overhead shifted. Picking the right one for a workload still genuinely matters in production.
Generational garbage collection concepts, G1 (the long-standing default), ZGC's low-pause design, and when to choose a non-default collector.
Concepts That Cross Languages
These pages explain ideas that are not specific to Java — they apply to every language you will ever learn. Read them once and the next language costs you far less effort. They pair well with the lessons above rather than replacing them.
Build Real Things — 10 Projects
Reading is not enough. These 10 projects — five basic, five medium — are chosen because each one exercises something specific you learned above, and because they are genuinely idiomatic Java rather than generic exercises. Each link below opens the project brief; the Java implementation walkthrough is in progress.
Scanner input parsing, loops, and java.util.Random.
Static methods, a switch expression for the conversion direction, and formatted output.
Reading files with NIO's Files API, and HashMap<String, Integer> for a frequency table.
A record for the task shape, and hand-rolled persistence with the Files API.
Enums for character-set options, and SecureRandom for genuinely unpredictable output.
java.net.http's HttpClient for real requests, and pattern matching over the parsed response.
com.sun.net.httpserver's built-in HttpServer, sealed types for typed responses, and no external framework.
Sealed interfaces for subcommands, and exhaustive switch expressions so a missing case is a compile error.
A thread-safe in-memory store with ConcurrentHashMap, and virtual threads for the HTTP server.
Streams for parsing and aggregating semi-structured lines, and generics for a reusable top-N counter.
Practice & Experimentation
Ongoing, not a final step. Use these throughout the course — try every snippet you read, and run anything you are unsure about rather than assuming.
Run Java code without installing anything, via the official Java Playground.
Copy-ready idiomatic patterns to keep beside you while you build.
All 30 topic pages in one index, for looking things up later.