Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Control Structures and Basic Data Structures

important

  • Conditional statements (if, switch)
  • Loop statements (for, while, do-while) and list-like data structures
  • Understanding that in Java, strings are compared using the equals() method rather than the == operator

Programming is rarely just about executing lines of code sequentially. To make programs useful, they must be able to make decisions, repeat actions, and manage data in a sensible way. In this section, we will cover Java's control flow mechanisms, iteration structures, and two ways of storing data: traditional arrays and flexible lists.

Comparison Operators

Before we can teach a program to make choices ("if this, then that"), we need to understand how a computer sees the world. Computer logic is binary: statements are either true (true) or false (false). Comparison operators can be thought of as questions that return a Boolean value. Below are the most common comparison operators in Java.

OperatorMeaningExample (when x = 5, y = 3)Result
==Equal tox == yfalse
!=Not equal tox != ytrue
>Greater thanx > ytrue
<Less thanx < 4false
>=Greater than or equal tox >= 5true
<=Less than or equal toy <= 3true

Often, decisions depend on more than one condition. For example:

"I will go outside IF it is not raining AND I have free time."

For this purpose, we use logical operators to combine conditions.

  • && (AND): The expression is true only if both conditions are true.
  • || (OR): The expression is true if at least one of the conditions is true.
  • ! (NOT): Reverses a Boolean value (true becomes false, false becomes true).

warning

Do not confuse the assignment operator = with the comparison operator ==.

  • if (x = 5) attempts to assign the value 5 to x (error)
  • if (x == 5) checks whether the value of x is 5 (correct)

Comparing Reference-Type Variables

Unlike primitive types (int, double, etc.), the == operator in Java compares references when used with reference types rather than the contents of the objects themselves. For this reason, the equals() method should be used when comparing the contents of strings and other reference-type variables.

void main() {
    String string1 = "Slush";
    String string2 = new String("Slush"); // Force creation of a new String object

    // INCORRECT: Compares references -> prints false
    IO.println(string1 == string2);

    // CORRECT: Compares contents -> prints true
    IO.println(string1.equals(string2));
}

Comparing Against null

Checking for a null reference can, however, be done with the == operator, because in this case we specifically want to compare references.

void main() {
    String string = null;
    IO.println(string == null);
}

Methods often use a null reference to represent the absence of a value. For example, IO.readln() may return null if input could not be read (see JavaDoc documentation). This may occur if the user terminates the program unexpectedly. In such situations, it can be sensible to verify that a variable is not null before using it.

void main() {
    String input = IO.readln("Enter input");

    if (input == null) {
        // Do something if no input was provided
        // For example: exit the program
        return;
    }

    // Here we know that input contains at least some string
    IO.println("Text in uppercase: " + input.toUpperCase());
}

Conditional Statements

Conditional statements control the flow of a program. Constructing a conditional statement always requires one or more Boolean expressions.

The if Statement

The basic conditional statement is if. The code block inside it is executed only if the Boolean expression inside the parentheses evaluates to true. Often, we also need alternative branches, in which case we use else if and else structures.

The syntax for the if statements is the following:

if (points >= 90) {
    IO.println("Grade: 5");
} else if (points >= 50) {
    IO.println("Grade: Pass");
} else {
    // Executed if none of the above conditions were met
    IO.println("Grade: Fail");
}

The switch Statement

When you want to compare the value of a single variable against several specific values (for example, a menu selection), a switch statement may be clearer than a long if-else chain.

int choice = 2;

switch (choice) {
    case 1:
        IO.println("You selected option 1");
        break; // Important: stops execution within this switch block
    case 2:
        IO.println("You selected option 2");
        break;
    default:
        IO.println("Unknown selection");
}

In addition to the traditional switch statement, modern versions of Java (Java 14+) support a more concise arrow syntax using -> operator.

void main() {
int choice = 2;

switch (choice) {
    case 1 -> IO.println("You selected option 1");
    case 2 -> IO.println("You selected option 2");
    case 6,7 -> IO.println("Six or Seven");
    default -> IO.println("Unknown selection");
}
}

The Ternary Operator

In simple either-or situations where you want to assign a value to a variable based on a condition, you can use the ternary operator (?:).

Syntax: (condition) ? valueIfTrue : valueIfFalse;

Example:

void main() {
    int number1 = 5;
    int number2 = 8;

    // Read as:
    // If number1 is greater than number2,
    // assign number1 to larger, otherwise assign number2
    int larger = (number1 > number2) ? number1 : number2;

    IO.println("The larger number is: " + larger);
}

Loops

Loops are needed when you want to perform actions repeatedly. Java provides four main ways to create loops: for, for-each, while and do-while.

for

Use a for loop when you know in advance how many repetitions are needed or when you need an index (position number) during the iteration. The structure is as follows:

for (initialization; condition; update) {
    // loop body
}

