☕ Java

Nested if

A nested if is an if statement placed inside the body of another if or else block. Nesting allows multi-level decision making — first check a broad condition, then refine with a more specific condition inside it. While nesting is sometimes necessary, deep nesting quickly reduces readability and should be refactored using guard clauses, logical operators, or extracted methods.

Syntax and Basic Nested if

A nested if is written by placing an if statement inside the body of another if or else. The inner if is only evaluated when the outer condition is true. Indentation visually represents the nesting level — each level is indented further to show which if it belongs to.
Java
// ── Basic syntax: ────────────────────────────────────────────────────
//
//  if (outerCondition) {
//      // inner if only reached when outerCondition is true:
//      if (innerCondition) {
//          // executes when BOTH outer AND inner are true
//      } else {
//          // executes when outer is true AND inner is false
//      }
//  } else {
//      // executes when outerCondition is false
//  }

// ── Example: eligibility check: ──────────────────────────────────────
int age    = 20;
boolean hasID = true;

if (age >= 18) {
    // Only checked if age >= 18:
    if (hasID) {
        System.out.println("Entry allowed.");       // executes
    } else {
        System.out.println("ID required.");
    }
} else {
    System.out.println("Must be 18 or over.");
}

// ── Execution trace for age=20, hasID=true: ───────────────────────────
// 1. age >= 18true  → enter outer if block
// 2. hasID      → true  → enter inner if block
// 3. Print "Entry allowed."
// 4. Exit both blocks

// ── Execution trace for age=20, hasID=false: ─────────────────────────
// 1. age >= 18true  → enter outer if block
// 2. hasID      → false → enter inner else block
// 3. Print "ID required."

// ── Execution trace for age=15, hasID=true: ──────────────────────────
// 1. age >= 18false → enter outer else block
// 2. Print "Must be 18 or over."
// 3. Inner if is NEVER evaluated (outer was false)

Multi-Level Nesting

Nesting can extend to three or more levels. Each additional level is only reached when all outer conditions are true. While technically unlimited, nesting beyond two levels is a strong signal to refactor — each extra level multiplies the number of execution paths and makes the code exponentially harder to understand and test.
Java
// ── Three-level nesting example: ─────────────────────────────────────
int age       = 22;
boolean member    = true;
boolean hasCoupon = true;

if (age >= 18) {
    System.out.println("Age check passed.");

    if (member) {
        System.out.println("Member discount applied.");

        if (hasCoupon) {
            System.out.println("Coupon discount applied.");
            System.out.println("Total discount: 25%");    // executes
        } else {
            System.out.println("Total discount: 15%");
        }
    } else {
        System.out.println("No membership discount.");

        if (hasCoupon) {
            System.out.println("Total discount: 10%");
        } else {
            System.out.println("No discount.");
        }
    }
} else {
    System.out.println("Must be 18 or over — no purchase.");
}

// ── All execution paths in the above: ────────────────────────────────
//
//  age >= 18 → member → hasCoupon → "25%"
//  age >= 18 → member → !hasCoupon → "15%"
//  age >= 18 → !member → hasCoupon → "10%"
//  age >= 18 → !member → !hasCoupon → "No discount"
//  age < 18"Must be 18"
//
//  3 binary conditions = 2³ = 8 possible combinations
//  (but age gate reduces effective paths to 5)

// ── The nesting problem — harder to read as depth grows: ─────────────
// Level 1: if (a) {
// Level 2:     if (b) {
// Level 3:         if (c) {
// Level 4:             if (d) {    ← very hard to reason about
// Level 5:                 if (e) { // ← refactor immediately
//                          }
//                      }
//                  }
//              }
//          }

The Dangling else Problem

When if statements are nested without braces, it is ambiguous which if an else belongs to. Java resolves this by matching each else to the nearest preceding unmatched if — but this can produce unexpected behaviour. Always use braces to make the association explicit and unambiguous.
Java
// ── Dangling else — ambiguous without braces: ────────────────────────
int x = 10;
int y = 5;

// Which if does the else belong to?
if (x > 5)
    if (y > 10)
        System.out.println("A");
    else
        System.out.println("B");    // belongs to INNER if (y > 10)
                                    // NOT the outer if (x > 5)

// Java rule: else matches the NEAREST unmatched if.
// Execution: x>5 true → y>10 false → print "B"

// ── Same code with misleading indentation: ────────────────────────────
if (x > 5)
    if (y > 10)
        System.out.println("A");
else                                // looks like it belongs to outer if
    System.out.println("B");        // but Java matches it to the inner if!

// ── ALWAYS use braces to make intent explicit: ────────────────────────

// Else belongs to inner if:
if (x > 5) {
    if (y > 10) {
        System.out.println("A");
    } else {
        System.out.println("B");    // clearly belongs to inner if
    }
}

// Else belongs to outer if:
if (x > 5) {
    if (y > 10) {
        System.out.println("A");
    }
} else {
    System.out.println("B");        // clearly belongs to outer if
}
// Execution: x>5 true → enter outer block → y>10 false → nothing prints
// (inner if false and inner else does not exist here)

Refactoring Nested if Statements

