Zero-Day Java Guide 2 – Operators and Control Flow

Operators

In the previous sections, we learned how to create and use Java variables and arrays. Now that we can store data as variables, we need to perform operations on them, such as adding or multiplying. In this section, we’ll look at the basic mathematical operators that we can perform on all of the Java number types. Then we’ll move on to the very important concept of mix-mode math: what if we have a statement with integers and floats? Towards the end of this section, we’ll move on to operations that can be performed on a single variable and operations that return a boolean value.

2.1 – Operator Precedence

The above table shows the order of precedence when we have a statement with multiple operators. This table is here solely for reference. Operators on the above table are the ones we’ll be looking at though more operators exist.

Basic Arithmetic Operators

2.2 – Arithmetic Operators

We’re very familiar with the operators in this section since we learned almost all of them back in grade school. The above table shows the arithmetic operators that we might encounter in Java. We’re already familiar with the operators for addition, subtraction, multiplication, and division. It is important to note that all of these operators are what we call binary operators, as opposed to unary operators, because they require two operands, or inputs. It doesn’t make sense to perform addition on a single number!

Perhaps the one operator we might not be as familiar with is the remainder operator, denoted by a percent sign. (This is not to be confused with the modulus. They’re not quite the same thing!) The remainder operator simply returns the remainder of an integer division. We’ll talk more about integer division in the next section, but, suppose we have a statement like 8 % 3. We expect this to return two because three goes into eight two times and leaves a remainder of two. The remainder operator, in the case of x % 3, can return any integer in range of values from 0 to (3-1). To generalize this, if have x % y, then the remainder can be anywhere from 0 to (y-1). As we’ll see later on, this operator is particularly useful in determining the parity of a number. In other words, it helps us determine whether an integer is even or odd. The expression to do so is x % 2, where x is any integer variable. We expect this to return zero if x is divisible by two and therefore even, and we expect this to return something that isn’t zero if the number is odd.

One misconception about the assignment operator is that it is the same as the “equals” sign in mathematics. Let’s look at an example of why this is not the case. Suppose we have the following Java statements:

int x = 1;
x = x + 3;

From our previous knowledge, we know that we are declaring an integer called “x” and assigning it the value of one. The next line is particularly interesting because it makes perfect sense to Java but no sense in mathematics. How can x be equal to x plus three? How can any number be equal to three more than itself? It can’t! This is precisely the reason it doesn’t make sense to interpret this as being a purely mathematical statement. However, to the Java compiler, this statement is clear: set the new value x to be the old value of x plus three. After that line executes, x will have a value of four. For primitive types anyway, this is exactly what the assignment operator does. It takes the value of the right-hand side and puts it in the variable on the left-hand side. This works a bit differently for reference types, such as Strings, but we’ll get to that when we talk about objects towards the end of this book.

Speaking of Strings, the addition operator serves another purpose if the two operands are Strings. The operator will perform an addition of sorts on the two String in the sense that it will concatenate them together. In other words, the addition operator also serves as the concatenation operator where it will combine the two Strings together into a single String. Let’s look at an example:

String hello = “Hello";
String world = “ World!”;
String helloWorld= hello + world;

In the above example, we have two Strings that we set to be “Hello” and “ World!” The String resulting from their concatenation will be “Hello World!” Note the space at the beginning of the world String. Spaces are characters and aren’t added automatically, so we have to add them to a String whenever necessary.

2.3 – Compound Assignment Operators

In some cases, we can combine the assignment operator with another arithmetic operator to form a compound assignment operator. These operators are really no more than a short-hand way of performing basic operations where we are changing the variable by some literal or constant.

int x = 1;
x = x + 1;
x += 1;

The last two statements do exactly the same thing: they increment the value of x by one. When we get to unary operators, we’ll see an even shorter way of denoting this.

In this section we learned about the basic arithmetic operators that we can use to perform operations on two variables. These operators include addition, subtraction, multiplication, division, and remainder. The remainder operator simply returns the remainder of an integer division (i.e. 17 % 6 = 5.) This assignment operator is different than the “equals” relation in mathematics because takes the value of the right-hand side and stores it in the variable on the left-hand side. The addition operator becomes the concatenation operator when both operands are Strings; the operator combines two Strings together and returns a single one.

