Control Flow: Making Decisions
Duration: 45 min
Control Flow: Making Decisions
Duration: 45 min
Introduction
Control flow statements allow your programs to make decisions and execute different code paths based on conditions. Instead of running code line-by-line, you can direct the execution based on whether conditions are true or false. This module covers the fundamental decision-making structures in Java: if/else statements, switch expressions, ternary operators, and nested conditions.
When writing real-world applications, control flow is everywhere. Authentication systems check user credentials. E-commerce platforms determine shipping costs based on location. Games decide what happens when a player collects an item. All of these use the patterns you'll learn in this module.
If/Else Statements
The if statement executes a block of code only when a condition is true. The else clause runs when the condition is false. The else-if allows you to chain multiple conditions.
public class LoginValidator {
public static void validateLogin(String username, String password) {
if (username == null || username.isEmpty()) {
System.out.println("Error: Username cannot be empty");
} else if (password.length() < 8) {
System.out.println("Error: Password must be at least 8 characters");
} else if (username.length() > 50) {
System.out.println("Error: Username too long");
} else {
System.out.println("Login credentials accepted");
}
} public static void main(String[] args) {
validateLogin("john", "short"); // Password too short
validateLogin("alice_smith", "securePass123"); // Valid
validateLogin("", "password123"); // Username empty
}
}
This code demonstrates several key patterns. First, we check for empty strings using isEmpty(), a common validation pattern. Second, we use || (OR) to combine multiple conditions. Third, we chain else if statements to check progressively more specific conditions. Each condition is evaluated only if the previous ones were false, making your code efficient.
The order of conditions matters. If you check the most common case first, you reduce unnecessary evaluations. In authentication, checking for empty credentials is cheaper than checking password policies, so it should come first.
Switch Statements
Switch statements are ideal when you have many conditions checking the same variable. They're more readable than long if-else chains.
public class GradeCalculator {
public static String getGradeDescription(char grade) {
switch (grade) {
case 'A':
return "Excellent - 90-100%";
case 'B':
return "Good - 80-89%";
case 'C':
return "Satisfactory - 70-79%";
case 'D':
return "Passing - 60-69%";
case 'F':
return "Failing - Below 60%";
default:
return "Invalid grade";
}
} public static void main(String[] args) {
System.out.println(getGradeDescription('A')); // Excellent
System.out.println(getGradeDescription('D')); // Passing
System.out.println(getGradeDescription('X')); // Invalid
}
}
Java switch statements fall through to the next case unless you use break. This is sometimes useful for combining cases, but it's also a common source of bugs. Always include break unless you specifically want fall-through behavior. The default case acts like else in if statements—it catches any values you didn't explicitly handle.
Modern Java (version 14+) supports switch expressions, which are more concise and prevent accidental fall-through:
public class ModernSwitch {
public static String getDayType(int day) {
// Switch expression - returns a value
return switch(day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> "Weekend";
default -> "Invalid";
};
} public static void main(String[] args) {
System.out.println(getDayType(1)); // Weekday
System.out.println(getDayType(6)); // Weekend
}
}
Ternary Operator
The ternary operator is a compact way to choose between two values based on a condition. The syntax is condition ? valueIfTrue : valueIfFalse.
public class DiscountCalculator {
public static void calculatePrice(double originalPrice, int customerAge) {
// Ternary operator: seniors get 20% discount
double finalPrice = customerAge >= 65 ? originalPrice * 0.8 : originalPrice;
// Can be nested for multiple conditions
String label = customerAge < 18 ? "Child" :
customerAge < 65 ? "Adult" :
"Senior";
System.out.printf("Price: $%.2f (%s)%n", finalPrice, label);
} public static void main(String[] args) {
calculatePrice(100, 70); // Senior price: $80.00
calculatePrice(100, 35); // Adult price: $100.00
calculatePrice(100, 12); // Child price: $100.00
}
}
Use ternary operators for simple, readable decisions. Nested ternaries quickly become confusing and should be replaced with if-else statements instead.
Nested Conditions
Complex logic often requires nested conditions. The key is to structure them clearly and avoid deeply nested code.
public class LoanApproval {
public static void approveLoan(double income, int creditScore, int yearsEmployed) {
boolean incomeQualifies = income > 30000;
boolean creditQualifies = creditScore >= 650;
boolean employmentQualifies = yearsEmployed >= 2; if (incomeQualifies) {
if (creditQualifies) {
if (employmentQualifies) {
System.out.println("Loan approved!");
} else {
System.out.println("Need 2+ years employment");
}
} else {
System.out.println("Credit score too low (min: 650)");
}
} else {
System.out.println("Income too low (min: $30,000)");
}
}
public static void main(String[] args) {
approveLoan(50000, 700, 5); // Approved
approveLoan(50000, 600, 5); // Credit too low
approveLoan(25000, 700, 5); // Income too low
}
}
The deeply nested structure is hard to follow. A better approach uses early returns or combines conditions:
public class LoanApprovalRefactored {
public static void approveLoan(double income, int creditScore, int yearsEmployed) {
if (income <= 30000) {
System.out.println("Income too low (min: $30,000)");
return;
}
if (creditScore < 650) {
System.out.println("Credit score too low (min: 650)");
return;
}
if (yearsEmployed < 2) {
System.out.println("Need 2+ years employment");
return;
}
System.out.println("Loan approved!");
}
}
This "guard clause" pattern is much clearer—each rejection condition exits early, leaving the happy path at the bottom.
Logical Operators
Combining conditions with logical operators (&&, ||, !) is fundamental to control flow.
public class AccessControl {
public static void checkAccess(boolean isAdmin, boolean isOwner, boolean isPremium) {
// AND operator: all conditions must be true
if (isAdmin && isPremium) {
System.out.println("Full admin access granted");
}
// OR operator: at least one condition must be true
else if (isAdmin || isOwner) {
System.out.println("Elevated access granted");
}
// NOT operator: inverts the condition
else if (!isPremium) {
System.out.println("Basic access only (consider upgrading)");
}
// Complex combinations
if ((isAdmin || isOwner) && isPremium) {
System.out.println("Premium member with elevated privileges");
}
} public static void main(String[] args) {
checkAccess(true, false, true); // Full admin access
checkAccess(false, true, false); // Elevated access
checkAccess(false, false, true); // Premium basic access
}
}
Remember operator precedence: ! (NOT) is evaluated first, then && (AND), then || (OR). Use parentheses to clarify complex expressions.
Common Patterns
Pattern 1: Range Validation
int score = 85;
if (score >= 0 && score <= 100) {
System.out.println("Valid score");
} else {
System.out.println("Score out of range");
}
Pattern 2: Null Checks
String name = getUserName();
if (name != null && !name.isEmpty()) {
System.out.println("Hello, " + name);
} else {
System.out.println("Name not provided");
}
Pattern 3: Status Checking
String status = getOrderStatus();
if ("PENDING".equals(status)) {
// Use .equals() for string comparison, never ==
System.out.println("Order is being prepared");
}
When to Use Each Structure
- If/else: Multiple different conditions or complex logic
- Switch: Single variable with many specific values
- Ternary: Simple choice between two values (assignment context)
- Nested if: Related conditions, but consider refactoring for clarity
Advanced Control Flow Patterns
Boolean Variables and Readability
Often, complex conditions are clearer when stored in descriptive boolean variables:
public class AgeGatedContent {
public static void displayContent(int age, boolean isPremium, boolean isVerified) {
boolean canAccessFreeContent = age >= 13;
boolean canAccessPremium = isPremium && isVerified;
boolean canAccessAdult = age >= 18;
if (canAccessAdult) {
System.out.println("Adult content available");
} else if (canAccessPremium) {
System.out.println("Premium content available");
} else if (canAccessFreeContent) {
System.out.println("Free content available");
} else {
System.out.println("Content restricted for your age");
}
}
}
This approach makes the code self-documenting. Anyone reading canAccessAdult immediately understands its purpose without parsing the age logic.
Short-Circuit Evaluation
Java evaluates && and || operations from left to right and stops as soon as it knows the result:
public class ShortCircuit {
public static boolean expensiveCheck(String value) {
System.out.println("Running expensive check...");
return value.length() > 5;
}
public static void main(String[] args) {
String input1 = "hi";
String input2 = "hello world";
// Short-circuit: if false, expensiveCheck never runs
if (input1 != null && expensiveCheck(input1)) {
System.out.println("Check passed");
}
// expensiveCheck runs because input2 is not null
if (input2 != null && expensiveCheck(input2)) {
System.out.println("Check passed");
}
// Short-circuit: if first is true, second never evaluates
if (true || expensiveCheck(input1)) {
System.out.println("Always true");
}
}
}
Use this knowledge strategically: place cheap/likely-to-fail conditions before expensive ones. For &&, put conditions most likely to be false first. For ||, put conditions most likely to be true first.
De Morgan's Laws for Cleaner Logic
public class DeMorgan {
// Not (A and B) equals (Not A) or (Not B)
boolean notBothValid = !(isValid && isComplete);
boolean equivalentForm = !isValid || !isComplete;
// Not (A or B) equals (Not A) and (Not B)
boolean neitherValid = !(isValid || isActive);
boolean equivalentForm2 = !isValid && !isActive;
}
These logical equivalences help you simplify complex conditions and make them more readable.
Real-World Scenarios
User Authentication with Multiple Factors
public class MultiFactorAuth {
public static boolean canLogin(String username, String password,
boolean has2FA, boolean is2FAVerified) {
// Guard clause: reject immediately if credentials are invalid
if (username == null || username.isEmpty()) {
return false;
}
if (password == null || password.length() < 8) {
return false;
}
// If 2FA is enabled, verify it
if (has2FA && !is2FAVerified) {
return false;
}
// All checks passed
return true;
}
}
Payment Processing with Business Rules
public class PaymentProcessor {
public static String processPayment(double amount, String accountType,
double balance, boolean isVerified) {
// Minimum transaction amount
if (amount < 0.01) {
return "Amount too small";
}
// Unverified accounts have limits
if (!isVerified && amount > 1000) {
return "Unverified accounts limited to $1000 per transaction";
}
// Premium accounts get higher limits
if (accountType.equals("premium") && isVerified) {
if (amount > 50000) {
return "Amount exceeds premium limit";
}
} else if (accountType.equals("standard")) {
if (amount > 10000) {
return "Amount exceeds standard limit";
}
}
// Insufficient funds
if (amount > balance) {
return "Insufficient funds";
}
return "Payment processed successfully";
}
}
Performance Considerations
Avoiding Repeated Method Calls
public class PerformanceTips {
// Inefficient: calls length() multiple times
if (text.length() > 0 && text.length() < 100) {
// Process text
}
// Better: store in variable
int textLength = text.length();
if (textLength > 0 && textLength < 100) {
// Process text
}
// Best: use local variable within scope
if (!text.isEmpty() && text.length() < 100) {
// Process text
}
}
Common Pitfalls and How to Avoid Them
Pitfall 1: Using = Instead of ==
// WRONG - assigns instead of compares
if (x = 5) { } // Compilation error in most cases// CORRECT - compares
if (x == 5) { }
Pitfall 2: Forgetting Scope of Variables
if (condition) {
int x = 10;
}
// System.out.println(x); // ERROR - x doesn't exist here
Pitfall 3: Switch Fall-Through
// WRONG - falls through to next case
switch (day) {
case "Monday":
System.out.println("Start of week");
case "Tuesday":
System.out.println("Second day");
break; // Missing break before this
}
Key Takeaways
1. Control flow allows your code to make decisions based on conditions
2. If/else chains handle multiple scenarios; switch works for discrete values
3. Ternary operators are compact for simple decisions
4. Logical operators (&&, ||, !) combine multiple conditions
5. Early returns and guard clauses keep nested code readable
6. Always use .equals() for string comparison, not ==
7. Order conditions efficiently—check cheap/common cases first
8. Use boolean variables to make complex conditions self-documenting
9. Leverage short-circuit evaluation for performance
10. De Morgan's Laws can simplify complex boolean logic
Quiz
Question 1: What's the output of this code?
int x = 10;
if (x > 5) {
if (x > 15) {
System.out.println("A");
} else {
System.out.println("B");
}
} else {
System.out.println("C");
}
- A) A
- B) B ✓
- C) C
- D) No output
Question 2: Which is the correct way to compare two strings in Java?
- A)
if (str1 == str2)
- B)
if (str1.equals(str2))✓
- C) Both are equally correct
- D) Neither works in Java
Question 3: What's the output?
int score = 85;
String result = score >= 90 ? "Pass" : score >= 70 ? "Conditional" : "Fail";
System.out.println(result);
- A) Pass
- B) Conditional ✓
- C) Fail
- D) Compilation error
Question 4: What will this code print?
for (int i = 0; i < 3; i++) {
switch (i) {
case 0:
case 1:
System.out.print(i);
case 2:
System.out.print(i);
}
}
- A) 012
- B) 0122 ✓
- C) 0 1 2 2
- D) Compilation error
Question 5: Which condition is false?
- A)
true && falseevaluates to false ✓
- B)
true || falseevaluates to true
- C)
!trueevaluates to false ✓
- D)
false && falseevaluates to false ✓
Question 6: What's the best practice for checking if a string is not null and not empty?
- A)
if (str != null || !str.isEmpty())
- B)
if (str != null && !str.isEmpty())✓
- C)
if (str != "")
- D)
if (!str.equals(""))