Deep nesting is a code smell. There are four standard techniques to flatten nested if structures: combining conditions with logical operators, using guard clauses with early returns, extracting nested logic into separate methods, and replacing multi-branch logic with switch or a data structure. Each technique trades nesting depth for readability.
Java
// ── Original deeply nested code: ─────────────────────────────────────
public String getShippingCost(Order order) {
    if (order != null) {
        if (order.getCustomer() != null) {
            if (order.getCustomer().isPremium()) {
                if (order.getTotalAmount() > 50) {
                    return "Free";
                } else {
                    return 2.99";
                }
            } else {
                if (order.getTotalAmount() > 100) {
                    return 2.99";
                } else {
                    return 5.99";
                }
            }
        } else {
            return "No customer";
        }
    } else {
        return "No order";
    }
}

// ── Technique 1: Guard clauses (early return for error cases): ─────────
public String getShippingCostV2(Order order) {
    if (order == null)                  return "No order";
    if (order.getCustomer() == null)    return "No customer";

    boolean isPremium   = order.getCustomer().isPremium();
    double  total       = order.getTotalAmount();

    if (isPremium && total > 50)   return "Free";
    if (isPremium)                 return 2.99";
    if (total > 100)               return 2.99";
    return 5.99";
}

// ── Technique 2: Combine with logical operators: ──────────────────────
public boolean canAccessResource(User user, Resource resource) {
    // Nested version:
    // if (user != null) {
    //     if (user.isActive()) {
    //         if (resource != null) {
    //             if (resource.isPublic() || user.hasPermission(resource)) {
    //                 return true;
    //             }
    //         }
    //     }
    // }
    // return false;

    // Flat version — one compound condition:
    return user != null
        && user.isActive()
        && resource != null
        && (resource.isPublic() || user.hasPermission(resource));
}

// ── Technique 3: Extract inner logic to a method: ─────────────────────
// Nested version:
public void processUser(User user) {
    if (user != null) {
        if (user.isActive()) {
            if (user.getRole() == Role.ADMIN) {
                sendAdminWelcome(user);
                grantAdminPermissions(user);
                logAdminLogin(user);
            } else {
                sendUserWelcome(user);
                grantUserPermissions(user);
            }
        }
    }
}

// Extracted version:
public void processUserClean(User user) {
    if (user == null || !user.isActive()) return;
    setupUserByRole(user);
}

private void setupUserByRole(User user) {
    if (user.getRole() == Role.ADMIN) {
        sendAdminWelcome(user);
        grantAdminPermissions(user);
        logAdminLogin(user);
    } else {
        sendUserWelcome(user);
        grantUserPermissions(user);
    }
}

Nested if Inside else — else-if vs True Nesting

A common pattern is placing an if inside an else block to create a chain of mutually exclusive conditions. Java's else-if syntax is actually syntactic sugar for this — an if nested inside an else, written without an extra level of indentation. Understanding this equivalence explains why else-if chains do not require extra braces around the nested if.
Java
// ── else-if is syntactically an if nested inside else: ───────────────

// Written as else-if (standard style — preferred):
int score = 75;
if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");        // executes
} else {
    System.out.println("F");
}

// Exactly equivalent with explicit braces around nested ifs:
if (score >= 90) {
    System.out.println("A");
} else {
    if (score >= 80) {
        System.out.println("B");
    } else {
        if (score >= 70) {
            System.out.println("C");    // executes — same result
        } else {
            System.out.println("F");
        }
    }
}

// ── When to use true nesting vs else-if: ─────────────────────────────
//
// Use else-if when: checking the same or related variable across conditions.
//   → grades, status codes, categories, ranges on one variable
//
// Use true nesting when: inner logic involves a completely different
//   category of check that only applies within the outer context.

// True nesting appropriate here — inner checks are categorically different:
public void handleUserRequest(User user, Request request) {
    if (user.isAuthenticated()) {
        // Different category: now checking request details
        if (request.isReadOnly()) {
            handleReadRequest(user, request);
        } else if (user.hasWritePermission()) {
            handleWriteRequest(user, request);
        } else {
            throw new AccessDeniedException("Write permission required");
        }
    } else {
        redirectToLogin(request);
    }
}

Related Topics in Control Statements

while Loop
The while loop is a condition-controlled loop — it continues executing as long as its boolean condition remains true, without specifying in advance how many iterations will occur. It is the natural choice for reading input until end-of-stream, polling until a condition changes, retrying an operation, or any scenario where the termination condition depends on data or events that cannot be predetermined. This entry covers syntax, execution flow, common patterns, infinite loops with controlled exit, and the differences between while and for.
do-while Loop
The do-while loop is Java's only post-test loop — the condition is evaluated after the body executes, which guarantees the body runs at least once regardless of the condition's initial value. This property makes do-while the natural fit for scenarios where the first execution must happen before the first check: menu-driven programs, input validation, digest computation, and game turns. This entry covers syntax, when to use do-while over while, input validation patterns, and the subtle differences that matter in practice.
Enhanced for Loop
The enhanced for loop — also called the for-each loop — was introduced in Java 5 to provide a clean, readable syntax for iterating over arrays and any class that implements Iterable. It eliminates index management and iterator boilerplate entirely. The developer declares what each element is and what to do with it, without concerning themselves with how the iteration mechanism works. This entry covers syntax, the Iterable contract, limitations compared to the indexed loop, modification restrictions, and how for-each maps to iterators under the hood.
break Statement
The break statement immediately terminates the nearest enclosing loop or switch block and transfers control to the first statement after that block. It is one of Java's three jump statements alongside continue and return. break is essential for exiting loops early when a search condition is met, preventing infinite loops from running forever, and stopping switch execution after a matched case.