Mix-Mode Math

In this section, we’re going to look a the dangers of incorrectly using the arithmetic operators and interpreting Java to be mathematics. To give an example, let’s look at the following Java statement

double x;
x = 1 / 2;

Mathematically speaking, x should have a value of 0.5. However, if we print the value of this variable out to the console (using System.out.println(x)), we’ll see that the value is actually zero.

But why is this the case? If we remember from our chart of variable types, we know that a double can have a decimal value, but that doesn’t appear to be the case here. This is because division is tricky in Java in the sense that the type of the result of the division depends on the types of operands it receives. In this case, we are dividing two integers, therefore Java gives us back an integer. But we know integers can’t have decimal parts so Java discards, or truncates, anything after the decimal point as opposed to rounding up or down. If we remember back to the table of variable types, they were arrange in a specific manner. Doubles and floats are at the top of the food chain when it comes to Java types. This is because if we insert a double into our statement, Java will convert the entire result to a double. This doesn’t just work for doubles, but also for any “superset” variable type. Essentially, we can fit any number variable into a double, but we can’t fit a double into an int since it is a “subset” of double. In other words, double is the largest, most general set and every number type is a smaller subset of double.

Sets-of-Numbers

This is particularly useful when it comes to division. Let’s fix our previous statement so that it correctly returns a double:

int x;
x = 1.0 / 2;

Note the difference here: we’re using 1.0 instead of just 1, and, by doing so, returning a value of 0.5 instead of 0. This is because adding a decimal point to 1 converts it to a double. Now, Java sees that we are dividing a double by an int, and, since doubles are a superset of ints, the entire division returns a double. To use another example, suppose we wanted to add two shorts, but we weren’t getting the expected result. We could add a short and an int, and Java will return us an int since ints are a superset of shorts.

Unary Operators

2.5 – Unary Operators

This section explores Java’s unary operations. The differ from the arithmetic operators and conditional operators (that we’ll talk about in the next section) because they only require a single operand, hence the name unary. There are some things to note when using these that we’re going to cover more in depth in this section. To start off, the unary minus operator simply returns the opposite of the given number type. However, it does not change the value of the operand. To see this, let’s look at an example:

int x = 5;
int y = -x;
System.out.println(x);
System.out.println(y);

We see that x still return positive five and y returns negative five. This means that the unary minus operator didn’t change the value of x, but it returned the the opposite value, from positive to negative and negative to positive.

Another important concept to grasp is the positioning of the increment and decrement operators. Saying ++x is different than saying x++. Placing the that operator before the variable means the operator is in the prefix position, and placing that operator after the variable means the operator is in the postfix position. If the increment or decrement operator is in the prefix condition, it means that the value of x will be incremented and the expression (++x) will return that new value of x. However, if the increment or decrement operator is in the postfix condition, then x will be incremented AFTER returning it’s value. In other words, the expression (x++) will return x, then increment it by one. For one line statements, the position doesn’t affect the result.

int x = 1;
x++;
++x;

In the above code snippet, the two statements do exactly the same thing: increment the value of x by one. However, suppose we run the following code.

int x = 1;
System.out.println(++x);
System.out.println(x++);
System.out.println(x);

The first print statement would print two since the expression (++x) returns the newly incremented value of x. However, the subsequent line would print two since the expression (x++) will return the value of x, then increment it. The final line will verify this claim because it prints three. If they’re being used in a larger statement, the position of the increment and decrement operators really do matter.

In this section, we learned about the unary operators as well as some additional information about the unary minus and increment and decrement operators that can save us headaches in the future. The unary minus operator only returns the inverted value of the operand but doesn’t change the actual operand. Regarding the increment and decrement operators, positioning them in the prefix position will return the newly incremented or decremented value; however, putting them in the postfix position will return the current value of the operand and then increment or decrement that operand after the completion of the statement.

Conditional Operators

2.6 – Conditional and Relational Operators

