Learn & Reference

Java Tutorials
& Snippets

In-depth Java tutorials with practical, copy-paste ready code examples. Learn core concepts, best practices, and real-world techniques.

Thinking
6
TUTORIALS
38
SNIPPETS
14
CATEGORIES
17+
JAVA VERSION
Table of Contents
1 Tutorial

Getting Started with Java

Java is one of the most widely used programming languages in the world, powering everything from enterprise backend systems to Android mobile applications. Originally developed by James Gosling at Sun Microsystems in 1995, Java follows the "write once, run anywhere" philosophy — your compiled Java code can run on any device that has a Java Virtual Machine (JVM) installed, regardless of the underlying operating system or hardware architecture.

Understanding the JDK, JRE, and JVM

Before writing your first Java program, it is important to understand the three core components of the Java ecosystem. The JVM (Java Virtual Machine) is the runtime engine that executes compiled Java bytecode. The JRE (Java Runtime Environment) includes the JVM plus the core class libraries needed to run Java applications. The JDK (Java Development Kit) includes everything in the JRE plus the development tools like the Java compiler (javac), debugger, and jar packaging tool. To develop Java applications, you need the JDK installed on your machine.

How Java Compilation Works

Unlike interpreted languages such as Python or JavaScript, Java uses a two-step compilation process. First, the Java compiler (javac) translates your .java source files into bytecode stored in .class files. This bytecode is not native machine code — it is an intermediate representation. Second, the JVM reads this bytecode and uses its Just-In-Time (JIT) compiler to translate frequently executed bytecode paths into optimized native machine code at runtime. This hybrid approach gives Java both platform independence and competitive performance.

Your First Java Program

Every Java application must have at least one class, and execution begins in the main method. Here is the classic "Hello World" program with a detailed breakdown of each component:

public class HelloWorld { // The JVM looks for this exact method signature to start execution public static void main(String[] args) { System.out.println("Hello, World!"); } }

Let's break this down line by line. public is an access modifier making the class accessible from everywhere. class declares a new class, and HelloWorld is the class name — which must match the filename (HelloWorld.java). The static keyword means the method belongs to the class itself, not to an instance, so the JVM can call it without creating an object. void means the method returns no value. String[] args is an array of command-line arguments.

Pro Tip

In Java 11 and later, you can run a single-file Java program directly with java HelloWorld.java without manually compiling it first. The JVM compiles and runs it in one step — great for quick testing and learning.

Setting Up Your Development Environment

To start coding in Java, you have several options. The most straightforward approach is to download the latest LTS (Long Term Support) JDK from Adoptium (Eclipse Temurin), Oracle, or Amazon Corretto. As of 2024, Java 21 is the latest LTS release and is recommended for most projects. For an IDE, IntelliJ IDEA Community Edition (free) is the most popular choice among professional Java developers. Alternatives include Eclipse, VS Code with the Java Extension Pack, and Apache NetBeans.

If you prefer not to install anything locally, you can use online Java compilers like Replit, OnlineGDB, or JDoodle to practice writing and running Java code directly in your browser. These tools are excellent for following along with the tutorials on this page.

Key Takeaways

