Control Flow Syntax and Design in Java

Control Flow Syntax and Design

At this point in the series, we have a pretty good grasp of the basics. Now, we’re going to get into what really makes programming powerful: control flow.

Table of Contents

An Introduction to Control Flow

So far, we’ve learned a lot. For example, we can now create a class that contains any number of methods and fields. In terms of methods, we’re able to handle some getters and setters as well as the main method.

Of course, our knowledge of these subjects is fairly limited. For instance, we can create plenty of methods, but we really aren’t sure how to handle anything more than some arithmetic. That said, we’ve made some solid progress. Let’s take that knowledge to the next level with control flow!

Control flow sounds a lot like technical jargon because, well, it is. In reality, control flow is just a term that describes program branching. Still confused? No worries! As we go through this tutorial, the concept should become second nature.

At this point, we’re going to focus on the conditions portion of control flow. A condition is an expression which evaluates to true or false. We can use these boolean values to make decisions within code. For example, we might add two numbers together if a certain condition is true and subtract those same numbers if a certain condition is false.

In Computer Science, we call this type of program behavior branching because it increases program complexity by adding two possible execution paths. Branching is a concept we’ll continue to see throughout this series, but for now we’ll cover the most basic unit of control flow in Java: the if statement.

The Classic If Statement

The most basic unit of control flow in Java and most other languages is the if statement. The if statement is a piece of logic which accepts a condition and executes its code block if the condition is true.

The Syntax

Remember when we played around with the various operators? This is where the comparison operators make an appearance (!=>=, >, ==, <, <=). We’ll see an example using == shortly. For now, the basic syntax for an if statement in Java can be seen below:

if (condition) {
   // code block
}

Here we see the if keyword followed by a set of parentheses. Unlike languages like Python, these parentheses are obligatory.

Inside the parentheses is a condition. The condition must evaluate to a boolean result—either true or false. If the condition evaluates to true, we jump into the code block section. Otherwise, we skip over the code block and move on. This is a concrete example of program branching.

As for the code block, it can contain any code that we might include in a method including more if statements. Once we have finished executing the code block, we will hop back out of the if statement and continue processing.

In some cases, we may choose to exit from the method via the code block. For instance, the code block may contain a return statement. In any case, the branching depends entirely on the result of the expression between the parentheses.

The True Case

To illustrate how if statements work, here’s another example:

int value = 5;

if (value == 5) {
    System.out.println("Value is equivalent to 5");
}

In this example, we start by creating an integer which stores a value of 5. In line 3, we use our new variable to evaluate the following condition: value == 5. Since value is equal to 5, the condition returns true. As a result, we jump into the code block which prints us a nice message.

The False Case

Now what happens if value is not equal to 5? Well, we can test that by reassigning value to a different integer. When we run the code, we should see that nothing happens. In other words, the condition in the if statement evaluates to false, so the code block is never executed. Instead, we jump passed the code block and continue down the program.

Of course, why don’t we just test it? The following example adds a print statement after the code block:

int value = 6;

if (value == 5) {
    System.out.println("Value is equivalent to 5");
}

System.out.println("Value is NOT equivalent to 5");

Upon inspection, this implementation appears to work great since we skip over the if statement and execute the correct print statement.

The Else Case

However, what happens if we switch value back to 5? As it turns out, we get a slightly nasty bug—both statements print.

Fortunately, Java has just the syntax to fix this issue: the else statement. The else statement provides a keyword that allows us to catch all other behaviors that don’t fit the criteria of any previous if statements in the set. That makes the else statement sort of like the term “otherwise” in English.

To build on our example, let’s take a look at the else syntax:

int value = 6;

if (value == 5) {
    System.out.println("Value is equivalent to 5");
} else {
    System.out.println("Value is NOT equivalent to 5");
}

In this example, we’ll notice that the else statement doesn’t have any parentheses. This is because it creates a branch for all conditions that don’t satisfy the previous if statement. In particular, this else statement triggers for every possible assignment to value excluding 5.

Of course, the beauty of the else statement is that we don’t have to create an if statement for every value outside of the values we care about. If we’re confident that there are no other important branches, we can catch the remaining possibilities in the else case.

The Third Case

Now what if we want to add an additional case? For instance, let’s say we care about 5 and 8. We could try just inserting another if statement in the middle. In fact, we may even find that it appears to work.

However, this type of implementation is dangerous. The else statement is now only linked to the previous if statement while the two if statements are independent of each other. In other words, the second if statement will be evaluated even when the first if statement evaluates to true.

To see this bug in action, let’s try implementing the following:

int value = 5;

if (value == 5) {
    System.out.println("Value is equivalent to 5");
} if (value == 8) {
    System.out.println("Value is equivalent to 8");
} else {
    System.out.println("Value is NOT equivalent to 5 or 8");
}

If we run this code, we’ll see that both the first and third statements will print. Not only is this an annoying bug, but the third statement actually directly contradicts the first statement. The value variable cannot both be 5 and not 5 unless we somehow managed to change value between the if statements.

The Else If Case

Fortunately, Java has a nice piece of syntax to solve this problem: the else if statement. The else if statement makes each if statement dependent on all previous if statements in the set. If any of the if statements evaluate to true, we will execute its code block and continue just beyond the last case.

The following improves on our previous example by using the proper syntax:

int value = 5;

if (value == 5) {
    System.out.println("Value is equivalent to 5");
} else if (value == 8) {
    System.out.println("Value is equivalent to 8");
} else {
    System.out.println("Value is NOT equivalent to 5 or 8");
}