In this section, we’re going to look at relational and conditional operators that we can apply to variables and return boolean results. These boolean results will be particularly useful in the next section when we talk about decision control flow.

Like the arithmetic operators, these should also look familiar since they are quite commonly used, but we have to be very precise in our typing. We cannot interchange these characters: => is not valid Java! Note that the equality check operator is == and not =, the assignment operator. All of these operators return boolean values: either true or false.

2.7 – Truth Table for AND and OR

The conditional operators are the conditional AND and conditional OR. We use these on two booleans operands to return another boolean. In the above figure, we see a truth table for the AND and OR operations. Simply put, the AND operator only returns true if both of the boolean operands are true; the OR operator returns true if at least one of the boolean operands are true.

If we look at the truth table, we can see that we only need a single false for AND to return false and a single true for OR to return true. These exhibit what we call short-circuiting behavior. In an AND, this means that if the first operand is false, the second one won’t even be evaluated! For an OR, if the first operand is true, the second one won’t be evaluated either.

In this section, we learned about the different types of relational operators to check for equality and inequality. It is important to note the difference between the equality check operator (==) and the assignment operator (=). We can use the conditional AND and conditional OR operators to combine two boolean statements to return a boolean that depends on both of the boolean operands.

Control Flow: Decisions

Now that we’re learned about the relational and conditional operators, we can actually use them in both decision control flow and looping constructs. In this section, we’ll focus on the portion of control flow that relates to decision making. Suppose we only want to run a block of code if a particular condition is true. We can do exactly this using decision control flow. After we learn about decision control flow, we’ll move on to looping constructs.

if-then and if-then-else

The most basic of all control flow is the if-then statement. Suppose we want to check if a number is even or odd. We can do this via the following code snippet.

int x = 29846392741;
if (x % 2 == 0) {
    System.out.println(“x is even!”);
}

In the above code, “x is even!” will only print if the condition in the if statement is true. Let’s take a look at the syntax: the first thing we need is the “if” keyword. Following the keyword, the condition is in parentheses. The condition must be a boolean statement since it can only have two possible values. Next is the code that we want to run if the condition is true in curly braces. In general, blocks of code go in curly braces. Now if the condition is evaluated to true, then the lines of code in the curly braces will execute; if the condition is evaluated to false, those lines are skipped over entirely.

The above code only print outs whether x is even or not. Suppose we want to also know when it is odd. For this, Java has a construct called if-then-else. It works in a similar fashion to the if-then construct, except we have an else case that will execute if the condition in the if statement is false.

int x = 29846392741;
if (x % 2 == 0) {
    System.out.println(“x is even!”);
} else {
    System.out.println(“x is odd!”);
}

In either case, something is printed out to the console: the parity of x, either even or odd. If the condition (x % 2 == 0) is false, then the code in the else block will execute. But now suppose we want to know the sign of a number, which is common information to know about an number. However, we know that numbers can be positive, negative, or zero (due to the trichotomy law). This is three cases, not just two! It turns out we can combine our if-then and if-then-else into a construct that will let us test as many conditions as we want.

int x = -29846392741;
if (x > 0) {
    System.out.println(“x is positive!”);
} else if (x < 0) {
    System.out.println(“x is negative!”);
} else {
    System.out.println(“x is zero!”);
}

In the above code, we use “else if (condition)” in case the first condition fails. We expect this to print “x is negative!” to the console. But let’s look more into the code. The first if condition will be checked, and, if it is true, ONLY the code in the if block will run. Suppose it is false, then control will pass to the next else if condition, and, if that is true, ONLY the code in THAT else if block will run. In the case that none of the conditionals is true, then ONLY the code in the else block will run. In other words, once some condition is met or none of the conditions are met, only one block of code executes.

In this section, we learned about the most basic control flow statements: if-then and if-then-else. These statements will execute a block of code only if a condition is met. In the case of the if-else-if-else statement, all of the else if conditions will be checked in order until one is met, or, if none are met, the else block will execute.

Switch Case

