|$ curl https://forge-ai.dev/api/markdown?path=docs/java
$cat docs/java-—-0-to-hero.md
updated Recently·28 min read·published

Java — 0 to Hero

JavaBackendJVMBeginner to Intermediate🎯Free Tools
Introduction

Java is a statically typed, object-oriented programming language that has powered enterprise backends, Android applications, big-data pipelines, and cloud-native services for nearly three decades. Designed around the mantra "write once, run anywhere," Java compiles to bytecode that executes on the Java Virtual Machine (JVM), giving it portability across operating systems and hardware.

This guide covers everything you need to get productive with modern Java: installing the JDK, writing your first program, mastering core syntax and object-oriented patterns, working with collections and streams, handling exceptions, understanding the JVM, building projects with Maven and Gradle, and leveraging features from recent Long-Term Support (LTS) releases.

info

Modern Java is not the verbose language of the early 2000s. With records, pattern matching, lambdas, the Stream API, and virtual threads, contemporary Java code can be expressive, concise, and high performance.
Installing the JDK

The Java Development Kit (JDK) includes the compiler (javac), the runtime (java), debugging tools, and the standard library. You need a JDK to compile and run Java programs. The Java Runtime Environment (JRE) alone is not sufficient for development.

For new projects, choose an LTS release. Java 17 and Java 21 are the current LTS versions, with Java 21 offering virtual threads and structured concurrency. Temurin by Adoptium and Oracle JDK are the most common distributions; Temurin is open-source and free for production use.

install-jdk.sh
Bash
1# macOS (Homebrew)
2brew install --cask temurin
3
4# Ubuntu/Debian
5sudo apt update
6sudo apt install openjdk-21-jdk
7
8# Verify installation
9java -version
10javac -version
11
12# Set JAVA_HOME (add to ~/.zshrc or ~/.bashrc)
13export JAVA_HOME=$(/usr/libexec/java_home -v 21)
14export PATH="$JAVA_HOME/bin:$PATH"

warning

Do not rely on the system default Java version for builds. Pin JAVA_HOME per project using a version manager such as SDKMAN!, jEnv, or a build-tool toolchain declaration. Inconsistent JDK versions are a common source of "works on my machine" failures.
Hello, Java

Every Java program starts with at least one class. The filename must match the public class name, and execution begins in a public static void main(String[] args) method. Below is the smallest valid Java program.

HelloWorld.java
JAVA
1public class HelloWorld {
2 public static void main(String[] args) {
3 System.out.println("Hello, Java!");
4 }
5}

Compile with javac HelloWorld.java to produce HelloWorld.class, then run it with java HelloWorld. The .class file contains JVM bytecode, not native machine code.

Language Fundamentals

Java is statically typed: every variable has a declared type that is checked at compile time. The language provides primitive types for raw values and reference types for objects. Understanding the distinction is essential for writing correct and efficient code.

TypeSizeExample
byte8-bitbyte flags = 0x1F;
short16-bitshort port = 8080;
int32-bitint count = 42;
long64-bitlong timestamp = 1_700_000_000_000L;
float32-bitfloat ratio = 0.5f;
double64-bitdouble pi = 3.14159;
booleanJVM-dependentboolean active = true;
char16-bit UTF-16char letter = 'A';
Fundamentals.java
JAVA
1public class Fundamentals {
2 public static void main(String[] args) {
3 // Variables and constants
4 final double TAX_RATE = 0.08;
5 int quantity = 10;
6 double price = 19.99;
7
8 // Arithmetic and compound assignment
9 double total = quantity * price * (1 + TAX_RATE);
10 quantity += 5;
11
12 // Strings are immutable reference types
13 String name = "Forge";
14 String greeting = "Hello, " + name;
15
16 // Type inference with var (Java 10+)
17 var message = " inferred type"; // String
18
19 System.out.println(greeting + message);
20 System.out.println("Total: $" + String.format("%.2f", total));
21 }
22}

Java supports the usual control-flow constructs: if/else, switch (with pattern matching in recent releases), for, while, do-while, and break/continue. Methods are declared with a return type, name, parameters, and optional access modifier.

