Java4FHDo Learning Guide

Java 21 · Chapters 1–10 · Chapter 1 ready

Learn how Java actually works

An expanded, visual version of the Java4FHDo course. Every lesson explains the problem first, then the idea, then the code — with labs you can click through, answers you can reveal, and a self-check that remembers your progress in this browser.

Essential needed to continue Deeper look optional background Correction fixes the original course Runnable program complete file, tested on OpenJDK 21.0.10 Snippet goes inside a method

Roadmap

The course has two halves. Chapters 1–5 build the language foundation. Chapters 6–10 use that foundation to write modern, safe, maintainable software.

flowchart LR
    C1["1 · Java Basics<br/>how programs run"] --> C2["2 · OOP I<br/>classes & objects"]
    C2 --> C3["3 · OOP II<br/>inheritance & interfaces"]
    C3 --> C4["4 · Exceptions & I/O<br/>when things go wrong"]
    C3 --> C5["5 · Collections & Generics<br/>storing many objects"]
    C4 --> C5
    C5 --> C6["6 · Lambdas & Streams<br/>processing data declaratively"]
    C6 --> C7["7 · Concurrency<br/>doing things at the same time"]
    C3 --> C8["8 · Design Patterns<br/>structuring larger programs"]
    C6 --> C8
    C8 --> C9["9 · Reflection & Modern Java<br/>records, sealed types"]
    C8 --> C10["10 · Testing & Clean Code<br/>proving it works"]
    C9 --> C10

Arrows mean "builds on". For example, Chapter 6 needs the interfaces from Chapter 3 and the collections from Chapter 5.

Ch. Topic Big question it answers What the course app gains
1 Java Basics How does Java code become a running program? A grade report built from arrays
2 OOP I How do I bundle data with the rules that protect it? A Student class with validated grades
3 OOP II How do I reuse and swap behavior? PersonStudent / Lecturer, a Gradable interface
4 Exceptions & I/O How do I handle failures and save data? Load and save students from a CSV file
5 Collections & Generics How do I store and find many objects safely? List, Set, Map of students and courses
6 Lambdas & Streams How do I filter, transform, and summarize data clearly? Rankings and statistics with streams
7 Concurrency How do I do several things at once without corrupting data? Parallel grade imports with a thread-safe counter
8 Design Patterns How do I organize code so it stays easy to change? Strategies for grading, observers for notifications
9 Reflection & Modern Java How do frameworks inspect code? What do records and sealed types add? Records for immutable data, sealed result types
10 Testing & Clean Code How do I prove my code works and keep it readable? A Maven/Gradle project with JUnit 5 tests

The recurring example. We build a small course-management app step by step. It has students, courses, and grades. It uses the German grading scale that the course uses: 1.0 is the best grade, 4.0 is the lowest pass, and 5.0 is a fail. Each chapter adds only what that chapter teaches.

The chapter template. Every chapter ends with the same revision block: recap → glossary → syntax reference → exercises → predict the output → solutions → self-check.

What you need. Install a JDK 21 (for example Eclipse Temurin or Oracle JDK). Check it in a terminal:

Terminal / output
java -version
javac -version

Both should report version 21 or later.


Chapter 1: Java Basics

Learning objectives (from the course)

What this chapter adds

The course's version of this chapter is short. This version also covers the vocabulary it assumes: variables, types, operators, arrays, methods, and strings. The examples later in the course use all of these without introducing them.

Chapter map

flowchart TD
    A["1.1 From source code to running program"] --> B["1.2 Anatomy of a Java program"]
    B --> C["1.3 Variables, types, and operators"]
    C --> D["1.4 Primitive types vs. reference types"]
    D --> E["1.5 Making decisions: if / else, switch"]
    E --> F["1.6 Repeating work: loops"]
    F --> G["1.7 Arrays and methods"]
    G --> H["1.8 Working with Strings"]
    H --> I["1.9 Course app v1: a grade report"]

1.1 From source code to running program Essential

The problem

A computer's processor understands only machine code, which is long sequences of numbers that are specific to one kind of processor. Code written for an Intel laptop does not run on an ARM phone. Programmers want to write a program once, in readable text, and run it on many kinds of machines.

Intuition

Imagine you write a recipe in a simple, standard "kitchen language." Every kitchen in the world has an interpreter who reads that standard language and carries it out with the local equipment. You write the recipe once. Each kitchen's interpreter handles the local details.

Where the analogy stops working: a real JVM does not only read bytecode step by step. It also watches which parts of your program run most often and translates them into fast machine code while the program is running. This is called just-in-time (JIT) compilation.

How it works

Some terms first:

The steps:

  1. You write HelloWorld.java.
  2. javac checks it for errors, such as wrong types or missing semicolons. If it finds any, it stops and reports them. These are compile-time errors.
  3. If the code is valid, javac writes HelloWorld.class, which contains bytecode.
  4. You run java HelloWorld. The JVM starts, loads the class, checks the bytecode, and begins executing main.
  5. While the program runs, the JVM interprets the bytecode and JIT-compiles frequently used parts. Problems that appear only at this stage, such as dividing by zero, are runtime errors.

Visual explanation