In the previous section, we learned about the basic control flow statements, including the if-else-if construct where we can run different code for different conditions. But this can get very cumbersome if we are checking many conditions and want to run many different blocks of code. In this section, we’re going to look at a shorthand way of doing exactly this using the switch-case construct.

Suppose we have the following sequence of if-else-if blocks checking the resulting value from an HTTP request. Whenever we ask for a website or send information, our web browser gets back a status code to tell us how the entire transaction went. If we get back a 200, then we know everything went fine! If not, then the status code tells us what went wrong. The code below enumerates only some of the possibilities.

int x = 200;
if (x == 200) {
    System.out.println(“Everything is OK!”);
} else if (x == 404) {
    System.out.println(“Not Found!”);
} else if (x == 301) {
    System.out.println(“Moved Permanently!”);
} else if (x == 500) {
    System.out.println(“Internal Server Error!”);
}
// And so on…
} else {
    System.out.println(“Unknown Code!”);
}

As we can see, this can get very clumsy if there are many cases. One way to combat this awkward formatting and syntax is to use a switch-case construct. The below code performs the exact same function as the above code, but looks much cleaner.

int x = 200;
switch (x) {
    case 200:
        System.out.println(“Everything is OK!”);
        break;
    case 404:
        System.out.println(“Not Found!”);
        break;
    case 301:
        System.out.println(“Moved Permanently!”);
        break;
    case 500:
        System.out.println(“Internal Server Error!”);
        break;
    default:
        System.out.println(“Unknown Code!”);
        break;
}

The first thing we need is the switch keyword and the variable that we’re switching on. This variable can be a byte, short, char, int, or even a String. However, it can’t be a long, float, or double since those types are very large, and, in the case, of floats and doubles, they can have a near-infinite number of possibilities due to the fact they store decimal numbers. Afterwards, in the block of the switch case, we can have any number of case statements. These case statements check if the value of the variable we’re switching on is equal to the value after the case keyword. Looking at the first statement, “case 200:”, we’re checking x is equal to 200. This is important to note because we can’t do checks for inequality, such as less than or not equal to.

After the colon, the control continues until it reaches the “break;” statement. This break statement is crucial because it moves the control flow out of the switch case after execute the code of a particular case. If we don’t have that break statement, we get a phenomenon called “falling through” case statements. Consider above code snippet with a few break statement removed and the value of x changed.

int x = 404;
switch (x) {
    case 200:
        System.out.println(“Everything is OK!”);
        break;
    case 404:
        System.out.println(“Not Found!”);
        // Should be a break here!
    case 301:
        System.out.println(“Moved Permanently!”);
        // And another break here!
    case 500:
        System.out.println(“Internal Server Error!”);
        break;
    default:
        System.out.println(“Unknown Code!”);
        break;
}

Removing the break statement in the 404 case will cause it to “fall through” to the cases underneath it in the switch case until it reaching a case where there is a break statement. When this code executes, the console will print out the following.

Not Found!
Moved Permanently!
Internal Server Error!

This is not what we expect! We only expected the console to print “Not Found!” instead of three lines! This is because control will go into the 404 case, but no break statement will jump control out of that case and the switch case. Control will go down to the subsequent case statements and execute code in those cases until it finally reaches a case where a break statement exists. Hopefully, this example illustrates the danger of forgetting a break statement!

That being said, there are instances where we can safely use this “fall through” phenomenon. For example, suppose we want to print out whether x is a vowel or consonant, given that x is a lowercase letter in the alphabet and not anything else. There are more than one vowel, but we want all of them to print the exact same thing. We can use the “fall through” phenomenon to our advantage and write code that looks like the following.

char c = ‘e’;
switch (x) {
    case ‘a’:
    case ‘e’:
    case ‘i’:
    case ‘o’:
    case ‘u’:
        System.out.println(“c is a vowel!”)
        break;
    case ‘y’:
        System.out.println(“c is sometimes a vowel!”)
        break;
    default:
        System.out.println(“c is a consonant!”)
        break;
}

