</>
CodeLearn Pro
Start Learning Free โ†’

Java โ€” Complete Beginner Tutorial

6 modules ยท 17 examples ยท Click โ–ถ Run on any example to see the output

โœฆ Beginnerโ† Overview

๐Ÿ’ก How to use this tutorial

Read each explanation, study the code, then click โ–ถ Run to see the output.

Module 01

Variables & Data Types

Every Java program lives inside a class, and execution starts at the main() method. Java is a statically typed language โ€” every variable must have a declared type. There are two categories of types: primitive types (int, double, boolean, char) that store values directly, and reference types (String, arrays, objects) that store references to objects in memory.

Primitive Data Types

Java has 8 primitive types. The most commonly used are int (whole numbers), double (decimal numbers), boolean (true/false), and char (a single character). Primitives are stored directly in memory and are not objects โ€” they don't have methods. They are fast and efficient.

.java
public class DataTypes {
    public static void main(String[] args) {
        // Integer types
        int age = 25;
        long population = 8_000_000_000L; // L suffix for long

        // Floating point
        double price = 9.99;
        float rating = 4.5f;             // f suffix for float

        // Boolean
        boolean isStudent = true;
        boolean hasPassed = false;

        // Character (single quotes)
        char grade = 'A';

        // Print them
        System.out.println("Age: " + age);
        System.out.println("Population: " + population);
        System.out.println("Price: " + price);
        System.out.println("Rating: " + rating);
        System.out.println("Student: " + isStudent);
        System.out.println("Grade: " + grade);

        // Arithmetic
        int sum = age + 10;
        double tax = price * 0.15;
        System.out.println("Sum: " + sum);
        System.out.printf("Tax: %.2f%n", tax);
    }
}

Java uses double by default for decimal literals and int for whole numbers. Use the L suffix for long literals and f for float. The underscore in 8_000_000_000L is purely cosmetic โ€” Java ignores it, but it makes big numbers readable.

Strings and String Methods

In Java, String is a class (not a primitive). Strings are immutable โ€” once created, their content cannot be changed. Java provides a rich String API with dozens of useful methods. String concatenation uses + and is very common in Java code. For building strings dynamically, StringBuilder is much more efficient.

.java
public class Strings {
    public static void main(String[] args) {
        String name = "Alice Smith";
        String lang = "Java";

        // Concatenation
        String greeting = "Hello, " + name + "!";
        System.out.println(greeting);

        // String methods
        System.out.println(name.length());          // 11
        System.out.println(name.toUpperCase());     // ALICE SMITH
        System.out.println(name.toLowerCase());     // alice smith
        System.out.println(name.contains("Alice")); // true
        System.out.println(name.startsWith("Ali")); // true
        System.out.println(name.replace("Alice", "Bob")); // Bob Smith
        System.out.println(name.trim());            // removes whitespace
        System.out.println(name.charAt(0));         // A
        System.out.println(name.substring(6));      // Smith

        // String formatting
        String info = String.format("Name: %s, Lang: %s", name, lang);
        System.out.println(info);

        // StringBuilder โ€” efficient string building
        StringBuilder sb = new StringBuilder();
        sb.append("Hello");
        sb.append(", ");
        sb.append(name);
        sb.append("!");
        System.out.println(sb.toString());
    }
}

Never compare Strings with == in Java โ€” use .equals() instead. The == operator checks if two variables point to the same object in memory, not if their contents are the same. String.equals("hello") compares the actual characters.

Variables, Constants, and Type Casting

Java variables must be declared before use. The final keyword makes a variable a constant โ€” its value cannot be changed after assignment. Type casting lets you convert between numeric types. Widening conversions (int to double) are automatic; narrowing conversions (double to int) require an explicit cast.

.java
public class Variables {
    // Class-level constant (convention: ALL_CAPS)
    static final double PI = 3.14159;
    static final int MAX_SCORE = 100;

