Einführung in die Programmierung (252-0027), ETH Zürich. Slides: “3. Kontrollstrukturen”, sections 3.1–3.4. Lecturers: Malte Schwerhoff (3.1), Manuela Fischer (3.2–3.3).

Sources and coverage

  • Week 3 Tuesday, second half: start of 3.1: if, if-else, boolean, relational operators, = vs ==.
  • Week 3 Thursday: rest of 3.1: logical operators (recap, since they were already covered in the exercise session), separate ifs vs else if, the three if constructs, style, ternary operator, braces. Short-circuit evaluation was only referenced: “covered in the exercise session, details on the slides”.
  • Week 3 Friday: 3.2 (all loops of part I) and 3.3.1–3.3.2 (parameters, return values) up to “missing return statement”.
  • 3.3.3 (scope), 3.3.4 (overloading) and all of 3.4 were announced for the following Tuesday. No recording of them was provided, so these parts are built from the slides alone; all slide examples were worked out and checked.
  • Where slides contain bugs or the lecturer misspoke, the correct version is given and the discrepancy is flagged.

1. Executive Overview & Core Objectives

Until now programs ran linearly, top to bottom. This chapter introduces control structures (Kontrollstrukturen):

  • branches (Verzweigungen: if, if-else, else if, ?:) controlled by boolean expressions (Boolesche Ausdrücke);
  • loops (Schleifen: for, while, do-while) for repetition.

It completes methods with parameters (passed by value), return values, scope (Namensraum) and overloading (Methodenüberladung). It ends with increment/decrement and their side effects, typical loop errors (fencepost, off-by-one, infinite loops) and input-controlled sentinel loops.

Learning objectives. After this week you should be able to:

  • evaluate boolean expressions with relational and logical operators, including precedence, short-circuit evaluation and De Morgan’s laws;
  • choose between independent ifs, if/else if/else, and else if without a final else, depending on whether the cases must exclude each other;
  • write and trace for, nested for, while and do-while loops, and derive loop bounds from a table of the output pattern;
  • write methods with parameters and return values, explain value semantics, and determine where each variable is visible;
  • detect “missing return statement” by thinking like the compiler, and resolve overloaded calls;
  • evaluate expressions with ++/-- (pre/post) and compound assignments, and recognise when short-circuiting suppresses a side effect;
  • solve fencepost problems and write sentinel loops that don’t process the sentinel.

2. Core Concepts & Formal Definitions

3.1 Branches (Verzweigungen)

Definition (if statement, If-Anweisung)

Executes a group of statements A only if a condition test is true (conditional execution, bedingte Ausführung). Execution then continues with C either way.

if (test) {
    A        // if-block
}
C

Control-flow diagram: test branches to “yes”, which runs A and then C, or “no”, which runs C directly.

Definition (if-else statement, If-else-Anweisung)

Executes either A or B, depending on test. Then C.

if (test) {
    A        // if-block
} else {
    B        // else-block
}
C

Intuition from the lecture: in the morning you check the fridge. If there is milk, you drink it; else you go to the supermarket.

double punkte = console.nextDouble();
if (punkte >= 50.0) {
    System.out.println("Prüfung bestanden");
} else {   // punkte < 50.0
    System.out.println("Prüfung nicht bestanden");
}

Definition (Boolean expression, Boolescher Ausdruck)

An expression that is either true or false. It is used as the condition of if statements.

  • Its type is boolean, a primitive type with exactly two values, true and false, which are also its only two literals.
  • Examples: the literals true, false; comparisons such as x >= 0.

Definition (Relational operators, Vergleichsoperatoren)

OperatorMeaningExampleValue
==equal1 + 1 == 2true
!=not equal3 != 2true
<less than10 < 5false
>greater than10 > 5true
<=less than or equal126 <= 100false
>=greater than or equal5 >= 5true
  • Not every operator makes sense for every type. 1 < true is a compiler error. Java does not give integers a truth value, unlike C/C++ or Python. Java is strict: the code might be a mistake, so you must state your intention clearly.
  • Relational operators have lower precedence than arithmetic ones. 1 + 1 == 2 means (1 + 1) == 2, not 1 + (1 == 2); the latter would be int + boolean, a compile error. The lecturer demonstrated both: the explicit 1 + (1 == 2) is rejected.

Discrepancy: the lecturer said he believed all relational operators have the same precedence. Correctly, < <= > >= bind tighter than == !=. It rarely matters, because chaining them is a type error anyway (next callout).

Exam Trap: No chained comparisons

2 <= x <= 10 is common in maths and allowed in Python, but not in Java. The operators are left-associative, so this means (2 <= x) <= 10, i.e. true <= 10 or false <= 10. That is a compile error (“bad operand types for operator <=”). Write 2 <= x && x <= 10. Chaining is just notation; nothing is lost.

Definition (= vs ==)

  • == is the comparison operator: a truth value stating whether both sides are equal.
  • = is the assignment operator: x = e has the type of x and evaluates to the new value of x. In general, therefore, it is not a truth value.
int x = 0;
if (x = 0) { … }   // ✗ compile error: incompatible types: int cannot be converted to boolean

Example: The degenerate boolean case (lecture demo)

boolean b = false;
if (b = true) {
    System.out.println("This will never be printed.");
} else {
    System.out.println("This will always be printed.");
}
// Output: This will never be printed.

The class predicted a compile error. But since b is a boolean, the assignment b = true evaluates to true and is a valid condition. It is equivalent to b = true; if (b) …. With an int variable the same slip is a compile error. In languages without a strict int/boolean separation, this mistake is even easier to make.

Definition (Logical operators, Logische Operatoren)

Boolean expressions are combined with logical operators into new boolean expressions:

OperatorMeaningExampleValue
&&and(2 == 3) && truefalse
||or(2 == 3) || (-1 < 5)true
!not!(2 == 3)true

Precedence: ! before && before ||. && and || are left-associative; ! (unary) is right-associative.

Truth table (Wahrheitstabelle):

p && qp || q!p
truetruetruetruefalse
truefalsefalsetruefalse
falsetruefalsetruetrue
falsefalsefalsefalsetrue

Remark (XOR and NAND; Thursday student discussion)

The lecturer: Java doesn’t provide XOR or NAND, but &&, || and ! suffice to express every boolean function, a simple provable fact. If needed often, you can wrap the combination in a method:

  • XOR: (a || b) && !(a && b)
  • NAND: !(a && b)

Correction: Java does have ^ (XOR) for booleans, and a != b also computes XOR. The course works with &&, || and !.

Operator precedence table, so far (highest first):

LevelOperatorsAssociativity
1postfix x++ x--
2unary ++x --x + - ! (type)right
3* / %left
4+ - (incl. string concatenation)left
5< <= > >=(not chainable)
6== !=left
7&&left
8||left
9? :right
10= += -= *= /= %=right

Definition (if-else-if, If-else-if-Anweisung)

A test cascade. Tests are checked in order, and the first true one wins. Its block runs, and only that block. The final else is entered only if all previous tests failed. Any number of else if blocks is possible.