ControlFlow.java
JAVA
1public class ControlFlow {
2 public static void main(String[] args) {
3 int[] scores = {85, 92, 78, 95, 88};
4 int sum = 0;
5
6 for (int score : scores) {
7 sum += score;
8 }
9
10 double average = (double) sum / scores.length;
11
12 String grade = switch ((int) average / 10) {
13 case 10, 9 -> "A";
14 case 8 -> "B";
15 case 7 -> "C";
16 default -> "F";
17 };
18
19 System.out.println("Average: " + average);
20 System.out.println("Grade: " + grade);
21 }
22}

best practice

Prefer var for local variables when the type is obvious from the right-hand side. It reduces verbosity without sacrificing type safety. Avoid var when the inferred type is unclear or when the initializer returns a broad interface.
Object-Oriented Programming

Java is fundamentally object-oriented. Classes bundle data (fields) and behavior (methods). Objects are instances of classes, created with the new keyword. Core OOP principles in Java include encapsulation, inheritance, polymorphism, and abstraction.

User.java
JAVA
1public class User {
2 private final String id;
3 private String email;
4 private boolean active;
5
6 public User(String id, String email) {
7 this.id = id;
8 this.email = email;
9 this.active = true;
10 }
11
12 public String getId() {
13 return id;
14 }
15
16 public String getEmail() {
17 return email;
18 }
19
20 public void setEmail(String email) {
21 this.email = email;
22 }
23
24 public boolean isActive() {
25 return active;
26 }
27
28 public void deactivate() {
29 this.active = false;
30 }
31
32 @Override
33 public String toString() {
34 return "User{id='" + id + "', email='" + email + "', active=" + active + "}";
35 }
36}

Inheritance lets you build specialized classes from general ones. Use extends for class inheritance and implements for interfaces. Java supports single inheritance of classes but multiple inheritance of interfaces.

OOP.java
JAVA
1// Interface defines a contract
2public interface PaymentMethod {
3 void process(double amount);
4}
5
6// Abstract class provides shared behavior
7public abstract class Account {
8 protected String accountId;
9 protected double balance;
10
11 public Account(String accountId, double balance) {
12 this.accountId = accountId;
13 this.balance = balance;
14 }
15
16 public abstract void deposit(double amount);
17}
18
19// Concrete class
20public class BankAccount extends Account implements PaymentMethod {
21 public BankAccount(String accountId, double balance) {
22 super(accountId, balance);
23 }
24
25 @Override
26 public void deposit(double amount) {
27 balance += amount;
28 }
29
30 @Override
31 public void process(double amount) {
32 if (balance >= amount) {
33 balance -= amount;
34 } else {
35 throw new IllegalStateException("Insufficient funds");
36 }
37 }
38}

info

Favor composition over inheritance. Use interfaces to decouple contracts from implementation, and prefer final classes and records when mutability is not required. Records are the modern, concise way to model immutable data carriers.
Collections Framework

The Java Collections Framework provides reusable data structures and algorithms. The main interfaces are List, Set, Map, and Queue. Choose implementations based on ordering, uniqueness, and performance requirements.

InterfaceImplementationsUse When
ListArrayList, LinkedListOrdered, indexable sequence; duplicates allowed
SetHashSet, LinkedHashSet, TreeSetUnique elements; choose for unordered, insertion-ordered, or sorted
MapHashMap, LinkedHashMap, TreeMapKey-value pairs; keys must be unique
QueueArrayDeque, PriorityQueueFIFO or priority ordering
CollectionsDemo.java
JAVA
1import java.util.*;
2
3public class CollectionsDemo {
4 public static void main(String[] args) {
5 // List preserves insertion order and allows duplicates
6 List<String> names = new ArrayList<>();
7 names.add("Alice");
8 names.add("Bob");
9 names.add("Alice");
10
11 // Set removes duplicates
12 Set<String> unique = new HashSet<>(names);
13
14 // Map stores key-value pairs
15 Map<String, Integer> scores = new HashMap<>();
16 scores.put("Alice", 95);
17 scores.put("Bob", 87);
18
19 // Immutable collections (Java 9+)
20 List<String> colors = List.of("red", "green", "blue");
21 Map<String, String> config = Map.of("host", "localhost", "port", "8080");
22
23 System.out.println(names);
24 System.out.println(unique);
25 System.out.println(scores);
26 System.out.println(colors);
27 }
28}