In the above snippet, the console will correctly print “c is a vowel”. Let’s take a closer look at what is happening. We know that c is ‘e’ so the first case isn’t executed. The next case is executed, but there’s no code or break statement! Therefore, control will “fall through” to the next case, which also doesn’t have any code or a break statement! This trend will continue until we reach the ‘u’ case. We see that there is code to print to the console and a break statement to get us out of the switch case. Success! This code does exactly what we want it to do, and we’re using the “fall through” phenomenon. If c was ‘y’ or none of the above cases, only the code in those case statements would execute and then break out of the loop. We’ll look at break statements again when we talk about branching statements after for loops.

In this section, we looked at a cleaner way to display many if-else-if statements: the switch-case construct. This construct only allows us to check for equality and requires us to put break statements after the code for each case, or control will “fall through” to all subsequent cases until a break statement is encountered. However, we learned that we can use this phenomenon to our benefit if we want to have a block of code run only once for multiple cases.

Control Flow: Loops

Besides decision-making constructs, loops are the other important part of control flow. Loops allow us to run the same block of code while a condition holds true. In this section, we’ll talk about the main three loops: while, do-while, and for. In addition, we’ll also be looking at arrays agains and seeing how we can use loops in conjunction with arrays. At the end of this section, we’ll also look at branching statements, like break and continue, which allow us to manipulate control flow even more thoroughly.

while and do-while loop

The most basic of all loops is the while loop. Suppose we want to sum up all of the numbers between 1 and 100. Carl Friedrich Gauss, a very famous mathematician and child prodigy, actually did this by hand as a child in the 1700s! But let’s use a while loop instead to sum these numbers up.

int sum = 0;
int i = 1;
while (i <= 100) {
    sum = sum + i;
    i++;
}

We get the same answer Gauss did a few centuries ago! Let’s take a closer look at the code. First, we declare two integers, one to hold the sum and another as a counter variable. In this case, it is ok for us to use a single letter to use as a counter variable since that is the sole purpose of it and it is convention to use ‘i’ (for index) or ‘j’ or ‘k’ as counter variable names. Next, we have the while construct that will execute the code in the curly braces until the condition is no longer met. First, the boolean condition is checked to see if it applies, then the sum is added. If we remember back from the Operators section, we could have expressed the sum more concisely using compound assignment operators: “sum += i;”. The next line is crucial to the loop because it increments the counter. If we forgot to do this, then the loop would never end! We call this an infinite loop when the condition is always true and control never exits the loop. Our loop counter will go from one to 101, since that is the number that gets us out of the loop. What we have is the sum of all numbers between one and a hundred! Note that the condition in the while is like that of the decision-making control flow in the sense that it can be any boolean so we can use relational and conditional operators too.

Suppose we are writing a bookkeeping program that requires the user to input some price. We know that a price can only be zero or any positive number. In other words, we can’t have the user input a negative price. But we also don’t want to terminate the entire program if the user makes a mistake! What we really want is to keep asking the user for a valid price until they do. Instead of a while loop, a do-while loop would be perfect here.