if (test1) {
    A        // if-block
} else if (test2) {
    B        // else-if-block
} else {
    C        // else-block
}
D

Student question: what is D? Just the next statement after the whole cascade. It is always executed, regardless of the tests.

if (x > 0) {
    System.out.println("positive");
} else if (x < 0) {
    System.out.println("negative");
} else {   // x == 0
    System.out.println("zero");
}
 
if (place == 1) {
    System.out.println("Gold!");
} else if (place == 2) {
    System.out.println("Silver!");
} else if (place == 3) {
    System.out.println("Bronze!");
} else {
    System.out.println("No luck!");
}

“If you win gold, you don’t get silver and bronze thrown in as well.”

The three if constructs (characterised together with the class)

ConstructShapeProperty
if / else if / elseif (t1) {A} else if (t2) {B} else {C}mutual exclusion, exactly one of A, B, C
if / else if (no final else)if (t1) {A} else if (t2) {B} else if (t3) {C}mutual exclusion, at most one of A, B, C (possibly none)
independent ifsif (t1) {A} if (t2) {B} if (t3) {C}no mutual exclusion; any number of A, B, C, no guarantee of even one

Definition (Ternary operator, ternärer Operator)

test ? valueTrue : valueFalse

  • test is a boolean expression. valueTrue and valueFalse are expressions, typically of the same type T.
  • The result has type T: valueTrue if test is true, otherwise valueFalse.
  • It is an expression, while if is a statement.
if (test) { System.out.println("A"); } else { System.out.println("B"); }
System.out.println(test ? "A" : "B");      // same effect

Use: remove redundancy when both branches differ only in a value. Trade-off: simpler, more local control flow, but more complex expressions. It isn’t always better.

Definition (Omitting braces)

If a block consists of exactly one statement, the braces may be omitted: if (test) statementTrue; else statementFalse;

Recommendation: always use braces. If someone later adds a second statement, or merely indents it, it is outside the braces and no longer protected by the test. In the Java community, braces are standard; in C++ less so.

“If you think you’d never make this mistake, search for Apple goto fail”: very well-paid engineers made exactly this mistake and caused a serious security bug.

Definition (Short-circuit evaluation, Kurzschlussauswertung)

For p && q and p || q, Java evaluates the left operand p first, and stops as soon as the result is known:

  • p && q: if p is false, the result is false, so q is not evaluated.
  • p || q: if p is true, the result is true, so q is not evaluated.

In other words, q is evaluated for && only if p is true, and for || only if p is false.

Consequences of short-circuiting

  • && and || are not commutative in Java. p && q is not necessarily equivalent to q && p, and the same for ||. This is discussed later.
  • Possible side effects of q happen only conditionally.

Reasons for the design:

  • Safety: q may only be evaluated under certain conditions p (p && q).
  • Efficiency: q needn’t be evaluated if p already succeeded (p || q).

