Java • 2026-10-10

Java Tutorial: Classes, Objects, Collections, and Exception Handling

Learn Java fundamentals: the JVM, types, control flow, classes and objects, inheritance, interfaces, records, collections, generics, streams, exceptions, and a complete runnable example.

Learn Java fundamentals: the JVM, types, control flow, classes and objects, inheritance, interfaces, records, collections, generics, streams, exceptions, and a complete runnable example.

Developer workspace with code on a screen
Build your programming knowledge one practical example at a time.

What Is Java?

Java is a statically typed, object-oriented language commonly used for backend services, enterprise systems, Android development, and tools. Java source code is compiled to bytecode that runs on a Java Virtual Machine (JVM).

JDK, JRE, and JVM

  • JVM: executes bytecode and provides memory management and garbage collection.
  • JRE: the runtime environment that includes the JVM and standard libraries.
  • JDK: the development kit with the compiler (javac), tools, and the runtime. Install a current long-term-support JDK to develop.

Why Java is popular

  • Strong typing and tooling catch many mistakes before runtime.
  • Automatic garbage collection removes manual memory management.
  • Large ecosystem: Spring, Hibernate, Maven, Gradle, JUnit.
  • Runs on many platforms wherever a compatible JVM is available.

Hello, world and running it

// Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}

// Compile and run:
//   javac Main.java
//   java Main

