☕ 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

Control Statements
Control statements determine the flow of execution in a Java program. Without them, code executes line by line from top to bottom. Control statements allow the program to make decisions, repeat actions, and jump to different parts of the code based on conditions. Java provides three categories: selection statements (if, if-else, switch), iteration statements (for, while, do-while), and jump statements (break, continue, return).
if Statement
The if statement is the most fundamental control flow construct in Java. It evaluates a boolean expression and executes a block of code only if the expression is true. If the condition is false the block is skipped entirely and execution continues with the next statement after the if block.
if-else Statement
The if-else statement extends the basic if by providing an alternative block that executes when the condition is false. It guarantees that exactly one of two blocks always executes — either the if block (condition true) or the else block (condition false). The if-else-if chain extends this to choose among more than two alternatives.
switch Statement
The switch statement selects one of several code blocks to execute based on the value of a single expression. It is a cleaner alternative to a long if-else-if chain when choosing among multiple constant values. Java switch works with byte, short, int, char, String, and enum types. Each case label must be a compile-time constant and the optional default case handles all unmatched values.