Definition (De Morgan's laws)

Distribution rules for negating boolean expressions:

OriginalNegatedNegated (De Morgan)
a && b!(a && b)!a || !b
a || b!(a || b)!a && !b

Negating a comparison flips it: !(x == 7) is x != 7; !(y > 3) is y <= 3. So these are equivalent:

if (x == 7 && y > 3) { A } else { B }
if (x != 7 || y <= 3) { B } else { A }

3.2 Loops I (Schleifen I)

Definition (Loop, Schleife; iteration)

A loop executes a section of code repeatedly. Each execution of the section is an iteration (Iteration).

Motivation: writing a line 5, 100 or 1000 times by copy-paste is tedious and error-prone. You have to count the copies, and a change such as numbers up to 100 instead of 10 must be made everywhere. The lecture’s examples were The Shining, the Simpsons chalkboard gag, and a random times-table quiz copied ten times.

3.2.1 The (simple) for loop

Definition (for loop, for-Schleife)

Syntax (simplified):

for (initialization; test; update) {   // head (Kopf)
    body                                 // body (Rumpf, Körper)
}
statement;
  • initialization: a variable declaration or assignment.
  • test: a boolean expression, the continuation condition (Fortsetzungsbedingung).
  • update: an expression with a side effect.

Semantics:

  1. Execute initialization once.
  2. Repeat until termination:
    1. Check whether test evaluates to true. If not, terminate: jump to statement.
    2. Execute the body.
    3. Execute update.

Typical use (intended behaviour; the compiler does not check it)

  • initialization initialises the loop variable (Schleifen-/Laufvariable). A variable declared there is valid only inside the loop.
  • test checks whether the loop variable has reached a certain value, typically with < <= > >= == !=.
  • update changes the loop variable and brings it closer to the termination value.
  • Inside the body you can read the loop variable like a normal variable, e.g. print i to count from 1 to 100.
  • An already existing variable can also be used as the loop variable.
  • The compiler does not enforce the intended use. true as the test, or updating j instead of i, gives an infinite loop. IntelliJ sometimes warns, e.g. “Condition j <= 100 is always true”.
for (int i = 1; i <= 5; i = i + 1) {
    System.out.println(i);            // 1 2 3 4 5 (one per line)
}

Changing the 5 to 100 repeats the body 100 times; no copying is needed. Python’s for looks different, but the principle is the same.

3.2.2 Nested for loops (verschachtelte Schleifen)

Definition (Nested loop)

A loop inside the body of another loop, typically with a different loop variable (here j). For each iteration of the outer loop, the inner loop is executed completely. With 5 outer and 10 inner iterations, the inner body runs 50 times.

Picture a 2D table: i indexes the rows, j the columns.

Any statement can be in a loop body: if/else, other loops, method calls. The inner loop’s bounds may depend on the outer variable.

3.2.3 The while loop

Definition (Definite vs. indefinite loops)

  • Definite loop (bestimmte Schleife), also counting loop (Zählschleife): the number of iterations is known before the loop starts. Examples: print the first 10 squares; print all odd numbers between 7 and 91. Typically a for loop: one glance at the head shows how often it runs.
  • Indefinite loop (unbestimmte Schleife): the number of iterations is not known beforehand; it may depend on a complex condition or on user input. Examples:
    • read input until the user types a negative number (or 42);
    • read a file until three consecutive sentences end with ”!”;
    • accept deposits until a certain amount is exceeded.

Definition (while loop, while-Schleife)

while (test) {
    body
}
statement;

The body runs as long as test evaluates to true; the loop stops as soon as test is false. test is checked before every iteration, so the body may run zero times.

Unlike for, there is no initialisation and no update in the head. If you need a loop variable, initialise it before the loop and update it inside the body; you are responsible for both.

Minor slip: the lecturer once said “we jump out of the for loop” while explaining the while loop.

A counting loop can be written as while (int i = 1; while (i <= 100) { …; i = i + 1; }), but people would wonder why you didn’t use for.

3.3 Methods II (Methoden II)

Recap: a method is a named sequence of statements that runs whenever the method is called. A method call is also a control structure: jump to the body, execute it, jump back.

3.3.1 Parameters (Parameter)

Motivation:

  • Two methods vorsatzManuela() and vorsatzMalte() differ in exactly one place, the name.
  • Changing the vow (“I will always do the exercises”) means editing both.
  • A third person needs a third method. This is not maintainable.
  • “Two methods that do the same thing except for one value: that screams for methods with parameters.”

Definition (Parameter, Parametrisierung)

Parametrising a method means giving it parameters. A parameter is a value that the called method receives from the caller.

  • Declaration: public static void methodName(parameterType parameterName) { … }. The method states that it needs a parameter, with its type (int, double, boolean, String, …) and name.
  • Call: methodName(parameterValue);. The value is any expression of type parameterType. It is stored in the parameter variable, and the method’s statements run with that value.
  • Inside the method: the parameter is used like a variable.

Definition (Formal vs. actual parameter)

  • Formal parameter (formaler Parameter): the parameter variable declared by the method, e.g. String name.
  • Actual parameter (tatsächlicher Parameter), also argument (Argument): the value passed at a call. It can differ per call: "Manuela", "Malte", "Hans", or a variable.
  • At each call, the formal parameter is initialised with the value of the actual parameter. This is nothing but an assignment, so the usual assignment and conversion rules apply.

Definition (Multiple parameters)

Separated by commas, in declaration and call:

public static void name(type1 name1, type2 name2, …, typeN nameN) { … }
name(value1, value2, …, valueN);

Arguments are matched to parameters by position, not by name.

Definition (Value semantics, Wertübergabe)

For primitive parameter types (int, boolean, …):

  • The caller evaluates the argument expression and copies the result into the parameter variable.
  • The called method only receives a copy. It does not know where the value came from, and it does not know the caller’s variables.
  • Changes to the parameter variable have no effect on the calling method.
  • The parameter variable is a new variable, even if it happens to have the same name as a variable in the caller. It disappears when the call ends.

More general types come later.

No choice between by-value and by-reference in Java (student question)

In C++ you choose per parameter whether to pass by value or by reference. In Java the language decides: primitive types are always passed by value (copied), and certain other types are always passed by reference. You can simulate other behaviour, but you can’t choose it. References are covered later.

3.3.2 Return values (Rückgabewerte)

Definition (Return value, Rückgabewert)

The counterpart to parameters:

  • Parameters send values from the caller into the called method.

  • Return values send a value from the called method back to the caller.

  • The return type is written before the method name. void means “no return value”; that explains the void seen all along.

  • The value is returned with a return statement inside the method.

public static returnType name(…) {
    …
    return expression;
}

Definition (Return statement, Rückgabeanweisung)

return expression;

  1. Evaluates expression.
  2. Returns the value to the caller (a copy, for primitive types).
  3. Terminates the called method.

The call is “replaced” by the return value, so a call can be part of an expression, like in maths. Parentheses around the returned expression are optional; return x + 1; is fine.

Definition (Local variables)

Variables declared inside a method, including its parameters, are local. They are not visible to the caller and disappear when the call terminates. The caller only receives a copy of the return value.

Rules for return statements

  • Methods without a return value (void) need no return, but may contain return; to leave the method early. It sends no value back.
  • Methods with a return value need at least one return. Otherwise it is a compile error.
  • More than one return is allowed and sometimes sensible, especially for case distinctions with if-else.
  • Every execution path through the method must end in a return. The compiler checks this by looking at the structure of the code (§3, “how the compiler thinks”).
  • You can’t continue computing after return. If you need more work, store intermediate results and return at the end.

Definition (Ignored return values)

Return values may be ignored. squareNumber(3); computes 9 and discards it, with no warning or error. This is not useful yet, but becomes so for methods with side effects, where you only want the effect.

3.3.3 Scope (Namensräume) (slides only)

Definition (Scope)

The scope of a name is the part of the program in which the name/symbol is visible.

  • A variable can only be read or modified within its scope.
  • A method can only be called within its scope (more on method visibility later).

The rules below are an approximation; further aspects follow later.

Scope rules for variables

  • General: from the declaration to the end of the current block ({ … }), including all enclosed (nested) blocks.
  • In methods: a variable declared in a method, including parameter variables, exists only in that method.
  • In branches: a variable declared in an if/else block exists only in that block.
  • In a for head: a variable declared in the head of a for loop exists only in that loop (head and body).
  • In a loop body: a variable declared in the body exists for one iteration only. The next iteration creates a new variable with the same name.
  • Unique declaration: the scopes of variables with the same name must not overlap. Variables with non-overlapping scopes may share a name; they are two different variables that happen to be called the same.

Why scopes?

  • Readability: the same names can be reused in different places and contexts.
  • Simpler memory management (not relevant for EProg).

3.3.4 Method overloading (Methodenüberladung) (slides only)

Definition (Signature, overloading)

  • The signature (Signatur) of a method is its name plus the list of its parameter types: methodName(type1, type2, …, typeN). Parameter names and return type are not part of it.
  • Several methods with the same signature are not allowed. Compiler error: “method is already defined”.
  • Several methods with the same name are allowed if they have different parameter types and/or a different number of parameters. This is method overloading.
  • At a call, the most fitting method is chosen. There must be a unique best method, otherwise it is a compiler error.

3.4 Loops II (Schleifen II) (slides only)

3.4.1 Increment and decrement (Inkrement und Dekrement)

Definition (Shorthand forms)

FormNameEffectValue of the expression
x++post-increment (Post-Inkrement)x = x + 1the old x: read first, then change
x--post-decrementx = x - 1the old x
++xpre-increment (Prä-Inkrement)x = x + 1the new x: change first, then read
--xpre-decrementx = x - 1the new x
x += vx = x + vthe new x
x -= v, x *= v, x /= v, x %= vanalogousthe new x

These work for double too: double y = 2.4; y--; gives 1.4.

Exam Trap: =+ is not +=

x =+ 3 parses as x = (+3): an assignment of positive 3, not an increment. It compiles silently.

Definition (Side effect, Seiteneffekt)

Strongly simplified, there are two kinds of code components:

  • those that have a value: literals, expressions;
  • those that have an effect: statements, which change the program’s state.

Expressions with side effects change the state too: increment/decrement (x++, x--), assignments, operations that raise errors. Methods with side effects come later.

Recommendations for shorthands

  • Shorthands are nice but dangerous, especially with short-circuit evaluation.
  • The goal is understandable programs; reading code should not be a puzzle. You must still recognise increments and decrements.
  • Recommendation: use the shorthand only as a stand-alone statement, never inside larger expressions. Use it mainly in loop updates: for (int i = 0; i < n; i++).

3.4.2 Typical errors (Typische Fehler)

Definition (Fencepost error, Zaunpfahlfehler)

A fence of posts (Pfosten, Pfähle) has only rails (Latten, Querstreben). A loop that always does “post + rail” (or “rail + post”) produces one rail too many.

Solution: move one post outside the loop, the first or the last one, and loop times.

for (int i = 1; i <= n - 1; i++) { post(); rail(); }
post();                                  // last post outside
 
post();                                  // first post outside
for (int i = 1; i <= n - 1; i++) { rail(); post(); }

Definition (Off-by-one error)

The loop runs once too often or once too few, or the loop variable is off by one (an indexing problem). The fencepost error is a special case.

Tip:

  • Trace a small example with 2–3 iterations by hand.
  • Check special cases: what happens for or ?

3.4.3 User-controlled loops (Benutzergesteuerte Schleifen)

Definition (Sentinel loop)

Input can control not only the computation but also the termination of loops. A sentinel loop is an indefinite loop that runs until a sentinel is read: a value that signals something, typically the end of the input. The sentinel is not data.

3.4.4 The do-while loop

Definition (do-while loop, do-while-Schleife)

do {
    body
} while (test);     // note the semicolon
statement;

The test is evaluated only at the end of the body, to decide whether another iteration is needed. So there is always at least one iteration.

Equivalence of the three loop types

for, while and do-while are equally expressive: each can simulate the others. Depending on context, one is more sensible:

  • for for counting loops;
  • while for indefinite loops that may run zero times;
  • do-while when at least one iteration is required.

The slides leave the proof as an exercise; see §3.

Remark (break)

break; leaves a loop immediately. The slides show it once and state that it is not discussed further and not exam-relevant.


3. Step-by-Step Derivations

3.1 Evaluation order of a mixed boolean expression (slide 10)

5 * 7 >= 8 + 4 * (7 - 1): arithmetic before relational.

  1. Parentheses: 5 * 7 >= 8 + 4 * 6
  2. * before +: 35 >= 8 + 24
  3. +: 35 >= 32
  4. >=: true

3.2 Leap year (Schaltjahr) expression (slide 15)

Requirement: a year is a leap year if it is divisible by 4 and not by 100, unless it is divisible by 400.

jahr % 4 == 0 && jahr % 100 != 0 || jahr % 400 == 0

Implicit bracketing, step by step:

  1. % binds tightest, then ==/!=.
  2. && binds tighter than ||.

So the expression means

((jahr % 4 == 0) && (jahr % 100 != 0)) || (jahr % 400 == 0)

This is the only bracketing that matches the textual requirement; any other would contradict it.

Checks:

  • 1992: divisible by 4, not by 100, so true.
  • 1900: divisible by 100 and not by 400, so false.
  • 2000: divisible by 400, so true.

The lecturer would add the brackets around the && part even though precedence makes it unnecessary, for readers. Full bracketing of every comparison is “one notch too much”.

3.3 Short-circuiting as a guard (slide 29)

int a = …;
int b = …;
if (a / b > 2) { … }               // what if b == 0? Runtime error
if (b != 0 && a / b > 2) { … }     // safe

With b == 0:

  1. b != 0 is false.
  2. false && (a / b > 2): the right side is not evaluated, so no division by zero happens. The result is false.

The order of operands is relevant: if (a / b > 2 && b != 0) evaluates the division first, so a division by 0 is possible, which is a runtime error.

3.4 Executing a for loop (slides 37–38)

for (int i = 1; i <= 5; i = i + 1) {
    System.out.println(i);
}
Stepitest: i <= 5Action
init1
11trueprint 1; update: i = 2
22trueprint 2; i = 3
33trueprint 3; i = 4
44trueprint 4; i = 5
55trueprint 5; i = 6
66falseleave the loop

The body runs 5 times and the test is evaluated 6 times.

3.5 Deriving inner-loop bounds with a table (slides 51–53)

Target:

....1
...2
..3
.4
5
  1. Outer loop: one iteration per row, row = 1, 2, 3, 4, 5.
  2. Inner loop: the pattern of the row: some dots (depending on row), then the number row.
  3. How many dots? Tabulate:
row# dots row row
14−14
23−23
32−32
41−41
50−50
  1. The count decreases by 1 per row, so the formula contains . The values are shifted from the target by 5, so add 5: dots .
for (int row = 1; row <= 5; row = row + 1) {
    for (int pt = 1; pt <= -row + 5; pt = pt + 1) {
        System.out.print(".");
    }
    System.out.print(row);
    System.out.println();
}

“Maybe overkill here, but it helps with harder patterns.” Descriptive loop variable names (row, pt) make it clear which variable iterates over what.

3.6 Value semantics, traced with “boxes” (slides 76–80)

public static void plusOne(int x) {
    x = x + 1;
    System.out.println(x);   // extra output (slide 80)
}
 
int x = 6;
int y = 7;
plusOne(x);
System.out.println(x);
plusOne(y);
System.out.println(y);
Stepmain’s xmain’s yplusOne’s x (parameter box)Output
int x = 6; int y = 7;67(none)
plusOne(x): copy 6 into a new box676
x = x + 1 inside6777
return: the parameter box disappears67(none)
println(x) in main676
plusOne(y): copy 7677
x = x + 1 inside6788
println(y) in main677

Output: 7, 6, 8, 7. The lecture asked the class to predict println(x) after plusOne(x); the answer is 6. The two variables named x are unrelated memory cells.

How to communicate a result back: return it, and assign it at the caller.

public static int plusOne(int x) {
    x = x + 1;          // variant 1
    return x;
}
public static int plusOne(int x) {
    return (x + 1);     // variant 2; parentheses recommended but optional
}
System.out.println(plusOne(6));   // 7

Further variant from the lecture: int y = x + 1; return y;. “Artistic freedom.”

3.7 How the compiler thinks about return paths (slides 93–96, Friday demo)

Rule: every execution path through a non-void method must reach a return. The compiler does not reason about whether your conditions are logically exhaustive. An if without else can always be skipped.

public static int max(int a, int b) {
    if (a > b) {
        return a;
    } else if (b > a) {
        return b;
    }
}   // ✗ "missing return statement": genuinely no return when a == b
 
public static int max(int a, int b) {
    if (a >= b) {
        return a;
    } else if (b > a) {
        return b;
    }
}   // ✗ logically complete, but the compiler sees a path through the "implicit else"
 
public static int silly(int x) {
    if (x > 0) {
        return x;
    } else {
        if (x <= 0) {
            return -x;
        }
    }
}   // ✗ the inner if has no else, so there is a path without return

Control-flow view of silly:

  • x > 0 true leads to return x ✓.
  • x > 0 false leads to the test x <= 0:
    • true leads to return -x ✓;
    • false (impossible logically, but structurally present) leads to the end of the method without a return ✗.

Example: Friday demo, abs

public static int abs(int x) {
    if (x < 0) {
        return -x;
    } else if (x > 0) {
        return x;
    }
}
  • First reaction from the class: the case x == 0 is missing. Adding else if (x == 0) return 0; still gives “missing return statement”.
  • A student: “maybe returns inside if/else are the problem”. Returns inside branches are allowed; the issue is the missing unconditional path.
  • Fixes: make the last case a plain else, or add return …; after the cascade. “Even if you know all cases are covered, the compiler doesn’t have to know it. Sometimes you are smarter than the compiler, sometimes it is smarter than you.”
  • The lecturer’s preferred style (less nesting):
public static int abs(int x) {
    if (x < 0) {
        return -x;
    }
    return x;     // reaching here means we didn't return above, so x >= 0
}

Method: draw the control-flow graph and check every branch for a return.

Two styles for max, both correct:

public static int max(int a, int b) {        // variant 1: single return
    int result;
    if (a >= b) { result = a; } else { result = b; }
    return result;
}
public static int max(int a, int b) {        // variant 2: one return per case
    if (a >= b) { return a; } else { return b; }
}

Some languages enforce a single return; Java doesn’t, and multiple returns often simplify code.

3.8 Unrolling post-increment (slides 119–121)

Execution of an assignment

  1. The right side (an expression) is evaluated. Any ++ side effects happen during this step.
  2. The result is stored in the variable on the left side.

int y = x++; with x = 2:

  1. x++ reads 2 (the expression’s value), then sets x = 3.
  2. y = 2.

So one line changes two variables: x == 3, y == 2.

println(x++) twice, starting with x = 2: prints 2 (x becomes 3), then prints 3 (x becomes 4).

a = a++; with a = 1:

  1. aOld = a, which is 1 (the value of the expression a++).
  2. a = a + 1, which is 2 (the side effect of ++).
  3. a = aOld, which is 1 (the assignment, executed last).

Result: a stays 1.

int c = b-- - b--; with b = 10:

  1. bOld1 = b (10), then b = 9.
  2. bOld2 = b (9), then b = 8.
  3. c = bOld1 - bOld2 = 1.

Result: c = 1, b = 8.

3.9 Pre- and post-increment in one expression (slide 123)

int x = 2; System.out.println(++x + x++ + " " + x + ++x + x);

Evaluate left to right:

Sub-expressionValuex afterwards
++x33
x++34
3 + 3 (still int addition)6
6 + " ""6 "
+ x"6 4"4
+ ++x"6 45"5
+ x"6 455"5

Output: 6 455. “From here on it’s strings.”

3.10 Short-circuit suppresses a side effect (slide 124)

int x = 2;
boolean q = p && x++ < 3;
  • p = true: evaluate x++ < 3, which is 2 < 3 = true, and x becomes 3. So q = true, x is 3.
  • p = false: false && … is false without evaluating the right side. So q = false, x stays 2.

3.11 Equivalence of the loop types (slide 154, exercise)

SimulateBy
for (init; test; update) { body }init; while (test) { body; update; } (difference: the scope of the loop variable)
while (test) { body }for ( ; test ; ) { body } (the head parts may be empty)
do { body } while (test);body; while (test) { body; }
while (test) { body }if (test) { do { body } while (test); }

Hence all three are equally expressive.


4. Worked Examples & Applications

4.1 Boolean expressions (slide 14)

With int x = 42, y = 17, z = 25;:

#ExpressionStepsResult
1y < x && y <= z17 < 42 true, 17 <= 25 truetrue
2x % 2 == y % 2 || x % 2 == z % 20 == 1 false, 0 == 1 falsefalse
3x <= y + z && x >= y + zy + z = 42: 42 <= 42 true, 42 >= 42 truetrue (this is how to “chain”)
4!(x < y && x < z)42 < 17 false, so && short-circuits to false; !falsetrue
5(x + y) % 2 == 0 || !((z - y) % 2 == 0)59 % 2 = 1, so false; 8 % 2 == 0 true, negated falsefalse

4.2 The grading code: independent ifs (Thursday)

int points = 80;
if (points >= 90) { System.out.println("Note 6."); }
if (points >= 80) { System.out.println("Note 5."); }
if (points >= 70) { System.out.println("Note 4."); } else { System.out.println("Fail."); }

Output for 80: Note 5. and Note 4., because each test is independent.

In the lecture the value was raised to 90, giving Note 6., Note 5., Note 4.. “If you see 6, 5, 4, 3, 2, 1 as your grade in myStudies, that doesn’t help you much.” The control-flow diagram makes it obvious: a decision on one level has no effect on the next.

Repair while keeping separate ifs (class discussion):

  1. Exclude the cases already handled. The second test becomes points >= 80 && !(points >= 90), i.e. points < 90.
  2. Likewise points >= 70 && points < 80.
  3. Still wrong: the unconditional else depends only on the last test, so 80 or 90 points also print “Fail.”. The else must be replaced by a real condition, if (points < 70).
  4. A student suggested dropping the first if and printing “6” in the last else. This fails for the same reason: the else still depends only on its own if, so 80 points print “5” and “6”. Swapping which cases go wrong doesn’t remove the problem.
if (points >= 90)                 { System.out.println("Note 6."); }
if (points >= 80 && points < 90)  { System.out.println("Note 5."); }
if (points >= 70 && points < 80)  { System.out.println("Note 4."); }
if (points < 70)                  { System.out.println("Fail."); }

This works, but every boundary is duplicated. Changing a threshold means editing several places.

“Better?” (slide 17) is still buggy:

if (points >= 0 && points < 70)  { System.out.println("Fail."); }
if (points >= 70 && points < 80) { System.out.println("Note 4."); }
if (points >= 80 && points < 90) { System.out.println("Note 5."); }
else                             { System.out.println("Note 6."); }

The else belongs only to the last if. For 75 points the output is Note 4. and Note 6.; every score below 80 gets an extra “Note 6.”.

Real-world example (r/programminghorror): a progress bar drawn with ten interval ifs. It has two redundancies:

  1. the interval bounds are all repeated (changing to 20 %-steps means editing all of them);
  2. the full/empty circle strings are repeated, although each row only adds one more full circle. Loops remove this second kind.

The correct solution is an else if cascade (exclusivity):

if (points >= 90) {
    System.out.println("Note 6.");
} else if (points >= 80) {
    System.out.println("Note 5.");
} else if (points >= 70) {
    System.out.println("Note 4.");
} else {
    System.out.println("Fail.");
}

Each later test may assume the earlier ones failed, so no upper bounds are needed and nothing is duplicated.

4.3 Which if construct? (Thursday class exercise)

Example: Start groups

Task: runners go into the fast group 1, medium group 2 or slow group 3 by their previous time. Exactly one group, so if / else if / else:

if (previousTime < timeGroup1) {
    startGroup = 1;
} else if (previousTime < timeGroup2) {
    startGroup = 2;
} else {
    startGroup = 3;
}

Variants from students:

  • else if (previousTime <= timeGroup3) instead of else. Using <= at the boundary is a requirement question. More importantly, runners slower than the group-3 limit then get no group, whereas with else everyone, “even someone taking 300 years”, lands in group 3. Which is right must be clarified with the requirements.
  • Swapping the tests so that < timeGroup2 comes first leaves group 1 empty. Since , every time below is also below and is caught first.
  • Giving every test both bounds also works, but duplicates the limits again.

Example: Medal or certificate

Task: a medal if the average is above 5.9; else a certificate if above 5.75 (made-up rules). At most one, so if / else if with no final else:

boolean medal = false;
boolean award = false;
if (avgGrade > 5.9) {
    medal = true;
} else if (avgGrade > 5.75) {
    award = true;
}
  • A final else { medal = false; award = false; } (student suggestion) is unnecessary here, because both flags were initialised with false. If they were only declared, it would be needed.
  • medal = avgGrade > 5.9; award = avgGrade > 5.75; assigns boolean expressions directly. That is a nice technique, but it is not equivalent: a 6.0 would get both.
  • An int status (0 = nothing, 1 = certificate, 2 = medal) set in an if / else if is an equivalent alternative to two flags.

Example: Divisible by 2, 3 or 5

Task: any combination is possible, so independent ifs:

boolean divisible = false;
if (number % 2 == 0) { divisible = true; }
if (number % 3 == 0) { divisible = true; }
if (number % 5 == 0) { divisible = true; }
  • Keeping only the first test (student suggestion) is not equivalent. 5 is divisible by 5 but not by 2, so it would give false.
  • For 30, divisible is set to true three times, which is redundant. A student suggested skipping the remaining tests once it is already true.
  • Better: boolean divisible = number % 2 == 0 || number % 3 == 0 || number % 5 == 0;. It needs no branching, and short-circuiting stops at the first true test, the same efficiency gain. Trade-off: less branching, a more complex expression.

4.4 Style: comparing booleans with true/false (slides 23–24)

RedundantElegant
if (isPrime == true)if (isPrime)
if (test == false)if (!test)

test already evaluates to true or false; comparing it with a literal adds nothing. It isn’t faster either, since the compiler optimises it away. But it immediately signals “someone is still learning”. “You don’t want to give that impression in a team; get rid of it right away.”

4.5 for-loop examples (slides 40–43)

for (int i = 1; i <= 12; i = i + 1) {
    System.out.println("I WILL NOT WASTE CHALK.");
}
 
int mistakes = 0, first, second;      // three variables declared; only mistakes initialised
for (int i = 1; i <= 10; i = i + 1) {
    first = 1 + rand.nextInt(10);
    second = 1 + rand.nextInt(10);
    System.out.print(first + " * " + second + " = ");
    if (console.nextInt() != (first * second)) {
        mistakes = mistakes + 1;
    }
}
System.out.println((10 - mistakes) + " out of 10 correct!");

Countdown (count down instead of up):

System.out.print("T minus ");
for (int i = 10; i >= 1; i = i - 1) {
    System.out.print(i + ", ");
}
System.out.println("blastoff!");
// T minus 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, blastoff!

You could also count up and compute the output in reverse, but letting the loop variable run backwards is more natural. The step size may also be 2 or anything else.

Exam Trap: Infinite loop (Endlosschleife)

for (int i = 10; i % 2 == 0; i = i + 2) {
    System.out.print(i + " ");
}
// 10 12 14 16 18 20 22 …   never terminates

i stays even forever, so the test never becomes false. In IntelliJ you stop the program with the red stop button in the Run tab.

Live demo: a loop testing j <= 100 while updating i; IntelliJ warned “Condition j <= 100 is always true”.

4.6 Nested loops (slides 45–50)

Rectangle 5 × 10:

for (int i = 1; i <= 5; i = i + 1) {
    for (int j = 1; j <= 10; j = j + 1) {
        System.out.print("*");
    }
    System.out.println();
}

The intermediate version, ten print("*") statements inside the outer loop, is exactly the copying that loops should eliminate.

Triangle (class suggestion: make the inner bound depend on i):

for (int i = 1; i <= 5; i = i + 1) {
    for (int j = 1; j <= i; j = j + 1) {
        System.out.print("*");
    }
    System.out.println();
}
Print "*"Print iPrint j
*11
**2212
***333123
****44441234
*****5555512345

Exam Trap: Wrong variable in a nested head

for (int i = 1; i <= 5; i = i + 1) {
    for (int j = 1; j <= 10; i = i + 1) { … }   // updates i, not j: j <= 10 is always true
}
for (int i = 1; i <= 5; i = i + 1) {
    for (int j = 1; i <= 10; j = j + 1) { … }   // tests i, which doesn't change inside
}

Both inner loops are infinite. The demo printed endless stars in the first outer iteration. i and j look alike: “when you see j and then suddenly i in a head, all alarm bells should ring.”

4.7 while examples (slides 57–58)

int num = 1;                    // initialisation (before the loop)
while (num * num <= 2000) {     // test
    System.out.print(num + " ");
    num = num * 2;              // update (inside the body)
}
// 1 2 4 8 16 32

The loop stops because but . “If you’re good at mental arithmetic you know the number of iterations; I’d rather work with the condition.”

int n = 91;
int factor = 2;                 // 1 is a trivial divisor, so start at 2
while (n % factor != 0) {       // as long as factor does not divide n
    factor = factor + 1;
}
System.out.println("first divisor is " + factor);   // first divisor is 7 (91 = 7 · 13)

4.8 Methods with parameters (slides 61–75)

public static void vorsatz(String name) {                     // parameter declaration
    System.out.print(name + " verspricht: ");                 // parameter used like a variable
    System.out.println("\"Ich werde die Übungen machen.\"");
}
public static void main(String[] args) {
    vorsatz("Manuela");                                       // value passed at the call
    vorsatz("Malte");
}
// Manuela verspricht: "Ich werde die Übungen machen."
// Malte verspricht: "Ich werde die Übungen machen."

In the demo, a variable holding "Hans" was also passed, and a missing space was added after the name.

public static void echoPIN(int pin) {
    System.out.println("Die Geheimnummer ist " + pin);
}
echoPIN(123456);    // pin = 123456
echoPIN(40 + 2);    // the argument expression is evaluated first: pin = 42

Possible errors:

  • echoPIN(); is a compile error: an int argument is required.
  • echoPIN(1.67); is a compile error: “possible lossy conversion”. The conversion rules are the same as before, because parameter passing is an assignment.
public static void sumFromTo(int from, int to) {
    int sum = 0;
    for (int i = from; i <= to; i = i + 1) {
        sum = sum + i;
    }
    System.out.println("the sum is " + sum);
}
sumFromTo(10, 11);   // the sum is 21
sumFromTo(1, 100);   // the sum is 5050 (= 100 · 101 / 2)

Example: "Mystery function" quiz (Friday, EduApp)

A method with parameters in the order (x, z, y) was called from main with variables named x, y, z in a different order. The output in the lecture was 2493; the code is in the uploaded quiz slides, which were not part of the material provided here.

Takeaway: the order of the arguments matters, not their names. Whatever the caller’s first argument is goes into the first parameter, regardless of what either is called. The lecturer called this “terrible code you’d never want to write or read”, but a typical error source.

4.9 Methods with return values (slides 84–91)

public static int myNumber() {
    return 6;
}
System.out.println(1 + myNumber() + 3);   // 1 + 6 + 3 = 10
 
public static double fahrenheitToCelsius(double fahrenheit) {
    return 5.0 / 9.0 * (fahrenheit - 32.0);                 // variant 1
}
public static double fahrenheitToCelsius(double fahrenheit) {
    double celsius = 5.0 / 9.0 * (fahrenheit - 32.0);       // variant 2: helper variable
    return celsius;
}
  • celsius is local: invisible and irrelevant to the caller, who only gets a copy of the return value.
  • 5 / 9 would be integer division, giving 0, which is why 5.0 / 9.0 is used.
public static int abs(int x) {
    if (x < 0) {
        x = -x;       // only the local copy changes
    }
    return x;
}
int x = -6;
System.out.println("absolute value of " + x + ": " + abs(x));   // absolute value of -6: 6

Primality test (slide 88):

public static boolean isPrime(int n) {
    boolean foundDivisor = false;
    int d = 2;
    while (!foundDivisor) {
        if (n % d == 0) {
            foundDivisor = true;
        } else {
            d = d + 1;
        }
    }   // d is the smallest divisor of n that is >= 2
    return d == n;   // prime iff the smallest divisor >= 2 is n itself
}
  • For a prime the loop tries and only stops at , so it returns true.
  • For a composite it stops at a divisor , so it returns false.
  • Precondition: (not stated on the slide). For no divisor exists, so the loop runs practically forever.

Example: Quiz "What happens here?" (slide 90)

public static void main(String[] args) {
    squareNumber(3);
    System.out.println("3^2 = " + result);
}
public static int squareNumber(int x) {
    int result = x * x;
    return result;
}

Answer: compile error, “cannot find symbol result”. result is a local variable of squareNumber, invisible from main.

A student suspected missing parentheses around the returned value. That is not the reason; return result; without parentheses is fine.

Fix: int result = squareNumber(3); in main, which is a new local variable of main.

public static void nonSense() {
    System.out.println("This is reached.");
    return;
    System.out.println("This is not reached.");
}
nonSense();   // This is reached.

4.10 Scope examples (slides 104–108)

public static void example() {
    int x = 3;
    x = x * x;
    for (int i = 1; i <= 10; i = i + 1) {
        System.out.println(x + i);      // x is visible (outer block)
    }   // i no longer exists here
    System.out.println(x);
    System.out.println(i);              // ✗ ERROR: cannot find symbol "i"
}
for (int i = 1; i <= 100; i = i + 1) {
    int i = 2;                                   // ✗ ERROR: scopes overlap
    System.out.print(i);
}
for (int i = 1; i <= 100; i = i + 1) {
    for (int i = 1; i <= 100; i = i + 1) { … }   // ✗ ERROR: inner i overlaps outer i
}
 
for (int i = 1; i <= 100; i = i + 1) { System.out.print(i); }
int i = 5;                                       // ✓ allowed: the loop's i is already gone
public static void example(int x) {
    for (int i = 1; i <= 10; i = i + 1) {
        for (int j = i; j <= 10; j = j + 1) {
            System.out.print(x + i + j + " ");
        }   // j no longer exists here
        System.out.println(i);
        if (i % 2 == 0) {
            int j = 4;                 // ✓ a new j; the old j's scope has ended
            System.out.print(i * j);
        }   // j no longer exists here
    }   // i no longer exists here
    System.out.println(x);
}   // x no longer exists here

4.11 Overloading examples (slides 112–115)

public static int max(int a, int b)             { return a >= b ? a : b; }
public static boolean max(boolean a, boolean b) { return a || b; }
max(3, 2);          // 3: calls the first method
max(true, false);   // true: calls the second method
 
public static int sum(int from, int to) {
    int sum = 0;
    for (int i = from; i <= to; i = i + 1) { sum = sum + i; }
    return sum;
}
public static int sum(int to) { return sum(1, to); }    // reuses the general version
sum(2, 3);   // 5: first method
sum(3);      // 6: second method

Inside the first sum, a local variable is also named sum. That is legal, because variables and methods are different kinds of names.

Not allowed:

  • int f(int a) and int f(int b): only the parameter names differ.
  • int f(int a) and void f(int a): only the return type differs.
  • Any combination of these.
public static String foo(int a, double b) { return "int double"; }
public static String foo(double a, int b) { return "double int"; }
foo(1, 1.0);    // "int double": only the first fits (1.0 can't become an int)
foo(1.0, 1);    // "double int": only the second fits
foo(1.0, 1.0);  // ✗ no matching method: one argument would need double → int
foo(1, 1);      // ✗ ambiguous: both fit by widening one int to double; neither is better

4.12 The fencepost problem in practice (slides 128–135)

Task: printNumbers(int n) prints 1, 2, 3, 4, 5, 6 for .

Candidate bodyOutput for Problem
print(", " + i + ", "), 1, , 2, , 3, , 4, , 5, commas everywhere
print(i + ", ")1, 2, 3, 4, 5, trailing comma
print(", " + i), 1, 2, 3, 4, 5leading comma

Numbers are posts, commas are rails: numbers need commas.

public static void printNumbers(int n) {        // last post outside
    for (int i = 1; i <= n - 1; i++) {
        System.out.print(i + ", ");
    }
    System.out.print(n);
}
 
public static void printNumbers(int n) {        // first post outside
    System.out.print(1);
    for (int i = 2; i <= n; i++) {
        System.out.print(", " + i);
    }
}

Edge cases:

  • The first version runs no iteration for , but still prints n (e.g. 0).
  • The second runs none for , but still prints 1.

A guard if (n < 1) return; handles both cleanly.

Harder task: printPrimes(int n). For : 2, 3, 5, 7, 11, 13, 17, 19.

public static void printPrimes(int n) {
    if (n < 2) {
        return;                         // nothing to print for n < 2
    }
    System.out.print(2);                // first post: 2 is always the first prime
    for (int i = 3; i <= n; i++) {
        if (isPrime(i)) {
            System.out.print(", " + i); // rail + post
        }
    }
}
 
public static boolean isPrime(int n) {  // counting variant
    int numberOfDivisors = 0;
    for (int d = 1; d <= n; d++) {
        if (n % d == 0) {
            numberOfDivisors++;
        }
    }
    return numberOfDivisors == 2;       // exactly 1 and n; also correct (false) for n < 2
}

4.13 Sentinel loops step by step (slides 138–147)

Task: read numbers until the user types 0, then print the sum.

Enter a number (0 to quit): 10
Enter a number (0 to quit): 20
Enter a number (0 to quit): 30
Enter a number (0 to quit): 0
The total is 60

First attempt:

int sum = 0;
int number;
while (number != 0) { … }    // ✗ compile error: number not initialised

Second attempt: int number = -1; // dummy, anything but 0. Inside the loop: prompt, read, sum = sum + number.
This is correct, but bad style: the sentinel itself is added to the sum. That is harmless only because adding 0 changes nothing.

Switching the sentinel to −1 exposes the bug: inputs 10, 20, 30, −1 give The total is 59. The code should work independently of the choice of sentinel.

Ugly fixes:

  1. Start with sum = 1 to “compensate”. A hack that depends on the sentinel value.
  2. Add if (number != -1) sum = sum + number; inside the loop. This tests the same condition twice per iteration.

Proper solution: it is a fencepost problem. We read numbers but add only . So: read once before the loop (first post); in the loop, add (rail), then read (post).

sum = 0
print prompt and read input
while input is not sentinel:
    add input to sum
    print prompt and read input
print sum
int sum = 0;
Scanner console = new Scanner(System.in);
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): ");   // redundancy: prompt + read appear twice
    number = console.nextInt();
}
System.out.print("The total is " + sum);