flowchart LR
    SRC["HelloWorld.java<br/>(source code, text)"] -- "javac compiles" --> BC["HelloWorld.class<br/>(bytecode)"]
    BC -- "same file copied to" --> W["JVM on Windows / x86"]
    BC -- "same file copied to" --> M["JVM on macOS / ARM"]
    BC -- "same file copied to" --> L["JVM on Linux / x86"]
    W -- "interprets + JIT-compiles" --> OUT1["Machine code<br/>for x86"]
    M -- "interprets + JIT-compiles" --> OUT2["Machine code<br/>for ARM"]
    L -- "interprets + JIT-compiles" --> OUT3["Machine code<br/>for x86"]

What to notice: the .class file is the same everywhere. The JVMs are different, because each JVM is built for its own operating system and processor. So "platform-independent" really means that your bytecode is platform-independent. The JVM is not.

Try it

Where does the error stop the program?

Pick a version of HelloWorld and run it through the pipeline. Watch which stage catches the problem.


HelloWorld.javasource text
javac
HelloWorld.classbytecode
java
JVMloads & runs main
prints
Outputterminal
Press “Run pipeline”.

JDK, JRE, and JVM

Name Stands for What it contains Who needs it
JVM Java Virtual Machine The engine that runs bytecode Anything that runs Java
JRE Java Runtime Environment JVM + the standard class library Historically: people who only run Java programs
JDK Java Development Kit JRE + development tools (javac, jar, javadoc, debugger, …) Developers (you)
flowchart TB
    subgraph JDK["JDK: Java Development Kit"]
        TOOLS["Tools: javac, jar, javadoc, jshell, …"]
        subgraph JRE["Runtime (formerly shipped separately as the JRE)"]
            LIB["Standard library: String, Math, collections, …"]
            JVM["JVM: loads, verifies, and runs bytecode"]
        end
    end

Correction / update The course describes the JRE as something you install separately. Since JDK 11, Oracle no longer ships a separate JRE download; you install a JDK. Applications can bundle their own trimmed runtime instead (built with the jlink tool). "JRE" is still useful as a concept: the runtime part of the JDK.

Small example

Runnable program, file HelloWorld.java:

Java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, graduate students!");
    }
}

Compile and run in a terminal, from the folder that contains the file:

Terminal / output
javac HelloWorld.java      # creates HelloWorld.class
java HelloWorld            # runs it — note: class name, no ".class"

Expected output:

Terminal / output
Hello, graduate students!

Deeper look Shortcut (Java 11+). For a program in a single file, java HelloWorld.java compiles it in memory and runs it in one step. No .class file is written. This is handy for experiments. Use the two-step version whenever you want to see the compiler's work separately.

Deeper look Newer versions. Java 25 finalized "compact source files" (JEP 512). They let a beginner write void main() { IO.println("Hi"); } without a class. This guide uses Java 21, so we always write the full class.

Common mistakes

Mistake What you see Why Fix
File name doesn't match the public class error: class HelloWorld is public, should be declared in a file named HelloWorld.java Java requires a public top-level class to live in a file with exactly the same name. Upper and lower case count. Rename the file or the class so they match.
Running with the extension java HelloWorld.classCould not find or load main class HelloWorld.class java expects a class name, not a file name. java HelloWorld
Misspelled main Compiles fine, but running prints Error: Main method not found in class HelloWorld The JVM looks for a method with exactly this name and signature. public static void main(String[] args)

Quick check

  1. Which of these errors does javac report: a missing semicolon, or dividing by zero?
  2. If you copy HelloWorld.class to a friend's computer that has a different operating system, what does the friend need in order to run it?
Show answer
1. The missing semicolon. Division by zero only happens while the program runs. 2. A Java runtime, which in practice means an installed JDK of the same or newer version.

1.2 Anatomy of a Java program Essential

The problem

When the JVM starts, it needs to know where your program begins. Java also needs a way to organize code into named units.

How it works

Here is every word of HelloWorld:

Java
public class HelloWorld {                       // (1) (2) (3)
    public static void main(String[] args) {    // (4) (5) (6) (7) (8)
        System.out.println("Hello, graduate students!");  // (9)
    }                                           // end of main
}                                               // end of class
# Part Meaning
1 public An access modifier: code anywhere may use this class. (Chapter 2 covers private.)
2 class Declares a class. For now, a class is a named container for code. In Chapter 2 it also becomes a blueprint for objects.
3 HelloWorld { … } The class name. By convention class names use UpperCamelCase. The curly braces { } mark where the class begins and ends.
4 public The JVM must be able to call main from outside the class.
5 static The method belongs to the class itself, so the JVM can call it without first creating an object. (Explained fully in Chapter 2.)
6 void The method returns no value.
7 main The special name the JVM looks for. This is the entry point of the program.
8 String[] args A parameter: an array of text values passed on the command line. java HelloWorld a b gives args = {"a", "b"}.
9 System.out.println(...) Prints text followed by a line break. System is a built-in class, out is its standard output stream, and println means "print line". Every statement ends with ;.

Comments are notes for humans. The compiler ignores them.

Java
// a single-line comment
/* a comment
   over several lines */
/** a documentation comment (read by the javadoc tool) */

Visual explanation

flowchart TD
    START(["java HelloWorld"]) --> LOAD["JVM loads HelloWorld.class"]
    LOAD --> FIND{"Is there a<br/>public static void main(String[])?"}
    FIND -- "no" --> ERR["Error: Main method not found"]
    FIND -- "yes" --> RUN["Run statements in main,<br/>top to bottom"]
    RUN --> END(["main returns → program ends"])

