java-foundations Lesson 9 18 min read

Complex Conditionals

Compound conditions, De Morgan's Laws, and the guard pattern

Reading: Reges & Stepp: Ch. 4 §4.2–4.3

After this lesson, you will be able to:

  • Build compound conditions with && (AND), || (OR), and ! (NOT)
  • Apply De Morgan’s Laws to simplify negated conditions
  • Use the short-circuit guard pattern to prevent crashes
  • Write range checks and input validation using compound conditions
  • Use the ternary operator for simple conditional assignments

When One Condition Is Not Enough

Lesson 1.3 covered if/else with a single condition. But real decisions are rarely that simple:

  • “Can they rent a car?” → must be at least 25 and have a license
  • “Do they get the day off?” → it is a weekend or a holiday
  • “Is the score valid?” → must be at least 0 and at most 100

Each of these requires compound conditions — multiple boolean expressions combined with logical operators.

From CSCD 110: You used and, or, and not in Python conditionals. Java uses symbols instead: && for and, || for or, ! for not. The logic is identical — only the syntax changes.

Logic Python Java
Both true age >= 21 and has_id age >= 21 && hasId
At least one true is_weekend or is_holiday isWeekend \|\| isHoliday
Negate not raining !raining

&& (AND): Both Must Be True

&& is a checklist — every item must be checked off:

int age = 25;
boolean hasLicense = true;

if (age >= 25 && hasLicense) {
    System.out.println("Can rent a car");
}

If either side is false, the whole expression is false.

|| (OR): At Least One Must Be True

|| is alternatives — having either one (or both) is enough:

boolean isWeekend = false;
boolean isHoliday = true;

if (isWeekend || isHoliday) {
    System.out.println("No work today!");
}

The expression is false only when both sides are false.

! (NOT): Invert

! flips true to false and vice versa:

boolean isRaining = false;

if (!isRaining) {
    System.out.println("Go outside!");
}

Precedence: ! Before && Before ||

Logical operators have a precedence order, just like arithmetic:

Priority Operator Meaning
1 (highest) ! NOT
2 && AND
3 (lowest) || OR

This means && binds tighter than ||:

// Java sees this as: a || (b && c)
a || b && c

// If you wanted (a || b) && c, you MUST use parentheses

Common Pitfall: Don’t rely on precedence. Always use parentheses to make compound conditions readable. If you have to think about precedence, your reader will too.

Check Your Understanding

Given boolean a = true, b = false, c = true;, what is a || b && c?


The Range-Checking Pattern

Checking whether a value falls within a range is one of the most common compound conditions:

// In range: use &&
if (score >= 0 && score <= 100) {
    System.out.println("Valid score");
}

// Out of range: use ||
if (score < 0 || score > 100) {
    System.out.println("Invalid: must be 0-100");
}

Common Pitfall: Java does not support chained comparisons like math notation or Python:

// WRONG — does not compile in Java
if (0 <= score <= 100) { }

// CORRECT — two comparisons joined with &&
if (score >= 0 && score <= 100) { }

Short-Circuit Evaluation: The Guard Pattern

Java’s && and || are short-circuit operators — they stop evaluating as soon as the result is determined:

  • false && ... → result is false (right side skipped)
  • true || ... → result is true (right side skipped)

This is not just an optimization — it is a safety feature:

// Safe: checks for zero BEFORE dividing
if (count != 0 && total / count > 10) {
    System.out.println("High average");
}
// If count is 0, Java stops at "count != 0" (false)
// and never evaluates "total / count" (which would crash)
// Safe: checks for null BEFORE calling .length()
if (name != null && name.length() > 0) {
    System.out.println("Name: " + name);
}

The Trick: The guard pattern — put the safety check first in an && expression. If the guard fails, the dangerous operation is never attempted.


De Morgan’s Laws

De Morgan’s Laws tell you how to distribute negation across && and ||:

Law Equivalence
Law 1 !(A && B) = !A \|\| !B
Law 2 !(A \|\| B) = !A && !B

The recipe: flip && to || (or vice versa), and negate each piece.

// Original: eligible if 18+ AND has license
if (age >= 18 && hasLicense) {
    System.out.println("Eligible");
}

// De Morgan's: NOT eligible means under 18 OR no license
if (age < 18 || !hasLicense) {
    System.out.println("Not eligible");
}

A practical application — simplifying confusing negated conditions:

// Confusing: outer ! with compound condition
if (!(temperature >= 68 && temperature <= 72 && humidity < 60)) {
    System.out.println("Uncomfortable");
}

// Simplified with De Morgan's — much easier to read
if (temperature < 68 || temperature > 72 || humidity >= 60) {
    System.out.println("Uncomfortable");
}

Key Insight: In-range checks use &&. Out-of-range checks use ||. These are De Morgan duals of each other — if you know one, flip the operators and negate each piece to get the other.

Check Your Understanding

Which expression is equivalent to !(x > 5 && y < 10)?


The Ternary Operator

For simple conditional assignments, the ternary operator is a one-line if-else:

condition ? valueIfTrue : valueIfFalse
int score = 85;
String status = (score >= 60) ? "Pass" : "Fail";
System.out.println(status);  // "Pass"

// Equivalent if-else:
String status;
if (score >= 60) {
    status = "Pass";
} else {
    status = "Fail";
}

Good uses: simple assignments, choosing labels, inline expressions:

int max = (a > b) ? a : b;
String label = (count == 1) ? "item" : "items";
System.out.println(count + " " + label);

Key Insight: Use the ternary for simple, one-value assignments. If the condition or values are complex, or you need to do more than assign a value, use if-else. Never nest ternary operators — it becomes unreadable.


Input Validation: Validate Before You Use

Every interactive program should follow: Input → Validate → Process → Output. Validate before you use the data:

import java.util.Scanner;

public class GradeCalculator {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Enter score (0-100): ");
        int score = scanner.nextInt();

        // Validate BEFORE using
        if (score < 0 || score > 100) {
            System.out.println("Error: Score must be between 0 and 100.");
            return;
        }

        // Safe to process — score is guaranteed 0-100
        String grade;
        if (score >= 90) {
            grade = "A";
        } else if (score >= 80) {
            grade = "B";
        } else if (score >= 70) {
            grade = "C";
        } else if (score >= 60) {
            grade = "D";
        } else {
            grade = "F";
        }

        System.out.println("Score: " + score + " → Grade: " + grade);
    }
}

Common Pitfall: Validate before using data, not after. If you divide before checking for zero, the crash happens before your error message.


Complete Example: Discount Calculator

This program combines compound conditions, input validation, the ternary operator, and printf:

import java.util.Scanner;

public class DiscountCalculator {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Loyalty member? (true/false): ");
        boolean isMember = scanner.nextBoolean();

        System.out.print("Is it a holiday? (true/false): ");
        boolean isHoliday = scanner.nextBoolean();

        System.out.print("Enter total: $");
        double total = scanner.nextDouble();

        if (total < 0) {
            System.out.println("Error: Total cannot be negative.");
            return;
        }

        // Gets discount if: member, OR (holiday AND total > 50)
        boolean getsDiscount = isMember || (isHoliday && total > 50);

        String status = getsDiscount ? "DISCOUNT APPLIED" : "No discount";
        double discount = getsDiscount ? total * 0.10 : 0;
        double finalTotal = total - discount;

        System.out.println("\n--- Receipt ---");
        System.out.printf("Subtotal:  $%.2f%n", total);
        System.out.printf("Discount:  $%.2f (%s)%n", discount, status);
        System.out.printf("Total:     $%.2f%n", finalTotal);
    }
}

Sample run:

Loyalty member? (true/false): false
Is it a holiday? (true/false): true
Enter total: $75.00

--- Receipt ---
Subtotal:  $75.00
Discount:  $7.50 (DISCOUNT APPLIED)
Total:     $67.50
Check Your Understanding

Why is count != 0 && total / count > 10 safe from division by zero?


Summary

Compound conditions let you express complex decision logic. && requires both sides true (checklist). || requires at least one side true (alternatives). ! inverts.

Precedence: ! before && before ||. Always use parentheses for clarity.

Short-circuit evaluation is a safety tool: guard && dangerousOperation prevents crashes when the guard fails.

De Morgan’s Laws simplify negated conditions: flip && to || and negate each piece. In-range checks use &&; out-of-range checks use ||.

The ternary operator (condition ? a : b) is a concise one-line if-else for simple assignments. Validate input before processing — the pattern is Input → Validate → Process → Output.

Next lesson: Loop patterns and debugging — fencepost problems, sentinel loops, and boolean flag patterns that build on everything you have learned so far.