Removing the redundancy with a method:

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

With do-while. Slide bug: slides 150–152 write readNumber(); without storing the result, so number is never set. Corrected:

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

No dummy initialisation is needed: the compiler can see that number is assigned before the test.

With break (not exam-relevant):

do {
    number = readNumber();
    if (number == -1) {
        break;          // leaves the loop immediately
    }
    sum = sum + number;
} while (true);

The same task with three loop types ( inputs, the last being −1):

LoopStructureIterations / reads
forread once; for ( ; number != -1; ) { add; read; } iterations, reads
whileread once; while (number != -1) { add; read; } iterations, reads
do-whiledo { read; if (number != -1) add; } while (number != -1); iterations, reads

The fencepost with three loop types ( posts, rails):

post();                                   // for: n-1 iterations
for (int i = 2; i <= n; i++) { rail(); post(); }
 
post();                                   // while: n-1 iterations
int i = 2;
while (i <= n) { rail(); post(); i++; }
 
int i = 1;                                // do-while, variant 1: n-1 iterations
do { post(); rail(); i++; } while (i < n);
post();
 
int i = 1;                                // do-while, variant 2: n iterations
do { post(); if (i < n) { rail(); } i++; } while (i <= n);

4.14 Exam traps collection

Exam Trap: if (a = b)