    public static void main(String[] args) {
        // Local variables โ€” must be initialised before use
        int x = 10;
        int y = 3;

        // Integer division vs floating point
        int divInt = x / y;        // 3 (truncates)
        double divDouble = (double) x / y; // 3.333...

        System.out.println("Int division: " + divInt);
        System.out.printf("Double division: %.3f%n", divDouble);

        // Widening cast (automatic)
        int score = 85;
        double scoreDouble = score; // int -> double, no cast needed

        // Narrowing cast (explicit)
        double pi = 3.99;
        int piInt = (int) pi;      // truncates to 3

        System.out.println("Score as double: " + scoreDouble);
        System.out.println("Pi truncated: " + piInt);
        System.out.println("Constant PI: " + PI);
        System.out.println("Max score: " + MAX_SCORE);

        // var (Java 10+) โ€” type inferred by compiler
        var message = "Hello Java!";
        var count = 42;
        System.out.println(message + " Count: " + count);
    }
}

The var keyword (Java 10+) lets the compiler infer the type, similar to how Python and JavaScript work. It is useful for reducing verbosity, but the variable is still strongly typed โ€” var just means "figure out the type for me". You cannot reassign a var to a different type.

Module 02

Control Flow

Java has the same control flow tools as most C-family languages. if/else handles decisions, switch handles multiple fixed conditions, and Java has three loop types: for (when you know the count), while (when you check a condition first), and do-while (when you always run at least once). Java 14+ also has an enhanced switch expression with arrow syntax.

if / else if / else

Java if statements require parentheses around the condition and curly braces around the body (curly braces are optional for single statements but always recommended). You can chain multiple conditions with else if. Java also has a ternary operator for compact one-line conditionals.

.java
public class ControlFlow {
    public static void main(String[] args) {
        int score = 73;

        // Basic if / else if / else
        if (score >= 90) {
            System.out.println("Grade: A");
        } else if (score >= 80) {
            System.out.println("Grade: B");
        } else if (score >= 70) {
            System.out.println("Grade: C");
        } else if (score >= 60) {
            System.out.println("Grade: D");
        } else {
            System.out.println("Grade: F");
        }

        // Ternary operator โ€” compact if/else
        String result = score >= 60 ? "PASS" : "FAIL";
        System.out.println("Result: " + result);

        // Logical operators
        boolean isWeekend = true;
        boolean isSunny = false;

        if (isWeekend && isSunny) {
            System.out.println("Go to the beach!");
        } else if (isWeekend || isSunny) {
            System.out.println("Could be a nice day.");
        } else {
            System.out.println("Back to work.");
        }

        // Checking multiple values with && and ||
        int age = 20;
        if (age >= 18 && age <= 65) {
            System.out.println("Working age group.");
        }
    }
}

Always use curly braces {} even for single-line if bodies. Without them, adding a second line to what looks like an if body will silently break your logic โ€” the second line runs regardless of the condition. This is a very common bug.

switch and switch Expressions

The classic switch statement tests a value against multiple cases. Each case needs a break to prevent "fall-through" (where execution continues into the next case). Java 14+ introduced switch expressions with arrow syntax โ€” no fall-through, cleaner code, and they return a value.

.java
public class SwitchDemo {
    public static void main(String[] args) {
        // Classic switch statement
        int day = 3;
        String dayName;
        switch (day) {
            case 1:
                dayName = "Monday";
                break;
            case 2:
                dayName = "Tuesday";
                break;
            case 3:
                dayName = "Wednesday";
                break;
            case 4:
                dayName = "Thursday";
                break;
            case 5:
                dayName = "Friday";
                break;
            default:
                dayName = "Weekend";
        }
        System.out.println("Day: " + dayName);

        // Modern switch expression (Java 14+)
        String season = "WINTER";
        String activity = switch (season) {
            case "SPRING" -> "Go hiking";
            case "SUMMER" -> "Go swimming";
            case "AUTUMN" -> "Jump in leaves";
            case "WINTER" -> "Stay inside";
            default       -> "Unknown season";
        };
        System.out.println("Activity: " + activity);

        // switch on String
        String lang = "Java";
        switch (lang) {
            case "Java":
            case "Kotlin":
                System.out.println("JVM language");
                break;
            case "Python":
                System.out.println("Scripting language");
                break;
            default:
                System.out.println("Other language");
        }
    }
}

In the classic switch, forgetting break causes fall-through โ€” execution continues into the next case until it hits a break or the end of the switch. Sometimes fall-through is intentional (like the Java/Kotlin case above), but usually it is a bug. The modern arrow switch avoids this entirely.