Below is an example of calculating a sum with a for loop.

void main() {
int[] numbers = {1, 2, 3, 4};
int sum = 0;

// Traverse the array using indices 0, 1, 2, and 3
for (int i = 0; i < numbers.length; i++) {
    sum += numbers[i];
}

IO.println("The sum is: " + sum);
}

The initialization, condition, and update expressions can technically be left empty, but the semicolons must remain in place. An infinite loop can be created by leaving the condition empty, although this is rarely desirable.

for-each

A for-each loop is often the most readable and safest way to iterate through an entire data structure. If you do not need the index and do not intend to modify the size of the structure, use a for-each loop.

The for-each loop has some limitations: You do not know the index of the current element and you cannot modify the structure by adding or removing elements.

void main() {
    int[] numbers = {1, 2, 3, 4};
    int sum = 0;

    // "For each number in the numbers array..."
    for (int number : numbers) {
        sum += number;
    }

    IO.println(sum);
}

while

A while loop is a good choice when you do not know beforehand how many times an action must be repeated. The loop continues as long as the condition remains true. Typical examples include reading a file line by line or running a game loop.

void main() {
    String input = "";

    IO.println("Welcome to the game! (Type 'quit' to exit)");

    // Note the "!" (NOT operator) and .equals() for strings
    // The loop continues as long as
    // input is NOT missing (i.e., not null)
    // AND input is NOT "quit"
    while (input != null && !input.equals("quit")) {
        input = IO.readln("> "); // Stops to wait user input

        IO.println("Echo: " + input);
    }

    IO.println("Game ended.");
}

do-while

This loop works like a while loop, but with one important difference: the loop body is always executed at least once because the condition is checked only at the end.

do-while is the only loop whose closing line ends with a semicolon.

The following pseudocode example repeatedly generates a new location for an apple if it is too close to the player.

void main() {
    Vector2D playerPosition = new Vector2D(0, 0);
    Vector2D applePosition = new Vector2D(0, 0);

    do {
        // Generate a new position for the apple
        applePosition.x = Math.random() * 10;
        applePosition.y = Math.random() * 10;
        // If the apple is too close to the player, generate a new position
    } while (applePosition.distanceTo(playerPosition) < 2.0);
}

Controlling Loop Execution

When necessary, loop execution can be controlled using the following statements:

  • break: Terminates the loop and continues execution with the code following the loop.
  • continue: Ends the current iteration and proceeds directly to the loop's update expression and condition check.

In the example below, the numbers 1, 2, 3, 4, 6, and 7 are printed. The number 5 is skipped, and the loop terminates when it reaches 8.

 void main() {
for (int i = 1; i <= 10; i++) {
    if (i == 5) {
        continue; // Move to the next iteration
    }

    if (i == 8) {
        break; // Stop the loop
    }

    IO.println(i);
}
}

Combining Control Structures

A loop structure can contain other structures, such as conditional statements and additional loops. When using nested structures, it is important to understand variable scope: a variable defined inside an inner structure is not visible outside that structure and a variable defined in an outer structure is visible inside nested structures.

For example, a variable defined inside an if statement cannot be used outside the if block.

void main() {
    int number = 10;

    if (number > 5) {
        int number2 = number + 1; // OK: number is defined in an outer scope
        number2 *= 5;
    }

    IO.println(number2); // ERROR: number2 is defined inside the if-block
}
error: cannot find symbol
    IO.println(number2);
               ^
  symbol:   variable number2

In situations like this, one solution is to move the variable declaration into the outer scope.

void main() {
    int number = 10;

    // Define and initialize the variable in the outer scope
    int number2 = 0;

    if (number > 5) {
        // OK: both number and number2 are defined in the outer scope
        number2 = number + 1;
        number2 *= 5;
    }

    // OK: number2 is defined in the same scope as this statement
    IO.println(number2);
}

Nested Loops

Nested loops mean writing one or more loop structures inside another loop. For each iteration of the outer loop, a certain number of additional iterations are performed by the inner loop.

A common use case for nested loops is processing multidimensional arrays T[][].

void main() {
int[][] table2D = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

for (int rowIndex = 0; rowIndex < table2D.length; rowIndex++) {
    IO.println("Row " + rowIndex + ":");
    for (int columnIndex = 0; columnIndex < table2D[rowIndex].length; columnIndex++) {
        int element = table2D[rowIndex][columnIndex];
        IO.println("  Column " + columnIndex + ": " + element);
    }
}
}

Remember that int[][] means an array whose elements are themselves arrays of integers. Therefore, in the example above:

  • table2D.length returns the number of arrays contained in table2D, that is, the number of rows.
  • table2D[rowIndex] returns the int[] array stored at the specified index, containing all elements of that row.
  • table2D[rowIndex][columnIndex] returns the element at index columnIndex within the row table2D[rowIndex].