  • Java compiles to bytecode, not native code — this enables platform independence via the JVM.
  • The JDK includes the compiler, JRE includes the JVM, and the JVM executes bytecode.
  • Every Java program needs a class with a main method as the entry point.
  • Java 21 is the current LTS release — use it for new projects.
  • IntelliJ IDEA Community is the recommended free IDE for Java development.
2 Tutorial

Core Java Concepts Every Developer Should Know

Before diving into advanced topics like frameworks and concurrency, you need a solid understanding of Java's fundamental building blocks. This tutorial covers the core concepts that form the foundation of every Java program: data types, variables, operators, control flow statements, and methods. Mastering these basics will make learning everything else significantly easier.

Primitive Data Types and Variables

Java is a statically typed language, meaning every variable must be declared with a type before it can be used, and that type cannot change. Java has eight primitive data types divided into four categories: integer types (byte, short, int, long), floating-point types (float, double), character type (char), and boolean type (boolean). Unlike many other languages, Java's primitives have fixed sizes across all platforms — an int is always 32 bits whether you run on a 32-bit or 64-bit system.

// Primitive types with their sizes and ranges int age = 25; // 32-bit: -2^31 to 2^31-1 long population = 8_000_000_000L; // 64-bit: note the 'L' suffix double price = 19.99; // 64-bit floating point boolean isActive = true; // true or false only char grade = 'A'; // 16-bit Unicode character // Type inference with 'var' (Java 10+) var name = "Alice"; // Compiler infers String type var count = 42; // Compiler infers int type // Constants use 'final' — cannot be reassigned final double PI = 3.14159;

Common Mistake

Never use float or double for financial calculations. They suffer from floating-point precision issues. For example, 0.1 + 0.2 does not equal 0.3 exactly. Use java.math.BigDecimal for money instead.

Control Flow: Making Decisions and Loops

Control flow statements determine the order in which your code executes. Java provides the standard if/else and switch statements for branching, and for, while, and do-while for looping. Starting with Java 14, the enhanced switch expression was introduced, which is more concise, prevents fall-through bugs, and can be used as an expression that returns a value.

// Enhanced switch expression (Java 14+) String result = switch (day) { case MONDAY, FRIDAY -> "Busy day"; case SATURDAY, SUNDAY -> "Weekend"; default -> "Regular day"; }; // Enhanced for loop (for-each) for (String item : items) { System.out.println(item); } // Modern iteration with List.forEach items.forEach(item -> System.out.println(item));

Methods: Organizing Your Code

A method (also called a function in other languages) is a reusable block of code that performs a specific task. In Java, every method must be declared inside a class. Methods promote code reusability, make your programs easier to read and maintain, and allow you to break complex problems into smaller, manageable pieces. A well-designed method should do one thing, do it well, and have a clear name that describes what it does.

// Method with parameters and return value public static double calculateDiscount(double price, double percent) { if (percent < 0 || percent > 100) { throw new IllegalArgumentException("Invalid discount"); } return price - (price * percent / 100); } // Varargs — accepts any number of arguments public static int sum(int... numbers) { return Arrays.stream(numbers).sum(); } sum(1, 2, 3); // returns 6 sum(10, 20); // returns 30

Pro Tip

Keep methods short — ideally under 20 lines. If a method is getting long, look for opportunities to extract sub-tasks into separate methods. This follows the Single Responsibility Principle and makes your code much easier to test and debug.

Key Takeaways