for, while, and do-while Loops

Java has three loop types. The classic for loop is best when you know the number of iterations. The enhanced for-each loop is cleanest for iterating over arrays and collections. while loops check the condition before each iteration. do-while always runs at least once.

.java
public class Loops {
    public static void main(String[] args) {
        // Classic for loop
        System.out.println("Counting up:");
        for (int i = 1; i <= 5; i++) {
            System.out.print(i + " ");
        }
        System.out.println();

        // Enhanced for-each (best for arrays/collections)
        String[] fruits = {"Apple", "Banana", "Cherry"};
        System.out.println("Fruits:");
        for (String fruit : fruits) {
            System.out.println("  - " + fruit);
        }

        // while loop
        System.out.println("while loop:");
        int n = 1;
        while (n <= 4) {
            System.out.print(n + " ");
            n++;
        }
        System.out.println();

        // do-while โ€” always runs at least once
        System.out.println("do-while:");
        int count = 0;
        do {
            System.out.print(count + " ");
            count++;
        } while (count < 3);
        System.out.println();

        // break and continue
        System.out.println("break/continue:");
        for (int i = 0; i < 10; i++) {
            if (i == 3) continue; // skip 3
            if (i == 7) break;    // stop at 7
            System.out.print(i + " ");
        }
    }
}

Prefer the enhanced for-each loop whenever you just need to read every element in an array or collection. It is cleaner, less error-prone, and removes the possibility of off-by-one errors. Use the classic for loop only when you need the index.

Module 03

Methods & Classes

A method is a named block of code that performs a task. Methods prevent repetition โ€” write once, call many times. In Java, every method lives inside a class. Methods have a return type (or void if they return nothing), a name, and zero or more parameters. Classes are blueprints for objects โ€” they bundle data (fields) and behaviour (methods) together.

Defining and Calling Methods

A method declaration specifies the return type, name, and parameters. static methods belong to the class itself (not an instance). void means the method returns nothing. When a method returns a value, use the return keyword. Java supports method overloading โ€” multiple methods with the same name but different parameter lists.

.java
public class Methods {

    // Static method โ€” belongs to the class
    static void greet(String name) {
        System.out.println("Hello, " + name + "!");
    }

    // Returns a value
    static int add(int a, int b) {
        return a + b;
    }

    // Method overloading โ€” same name, different parameters
    static double add(double a, double b) {
        return a + b;
    }

    // Method with multiple parameters
    static String formatScore(String player, int score, int max) {
        double percent = (double) score / max * 100;
        return String.format("%s: %d/%d (%.1f%%)", player, score, max, percent);
    }

    // Recursive method
    static int factorial(int n) {
        if (n <= 1) return 1;
        return n * factorial(n - 1);
    }

    public static void main(String[] args) {
        greet("Alice");
        greet("Bob");

        System.out.println("3 + 4 = " + add(3, 4));
        System.out.println("1.5 + 2.5 = " + add(1.5, 2.5));

        System.out.println(formatScore("Alice", 87, 100));
        System.out.println(formatScore("Bob", 63, 100));

        System.out.println("5! = " + factorial(5));
        System.out.println("10! = " + factorial(10));
    }
}

Method overloading lets you use the same method name for related operations. Java picks the right method based on the argument types at compile time. This is different from method overriding (which applies to inheritance and happens at runtime).

Classes and Objects

A class is a blueprint. An object is an instance of that blueprint. Classes have fields (variables that store data) and methods (functions that define behaviour). The constructor is a special method called when creating an object with new. The this keyword refers to the current object.

.java
// Define the class (blueprint)
class Person {
    // Fields
    String name;
    int age;
    String city;

    // Constructor โ€” runs when object is created
    Person(String name, int age, String city) {
        this.name = name;  // this.name = field, name = parameter
        this.age = age;
        this.city = city;
    }

    // Methods
    void introduce() {
        System.out.println("Hi! I am " + name +
            ", " + age + " years old, from " + city + ".");
    }

    boolean isAdult() {
        return age >= 18;
    }

    String getStatus() {
        return name + " is " + (isAdult() ? "an adult" : "a minor");
    }
}