Now, we can assign 5 to value and get exactly what we expect. We should be aware of this minor difference when we start playing around with more complex examples of control flow. For now, let’s take a look at nested if statements.

Nested If Statements

Sometimes we want to be able to support more complicated logic. For instance, we might want to report an error if value from our previous examples is outside a particular range. We can accomplish a basic version of this with the following code:

int value = 5;

if (value > 2) {
    System.out.println("ERROR: Value is greater than 2");
}

Of course, what do we do when we care about a more complicated range. For example, what if we want to report an error if value is greater than 2 but less than 57?

If we work with what we know, we might try printing two messages using independent if statements. However, this implementation is clunky and can get confusing. Ideally, we would only want a single message to be printed.

Fortunately, we can nest if statements such that we evaluate the first condition, enter the code block if true, then evaluate the second condition. Only when the second if statement is true do we actually print an error message.

Let’s see it in action:

int value = 5;

if (value > 2) {
    if (value < 57) {
        System.out.println("ERROR: Value is greater than 2 and less than 57");
    }
}

This solution gives us exactly the behavior we want, and we can nest as much as needed.

That said, nesting if statements can become clunky as more variables are introduced to the system. In addition, nested code tends to be difficult to read especially if each if statement has several else if statements.

In the future, we’ll hit readability more. For now, let’s take a look at one way to cleanup this code using boolean operators.

Logic Gates Revisited

All the way back in lesson one, we introduced logic gates. In particular, we briefly introduced the four basic logic gates: AND, OR, NAND, and NOR. These are called bitwise logic operations because they work directly on bits.

Conditional Operators

If you find yourself working with bytes, Java actually includes bitwise logic operators. However, we won’t be needing them. For our purposes, the concepts behind three of the basic logic gates have made their way into Java as conditional operators: &&, ||, and !.

The && symbol is the logical AND operator which returns true only when the surrounding expressions also return true. In other words, a && b is only true when a is true and b is true. Otherwise, the expression evaluates to false.

Meanwhile, the || symbol is the logical OR operator which returns true in every case except when all the surrounding expressions return false. In other words, a || b is only false when a is false and b is false. Otherwise, the expression evaluates to true.

Finally, the ! symbol is the NOT operator. We really didn’t talk about this back when we chatted about logic gates, but it flips the value of the bit or in our case the value of the boolean. In other words, !a is true when a is false.

Conditional Operator Syntax

In code, these operators might be used as follows:

boolean isRed = true;
boolean isHot = true;

boolean isFire = isHot && isRed;

Here we have created a pair of variables to assess whether or not we think something is fire. We store the criteria as boolean types in the variables isRed and isHot. We know we have fire if both of those statements are true, so we test for it using the && operator. In this case, we know we have fire because isFire stores true.

If we look back at the nested if statement section, we’ll notice we might have a way to clean up our code. We can now convert our nested if statement to a single if statement using the && operator, so let’s try it:

int value = 5;

if (value > 2 && value < 57) {
        System.out.println("ERROR: Value is greater than 2 and less than 57");
}

Now, we have a much more elegant solution which is more intuitive to read. We can actually improve the readability even more by extracting the expressions into variables using clear naming conventions. However, we won’t do that here. We’ll save that for our readability lesson.

Short-Circuit Evaluation

At this point, we’ve covered the major conditional operators which can use to add logical conditions to our code. As it turns out, these operators have a fun property called short-circuit evaluation which we can leverage to optimize our code.

Short-circuit evaluation is a property of conditional operators in which the second term is only evaluated if the first term isn’t enough to determine the value of the entire expression. For example, if the first term in an AND expression evaluates to false, we know that the entire expression will evaluate to false. There’s no need to evaluate the second term in the expression.

As result, short-circuit evaluation can be really handy when we want to avoid doing a long calculation:

if (shortCalculation() || longCalculation()) {
  // do something
}

In this example, if shortCalculation() returns true, we can skip over the longCalculation() and jump into the code block. Make sense? Let me know in the comments!

Switch Statements

While if statements are great for organizing logic, Java provides another syntax for control flow: the switch statement.

The switch statement is essentially a method which allows us to provide an input that serves as the key to a mapping of various operations. We can use a switch statement to implement the if statement example using 5 and 8:

int value = 5;
String err;

switch (value) {
    case 5: err = "Value is 5";
        break;
    case 8: err = "Value is 8";
        break;
    default: err = "Value is neither 5 nor 8";
        break;
}

System.out.println(err);

In this example, we declare an int variable named value and a String variable to store our error message. The value variable then gets passed into the switch statement where it is quickly mapped to the appropriate case.

As for the String variable, it is assigned before we hit a break statement. We won’t cover break in detail because it is generally considered bad practice, but in this case it allows us to exit the switch statement. Without it, we would run directly into the next case. This is called switch statement fallthrough which allows us to link multiple values to the same behavior.

What’s Next?

In this lesson, we covered the nuances of if statements and the various types of syntax we may see in Java code. In addition, we visited logical operators as a means of organizing conditions. We also touched on the switch statement as well as this notion of break.

Now, that we understand the basics of control flow, we can start organizing more complex classes. In addition, we can now construct methods which make decisions based on their input. These decisions are known as branches, and branches are the first step to adding complexity to code.

In the next lesson, we’ll look at a testing framework that we can use to explore these branches. Unit testing will be invaluable moving forward as we tackle more complicated concepts like loops, lists, and recursion.

As always, share this lesson with your friends if you enjoyed it. And if you really enjoyed it, why not subscribe?

Series Navigation← Class Structure Introduction in JavaJUnit Testing in Java →
Advertisements

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.