With ints it is a compile error. With booleans it compiles and silently assigns.

Exam Trap: else binds to the nearest if

Several independent ifs plus one trailing else means “else of the last test only”, not “else of all”.

Exam Trap: Short-circuit and side effects

In p && q / p || q, side effects of q (x++, a division by zero) may or may not happen. Determine p first.

Exam Trap: a = a++

a is unchanged: the old value is reassigned last.

Exam Trap: Parameters are copies

A method changing its primitive parameter does not change the caller’s variable. Changes only “come back” through return plus an assignment: x = plusOne(x);.

Exam Trap: Loop variable scope

A variable declared in the for head doesn’t exist after the loop. Declare it before the loop if you need its final value.

Exam Trap: missing return

“Missing return statement” appears even when your conditions are logically exhaustive. The compiler only sees an if without else.

Exam Trap: Empty loop body

A ; right after the head, for (…); or while (…);, makes the body empty. The block after it is not part of the loop, and while (…); often loops forever.


5. Intuition & Conceptual Bridges

Intuition: EBNF control forms ↔ Java control structures

Week 1’s sequence, selection/option and repetition correspond directly to statement sequences, if/else and loops. Week 1 also showed that EBNF repetition cannot count, whereas a Java for loop can: it has a counter variable. That is why recursion and iteration are equally powerful in programming languages but not in EBNF.

