- Controlling Program Flow
- Create program flow control constructs including if/else, switch statement and expressions, loops, and break and continue statements
- Utilizing Java Object-Oriented Approach
- Implement polymorphism and differentiate object type versus reference type. Perform type casting, identify object types using instanceof operator and pattern matching
Java operators allow us to create a lot of complex expressions, but they're limited in the manner in which they can control program flow.
In this section, we discuss decision-making statements including if/else, along with the new pattern matching feature.
A Java statement is a complete unit of execution, terminated with a semicolon ;
.
Control flow statements break up the flow of execution by using decision-making, looping and branching, allowing the application to selectively execute particular segments of code.
A block of code
in Java is a group of zero or more statements between braces {}
.
For example:
patrons++;
{
patrons++;
}
A statement or block often serves as the target of a decision-making statement.
if (ticketsTaken > 1)
patrons++;
if (ticketsTaken > 1) {
patrons++;
}
Both of these snippets are equivalent.
Using blocks is often preferred, even if the block has only one statement.
The if statement allow us to execute a block if and only if a boolean expression evaluates to true at runtime.
if (booleanExpression) {
}
- if keyword
- Parentheses (required)
- Curly braces required for block of multiple statements, optional for single statement
Imagine had a function that used the hour of day, an integer value from 0 to 23 to display a message to the user:
if (hourOfDay < 11) {
System.out.println("Good morning");
}
If the hour of the day is less than 11, then the message will be displayed.
What if we want to display a different message if it is 11 a.m. or later?
if (hourOfDay < 11) {
System.out.println("Good morning");
}
if (hourOfDay >= 11) {
System.out.println("Good afternoon");
}
This seems a bit redundant, though since we're performing an evaluation on hourOfDay twice.
In this case we can use the else statement.
if (booleanExpression) {
// branch if true
} else {
// branch if false
}
- if keyword
- Parentheses (required)
- Optional else statement
And returning to the previous example:
if (hourOfDay < 11) {
System.out.println("Good morning");
} else {
System.out.println("Good afternoon");
}
Now the code is branching between one of the two possible options with the boolean evaluation happening only once.
The else operator also takes a statement or block of statements, in the same manner as the if statement, for example:
if (hourOfDay < 11) {
System.out.println("Good morning");
} else if (hourOfDay < 15) {
System.out.println("Good afternoon");
} else {
System.out.println("Good evening");
}
In this example, Java process will continue execution until it encounters an if statement that evaluates to true, if neither of the first two expressions is true, it will execute the final code of the else block.
Java 16 introduced pattern matching with if statements and the instanceof operator.
Pattern matching is a technique of controlling program flow that only executes a section of code that meets certain criteria.
It is used in conjunction with if statements for greater program control.
Pattern matching is a new tool at your disposal to reduce boilerplate in our code.
To understand why this tool was added, consider the following code:
void compareIntegers(Number number) {
if (number instanceof Integer) {
Integer data = (Integer) number;
System.out.println(data.compareTo(5));
}
}
The cast is needed since the compareTo()
method is defined on Integer but not on Number.
Code that first checks if a variable is of a particular type and then immediately casts it to that type is extremely common. So the authors of Java decided to implement a shorter syntax for it:
void compareIntegers(Number number) {
if (number instanceof Integer data) {
System.out.println(data.compareTo(5));
}
}
The variable data
in this example is referred to as the pattern variable
.
Notice that this code also avoids any potential ClassCastException.
While possible, it is a bad practice to reassign a pattern variable since doing so can lead to ambiguity about what is and is not in scope.
if (number instanceof Integer data) {
data = 10;
}
The reassignment can be prevented with a final modifier.
if (number instanceof final Integer data) {
data = 10; // DOES NOT COMPILE
}
Pattern matching includes expressions that can be used to filter data out, such as in the following example:
void printIntegerGreaterThan5(Number number) {
if (number instanceof Integer data && data.compareTo(5) > 0) {
System.out.println(data);
}
}
Notice that we're using the pattern variable in an expression in the same line in which it is declared.
The type of the pattern variable must be a subtype of the variable on the left side of the expression.
It also cannot be the same type.
This rule does not exist for traditional instanceof operator expressions.
Integer value = 123;
if (value instanceof Integer) {}
if (value instanceof Integer data) {} // DOES NOT COMPILE
The pattern variable should be a strict subtype of Integer.
The compiler applies flow scoping when working with pattern matching.
Flow scoping means the variable is only in scope when the compiler can definitively determine its type.
Flow scoping is unlike any other type of scoping in that it is not strictly hierarchical like instance, class or local scoping.
void printIntegerTwice(Number number) {
if (number instanceof Integer data) {
System.out.println(data.intValue());
}
System.out.println(data.intValue()); // DOES NOT COMPILE
}
What if we have a lot of possible branches or paths for a single value?
For example, print a different message based on the day of the week.
We could combine seven if or else statements, but that tends to create code that is long, difficult to read and hard to maintain.
A switch statement is a complex decision-making structure in which a single value is evaluated and flow is redirected to the first matching branch, knows as a case
statement.
If no such case statement is found that matches the value, an optional default
statement will be called if provided.
If no such default option is available, the entire switch will be skipped.
switch (variableToTest) {
case constantExpresion:
// branch for case
break;
case constantExpression2, constantExpression3:
// branch for case2 and case3
break;
default:
// branch for default
}
- switch keyword
- Parentheses (required)
- case scenarios
- Optional break
- Optional default as fallback
Going back to our example and using switch to print the days of week.
void printDayOfWeek(int day) {
switch (day) {
case 0:
System.out.println("Sunday");
break;
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
case 4:
System.out.println("Thursday");
break;
case 5:
System.out.println("Friday");
break;
case 6:
System.out.println("Saturday");
break;
default:
System.out.println("Invalid value");
}
}
A break statement terminates the switch statement and returns flow control to the enclosing process.
It ends the switch statement immediately.
The break statements are optional, but without them the code will execute every branch following a matching case statement, including any default statement it finds.
The following is a list of all data types supported by switch statements:
- int and Integer
- byte and Byte
- short and Short
- char and Character
- String
- enum values
- var (if the type resolves to one of the preceding types)
The types boolean
, long
, float
and double
are excluded as are their associated Boolean
, Long
, Float
and Double
classes.
The reasons are varied, such as boolean having too small range of values
and floating-point numbers having quite a wide range of values
.
Not just any variable or value can be used in a case statement.
First, the values in each case statement must be compile-time constant values of the same data type as the switch value. This means you can use only literals, enum constants or final constant variables of the same data type.
We can't have a case statement value that requires executing a method at runtime, for example:
int getCookie() {
return 4;
}
void feedAnimals() {
final int bananas = 1;
int apples = 2;
int numberOfAnimals = 3;
final int cookies = getCookies();
switch (numberOfAnimals) {
case bananas:
case apples: // DOES NOT COMPILE
case getCookie(): // DOES NOT COMPILE
case cookies: // DOES NOT COMPILE
case 3 * 5:
}
}
- The bananas variable is marked final and its value is knows at compile-time, so it is valid
- The apples variable is not marked final, even though its value is knows, so it is not permitted
- The values getCookies() and cookies do not compile because methods are not evaluated until runtime, so they cannot be used as the value of a case statement
- The value 3 * 5 does compile as expressions are allowed as case values, provided the value can be resolved at compile-time
Out implementation of printDayOfWeek
is quite long. That there was a lot of boilerplate code with numerous break statements.
There is a new switch expression added to Java 14 that is more compact.
The switch expression supports two types of branches, expression and a block.
int result = switch (variableToTest) {
case constantExpression -> 5;
case constantExpression2, constantExpression3 -> {
yield 10;
}
default -> 20;
}
- The assignment to the variable result is optional
- switch keyword
- Parentheses (required)
- case expressions followed by the arrow operator (required)
- yield required for case block if switch returns a value
- default branch is required if all possible case statement values are not handled
Rewriting the previous printDayOfWeek
:
void printDayOfWeek(int day) {
var result = switch (day) {
case 0 -> "Sunday";
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
case 4 -> "Thursday";
case 5 -> "Friday";
case 6 -> "Saturday";
default -> "Invalid value";
};
System.out.println(result);
}
There are some new rules:
- All the branches of a switch expression that do not throw an exception must return a consistent data type (if the switch expression returns a value)
- If the switch expression return a value, then every branch that is not an expression must yield a value
- A default branch is required unless all cases are covered or no value is returned
You can't return incompatible or random data types. ReturningConsistentDataTypes.java
A switch expression supports both an expression and a block. It also includes a yield statement if the switch expression returns a value. ApplyingCaseBlock.java
A switch expression that returns a value must handle all possible input values.
int canis = 4;
String type = switch (canis) { // DOES NOT COMPILE
case 1 -> "dog";
case 2 -> "wolf";
case 3 -> "coyote";
};
There's no case branch to cover 4, 5, etc...
But there are two ways to address this:
- Add a default branch
- If the switch expression takes an enum value, add a case branch for every possible enum value
For example: CoveringAllPossibleValues.java
Since all possible permutations of Season are covered, a default branch is not required.
A common practice when writing software is doing the same task some number of times.
A loop is a repetitive control structure that can execute a statement of code multiple times in succession.
The following loop executes exactly 10 times:
int counter = 0;
while (counter < 10) {
double price = counter * 10;
System.out.println(price);
counter++;
}
The simplest repetitive control structure in java is the while statement.
Like all repetition control structures, it has a termination condition, implemented as a boolean expression, that will continue as long the expression evaluates to true.
while (booleanExpression) {
// body
}
- while keyword
- Parentheses (required)
A while loop is similar to an if statement in that it is composed of a boolean expression and a block of statements.
During execution, the boolean expression is evaluated before each iteration of the loop and exits if the evaluation return false.
int roomInBelly = 5;
void eatCheese(int bitesOfCheese) {
while (bitesOfCheese > 0 && roomInBelly > 0) {
bitesOfCheese--;
roomInBelly--;
}
System.out.println(bitesOfCheese + " pieces of cheese left");
}
This method takes an amount of cheese and continues until has no room in belly.
Another form to do loops is called do/while loop.
do {
// body
} while (booleanExpression);
- do keyword
- while keyword followed by a boolean expression
- Parentheses (required)
- Semicolon (required)
Unlike a while loop, a do/while loop guarantees that the block will be executed at least once
.
int lizard = 0;
do {
lizard++;
} while (false);
System.out.println(lizard); // 1
The block will be executed and then check the loop condition.
The most important thing when using any repetition control structure is to make sure they always terminate
.
Failure to terminate a loop can lead to numerous problems, including overflow exception, memory leaks, slow performance and even bad data.
For example:
int pen = 2;
int pigs = 5;
while (pen < 10) {
pigs++;
}
The problem with this while statement is that it will never end since the variable pen is never modified and will always evaluate to true.
This is referred to as infinite loop because the termination condition is never reached.
So make sure that the loop condition, or the variables the condition is dependent, are changing between executions. Then ensure that the termination condition will be eventually reached in all circumstances.
A loop can also exit under other conditions, such as a break statement.
There are two types of for loops, although both use the same for keyword.
The first is referred to as the basic
for loop, and the second is often called the enhanced
for loop.
A basic for loop ha the same conditional boolean expression and block of statements, as the while loops.
for (initialization; booleanExpression; updateStatement) {
// body
}
- Initialization statement executes
- If booleanExpression is true, continue, else exit loop
- Body executes
- Execute updateStatement
- Return to Step 2
The organization of the components and flow allow us to create extremely powerful statements in a single line that otherwise would take multiple lines with a while loop.
Each of the three sections is separated by a semicolon ;
.
Variables declared in the initialization block of a for loop have limited scope and are accessible only within the for loop.
for (int i = 0; i < 10; i++) {
System.out.println("Value is " + i);
}
System.out.println(i); // DOES NOT COMPILE
Alternatively, variables declared before the for loop and assigned a value in the initialization block or in the body may be used outside the for loop.
int i;
for (i = 0; i < 10; i++) {
System.out.println("Value is " + i);
}
System.out.println(i); // 10
for (var counter = 4; counter >= 0; counter--) {
System.out.println(counter);
}
for (;;) {
System.out.println("Hello World");
}
Although this for loop may look like it does not compile, it will in fact compile and run without issue. It is actually an infinite loop.
Note that the semicolons separating the three sections are required.
int x = 0;
for (long y = 0, z = 4; x < 5 && y < 10; x++, y++) {
System.out.println(y);
}
System.out.println(x);
This code demonstrates three variations of the for loop.
- It is possible to declare a variable, such as x, before the loop begins
- The initialization block, boolean expression and update statements can include extra variables that may or may not reference each other, such as z, that is defined in the initialization block and is never used
- The update statement can modify multiple variables
int x = 0;
for (int x = 4; x < 5; x++) { // DOES NOT COMPILE
System.out.println(x);
}
This example looks similar to the previous, but it does not compile because of the initialization block.
The difference is that x is repeated in the initialization block after already being declared before the loop.
int x = 0;
for (long y = 0; int z = 4; x < 5; x++) { // DOES NOT COMPILE
System.out.println(y);
}
This code will not compile. The variables in the initialization block must all be of the same type.
for (int i = 0; i < 10; i++) {
i++;
}
As a general rule is considered a bad practice to modify loop variables due to the unpredictability of the result.
It also tends to make code difficult for other people to follow.
The for-each loop is a specialized structure design to iterate over arrays and various Collections Framework classes.
for (datatype instance : collection) {
// body
}
- for keyword
- instance is the object to be iterated
- collection is the array that will be traversed
The right side
of the for-each loop must be:
- A built-in Java array
- An object whose type implements
java.lang.Iterable
. In other words, the right side must be an array or collection of items, such as a List or a Set.
The left side
of the for-each loop must include a declaration for an instance of a variable whose type is compatible with the type of the array or collection on the right side. On each iteration of the loop, the named variable on the left side is assigned a new value from the array or collection.
// for
public void printNames(String[] names) {
for (int counter = 0; counter < names.length; counter++) {
System.out.println(names[counter]);
}
}
// for-each
public void printNames(String[] names) {
for (var name : names) {
System.out.println(name);
}
}
The for-each loop is a lot shorter.
We no longer have a counter loop variable that we need to create, increment and monitor.
We have been dealing with single loops that ended only when their boolean expression evaluated to false.
We will see now other ways loops could end or branch.
A nested loop is a loop that contains another loop, including while, do/while, for and for-each loops.
For example, consider the following code that iterates over a two-dimensional array, which is and array that contains other arrays.
NestedLoopUsingTwoDimensionalArray.java
int[][] myComplexArray = { { 5, 2, 1, 3 }, { 3, 9, 8, 9 }, { 5, 7, 12, 7 } };
for (int[] mySimpleArray : myComplexArray) {
for (int i = 0; i < mySimpleArray.length; i++) {
System.out.println(mySimpleArray[i]);
}
}
The outer loop will execute three times. Each time the outer loop executes, the inner loop is executed four times, giving the following output:
5 2 1 3
3 9 8 9
5 7 12 7
Nested loops also can include while and do/while, for example:
int hungryHippopotamus = 8;
while (hungryHippopotamus > 0) {
do {
hungryHippopotamus -= 2;
} while (hungryHippopotamus > 5);
hungryHippopotamus--;
System.out.println(hungryHippopotamus);
}
- The first iteration of the while, the inner loop repeats until the value of hungryHippopotamus is 4
- Leaving the do/while, the hungryHippopotamus will be decremented to 3
- 3 will be printed
- The next iteration of the while, the do will be executed, since the do/while always execute once, causing hungryHippopotamus to have the value 1
- Leaving the do/while, the hungryHippopotamus will be decremented to 0
- 0 will be printed
- Exit the while
Giving the following output:
3
0
A label is an optional pointer to the head of a statement that allows the application flow to jump to it or break from it.
It is a single identifier that is followed by a colon :
, for example:
int[][] myComplexArray = { { 5, 2, 1, 3 }, { 3, 9, 8, 9 }, { 5, 7, 12, 7 } };
OUTER_LOOP: for (int[] mySimpleArray : myComplexArray) {
INNER_LOOP: for (int i = 0; i < mySimpleArray.length; i++) {
System.out.println(mySimpleArray[i]);
}
}
For readability, they are commonly expressed using uppercase letters in snake_case.
When dealing with only one loop, labels do not add any value, but can be extremely useful in nested structures.
A break statement transfers the flow of control out to the enclosing statement.
OPTIONAL_LABEL: while (booleanExpression) {
// body
// somewhere in the loop
break OPTIONAL_LABEL;
}
- OPTIONAL_LABEL keyword followed by colon (required if label is present)
- while keyword followed by the booleanExpression
- Parentheses (required)
- body and some inner loop
- break keyword
Notice that the break statement can take an optional label parameter.
Without a label parameter, the break will terminate the nearest inner loop is currently in the process of executing.
The optional label parameter allows us to break out of a higher-level outer loop.
For example: UsingBreakWithLabels.java
The continue statement causes flow to finish the execution of the current loop iteration.
OPTIONAL_LABEL: while (booleanExpression) {
// body
// somewhere in the loop
continue OPTIONAL_LABEL;
}
- OPTIONAL_LABEL keyword followed by colon (required if label is present)
- while keyword followed by the booleanExpression
- Parentheses (required)
- body and some inner loop
- continue keyword
Notice that continue and break statements are mirrors. In fact, the statements are identical in how they are used but with different results.
While the break statement
transfers control to the enclosing statement, the continue statement
transfers control to the booleanExpression that determines if the loop should continue. In other words, it ends the current iteration of the loop.
For example: CleaningSchedule.java
With the structure as defined, the loop will return control to the parent loop any time the first value is b or the second value is 2.
The following is printed:
Cleaning: a, 1
Cleaning: c, 1
Cleaning: d, 1
The return statement can be used as an alternative to using labels and break statements, for example: FindInMatrixUsingReturn.java
This class is functionally the same as the first class we made earlier using break.
We find code without labels and break statements a lot easier to read and debug. Also, making the search logic an independent function makes the code more reusable and the calling main()
method a lot easier to read.
One facet of break
, continue
and return
that we should be aware of is that any code placed immediately after them in the same block is considered unreachable and will not compile
, for example:
int checkDate = 0;
while (checkDate < 10> {
checkDate++;
if (checkDate < 100> {
break;
checkDate++; // DOES NOT COMPILE
})
})
Even though it is not logically possible for the if statement to evaluate to true in this code, the compiler notices that we have statements immediately following the break and will fail to compile with "unreachable code" as the reason. The same will occur for continue and return statements.
Support labels | Support break | Support continue | Support yield | |
---|---|---|---|---|
while | Yes | Yes | Yes | No |
do/while | Yes | Yes | Yes | No |
for | Yes | Yes | Yes | No |
switch | Yes | Yes | No | Yes |