warning

Mutable collections shared across threads require synchronization or concurrent implementations such as ConcurrentHashMap and CopyOnWriteArrayList. Standard ArrayList and HashMap are not thread-safe.
Exceptions

Java uses exceptions to handle runtime errors. Exceptions are objects that extend Throwable. Checked exceptions (subclasses of Exception but not RuntimeException) must be declared or handled. Unchecked exceptions (RuntimeException) and errors (Error) do not require explicit handling.

ExceptionDemo.java
JAVA
1import java.io.IOException;
2import java.nio.file.Files;
3import java.nio.file.Path;
4
5public class ExceptionDemo {
6 public static void main(String[] args) {
7 try {
8 String content = Files.readString(Path.of("config.json"));
9 System.out.println(content);
10 } catch (IOException e) {
11 System.err.println("Failed to read config: " + e.getMessage());
12 } finally {
13 System.out.println("Cleanup runs regardless of outcome");
14 }
15 }
16
17 public double divide(int a, int b) {
18 if (b == 0) {
19 throw new IllegalArgumentException("Divisor cannot be zero");
20 }
21 return (double) a / b;
22 }
23}

Java 7 introduced multi-catch and try-with-resources for automatic resource management. Use try-with-resources for any class implementing AutoCloseable to prevent resource leaks.

TryWithResources.java
JAVA
1import java.io.BufferedReader;
2import java.io.FileReader;
3import java.io.IOException;
4
5public class TryWithResources {
6 public static void main(String[] args) {
7 try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
8 String line;
9 while ((line = reader.readLine()) != null) {
10 System.out.println(line);
11 }
12 } catch (IOException e) {
13 System.err.println("I/O error: " + e.getMessage());
14 }
15 }
16}

best practice

Do not catch Exception or Throwable unless you genuinely plan to recover. Swallowing exceptions hides bugs and makes debugging painful. Log or rethrow with context instead.
Streams & Lambda Expressions

Java 8 introduced lambda expressions and the Stream API, bringing functional-style operations to collections. A lambda is an anonymous function: (parameters) -> expression. Streams support map/filter/reduce pipelines that can run sequentially or in parallel.

StreamDemo.java
JAVA
1import java.util.List;
2
3public class StreamDemo {
4 public static void main(String[] args) {
5 List<String> names = List.of("alice", "bob", "charlie", "dave");
6
7 List<String> result = names.stream()
8 .filter(n -> n.length() > 3)
9 .map(String::toUpperCase)
10 .sorted()
11 .toList();
12
13 System.out.println(result); // [ALICE, CHARLIE, DAVE]
14
15 int totalLength = names.stream()
16 .mapToInt(String::length)
17 .sum();
18
19 System.out.println("Total length: " + totalLength);
20 }
21}

Streams are lazy: intermediate operations such as filter and map are not evaluated until a terminal operation such as collect, forEach, or reduce is invoked. Once consumed, a stream cannot be reused.

StreamPatterns.java
JAVA
1import java.util.List;
2import java.util.Optional;
3
4public class StreamPatterns {
5 record Person(String name, int age) {}
6
7 public static void main(String[] args) {
8 List<Person> people = List.of(
9 new Person("Alice", 30),
10 new Person("Bob", 25),
11 new Person("Carol", 35)
12 );
13
14 Optional<Person> firstAdult = people.stream()
15 .filter(p -> p.age() >= 30)
16 .findFirst();
17
18 firstAdult.ifPresent(p -> System.out.println(p.name()));
19
20 double averageAge = people.stream()
21 .mapToInt(Person::age)
22 .average()
23 .orElse(0.0);
24
25 System.out.println("Average age: " + averageAge);
26 }
27}

info

Use method references such as String::toUpperCase when they make the code clearer. Prefer streams for declarative data transformations, but use plain loops when performance is critical or when side effects make streams harder to read.
The Java Virtual Machine