Intuition: Loops are methods for time

Methods remove repetition in space: the same code at different places in the program. Loops remove repetition in time: the same code many times in a row. Parameters make method bodies flexible, just as the loop variable makes loop bodies flexible.

Intuition: Value semantics in the memory-cell picture

A call copies the argument’s value into a fresh cell that belongs to the method and is discarded afterwards. That is why the method can’t touch the caller’s cells. With arrays and objects (next chapter), the copied value is a reference, and the picture gets more interesting.

Intuition: Short-circuiting is a hidden branch

p && q behaves like p ? q : false, and p || q like p ? true : q. Seen that way, conditional side effects and the guard pattern b != 0 && a / b > 2 are no surprise.

Intuition: Thinking in fences

“How many posts, how many rails?” comes up everywhere: separators between list elements, sentinel input, array indices (next chapter). Trace before trusting a loop.

Connections:

  • Prior: week 2’s operator precedence table is extended with relational and logical operators. Assignment is an expression with a value, which is why if (b = true) compiles. Variables, scope and static methods are generalised.
  • Next (chapter 4, Sequenzen): strings and arrays, processed with exactly these loops; reference semantics, where passing an array lets a method modify the caller’s data (completing value semantics); the allTrue/early-return and max patterns.
  • Later: recursion (cf. week-1 EBNF recursion); the debugger in the week-5 exercises; break is mentioned but not exam-relevant.