public class ClassDemo {
    public static void main(String[] args) {
        // Create objects with new
        Person alice = new Person("Alice", 25, "Johannesburg");
        Person bob = new Person("Bob", 16, "Cape Town");

        // Call methods on objects
        alice.introduce();
        bob.introduce();

        System.out.println(alice.getStatus());
        System.out.println(bob.getStatus());

        // Access fields directly
        System.out.println(alice.name + " is " + alice.age);

        // Objects are independent
        alice.age = 26;
        System.out.println("After birthday: " + alice.age);
        System.out.println("Bob unchanged: " + bob.age);
    }
}

Each object created with new gets its own copy of the fields. Changing alice.age does not affect bob.age. This is the fundamental concept of objects โ€” independent instances of the same blueprint, each with their own state.

Module 04

Object-Oriented Programming

OOP has four pillars: Encapsulation (hiding data behind methods), Inheritance (classes extending other classes), Polymorphism (same interface, different behaviour), and Abstraction (hiding complexity). Java is built around these concepts. The access modifiers private, protected, and public control visibility. The extends keyword creates inheritance. Interfaces define contracts.

Encapsulation โ€” Getters and Setters

Encapsulation means hiding a class's fields from outside access and providing controlled access through public getter and setter methods. Fields are declared private, then public methods expose them safely. This lets you add validation, logging, or computed values without changing the external interface.

.java
class BankAccount {
    private String owner;     // private โ€” not directly accessible
    private double balance;   // private โ€” protected from direct change

    BankAccount(String owner, double initialBalance) {
        this.owner = owner;
        this.balance = initialBalance >= 0 ? initialBalance : 0;
    }

    // Getter โ€” read-only access to balance
    public double getBalance() {
        return balance;
    }

    public String getOwner() {
        return owner;
    }

    // Methods with validation
    public void deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Deposit must be positive.");
            return;
        }
        balance += amount;
        System.out.printf("Deposited R%.2f. Balance: R%.2f%n", amount, balance);
    }

    public void withdraw(double amount) {
        if (amount <= 0) {
            System.out.println("Withdrawal must be positive.");
        } else if (amount > balance) {
            System.out.println("Insufficient funds.");
        } else {
            balance -= amount;
            System.out.printf("Withdrew R%.2f. Balance: R%.2f%n", amount, balance);
        }
    }
}

public class EncapsDemo {
    public static void main(String[] args) {
        BankAccount acc = new BankAccount("Alice", 1000.00);
        System.out.println("Owner: " + acc.getOwner());
        System.out.printf("Balance: R%.2f%n", acc.getBalance());

        acc.deposit(500);
        acc.withdraw(200);
        acc.withdraw(2000); // Should fail
        acc.deposit(-50);   // Should fail
    }
}

Making fields private and providing getters/setters is called the JavaBeans convention. Many frameworks (Spring, Jackson, Hibernate) depend on this pattern to automatically read and write your class fields. It is standard Java style.

Inheritance and Polymorphism

Inheritance lets a class (subclass) inherit fields and methods from another class (superclass) using extends. The subclass can add new fields/methods and override existing ones. Polymorphism means you can treat a subclass object as its parent type โ€” Java calls the correct overridden method at runtime.

.java
// Superclass (parent)
class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    // This method will be overridden
    void makeSound() {
        System.out.println(name + " makes a sound.");
    }

    void describe() {
        System.out.println("I am " + name + " the " +
            getClass().getSimpleName());
    }
}

// Subclasses (children)
class Dog extends Animal {
    Dog(String name) {
        super(name); // call parent constructor
    }

    @Override
    void makeSound() {
        System.out.println(name + " says: Woof!");
    }
}

class Cat extends Animal {
    Cat(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " says: Meow!");
    }
}

public class InheritanceDemo {
    public static void main(String[] args) {
        Dog rex = new Dog("Rex");
        Cat whiskers = new Cat("Whiskers");

        rex.makeSound();       // Dog's version
        whiskers.makeSound();  // Cat's version

        rex.describe();        // Inherited from Animal
        whiskers.describe();   // Inherited from Animal

        // Polymorphism โ€” store as parent type
        Animal[] animals = { new Dog("Buddy"), new Cat("Luna") };
        System.out.println("\nAll animals speak:");
        for (Animal a : animals) {
            a.makeSound(); // Calls the correct subclass method
        }
    }
}