The JVM is the runtime engine that executes Java bytecode. It provides memory management, garbage collection, security, and platform abstraction. Understanding the JVM helps you diagnose performance issues, memory leaks, and garbage-collection pauses.

ComponentPurpose
Class LoaderLoads class files into memory at runtime
Bytecode VerifierEnsures loaded bytecode is safe and well-formed
Execution EngineInterprets or compiles bytecode to native code via JIT
Garbage CollectorReclaims unreachable objects automatically
Runtime Data AreasHeap, stack, metaspace, program counter registers
jvm-commands.sh
Bash
1# View JVM version and default garbage collector
2java -XX:+PrintCommandLineFlags -version
3
4# Run with a specific GC and heap size
5java -Xms512m -Xmx2g -XX:+UseG1GC -jar app.jar
6
7# Generate a heap dump on OutOfMemoryError
8java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/app.hprof -jar app.jar
9
10# Inspect heap with jcmd
11jcmd <pid> GC.heap_dump /tmp/heap.hprof
12jcmd <pid> VM.metaspace
📝

note

Modern JVMs use Just-In-Time (JIT) compilation to translate hot bytecode into native machine code at runtime. This is why well-tuned Java can match the performance of native languages for long-running server workloads.
Build Tools: Maven & Gradle

Java projects are rarely built by hand. Maven and Gradle are the dominant build tools. They manage dependencies, compile source code, run tests, package artifacts, and publish libraries. Both integrate with IDEs and CI/CD pipelines.

pom.xml
XML
1<!-- pom.xml — Maven project descriptor -->
2<project xmlns="http://maven.apache.org/POM/4.0.0">
3 <modelVersion>4.0.0</modelVersion>
4 <groupId>dev.forgelearn</groupId>
5 <artifactId>java-demo</artifactId>
6 <version>1.0.0</version>
7 <packaging>jar</packaging>
8
9 <properties>
10 <maven.compiler.source>21</maven.compiler.source>
11 <maven.compiler.target>21</maven.compiler.target>
12 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
13 </properties>
14
15 <dependencies>
16 <dependency>
17 <groupId>org.junit.jupiter</groupId>
18 <artifactId>junit-jupiter</artifactId>
19 <version>5.10.0</version>
20 <scope>test</scope>
21 </dependency>
22 </dependencies>
23</project>
build.gradle.kts
GROOVY
1// build.gradle.kts — Gradle Kotlin DSL
2plugins {
3 java
4 application
5}
6
7group = "dev.forgelearn"
8version = "1.0.0"
9
10java {
11 toolchain {
12 languageVersion.set(JavaLanguageVersion.of(21))
13 }
14}
15
16repositories {
17 mavenCentral()
18}
19
20dependencies {
21 testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
22 testRuntimeOnly("org.junit.platform:junit-platform-launcher")
23}
24
25application {
26 mainClass.set("dev.forgelearn.App")
27}
28
29tasks.test {
30 useJUnitPlatform()
31}
CommandMavenGradle
Compilemvn compile./gradlew compileJava
Run testsmvn test./gradlew test
Packagemvn package./gradlew jar
Run applicationmvn exec:java./gradlew run

best practice

Pin your Java toolchain version in the build file rather than relying on the environment JAVA_HOME. Both Maven and Gradle support toolchain declarations that automatically download the correct JDK if it is missing.
Modern Java Features

Java has evolved rapidly since Java 8. Recent LTS releases introduced syntax sugar, performance improvements, and concurrency models that make the language competitive with newer alternatives. Below are the most impactful features to know.

ModernJava.java
JAVA
1// Records (Java 16) — immutable data carriers
2public record Point(int x, int y) {}
3
4// Pattern matching for switch (Java 21)
5public String describe(Object obj) {
6 return switch (obj) {
7 case Point p when p.x() == 0 -> "origin column";
8 case Point p -> "point at " + p.x() + "," + p.y();
9 case String s when s.isEmpty() -> "empty string";
10 case String s -> "string: " + s;
11 case null -> "nothing";
12 default -> "unknown";
13 };
14}
15
16// Text blocks (Java 15)
17String json = """
18 {
19 "name": "ForgeLearn",
20 "url": "https://forgelearn.dev"
21 }
22 """;
23
24// Sealed classes restrict inheritance
25public sealed interface Shape permits Circle, Rectangle {}
26public record Circle(double radius) implements Shape {}
27public record Rectangle(double width, double height) implements Shape {}