6. Summary & Self-Assessment Questions

Cheat sheet

TopicKey facts
if / elsecondition must be boolean; else attaches to the nearest if
Cascadeif … else if … else: first true test wins; exactly one block runs
Constructsif/else if/else: exactly one; if/else if: at most one; separate ifs: any number
Relational ops== != < > <= >=, lower precedence than arithmetic; no chaining
Logical ops! > && > ||; short-circuit: && skips if left is false, || skips if left is true
De Morgan!(a && b) = !a || !b; !(a || b) = !a && !b
Ternarytest ? a : b is an expression
= vs ==assignment (evaluates to the new value) vs. comparison
Styleif (b) not if (b == true); always use braces
forinit once; then test, body, update, test, …; loop variable scoped to the loop
whiletest before each iteration; 0 or more iterations
do-whiletest after each iteration; at least 1; while (test);
Nested loopsthe inner loop runs fully per outer iteration; derive bounds from a row/count table
Parametersformal = variable, actual = argument value; copied (value semantics); matched by position
returnends the method; every path of a non-void method must return; the call is replaced by the value
Scopefrom declaration to end of block; scopes of equal names must not overlap
Overloadingsame name, different parameter types/count; a unique best match or a compile error
++ / --post: use the old value; pre: use the new value; use only as stand-alone statements
Compoundx += v etc.; beware x =+ v
Fencepost posts, rails: move one post out of the loop
Sentinelread before the loop; inside: process, then read

