Java — 0 to Hero
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
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.
| 1 | # macOS (Homebrew) |
| 2 | brew install --cask temurin |
| 3 | |
| 4 | # Ubuntu/Debian |
| 5 | sudo apt update |
| 6 | sudo apt install openjdk-21-jdk |
| 7 | |
| 8 | # Verify installation |
| 9 | java -version |
| 10 | javac -version |
| 11 | |
| 12 | # Set JAVA_HOME (add to ~/.zshrc or ~/.bashrc) |
| 13 | export JAVA_HOME=$(/usr/libexec/java_home -v 21) |
| 14 | export PATH="$JAVA_HOME/bin:$PATH" |
warning
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.
| 1 | public 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.
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.
| Type | Size | Example |
|---|---|---|
| byte | 8-bit | byte flags = 0x1F; |
| short | 16-bit | short port = 8080; |
| int | 32-bit | int count = 42; |
| long | 64-bit | long timestamp = 1_700_000_000_000L; |
| float | 32-bit | float ratio = 0.5f; |
| double | 64-bit | double pi = 3.14159; |
| boolean | JVM-dependent | boolean active = true; |
| char | 16-bit UTF-16 | char letter = 'A'; |
| 1 | public 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.
| 1 | public 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
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.
| 1 | public 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.
| 1 | // Interface defines a contract |
| 2 | public interface PaymentMethod { |
| 3 | void process(double amount); |
| 4 | } |
| 5 | |
| 6 | // Abstract class provides shared behavior |
| 7 | public 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 |
| 20 | public 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
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.
| Interface | Implementations | Use When |
|---|---|---|
| List | ArrayList, LinkedList | Ordered, indexable sequence; duplicates allowed |
| Set | HashSet, LinkedHashSet, TreeSet | Unique elements; choose for unordered, insertion-ordered, or sorted |
| Map | HashMap, LinkedHashMap, TreeMap | Key-value pairs; keys must be unique |
| Queue | ArrayDeque, PriorityQueue | FIFO or priority ordering |
| 1 | import java.util.*; |
| 2 | |
| 3 | public 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
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.
| 1 | import java.io.IOException; |
| 2 | import java.nio.file.Files; |
| 3 | import java.nio.file.Path; |
| 4 | |
| 5 | public 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.
| 1 | import java.io.BufferedReader; |
| 2 | import java.io.FileReader; |
| 3 | import java.io.IOException; |
| 4 | |
| 5 | public 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
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.
| 1 | import java.util.List; |
| 2 | |
| 3 | public 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.
| 1 | import java.util.List; |
| 2 | import java.util.Optional; |
| 3 | |
| 4 | public 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
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.
| Component | Purpose |
|---|---|
| Class Loader | Loads class files into memory at runtime |
| Bytecode Verifier | Ensures loaded bytecode is safe and well-formed |
| Execution Engine | Interprets or compiles bytecode to native code via JIT |
| Garbage Collector | Reclaims unreachable objects automatically |
| Runtime Data Areas | Heap, stack, metaspace, program counter registers |
| 1 | # View JVM version and default garbage collector |
| 2 | java -XX:+PrintCommandLineFlags -version |
| 3 | |
| 4 | # Run with a specific GC and heap size |
| 5 | java -Xms512m -Xmx2g -XX:+UseG1GC -jar app.jar |
| 6 | |
| 7 | # Generate a heap dump on OutOfMemoryError |
| 8 | java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/app.hprof -jar app.jar |
| 9 | |
| 10 | # Inspect heap with jcmd |
| 11 | jcmd <pid> GC.heap_dump /tmp/heap.hprof |
| 12 | jcmd <pid> VM.metaspace |
note
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.
| 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> |
| 1 | // build.gradle.kts — Gradle Kotlin DSL |
| 2 | plugins { |
| 3 | java |
| 4 | application |
| 5 | } |
| 6 | |
| 7 | group = "dev.forgelearn" |
| 8 | version = "1.0.0" |
| 9 | |
| 10 | java { |
| 11 | toolchain { |
| 12 | languageVersion.set(JavaLanguageVersion.of(21)) |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | repositories { |
| 17 | mavenCentral() |
| 18 | } |
| 19 | |
| 20 | dependencies { |
| 21 | testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") |
| 22 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") |
| 23 | } |
| 24 | |
| 25 | application { |
| 26 | mainClass.set("dev.forgelearn.App") |
| 27 | } |
| 28 | |
| 29 | tasks.test { |
| 30 | useJUnitPlatform() |
| 31 | } |
| Command | Maven | Gradle |
|---|---|---|
| Compile | mvn compile | ./gradlew compileJava |
| Run tests | mvn test | ./gradlew test |
| Package | mvn package | ./gradlew jar |
| Run application | mvn exec:java | ./gradlew run |
best practice
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.
| 1 | // Records (Java 16) — immutable data carriers |
| 2 | public record Point(int x, int y) {} |
| 3 | |
| 4 | // Pattern matching for switch (Java 21) |
| 5 | public 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) |
| 17 | String json = """ |
| 18 | { |
| 19 | "name": "ForgeLearn", |
| 20 | "url": "https://forgelearn.dev" |
| 21 | } |
| 22 | """; |
| 23 | |
| 24 | // Sealed classes restrict inheritance |
| 25 | public sealed interface Shape permits Circle, Rectangle {} |
| 26 | public record Circle(double radius) implements Shape {} |
| 27 | public 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.
| 1 | import java.util.concurrent.Executors; |
| 2 | |
| 3 | public 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
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.
| 1 | import java.util.*; |
| 2 | import java.util.stream.Collectors; |
| 3 | |
| 4 | public 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 | } |
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.
| Domain | Common Tools & Frameworks |
|---|---|
| Web & Microservices | Spring Boot, Spring WebFlux, Quarkus, Micronaut, Vert.x |
| Data Access | JPA/Hibernate, jOOQ, MyBatis, Spring Data JDBC |
| Testing | JUnit 5, AssertJ, Mockito, Testcontainers |
| Big Data | Apache Spark, Kafka, Flink, Hadoop |
| Mobile | Android SDK, Kotlin Multiplatform |
| Build & Ops | Maven, 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
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.