Java 21 introduced virtual threads, which are lightweight threads managed by the JVM. They make it practical to write straightforward blocking I/O code that scales to millions of concurrent operations, reducing the need for reactive programming in many server applications.

VirtualThreads.java
JAVA
1import java.util.concurrent.Executors;
2
3public class VirtualThreads {
4 public static void main(String[] args) throws Exception {
5 // Virtual thread per task executor
6 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
7 for (int i = 0; i < 100_000; i++) {
8 int taskId = i;
9 executor.submit(() -> {
10 System.out.println("Task " + taskId + " on " + Thread.currentThread());
11 });
12 }
13 }
14 }
15}

info

Virtual threads are not faster for CPU-bound work. Use them for I/O-bound workloads with many blocking calls, such as HTTP clients, database queries, and message consumers. For CPU-bound tasks, continue using platform threads and parallel streams.
Complete Example: Task Manager

The following program combines classes, collections, streams, exceptions, and records into a small but complete command-line task manager. It demonstrates how modern Java idioms fit together in a real application.

TaskManager.java
JAVA
1import java.util.*;
2import java.util.stream.Collectors;
3
4public class TaskManager {
5 record Task(String id, String title, boolean completed) {}
6
7 private final Map<String, Task> tasks = new HashMap<>();
8
9 public void add(String title) {
10 String id = UUID.randomUUID().toString().substring(0, 8);
11 tasks.put(id, new Task(id, title, false));
12 }
13
14 public void complete(String id) {
15 Task existing = tasks.get(id);
16 if (existing == null) {
17 throw new NoSuchElementException("Task not found: " + id);
18 }
19 tasks.put(id, new Task(id, existing.title(), true));
20 }
21
22 public List<Task> listPending() {
23 return tasks.values().stream()
24 .filter(t -> !t.completed())
25 .sorted(Comparator.comparing(Task::title))
26 .collect(Collectors.toList());
27 }
28
29 public long completedCount() {
30 return tasks.values().stream().filter(Task::completed).count();
31 }
32
33 public static void main(String[] args) {
34 TaskManager manager = new TaskManager();
35 manager.add("Learn Java collections");
36 manager.add("Build a REST API");
37 manager.add("Deploy to production");
38
39 String firstId = manager.listPending().get(0).id();
40 manager.complete(firstId);
41
42 System.out.println("Pending tasks:");
43 manager.listPending().forEach(t -> System.out.println(" - " + t.title()));
44 System.out.println("Completed: " + manager.completedCount());
45 }
46}
Ecosystem & Use Cases

Java's ecosystem is enormous. The language dominates enterprise backends, financial systems, and large-scale data infrastructure. The Spring framework and its Boot variant provide a comprehensive platform for building microservices, while Jakarta EE remains prevalent in traditional application servers.

DomainCommon Tools & Frameworks
Web & MicroservicesSpring Boot, Spring WebFlux, Quarkus, Micronaut, Vert.x
Data AccessJPA/Hibernate, jOOQ, MyBatis, Spring Data JDBC
TestingJUnit 5, AssertJ, Mockito, Testcontainers
Big DataApache Spark, Kafka, Flink, Hadoop
MobileAndroid SDK, Kotlin Multiplatform
Build & OpsMaven, Gradle, GitHub Actions, Jenkins, Docker, Kubernetes

When choosing libraries, prefer mature, well-documented options with active maintenance. The Java ecosystem values backward compatibility, so older APIs often remain stable for many years. That stability is a strength, but it also means you should verify that dependencies support your target JDK version.

📝

note

Kotlin runs on the JVM and is fully interoperable with Java. Many teams use Kotlin for new services while keeping legacy Java modules. Learning Java first makes Kotlin easier to understand, since both share the same runtime, libraries, and tooling.
$Blueprint — Engineering Documentation·Section ID: JAVA-OVR·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.