double price = -1;
do {
    price = askUser(“Please enter a valid price: ");
} while (price < 0);

In the above code, assume askUser is something that asks the user for a price and return a double. We’ll be talking more about methods later. First, we set the price to an invalid value starting off. Then, we have our do-while construct. The main difference between the while and do-while loops is that the do-while loop’s condition is check AFTER the loop runs. This guarantees that the loop runs at least once, which is exactly what we want. We want to give the user a chance to enter a valid number at least once. The do-while loop will ask the user for a price first. Suppose that price is valid, then the loop condition would be false, and control would break out of the loop. Suppose it was false, then the loop condition would be true, and the loop would run again, asking the user to input a valid price again. This cycle would continue until the user finally enters a valid price. Do-while loops aren’t used as much as while or for loops, but they’re great for input validation!

In this section, we looked at two of the most basic loops: while and do-while loops. The while loop will execute the code in the curly braces until the loop condition is false. The do-while loop will do the same thing, except for the key difference that the condition is checked at the end of the loop. Code in the do-while loop is guaranteed to run at least once. This makes the loop ideal for input validation.

for loop

After looking at the basic loops, we’re going to look at a more complicated loop: the for loop. Java also includes an extension of this called for the enhanced for, or for each, loop. We’ll also look at how we can use loops to declare and manipulate arrays using both for and for each loops. To learn about these new constructs, let’s first look at something we know.

int i = 0;
while (i < 100) {
    System.out.println(“Counter is at “ + i);
    i++;
}

We know this is a while loop that will simply print out “Counter is at “ and the current value of the counter variable. Note that this loop will run a hundred times since, within the loop, the counter will run from 0 to 99 inclusive, which is a hundred times. If we had a more complicated loop, then this would take more lines. We can represent the same while loop as the following for loop. (Technically, in the above while loop, we need to wrap the whole snippet in an instance initialization block so that the counter variable’s scope expires after the loop executes.)

for (int i = 0; i < 100; i++) {
    System.out.println(“Counter is at “ + i);
}

The previous two code snippets will produce the exact same output. The for loop is a nice, compact way to represent a while loop. There are three parts to a for loop: initialization, condition, and update. The initialization is where the loop counter is created and set. Following initialization is the condition, which is the same as the while loop. Finally, we give the statement to alter the loop variable to break out of the for loop. The update step doesn’t necessarily have to increment the variable. Instead, we could increment by two or decrement by seven, but we need a way to exit the loop.

For-Loop-Parts

So far, we’ve only looked at looping being used for printing to the console, but we can also use them to initialize large arrays. If we remember back to arrays, we know that we can initialize an array using the curly braces array initialization syntax. But what if we wanted to create a blank array of a hundred elements? We certainly don’t want to have type in a hundred zeros! Luckily, we can use loops to do this job for us.

int[] data = new int[100];
for (int i = 0; i < data.length; i++) {
    data[i] = 0;
}

In the above code, we initialize an array of a hundred elements. Using a for loop, we set each of those elements to zero. Note that we can get the length of an array by calling “someArray.length”. In the for loop, we’re setting the ith element of the data array equal to zero. The counter variable i goes from 0 to the length of the array. Our condition is strictly less than instead of less than or equal to because array indices start at 0! There is no such thing as the 100th index of an array of size 100!

Suppose we now want to print out each of elements of our data array. We could write another for loop that iterates through each element and prints it. There is another type of for loop that we can use when we just need to look at the elements of an array and not change them. We call this the enhanced for or for each loop. The syntax is more concise than the standard for loop.

for (int datum : data) {
    System.out.println(datum);
}

In the above code, we’re saying that for each datum in the data array, print it out to the console. This gives us a way to iterate through an array and see each element without having to worry about the array index, length of the array, or increment conditions. We can also iterate over List, Queues, and other Java collection types.

Branching Statements

To give us more control over loops, we have these statements call branching statements. We’ve already seen one before: break. The break statement allows us to forcibly exit the nearest loop. We have another statement called the continue statement that allows us to skip the current iteration of the loop. For example, suppose we were iterating through each character of a phone number and only printing out the digits. If the user supplied us with parentheses or a hyphen, then we need to skip over it.

String phoneNumber = “012-345-6789”;
for (int i = 0; i < number.length(); i++) {
    char number = phoneNumber.charAt(i);
    if (number == ‘-‘) {
        continue;
    }
    System.out.println(number)
}

In the above code snippet, we use a for loop to iterate through the String. We’ll talk more about methods later, but we can get the length of a String by calling .length() on it (as opposed to just .length for arrays). To get a single character from a String, we can call .charAt(…) and pass in an int to get the character at that particular index. The interesting part is in the if statement. We say that if the number is actually a hyphen, we use the continue statement, skipping over this current iteration and moving on to the next one.

Now suppose we replaced that continue statement with a break. Then the program would exit the loop and stop printing to the console. The console would only show the first three digits since after those, the loop encounters a hyphen; therefore the program breaks out of the nearest loop, which is the for loop. If we had nested loops, the break statement would only get us out of the nearest, or most indented, loop.

In this section, we learned about the two branching statements: break and continue. Break allows us exit the nearest loop. Continue allows us to skip over the current iteration of the loop and move to the next iteration. These control statements will allows us finer-grain manipulation of loops.