The @Override annotation tells the compiler you intend to override a parent method. If you misspell the method name, the compiler catches it immediately. Without @Override, a misspelling just creates a new method silently โ€” a very hard bug to find.

Interfaces and Abstraction

An interface is a contract โ€” it declares what methods a class must implement without specifying how. A class implements an interface using the implements keyword and must provide all the declared methods. A class can implement multiple interfaces, which Java uses instead of multiple inheritance.

.java
// Interface โ€” defines a contract
interface Drawable {
    void draw();                         // must implement
    default String getType() {           // default has implementation
        return "Shape";
    }
}

interface Resizable {
    void resize(double factor);
}

// Class implementing two interfaces
class Circle implements Drawable, Resizable {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.printf("Drawing circle with radius %.1f%n", radius);
    }

    @Override
    public void resize(double factor) {
        radius *= factor;
        System.out.printf("Circle resized. New radius: %.1f%n", radius);
    }
}

class Rectangle implements Drawable, Resizable {
    double width, height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public void draw() {
        System.out.printf("Drawing rectangle %.1f x %.1f%n", width, height);
    }

    @Override
    public void resize(double factor) {
        width *= factor;
        height *= factor;
        System.out.printf("Rectangle resized to %.1f x %.1f%n", width, height);
    }
}

public class InterfaceDemo {
    public static void main(String[] args) {
        Drawable[] shapes = { new Circle(5), new Rectangle(4, 3) };
        for (Drawable shape : shapes) {
            shape.draw();
            System.out.println("Type: " + shape.getType());
        }

        Circle c = new Circle(10);
        c.resize(0.5);
        c.draw();
    }
}

Interfaces define what, classes define how. Program to interfaces, not implementations โ€” store objects as their interface type (Drawable shape = new Circle(5)) rather than the concrete type. This makes your code flexible and easy to change.

Module 05

Collections & Generics

Java arrays have a fixed size. The Collections framework provides dynamic, flexible data structures. The most important ones are ArrayList (dynamic array), HashMap (key-value store), HashSet (unique values), and LinkedList (fast insertion/removal). Generics let you specify the type stored in a collection: ArrayList<String> means a list that can only hold Strings โ€” the compiler enforces this.

ArrayList โ€” Dynamic Lists

ArrayList is like an array that grows automatically. You add items with add(), access by index with get(), remove with remove(), and check the size with size(). The <String> in ArrayList<String> is the generic type parameter โ€” it means this list can only hold Strings, catching type errors at compile time.

.java
import java.util.ArrayList;
import java.util.Collections;

public class ArrayListDemo {
    public static void main(String[] args) {
        // Create an ArrayList of Strings
        ArrayList<String> languages = new ArrayList<>();

        // Adding elements
        languages.add("Java");
        languages.add("Python");
        languages.add("Go");
        languages.add("Rust");
        languages.add("JavaScript");

        System.out.println("Languages: " + languages);
        System.out.println("Size: " + languages.size());

        // Access by index
        System.out.println("First: " + languages.get(0));
        System.out.println("Last: " + languages.get(languages.size() - 1));

        // Check if contains
        System.out.println("Has Java? " + languages.contains("Java"));

        // Remove by value
        languages.remove("Go");
        System.out.println("After removing Go: " + languages);

        // Sort alphabetically
        Collections.sort(languages);
        System.out.println("Sorted: " + languages);

        // Iterate with for-each
        System.out.println("All languages:");
        for (String lang : languages) {
            System.out.println("  " + lang);
        }

        // ArrayList of integers
        ArrayList<Integer> scores = new ArrayList<>();
        scores.add(85);
        scores.add(92);
        scores.add(78);
        System.out.println("Max score: " + Collections.max(scores));
    }
}

ArrayList<Integer> uses Integer (capital I), not int. Java cannot use primitives in generics, so it uses wrapper classes: Integer for int, Double for double, Boolean for boolean. Java auto-converts between them (autoboxing), so scores.add(85) works even though 85 is a primitive.

HashMap โ€” Key-Value Pairs