  • Java has 8 primitive types with guaranteed fixed sizes across all platforms.
  • Use var for local variables when the type is obvious from context.
  • Never use floating-point types for money — always use BigDecimal.
  • Prefer the enhanced switch expression (Java 14+) over the traditional switch statement.
  • Methods should be short, focused, and have descriptive names.
3 Tutorial

Object-Oriented Programming in Java

Java is an object-oriented programming (OOP) language at its core. Unlike procedural programming where you write sequences of instructions, OOP organizes code around objects — self-contained entities that combine data (fields) and behavior (methods). Understanding OOP is essential for writing clean, maintainable Java code, and it is a fundamental skill tested in virtually every Java job interview.

There are four pillars of OOP that you need to understand: Encapsulation, Inheritance, Polymorphism, and Abstraction. Let's explore each one with practical Java examples.

Classes and Objects

A class is a blueprint or template that defines the structure and behavior of its objects. An object is a specific instance of a class. Think of a class as the architectural drawing for a house, and objects as the actual houses built from that drawing. Each house (object) has the same structure but can have different values for its properties — different paint colors, different addresses, and so on.

// Defining a class public class BankAccount { // Encapsulated fields — private, accessed via methods private String accountNumber; private double balance; // Constructor — called when creating a new object public BankAccount(String accountNumber, double initialBalance) { this.accountNumber = accountNumber; this.balance = initialBalance; } // Public methods provide controlled access to private data public void deposit(double amount) { if (amount <= 0) throw new IllegalArgumentException("Amount must be positive"); balance += amount; } public void withdraw(double amount) { if (amount > balance) throw new InsufficientFundsException(); balance -= amount; } public double getBalance() { return balance; } } // Creating objects var account = new BankAccount("12345", 1000.00); account.deposit(250.00); System.out.println(account.getBalance()); // 1250.0

Encapsulation: Hiding Implementation Details

Encapsulation is the practice of hiding an object's internal state and requiring all interaction to be performed through an object's methods. In Java, you achieve this by declaring fields as private and providing public getter and setter methods. This gives you control over how the data is accessed and modified — you can add validation, logging, or trigger side effects when a field changes. In the BankAccount example above, the deposit method validates that the amount is positive before modifying the balance, preventing invalid state.

Inheritance and Polymorphism

Inheritance allows a new class (subclass) to inherit fields and methods from an existing class (superclass), promoting code reuse. Polymorphism (meaning "many forms") allows objects of different subclasses to be treated as objects of their common superclass, with the correct method implementation being called at runtime. This is one of the most powerful features of OOP — it lets you write flexible code that works with a superclass type while automatically using the correct subclass behavior.

// Abstract superclass public abstract class Shape { private String color; public Shape(String color) { this.color = color; } // Abstract method — subclasses MUST implement this public abstract double area(); public String getColor() { return color; } } // Concrete subclasses public class Circle extends Shape { private final double radius; public Circle(String color, double radius) { super(color); this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } // Polymorphism in action List<Shape> shapes = List.of(new Circle("red", 5), new Rectangle("blue", 4, 6)); for (Shape s : shapes) { System.out.println(s.area()); // Calls the correct area() for each type }

Pro Tip

Favor composition over inheritance. Instead of creating deep inheritance hierarchies, prefer composing objects from smaller, focused components. This makes your code more flexible and easier to change. Use inheritance only when there is a clear "is-a" relationship, and prefer implementing interfaces for polymorphism.

Modern Java: Records (Java 16+)

If you need a simple class that primarily holds data (a "data carrier"), Java 16 introduced records as a concise way to declare immutable classes. A record automatically generates the constructor, accessor methods, equals(), hashCode(), and toString() for you. What would take 30+ lines of boilerplate with a traditional class can be expressed in a single line.

// A record — immutable data carrier in one line public record Point(double x, double y) {} // Usage var p = new Point(3.0, 4.0); p.x(); // 3.0 — accessor method (no "get" prefix) p.toString(); // "Point[x=3.0, y=4.0]"

Key Takeaways

  • A class is a blueprint; an object is a specific instance created from that blueprint.
  • Encapsulation protects data integrity by making fields private and exposing methods.
  • Polymorphism lets you write code that works with superclass types while using subclass behavior.
  • Favor composition over inheritance for more flexible designs.
  • Use records for simple immutable data carriers instead of writing boilerplate classes.
Advertisement
4 Tutorial

Exception Handling in Java: A Complete Guide

Exceptions are Java's mechanism for handling runtime errors gracefully. Instead of crashing your program when something goes wrong — a file not found, a network timeout, or invalid user input — Java's exception handling system lets you detect, report, and recover from errors in a structured way. Understanding how exceptions work is critical for writing robust, production-quality Java applications.

Checked vs Unchecked Exceptions

Java divides exceptions into two categories. Checked exceptions (subclasses of Exception but not RuntimeException) must be either caught with a try-catch block or declared in the method signature with throws. These represent conditions that a reasonable application might want to recover from, such as IOException or SQLException. Unchecked exceptions (subclasses of RuntimeException) do not need to be declared or caught. These typically represent programming errors like NullPointerException, IllegalArgumentException, or IndexOutOfBoundsException.

// Checked exception — must be handled or declared public void readFile(String path) throws IOException { String content = Files.readString(Path.of(path)); System.out.println(content); } // Handling with try-catch try { readFile("data.txt"); } catch (IOException e) { System.err.println("Failed to read file: " + e.getMessage()); } // Unchecked exception — no declaration needed public void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("Invalid age: " + age); } this.age = age; }

Try-with-Resources: Automatic Resource Management

One of the most common sources of resource leaks in Java is forgetting to close files, database connections, or network sockets. The try-with-resources statement (introduced in Java 7) solves this problem elegantly. Any object that implements the AutoCloseable interface can be declared in the try statement header, and Java guarantees it will be closed automatically when the block exits — whether it exits normally or due to an exception. This eliminates the need for a finally block just to close resources.

// Old way — error-prone, verbose BufferedReader reader = null; try { reader = new BufferedReader(new FileReader("data.txt")); return reader.readLine(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (reader != null) try { reader.close(); } catch (IOException e) { } } // Modern way — clean, safe, automatic closing try (var reader = new BufferedReader(new FileReader("data.txt"))) { return reader.readLine(); } // reader is automatically closed here

Common Mistake

Never use an empty catch block like catch (Exception e) { }. This silently swallows errors and makes debugging extremely difficult. At minimum, log the exception. Only catch exceptions you can actually handle — let the rest propagate up the call stack.

Creating Custom Exceptions

When the built-in exception types don't accurately describe the error condition in your domain, you should create custom exceptions. This makes your error handling more expressive and allows callers to catch and handle specific business errors differently. A custom exception is simply a class that extends Exception (for checked) or RuntimeException (for unchecked). Always provide constructors that accept a message and a cause (the original exception that triggered this one) for proper exception chaining.

// Custom unchecked exception public class InsufficientFundsException extends RuntimeException { private final double balance; private final double requested; public InsufficientFundsException(double balance, double requested) { super(String.format("Insufficient funds: balance=%.2f, requested=%.2f", balance, requested)); this.balance = balance; this.requested = requested; } }

Key Takeaways

