Sources

This note combines the two lectures of week 4 (HS 2026):

  • Lecture A finishes Chapter 3, Control Structures (Kontrollstrukturen): scope, method overloading, increment/decrement, short-circuit evaluation, the fencepost problem, sentinel loops and do-while.
  • Lecture B starts the topic of Sequences (Sequenzen): Strings, arrays, and reference semantics vs. value semantics.

Where the transcript was garbled or a slide was only shown and not read aloud, the code was reconstructed. Each such place is marked (reconstructed).


Part A: Control Structures (continued)

1. Executive Overview & Core Objectives

Lecture A completes the chapter on control structures. It explains where names are valid (scope), when several methods may share one name (overloading), and how the shorthand operators ++, -- and += behave, including their side effects. The second half covers two loop errors that recur again and again: the fencepost (off-by-one) problem and badly designed sentinel loops. It closes with the do-while loop and the fact that all three loop types are equally expressive.

Learning objectives

  • Determine the scope of any variable: block variables, method parameters, for-header variables, and variables declared inside a loop body.
  • Know which scope overlaps cause a compile error and which reuses of a name are allowed.
  • Define a method signature and decide whether a set of overloaded methods is legal.
  • Decide which overloaded method a call resolves to, or whether the call is unmatched or ambiguous.
  • Evaluate expressions containing pre/post increment step by step.
  • Explain short-circuit evaluation and the danger of side effects inside larger expressions.
  • Recognise and fix fencepost errors.
  • Write correct sentinel loops, with a priming read or with do-while.
  • Convert between for, while and do-while.

2. Core Concepts & Formal Definitions

2.1 Recap of week 3

  • if/else statement (Anweisung): executes one of two branches depending on a boolean condition. It is not a loop.
  • for and while loops (Schleifen): repeat a block of code as long as a condition holds.
  • Methods (Methoden): can take parameters and can return a value with return.

Exam Trap: terminology

There is no such thing as an “if-loop” (FLs-Schleife). A loop (Schleife) always means repetition. if/else is a conditional statement (bedingte Anweisung). The lecturer stressed this explicitly.

The running example that reappears throughout the lecture is a prime test:

public static boolean isPrime(int n) {
    int divisors = 0;
    for (int d = 1; d <= n; d++) {      // every candidate divisor 1..n
        if (n % d == 0) {               // d divides n without remainder
            divisors++;
        }
    }
    return divisors == 2;               // a prime has exactly the divisors 1 and n
}

The method loops over all candidate divisors and counts those with . A number is prime if and only if it has exactly two divisors.

The lecturer said one might need to be careful with and . In fact the code already handles both: for the count is , so the result is false; for the divisors are and , so the result is true. For the loop body never runs, so the count is and the result is false.

2.2 Scope (Namensraum / Gültigkeitsbereich)

Definition (Scope)

The scope (Namensraum) of a name, whether a variable or a method, is the part of the program in which that name is known and visible.

  • A variable can only be read or modified inside its scope.
  • A method can only be called inside its scope.
  • Outside its scope, a name behaves as if it had never been declared.

Rules for local variables

  1. Block (Block): A block is a region delimited by { ... }. Method bodies, if and else branches and loop bodies are all blocks, and a bare { ... } is also legal. A variable declared inside a block is visible from its declaration to the end of that block.
  2. Nesting (Verschachtelung): A variable is also visible in all blocks nested inside its block. For example, a sum declared before a loop can be updated inside the loop.
  3. Methods: Local variables, including the parameters, live only until the method call ends. Changing a parameter never affects the caller. The only way to pass a result out of a method is return.
  4. if/else: A variable declared in the if block exists only there, and the same holds for the else block. If both branches need a variable, declare it before the if.
  5. for header: A variable declared in the header, such as int i = 0, is valid in the whole header (condition and update) and the whole body. It disappears when the loop terminates.
  6. Variable declared in a loop body: It lives for exactly one iteration. Each iteration creates a new variable with the same name.

Rule (No overlapping scopes for equal names)

Two local variables with the same name must not have overlapping scopes. Otherwise the compiler reports that the variable is already defined.

  • Allowed: two sequential for loops that each declare their own i, because the scopes do not overlap.
  • Not allowed: an inner for (int i ...) nested inside an outer for (int i ...).
  • Not allowed: int i declared inside the body of a loop whose header already declares i.

Why does Java have scopes at all?

  • Reuse of names: Without scopes, every loop would need a fresh name such as iLoop1, iLoop2, and so on. Reusing i makes code much more readable.
  • Readability: The same holds for descriptive names like size used in different methods.
  • Memory management: Variables can be freed when their scope ends. This is only an outlook and not exam-relevant.

2.3 Method signature and overloading (Methodenüberladung)

Definition (Signature)

The signature (Signatur) of a method is its name together with the ordered list of its parameter types.

  • For static int f(int a, int b), the signature is f(int, int).
  • The return type and the parameter names are not part of the signature.

Rule (Uniqueness of signatures)

A class must not contain two methods with the same signature. This holds even if their return types differ. The compiler error is “method is already defined”.

Definition (Method overloading)

Overloading (Method Overloading) means having several methods with the same name but different signatures. The parameter lists must differ in the number of parameters, their order, or their types.

  • Differing only in parameter names is not allowed.
  • Differing only in return type is not allowed.
  • Any combination of these two is not allowed either.

When a method is called, the compiler chooses the matching method. Passing an argument works like an assignment to the parameter, so the implicit widening int → double is allowed but double → int is not. There must be a unique best match. If no method matches, or if two match equally well, the call is a compile error.

2.4 Increment, decrement, compound assignment

Definition (Post-increment / post-decrement)

The expression x++ (or x--) does two things:

  • Its value is the old value of x.
  • As a side effect, x is increased (or decreased) by 1 afterwards.

In short: read first, then change. The operator stands after the variable, hence “post”.

Definition (Pre-increment / pre-decrement)

The expression ++x (or --x) first increases (or decreases) x by 1. Its value is the new value.

In short: change first, then read.

Both forms have the same effect on x. They differ only in the value of the expression. Their equivalent long forms are shown below.

// System.out.println(x++);  is equivalent to:
int xOld = x;
x = x + 1;
System.out.println(xOld);
 