HashMap stores data as key-value pairs, like a dictionary. Each key maps to exactly one value. Keys must be unique. Access is extremely fast โ€” O(1) average. Use HashMap when you need to look up values by a label rather than by position.

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

public class HashMapDemo {
    public static void main(String[] args) {
        // HashMap<KeyType, ValueType>
        HashMap<String, Integer> scores = new HashMap<>();

        // Adding key-value pairs
        scores.put("Alice", 92);
        scores.put("Bob", 78);
        scores.put("Carol", 88);
        scores.put("Dave", 95);

        System.out.println("All scores: " + scores);
        System.out.println("Size: " + scores.size());

        // Retrieve by key
        System.out.println("Alice's score: " + scores.get("Alice"));

        // Check if key exists
        System.out.println("Has Eve? " + scores.containsKey("Eve"));

        // Default value if key not found
        int eveScore = scores.getOrDefault("Eve", 0);
        System.out.println("Eve's score (default): " + eveScore);

        // Update a value
        scores.put("Bob", 82); // overwrites existing
        System.out.println("Bob updated: " + scores.get("Bob"));

        // Remove
        scores.remove("Dave");

        // Iterate over entries
        System.out.println("\nFinal scores:");
        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            String grade = entry.getValue() >= 90 ? "A" : "B";
            System.out.printf("  %s: %d (%s)%n",
                entry.getKey(), entry.getValue(), grade);
        }

        // word count example
        String sentence = "java is great and java is fast";
        HashMap<String, Integer> wordCount = new HashMap<>();
        for (String word : sentence.split(" ")) {
            wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
        }
        System.out.println("\nWord count: " + wordCount);
    }
}

HashMap does not guarantee insertion order. If you need insertion order, use LinkedHashMap. If you need sorted keys, use TreeMap. For most use cases, HashMap is the right choice โ€” it is the fastest.

Module 06

Exceptions & Practical Java

Exceptions are Java's way of handling errors at runtime. Instead of crashing, you catch the exception and handle it gracefully. The try/catch/finally block is the core mechanism. Java also has two categories: checked exceptions (you must handle them) and unchecked exceptions (runtime errors you can optionally handle). This module also covers arrays, the Scanner for user input, and putting it all together.

try / catch / finally

Wrap risky code in a try block. If an exception occurs, execution jumps to the matching catch block. The finally block always runs โ€” whether an exception occurred or not. This is used for cleanup like closing files or database connections.

.java
public class ExceptionsDemo {
    // Method that throws a checked exception
    static double divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Cannot divide by zero!");
        }
        return (double) a / b;
    }

    static int parseAge(String input) {
        return Integer.parseInt(input); // throws NumberFormatException
    }

    public static void main(String[] args) {
        // Basic try/catch
        try {
            double result = divide(10, 2);
            System.out.println("10 / 2 = " + result);

            double bad = divide(5, 0); // throws exception
            System.out.println("This won't print");
        } catch (ArithmeticException e) {
            System.out.println("Math error: " + e.getMessage());
        }

        // Multiple catch blocks
        String[] inputs = {"25", "abc", null};
        for (String input : inputs) {
            try {
                int age = parseAge(input);
                System.out.println("Parsed age: " + age);
            } catch (NumberFormatException e) {
                System.out.println("Not a number: " + input);
            } catch (NullPointerException e) {
                System.out.println("Input was null!");
            }
        }

        // finally โ€” always runs
        System.out.println("\nWith finally:");
        try {
            System.out.println("Trying...");
            int[] arr = new int[3];
            arr[10] = 5; // ArrayIndexOutOfBoundsException
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("Array error: " + e.getMessage());
        } finally {
            System.out.println("Finally always runs.");
        }
    }
}

Catch the most specific exception type first, most general last. Catching Exception (the base class) too early can hide bugs. A good rule: only catch exceptions you know how to handle. If you cannot handle it meaningfully, let it propagate up to where it can be handled.

Arrays and Useful Patterns

Arrays in Java have a fixed size set at creation. They are zero-indexed. The Arrays utility class provides sorting, searching, and printing. Knowing how to work with arrays is fundamental โ€” they are the basis for all Java collections.

.java
import java.util.Arrays;