  • Checked exceptions must be caught or declared; unchecked exceptions don't need to be.
  • Always use try-with-resources for files, connections, and streams.
  • Never use empty catch blocks — at minimum, log the exception.
  • Create custom exceptions for domain-specific error conditions.
  • Only catch exceptions you can actually handle — let others propagate.
5 Tutorial

Mastering the Java Collections Framework

The Java Collections Framework (JCF) is a unified architecture for representing and manipulating collections of objects. It provides interfaces (like List, Set, Map), implementations (like ArrayList, HashSet, HashMap), and algorithms (like sorting and searching) that you use in virtually every Java program. Understanding which collection to use in a given situation is a hallmark of an experienced Java developer.

List, Set, and Map — When to Use What

The three most important collection interfaces serve different purposes. A List is an ordered collection that allows duplicates and provides positional access (you can get elements by index). Use a List when you need to maintain insertion order or access elements by position. A Set is a collection that does not allow duplicate elements. Use a Set when you need to eliminate duplicates or check membership efficiently. A Map maps keys to values — each key can appear at most once. Use a Map when you need to associate values with keys and look them up quickly.

Interface Ordered? Duplicates? Best Implementation
ListYes (by index)YesArrayList
SetDepends on implNoHashSet
MapDepends on implNo (keys)HashMap

Choosing the Right Set Implementation

Not all sets behave the same way. HashSet offers the fastest operations (O(1) average) but does not maintain any order. LinkedHashSet maintains insertion order while still providing near-O(1) operations — use it when you need to iterate over elements in the order they were added. TreeSet keeps elements sorted in natural order (or by a custom Comparator) but operations take O(log n) time. The same pattern applies to maps: HashMap for speed, LinkedHashMap for insertion order, TreeMap for sorted keys.

// Immutable collections (Java 9+) — preferred when data won't change List<String> colors = List.of("Red", "Green", "Blue"); Set<String> uniqueNames = Set.of("Alice", "Bob"); Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25); // Mutable collections List<String> list = new ArrayList<>(); list.add("Item"); list.remove(0); // Common operations list.contains("Item"); // true list.indexOf("Item"); // 0 list.subList(0, 2); // elements from index 0 (inclusive) to 2 (exclusive) Collections.sort(list); // in-place sort

Pro Tip

Program to interfaces, not implementations. Declare your variables as List<String> list = new ArrayList<>() rather than ArrayList<String> list = new ArrayList<>(). This makes it easy to swap implementations later without changing the rest of your code.

Key Takeaways