// System.out.println(++x);  is equivalent to:
x = x + 1;
System.out.println(x);

The operators also work on double, although that is unusual. Python has no ++ operator, but most C-like languages do.

Definition (Compound assignment)

Each compound assignment updates a variable using its own current value:

  • var += value means var = var + value.
  • var -= value means var = var - value.
  • var *= value means var = var * value.

The same pattern applies to the other arithmetic operators.

Recall how any assignment is executed: first evaluate the right-hand side, then store the result in the left-hand side. This order is what makes x = x + 1 meaningful.

2.5 Short-circuit evaluation and side effects

Definition (Short-circuit / lazy evaluation)

Java evaluates && and || from left to right and stops as soon as the result is determined. This is called short-circuit evaluation (Kurzschlussauswertung, Lazy/Shortcut Evaluation).

  • For a && b: if a is false, then b is not evaluated, because false && anything is false.
  • For a || b: if a is true, then b is not evaluated.

Definition (Expression, statement, side effect)

Simplified, code components fall into these kinds:

  • An expression (Ausdruck) has a value. Examples are x, 6 and x + 1. You can compute with it and print it.
  • A statement (Anweisung) has an effect. An example is System.out.println(...).
  • A side effect (Seiteneffekt) belongs to an expression that has both a value and an effect. x++ is an example: its value is the old x, and its effect is that x changes.

Style rule from the lecture

Use ++, -- and += only standing alone on a line or in the update part of a for loop.

  • Fine: i++ in a loop header, or sum += i; on its own line.
  • Never: something like x = i++ + ++i;.

Programs should be understandable, not puzzles.

2.6 Fencepost / off-by-one errors

Definition (Fencepost problem)

The fencepost problem (Zaunpfahlfehler, also called off-by-one, “um eins daneben”) arises when a task needs items of one kind with items of another kind between them.

  • Fence picture: posts (Pfähle) need rails (Latten/Querstreben).
  • Output picture: numbers need commas.

A loop that always emits “post + rail” or always “rail + post” produces exactly one rail too many, either at the end or at the start.

Standard fix

Pull one post out of the loop. Two forms work:

  • Print one post first, then loop times printing “rail, post”.
  • Loop times printing “post, rail”, then print one final post.

2.7 Sentinel loops and do-while

Definition (User-controlled loop, sentinel)

A user-controlled loop (benutzergesteuerte Schleife) is controlled by input rather than a precomputed count. It is an indefinite loop (unbestimmte Schleife).

  • A sentinel (Sentinel, “Wächter”) is a special input value that means “stop”, such as 0 or -1.
  • A loop that repeats until the sentinel is read is a sentinel loop.

Definition ( do-while loop)

The body of a do-while loop is executed at least once. The condition is tested after each execution, so the loop is “do once, then repeat while”. Note the semicolon after the condition.

do {
    // body
} while (condition);

Equivalence of loop types

for, while and do-while are equally expressive: each can be rewritten using any other. Which one to use is a matter of taste, with these rules of thumb:

  • Use for when the initialisation, test and update are simple.
  • Use while when the condition or update is more complex.
  • Use do-while when at least one iteration is required, which is typical for user input.

3. Step-by-Step Derivations

3.1 Why a loop variable is unusable after the loop

for (int d = 2; d < n; d++) { ... }
System.out.println(d);   // COMPILE ERROR: cannot find symbol d

By rule 5 in §2.2, the scope of d is the header plus the body. After the closing brace, d does not exist, so this is the same as using an undeclared variable.

Fix: declare d before the loop when you need to know where the loop stopped.

int d;
for (d = 2; d < n; d++) { ... }
System.out.println(d);   // OK

3.2 A variable declared in the body lives one iteration

for (int k = 0; k < 100; k++) {
    int a = 0;
    a = a + 1;          // a is 1 here
}                       // a is destroyed here, every iteration

Each iteration creates a new a initialised to 0, so a goes a hundred times. After the loop, a does not exist at all. An accumulator such as a sum or a counter must therefore be declared outside the loop.

3.3 Equal names in different methods are independent

public static void a(int a) { a = a + 1; }   // parameter a, local to this method
 
public static void main(String[] args) {
    int a = 5;     // a different variable, local to main
    a(a);          // the value 5 is copied into the parameter
    System.out.println(a);   // prints 5: main's a is unaffected
}

The two variables share a name by coincidence. They have disjoint scopes, so changing one cannot affect the other.

4. Worked Examples & Applications

4.1 Clicker question: first prime (reconstructed)

The code was only shown on screen. Its essential features were these:

  • a while (true) loop whose only exit is a return inside an if,
  • a for loop with an empty body (;) and a compound continuation condition,
  • the loop variable d declared in the for header and used after the for loop.

A version with all of these features looks like this:

public static int firstPrimeFrom(int n) {
    while (true) {
        for (int d = 2; d < n && n % d != 0; d++);   // empty body
        if (d == n) {          // uses d outside its scope
            return n;
        }
        n++;
    }
}

Each answer option is analysed below.

OptionVerdictReason
A for loop without a body is illegalNoLegal. The condition and the update do all the work: d advances until the condition fails.
Compile error: missing returnNowhile (true) without break can never complete normally. Java sees that the only way out is the return, so no return is needed after the loop.
Non-termination / runtime error / a number is printedNoThe program never runs because it does not compile.
Compile error: d is not in scopeYesd was declared in the for header and is used after the loop (§3.1).

The lecturer said this question was intentionally very hard. The reasoning in the second row is reasonable: in general you cannot always predict what the compiler’s return analysis can prove.

4.2 Overloading resolution

static void f(int a, double b)  { ... }   // (1)
static void f(double a, int b)  { ... }   // (2)
CallResultReason
f(1, 1.0)calls (1)The types match (1) exactly. (2) would require 1.0 → int, which is not allowed.
f(1.0, 1)calls (2)This is the mirror case.
f(1.0, 1.0)compile error: no matching methodEach method would need a double → int conversion, which is illegal, just like int x = 1.0;.
f(1, 1)compile error: ambiguousEither method works by widening one int to double. Neither is better, so the compiler refuses to decide. With only one of the two methods declared, the call would compile.

These pairs are legal overloads:

static int max(int a, int b)              { return a > b ? a : b; }
static boolean max(boolean a, boolean b)  { return a || b; }  // true if at least one is true

A common pattern is delegation: a version with fewer parameters fixes one argument and calls the general version.

static double power(double base, int exp) { ... }       // general version
static double power(int exp) { return power(2.0, exp); } // base fixed to 2

These are illegal:

  • int g(int a) together with int g(int b): only the parameter names differ.
  • int g(int a) together with double g(int a): only the return type differs.

4.3 Increment traces

Example 1.

int x = 2;
System.out.println(x++);   // prints 2, then x becomes 3
System.out.println(x++);   // prints 3, then x becomes 4

The output is 2 and then 3, and afterwards .

Example 2.

int x = 2;
int y = x++;               // y gets the old value 2, then x becomes 3
System.out.println(x);     // 3
System.out.println(y);     // 2

The output is 3 then 2. In the lecture, students offered both “3, 2” and “2, 3”. The line y = x++ changes two variables, which is exactly why it is bad style.

Exam Trap: =+ vs +=

With int x = 7;:

  • x += 3; sets x to 10.
  • x =+ 3; sets x to 3, because it parses as x = +3 (unary plus). It compiles without any error.

A student once lost all points on an exam task because of this typo.

Example 3 (the big puzzle). With int x = 2;, what does this print?

System.out.println(++x + x++ + " " + x + ++x + x);

Student guesses in the lecture included 6 4 5 5, 6 3 4 5, 6 4 4 and 8 5 3 5.

Step 1: bracketing. + is left-associative, so the expression parses as

Step 2: evaluate operands strictly left to right and track :

TermRuleValue afterwards
start2
A = ++xincrement, then read33
B = x++read, then increment34
A + Bint + int is arithmetic64
+ " "int + String is concatenation"6 "4
C = xread4, giving "6 4"4
D = ++xincrement, then read5, giving "6 45"5
E = xread5, giving "6 455"5

The output is 6 455, which matches the guess “6 4 5 5”. Two key points:

  • The first + is still an integer addition, because no String has appeared yet.
  • Once a String is involved, every later + is a concatenation.

4.4 Short-circuit and side effect

boolean p = ...;            // unknown
int x = 2;
boolean q = p && x++ < 3;
// What is x now?
  • Case p == true: The right side must be evaluated. x++ has value 2, and is true, so q = true. The side effect sets x = 3.
  • Case p == false: false && … is always false, so the right side is not evaluated. q = false and x stays 2.

So depending on p, and nothing in the code makes this visible. Short-circuit evaluation makes the side effect conditional. This is exactly why ++ must not appear inside larger expressions.

4.5 Fencepost: printNumbers(n)

The task is to print 1, 2, ..., n separated by commas. For n = 6 the output should be 1, 2, 3, 4, 5, 6.

Three wrong attempts were shown (reconstructed):

// (a) a comma on both sides: ",1,,2,,3,"  (leading comma and double commas)
for (int i = 1; i <= n; i++) System.out.print("," + i + ",");
 
// (b) a comma after each number: "1,2,3,"  (trailing comma)
for (int i = 1; i <= n; i++) System.out.print(i + ",");
 
// (c) a comma before each number: ",1,2,3"  (leading comma)
for (int i = 1; i <= n; i++) System.out.print("," + i);

All three are wrong. Many students picked (b) or (c) by elimination. The classic nature of this problem is the point: it has its own Wikipedia page.

The two correct versions pull one item out of the loop:

// Version 1: first post outside the loop, then "rail + post"
public static void printNumbers(int n) {
    System.out.print(1);
    for (int i = 2; i <= n; i++) {
        System.out.print(", " + i);
    }
    System.out.println();
}
 
// Version 2: "post + rail" n-1 times, then the last post
public static void printNumbers(int n) {
    for (int i = 1; i <= n - 1; i++) {
        System.out.print(i + ", ");
    }
    System.out.println(n);
}

Both versions assume a sensible argument. For n <= 0, version 1 prints 1 and version 2 prints 0 or a negative number, so invalid inputs must be handled separately.

Testing tip from the lecture

Trace every loop by hand with 2–3 iterations, for example n = 3. Then check the edge cases n = 1, n = 0 and negative n, if they are possible inputs.

4.6 Fencepost with a hidden end: printPrimes(n)

The task is to print all primes in separated by commas. For n = 20 the output should be 2, 3, 5, 7, 11, 13, 17, 19.

The first attempt in the lecture had a trailing comma:

for (int i = 2; i <= n; i++) {
    if (isPrime(i)) System.out.print(i + ", ");
}

Here the “drop the last iteration” trick fails. We do not know which number will be the last prime, and itself may not be prime.

The insight is that the first prime is trivially known: it is 2. So the comma goes before each number instead:

public static void printPrimes(int n) {
    if (n < 2) {                 // edge case: no primes at all
        return;                  // early return
    }
    System.out.print(2);         // the first post, known in advance
    for (int i = 3; i <= n; i++) {
        if (isPrime(i)) {
            System.out.print(", " + i);
        }
    }
    System.out.println();
}

Without the if (n < 2) guard, printPrimes(1) would print 2, which is wrong.

Early returns

Handle special cases at the very top of a method and leave with return;. Everything below can then assume valid input. The lecturer called early returns “your friend”.

A student proposed a different approach: first search downward from n for the largest prime . Once is known, a loop can stop just before it, and is printed last without a comma. This is left as an exercise.

4.7 Sentinel loop: simple calculator

The task is to read integers until the user types 0, then print their sum. For the input 10 20 30 0 the output should be 60.

Attempt 1:

Scanner console = new Scanner(System.in);
int sum = 0;
int number;                       // not initialised
while (number != 0) {             // COMPILE ERROR: number might not be initialised
    System.out.print("Enter a number (0 to quit): ");
    number = console.nextInt();
    sum = sum + number;
}
System.out.println("The total is " + sum);

The bugs are found one by one:

  1. number is read before it was ever assigned. Java refuses to compile this.
  2. With int number = 0; the condition is false immediately, so the loop runs zero times and the program is useless.
  3. With a dummy value like int number = -1; (anything except 0), the program works. The last read value 0 is still added to the sum, but adding 0 changes nothing. A comment such as “this must never be 0” is a sign of a bad design.
  4. Now change the sentinel to -1 and the dummy to -2. The input 10 20 30 -1 gives 59, because the sentinel -1 is added to the sum.