Self-assessment questions

Question 1. Give the value of every variable after this code.

int x = 5;
boolean b = (x > 3) || (x++ > 10);
boolean c = (x < 3) && (++x > 0);
boolean d = !(x == 5) || x-- == 5;
int e = x++ + ++x;

Question 2. What does this print? Then rewrite the outer loop as a while loop.

for (int i = 1; i <= 4; i++) {
    for (int j = 1; j <= 4 - i; j++) {
        System.out.print(" ");
    }
    for (int k = 1; k <= 2 * i - 1; k++) {
        System.out.print("*");
    }
    System.out.println();
}

Question 3. What does this print? Explain using value semantics.

public static int f(int a, int b) {
    a = a + b;
    b = a - b;
    return a * b;
}
public static void main(String[] args) {
    int a = 2;
    int b = 3;
    int c = f(b, a);
    System.out.println(a + " " + b + " " + c);
}

Question 4. Write int digitSum(int n) returning the sum of the decimal digits of n, e.g. digitSum(4096) = 19 and digitSum(-35) = 8. Use a while loop, % and /, and trace it for 4096.

Question 5. Find every compile error and explain why.

public static int classify(int n) {
    if (n > 0) {
        int sign = 1;
    } else if (n <= 0) {
        int sign = -1;
    }
    for (int i = 0; i < 3; i++) {
        int sign = 0;
    }
    System.out.println(i);
    return sign;
}
public static void classify(int m) { }