  • Use List for ordered data with duplicates, Set for uniqueness, Map for key-value lookups.
  • Prefer immutable collections (List.of()) when data won't change.
  • Choose Set/Map implementations based on ordering needs vs. performance.
  • Always program to interfaces, not concrete implementations.
  • Use computeIfAbsent and merge for atomic map operations.
6 Tutorial

Java Streams: A Practical Guide

The Stream API, introduced in Java 8, fundamentally changed how Java developers process collections of data. A stream is a sequence of elements supporting sequential and parallel aggregate operations. Unlike collections, which are data structures that store elements, streams are a pipeline for computing values from a data source. They don't store data themselves — they transport it through a series of operations to produce a result.

Streams encourage a declarative style of programming: instead of specifying exactly how to iterate and process each element (imperative), you describe what you want to achieve, and the stream framework figures out the how. This leads to more readable, more maintainable, and often more parallelizable code.

Stream Pipeline: Source → Operations → Result

Every stream pipeline consists of three parts: a source (a collection, array, I/O channel, or generator function), zero or more intermediate operations (which transform the stream into another stream — like filter, map, sorted), and one terminal operation (which produces a result or a side effect — like collect, forEach, count). Intermediate operations are lazy — they don't execute until a terminal operation is invoked.

// A complete stream pipeline List<String> result = employees.stream() // Source .filter(e -> e.getSalary() > 50000) // Intermediate: keep only high earners .map(Employee::getName) // Intermediate: extract names .distinct() // Intermediate: remove duplicates .sorted() // Intermediate: sort alphabetically .collect(Collectors.toList()); // Terminal: collect to List // Grouping and aggregation Map<String, Double> avgSalaryByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.averagingDouble(Employee::getSalary) )); // Partitioning (special case of grouping with boolean key) Map<Boolean, List<Employee>> partitioned = employees.stream() .collect(Collectors.partitioningBy(e -> e.getAge() > 30));

When to Use Streams vs For Loops

Streams are not always the best choice. Use streams when you are performing transformations, filtering, or aggregations on a collection — operations that naturally describe "what" you want. Streams shine with complex pipelines where the declarative style improves readability. However, use traditional for loops when you need to modify elements in place, when you need explicit control over iteration (like breaking early with complex conditions), or when dealing with primitive arrays where the boxing overhead of streams would hurt performance. In practice, most real-world codebases use a mix of both approaches.

// Good use of streams: complex data transformation Map<String, Long> wordCounts = Files.lines(Path.of("book.txt")) .flatMap(line -> Arrays.stream(line.split("\\s+"))) .map(String::toLowerCase) .filter(word -> word.length() > 3) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); // Better as a for loop: modifying elements in place for (Employee e : employees) { if (e.getSalary() < 50000) { e.setSalary(e.getSalary() * 1.1); // mutate in place } }

Common Mistake

A stream can only be consumed once. If you try to use the same stream reference for two terminal operations, you'll get an IllegalStateException: stream has already been operated upon or closed. If you need to process the same data multiple times, either create a new stream from the source or collect the results into a collection first.

Pro Tip

Be careful with .parallelStream(). Parallel streams use the common ForkJoinPool, which has a fixed number of threads equal to available processors. For CPU-bound tasks on large datasets, parallelism can provide significant speedups. But for small collections, I/O-bound operations, or when running in a web server (where the common pool is shared across requests), parallel streams can actually be slower and cause thread starvation. Always benchmark before switching to parallel.

Key Takeaways

  • Streams are pipelines: source → intermediate ops → terminal op.
  • Intermediate operations are lazy — nothing happens until a terminal operation is called.
  • Use streams for transformations and aggregations; use for loops for mutation.
  • A stream can only be consumed once — create a new one if you need to reprocess.
  • Don't use parallel streams blindly — benchmark and consider the context.
Advertisement
Quick Reference

Java Code Snippets

Copy-paste ready code examples for everyday Java development tasks. Use the filters below or search to find what you need.

Core Java Collections Streams File I/O Date & Time Concurrency Networking JDBC JSON Spring Testing Utility Performance Patterns
Advertisement