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 && false evaluates to false ✓
  • B) true || false evaluates to true
  • C) !true evaluates to false ✓
  • D) false && false evaluates 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(""))