Two hacks were shown with the explicit warning not to use them:

  • Starting at sum = 1 to compensate for the -1. This is unreadable and breaks as soon as anything changes.
  • break, which is not used in this course. The lecturer likes it, but the course design excludes it, and it is not exam-relevant.

Diagnosis. The loop must perform “read, add, read, add, …, read”. That is reads but only additions, because the last value read is the sentinel. This is another fencepost problem.

Fix A: guard inside the loop.

int number = -2;                   // dummy value, anything but -1
while (number != -1) {
    System.out.print("Enter a number (-1 to quit): ");
    number = console.nextInt();
    if (number != -1) {            // only add real data
        sum = sum + number;
    }
}

Fix B: priming read (the fencepost pattern: one “post” before the loop).

System.out.print("Enter a number (-1 to quit): ");
int number = console.nextInt();
while (number != -1) {
    sum = sum + number;
    System.out.print("Enter a number (-1 to quit): ");
    number = console.nextInt();
}
System.out.println("The total is " + sum);

Fix B duplicates the prompt-and-read code. The standard remedy for duplication is to extract a method:

public static int readNumber(Scanner console) {
    System.out.print("Enter a number (-1 to quit): ");
    return console.nextInt();
}
 
// in main:
Scanner console = new Scanner(System.in);
int sum = 0;
int number = readNumber(console);
while (number != -1) {
    sum += number;
    number = readNumber(console);
}
System.out.println("The total is " + sum);

A student asked about creating a new Scanner inside readNumber on every call. The answer was to create it once outside and pass it as a parameter, as done above.

Fix C: do-while.

int sum = 0;
int number;
do {
    number = readNumber(console);
    if (number != -1) {
        sum += number;
    }
} while (number != -1);
System.out.println("The total is " + sum);

No dummy value is needed here: the body always runs once, so number is definitely assigned before the test.

Fix D: for loop. The calculator can also be written as a for loop with an empty initialisation and update. This shows the equivalence of the loop types.

for (int number = readNumber(console); number != -1; number = readNumber(console)) {
    sum += number;
}

5. Intuition & Conceptual Bridges

Intuition

  • Scope: Think of each { } as a room. A variable is created in a room and destroyed when you leave it. People in inner rooms can see out, but nobody outside can see in.
  • Signature: A signature is a method’s “ID card”. The compiler uses only the name and the argument types to pick a method, never what the method returns. That is why the return type cannot distinguish overloads.
  • Fencepost: Whenever the pattern is “item, separator, item, …, item”, one item belongs outside the loop. This covers commas, sentinel reads, and later printing arrays.

Connections

  • Week 3: method parameters already use pass-by-value, so changing a parameter does not affect the caller. Part B shows that for arrays the reference is copied, which looks different (§B.2.5).
  • Week 2: operator precedence and left associativity are what make the ++ puzzle solvable.
  • Upcoming: overloading returns together with inheritance, and break may appear in future years.

6. Summary & Self-Assessment

ConceptKey fact
Block scopevisible from the declaration to the closing } of the block, including nested blocks
for-header variableheader + body only; gone after the loop
Variable in a loop bodylives exactly one iteration
Equal namesoverlapping scopes are a compile error; disjoint scopes are fine
Signaturename + ordered parameter types; not the return type or parameter names
Overload resolutionneeds a unique best match; otherwise “no match” or “ambiguous”
x++value is the old x, then x increases by 1
++xx increases by 1, value is the new x
x =+ 3the same as x = 3
a && b, a || bb is skipped if a already decides the result
Fencepost posts need rails: pull one post out of the loop
Sentinel looppriming read + loop, or do-while with an if
Loop typesfor, while, do-while are equally expressive

Q1. Does this code compile? If yes, what does it print?

int s = 0;
for (int i = 0; i < 3; i++) {
    int t = i;
    s += t;
}
System.out.println(s + " " + t);

Q2. Which of the following can coexist in one class with static int h(int a, double b)?
(a) static double h(int x, double y)
(b) static int h(double a, int b)
(c) static void h(int a)
(d) static int h(int b, double a)

Q3. With int x = 5;, what is printed by System.out.println(x-- + " " + --x + x++);, and what is x afterwards?

Q4. Write printSquares(n), which prints 1, 4, 9, ..., n*n with commas between the values and prints nothing for n < 1.

Q5. Rewrite this for loop as an equivalent while loop and as an equivalent do-while loop. Is the do-while version exactly equivalent for every n?

for (int i = 0; i < n; i++) { System.out.println(i); }

Part B: Sequences: Strings, Arrays, References

1. Executive Overview & Core Objectives

A sequence (Sequenz) is an ordered, indexable collection of values of the same type. Java’s two basic sequences are Strings, which are immutable and accessed through methods, and arrays, which are mutable and accessed with the index operator []. Arrays are also the first non-primitive type that makes reference semantics visible. Assignments and parameter passing copy a reference, not the data, and this leads to aliasing, null references and garbage collection.

Learning objectives

  • Use the common String methods and explain why none of them change the string.
  • Distinguish char and String, and convert between them.
  • Compare Strings correctly with equals, and explain why == is wrong for them.
  • Declare, create and initialise arrays, and know their default values.
  • Read and write array elements and loop over arrays safely using length.
  • Avoid ArrayIndexOutOfBoundsException and NullPointerException.
  • Compute statistics such as average, count above average, minimum and maximum with correct initial values.
  • Explain value semantics vs. reference semantics.
  • Predict the effect of aliasing, of modifying an array through a parameter, and of reassigning a parameter.

2. Core Concepts & Formal Definitions

2.1 Strings

Definition (String)

String is a type for sequences of characters (Zeichen).

  • A literal is written in double quotes: "Hallo".
  • A String is an object. Its methods are called as s.method(arguments), and each returns a value such as a String, int, boolean or char.
  • These methods are part of the standard library and need no import.

Immutability

Strings are immutable (unveränderbar). No method changes an existing String. Every String method, and every concatenation, produces a new String at a new place in memory. There is no way to overwrite a single character of an existing String.