Types, Variables, and Strings

  • Primitive types: int, long, double, boolean, char, byte, short, float.
  • Reference types: classes, arrays, interfaces, and String.
  • var lets the compiler infer local variable types.
  • String is immutable; use StringBuilder to build text in loops.
  • Compare strings with .equals(), not ==.
  • Use text blocks (""") for multi-line strings.

Variables, strings, and StringBuilder

public class Types {
    public static void main(String[] args) {
        int age = 21;
        double price = 19.99;
        boolean active = true;
        var name = "Asha";                     // inferred as String

        String a = "java";
        String b = new String("java");
        System.out.println(a == b);            // false (different objects)
        System.out.println(a.equals(b));       // true

        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= 3; i++) {
            sb.append("Item ").append(i).append('\n');
        }
        System.out.print(sb);

        System.out.printf("%s is %d and costs %.2f%n", name, age, price);
    }
}

Control Flow: Conditions, Loops, and Switch Expressions

  • if/else, for, enhanced for, while, and do...while behave as in other C-style languages.
  • The enhanced for loop iterates over arrays and collections.
  • Modern switch expressions use ->, return a value, and need no break.
  • Always use braces for blocks, even for single statements.

Loops and a switch expression

public class Flow {
    public static void main(String[] args) {
        int[] scores = {92, 74, 58};

        for (int score : scores) {
            String grade = switch (score / 10) {
                case 10, 9 -> "A";
                case 8, 7  -> "B";
                default    -> "Needs work";
            };
            System.out.println(score + " -> " + grade);
        }

        int i = 0;
        while (i < 3) {
            System.out.println("Attempt " + (++i));
        }
    }
}

Classes and Objects

A class defines fields and methods; an object is an instance of a class. Encapsulation keeps data and behavior together.

  • Make fields private and expose behavior through methods.
  • Constructors create valid objects; use this to refer to the current instance.
  • Use static for members that belong to the class, not an instance.
  • Override toString, equals, and hashCode thoughtfully for value-like classes.
  • Use access modifiers intentionally and keep class responsibilities focused.

A well-encapsulated class

public class BankAccount {
    private final String owner;
    private double balance;

    public BankAccount(String owner, double initialBalance) {
        if (initialBalance < 0) {
            throw new IllegalArgumentException("Balance cannot be negative");
        }
        this.owner = owner;
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        balance += amount;
    }

    public boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }

    public double getBalance() { return balance; }
    public String getOwner()   { return owner; }

    @Override
    public String toString() {
        return owner + ": " + balance;
    }
}

Inheritance, Interfaces, and Polymorphism

  • extends creates a subclass; Java supports single class inheritance.
  • An interface defines a contract that classes implement; a class can implement many.
  • Polymorphism lets code work with the interface type while each object supplies its own behavior.
  • Use @Override so the compiler checks your intent.
  • Prefer composition and interfaces over deep inheritance trees.
  • abstract classes provide partial implementations.

An interface with polymorphic implementations

import java.util.List;

interface Shape {
    double area();
    default String describe() {
        return getClass().getSimpleName() + " with area " + String.format("%.2f", area());
    }
}

class Circle implements Shape {
    private final double radius;
    Circle(double radius) { this.radius = radius; }
    @Override public double area() { return Math.PI * radius * radius; }
}

class Rectangle implements Shape {
    private final double width, height;
    Rectangle(double width, double height) { this.width = width; this.height = height; }
    @Override public double area() { return width * height; }
}

public class Shapes {
    public static void main(String[] args) {
        List<Shape> shapes = List.of(new Circle(2), new Rectangle(3, 4));
        for (Shape s : shapes) {
            System.out.println(s.describe());
        }
    }
}

Records and Enums

  • A record is a compact, immutable data carrier with generated constructor, accessors, equals, hashCode, and toString.
  • An enum defines a fixed set of constants and can have fields and methods.
  • Records can validate input in a compact constructor.

A record and an enum

public class Models {
    record Point(int x, int y) {
        Point {
            if (x < 0 || y < 0) throw new IllegalArgumentException("Negative coordinate");
        }
        double distanceFromOrigin() { return Math.sqrt(x * x + y * y); }
    }

    enum Level {
        LOW(1), MEDIUM(5), HIGH(10);
        private final int weight;
        Level(int weight) { this.weight = weight; }
        int weight() { return weight; }
    }

    public static void main(String[] args) {
        Point p = new Point(3, 4);
        System.out.println(p + " -> " + p.distanceFromOrigin());   // Point[x=3, y=4] -> 5.0
        System.out.println(Level.HIGH.weight());
    }
}

Collections and Generics

The Java Collections Framework provides reusable data structures. Generics express which types a collection contains and catch mistakes at compile time.

  • **List (ArrayList):** ordered, allows duplicates, index access.
  • **Set (HashSet, TreeSet):** unique elements; TreeSet keeps them sorted.
  • **Map (HashMap, TreeMap):** key-value pairs.
  • **Deque (ArrayDeque):** stack or queue behavior.
  • Program to interfaces: declare List<String> items = new ArrayList<>();.
  • List.of, Set.of, and Map.of create immutable collections.

Working with List, Set, and Map

import java.util.*;

public class Collections1 {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Asha", "Ravi"));
        names.add("Meera");
        names.sort(Comparator.naturalOrder());
        System.out.println(names);

        Set<Integer> unique = new TreeSet<>(List.of(3, 1, 3, 2));   // [1, 2, 3]
        System.out.println(unique);

        Map<String, Integer> ages = new HashMap<>();
        ages.put("Asha", 21);
        ages.merge("Asha", 1, Integer::sum);          // 22
        ages.putIfAbsent("Ravi", 19);

        for (Map.Entry<String, Integer> e : ages.entrySet()) {
            System.out.println(e.getKey() + " -> " + e.getValue());
        }
    }
}

Lambdas and the Streams API

  • A lambda is a short anonymous function: (a, b) -> a + b.
  • Streams process collections with a pipeline: source, intermediate operations, terminal operation.
  • Common operations: filter, map, sorted, limit, collect, reduce, count.
  • Streams do not modify the source collection.
  • Use a simple loop when it is clearer than a stream.

Filtering, mapping, and grouping with streams

import java.util.*;
import java.util.stream.Collectors;

public class StreamsDemo {
    record Employee(String name, String dept, double salary) {}

    public static void main(String[] args) {
        List<Employee> staff = List.of(
            new Employee("Asha", "Engineering", 90000),
            new Employee("Ravi", "Engineering", 80000),
            new Employee("Meera", "Design", 70000)
        );

        List<String> highEarners = staff.stream()
            .filter(e -> e.salary() >= 80000)
            .map(Employee::name)
            .sorted()
            .toList();
        System.out.println(highEarners);           // [Asha, Ravi]

        Map<String, Double> avgByDept = staff.stream()
            .collect(Collectors.groupingBy(Employee::dept,
                     Collectors.averagingDouble(Employee::salary)));
        System.out.println(avgByDept);
    }
}

Exception Handling

  • Checked exceptions (such as IOException) must be caught or declared; they signal recoverable problems.
  • Unchecked exceptions (subclasses of RuntimeException) signal programming errors such as bad arguments.
  • Catch specific exception types where recovery is possible; avoid empty catch blocks that hide failures.
  • Use try-with-resources so files and connections are closed automatically.
  • Create custom exceptions for meaningful domain failures.
  • Preserve the cause when wrapping: throw new X("message", cause).

try-with-resources and a custom exception

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class Exceptions {
    static class ConfigException extends Exception {
        ConfigException(String message, Throwable cause) { super(message, cause); }
    }

    static String readFirstLine(Path path) throws ConfigException {
        try (BufferedReader reader = Files.newBufferedReader(path)) {
            return reader.readLine();
        } catch (IOException e) {
            throw new ConfigException("Could not read " + path, e);
        }
    }

    public static void main(String[] args) {
        try {
            System.out.println(readFirstLine(Path.of("config.txt")));
        } catch (ConfigException e) {
            System.err.println(e.getMessage() + " (" + e.getCause().getClass().getSimpleName() + ")");
        } finally {
            System.out.println("Done");
        }
    }
}

Packages, Build Tools, and Testing

  • Packages group related classes: package com.example.app;.
  • Use Maven or Gradle to manage dependencies and builds.
  • Use JUnit 5 for unit tests.
  • Keep one top-level public class per file, named after the file.

A simple JUnit 5 test

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class BankAccountTest {
    @Test
    void depositIncreasesBalance() {
        BankAccount account = new BankAccount("Asha", 100);
        account.deposit(50);
        assertEquals(150, account.getBalance(), 0.0001);
    }

    @Test
    void negativeDepositIsRejected() {
        BankAccount account = new BankAccount("Asha", 100);
        assertThrows(IllegalArgumentException.class, () -> account.deposit(-5));
    }
}

Best Practices

  • Keep fields private and objects valid after construction.
  • Prefer immutability: final fields, records, and immutable collections.
  • Program to interfaces and use generics instead of raw types.
  • Use equals, not ==, for object comparison.
  • Never swallow exceptions silently; log or rethrow with context.
  • Use try-with-resources for anything that must be closed.
  • Write unit tests and keep methods short and focused.
  • Follow common naming conventions: CamelCase classes, camelCase methods and variables.

Complete Example: Counting Words in a List

Save the code as Main.java. With a recent JDK, compile using javac Main.java and run with java Main.

Count words in a list

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        List<String> languages = List.of(
            "Java", "Python", "Java", "C++", "Python", "Java"
        );

        Map<String, Integer> counts = new HashMap<>();
        for (String language : languages) {
            counts.merge(language, 1, Integer::sum);
        }

        counts.forEach((language, count) ->
            System.out.println(language + ": " + count)
        );
    }
}

Frequently Asked Questions

Is Java platform independent?

Java bytecode can run on platforms that provide a compatible JVM, though native dependencies and environment details can affect portability.

What is the JVM?

The Java Virtual Machine executes Java bytecode and provides runtime services such as memory management and garbage collection.

Why use generics?

Generics express type expectations for classes and collections and reduce the need for unsafe casts.

What is the difference between == and equals in Java?

== compares primitive values or whether two references point to the same object, while equals compares the logical content of objects when a class overrides it, as String does.

What is the difference between checked and unchecked exceptions?

Checked exceptions must be caught or declared and represent recoverable conditions such as IOException. Unchecked exceptions extend RuntimeException and usually indicate programming errors.

Should I learn Java or Python first?

Python has gentler syntax for beginners, while Java teaches strong typing and object-oriented structure. Pick the one that matches your goals, such as backend or Android work for Java.

← Back to All Articles