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.
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? | Person → Student / 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:
java -version
javac -version
Both should report version 21 or later.
Chapter 1: Java Basics
Learning objectives (from the course)
- Understand Java program structure (class,
mainmethod, JVM/JDK/JRE) - Distinguish between primitive types and reference types
- Correctly use control structures (
if/else,switch, loops) - Write a simple, compilable Java program
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.
- The recipe in your own words is your Java source code.
- The standard kitchen language is bytecode.
- The interpreter in each kitchen is the Java Virtual Machine (JVM).
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:
- Source code is the text you write, saved in a file ending in
.java. - A compiler is a program that translates code from one form into another. Java's compiler is called
javac. - Bytecode is a compact, standardized instruction format, saved in
.classfiles. It is not tied to any particular processor. - The JVM is a program that loads
.classfiles and runs them on your actual machine.
The steps:
- You write
HelloWorld.java. javacchecks it for errors, such as wrong types or missing semicolons. If it finds any, it stops and reports them. These are compile-time errors.- If the code is valid,
javacwritesHelloWorld.class, which contains bytecode. - You run
java HelloWorld. The JVM starts, loads the class, checks the bytecode, and begins executingmain. - 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.
Where does the error stop the program?
Pick a version of HelloWorld and run it through the pipeline. Watch which stage catches the problem.
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
jlinktool). "JRE" is still useful as a concept: the runtime part of the JDK.
Small example
Runnable program, file HelloWorld.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:
javac HelloWorld.java # creates HelloWorld.class
java HelloWorld # runs it — note: class name, no ".class"
Expected 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.class → Could 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
- Which of these errors does
javacreport: a missing semicolon, or dividing by zero? - If you copy
HelloWorld.classto a friend's computer that has a different operating system, what does the friend need in order to run it?
Show answer
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:
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.
// 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:
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:
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.
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.
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
int count;
System.out.println(count); // ❌ compile error
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
- A primitive variable is a sheet of paper with the number written on it. If you photocopy the sheet and scribble on the copy, the original is unchanged.
- A reference variable is a sheet of paper with a home address written on it. If you photocopy the address, both sheets lead to the same house. If someone repaints the house using the copy, the owner of the original sees the new paint too.
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:
- The stack holds one frame per running method call. A frame contains that method's local variables and parameters. When the method returns, its frame is removed.
- The heap is where objects live, including arrays,
Strings, and later your own classes. Objects created withnewstay on the heap while something still refers to them. After that, the garbage collector reclaims the memory automatically.
| 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 gradeAverageor an element of anint[], 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:
aandbeach hold their own number.firstandsecondpoint to one array, so a change made throughsecondis visible throughfirst.thirdhas the same contents as array #1 but is a different object, sofirst == thirdisfalse.- The numbers inside the arrays are primitives, and they live on the heap.
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.
Small example
Runnable program, file ValuesAndReferences.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
int[] first = {10, 20, 30};creates an array object on the heap and stores a reference to it infirst. (Arrays are covered in section 1.7.)int[] second = first;does not create a new array. Only the reference is copied.second[0] = 99;follows the reference to the shared array and changes element 0."a = " + auses+with aStringon one side. That means concatenation: the number is converted to text and joined.(first == second)must be in parentheses. Without them, Java would first join"first == second: " + firstinto a string and then compare that string withsecond, which doesn't compile.
Expected output
a = 10, b = 20
first[0] = 99, second[0] = 99
first == second: true
first == third: false
Common mistake: expecting == to compare contents
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
- Java evaluates the first condition, which must be a
booleanexpression. - If it is
true, Java runs that block and skips all the rest. - Otherwise it tries the next
else if, and so on. - The
elseblock runs only if no condition wastrue.
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.
case 1, 2 ->matches either value. There is no fall-through between cases.default ->covers every other value.- When a
switchexpression produces a value, it must handle every possible input. For anint, that means you needdefault.
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 |
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.
grade <= 2print "Excellent"grade <= 4print "Passed"elseprint "Failed"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:
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:
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.
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 ==.
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:
initruns once, for exampleint i = 0.conditionis checked. If it isfalse, the loop ends.- The
bodyruns. updateruns, for examplei++. 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"])
Loop tracer
Build a for loop and trace it one check at a time. Try the presets, then invent your own.
| Check # | i | Condition | What happens |
|---|
Common mistakes
Off-by-one: using <= with length.
Runnable program, file Mistakes.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:
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
i = 10, 7, 4, 1.1.7 Arrays and methods Essential
The problem
- Arrays: you need to keep many values of the same type together, such as all the grades of a course.
- Methods: you need the same calculation, such as an average, in several places. Copy-pasted code is harder to fix when it contains a bug.
Intuition
- An array is a row of numbered lockers. All the lockers have the same size, the numbering starts at 0, and the row cannot grow or shrink after it is built.
- A method is a named, reusable recipe. You hand it ingredients (arguments), it does its work, and it may hand back a result (return value).
How it works: arrays
✂ Snippet:
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
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)
- Parameters are the variables in the method definition. Arguments are the actual values passed when you call the method.
return value;ends the method and hands the value back to the caller.- Java passes arguments by value. For a primitive, the method gets a copy of the number. For a reference, the method gets a copy of the reference, so it can change the object the reference points to (compare with section 1.4).
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:
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
for (int v : values)is a for-each loop. In each repetition,vis the next element of the array.(double) sum / values.length: the cast applies tosumbefore dividing, so the division uses decimals.(double) (sum / values.length)would divide as integers first (9 / 5 = 1) and only then convert the result to1.0.throw new IllegalArgumentException(...)stops the method and reports a problem to the caller. Chapter 4 explains exceptions in detail. For now: this makes the program stop with a clear message instead of producing a nonsense result.
Expected output
Average: 1.8
Integer division: 1
With cast: 1.8
Correction: empty input The course's
averagehas no check for an empty array. Withvalues.length == 0, it computes(double) 0 / 0. Decimal division by zero does not crash: it silently producesNaN("Not a Number"). A pure integer version,0 / 0, would crash withArithmeticException: / by zero. Neither is a sensible answer, so the method above rejects empty input explicitly. Other valid designs are to return0.0with a clear comment, or to returnOptionalDouble(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
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) |
substring and split explorer
Small example
Runnable program, file StringDemo.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:
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
toUpperCase()returns a new string.sentenceitself is unchanged, as the fourth line of output shows.sentence.split(" ").lengthchains two calls:splitreturns an array, and.lengthreads that array's size.- Edge case:
split(" ")on the messy string splits at every single space, which creates empty pieces between neighboring spaces. Java drops the empty pieces at the end but keeps those at the start and in the middle, so the result is 9 pieces instead of 4. Usingstrip()and thensplit("\\s+")gives the intended 4 words. (Splitting an empty string""returns an array with one empty element, so counting words in empty input needs a separate check.)
Common mistakes
Comparing text with ==.
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:
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
ratechecks for invalid input first (a guard), then the ranges from best to worst.System.out.printfprints formatted text. Inside the format string:%-6sprints a string, left-aligned, padded to 6 characters.%.1fprints a decimal number with 1 digit after the point, and%.2fwith 2 digits.%nis a line break that works on every operating system.
- A regular
forloop is used here, not a for-each loop, because we need the indexito read from both arrays.
Expected 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, "...", ...).
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
javaccompiles.javasource into platform-independent.classbytecode, and the platform-specific JVM runs it. The JDK is what you install as a developer.- A program starts at
public static void main(String[] args), inside a class whose name matches its file. - Java is statically typed: every variable has a type the compiler checks. There are 8 primitive types. Everything else is a reference type.
- A primitive variable holds a value. A reference variable holds a reference to an object on the heap. Assigning a reference copies the reference, not the object.
if/elsechecks conditions in order. The switch expression (Java 14+) maps values to results without fall-through.for, for-each,while, anddo-whilerepeat work. Watch for off-by-one errors.- Arrays have a fixed length and indexes
0 … length-1. Methods package reusable logic. Arguments are passed by value. - Strings are immutable. Compare them with
equals, never==. - Watch for integer division, overflow, and empty input.
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 |
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:
static int countPassed(double[] grades), which returns how many grades are between 1.0 and 4.0 inclusive. It must return0for an empty array.static String describe(int roundedGrade), which uses a switch expression to map 1→"very good", 2→"good", 3→"satisfactory", 4→"sufficient", 5→"fail", and anything else to "invalid".
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}.
- Write
static int indexOfBest(double[] grades). It returns the index of the best (lowest) grade, or-1for an empty array. - Print the best student, and count how many students are Good (≤ 2.5), Passed (≤ 4.0), and Failed.
Exercise 4: predict the output. Without running it, write down exactly what this prints:
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);
}
}
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
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));
}
}
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
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) + ")");
}
}
}
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 along, so we cast it toint.- Look closely at 4.3. It rounds to 4, "sufficient", yet
countPassedtreats 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
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]));
}
}
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
-1as 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. ifwithout 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
1
10
Java
Step by step:
b = acopies the reference, sob[1] = 10changes the shared array to{1, 10, 3}.- The loop uses
i += 2, so it visits indexes 0 and 2:total = 1 + 3 = 4. total / 3is integer division: 4 / 3 =1.a[1]is10, changed throughb.s.toUpperCase()creates a new string that is never stored.sis 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?
The compiler writes platform-independent bytecode; the JVM turns it into machine code while the program runs.
Q2 What is stored in int x = 7 / 2;?
Both operands are int, so the fraction is discarded (truncated, not rounded).
Q3 After int[] p = {1}; int[] q = p; q[0] = 5;, what is p[0]?
q = p copies the reference; both variables point to the same array.
Q4 An object has a field double gradeAverage. Where is that value stored?
Fields live with their object on the heap. Only local variables live in a stack frame.
Q5 Which reliably checks that two Strings contain the same text?
== compares references; = assigns. equals compares characters.
Q6 What does (double) 0 / 0 evaluate to?
Decimal division by zero does not throw; 0.0 / 0 is “Not a Number”. That is why the corrected average rejects empty arrays.
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.