(Updated September 1, 2024)
Table of contents
-
Relational Operators
One-way and two-way selection
Compound Statements
Multi-way selection
The Dangling
else
Logical Operators
Comparing Strings
The
switch
StatementShort-circuit Evaluation
The ternary operator
?:
Quiz
Exercises
Relational Operators
Up to this point, our statements have been executing sequentially; they have been processed in order without any of the sequence changing. The program will execute statements in the order they are written. However, there are times that we would like to execute statements conditionally rather than unconditionally.
Statements can be executed in three ways:
- Sequential (unconditional)
- Conditional (selective or branch)
- Iterative (looping)
Programs may contain all three forms of statement execution but, as we have seen, are not required to do anything more than sequential. Iterative statements will be covered in the next chapter. This chapter will concentrate on the conditional execution of our programs.
When deciding what should be done when a condition is met, we must first determine the condition to be detected. In other words, what condition must be met, and how will we know when that condition or event has occurred? Some examples are:
if 18 is evenly divisible by 2
then print “the number is even”
if 8 is less than 0
then print “the number is negative”
if the Sun is shining
note that it is daytime
otherwise
note that is is NOT daytime
Examples of conditional statements in English.
Of course, none of these are actual Java statements. These statements are known as pseudocode. Pseudocode is used to describe the details of a piece of logic that is yet to be written. The beauty of pseudocode is that we can express anything we like without writing in a specific language. Therefore, pseudocode assists in conceptualizing ideas while remaining language neutral.
Determining if a condition has been met often requires comparing a particular value with some known quantity. To make these comparisons, we will need a set of tools that perform the comparison work and give a yes or no result, or more to the point, a true or false result. The collection of Java relational operators gives us a true or false result based on comparisons. There are six relational operators, and they are shown in Table 1.
Relational Operator | Description |
---|---|
== | Equal to |
!= | Not equal to |
< | Less than |
<= | Less than or equal to |
> | Greater than |
>= | Greater than or equal to |
Table 1: Relational operators for comparison.
As we will see, the relational operators are binary, just like our arithmetic operators. The operators can involve integral and floating-point primitive data types. Only equal to (==) and not equal to (!=) may be applied when comparing the boolean primitive data type variables.
It would be best to be cautious when testing floating-point values for equality since there is a possibility of a loss of precision when these numbers are stored in memory and are more likely to occur when these values are involved in complex arithmetic expressions.
One-way and two-way selection
Let us take the first two examples from above and rewrite them in actual Java code. The first two examples demonstrate the concept of one-way selection; there is only one possible outcome based on the test being performed. The rewrite is shown below.
if ( 18 % 2 == 0 )
System.out.println(“The number is even.”);
if ( 8 < 0 )
System.out.println(“The number is negative.”);
Examples of conditional statements in Java.
These examples show a conditional test that must be satisfied before performing the indented portion of code. The line below the if
test is indented to show that the code will only be executed if the test evaluates to true
. Parentheses always surround the condition test itself.
It is now established that statements will be executed if conditions are true. The third example, however, shows an opportunity where work may need to be done when the condition is true
or alternate work done when the condition is false
– a two-way selection. Example 1 shows a complete program for the third example. The original pseudocode that has been converted is highlighted.
public class DayOrNight {
public static void main(String[] args) {
boolean daytime, sunShining;
sunShining = true;
if ( sunShining == true )
daytime = true;
else
daytime = false;
System.out.println("sunShining = " + sunShining);
System.out.println("daytime = " + daytime);
}
}
Example 1: Program demonstrating the else clause of the if statement.
The program output is as expected:
sunShining = true daytime = true
It was easy to predict the output without really knowing what the else
clause would do for our program. Since sunShining
was true
, we would expect the value of daytime to be set to true. The else
clause of the if
statement allows the programmer to offer an alternative if the condition test fails to evaluate to true
. Therefore, if the condition test failed, the statement indented beneath the else
clause would be executed setting daytime
to false
.
The else
portion of an if
is matched with the closest unfinished if
, that is, one that has no else
portion.
Compound Statements
Let us change our DayOrNight
class and add another boolean
variable, nighttime
. The product of which is Example 2. In this example we can see that two assignment statements will be executed for both the true and false portion of our if
/else
construct.
The if
portion and the else
portion, by default, can only have one statement associated to each. When we need either of them to perform more than one statement worth of work, we use a compound statement. A compound statement is formed the same way our main method body was formed – with curly braces. Note the closing of the curly braces before the else
portion begins.
As we will see in upcoming sections and future chapters, the compound statement is used by many constructs.
public class DayOrNight2 {
public static void main(String[] args) {
boolean daytime, nighttime, sunShining;
sunShining = true;
if ( sunShining == true ) {
daytime = true;
nighttime = false;
}
else {
daytime = false;
nighttime = true;
}
System.out.println("sunShining = " + sunShining);
System.out.println("daytime = " + daytime);
System.out.println("nighttime = " + nighttime);
}
}
Example 2: Two-way selection with a compound statement.
Multi-way selection
Multi-way selection is often needed when the data tested falls into more than two response categories. Consider the code in Example 3 and Example 4 where we want to test if a number is positive, negative or zero. Clearly a single else
is not enough, but we also cannot have more than one else
per if
statement. The concept of a nested if
can be applied to allow a test with more than two options.
import java.util.Scanner;
public class NumType {
static Scanner kb = new Scanner(System.in);
public static void main(String[] args) {
int x;
System.out.print("Enter an integer: ");
x = kb.nextInt();
System.out.print("The value " + x + " is ");
if ( x < 0 )
System.out.println("Negative.");
else if ( x > 0 )
System.out.println("Positive.");
else
System.out.println("Zero.");
}
}
Example 3: Multi-way if statement.
import javax.swing.JOptionPane;
public class NumTypeGUI {
public static void main(String[] args) {
int x;
String s;
s = JOptionPane.showInputDialog("Enter an integer:");
x = Integer.parseInt(s);
s = "The number " + x + " is ";
if ( x < 0 )
s = s + "Negative.";
else if ( x > 0 )
s = s + "Positive.";
else
s = s + "Zero.";
JOptionPane.showMessageDialog(null, s, "Results",
JOptionPane.PLAIN_MESSAGE);
}
}
Example 4: Multi-way if statement with dialog boxes.
Technically speaking, a nested if
actually looks like:
if ( x < 0 )
System.out.println("Negative.");
else
if ( x > 0 )
System.out.println("Positive.");
else
System.out.println("Zero.");
The Example 3 format is preferred for ease of writing and is accepted conventional structure in several programming languages.
The Dangling else
Consider a multi-way selection where there is an else portion for the outer condition, but not for the inner one. Here is a piece of code to demonstrate:
if ( age >= 18 )
if ( age >= 21 )
System.out.println("Legal Drinking Age.");
else
System.out.println("Ineligible To Vote.");
The issue is that although the indentation shows the else belongs to the test of age >= 18
, Java (and other languages) associates the else
with the closest unfinished if
. As a result, the else
actually belongs to the test for legal drinking age. Those who are ages 18 to 20 are therefore ineligible to vote! This clearly needs fixing and can be done with a simple compound statement to correctly associate the else
as shown below:
if ( age >= 18 )
{
if ( age >= 21 )
System.out.println("Legal Drinking Age.");
}
else
System.out.println("Ineligible to vote.");
Logical Operators
The set of logical operators allows one to join logical expressions into larger ones. The logical
operators are listed in Table 2.
Logical Operator | Description |
---|---|
&& |
and |
|| |
or |
! |
not (unary operator) |
Table 2: Logical operators.
The ! (not) unary operator simply inverts the boolean value. Therefore !true
is false
and !false
is true
.
The two binary operators && (and) and || (or) work according to standard boolean algebra, where the result of && is only true if both operands are true. Likewise, || is only false when both operands are false. A standard truth table is shown in Table 3.
Boolean Expression | Result |
---|---|
true && true |
true |
true && false |
false |
false && true |
false |
false && false |
false |
true || true |
true |
true || false |
true |
false || true |
true |
false || false |
false |
Table 3: Truth table showing &&
and ||
.
The char
type can be used with relational operators since it is an integer primitive type, so let us add some characters to our examples of selection. Here are a few:
char ch;
//...
if ( ch >= '0' && ch <= '9' )
System.out.println("The character is a digit.");
if ( ch >= 'A' && ch <= 'Z' )
System.out.println("The character is an uppercase letter.");
if ( (ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') )
System.out.println("The character is a letter.");
if ( (ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') )
System.out.println("The character is a alphanumeric.");
The first two selection statements show simple determination of digit (in the range '0'
through '9'
) or uppercase letter (in the range 'A'
through 'Z'
).
The third one joins the concept of letters being upper or lowercase. Note the use of parentheses to separate the uppercase test from the lowercase test. Also, see that or was used to join the two tests since a character can't be both uppercase and lowercase.
Our final example takes on the more generalized classification of an alphanumeric character: the character is uppercase, lowercase or numeric.
Comparing Strings
Comparing strings is quite simple once you understand lexicographical analysis principles. A lexicographical analysis is a character by character comparison of two strings using the Unicode collating sequence until we either:
- Run out of characters in one of the strings.
- Two characters in the same position of the two strings are different.
- Both strings have been tested and no mismatches were found.
In other words, if the strings are of different lengths, they cannot be a match. If two characters from the same position, say position 12, in each string, are different, they cannot be a match.
Otherwise, they are a perfect match, and we say the strings are equal.
The compareTo()
method of the String class returns three possible values as a result of this character by character comparison. This is shown in Table 4 and it is assumed that str1
and str2
are valid String
reference variables.
Expression | Value | Description |
---|---|---|
str1.compareTo(str2) |
< 0 | str1 is less than str2 |
str1.compareTo(str2) |
> 0 | str1 is greater than str2 |
str1.compareTo(str2) |
== 0 | str1 and str2 are equal |
Table 4: Possible results for the compareTo()
String method.
Why does the compareTo()
method return these values? One way to look at this is to realize that the values are the same when you subtract one value from another if the result is zero. So, given two strings of equal length, if the difference of the last character of each string is zero, then the two strings match since to reach the last character of the strings, they must have matched all along.
Now consider when two characters do not match. If we take the characters from each string at the same position and subtract the Unicode value of the str2
character from the Unicode value of the str1
character, there will be either a positive or negative result. If the result is negative, then the Unicode value from str1
was less than the the Unicode value from str2
. That single character comparison is all that was needed to determine that str1
is less than str2
.
Consider the code in Example 5. This is an insight into the use of equals()
and compareTo()
methods.
public class ConditionalStrings {
public static void main(String[] args) {
String r, s, t, u, v;
r = "AAA";
s = "AAA";
t = "AAD";
u = "AAADK";
v = "FD";
System.out.println("r and s are lexicograhically equal.");
System.out.println("r.compareTo(s) = " + r.compareTo(s));
System.out.println("\nr is less than u based on length. (\"AAA\" matches the prefix).");
System.out.println("r.compareTo(u) = " + r.compareTo(u));
System.out.println("\nv is greater than t because 'F' - 'A' = 5. (Length is irrelevant here.)");
System.out.println("v.compareTo(t) = " + v.compareTo(t));
System.out.println("\ns is less than t because 'A' - 'D' = -3.");
System.out.println("s.compareTo(t) = " + s.compareTo(t));
System.out.println("\nt is greater than s because 'D' - 'A' = 3.");
System.out.println("t.compareTo(s) = " + t.compareTo(s));
/* Note that we are comparing the references!
This is the same as using equals() from
Object (the class from which all classes are
derived). */
System.out.println("\nr and s are equal based on ==.");
System.out.println("r == s is " + (r == s));
System.out.println("\nr and s are equal based on equals().");
System.out.println("r.equals(s) is " + r.equals(s));
}
}
Example 5: Many forms of String
comparison.
Below is another table of some String
methods that perform various comparisons and also some additional forms of manipulation, such as trimming whitespace and replacing characters or String
s.
String Method | Purpose |
---|---|
boolean equals(Object obj) |
Returns true if this is equal to obj . Equality is based on both references pointing to the same object. |
int compareTo(String s) |
Unlike equals() , compareTo() is a lexicographic analysis of this and s . If the lengths are different, the delta of the lengths is returned if the shorter string is a prefix to the longer. If negative, then this is less than s . If lengths are equal, then the character for character analysis continues whereby the character value from s is subtracted from the character value from this . |
int compareToIgnoreCase(String s) |
The same as compareTo() while ignoring the character case. |
boolean matches(String regex) |
Returns true if this matches the regular expression criteria contained in regex . |
String replace(char oldChar, char newChar) |
Returns a new String with all occurrences of oldChar replaced with newChar . |
String replaceAll(String regex, String newString) |
Returns a new String with all occurrences of matching regex replaced with newString . |
String replaceFirst(String regex, String newString) |
Returns a new String with first occurrence of matching regex replaced with newString . |
String trim() |
Returns a new String with all leading and trailing whitespace removed. |
Table 5: Some additional String
methods.
The switch
Statement
The multi-way selection of if-else-if-else-if-else is very reliable. There exists another method for accomplishing a similar task – the switch
statement. First, let us review the multi-way if
selection using an integer month
to reflect the month of the year.
import java.util.Scanner;
public class MonthIfElse {
static Scanner kb = new Scanner(System.in);
public static void main(String[] args) {
int month;
String monthStr;
System.out.print("Enter the month (1-12): ");
month = kb.nextInt();
if ( month == 1 )
monthStr = "January";
else if ( month == 2 )
monthStr = "February";
else if ( month == 3 )
monthStr = "March";
else if ( month == 4 )
monthStr = "April";
else if ( month == 5 )
monthStr = "May";
else if ( month == 6 )
monthStr = "June";
else if ( month == 7 )
monthStr = "July";
else if ( month == 8 )
monthStr = "August";
else if ( month == 9 )
monthStr = "September";
else if ( month == 10 )
monthStr = "October";
else if ( month == 11 )
monthStr = "November";
else if ( month == 12 )
monthStr = "December";
else
monthStr = "AN UNKNOWN MONTH";
System.out.println("The month selected is " + monthStr);
}
}
Example 6: Recap of the if
-else
multi-way selector.
As you can see in Example 6, the integer is used to determine the string value for the month. This approach can be rewritten using a switch
statement as in Example 7.
import java.util.Scanner;
public class MonthSelect {
static Scanner kb = new Scanner(System.in);
public static void main(String[] args) {
int month;
String monthStr;
System.out.print("Enter the month (1-12): ");
month = kb.nextInt();
switch ( month ) {
case 1: monthStr = "January";
break;
case 2: monthStr = "February";
break;
case 3: monthStr = "March";
break;
case 4: monthStr = "April";
break;
case 5: monthStr = "May";
break;
case 6: monthStr = "June";
break;
case 7: monthStr = "July";
break;
case 8: monthStr = "August";
break;
case 9: monthStr = "September";
break;
case 10: monthStr = "October";
break;
case 11: monthStr = "November";
break;
case 12: monthStr = "December";
break;
default: monthStr = "AN UNKNOWN MONTH";
break;
}
System.out.println("The month selected is " + monthStr);
}
}
Example 7: Multi-way selection using a switch
statement.
The switch
statement uses a parenthesized integer expression or selector. In this example the selector is simply month
. The switch
statement takes the selector value and compares it to the list of possible action cases. The selector value and the following list of cases must be an integer. Each case
represents a selection or path to be taken based on the value of the selector. In addition, all cases have a particular value to be matched, followed by a colon.
The optional default
case is used to catch selector values that have not matched any other cases.
All statements that follow the colon immediately after a matched case are executed and are not enclosed in curly braces as you would expect a compound statement would normally be. Instead of encasing the statement block in curly braces, a break
statement is used to signify both the end of the block and to leave the switch
statement. If a break
statement is absent, execution would continue through the next case
.
In Java 7, the ability to switch
on a String
was added. So, if we needed to go the other way, meaning convert a String
to an int
, we could use the following.
switch ( monthStr ) {
case "January": month = 1;
break;
case "February": month = 2;
break;
case "March": month = 3;
break;
case "April": month = 4;
break;
case "May": month = 5;
break;
case "June": month = 6;
break;
case "July": month = 7;
break;
case "August": month = 8;
break;
case "September": month = 9;
break;
case "October": month = 10;
break;
case "November": month = 11;
break;
case "December": month = 12;
break;
default: month = 0;
break;
}
Leaving a break
statement out could be accidental or intentional. The accidental omission of the break
statement would result in code that is logically incorrect in one or more cases of the switch
statement. However, the intentional omission of the break
statement serves to combine cases where appropriate, and this can help to simplify the code.
switch
options. This was further enhanced in Java 13 and made permanent in Java 14. Depending on your perspective, this was to help make the construct more of an expression or eliminate the use of break
. In the end, they added yield
, which is intended to be used when switch
is an expression.With the enhanced switch
now available in Java 14, we will provide an example of how this can benefit programmers.
Consider the clumsy, verbose example with variables letterGrade
as a char
and qp
as an int
which determines the quality points as a step in calculating the cumulative average.
switch (letterGrade) {
case 'a':
case 'A':
qp = 4;
break;
case 'b':
case 'B':
qp = 3;
break;
case 'c':
case 'C':
qp = 2;
break;
case 'd':
case 'D':
qp = 1;
break;
default:
qp = 0;
break;
}
This code can also be written as follows, but it is still a formatted mess.
switch (letterGrade) {
case 'a': case 'A': qp = 4; break;
case 'b': case 'B': qp = 3; break;
case 'c': case 'C': qp = 2; break;
case 'd': case 'D': qp = 1; break;
default: qp = 0; break;
}
As you can see, the format is rather loose with the whitespace, and the reformatting helps a little. But we can still improve it. We will swap the colon for an arrow (->
), combine cases using commas, and eliminate the break
, which is now implied.
switch (letterGrade) {
case 'a', 'A' -> qp = 4;
case 'b', 'B' -> qp = 3;
case 'c', 'C' -> qp = 2;
case 'd', 'D' -> qp = 1;
default -> qp = 0;
}
Finally, consider the switch
as an expression below. Note the semicolon at the end of the switch
since this is now an assignment statement involving qp
.
int qp = switch (letterGrade) {
case 'a', 'A' -> 4;
case 'b', 'B' -> 3;
case 'c', 'C' -> 2;
case 'd', 'D' -> 1;
default -> 0;
};
If there was an opportunity to provide an error message but not interrupt the flow, we could use yield
and a code block. Remember that for each case
in a switch
expression, all labels must provide a value.
int qp = switch (letterGrade) {
case 'a', 'A' -> 4;
case 'b', 'B' -> 3;
case 'c', 'C' -> 2;
case 'd', 'D' -> 1;
case 'F', 'f' -> 0;
default -> {
System.err.println("UNKNOWN GRADE ENCOUNTERED! " + letterGrade);
yield 0;
}
};
After all of this, the only remaining question is when do you choose if
-else
over switch
? For a possible answer to that question, consider the earlier code example where we identified a character as alphanumeric. With 52 letters and ten digits, it would be unlikely one would choose switch
.
In other words, if there is an extensive range of values or the condition test is not a simple comparison, it is probably better suited to an if
construct. If there are only a few values, it is possible either would work, and one version may require more code. It is really up to the programmer which is the better choice and could be easily modified to accommodate future changes.
Short-circuit Evaluation
Java offers a fail-early or pass-early model of evaluation called short circuit evaluation with the logical operators &&
and ||
. The details of how it works are shown below.
How does it work?
A short circuit is precisely what it implies - a connection with a low-resistance conductor, to use the electrical term. In other words, there is nothing to resist the circuit, and there will be a delivery of a large amount of energy in a short period - this is a bad thing, electrically speaking.
From a coding perspective, the same is true - because there is low resistance in our ability to determine the outcome of a condition. The difference here is that we can use less energy because of the predictive nature of truth tables.
Consider the following truth table.
Boolean Expression | Result |
---|---|
true && true |
true |
true && false |
false |
false && true |
false |
false && false |
false |
true || true |
true |
true || false |
true |
false || true |
true |
false || false |
false |
Table 5: Truth table showing &&
and ||
.
Table 5 is the same Table 3 with a few rows highlighted. The specific details of interest have to do with the left operand.
With AND (&&
) if either side of the expression is false
, then the answer is false
.
Therefore: With AND (&&
) when the left operand is false
, the entire expression is false
.
With OR (||
) if either side of the expression is true
, then the answer is true
.
Therefore: With OR (||
) when the left operand is true
, the entire expression is true
.
In each of these circumstances, it does not matter what the truth value of the right operand happens to be. No one cares - including the compiler! After your program compiles, the resultant code is such that when the left side is false
for AND (&&
) and the left side is true
for OR (||
), the right side of the expression is ignored since it is insignificant.
In other words, the result is already well-known, and we have short-circuited our way to the answer.
The ternary operator ?:
A ternary operator is a clever tool that has its origins back in the early C programming language. It offers a means to make an if-else
a bit more elegant.
Consider the following classic min/max example.
int x, y;
int min, max;
if ( x < y ) {
min = x;
max = y;
} else {
min = y;
max = x;
}
Although this code does what we expect, it is wordy. In its most powerful form, the ternary operator allows us to make decisions on the right-hand side of an assignment operator. It is most often written in the form:
resultVar = condition ? trueResult : falseResult;
The variable resultVar
will have the trueResult
value assigned if the condition
is true. Otherwise, it will have the falseResult
assigned. The previous example can be simplified to the following.
min = ( x < y ) ? x : y;
max = ( x > y ) ? x : y;
Another helpful example is the plural solution. Consider this bit of code.
if ( guesses > 1 )
response = "You have " + guesses + " guesses remaining."
else
response = "You have 1 guess remaining."
While this is not an overly complicated scenario, it lends itself to the ternary operator's opportunity.
response = "You have " + guesses + ( guesses > 1 ? " guesses " : " guess " ) + "remaining.";
The decision to use the ternary operator is often a personal one. It is generally chosen to simplify bits of code, as demonstrated above.
Quiz
Exercises
- (Beginner) Given a spell DC (difficulty class) of 16, generate a random number representing a d20 roll and add 2 to it (the constitution modifier). Write an
if
test to see if the roll meets or exceeds the DC representing success. Tell the user the DC, the roll, and their modifier. If the roll succeeds, tell them they avoided being poisoned; otherwise, indicate they are poisoned. - (Beginner) Write a
switch
statement to convert the String values of the days of the week "Sunday" through "Saturday" to the range 0 through 6. - (Intermediate) Convert the
switch
statement for letter grade to quality points conversion into a multi-wayif
test. - (Advanced) Read two strings from the user. Show them both strings and tell them which one is lexicographically greater than the other.
- (Advanced) Perform the previous exercise using the ternary operator
?:
.