Learn Java by watching it run.
From Hello World to virtual threads and the JVM memory model — every concept comes with a 3D visualization you can rotate.
What is Java & the JVM?
Write once, run anywhere — how bytecode and the JVM make it possible.
Java is a statically typed, object-oriented language that compiles to platform-neutral bytecode. The Java Virtual Machine (JVM) loads that bytecode and executes it on any operating system.
The lifecycle: your .java source is compiled by javac into a .class file of bytecode. The JVM's class loader pulls those classes into memory, the bytecode verifier checks them, and the execution engine (interpreter + JIT compiler) runs them — turning hot code paths into optimized native machine code at runtime.
Hello.java \u2192 javac \u2192 Hello.class \u2192 JVM \u2192 native CPU- \u2713 Java source compiles to bytecode, not to a specific CPU.
- \u2713 The JVM is the portable runtime; different JVMs exist for different OSes.
- \u2713 The JIT compiler turns hot methods into fast native code while the program runs.
Hello, World
Your first program — and every piece of the required ceremony explained.
Every Java program starts inside a class, and execution begins at a method named main with a very specific signature.
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}- \u25b8public — visible to the JVM launcher outside this class.
- \u25b8class Hello — the container; filename must be Hello.java.
- \u25b8static — no instance is needed to call main.
- \u25b8void — main returns nothing.
- \u25b8String[] args — command-line arguments.
- \u2713 Filename must match the public class name.
- \u2713 main has a fixed signature: public static void main(String[]).
- \u2713 Statements end with a semicolon; blocks use braces.
Variables & Primitive Types
Eight primitive types, one reference world, and why the difference matters.
Java has 8 primitive types stored directly on the call stack, and reference types whose values live on the heap while a reference to them sits on the stack.
int age = 27; // 32-bit integer
long views = 9_000_000L; // 64-bit integer
double pi = 3.14159; // 64-bit float
float ratio = 0.5f; // 32-bit float
boolean ready = true;
char grade = 'A';
byte flag = 0x1F;
short port = 8080;
String name = "Ada"; // reference type- \u2713 8 primitives: byte, short, int, long, float, double, boolean, char.
- \u2713 Primitives store the value; references store an address to a heap object.
- \u2713 Use _ as a digit separator for readable large numbers.
Operators & Expressions
Arithmetic, comparison, logical and bitwise — and Java's promotion rules.
int sum = 5 + 3; // 8
int div = 7 / 2; // 3 (integer division!)
double d = 7 / 2.0; // 3.5
int mod = 10 % 3; // 1
boolean eq = (a == b); // reference equality for objects
boolean ok = ready && !done;
int mask = flags & 0xFF; // bitwise AND- \u2713 Integer / integer = integer. Promote one side to get a double.
- \u2713 && and || short-circuit; & and | do not (and also work bitwise).
- \u2713 For objects, == checks identity, .equals() checks value.
Strings
Immutable text, the String pool, and how to concatenate efficiently.
String objects are immutable — every 'modification' creates a new object. Literals live in the String pool so identical literals share memory.
String a = "Java";
String b = a + " 21"; // new object "Java 21"
String c = String.format("Hi %s!", a);
// Efficient repeated concatenation:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) sb.append(i);
String result = sb.toString();
// Text blocks (Java 15+):
String json = """
{ "name": "Ada" }
""";- \u2713 Strings are immutable — reuse literals from the pool for equality.
- \u2713 Use StringBuilder inside loops instead of += concatenation.
- \u2713 Text blocks (""") preserve multi-line formatting.
Reading Input
Scanner for the console, and a peek at System.in.
import java.util.Scanner;
public class Ask {
public static void main(String[] args) {
try (Scanner in = new Scanner(System.in)) {
System.out.print("Your name: ");
String name = in.nextLine();
System.out.println("Hello, " + name);
}
}
}- \u2713 Scanner wraps System.in for line/token/number reads.
- \u2713 Close I/O resources — try-with-resources handles it for you.
if / else
Branching with boolean expressions.
if (score >= 90) grade = 'A';
else if (score >= 80) grade = 'B';
else if (score >= 70) grade = 'C';
else grade = 'F';
String label = (age >= 18) ? "adult" : "minor"; // ternary- \u2713 Conditions must be boolean.
- \u2713 The ternary ?: is a compact if/else expression.
switch & Pattern Switch
Classic switch statements and modern arrow-style expressions.
// Modern switch expression (Java 14+)
String day = switch (n) {
case 1, 7 -> "weekend";
case 2, 3, 4, 5, 6 -> "weekday";
default -> "unknown";
};
// Pattern switch (Java 21+)
String describe = switch (obj) {
case Integer i when i > 0 -> "positive int " + i;
case String s -> "string of length " + s.length();
case null -> "nothing";
default -> "something else";
};- \u2713 Arrow switch expressions return values and have no fall-through.
- \u2713 Pattern switch dispatches on the runtime type and can bind variables.
Loops
for, while, do-while, and the for-each loop.
for (int i = 0; i < 10; i++) { ... }
while (queue.hasNext()) { ... }
do { retry(); } while (!ok);
for (String name : names) { // for-each
System.out.println(name);
}- \u2713 for-each is the cleanest iteration over any Iterable.
- \u2713 Prefer streams over manual loops for data transformations.
Methods
Parameters, return types, and overloading.
public static int max(int a, int b) {
return a > b ? a : b;
}
// Overloading — same name, different parameters
public static double max(double a, double b) { ... }
// Varargs
public static int sum(int... nums) {
int total = 0;
for (int n : nums) total += n;
return total;
}- \u2713 Overloading picks the best-matching signature at compile time.
- \u2713 Varargs (int... nums) is really an array under the hood.
Recursion
A method that calls itself — and the stack frames it creates.
static long factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}- \u2713 Every recursive call needs a base case.
- \u2713 Watch the stack grow in the 3D visualization for factorial(5).
Classes & Objects
State + behavior packaged together.
public class Point {
double x, y; // fields (state)
double distance(Point other) { // method (behavior)
double dx = x - other.x, dy = y - other.y;
return Math.sqrt(dx*dx + dy*dy);
}
}
Point p = new Point(); // 'new' allocates on the heap
p.x = 3; p.y = 4;- \u2713 A class is a blueprint; an object is one instance in memory.
- \u2713 'new' allocates a heap object and returns a reference.
Constructors
How new objects are initialized.
public class Point {
final double x, y;
public Point(double x, double y) { // constructor
this.x = x;
this.y = y;
}
public Point() { this(0, 0); } // chained constructor
}- \u2713 Constructors have no return type and share the class name.
- \u2713 this(...) chains to another constructor in the same class.
this, static & final
Instance vs class members, and immutability.
public class Counter {
private static int total = 0; // shared across all instances
private final int id; // set once per instance
private int hits;
public Counter() {
this.id = ++total;
}
public static int totalCreated() { return total; }
}- \u25b8static members belong to the class, not any instance.
- \u25b8final variables can be assigned only once.
- \u25b8this refers to the current instance.
- \u2713 static = one copy per class.
- \u2713 final = write-once.
Inheritance
Extending a class to reuse and specialize.
class Animal {
String name;
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println(name + ": woof"); }
}
Animal a = new Dog(); // upcast
a.speak(); // dynamic dispatch \u2192 "woof"- \u2713 extends creates an 'is-a' relationship.
- \u2713 @Override lets the compiler check you're really overriding.
- \u2713 Method calls dispatch on the runtime type.
Polymorphism
One interface, many runtime behaviors.
Animal[] zoo = { new Dog(), new Cat(), new Cow() };
for (Animal a : zoo) a.speak(); // each speaks its own way- \u2713 Program to the parent type; the JVM picks the right override.
- \u2713 Enables 'open for extension, closed for modification' code.
Abstract Classes
Partial blueprints that can't be instantiated.
abstract class Shape {
abstract double area(); // subclass must implement
void describe() {
System.out.println("area = " + area());
}
}
class Circle extends Shape {
double r;
Circle(double r) { this.r = r; }
double area() { return Math.PI * r * r; }
}- \u2713 Use abstract when subclasses share code AND state.
- \u2713 You cannot new an abstract class directly.
Interfaces, Default & Sealed
Contracts with optional bodies, and restricted hierarchies.
interface Drawable {
void draw();
default void redraw() { // Java 8+ default method
clear(); draw();
}
private void clear() { /* ... */ }
}
// Sealed (Java 17+) — only listed types may implement.
sealed interface Shape permits Circle, Square {}
final class Circle implements Shape {}
final class Square implements Shape {}- \u2713 Interfaces are pure contracts; a class can implement many.
- \u2713 default methods add behavior without breaking implementers.
- \u2713 sealed types make pattern matching exhaustive.
Arrays
Fixed-size, contiguous, index-from-zero.
int[] nums = new int[5]; // {0,0,0,0,0}
int[] primes = {2, 3, 5, 7, 11};
System.out.println(primes[2]); // 5
System.out.println(primes.length);
int[][] grid = new int[3][3]; // multidimensional- \u2713 Length is fixed at creation.
- \u2713 Multidim arrays are arrays of arrays.
ArrayList & LinkedList
The two workhorse Lists — and when to use each.
import java.util.*;
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Grace");
names.remove(0);
List<Integer> queue = new LinkedList<>();
queue.add(1); queue.add(2);- \u25b8ArrayList: fast random access, slow inserts in the middle.
- \u25b8LinkedList: fast head/tail inserts, slow random access.
- \u2713 Prefer ArrayList by default.
- \u2713 Use List<T> as the declared type; swap implementations freely.
HashMap & HashSet
Key \u2192 value in O(1) average — using hashCode() and equals().
Map<String, Integer> ages = new HashMap<>();
ages.put("Ada", 36);
ages.put("Grace", 85);
int a = ages.getOrDefault("Ada", 0);
for (var entry : ages.entrySet()) {
System.out.println(entry.getKey() + " \u2192 " + entry.getValue());
}
Set<String> unique = new HashSet<>(List.of("a","b","a")); // {a,b}- \u2713 Custom keys must implement equals() AND hashCode() consistently.
- \u2713 HashMap is unordered; use LinkedHashMap for insertion order.
Generics
Type parameters for reusable, type-safe containers.
class Box<T> {
private T value;
public void set(T v) { value = v; }
public T get() { return value; }
}
Box<Integer> b = new Box<>();
b.set(42);
// Bounded type parameter
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
List<? extends Number> readOnly; // upper-bounded wildcard- \u2713 Generics are erased at runtime — type checks happen at compile time.
- \u2713 Use bounds (T extends X) to constrain what T can be.
Iterators & Iterable
The contract behind for-each.
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String n = it.next();
if (n.isEmpty()) it.remove(); // safe removal
}- \u2713 for-each unrolls to iterator().hasNext()/next().
- \u2713 Modifying a collection during for-each throws ConcurrentModificationException.
Comparable & Comparator
Natural order vs on-the-fly order.
class User implements Comparable<User> {
String name; int age;
public int compareTo(User o) { return this.age - o.age; }
}
users.sort(Comparator.comparing((User u) -> u.name)
.thenComparingInt(u -> u.age).reversed());- \u2713 Comparable defines the class's default order.
- \u2713 Comparator lets you sort by any strategy at the call site.
Exceptions & try-with-resources
Handling failure without cluttering happy paths.
try (var reader = Files.newBufferedReader(path)) {
return reader.readLine();
} catch (NoSuchFileException e) {
return "";
} catch (IOException e) {
throw new RuntimeException("read failed", e);
}
// Custom exception
class NotFoundException extends RuntimeException {
public NotFoundException(String msg) { super(msg); }
}- \u25b8Checked exceptions (extends Exception) must be caught or declared.
- \u25b8Unchecked (extends RuntimeException) don't require a throws clause.
- \u25b8try-with-resources closes AutoCloseable resources in reverse order.
- \u2713 Catch narrow types first, wide types last.
- \u2713 Always close I/O — let the compiler do it via try-with-resources.
Enums
A fixed, type-safe set of named constants — that can also have behavior.
enum Direction {
NORTH( 0, -1), EAST(1, 0), SOUTH(0, 1), WEST(-1, 0);
final int dx, dy;
Direction(int dx, int dy) { this.dx = dx; this.dy = dy; }
Direction opposite() {
return values()[(ordinal() + 2) % 4];
}
}- \u2713 Enums can carry fields and methods.
- \u2713 Prefer enums over public static final int for named sets.
Records
Immutable data carriers with equals/hashCode/toString for free (Java 14+).
record Point(double x, double y) {
// compact constructor — validation
public Point {
if (Double.isNaN(x) || Double.isNaN(y))
throw new IllegalArgumentException();
}
double distance(Point o) {
return Math.hypot(x - o.x, y - o.y);
}
}- \u2713 Records are implicitly final and immutable.
- \u2713 Fields become accessor methods: p.x() not p.x.
Lambdas & Functional Interfaces
Passing behavior as data.
Runnable r = () -> System.out.println("hi");
Function<String,Integer> len = s -> s.length();
BiFunction<Integer,Integer,Integer> add = (a, b) -> a + b;
Predicate<String> nonEmpty = s -> !s.isEmpty();
// Method reference
list.forEach(System.out::println);- \u2713 A lambda targets a functional interface — one abstract method.
- \u2713 Method references (Class::method) are compact lambda substitutes.
Streams API
Declarative pipelines over collections.
import java.util.stream.*;
int sum = List.of(1,2,3,4,5,6).stream()
.filter(n -> n % 2 == 0) // 2, 4, 6
.mapToInt(Integer::intValue)
.sum(); // 12
Map<Boolean,List<Integer>> parts = IntStream.rangeClosed(1, 10)
.boxed()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));- \u2713 Stages: source \u2192 intermediate ops \u2192 terminal op.
- \u2713 Streams are single-use; create a new one each time.
Optional
A container that may or may not hold a value — a nicer alternative to null.
Optional<User> found = repo.findById(42);
String name = found.map(u -> u.name).orElse("unknown");
found.ifPresent(u -> System.out.println(u.email));
// Chain
int emailLen = repo.findById(42)
.map(u -> u.email)
.map(String::length)
.orElse(0);- \u2713 Signals absence in the type system.
- \u2713 Use map/flatMap/orElse — avoid isPresent()+get().
File I/O (NIO.2)
Modern paths, files and streams.
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
Path p = Path.of("notes.txt");
Files.writeString(p, "hello", StandardCharsets.UTF_8);
String text = Files.readString(p);
List<String> lines = Files.readAllLines(p);
try (var stream = Files.lines(p)) {
stream.filter(l -> !l.isBlank()).forEach(System.out::println);
}- \u2713 Prefer java.nio.file over the old java.io.File API.
- \u2713 Files.lines() returns a lazy stream — close it.
Packages & Modules
Namespaces for classes, and stronger boundaries between JAR-sized units.
// File: com/acme/util/Math.java
package com.acme.util;
public class Math { ... }
// module-info.java (Java 9+)
module com.acme.util {
exports com.acme.util;
requires java.base;
}- \u2713 Packages group related classes and are the unit of visibility.
- \u2713 Modules add explicit exports/requires — stronger encapsulation than JARs.
Threads & Runnable
Multiple flows of execution inside one JVM.
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++)
System.out.println("tick " + i);
});
t.start(); // runs concurrently
t.join(); // wait for it- \u2713 start() spawns; run() would be sequential.
- \u2713 Multiple threads share heap objects — that's where the trouble starts.
synchronized & Locks
Coordinating access so shared state stays consistent.
class Counter {
private int n;
public synchronized void inc() { n++; }
public synchronized int get() { return n; }
}
// Explicit lock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try { /* critical section */ } finally { lock.unlock(); }- \u2713 synchronized guarantees mutual exclusion and visibility.
- \u2713 Prefer AtomicInteger / ConcurrentHashMap for simple cases.
ExecutorService
Thread pools — the right way to run many tasks.
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> heavyWork());
Integer result = f.get(); // blocks until done
pool.shutdown();- \u2713 Don't hand-roll threads — submit tasks to a pool.
- \u2713 Always shutdown() the executor.
CompletableFuture
Composable async pipelines.
CompletableFuture<String> page = CompletableFuture
.supplyAsync(() -> fetch(url))
.thenApply(String::trim)
.thenCombine(
CompletableFuture.supplyAsync(() -> fetch(url2)),
(a, b) -> a + b)
.exceptionally(err -> "fallback");
String result = page.join();- \u2713 thenApply/thenCombine chain non-blocking steps.
- \u2713 exceptionally / handle recover from failures.
Virtual Threads (Project Loom)
Millions of cheap threads scheduled on a few carriers (Java 21+).
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
exec.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return null;
});
}
}- \u2713 Write blocking code; the JVM parks/unparks the virtual thread.
- \u2713 Great fit for I/O-heavy servers.
JVM Memory Model
Heap, stack, metaspace and how they interact.
- \u25b8Stack — one per thread; primitives and references live here.
- \u25b8Heap — shared across threads; all objects live here.
- \u25b8Metaspace — class metadata, bytecode, method info.
- \u25b8PC register + native method stack — bookkeeping per thread.
- \u2713 Each thread gets its own stack.
- \u2713 Only the heap is garbage-collected.
Garbage Collection
How unreachable objects are reclaimed.
Modern collectors (G1, ZGC, Shenandoah) divide the heap into generations. Most objects die young, so a fast 'minor GC' sweeps the young generation. Long-lived objects are promoted to the old generation, which is collected less often but more thoroughly.
# Common tuning flags
-Xms512m -Xmx4g # initial / max heap
-XX:+UseG1GC # pick collector
-XX:+PrintGCDetails # log GC- \u2713 You don't free memory — you make objects unreachable.
- \u2713 Young/old generations exploit the 'weak generational hypothesis'.
Reflection & Annotations
Inspecting and manipulating classes at runtime.
Class<?> c = Class.forName("com.acme.User");
for (var m : c.getDeclaredMethods()) {
System.out.println(m.getName());
}
// Custom annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Benchmark { }
// Frameworks like Spring/JPA use reflection to wire your code.- \u2713 Reflection powers frameworks — use it sparingly in app code.
- \u2713 Annotations attach metadata for compile-time or runtime tools.
A Peek at Spring Boot
The most popular Java framework, in one file.
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@RestController
class HelloController {
@GetMapping("/hello")
String hello(@RequestParam String name) {
return "Hello, " + name;
}
}Run it and http://localhost:8080/hello?name=Ada answers 'Hello, Ada'. Spring uses annotations + reflection to wire the controller, start an embedded Tomcat, and serve HTTP.
- \u2713 Convention over configuration — sensible defaults everywhere.
- \u2713 Annotation-driven: @Service, @Repository, @Autowired, @Transactional.