1.3 Variables, types, and operators Essential

This section is a prerequisite that the original course skips.

The problem

Programs have to remember values, such as a grade, a count, or a name, and compute with them. Java also wants to catch mistakes like "add a name to a number" before the program runs.

Intuition

A variable is a labeled box. The type says what kind of thing may go in the box. A box labeled int accepts whole numbers only. If you try to put text in it, the compiler refuses.

Where the analogy stops working: for reference types (section 1.4), the box does not contain the object itself. It contains directions to where the object lives.

How it works

Declaring a variable creates the box. Assigning puts a value into it. Initializing means assigning a value for the first time.

Snippet:

Java
int count;                 // declaration: a box named count for whole numbers
count = 3;                 // assignment
double average = 2.43;     // declaration + initialization in one line
String name = "Anna";      // text
boolean passed = true;     // true or false
final int MAX_GRADE = 5;   // final: can be assigned only once (a constant)
var total = 10;            // Java 10+: the compiler infers the type (int) from the value

Java is statically typed. Every variable has a fixed type that the compiler knows before the program runs. Once count is an int, it stays an int.

The eight primitive types

Type Size Holds Example literal
byte 8 bits whole numbers −128 … 127 (byte) 10
short 16 bits whole numbers −32,768 … 32,767 (short) 1000
int 32 bits whole numbers ≈ ±2.1 billion 42, 1_000_000
long 64 bits very large whole numbers 3_000_000_000L (note the L)
float 32 bits decimal numbers, ~7 digits of precision 2.5f (note the f)
double 64 bits decimal numbers, ~15–16 digits of precision 2.5
boolean true / false true
char 16 bits a single UTF-16 character 'A' (single quotes)

In everyday code you mostly need int, double, boolean, and char, plus long for very large numbers.

Operators

Kind Operators Example Result
Arithmetic + - * / % 7 / 2, 7 % 2, 7.0 / 2 3, 1, 3.5
Comparison == != < <= > >= grade <= 2 true or false
Logical && (and), || (or), ! (not) g >= 1 && g <= 4 true if both are true
Assignment = += -= *= /= sum += v same as sum = sum + v
Increment ++ -- i++ adds 1 to i

% is the remainder operator. && and || are short-circuiting: if the left side already decides the result, the right side is not evaluated.

The two surprises every beginner meets

Integer division drops the fraction. When both sides of / are integers, the result is an integer and the part after the decimal point is thrown away:

Java
System.out.println(7 / 2);      // 3   (not 3.5)
System.out.println(7.0 / 2);    // 3.5 (one side is double → decimal division)

Numbers have limits and binary rounding.

Java
int big = Integer.MAX_VALUE;    // 2147483647
System.out.println(big + 1);    // -2147483648  (overflow wraps around silently!)
System.out.println(0.1 + 0.2);  // 0.30000000000000004

A double stores numbers in binary, and most decimal fractions, like 0.1, cannot be stored exactly in binary. For money, use java.math.BigDecimal (🔵 not needed for this course).

Casting converts a value to another type explicitly: (double) 7 gives 7.0, and (int) 3.9 gives 3. Casting to int cuts off the fraction; it does not round.

Try it

Operator playground

Choose the type of each operand. If either side is a double, Java computes in decimals; if both are int, it uses 32-bit whole-number arithmetic.

Common mistake: using a variable before giving it a value

Java
int count;
System.out.println(count);   // ❌ compile error
Terminal / output
error: variable count might not have been initialized

Why: Java does not let a local variable (one declared inside a method) be read before it has a value, because reading an undefined value is a classic source of bugs. Fix: initialize it, for example int count = 0;.

Quick check

What do 10 / 4, 10 % 4, and (double) 10 / 4 evaluate to?

Show answer
2, 2, 2.5. In the last one, the cast applies to 10 first, so the division is done with decimals.

1.4 Primitive types vs. reference types Essential

The problem

Some data is small and simple, like a single number. Other data is large or complex, like a list of 10,000 grades or a text. Copying large data every time you pass it around would be slow. Java therefore treats these two kinds of data differently, and that difference changes how your programs behave.

Intuition

Where the analogy stops working: in Java you can never see or calculate with the "address" itself. A reference is opaque. You can only follow it, compare it with ==, or set it to null, which means "no address".

How it works

Two memory areas matter here:

Primitive type Reference type
Examples int, double, boolean, char String, int[], Student, …
The variable holds the value itself a reference to an object (or null)
b = a copies the value the reference, so both variables point to the same object
== compares values whether two references point to the same object
Default value in a field or array 0, 0.0, false, '\u0000' null
Can be null? No Yes

Correction The course says primitives are "stored directly on the stack." That is true only for local variables. A primitive stored inside an object, such as a field double gradeAverage or an element of an int[], lives on the heap, as part of that object. The accurate rule: a variable of primitive type holds the value directly, wherever the variable itself lives. (🔵 The JIT compiler may also optimize objects so they never reach the heap. That is an internal detail that does not change how your program behaves.)

Visual explanation

State of memory at the end of the example below:

flowchart LR
    subgraph STACK["Stack: frame of main()"]
        A["a : int = 10"]
        B["b : int = 20"]
        F["first : int[] ●"]
        S["second : int[] ●"]
        T["third : int[] ●"]
    end
    subgraph HEAP["Heap"]
        ARR1["int[3] object #1<br/>[ 99 | 20 | 30 ]"]
        ARR2["int[3] object #2<br/>[ 99 | 20 | 30 ]"]
    end
    F -- "refers to" --> ARR1
    S -- "refers to (same object)" --> ARR1
    T -- "refers to" --> ARR2