Concatenation (Konkatenation). + with at least one String operand concatenates the two operands.

  • firstName + lastName with "James" and "Bond" gives "JamesBond", with no space. This was a bug on the lecture slide, spotted by a student. The correct version is firstName + " " + lastName.
  • "x = " + 42 gives "x = 42". Numbers, whether variables or literals, are appended as their characters. This is an easy way to convert values to Strings.

Text blocks. Long multi-line text can be written between triple quotes """. Line breaks are kept in the output, while the common leading indentation is stripped.

String poem = """
    Roses are red,
    violets are blue
    """;

Indexing. A String of length has characters at indices . Counting starts at 0, as almost everywhere in computer science. The last character is at s.length() - 1.

MethodReturnsMeaning
s.length()intnumber of characters
s.charAt(i)charcharacter at index i
s.indexOf(x)intfirst index of x (a char or a String), or -1 if absent
s.indexOf(x, from)intfirst index of x at or after from, or -1
s.substring(i1, i2)Stringcharacters from i1 inclusive to i2 exclusive
s.substring(i1)Stringfrom i1 to the end
s.toUpperCase() / s.toLowerCase()Stringcopy converted to upper or lower case
s.trim()Stringcopy without leading and trailing whitespace
s.equals(t)booleansame characters (case-sensitive)
s.equalsIgnoreCase(t)booleansame characters, ignoring case
s.startsWith(t) / s.endsWith(t)booleanprefix / suffix test
s.contains(t)booleansubstring test

Note that -1 as a “not found” result is a convention you will see often in Java.

char vs String

  • char is a primitive type holding one character, written with single quotes: 'B'.
  • String is a sequence of characters, written with double quotes: "B".
  • They are different types. A String is never a char, regardless of its length. "B", as returned by s.substring(0, 1), is a String of length 1, not the char 'B'.

Conversions:

  • char → String: "" + c.
  • String → char: s.charAt(0).

Two char values can be compared with ==. Python does not distinguish the two types; Java and many other languages do.

Method chaining. Because every String method returns a String or another value, calls can be chained, for example s.substring(2).toLowerCase().trim(). Chains are evaluated from left to right: ((s.substring(2)).toLowerCase()).trim().

2.2 Comparing Strings: equals vs ==

Rule

Compare String contents only with s1.equals(s2).

  • == is meant for primitive types. For Strings and other objects it compares whether two variables refer to the same object in memory, not whether their characters are equal.
  • The full explanation follows in §2.5 and later in the course.

Exam Trap: == sometimes "works"

== can return true for equal Strings, which hides the bug in your tests.

  • The lecturer explained this as Java being “smart for small strings”. That is imprecise: length does not matter.
  • The real reason is string interning: equal String literals, and compile-time constant expressions such as "12" + "34", share one object in the string pool, so == is true.
  • Strings built at runtime, from variables, user input or method calls, are new objects, so == is false even when the contents are equal.

Exam tests may use different inputs from yours, so a == comparison that passes your tests can fail there.

String a = "1234";
String b = "1234";
System.out.println(a == b);          // true: both literals refer to the same pooled object
 
int four = 4;
String s = "123" + four;             // built at runtime
String t = "12" + "34";              // compile-time constant, pooled
System.out.println(s == t);          // false: different objects
System.out.println(s.equals(t));     // true: same characters

In the lecture, s and t were both meant to be “1234”, and s == t printed false. The exact code on the slide is not recoverable from the recording, so the example above reproduces the same effect.

2.3 Arrays

Definition (Array)

An array (Array, Feld) is a sequence of a fixed number of elements (Elemente), all of the same type.

  • The elements are accessed by index .
  • Example: an int array of length 10 has its first element at index 0 and its tenth element at index 9.

Unlike Python lists, you cannot mix types: {7, "ABC", true, 5} is impossible. The only exception, seen later with inheritance, is that subtypes may be stored in an array of a supertype.

What we want to do with arrays:

  • declare and initialise them,
  • read elements,
  • write elements (unlike Strings),
  • query the length,
  • pass arrays to methods and return them from methods,
  • later: compare and print arrays.

Declaration and creation

An array variable is declared and a new array created like this:

type[] name = new type[length];
  • type[] means “array of type”. It is valid for any type: int, double, boolean, String, …
  • new type[length] creates a new array object with length slots. The exact meaning of new comes later.
  • length can be any int expression. For example, with int x = 4, new String[2 * x] has 8 slots.
  • The length is not part of the type. One variable of type int[] can refer to arrays of different lengths over time.

Default values

new fills every element automatically with a default value:

Element typeDefault
int (and other integer types)0
double0.0
booleanfalse
char'\0' (character code 0; not covered in the lecture)
String and all objectsnull

Declaration without creation. You can declare a variable without creating an array yet:

int[] data;            // declares a variable only; no array exists, no length is needed
data = new int[3];     // now data refers to an array {0, 0, 0}

The variable must be assigned before it is used, as with any local variable.

Index operator (Indexoperator) []. For arrays, [] replaces charAt and allows both reading and writing. An element a[i] behaves exactly like a normal variable:

System.out.println(names[2]);   // read
x = 2 * values[i + 1];          // read, with a computed index
names[i] = "Malte";             // write
prices[2] = 9.95;               // write

Length. The length is the field a.length, without parentheses, unlike s.length() for Strings. Useful indices:

  • first element: a[0],
  • last element: a[a.length - 1],
  • middle element: a[(a.length - 1) / 2], rounded down by integer division. This is useful later for recursion.

Exam Trap: invalid indices

Any index outside throws an ArrayIndexOutOfBoundsException, for both reads and writes. For Strings, charAt throws a StringIndexOutOfBoundsException.

  • There is no Python-style negative indexing. a[-1] is an error. Write a[a.length - 1]. In Python, -1 is just shorthand for len - 1.
  • The classic cause is an off-by-one loop: i <= a.length instead of i < a.length.
  • This is a runtime error (Laufzeitfehler). The compiler cannot know the array length, which may come from user input. As a rule of thumb, an Exception indicates a runtime error.

Array initialiser. When all values are known in advance, they can be written directly:

int[] primes = {2, 3, 5, 7, 11};             // length 5; values may be small expressions
String[] pets = {"Bobby", "Rex", "Felix"};

These forms are equivalent for small arrays. The first is recommended because it is shorter and shows the intent best.

int[] a = {1, 2, 3};                       // (1) initialiser
 
int[] b = new int[3];                      // (2) create with defaults, then set
b[0] = 1; b[1] = 2; b[2] = 3;
 
int[] c;                                   // (3) declare, create, set
c = new int[3];
c[0] = 1; c[1] = 2; c[2] = 3;

The {...} shorthand is only allowed in a declaration. To assign a new array to an existing variable, write a = new int[]{10, 9, 8, 4};.

Always loop with .length

Write for (int i = 0; i < a.length; i++) rather than a hard-coded length like 7.

  • If the size changes later, you would otherwise have to update it in several places.
  • Inside a method that receives an array, you cannot know the length anyway. A parameter of type int[] accepts int arrays of any length. There is no type such as “int array of length 27”.

2.4 Value semantics vs reference semantics

Definition (Value semantics)

Value semantics (Wertesemantik) means that assignment and parameter passing copy the value. This is call-by-value / pass-by-value.

  • Java uses value semantics for all primitive types: int, double, boolean, char, …
  • After y = x, the variables x and y are separate memory cells.

Definition (Reference semantics, aliasing)

Reference semantics (Referenzsemantik) means that a variable holds a reference (Referenz), which is a pointer to an object, and assignment copies only that reference.

  • Java uses reference semantics for all non-primitive types, including arrays and Strings.
  • After arr2 = arr1, both names refer to the same object.
  • Two names for one object is called aliasing. A change made through one name is visible through the other.

This split is Java-specific. Every language has some notion of both semantics, and in C++ you can choose per variable. In Java the choice is fixed by the type: primitives use value semantics, everything else uses reference semantics.

Java uses references for two reasons:

  • Efficiency: passing a large array to a method should not copy the whole array.
  • Sharing: sometimes several parts of a program should work on the same data set.

Definition ( null, NullPointerException)

  • null is the reference that points to no object. You can assign it (int[] arr = null;) and print it, which prints null.
  • Accessing an element or the length of a null reference (arr[0], arr.length) throws a NullPointerException. This is why “Java has no pointers” is not quite true.
  • Guard against both exceptions with a combined check:
if (data != null && data.length > 0) {
    System.out.println(data[0]);
}
  • The check relies on short-circuit evaluation from Part A: if data is null, then data.length is never evaluated.

Definition (Unreachable object, garbage collection)

  • An object becomes unreachable when no reference variable points to it any more. It can then no longer be read, changed or deleted.
  • Unreachable objects do not affect program correctness, but many large ones waste memory.
  • In some languages you must free memory explicitly. In Java, the garbage collector (Garbage Collector) reclaims unreachable objects automatically, at a time of its choosing that you cannot control.
  • This only matters for long-running or time-critical programs. It returns at the end of the course and is not a concern for now.

3. Step-by-Step Derivations

3.1 The String toUpperCase trap

String text = "Hello Everyone";
text.toUpperCase();
System.out.println(text);

This prints Hello Everyone. toUpperCase() returns a new String, but the result is discarded, and text still refers to the unchanged original.

System.out.println(text.toUpperCase());   // HELLO EVERYONE: the new String is used directly
System.out.println(text);                 // Hello Everyone: the original is unchanged

To make the variable hold the uppercase version, you need an assignment:

text = text.toUpperCase();   // the variable now refers to the new String
System.out.println(text);    // HELLO EVERYONE

The old String object is not modified. The variable is simply redirected to a new object.

3.2 Why the end index of substring is exclusive

To extract a known word from :

int start = s.indexOf(w);                         // index of the first character of w
String part = s.substring(start, start + w.length());

Because the end index is exclusive, start + length is exactly the right bound, with no - 1 correction. Other consequences of the same convention:

  • has length .
  • substring(0, k) and substring(k) split into two parts with no overlap.

3.3 “Changing” one part of a String

You cannot set the 15th character to uppercase directly. Instead, split the String, transform the middle part, and concatenate:

String s = "the quick brown fox";
String word = "brown";
int start = s.indexOf(word);
int end = start + word.length();
 
String before = s.substring(0, start);
String middle = s.substring(start, end).toUpperCase();
String after  = s.substring(end);
 
s = before + middle + after;     // "the quick BROWN fox"

3.4 Why the minimum must not start at 0

The lecture example computes statistics over a double[] height:

double minHeight = 0;                       // BUG
for (int i = 0; i < height.length; i++) {
    if (height[i] < minHeight) {
        minHeight = height[i];
    }
}

All heights are positive, so height[i] < 0 is never true. The minimum stays the dummy value 0, which is not an element of the data.

Principle: the initial value must be one that every real element beats.

  • For a minimum, start very large: Double.MAX_VALUE for doubles, Integer.MAX_VALUE for ints.
  • For a maximum, start very small: Integer.MIN_VALUE for ints, -Double.MAX_VALUE for doubles.
  • The lecturer first tried 3.0, reasoning that no human is taller than 3 m. That works, but it relies on an assumption about the data.
  • maxHeight = 0 happened to work in the lecture only because all heights are positive.
  • The lecturer also wrote Integer.MAX_VALUE in code that used double heights, then corrected himself: for doubles the right constant is Double.MAX_VALUE.

Exam Trap: Double.MIN_VALUE is not negative

Double.MIN_VALUE is the smallest positive double, approximately . It is not the most negative double.

  • As the start value for a maximum over data containing negative numbers, it is wrong.
  • Use -Double.MAX_VALUE or Double.NEGATIVE_INFINITY instead.
  • This was not covered in the lecture but follows directly from it.

A robust alternative that needs no sentinel at all (not shown in the lecture): start with the first element and loop from index 1. This assumes a.length > 0.

int max = a[0];
for (int i = 1; i < a.length; i++) { if (a[i] > max) max = a[i]; }

The slides also showed tracking the index of the maximum:

int max = Integer.MIN_VALUE;
int maxIndex = -1;                      // dummy index
for (int i = 0; i < a.length; i++) {
    if (a[i] > max) {
        max = a[i];
        maxIndex = i;
    }
}

4. Worked Examples & Applications

4.1 String warm-ups from the lecture

  • "1234".length() is 4.
  • "1234".charAt(1) is '2', because counting starts at 0. Students guessed this correctly.
  • For the 8-character String the lecturer used: index 0 is the first character and index 7 is the last.
  • "1234".charAt(4) throws StringIndexOutOfBoundsException. “We will all do this at least once.”
  • "12333".indexOf('3') is 2, the first occurrence.
  • "1233 abc 3".indexOf('3', 4) is 9. The search starts at index 4, so the 3s at indices 2 and 3 are skipped. The lecture’s own string gave 8; the principle is the same.
  • "1234".indexOf('5') is -1, meaning not found.

Counting occurrences of a character:

public static int count(char c, String s) {
    int count = 0;
    for (int i = 0; i < s.length(); i++) {   // indices 0 .. length-1
        if (s.charAt(i) == c) {              // chars may be compared with ==
            count++;
        }
    }
    return count;
}

4.2 Motivation: sports team statistics

The task: read heights (the team members happen to be giraffes), print the average, and print how many members are taller than the average.

The first version computes only the average:

Scanner console = new Scanner(System.in);
System.out.print("How many members? ");
int members = console.nextInt();
double sumHeight = 0;
for (int i = 0; i < members; i++) {
    System.out.print("Height of member " + (i + 1) + ": ");
    sumHeight += console.nextDouble();
}
double averageHeight = sumHeight / members;
System.out.println("Average height: " + averageHeight);

For the inputs 1.50, 1.60, 1.70, it printed something like 1.6000000000000003. This is floating-point rounding (Gleitkomma-Ungenauigkeit), as known from week 2.

The problem. Counting “taller than average” during input is impossible, because the average is only known after all values have been read. So the values must be stored. Separate variables height1, height2, … do not scale: a team of 60 breaks a program written for 50, and an artificial upper bound of 100 is not a clean solution either. What is needed is a number of variables chosen at runtime, which is exactly what an array provides.

The complete version, as finished on the slides:

Scanner console = new Scanner(System.in);
System.out.print("How many members? ");
int members = console.nextInt();
 
double[] height = new double[members];        // one slot per member, initialised to 0.0
double sumHeight = 0;
for (int i = 0; i < height.length; i++) {
    System.out.print("Height of member " + (i + 1) + ": ");
    height[i] = console.nextDouble();          // store the value
    sumHeight += height[i];
}
double averageHeight = sumHeight / members;
 
int numberTaller = 0;
double maxHeight = 0;                          // fine here because heights are positive
double minHeight = Double.MAX_VALUE;           // NOT 0 (see §3.4)
for (int i = 0; i < height.length; i++) {      // second pass over the stored data
    if (height[i] > averageHeight) {
        numberTaller++;
    }
    if (height[i] > maxHeight) {
        maxHeight = height[i];
    }
    if (height[i] < minHeight) {
        minHeight = height[i];
    }
}
System.out.println("Average: " + averageHeight);
System.out.println(numberTaller + " members are taller than average");
System.out.println("Max: " + maxHeight + ", Min: " + minHeight);

Tracing the inputs 1.20, 1.70, 2.00:

  • sum , average ,
  • taller than average: 1.70 and 2.00, so the count is 2,
  • max , min .

With the hard-coded heights {1, 2, 3, 4, 6, 9} used in the lecture:

  • average ,
  • taller: 6 and 9, so the count is 2,
  • max , min , provided the minimum starts large rather than at 0.

4.3 Array trace from the slides

int[] data;               // declared only; no array yet
data = new int[3];        // data -> {0, 0, 0}
System.out.println(data[2]);   // prints 0 (default value)
data[1] = -2;             // data -> {0, -2, 0}
int i = 5 / 2;            // integer division: i = 2
data[i] = 1;              // data -> {0, -2, 1}
System.out.println(data[data[2]]);   // data[2] = 1, so data[1] = -2; prints -2

The last line shows that an element of an int array can itself be used as an index.

4.4 Out-of-bounds demos

double[] height = new double[3];
System.out.println(height[-1]);           // ArrayIndexOutOfBoundsException
 
for (int i = 0; i <= members; i++) { ... height[i] ... }   // fails when i == members

In the second demo, the program compiled, read the input and printed the average. It crashed only when the loop reached the non-existent index members, which confirms that this is a runtime error.

4.5 Swap: primitives vs arrays

public static void swap(int a, int b) {    // receives copies
    int tmp = a; a = b; b = tmp;
}
int x = 1, y = 2;
swap(x, y);       // x == 1, y == 2: unchanged (pass-by-value)
 
public static void swap(int[] a) {         // receives a copy of the REFERENCE
    int tmp = a[0]; a[0] = a[1]; a[1] = tmp;
}
int[] pair = {1, 2};
swap(pair);       // pair == {2, 1}: changed, because the method used the same array

The method has no separate copy of the array; it works on the caller’s array.

4.6 Aliasing by assignment

int x = 1;
int y = x;        // copy: separate cells
y++;              // y == 2, x == 1
 
int[] array1 = {7, 8, 9};
int[] array2 = array1;     // copy of the reference: both point to the same array
array2[0]++;               // modifies the shared array
System.out.println(array1[0]);   // 8: the change is visible through array1

Student question: what about this?

int a2 = a[0];
a2 = 5;

Does this change a? No. a[0] is an int, which is a primitive, so a2 = a[0] copies the value. Once you break an array down to its elements, you are back to value semantics. With arrays of objects, the elements would be references and the situation would change, which comes later.

Two separately created arrays with equal contents are different objects. == returns false for them, just as for Strings. Explicit copying of arrays will be shown later.

4.7 Clicker question: modifying vs reassigning a parameter

public static void plusOneEach(int[] ys) {
    for (int i = 0; i < ys.length; i++) {
        ys[i]++;
    }
}
 
public static void main(String[] args) {
    int[] xs = {2, 4, 6};
    int[] ys = {2, 4, 6};
    plusOneEach(xs);
    System.out.println(Arrays.toString(xs));   // [3, 5, 7]
    System.out.println(Arrays.toString(ys));   // [2, 4, 6]
}

Arrays.toString prints an array as [a, b, c]; it will be introduced later. Most students answered correctly.

  • The parameter ys receives a reference to the same array as main’s xs, so incrementing through it changes that array.
  • main’s own ys is a different array and stays unchanged.
  • The fact that the parameter and main’s second array are both called ys is irrelevant, because their scopes are disjoint (Part A, §2.2).

Follow-up, where most of the class answered wrong: add a reassignment at the end of the method.

public static void plusOneEach(int[] ys) {
    for (int i = 0; i < ys.length; i++) {
        ys[i]++;
    }
    ys = new int[]{10, 9, 8, 4};    // reassign the parameter
}

What does main now print for xs? Still [3, 5, 7]. Here is the trace:

  1. Before the call: xs → {2, 4, 6}.
  2. Call: the parameter ys receives a copy of the reference, so ys → the same array.
  3. The loop modifies the array through ys, giving {3, 5, 7}. This is visible through xs.
  4. ys = new int[]{…} redirects (umhängen) only the local reference ys to a new array. xs in main is a different variable and still points to {3, 5, 7}.
  5. When the method ends, ys disappears and the array {10, 9, 8, 4} becomes unreachable.

Key distinction

  • Modifying the object through a reference (ys[i] = …) is visible to every alias.
  • Reassigning the reference (ys = …) only changes where this variable points. It never affects other variables, including the caller’s.

Student questions:

  • Can we get back to the old array after reassigning? Only if a reference to it was saved, for example in a temporary variable. Otherwise that connection is lost.
  • Why is ys = … possible without a type? ys was already declared as the parameter int[] ys, so this is a plain assignment. With an assignment, the shorthand {…} is not allowed and new int[]{…} is required.

4.8 Redirecting references (slide example)

int[] myData = {1, 2, 3};
int[] yourData = {4, 5, 6};
int[] data = myData;       // alias of the first array
data[0] = 99;              // myData is now {99, 2, 3}
data = yourData;           // redirect: data now points to the second array
data[0] = 77;              // yourData is now {77, 5, 6}; myData is unaffected

4.9 Lost object (slide example)

char[] letters = {'h', 'e', 't'};
char[] l = {'p', 'l', 'e', 'f'};
letters = l;      // both now refer to {'p','l','e','f'}

The array {'h','e','t'} had only one reference, letters, and it has been redirected. That array is now unreachable garbage, which the garbage collector will eventually reclaim.

Exam Trap collection

  • Calling s.toUpperCase(); without assigning the result changes nothing.
  • s.substring(0, 1) returns a String, not a char.
  • Comparing Strings with == is a bug, even when your tests happen to pass.
  • The array length is a.length; the String length is s.length().
  • The last element is at length - 1, and a[-1] throws an exception.
  • Starting a minimum at 0, or a maximum at Double.MIN_VALUE, gives wrong results.
  • int[] b = a; copies the reference, not the array.
  • Reassigning a parameter inside a method never changes the caller’s variable.
  • Using .length or [i] on null throws a NullPointerException.

5. Intuition & Conceptual Bridges

Intuition: boxes and arrows

  • A primitive variable is a box containing a value. Assignment copies the content into another box.
  • A reference variable is a box containing an arrow to an object somewhere in memory. Assignment copies the arrow, so now two arrows point to the same object.
  • Writing a[i] = … follows the arrow and changes the object.
  • Writing a = … repaints the arrow itself.

Always draw these diagrams when tracing aliasing code.

Intuition: why Strings feel like values

Strings use reference semantics too. Because they are immutable, nobody can change the shared object, so aliasing Strings is harmless. The only visible symptom of their reference nature is that == compares arrows, not content.

Connections

  • Part A, pass-by-value: Java is always pass-by-value. For arrays, the value being copied is the reference. That one fact explains both §4.5 and §4.7.
  • Part A, fencepost: printing an array as [3, 5, 7] is another “separator between items” problem.
  • Part A, short-circuit: data != null && data.length > 0 is safe only because && skips its right side.
  • Part A, scope: a parameter with the same name as a caller’s variable is a separate variable.
  • Upcoming: the remaining array chapter (copying, comparing, printing), then recursion. Middle indices like (length - 1) / 2 will be useful there. Later topics include objects, new, null and inheritance (arrays of supertypes, equals vs ==), plus main(String[] args), which receives command-line arguments, and garbage collection at the end of the course.

6. Summary & Self-Assessment

TopicKey fact
Stringimmutable object; methods return new Strings
Index range
substring(i, j), length
indexOffirst occurrence, or -1
char vs String'c' vs "c"; never the same type
Comparing Stringsequals, never ==
Array creationT[] a = new T[n]; or T[] a = {…};
Defaults0, 0.0, false, null
Lengtha.length (array) vs s.length() (String)
Bad indexArrayIndexOutOfBoundsException at runtime
Primitive typesvalue semantics: assignment copies the value
Non-primitive typesreference semantics: assignment copies the reference (aliasing)
Parameter int[] pp[i] = … is visible to the caller; p = … is not
nullno object; access throws NullPointerException
Unreachable objectreclaimed by the garbage collector

Q1. What is printed?

String s = "Programming";
s.substring(3).toUpperCase();
String t = s.substring(3, 7);
System.out.println(s + " " + t + " " + t.length() + " " + s.indexOf("g") + " " + s.indexOf('z'));

Q2. What are the contents of a and b at the end?

int[] a = {1, 2, 3};
int[] b = a;
b[0] = 10;
b = new int[]{7, 8};
b[1] = 20;
a[2] = b[1] + a.length;

Q3. Write static int indexOfMin(int[] a), which returns the index of the smallest element and returns -1 if a is null or empty. Explain your choice of initial values.

Q4. After the call, what does main print, and why?

public static void mystery(int[] arr, int k) {
    arr[0] = k;
    k = 100;
    arr = new int[]{k, k};
    arr[1] = -1;
}
public static void main(String[] args) {
    int[] nums = {5, 6};
    int k = 3;
    mystery(nums, k);
    System.out.println(nums[0] + " " + nums[1] + " " + k);
}

Q5. Explain why "ab" == "ab" is true but "a" + x == "ab" is false (with String x = "b";), and give the correct comparison.