3.2
Conditionals
Boolean branches, braces, ordered tests, and switch statements and expressions without hidden fallthrough.
Previously on
Section 3.1 gave you the relational operators, which turn numbers into true or false, and the logical operators that combine them. You also saw that && stops early, which is what makes a null check safe.
That short circuit becomes load bearing in this section. Nearly every condition you write from here on relies on it.
The problem
Everything you have written so far runs top to bottom, every line, every time. Useful programs also select work based on data.
A real program chooses. Show a different message for a failed login. Charge a different price on a weekend. Skip the record that has no email address.
Java gives you three ways to write a choice, and they are not interchangeable. Picking the wrong one gives you code that works and is painful to read six months later.
Two comparisons need special care. Both compile, but neither expresses a content comparison:
if (0.1 + 0.2 == 0.3) // false. Always.
if (name == "admin") // sometimes true, sometimes false, for the same textThe idea
if, else if, else
public class GradeDemo {
public static void main(String[] args) {
int score = 84;
char grade;
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else {
grade = 'C';
}
System.out.println(grade);
}
}Output:
BThe condition in the brackets must give a boolean. Not a number, not null, not an object. In C, if (5) is legal and means true. In Java it does not compile.
Java is refusing a whole category of bug there, the classic being if (x = 5), which in C assigns 5 and is always true. In Java that line is a type error, because assignment gives back a number and if wants a boolean.
switch
When you are checking one value against many fixed options, a ladder of if statements gets noisy. switch says it directly.
switch (day) {
case 6:
case 7:
type = "weekend";
break;
case 1:
type = "monday";
break;
default:
type = "midweek";
}Read the control flow from top to bottom:
case 6:andcase 7:stacked together means “either of these”.default:catches everything else. It is optional.- Without
break, execution continues into the statements under the next label.
Under the hood
Going deeperFallthrough, and why it exists
Without break, switch does not stop at the end of a case. It carries on.
switch (day) {
case 1:
result = "monday"; // no break
case 2:
result += " tuesday";
break;
}
// day = 1 gives you "monday tuesday"This behaviour is called fallthrough. It is almost always a bug when it happens by accident, and Java gives you no warning at all.
So why keep it? Because of the stacked-label shape:
case 6:
case 7:
type = "weekend";
break;case 6: has an empty body, so it falls straight into case 7:. That useful pattern and the dangerous bug are the same mechanism.
Arrow labels and switch expressions
Java 14 made arrow labels and switch expressions permanent language features.
String type = switch (day) {
case 6, 7 -> "weekend";
case 1 -> "monday";
default -> "midweek";
};| Colon labels | Arrow labels | |
|---|---|---|
| Fallthrough | yes, and silent | none. Each case is separate. |
| Leaving a statement case | usually break, return or throw | the arm ends by itself |
| Several values | stack the case labels | case 6, 7 -> |
| Use | common in older switch statements | preferred when fallthrough is not intended |
Label syntax and construct type are separate choices. A switch statement performs work. A switch expression produces a value and must cover every possible selector value. Arrow labels work in either construct. The example above is an expression because its result is assigned to type.
What classic switch accepts
| Allowed | Not allowed | |
|---|---|---|
| Whole numbers | byte, short, char, int and their wrappers | long and Long |
| Decimals | none | float, double |
| Other | String, enum | boolean |
These are language rules. They do not promise one bytecode or machine-code strategy. A compiler can use a dense table for dense integer cases or a lookup for sparse cases. String switching must still distinguish different strings that have the same hash code.
Use switch because it states a fixed choice over one selector. Use if for ranges, unrelated conditions, or conditions that depend on several values.
Passing null to a classic String, wrapper, or enum switch throws NullPointerException. Check for null first when it is a valid input state.
Floating-point equality
System.out.println(0.1 + 0.2 == 0.3); // falseYou already know why, from Section 2.2. Those three numbers have no exact form in binary, so the closest-to-0.1 plus the closest-to-0.2 is not bit for bit the closest-to-0.3.
If the domain means “close enough,” define a tolerance that matches its units and scale:
double tolerance = 0.000001;
if (Math.abs(a - b) <= tolerance) { } // domain-specific absolute toleranceOne fixed epsilon is not correct for every magnitude. Some programs use a relative tolerance,
some require exact bit-level equality, and money often needs decimal arithmetic such as
BigDecimal. Choose the comparison contract before choosing the code.
Text identity and text content
Literal pooling can make an identity comparison appear to compare text:
String a = "hi";
String b = "hi";
String c = new String("hi");
System.out.println(a == b); // true
System.out.println(a == c); // false
System.out.println(a.equals(c)); // trueSame three letters in all three variables. == says yes for one pair and no for the other.
The reason is that == on objects does not compare contents. It compares whether the two variables point at the same object.
Java interns matching String literals, so these two literal expressions designate one pooled object. That is why a == b is true. new String("hi") creates a distinct object, and runtime input commonly produces another distinct object. Compile-time concatenation can also be interned, so the construction syntax is not a reliable content test.
Scanner scanner = new Scanner(System.in);
String typed = scanner.nextLine(); // user types: admin
if (typed == "admin") { } // asks whether both references designate one object
if ("admin".equals(typed)) { } // compares text and is safe when typed is nullTwo later sections finish this story. Section 8.3 explains what equals() actually is and why you often have to write your own. Phase IX explains the String pool and why it exists.
The ternary, used well
String label = (count == 1) ? "item" : "items";Use it to choose between two short value expressions. If either branch performs several actions,
or nesting makes the types and grouping hard to read, use an if statement.
What it costs
Deep if nesting stops being readable fast. Four levels of indentation and nobody can hold the conditions in their head. The usual fix is to deal with the failure cases first and return early, so the main path stays flat:
if (user == null) return;
if (!user.isActive()) return;
// the real work, at one level of indentationThe constant-label switches in this lesson match one selector against fixed alternatives. Use if for ranges or unrelated boolean conditions. Modern Java also has pattern switch, which belongs after classes and inheritance.
Classic switch permits fallthrough, which can be useful for grouped behaviour but is easy to introduce accidentally. Arrow labels do not fall through.
== compares primitive values and reference identity. Identity is correct when you need to know whether two references designate one object. Value-like classes such as String normally need equals() for content.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
You write `if (a == b)` where both are `String` variables holding the text "hi". Sometimes it is true and sometimes it is false, with no obvious pattern. What is `==` actually comparing?
Show the answer
==is comparing whether they are the same object, not whether they hold the same text.Java keeps a pool of String literals. Write
"hi"twice in your source and both point at one shared object, so==is true. Build one withnew String("hi"), or read it from a file or the keyboard, and you get a separate object with identical contents, so==is false.A test built only from literals can therefore pass while a test using runtime input fails.
Use
.equals()for text content, with an explicit null policy. Section 8.3 explains whatequals()really is, and Phase IX explains the pool.Why does `switch` refuse a `long`, when it happily accepts `byte`, `short`, `char` and `int`?
Show the answer
That is a rule of the Java language, not a portable conclusion about one mandatory machine layout. Classic switch selectors support
char,byte,short,int, their wrappers,Stringand enum types. They do not supportlong,float,doubleorboolean.A compiler may emit a table switch, a lookup switch, or other code depending on the cases. Do not choose
switchbecause you assume it is faster. Choose it when one selector is being matched against fixed alternatives.In the old style `switch`, what happens if you forget a `break`, and why was that ever considered a good idea?
Show the answer
Execution carries on into the next case and keeps going until it hits a
breakor the end. This is called fallthrough.It is a bug almost every time it happens by accident, and it is silent. No warning, just extra code running.
It was kept because of one useful shape: stacking labels so several values share one block, like
case 6: case 7: return "weekend";. That works because of fallthrough.Modern arrow switch (
case 6, 7 -> ...) gives you that shape directly and prevents accidental fallthrough. Prefer it when fallthrough is not part of the design.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises85 pointsabout 95 minutes
String Identity and Content
ex-3-2-aReproduce the bug on purpose, so it never catches you by accident.
The first three lines already show the strange part: == says yes for one pair of identical strings and no for another. Run it and see.
Read admin from the keyboard and compare it with the literal using both operators. Explain why == can be false while equals is true by naming identity and content as different relations.
This is a bug that passes every test you write with hardcoded values, and fails the first time a real user types something.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Trap {
static boolean sameObject(String a, String b)
static boolean sameText(String a, String b)
static boolean twoLiteralsAreOneObject()
static boolean literalEqualsNew()
static boolean builtAtRuntimeIsSameObject(String typed)
static String safeCompare(String typed)
}safeCompare returns "match" or "no match" and must not throw when given null. Put the literal on the left.
What your program must do
- Show two literals passing an == comparison
- Show a literal and a new String failing it
- Show text built at run time also failing it
- Write a comparison that works for all three and does not throw on null
public class Trap {
static boolean sameObject(String a, String b) { return false; } // TODO: ==
static boolean sameText(String a, String b) { return false; } // TODO: equals
// Two literals with the same characters. Same object?
static boolean twoLiteralsAreOneObject() { return false; } // TODO
// A literal against new String("admin").
static boolean literalEqualsNew() { return false; } // TODO
// Text built while the program runs, which is what a Scanner gives you.
static boolean builtAtRuntimeIsSameObject(String typed) { return false; } // TODO
// "match" or "no match". Must not throw on null.
static String safeCompare(String typed) { return ""; } // TODO
public static void main(String[] args) {
// TODO: print all of the above
// TODO: build a String at run time with a StringBuilder and try == on it
}
}
Hint 1
== looks correct in small examples and fails on real input.Hint 2
new StringBuilder("adm").append("in").toString() gives you a String with the right characters and a different identity, which is what a Scanner produces.Hint 3almost the answer
"admin".equals(typed). That way a null input answers false instead of throwing, and you never need a null check.Fallthrough, Found and Fixed
ex-3-2-bThere is exactly one missing break in this method. Find it by reading, not by running.
Then work out precisely which months are wrong and what they report instead. Write your prediction down, then run the program and check.
Fix it with a break. Then rewrite the entire method using the arrow form from this section, and say in one sentence why that form makes this bug impossible.
That last part is the real exercise. Knowing a modern feature is nice. Knowing which old bug it deletes is what makes you reach for it.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Fall {
static String describeBroken(int month)
static String describeFixed(int month)
static String describeArrow(int month)
}Keep describeBroken exactly as it is, missing break and all. describeArrow uses the arrow form of switch, which cannot fall through.
What your program must do
- Find which months the broken version gets wrong, without changing it
- Say how many of the twelve it gets right
- Write the fixed version and the arrow version
- Say why grouped case labels are not the same thing as fallthrough
public class Fall {
// Leave this one broken. Find the missing break before you look at anything else.
static String describeBroken(int month) {
String season = "";
switch (month) {
case 12: case 1: case 2: season = "winter"; break;
case 3: case 4: case 5: season = "spring";
case 6: case 7: case 8: season = "summer"; break;
case 9: case 10: case 11: season = "autumn"; break;
default: season = "not a month";
}
return season;
}
// The same thing, fixed.
static String describeFixed(int month) { return ""; } // TODO
// The same again with the arrow form, which cannot fall through at all.
static String describeArrow(int month) { return ""; } // TODO
public static void main(String[] args) {
// TODO: print all three for months 1 to 13 and find where they disagree
}
}
Hint 1
break. Nine of the twelve months are still correct.Hint 2
case 12, 1, 2 -> "winter"; and it never falls through, so this bug cannot be written in it at all.Hint 3almost the answer
Comparing Decimals Safely
ex-3-2-cWrite a comparison for decimals that actually works, and then find out where your fix stops working too.
Start with the obvious version: take the difference, drop the sign, compare it against something tiny. Show it gets the 0.1 + 0.2 case right.
Then try it on very large numbers, and watch it behave strangely.
Working out why is the interesting part, and the answer is already in Section 2.2. It is about how far apart two neighbouring doubles are, and how that gap grows as the numbers get bigger.
There is no perfect answer here. That is worth knowing too. Comparing decimals is genuinely hard, and the honest response is to avoid needing to whenever you can.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Compare {
static boolean closeEnough(double a, double b)
static boolean closeRelative(double a, double b)
static double addTenth()
static boolean naiveEquals()
}closeEnough uses a fixed tolerance. closeRelative scales the tolerance to the size of the numbers. Both must say a value equals itself, including zero.
What your program must do
- Show that 0.1 + 0.2 does not equal 0.3
- Write a fixed tolerance version and show it handles that case
- Find where the fixed tolerance stops working
- Write a relative version and say when you would use each
public class Compare {
// A fixed tolerance.
static boolean closeEnough(double a, double b) { return false; } // TODO
// A tolerance that scales with the size of the numbers.
static boolean closeRelative(double a, double b) { return false; } // TODO
static double addTenth() { return 0; } // TODO: 0.1 + 0.2
static boolean naiveEquals() { return true; } // TODO: 0.1 + 0.2 == 0.3
public static void main(String[] args) {
// TODO: print the naive comparison and the real value of 0.1 + 0.2
// TODO: try closeEnough on 1e15 and 1e15 + 1. Predict first.
}
}
Hint 1
Math.abs(a - b) is the comparison.Hint 2
Hint 3almost the answer
a == b first, or comparing zero with zero divides by zero.Registry Status Rules
ex-3-2-dFive written rules. Turn them into code a human can check against the rules without a debugger.
The naive version of this is four levels of nested if, and it will pass the tests. Do not write that version. Write the version where each refusal is its own line and the method stays flat.
Return a reason for every refusal, not just false. A method that says “no” without saying why is a method somebody will be cursing at 2am.
Then prove your structure was worth it: invent a sixth rule, add it, and see whether you had to rearrange anything. If you did, your structure was wrong. If you added one line, it was right.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Status {
static String deploymentDecision(boolean isActive, double readiness, boolean onMedicalHold, int clearanceLevel, int supplies)
}Return exactly "DEPLOY" when the unit can go. Otherwise return a string starting with "NO" that names the rule that stopped it, using the words active, supplies, medical or readiness.
What your program must do
- Implement all five rules and give a specific reason for every refusal
- Work out which rules are absolute blockers and check those first
- Decide what happens at exactly 60.0 and exactly 40.0, and defend it
- Try a unit that breaks several rules and say which reason you report
public class Status {
// Rules for whether a unit can be deployed:
// 1. The unit must be active.
// 2. Readiness must be at least 60.0
// 3. A unit on medical hold cannot deploy, whatever its readiness.
// 4. A unit with clearance level 5 can deploy at readiness 40.0 or more.
// 5. A unit with zero supplies cannot deploy, no matter what.
//
// Return "DEPLOY", or a reason starting with "NO" that names the rule.
static String deploymentDecision(
boolean isActive, double readiness, boolean onMedicalHold,
int clearanceLevel, int supplies) {
return ""; // TODO
}
public static void main(String[] args) {
System.out.println(deploymentDecision(true, 75.0, false, 3, 100));
System.out.println(deploymentDecision(true, 45.0, false, 5, 100));
System.out.println(deploymentDecision(true, 95.0, true, 5, 100));
System.out.println(deploymentDecision(false, 95.0, false, 5, 100));
System.out.println(deploymentDecision(true, 95.0, false, 5, 0));
}
}
Hint 1
Hint 2
double required = clearance >= 5 ? 40.0 : 60.0; and then one comparison covers both.Hint 3almost the answer
After the credits
The String example introduced the difference between being the same object and having equal content. Later reference-type lessons develop that distinction.
In Section 4.1 you will work directly with reference values. Java keeps their physical representation opaque.
In Section 6.3 you will find out what that means when you pass an object to a method, and why copying one is harder than it looks.
In Section 8.3 you will write equals() yourself. The inherited implementation from Object uses identity semantics. A class that needs value equality must define it explicitly.
Section 8.4 uses an exhaustive switch expression over an enum, where the compiler can reject a missing case.
switch will return in 8.4 - Enums