What to notice:

Step through

Stack and heap, line by line

Step through ValuesAndReferences.main. Each click runs one line. Watch the arrows: two variables can point to the same heap object.

    Stack · frame of main()
    Heap
    
    

    Small example

    Runnable program, file ValuesAndReferences.java:

    Java
    public class ValuesAndReferences {
        public static void main(String[] args) {
            // Primitive: the variable holds the value itself
            int a = 10;
            int b = a;          // copies the value 10
            b = 20;             // changes only b
            System.out.println("a = " + a + ", b = " + b);
    
            // Reference: the variable holds a reference to an object
            int[] first = {10, 20, 30};
            int[] second = first;   // copies the reference, not the array
            second[0] = 99;         // changes the one shared array
            System.out.println("first[0] = " + first[0] + ", second[0] = " + second[0]);
    
            // A new array is a different object
            int[] third = {99, 20, 30};
            System.out.println("first == second: " + (first == second));
            System.out.println("first == third:  " + (first == third));
        }
    }
    

    Code walkthrough

    Expected output

    Terminal / output
    a = 10, b = 20
    first[0] = 99, second[0] = 99
    first == second: true
    first == third:  false
    

    Common mistake: expecting == to compare contents

    Java
    int[] x = {1, 2};
    int[] y = {1, 2};
    System.out.println(x == y);                          // false: two different objects
    System.out.println(java.util.Arrays.equals(x, y));   // true: same contents
    

    The same trap exists for Strings. Always compare text with .equals(...) (see section 1.8). Chapter 2 explains how to define "equal contents" for your own classes.

    Quick check

    After int[] p = {5}; int[] q = p; q = new int[]{7};, what is p[0]?

    Show answer
    5. The last statement makes q point to a new array. It does not change the array that p points to.

    1.5 Making decisions: if/else and switch Essential

    The problem

    A program must react differently to different data. A grade of 1.3 should print "Excellent", and a grade of 5.0 should print "Failed".

    Intuition

    if/else is a series of yes/no questions asked in order, and the first "yes" wins. switch is a lookup table: "for this exact value, do that."

    How it works

    if / else if / else

    1. Java evaluates the first condition, which must be a boolean expression.
    2. If it is true, Java runs that block and skips all the rest.
    3. Otherwise it tries the next else if, and so on.
    4. The else block runs only if no condition was true.

    The order matters: grade <= 2 is checked before grade <= 4. A grade of 1 satisfies both conditions, but only the first matching branch runs.

    switch expression (standard since Java 14)

    A switch expression produces a value, so you can assign its result.

    Deeper look Older syntax. You will still see the old switch statement with case 1: and break;. If you forget a break, execution "falls through" into the next case, which is a classic bug. The arrow form avoids that problem.

    Visual explanation

    flowchart TD
        S(["grade = 2"]) --> Q1{"grade <= 2 ?"}
        Q1 -- "true" --> E["print 'Excellent'"]
        Q1 -- "false" --> Q2{"grade <= 4 ?"}
        Q2 -- "true" --> P["print 'Passed'"]
        Q2 -- "false" --> F["print 'Failed'"]
        E --> D(["continue after the if"])
        P --> D
        F --> D
    

    With grade = 2, only the green path, Excellent, is taken. grade <= 4 is never checked.

    switch (grade) 1 2 3 4 anything else
    result Excellent Excellent Satisfactory Satisfactory Insufficient
    Try it

    Follow the decision path

    Move the slider to change grade. The if chain shows which conditions are checked, which one wins, and which are never evaluated.

    2
    if / else if / else
    grade <= 2print "Excellent"
    grade <= 4print "Passed"
    elseprint "Failed"
    switch expression
    case 1, 2 ->"Excellent"
    case 3, 4 ->"Satisfactory"
    default ->"Insufficient"
    
    

    Notice: grades 0, 6 and 7 are nonsense values, but the if chain still prints "Excellent" for 0. Validating input first (a guard) is covered in section 1.9.

    Small example

    This program is shared with section 1.6.

    Runnable program, file GradeRating.java:

    Java
    public class GradeRating {
        public static void main(String[] args) {
            int grade = 2;
    
            // if / else if / else
            if (grade <= 2) {
                System.out.println("Excellent");
            } else if (grade <= 4) {
                System.out.println("Passed");
            } else {
                System.out.println("Failed");
            }
    
            // switch expression (standard since Java 14)
            String rating = switch (grade) {
                case 1, 2 -> "Excellent";
                case 3, 4 -> "Satisfactory";
                default -> "Insufficient";
            };
            System.out.println("Rating: " + rating);
    
            // for loop: known number of repetitions
            for (int i = 0; i < 3; i++) {
                System.out.println("Iteration " + i);
            }
    
            // while loop: repeat while a condition holds
            int j = 0;
            while (j < 3) {
                j++;
            }
            System.out.println("j after while: " + j);
        }
    }
    

    Expected output:

    Terminal / output
    Excellent
    Rating: Excellent
    Iteration 0
    Iteration 1
    Iteration 2
    j after while: 3
    

    Note The course's version loops i < 5. It is shortened to 3 here so the trace in section 1.6 stays readable. Notice also that the two decision blocks use different labels for 3–4 ("Passed" vs. "Satisfactory"). That comes from the original course, which shows two independent techniques.

    Common mistakes

    Wrong order of conditions.

    Java
    if (grade <= 4) {
        System.out.println("Passed");
    } else if (grade <= 2) {          // ❌ unreachable for any grade <= 2
        System.out.println("Excellent");
    }
    

    A grade of 1 already matches the first condition, so "Excellent" is never printed. Fix: check the most specific (narrowest) condition first.

    Using = instead of ==.

    Java
    if (grade = 2) { … }   // ❌ error: incompatible types: int cannot be converted to boolean
    

    = assigns and == compares. Java catches this mistake because an int is not a boolean.

    Quick check

    Write a switch expression that maps the numbers 1–7 to "Weekday" (1–5) or "Weekend" (6–7), and anything else to "Invalid".

    Show answer
    String type = switch (day) { case 1, 2, 3, 4, 5 -> "Weekday"; case 6, 7 -> "Weekend"; default -> "Invalid"; };

    1.6 Repeating work: loops Essential

    The problem

    You want to process 30 students' grades without writing 30 nearly identical lines of code.

    Intuition

    A loop is a checklist you repeat: check whether you're done → if not, do the work → update your position → check again.

    How it works

    Loop Use it when Shape
    for You know how many repetitions you need, or you need an index for (init; condition; update) { body }
    enhanced for ("for-each") You want every element of an array or collection and don't need its index for (int v : values) { body }
    while You repeat while a condition holds and don't know how many times in advance while (condition) { body }
    do … while The body must run at least once, for example asking for input do { body } while (condition);

    How a for loop runs:

    1. init runs once, for example int i = 0.
    2. condition is checked. If it is false, the loop ends.
    3. The body runs.
    4. update runs, for example i++. Then execution goes back to step 2.

    break leaves the loop immediately. continue skips the rest of the body and moves on to the next repetition.

    A variable declared in the for header, like i, exists only inside the loop.

    Visual explanation: execution trace

    Trace of for (int i = 0; i < 3; i++) { System.out.println("Iteration " + i); }

    Step i Check i < 3 Action Output so far
    1 0 true print, then i++ Iteration 0
    2 1 true print, then i++ … Iteration 1
    3 2 true print, then i++ … Iteration 2
    4 3 false leave the loop (unchanged)

    What to notice: the condition is checked 4 times but the body runs 3 times. At the end, i has the value 3, the first value that fails the check.

    flowchart TD
        I["init: i = 0"] --> C{"i < 3 ?"}
        C -- "true" --> B["body: print 'Iteration ' + i"]
        B --> U["update: i++"]
        U -- "back to check" --> C
        C -- "false" --> X(["after the loop"])
    
    Step through

    Loop tracer

    Build a for loop and trace it one check at a time. Try the presets, then invent your own.

    for (int i = ; i ; i += )
    Check #iConditionWhat happens

    Common mistakes

    Off-by-one: using <= with length.

    Runnable program, file Mistakes.java:

    Java
    public class Mistakes {
        public static void main(String[] args) {
            int[] grades = {1, 2, 3};
            for (int i = 0; i <= grades.length; i++) {   // ❌ <= instead of <
                System.out.println(grades[i]);
            }
        }
    }
    

    Output:

    Terminal / output
    1
    2
    3
    Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    	at Mistakes.main(Mistakes.java:5)
    

    Why: an array of length 3 has indexes 0, 1, and 2. The loop also tries index 3. Fix: use i < grades.length, or use a for-each loop if you don't need the index.

    Infinite loop: while (j < 3) { } without j++. The condition never becomes false, so the program never ends. Stop it with Ctrl + C.

    Quick check

    How many times does for (int i = 10; i > 0; i -= 3) run its body, and which values does i take?

    Show answer
    4 times, with i = 10, 7, 4, 1.

    1.7 Arrays and methods Essential

    The problem

    Intuition

    How it works: arrays

    Snippet:

    Java
    int[] grades = {1, 2, 2, 3, 1};    // create with known values
    double[] averages = new double[3]; // create empty: {0.0, 0.0, 0.0}
    String[] names = new String[2];    // {null, null}
    
    grades[0]            // read the first element → 1
    grades[4] = 2;       // overwrite the last element
    grades.length        // number of elements → 5 (a field, no parentheses)
    
    Index 0 1 2 3 4
    grades 1 2 2 3 1

    How it works: methods

    Java
    static double average(int[] values) { … }
    //  │     │       │         │
    //  │     │       │         └─ parameter list: type and name of each input
    //  │     │       └─ method name (lowerCamelCase)
    //  │     └─ return type (void = returns nothing)
    //  └─ static: called on the class, no object needed (see Chapter 2)
    

    Visual explanation: a method call

    sequenceDiagram
        participant M as main()
        participant A as average(values)
        M->>A: call average(grades): the reference to the array is copied into values
        Note over A: sum = 1+2+2+3+1 = 9
        Note over A: (double) 9 / 5 = 1.8
        A-->>M: return 1.8
        Note over M: prints "Average: 1.8"
    

    Small example

    The course's Statistics program, made safe for empty input.

    Runnable program, file Statistics.java:

    Java
    public class Statistics {
    
        /** Returns the average of the values. Throws if the array is empty. */
        static double average(int[] values) {
            if (values.length == 0) {
                throw new IllegalArgumentException("Cannot average an empty array");
            }
            int sum = 0;
            for (int v : values) {
                sum += v;
            }
            return (double) sum / values.length;
        }
    
        public static void main(String[] args) {
            int[] grades = {1, 2, 2, 3, 1};
            System.out.println("Average: " + average(grades));
    
            int sum = 1 + 2 + 2 + 3 + 1;
            System.out.println("Integer division: " + sum / grades.length);
            System.out.println("With cast:        " + (double) sum / grades.length);
        }
    }
    

    Code walkthrough

    Expected output

    Terminal / output
    Average: 1.8
    Integer division: 1
    With cast:        1.8
    

    Correction: empty input The course's average has no check for an empty array. With values.length == 0, it computes (double) 0 / 0. Decimal division by zero does not crash: it silently produces NaN ("Not a Number"). A pure integer version, 0 / 0, would crash with ArithmeticException: / by zero. Neither is a sensible answer, so the method above rejects empty input explicitly. Other valid designs are to return 0.0 with a clear comment, or to return OptionalDouble (Chapter 6).

    Common mistake: forgetting that static methods need static helpers

    If you remove static from average, calling it from main fails with error: non-static method average(int[]) cannot be referenced from a static context. main runs without an object, so it can call only static methods directly. Chapter 2 explains the difference properly.

    Quick check

    Write the method header (only the first line) for a method that takes a double[] and returns the number of values above 4.0.

    Show answer
    for example static int countAbove4(double[] grades).

    1.8 Working with Strings Essential

    The problem

    Most programs handle text: names, input, file lines, messages. You need to transform text, split it into parts, and compare it.

    Intuition

    A String is a printed label. You cannot change the ink on an existing label. Every "change", such as making it uppercase, prints a new label and leaves the old one untouched. This property is called immutability.

    How it works

    String is a reference type (a class), with special language support: you can write literals like "Anna" and join strings with +.

    Method Purpose "Java in graduate school"
    length() number of characters 23
    toUpperCase() / toLowerCase() case conversion (returns a new String) "JAVA IN GRADUATE SCHOOL"
    substring(begin, end) characters from begin (included) up to end (excluded) substring(0, 4)"Java"
    charAt(i) the character at index i charAt(0)'J'
    split(regex) break the text into a String[] split(" ") → 4 pieces
    strip() remove leading/trailing whitespace (Java 11+)
    contains(s), startsWith(s) search contains("grad")true
    equals(s), equalsIgnoreCase(s) compare contents

    Note that length() is a method with parentheses, while the array version length is a field without them.

    split takes a regular expression (a pattern language for text). " " matches exactly one space, and "\\s+" matches "one or more whitespace characters". The backslash is doubled because \ is also the escape character inside Java string literals.

    Visual explanation: substring(0, 4)

    Index 0 1 2 3 4 5 6
    Char J a v a i n
    In substring(0, 4)? ❌ (end is excluded)
    Try it

    substring and split explorer

    s.substring(,)

    Small example

    Runnable program, file StringDemo.java:

    Java
    public class StringDemo {
        public static void main(String[] args) {
            String sentence = "Java in graduate school";
    
            System.out.println(sentence.toUpperCase());
            System.out.println(sentence.split(" ").length + " words");
            System.out.println(sentence.substring(0, 4));
            System.out.println("Unchanged original: " + sentence);
    
            // Edge case: several spaces between words
            String messy = "  Java   in  graduate school ";
            System.out.println(messy.split(" ").length + " pieces with split(\" \")");
            System.out.println(messy.strip().split("\\s+").length + " words with strip() + split(\"\\\\s+\")");
        }
    }
    

    Expected output:

    Terminal / output
    JAVA IN GRADUATE SCHOOL
    4 words
    Java
    Unchanged original: Java in graduate school
    9 pieces with split(" ")
    4 words with strip() + split("\\s+")
    

    Code walkthrough

    Common mistakes

    Comparing text with ==.

    Java
    String input = new java.util.Scanner(System.in).nextLine();  // user types: Anna
    if (input == "Anna") { … }        // ❌ usually false: different objects
    if (input.equals("Anna")) { … }   // ✅ compares the characters
    if ("Anna".equals(input)) { … }   // ✅ also safe when input is null
    

    == checks whether two references point to the same object, as in section 1.4. Two identical-looking strings may still be different objects. Some string literals get shared internally, so == sometimes seems to work, and that makes this bug hard to notice.

    Ignoring the return value. sentence.toUpperCase(); on its own line does nothing useful, because the new string is thrown away. Write sentence = sentence.toUpperCase(); instead.

    Index beyond the text. "abc".substring(0, 5) throws StringIndexOutOfBoundsException: Range [0, 5) out of bounds for length 3.

    Quick check

    What does "graduate".substring(2, 5) return?

    Show answer
    "adu", the characters at indexes 2, 3, and 4.

    1.9 Course app v1: a grade report Essential

    The problem

    Let's combine everything from this chapter into the first version of our course-management app. It stores the students' names and grades, prints a rating for each student, and prints the course average.

    At this point we have no classes of our own yet, so we use two parallel arrays: names[i] and grades[i] belong to the same student. That approach is fragile, because it is easy to update one array and forget the other. Chapter 2 fixes this with a Student class.

    Practical example

    Runnable program, file GradeReport.java:

    Java
    public class GradeReport {
    
        static double average(double[] values) {
            if (values.length == 0) {
                throw new IllegalArgumentException("No grades to average");
            }
            double sum = 0.0;
            for (double v : values) {
                sum += v;
            }
            return sum / values.length;
        }
    
        static String rate(double grade) {
            if (grade < 1.0 || grade > 5.0) {
                return "Invalid";
            } else if (grade <= 2.5) {
                return "Good";
            } else if (grade <= 4.0) {
                return "Passed";
            } else {
                return "Failed";
            }
        }
    
        public static void main(String[] args) {
            String[] names  = {"Anna", "Ben", "Cara"};
            double[] grades = {1.5, 3.7, 2.1};
    
            System.out.println("=== Course Report: Java4FHDo ===");
            for (int i = 0; i < names.length; i++) {
                System.out.printf("%-6s %.1f  %s%n", names[i], grades[i], rate(grades[i]));
            }
            System.out.printf("Course average: %.2f%n", average(grades));
        }
    }
    

    Code walkthrough

    Expected output

    Terminal / output
    === Course Report: Java4FHDo ===
    Anna   1.5  Good
    Ben    3.7  Passed
    Cara   2.1  Good
    Course average: 2.43
    

    The average is (1.5 + 3.7 + 2.1) / 3 = 7.3 / 3 = 2.4333…, which %.2f rounds to 2.43.

    Deeper look Locale note. printf uses your computer's language settings. On a German-language system, the output shows 2,43, with a comma. To always get a dot, use System.out.printf(java.util.Locale.ROOT, "...", ...).


    Try it

    Edit the course data

    Change names and grades, add or remove students, and watch the program's output update. Try an invalid grade like 6, or remove every student.

    names[i]grades[i]
    
    

    Chapter 1 Revision

    Recap

    Glossary

    Term Meaning
    Source code Human-readable program text in .java files
    Compiler (javac) Translates source code into bytecode and reports compile-time errors
    Bytecode Platform-independent instructions in .class files
    JVM The program that loads, verifies, and runs bytecode
    JIT compilation The JVM translating frequently used bytecode into machine code while the program runs
    JDK / JRE Development kit (tools + runtime) / runtime part only
    Entry point Where execution starts: the main method
    Statement One instruction, ending in ;
    Variable, type A named storage location, and the kind of value it may hold
    Statically typed Types are fixed and checked at compile time
    Primitive type One of byte, short, int, long, float, double, boolean, char
    Reference type A type whose variables hold references to objects (classes, arrays, String)
    null A reference that points to no object
    Stack / frame Memory for method calls and their local variables
    Heap Memory where objects live
    Garbage collector Frees heap objects that are no longer reachable
    Cast Explicit type conversion, e.g. (double) x
    Overflow A result too large for its type, which wraps around silently
    Switch expression A switch that produces a value, using case … ->
    Array A fixed-length, indexed sequence of same-typed elements
    Method, parameter, argument, return value A named block of code / its declared inputs / the actual values passed in / the result it hands back
    Pass by value Java copies each argument (for objects, it copies the reference)
    Immutable Cannot be changed after creation (e.g. String)
    Regular expression A pattern language for matching text, used by split

    Syntax reference

    Task Syntax
    Program skeleton public class Name { public static void main(String[] args) { … } }
    Compile / run javac Name.java then java Name · single file: java Name.java
    Print System.out.println(x); · System.out.printf("%.2f%n", x);
    Variable / constant int n = 0; · final double MAX = 5.0; · var s = "x";
    Cast (double) sum / count
    If if (c) { … } else if (d) { … } else { … }
    Switch expression var r = switch (x) { case 1, 2 -> "a"; default -> "b"; };
    Loops for (int i = 0; i < n; i++) · for (T v : array) · while (c) · do { } while (c);
    Loop control break; · continue;
    Array int[] a = {1, 2}; · new double[5] · a[i] · a.length
    Method static double name(int[] p) { return …; }
    String s.length() · s.substring(b, e) · s.split("\\s+") · s.strip() · s.equals(t)

    Exercises

    Exercise 1: beginner (min & max). Write MinMax.java. Given int[] points = {72, 95, 58, 88};, print the smallest value, the largest value, and the range (max − min). Use one loop.

    Exercise 2: intermediate (pass counter). Write PassCounter.java with:

    In main, test both methods with {1.5, 3.7, 2.1, 4.3, 5.0}, using Math.round to round each grade.

    Exercise 3: applied (course report v1.1). Extend the course app into GradeReportV2.java with four students: {"Anna", "Ben", "Cara", "Dev"} and {1.5, 3.7, 2.1, 4.6}.

    Exercise 4: predict the output. Without running it, write down exactly what this prints:

    Java
    public class Predict {
        public static void main(String[] args) {
            int[] a = {1, 2, 3};
            int[] b = a;
            b[1] = 10;
    
            int total = 0;
            for (int i = 0; i < a.length; i += 2) {
                total += a[i];
            }
            System.out.println(total / 3);
            System.out.println(a[1]);
    
            String s = "Java";
            s.toUpperCase();
            System.out.println(s);
        }
    }
    
    Your prediction

    Type the three output lines

    Try all four before reading on.


    Solutions

    Each solution is folded. Open one only after you have tried the exercise.

    Solution 1: MinMax.java
    Java
    public class MinMax {
        public static void main(String[] args) {
            int[] points = {72, 95, 58, 88};
    
            int min = points[0];
            int max = points[0];
            for (int p : points) {
                if (p < min) min = p;
                if (p > max) max = p;
            }
            System.out.println("Min: " + min);
            System.out.println("Max: " + max);
            System.out.println("Range: " + (max - min));
        }
    }
    
    Terminal / output
    Min: 58
    Max: 95
    Range: 37
    

    Why it works: min and max both start with the first element, not with 0. If max started at 0, an array of only negative numbers would wrongly report a maximum of 0. Edge case: points[0] fails on an empty array. A robust version checks points.length == 0 first, just as the corrected average does. The same issue affects the course's generic maximum method in Chapter 5.

    Solution 2: PassCounter.java
    Java
    public class PassCounter {
    
        static int countPassed(double[] grades) {
            int passed = 0;
            for (double g : grades) {
                if (g >= 1.0 && g <= 4.0) {
                    passed++;
                }
            }
            return passed;
        }
    
        static String describe(int roundedGrade) {
            return switch (roundedGrade) {
                case 1 -> "very good";
                case 2 -> "good";
                case 3 -> "satisfactory";
                case 4 -> "sufficient";
                case 5 -> "fail";
                default -> "invalid";
            };
        }
    
        public static void main(String[] args) {
            double[] grades = {1.5, 3.7, 2.1, 4.3, 5.0};
            System.out.println("Passed: " + countPassed(grades) + " of " + grades.length);
            System.out.println("Passed (empty): " + countPassed(new double[0]));
    
            for (double g : grades) {
                int rounded = (int) Math.round(g);
                System.out.println(g + " -> " + rounded + " (" + describe(rounded) + ")");
            }
        }
    }
    
    Terminal / output
    Passed: 3 of 5
    Passed (empty): 0
    1.5 -> 2 (good)
    3.7 -> 4 (sufficient)
    2.1 -> 2 (good)
    4.3 -> 4 (sufficient)
    5.0 -> 5 (fail)
    

    Notes:

    • An empty array needs no special case here: the loop body never runs, so the result stays 0.
    • Math.round(double) returns a long, so we cast it to int.
    • Look closely at 4.3. It rounds to 4, "sufficient", yet countPassed treats it as a fail, because 4.3 > 4.0. Rounding before classifying changes the meaning. Real grading rules must decide which comes first. This is a design decision, not a Java question.
    Solution 3: GradeReportV2.java
    Java
    public class GradeReportV2 {
    
        static int indexOfBest(double[] grades) {
            if (grades.length == 0) {
                return -1;                       // signal "no best student"
            }
            int best = 0;
            for (int i = 1; i < grades.length; i++) {
                if (grades[i] < grades[best]) {  // lower grade is better
                    best = i;
                }
            }
            return best;
        }
    
        public static void main(String[] args) {
            String[] names  = {"Anna", "Ben", "Cara", "Dev"};
            double[] grades = {1.5, 3.7, 2.1, 4.6};
    
            int good = 0, passed = 0, failed = 0;
            for (double g : grades) {
                if (g <= 2.5)      good++;
                else if (g <= 4.0) passed++;
                else               failed++;
            }
    
            int best = indexOfBest(grades);
            if (best == -1) {
                System.out.println("No students enrolled.");
            } else {
                System.out.println("Best student: " + names[best] + " (" + grades[best] + ")");
            }
            System.out.println("Good: " + good + ", Passed: " + passed + ", Failed: " + failed);
            System.out.println("Best in empty course: " + indexOfBest(new double[0]));
        }
    }
    
    Terminal / output
    Best student: Anna (1.5)
    Good: 2, Passed: 1, Failed: 1
    Best in empty course: -1
    

    Notes:

    • The method returns an index rather than a grade, so the caller can look up the name in the parallel array.
    • Returning -1 as a "nothing found" signal is a common convention, but the caller must remember to check for it. Chapter 4 (exceptions) and Chapter 6 (Optional) show safer alternatives.
    • if without braces is legal for a single statement. It is used here only for compact counting. In most code, always use braces.
    Solution 4: predict the output
    Terminal / output
    1
    10
    Java
    

    Step by step:

    1. b = a copies the reference, so b[1] = 10 changes the shared array to {1, 10, 3}.
    2. The loop uses i += 2, so it visits indexes 0 and 2: total = 1 + 3 = 4.
    3. total / 3 is integer division: 4 / 3 = 1.
    4. a[1] is 10, changed through b.
    5. s.toUpperCase() creates a new string that is never stored. s is still "Java".

    Chapter quiz

    Six quick questions. Click an answer to see whether it is right and why.

    Q1 What does javac HelloWorld.java produce?

    Q2 What is stored in int x = 7 / 2;?

    Q3 After int[] p = {1}; int[] q = p; q[0] = 5;, what is p[0]?

    Q4 An object has a field double gradeAverage. Where is that value stored?

    Q5 Which reliably checks that two Strings contain the same text?

    Q6 What does (double) 0 / 0 evaluate to?

    0 of 6 answered

    Self-check

    I understand this chapter if I can…


    End of Chapter 1. ▶ Next: Chapter 2, "Object-Oriented Programming I: Classes, Objects, Encapsulation". Section 2.1 will turn the two parallel arrays from section 1.9 into a Student class. Say "continue" to proceed.