public class ArraysDemo {
    // Find maximum in an array
    static int findMax(int[] arr) {
        int max = arr[0];
        for (int num : arr) {
            if (num > max) max = num;
        }
        return max;
    }

    // Calculate average
    static double average(int[] arr) {
        int sum = 0;
        for (int num : arr) sum += num;
        return (double) sum / arr.length;
    }

    public static void main(String[] args) {
        // Declare and initialise
        int[] scores = {85, 92, 78, 95, 88, 73, 90};

        System.out.println("Scores: " + Arrays.toString(scores));
        System.out.println("Length: " + scores.length);
        System.out.println("Max: " + findMax(scores));
        System.out.printf("Average: %.1f%n", average(scores));

        // Sort
        Arrays.sort(scores);
        System.out.println("Sorted: " + Arrays.toString(scores));

        // 2D array
        int[][] grid = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };
        System.out.println("\n3x3 Grid:");
        for (int[] row : grid) {
            for (int val : row) {
                System.out.printf("%3d", val);
            }
            System.out.println();
        }

        // String array
        String[] names = {"Charlie", "Alice", "Bob"};
        Arrays.sort(names);
        System.out.println("\nSorted names: " + Arrays.toString(names));
        System.out.println("Found Alice at: " + Arrays.binarySearch(names, "Alice"));
    }
}

Arrays.binarySearch() requires the array to be sorted first โ€” otherwise results are unpredictable. For searching an unsorted array, use a simple for loop. For dynamic lists that grow and shrink, always prefer ArrayList over arrays.

Putting It All Together โ€” Student Grade System

This final example combines classes, ArrayList, HashMap, methods, and exception handling into a mini grade management system. Study how the pieces connect โ€” this is the kind of structure you will write in real Java programs.

.java
import java.util.ArrayList;
import java.util.HashMap;

class Student {
    private String name;
    private ArrayList<Integer> grades;

    Student(String name) {
        this.name = name;
        this.grades = new ArrayList<>();
    }

    void addGrade(int grade) {
        if (grade < 0 || grade > 100) {
            throw new IllegalArgumentException(
                "Grade must be 0-100, got: " + grade);
        }
        grades.add(grade);
    }

    double getAverage() {
        if (grades.isEmpty()) return 0;
        int sum = 0;
        for (int g : grades) sum += g;
        return (double) sum / grades.size();
    }

    String getLetterGrade() {
        double avg = getAverage();
        if (avg >= 90) return "A";
        if (avg >= 80) return "B";
        if (avg >= 70) return "C";
        if (avg >= 60) return "D";
        return "F";
    }

    String getName() { return name; }

    @Override
    public String toString() {
        return String.format("%s: avg=%.1f (%s)",
            name, getAverage(), getLetterGrade());
    }
}

public class GradeSystem {
    public static void main(String[] args) {
        ArrayList<Student> students = new ArrayList<>();

        Student alice = new Student("Alice");
        alice.addGrade(92); alice.addGrade(88); alice.addGrade(95);

        Student bob = new Student("Bob");
        bob.addGrade(74); bob.addGrade(68); bob.addGrade(71);

        Student carol = new Student("Carol");
        carol.addGrade(55); carol.addGrade(62); carol.addGrade(58);

        students.add(alice);
        students.add(bob);
        students.add(carol);

        System.out.println("=== Grade Report ===");
        for (Student s : students) {
            System.out.println(s);
        }

        // Count grades
        HashMap<String, Integer> gradeCounts = new HashMap<>();
        for (Student s : students) {
            String letter = s.getLetterGrade();
            gradeCounts.put(letter, gradeCounts.getOrDefault(letter, 0) + 1);
        }
        System.out.println("\nGrade distribution: " + gradeCounts);

        // Exception handling
        try {
            alice.addGrade(150); // invalid
        } catch (IllegalArgumentException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

Notice how this example uses every concept from the tutorial: a class with private fields and public methods, ArrayList to store dynamic data, HashMap to aggregate results, a custom exception with a meaningful message, and String.format for clean output. Real Java programs are just compositions of these same patterns.

You finished the Java tutorial!

You now know Java's type system, control flow, methods, object-oriented programming, collections, and exception handling. These are the core building blocks of every real Java application.