Saltar a contenido

2. Selección: if / else if / else

UD3 — Control de Flujo y Depuración · RA3 — Escribe y depura código, analizando y utilizando las estructuras de control del lenguaje

IES Thiar · Curso 2026 - 2027

Aspecto Valor
Resultado de aprendizaje RA: 3 — Escribe y depura código, analizando y utilizando las estructuras de control del lenguaje.
Criterios de evaluación a los que contribuye esta sección CE 3.1 (escribe y prueba código que hace uso de estructuras de selección)
CE 3.5 (crea programas ejecutables combinando selección con lectura de datos)
CE 3.7 (comenta y documenta el código).
Marco normativo RD 405/2023 · RD 450/2010 · RD 659/2023
Tecnología base Java 25 LTS (OpenJDK Temurin 25) — soporte hasta 2030+
Horas estimadas ~5 h (de las 26 h de la UD3).

Requisitos previos (lo que ya sabes): condiciones booleanas, operadores relacionales y lógicos, paréntesis, De Morgan, variables booleanas con nombre y diagramas de flujo (todo de la sección 1 de la UD3); tipos primitivos, var, String (con .equals() y .length()), text blocks y final para constantes (de UD1); entrada con java.lang.IO (IO.readln, IO.println) y java.util.Scanner (nextInt, nextDouble, nextLine), conversión con Integer.parseInt / Double.parseDouble (de UD1). El operador ternario es contenido nuevo de esta sección, no un repaso.


2.1 ¿De qué va esta sección? — Por fin escribimos if

Bienvenido a la sección donde por fin pasa lo que llevas esperando desde que terminaste la sección 1: vamos a escribir if en Java. Después de todo el discurso teórico sobre los tres pilares y los diagramas de flujo, toca vestirlos con sintaxis.

Te prometo algo: si la sección 1 te pareció "filosofía", esta sección te va a parecer "oficina". Aquí es donde el programa empieza a comportarse como una aplicación de verdad, a pedir datos, a comprobarlos y a decidir qué hacer en función de ellos. Vas a escribir código que reacciona, no que solo "hace".

Vamos a cubrir, en este orden:

  1. La sintaxis básica del if: la forma simple, la forma con else y la forma encadenada con else if.
  2. La anatomía detallada del if: la condición, el bloque, las llaves y por qué siempre las usamos.
  3. Los if anidados y la técnica del early return (cláusulas de guarda), que es lo que separa a un principiante de un profesional.
  4. El operador ternario (contenido nuevo): qué es, en su sitio natural, cuándo vale la pena usarlo y cuándo es mejor un if.
  5. Los problemas más habituales del if, incluido el famoso dangling else y el bug de Apple "goto fail".
  6. La validación de entrada con Scanner y if: cómo pedir datos al usuario sin que el programa se vuelva loco.
  7. Varios ejemplos resueltos paso a paso, porque la teoría sin práctica es como un mapa sin viaje.

Analogía del agente de la estación

Piensa en el if como el agente de la estación de tren que mira tu billete. Si el billete es de primera, te manda al andén 1. Si es de segunda, al andén 2. Si no tienes billete, te manda a taquilla. El agente no te manda a los tres sitios a la vez: examina una condición, decide una rama, y descarta el resto. Eso es exactamente lo que hace Java con if / else if / else.

Vamos a empezar por el principio. Respira hondo, abre IntelliJ, crea una clase PruebasIf en tu proyecto Maven, y acompáñame línea por línea.


2.2 La sintaxis básica del if

El if es la estructura de selección más utilizada en cualquier lenguaje. En Java tiene tres formas básicas: simple, con alternativa y encadenada. Vamos a verlas una a una, con su sintaxis exacta, su diagrama de flujo y un ejemplo real en cada caso.

2.2.1 Forma 1: if simple

La forma más simple del if ejecuta un bloque de código solo si la condición se evalúa a true. Si la condición es false, el bloque se salta y el programa continúa por donde iba.

Sintaxis:

1
2
3
4
if (condicion) {
    // Bloque que se ejecuta solo si 'condicion' es true
}
// Aquí el programa sigue, haya entrado o no al bloque

Ejemplo:

1
2
3
4
5
6
7
// Programa: avisa solo si la temperatura es peligrosa
int temperatura = 38;

if (temperatura > 37) {
    IO.println("Atención: podrías tener fiebre.");
}
IO.println("Programa terminado.");

Si temperatura vale 38, la salida es:

Atención: podrías tener fiebre.
Programa terminado.

Si temperatura vale 36, la salida es solo:

Programa terminado.

Fíjate en dos cosas clave:

  1. La condición va siempre entre paréntesis: if (temperatura > 37). Sin paréntesis no compila.
  2. El bloque va entre llaves { }. Aunque Java te permite omitirlas cuando el bloque tiene una sola sentencia, nosotros siempre las pondremos. Más adelante verás por qué.

Diagrama de flujo:

flowchart TD
    Inicio([Inicio]) --> Condicion{"temperatura > 37 ?"}
    Condicion -->|Sí| Aviso["IO.println 'Atención: fiebre'"]
    Condicion -->|No| Skip["(salta el bloque)"]
    Aviso --> Fin([Fin])
    Skip --> Fin

La rama "No" no hace nada: simplemente se salta el bloque y continúa. Esa es la gracia del if simple.

2.2.2 Forma 2: if con else

Cuando quieres hacer una cosa si la condición es cierta y otra distinta si es falsa, añades un else. Garantizado: una de las dos ramas se va a ejecutar, nunca las dos, nunca ninguna.

Sintaxis:

1
2
3
4
5
6
if (condicion) {
    // Bloque A: se ejecuta si 'condicion' es true
} else {
    // Bloque B: se ejecuta si 'condicion' es false
}
// Aquí el programa continúa

Ejemplo:

1
2
3
4
5
6
7
8
9
// Programa: di si una persona es mayor o menor de edad
int edad = 17;

if (edad >= 18) {
    IO.println("Eres mayor de edad.");
} else {
    IO.println("Eres menor de edad.");
}
IO.println("Programa terminado.");

Salida con edad = 17:

Eres menor de edad.
Programa terminado.

Diagrama de flujo:

flowchart TD
    Inicio([Inicio]) --> Condicion{"edad >= 18 ?"}
    Condicion -->|Sí| Mayor["IO.println 'Mayor de edad'"]
    Condicion -->|No| Menor["IO.println 'Menor de edad'"]
    Mayor --> Fin([Fin])
    Menor --> Fin

Ese rombo con dos salidas es la imagen mental que debes tener siempre cuando veas un if. Si dibujas el flujo en papel antes de programar, no te equivocarás nunca.

2.2.3 Forma 3: if encadenado con else if

Cuando tienes más de dos caminos posibles, encadenas con else if. Java evalúa las condiciones en orden, una detrás de otra, y ejecuta solo el primer bloque cuya condición sea true. Una vez entra en uno, ignora todos los demás. Si ninguna condición se cumple, ejecuta el else final (si lo hay).

Sintaxis:

1
2
3
4
5
6
7
8
9
if (condicion1) {
    // Se ejecuta si condicion1 es true
} else if (condicion2) {
    // Se ejecuta si condicion1 es false Y condicion2 es true
} else if (condicion3) {
    // Se ejecuta si condicion1 y condicion2 son false Y condicion3 es true
} else {
    // Se ejecuta si TODAS las anteriores son false (opcional)
}

Ejemplo: clasificar una nota académica

// Programa: clasifica una nota en Suspenso, Aprobado, Notable o Sobresaliente
int nota = 7;

if (nota < 0 || nota > 10) {
    IO.println("Nota no válida.");
} else if (nota < 5) {
    IO.println("Suspenso.");
} else if (nota < 7) {
    IO.println("Aprobado.");
} else if (nota < 9) {
    IO.println("Notable.");
} else {
    IO.println("Sobresaliente.");
}

Salida con nota = 7:

Notable.

Detalles que te van a ahorrar problemas:

  • Las condiciones se evalúan en orden. En cuanto una es true, se ejecuta su bloque y se saltan todas las demás. Por eso no hace falta escribir nota >= 5 && nota < 7 en la segunda rama: si llegamos hasta ahí, ya sabemos que nota < 5 era false, es decir, nota >= 5 ya es cierto.
  • El else final es opcional. Si no lo pones y ninguna condición se cumple, simplemente no se ejecuta ninguna rama.
  • Puedes poner tantos else if como quieras. Pero si llegas a seis o siete, probablemente lo que necesitas es un switch (lo verás en la sección 3).

Diagrama de flujo de la clasificación de nota:

flowchart TD
    Inicio([Inicio]) --> C0{"nota < 0 o nota > 10 ?"}
    C0 -->|Sí| Invalid["IO.println 'Nota no válida'"]
    C0 -->|No| C1{"nota < 5 ?"}
    C1 -->|Sí| Susp["IO.println 'Suspenso'"]
    C1 -->|No| C2{"nota < 7 ?"}
    C2 -->|Sí| Aprob["IO.println 'Aprobado'"]
    C2 -->|No| C3{"nota < 9 ?"}
    C3 -->|Sí| Not["IO.println 'Notable'"]
    C3 -->|No| Sobre["IO.println 'Sobresaliente'"]
    Invalid --> Fin([Fin])
    Susp --> Fin
    Aprob --> Fin
    Not --> Fin
    Sobre --> Fin

Fíjate en la forma de "escalera": cada condición es una nueva decisión que solo se evalúa si todas las anteriores han sido false. Esa es la imagen mental del else if.

2.2.4 Comparativa de las tres formas

Antes de seguir, te dejo una tabla que resume las tres formas. Si solo te aprendes una tabla, que sea esta:

Forma ¿Cuándo se usa? ¿Cuántas ramas? ¿Tiene else?
if simple Cuando una acción es opcional. 1 (la del if). No.
if con else Cuando hay exactamente dos caminos alternativos. 2 (una en if, otra en else). Sí, obligatorio.
if con else if y else final Cuando hay tres o más caminos mutuamente excluyentes. N + 1 (N else if más el else final opcional). Sí, opcional al final.

Regla mnemotécnica del profesor:

  • 1 camino posible: no hace falta if, es secuencia pura.
  • 1 camino opcional: if simple (sin else).
  • 2 caminos: if + else.
  • 3+ caminos: if + else if + ... + else final.

Cuando llegues a más de 5 ramas, párate y piensa si lo que necesitas es un switch (sección 3) o, directamente, una tabla de datos (UD6).


2.3 Anatomía detallada del if

Ahora que ya has visto las tres formas por encima, vamos a despiezar el if pieza por pieza. Entender cada parte por separado te va a permitir leer cualquier if ajeno y escribir los tuyos sin errores.

2.3.1 La condición: siempre entre paréntesis, siempre booleana

La condición es la expresión que va entre los paréntesis, justo después de la palabra if. Java exige dos cosas:

  1. Tiene que ir entre paréntesis. Sin excepciones. if edad >= 18 no compila.
  2. Tiene que evaluarse a boolean. Cualquier expresión que devuelva true o false vale; cualquier otra cosa, no.

Ejemplos válidos:

1
2
3
4
5
6
7
if (edad >= 18) { /* ... */ }                       // comparación simple
if (edad >= 18 && tieneDNI) { /* ... */ }           // AND
if (edad >= 18 || esTutor) { /* ... */ }            // OR
if (!esAdmin) { /* ... */ }                         // NOT
if (nombre.equals("admin")) { /* ... */ }           // método que devuelve boolean
if (esValido) { /* ... */ }                         // variable booleana
if ((nota >= 0) && (nota <= 10)) { /* ... */ }      // con paréntesis extra (mejor)

Ejemplos que no compilan:

1
2
3
4
if edad >= 18 { /* ... */ }              // falta paréntesis
if (edad) { /* ... */ }                 // edad es int, no boolean
if (nombre) { /* ... */ }               // nombre es String, no boolean
if (edad = 18) { /* ... */ }            // = es asignación, devuelve int, no boolean

Comparar boolean con == true es redundante

Si esValido ya es boolean, escribir if (esValido == true) es redundante y feo. Lo correcto es if (esValido). Igualmente, en vez de if (esValido == false), escribe if (!esValido). Es más limpio y se lee como prosa.

2.3.2 El bloque: entre llaves

Después de la condición viene el bloque que se ejecuta si la condición es true. Java admite dos variantes:

Variante 1: con llaves (la que usaremos siempre)

1
2
3
4
if (edad >= 18) {
    IO.println("Eres mayor de edad.");
    IO.println("Puedes entrar.");
}

Variante 2: sin llaves (legal, pero evítala)

if (edad >= 18)
    IO.println("Eres mayor de edad.");

La variante sin llaves solo permite una sentencia. Si más tarde añades una segunda sentencia, la indentación te engaña y se rompe:

1
2
3
4
// BUG clásico: la segunda línea SIEMPRE se ejecuta, sin importar la condición
if (edad >= 18)
    IO.println("Eres mayor de edad.");
    IO.println("Puedes entrar.");   // esta línea está FUERA del if

Si edad = 16, la salida sería:

Puedes entrar.

¿Raro, no? Pues es exactamente lo que hace Java: solo la primera sentencia pertenece al if. La segunda se ejecuta siempre. Para evitar este bug, siempre llaves, aunque el bloque tenga una sola línea.

2.3.3 La regla de las llaves obligatorias

Esta es una de las pocas reglas absolutas que adoptaremos en el curso:

REGLA: En este curso, todos los bloques de if, else if y else van entre llaves, aunque tengan una sola sentencia. Sin excepciones.

¿Por qué tan tajantes? Porque el famoso bug "goto fail" de Apple en 2014, que dejó vulnerable el SSL/TLS de iOS y macOS durante meses, fue causado exactamente por esto: una segunda línea dentro de un if sin llaves que se ejecutaba siempre.

1
2
3
4
// Código real (simplificado) del bug de Apple
if ((err = SSLHashSHA1.update(&hashCtx, &signedCert)) != 0)
    goto fail;
    goto fail;   // <- esta línea se ejecuta SIEMPRE, salta incondicionalmente

Ese segundo goto fail estaba indentado como si estuviera dentro del if, pero no lo estaba, porque el if no tenía llaves. El compilador no se quejó, los tests no lo detectaron, y millones de dispositivos estuvieron meses con el handshake SSL roto. Una línea sin llaves → vulnerabilidad de seguridad en producción.

La regla no es caprichosa: es experiencia amarga del mundo real. Acostúmbrate a poner llaves siempre y nunca tendrás ese problema.

2.3.4 El else y el else if pertenecen al mismo "if statement"

Una cosa que confunde al principio: aunque veas if / else if / else en líneas separadas, son parte de la misma estructura. No son tres if distintos, son una sola sentencia de control. Esto importa por dos razones:

  1. Indentación correcta: el else if y el else se alinean con el if, no se indentan.
  2. Semántica: el else solo se evalúa si la condición del if fue false. No se ejecutan ambos.
// BIEN indentado: else if y else alineados con if
if (nota < 5) {
    IO.println("Suspenso");
} else if (nota < 7) {
    IO.println("Aprobado");
} else {
    IO.println("Notable o superior");
}

// MAL indentado: parece que son tres if distintos
if (nota < 5) {
    IO.println("Suspenso");
}
    else if (nota < 7) {
        IO.println("Aprobado");
    }
    else {
        IO.println("Notable o superior");
    }

IntelliJ te avisa del mal indentado con una línea roja. Acepta siempre su sugerencia de "Reformat code" (Ctrl+Alt+L en Windows/Linux, Cmd+Alt+L en Mac).

2.3.5 Anatomía visual

Para que no se te olvide, aquí tienes la "radiografía" de un if completo:

flowchart TD
    A["if (condicion1) { ... }"] --> B["¿condicion1 true?"]
    B -->|Sí| R1["Bloque 1"]
    B -->|No| C["else if (condicion2) { ... }"]
    C --> D["¿condicion2 true?"]
    D -->|Sí| R2["Bloque 2"]
    D -->|No| E["else { ... }"]
    E --> R3["Bloque else"]
    R1 --> Fin(["Continúa el programa"])
    R2 --> Fin
    R3 --> Fin

A partir de aquí, todos los if que veas en cualquier libro o proyecto los vas a leer como un rombo con flechas. Ese es el nivel de abstracción que queremos que consigas.


2.4 if anidados y la técnica del early return

2.4.1 ¿Qué es anidar un if?

Anidar un if es meter un if dentro del bloque de otro if. Es perfectamente legal y, a veces, necesario. Por ejemplo, para validar que la edad está en rango y luego comprobar si tiene carnet:

1
2
3
4
5
6
7
8
9
if (edad >= 18) {
    if (tieneCarnet) {
        IO.println("Puedes conducir.");
    } else {
        IO.println("Eres mayor de edad, pero no tienes carnet.");
    }
} else {
    IO.println("Eres menor de edad.");
}

Aquí el if (tieneCarnet) solo se evalúa si edad >= 18 ya es true. Es una estructura muy común: una validación dentro de otra.

2.4.2 El problema: la pirámide de la muerte

Anidar if dentro de if dentro de if produce lo que en la industria llamamos la pirámide de la muerte (o arrow anti-pattern, porque el código tiene forma de flecha apuntando a la derecha). Y es tan feo como suena:

// NO HAGAS ESTO EN CASA
if (datosValidos) {
    if (usuarioExiste) {
        if (tienePermisos) {
            if (!estaSancionado) {
                if (saldoSuficiente) {
                    IO.println("Operación permitida.");
                } else {
                    IO.println("Saldo insuficiente.");
                }
            } else {
                IO.println("Usuario sancionado.");
            }
        } else {
            IO.println("Sin permisos.");
        }
    } else {
        IO.println("Usuario no existe.");
    }
} else {
    IO.println("Datos no válidos.");
}

Ese código es ilegible. Cuando intentes depurarlo, te vas a perder. Cuando llegue otro programador a tocarlo, te va a maldecir. Y, sin embargo, la lógica que expresa es razonable: validar varias cosas antes de hacer una operación. El problema no es la lógica, es la forma.

2.4.3 La técnica del early return (cláusulas de guarda)

La solución profesional a la pirámide de la muerte se llama early return ("retorno anticipado") o cláusulas de guarda ("guard clauses"). La idea es invertir las condiciones y salir cuanto antes.

Aviso pedagógico: vas a usar return antes de estudiarlo a fondo

En el código de abajo aparece la palabra clave return, que todavía no hemos estudiado formalmente. No es un error ni una contradicción: es un adelanto deliberado, porque la técnica de early return es tan útil que merece la pena conocerla ya, aunque sea con una explicación mínima apoyada en lo que ya sabes.

Lo que necesitas por ahora:

  • Desde UD1 sabes que todo programa Java necesita un método main: es el único método que has escrito hasta ahora, y es el que la JVM ejecuta al arrancar el programa.

  • Lo único nuevo aquí es que return; hace que el método actual termine inmediatamente en ese punto. Como main es el único método que existe en tus programas por ahora, return; dentro de main equivale a "el programa termina aquí".

return se estudiará formalmente como sentencia de salto en la sección 7 de esta misma unidad (junto a break y continue), y en profundidad —con métodos propios, parámetros y tipos de retorno— en UD4. Hasta entonces, con la idea de "termina el método actual (por ahora, siempre main) en este punto" te basta para aplicar correctamente esta técnica.

// SÍ HAZ ESTO EN CASA: cláusulas de guarda
if (!datosValidos) {
    IO.println("Datos no válidos.");
    return;   // sale del método (aquí, de 'main'), no sigue
}
if (!usuarioExiste) {
    IO.println("Usuario no existe.");
    return;
}
if (!tienePermisos) {
    IO.println("Sin permisos.");
    return;
}
if (estaSancionado) {
    IO.println("Usuario sancionado.");
    return;
}
if (!saldoSuficiente) {
    IO.println("Saldo insuficiente.");
    return;
}
IO.println("Operación permitida.");

¿Ves la diferencia? En vez de ir anidando if para comprobar lo positivo, comprobamos lo negativo y salimos inmediatamente si no se cumple. El código queda plano, sin indentación creciente, y se lee como una lista de comprobaciones: "si esto falla, salgo; si no, sigo".

2.4.4 ¿Por qué funciona el early return?

El early return aprovecha la característica de return que acabas de ver: abandona el método inmediatamente (en nuestros programas, main), devolviendo el control a quien lo invocó —en el caso de main, a la JVM—. Volverás sobre esto con más detalle en la sección 7 (return como sentencia de salto) y en UD4 (métodos propios y su valor de retorno). Por eso, después de cada if negativo con return, no necesitas else: si llegamos a la siguiente línea, es porque la condición anterior no se cumplió.

Mentalmente se lee así:

"Si los datos no son válidos, error y me voy. Si el usuario no existe, error y me voy. Si no tiene permisos, error y me voy. Si está sancionado, error y me voy. Si no tiene saldo, error y me voy. Si he llegado hasta aquí, todo ha ido bien y hago la operación."

Es exactamente como piensa un cocinero cuando revisa la nevera antes de empezar a cocinar: si falta un ingrediente clave, cierra la nevera y no sigue.

2.4.5 Cuándo anidar y cuándo usar early return

Situación Recomendación
Validaciones previas a una operación (caso de error → salir). Early return.
Lógica de negocio con varias ramas mutuamente excluyentes (suspenso/aprobado/notable). else if (sin anidar).
Comprobaciones que dependen una de otra (solo me importa tieneCarnet si ya es mayor de edad). if anidado, máximo 2 niveles.
Más de 3 niveles de anidamiento. Refactoriza: extrae métodos o usa early return.

Regla del profesor:

Si tu if anidado llega a tres niveles de indentación, paras y reescribes. Tres niveles es el límite donde el cerebro humano empieza a perderse.

2.4.6 Ejemplo guiado: refactorización paso a paso

Vamos a transformar un código con pirámide en uno con early return. Partimos de:

// Código con pirámide (malo)
if (edad >= 18) {
    if (tieneCarnet) {
        if (!estaSancionado) {
            IO.println("Puedes alquilar un coche.");
        } else {
            IO.println("Estás sancionado.");
        }
    } else {
        IO.println("No tienes carnet.");
    }
} else {
    IO.println("Eres menor de edad.");
}

Paso 1: invertir las condiciones de los else (lo que era negativo pasa a positivo, y viceversa):

// Paso 1: invertir condiciones
if (edad < 18) {
    IO.println("Eres menor de edad.");
} else {
    if (!tieneCarnet) {
        IO.println("No tienes carnet.");
    } else {
        if (estaSancionado) {
            IO.println("Estás sancionado.");
        } else {
            IO.println("Puedes alquilar un coche.");
        }
    }
}

Paso 2: quitar los else porque el return ya ha salido (recuerda: por ahora usamos return dentro de main para terminar el programa; lo verás formalmente en la sección 7 de esta unidad y en profundidad en UD4):

// Paso 2: early return con cláusulas de guarda
if (edad < 18) {
    IO.println("Eres menor de edad.");
    return;
}
if (!tieneCarnet) {
    IO.println("No tienes carnet.");
    return;
}
if (estaSancionado) {
    IO.println("Estás sancionado.");
    return;
}
IO.println("Puedes alquilar un coche.");

Fíjate: la indentación ya no crece. Cada condición está alineada a la izquierda. Se lee de arriba a abajo como una lista. La legibilidad mejora radicalmente.

El return en main

En Java 25 puedes usar return; dentro de main para salir del programa (en void main() no devuelve nada, simplemente termina). Basta con que sepas que return; abandona el método actual; como main es, de momento, el único método que escribes, eso equivale a terminar el programa. Verás return formalmente como sentencia de salto en la sección 7 de esta unidad, y en profundidad —con métodos propios— en UD4.


2.5 El operador ternario en su sitio: cuándo sí y cuándo no

2.5.1 Una herramienta nueva: el operador ternario ? :

Hasta ahora en esta unidad has resuelto todas las decisiones con if / else if / else. Vamos a añadir una herramienta más a tu caja: el operador ternario ? :.

Lo que lo hace distinto de todo lo visto hasta ahora es que es una expresión (produce un valor que puedes asignar o pasar como argumento), no una sentencia (que ejecuta instrucciones). Su sintaxis:

Tipo resultado = condicion ? valorSiTrue : valorSiFalse;

Se lee así: "si condicion es true, el resultado es valorSiTrue; si es false, el resultado es valorSiFalse". Fíjate en el paralelismo con lo que ya conoces (dos fragmentos independientes, cada uno resuelve el mismo problema a su manera):

1
2
3
4
5
6
7
// Con if/else (lo que ya sabes hacer)
String mensajeIfElse;
if (edad >= 18) {
    mensajeIfElse = "Mayor de edad";
} else {
    mensajeIfElse = "Menor de edad";
}
// Con operador ternario (la novedad de esta sección): el mismo resultado en una sola línea
String mensajeTernario = (edad >= 18) ? "Mayor de edad" : "Menor de edad";

Ambos fragmentos hacen exactamente lo mismo: una línea en vez de cinco. El operador ternario no es una estructura de control distinta del if/else: es una forma más compacta de expresar un caso particular muy habitual, el de dos ramas que solo producen un valor cada una.

2.5.2 El operador ternario es if/else que produce un valor

La diferencia fundamental con if/else es:

Característica if / else Operador ternario ?:
¿Es una sentencia o una expresión? Sentencia: ejecuta instrucciones. Expresión: produce un valor.
¿Puede contener varias instrucciones? Sí. No: solo una expresión por rama.
¿Se puede asignar a una variable? No directamente (hay que asignar dentro de cada rama). Sí: var x = cond ? a : b;.
¿Puede ir como argumento a un método? No. Sí: IO.println(cond ? "Sí" : "No");.
¿Admite else if encadenados? Sí. Solo dos ramas (true/false).

La clave

El ternario es para elegir entre dos valores, no para ejecutar bloques de código.

2.5.3 Cuándo usar el operador ternario (casos buenos)

El operador ternario es excelente cuando:

  1. Quieres asignar una variable en función de una condición simple.
  2. Tienes dos valores claramente simétricos (uno para true, otro para false).
  3. La condición es legible sin paréntesis extraños.

Ejemplos recomendados:

1
2
3
4
5
6
7
8
// Caso 1: asignación simple
double descuento = (esVip) ? 0.20 : 0.05;       // 20 % si VIP, 5 % si no
String estado = (nota >= 5) ? "Aprobado" : "Suspenso";
int tarifa = (esFinDeSemana) ? 30 : 20;          // euros

// Caso 2: como argumento directo en una llamada
IO.println("Tu categoría es: " + (esAdmin ? "administrador" : "usuario"));
IO.println("Quedan " + (cantidad == 1 ? "1 unidad" : cantidad + " unidades"));

Estos casos se leen mejor con ternario que con if/else, porque ahorran cuatro líneas de boilerplate.

2.5.4 Cuándo NO usar el operador ternario (casos malos)

El operador ternario es horrible cuando:

  1. La condición es muy larga.
  2. Anidas ternarios dentro de ternarios.
  3. Tienes efectos secundarios (llamadas a métodos que cambian estado).
  4. La lógica no es simétrica (un valor es muy distinto del otro).

Ejemplos a evitar:

// MAL: ternario anidado. Ilegible.
String categoria = (edad < 18) ? "menor" : (edad < 65) ? "adulto" : "jubilado";

// MEJOR con if/else if
String categoria;
if (edad < 18) {
    categoria = "menor";
} else if (edad < 65) {
    categoria = "adulto";
} else {
    categoria = "jubilado";
}
1
2
3
4
5
6
// MAL: condición larguísima
double tarifa = (esVip && antiguedad > 5 && !estaSancionado && saldo > 1000) ? 0.30 : 0.05;

// MEJOR con variable booleana
boolean aplicaTarifaPreferente = esVip && antiguedad > 5 && !estaSancionado && saldo > 1000;
double tarifa = aplicaTarifaPreferente ? 0.30 : 0.05;
// PÉSIMO: efectos secundarios en un ternario
String r = (esValido) ? procesarDatos() : mostrarError();
// ¿Cuál de los dos métodos se ejecuta? Solo uno, pero es muy poco legible.

// MUCHO MEJOR con if/else
String r;
if (esValido) {
    r = procesarDatos();
} else {
    r = mostrarError();
}

2.5.5 Ternario en return: una de sus mejores virtudes

Donde el operador ternario brilla especialmente es en los return (cuando veas métodos en UD4). Permite devolver un valor u otro en una sola línea:

1
2
3
4
5
// Imagina un método (lo verás en UD4)
double calcularPrecio(int cantidad) {
    double precioBase = 10.0;
    return (cantidad >= 100) ? precioBase * 0.8 : precioBase;   // 20 % descuento si ≥ 100
}

Ese return con ternario es elegante y directo. Su equivalente con if/else ocupa cuatro líneas más:

double calcularPrecio(int cantidad) {
    double precioBase = 10.0;
    double precioFinal;
    if (cantidad >= 100) {
        precioFinal = precioBase * 0.8;
    } else {
        precioFinal = precioBase;
    }
    return precioFinal;
}

2.5.6 Ternario vs if/else: criterios de elección

Esta tabla te decide en segundos:

Si tu caso es... Usa...
Asignación simple entre dos valores simétricos. Operador ternario.
Asignación como argumento a una llamada (IO.println(...)). Operador ternario.
return con dos valores. Operador ternario.
Más de dos ramas posibles. if / else if / else.
Necesitas ejecutar varias sentencias en cada rama. if / else.
Tienes efectos secundarios (llamadas a métodos con impacto). if / else.
La condición es larga o compleja. if / else con variable booleana intermedia.

Regla del profesor:

Si dudas, usa if/else. El ternario es un lujo cuando encaja; el if/else siempre funciona. No te obligues a usar ternarios para "ahorrar líneas": a veces, cuatro líneas claras son mejores que una línea densa.


2.6 Errores más comunes del if

Esta sección es oro puro: son los bugs que verás una y otra vez, los que persiguen a los programadores junior durante años. Conocerlos antes de escribirlos te va a ahorrar horas de depuración.

2.6.1 Error 1: = vs ==

El error más antiguo del mundo. = es asignación. == es comparación.

1
2
3
4
5
6
7
8
9
// BUG: asignación en vez de comparación
if (edad = 18) {            // NO compila en Java porque = no devuelve boolean
    IO.println("Tienes 18.");
}

// CORRECTO: comparación
if (edad == 18) {
    IO.println("Tienes 18.");
}

Por qué Java es más amable que C: en C, edad = 18 asigna 18 a edad y devuelve 18, que se interpreta como true (cualquier valor distinto de 0 es verdadero). El if entra siempre, y el bug es silencioso. En Java, el compilador te salva: como la asignación devuelve int y la condición del if debe ser boolean, da error de compilación. Es una de las grandes virtudes de Java frente a C/C++.

Aun así, el error mental persiste: cuando escribas rápido, vas a escribir = en vez de ==. IntelliJ lo marca con un subrayado rojo y te ofrece "Convertir a ==". Acepta siempre.

2.6.2 Error 2: bloques sin llaves (bug de Apple)

Usar llaves siempre evita errores inesperados.

// BUG: la segunda línea se ejecuta SIEMPRE
if (edad >= 18)
    IO.println("Eres mayor de edad.");
    IO.println("Puedes entrar.");   // fuera del if

// CORRECTO: con llaves, todo el bloque pertenece al if
if (edad >= 18) {
    IO.println("Eres mayor de edad.");
    IO.println("Puedes entrar.");
}

Sin llaves, solo la primera sentencia pertenece al if. La indentación visual te engaña. La solución, como ya sabes: siempre llaves.

2.6.3 Error 3: el dangling else

El dangling else ("else colgando") es una trampa clásica de anidamiento. Java (como C y la mayoría de lenguajes) asocia cada else con el if más cercano sin else. Mira este código:

1
2
3
4
5
6
7
8
int x = 5;
int y = 0;

if (x > 0)
    if (y > 0)
        IO.println("Ambos positivos.");
else
    IO.println("x no es positivo.");

¿Qué crees que pasa si x = 5 e y = 0? A simple vista parece que "x no es positivo" se ejecutaría cuando x <= 0. Pero no: el else se asocia con el if (y > 0) más cercano, no con el if (x > 0). Entonces, con x = 5 e y = 0:

  • if (x > 0) → true, entra.
  • if (y > 0) → false, no imprime "Ambos positivos".
  • El else se asocia al if (y > 0), así que imprime "x no es positivo" — pero x sí es positivo. Bug.

La solución, otra vez: llaves. Con llaves, el else queda inequívocamente asociado al if que tú quieras:

1
2
3
4
5
6
7
8
// CORRECTO: else asociado al if externo
if (x > 0) {
    if (y > 0) {
        IO.println("Ambos positivos.");
    }
} else {
    IO.println("x no es positivo.");
}

Con esta versión, si x = 5 e y = 0, no se imprime nada (correcto). Si x = -1, se imprime "x no es positivo" (correcto).

Regla: cuando hay if anidados, siempre llaves y siempre claro con qué if se asocia cada else. Si tienes que pensar más de dos segundos para saber qué else va con qué if, el código está mal escrito.

2.6.4 Error 4: comparar String con ==

Lo vimos en UD1 y en la sección 1, pero se repite porque es el bug de Java más común entre principiantes:

// BUG: compara identidad, no contenido
String rol = IO.readln();
if (rol == "admin") {                       // casi nunca entra
    IO.println("Acceso total.");
}

// CORRECTO: .equals() compara contenido
if (rol.equals("admin")) {
    IO.println("Acceso total.");
}

// MEJOR: evita NullPointerException si rol es null
if ("admin".equals(rol)) {                  // el literal nunca es null
    IO.println("Acceso total.");
}

La versión ("admin".equals(rol)) pone el literal primero: si rol es null, no hay NullPointerException. Es una buena práctica defensiva.

Para ignorar mayúsculas/minúsculas, usa .equalsIgnoreCase():

1
2
3
4
// Acepta "admin", "ADMIN", "Admin", "AdMiN"...
if ("admin".equalsIgnoreCase(rol)) {
    IO.println("Acceso total.");
}

2.6.5 Error 5: el if no es un switch

Si te encuentras escribiendo algo como:

// MAL: comparar el mismo valor contra muchos literales
if (dia == 1) {
    IO.println("Lunes");
} else if (dia == 2) {
    IO.println("Martes");
} else if (dia == 3) {
    IO.println("Miércoles");
} else if (dia == 4) {
    IO.println("Jueves");
} else if (dia == 5) {
    IO.println("Viernes");
} else if (dia == 6) {
    IO.println("Sábado");
} else if (dia == 7) {
    IO.println("Domingo");
} else {
    IO.println("Día no válido.");
}

Funciona, pero es ineficiente y verboso. Cuando veas muchos else if comparando el mismo valor contra literales, lo que necesitas es un switch. Lo verás en la sección 3 y verás que en Java 25 es mucho más expresivo.

2.6.6 Error 6: magic numbers en condiciones

Ya lo vimos en la sección 1: un magic number es un número literal sin contexto. El código compila igual, pero es ilegible:

// MAL: 18, 65, 0, 120... ¿qué son?
if (edad >= 18 && edad <= 65) { /* trabajar */ }
if (edad < 0 || edad > 120) { /* error */ }

// BIEN: constantes con nombre
final int EDAD_MINIMA_TRABAJAR = 18;
final int EDAD_MAXIMA_TRABAJAR = 65;
final int EDAD_MINIMA_VIDA = 0;
final int EDAD_MAXIMA_VIDA = 120;

if (edad >= EDAD_MINIMA_TRABAJAR && edad <= EDAD_MAXIMA_TRABAJAR) { /* trabajar */ }
if (edad < EDAD_MINIMA_VIDA || edad > EDAD_MAXIMA_VIDA) { /* error */ }

Ahora, cuando dentro de tres meses abras el código, sabrás qué significa cada número. Y si el gobierno sube la edad laboral a 16, solo cambias la constante en un sitio.

2.6.7 Error 7: condiciones negadas (doble negación)

1
2
3
4
5
6
7
8
9
// PÉSIMO: doble negación
if (!(!estaListo || hayError)) {
    IO.println("Todo OK");
}

// MEJOR: aplicar De Morgan y leer en positivo
if (estaListo && !hayError) {
    IO.println("Todo OK");
}

La regla: prefiere condiciones positivas. Si tu condición empieza con !, plantéate si puedes escribirla en positivo. if (!esValido) suele ser mejor como if (esInvalido) (con la condición esInvalido definida con claridad).

2.6.8 Resumen de errores

Error Síntoma Solución
= vs == Error de compilación "incompatible types". Usar == para comparar.
Bloque sin llaves Línea "extra" que se ejecuta siempre. Siempre llaves.
Dangling else El else se asocia al if equivocado. Llaves en todos los if anidados.
== en String El if no entra aunque el contenido sea igual. Usar .equals() o .equalsIgnoreCase().
if en escalera con un mismo valor Código verboso y repetitivo. Cambiar a switch (sección 3).
Magic numbers El código se lee como un acertijo numérico. Definir constantes final int.
Doble negación Condición ilegible con dos !. Aplicar De Morgan o pensar en positivo.

2.7 Validación de entrada con IO.readln y if

Esta es la sección más práctica de todas: vas a aprender a pedir datos al usuario de forma robusta, de modo que tu programa no se vuelva loco cuando el usuario escriba algo distinto a lo esperado. En la sección 1 dibujamos el patrón; aquí lo vamos a programar.

2.7.1 Recordar java.lang.IO (de UD1)

En UD1 conociste la clase java.lang.IO, introducida en Java 25 mediante el JEP 512. Es una API moderna y ligera para entrada/salida por consola que viene incluida en el paquete java.lang y, por tanto, no necesita import, se puede usar directamente. Sus cuatro métodos básicos:

Método Qué hace Devuelve
IO.println(texto) Imprime texto y salto de línea. void
IO.print(texto) Imprime texto sin salto de línea. void
IO.readln() Lee una línea completa de teclado (sin el \n final). String
IO.readln(prompt) Imprime prompt y luego lee una línea. String

La gran ventaja de IO.readln() frente a Scanner.nextInt() o Scanner.nextDouble() es que siempre devuelve un String limpio: no hay buffer residual, no hay InputMismatchException sorpresivo, no hay que acordarse de "limpiar el salto de línea". Lo que escribió el usuario es exactamente lo que recibes. Tú decides cómo lo conviertes.

La contrapartida, como verás, es que tú eres responsable de la conversión: si pides un int, llamas a Integer.parseInt(IO.readln()). Y si la conversión falla, el programa lanza NumberFormatException. Como todavía no sabemos manejar excepciones (sección 8), vamos a suponer que las entradas son correctas.

En toda esta sección usaremos exclusivamente IO.readln(). Aún existen ejemplos en Internet y libros con Scanner, así que te conviene reconocer ambas formas; pero en tu código, en este curso, la entrada de momento será con IO.readln().

2.7.2 Validación de rango: edad entre 0 y 120

El patrón más común: pedir un número y comprobar que está en un rango válido.

IO.print("¿Qué edad tienes? ");
String entrada = IO.readln();
int edad = Integer.parseInt(entrada);

if (edad < 0 || edad > 120) {
    IO.println("Edad no válida. Debe estar entre 0 y 120.");
} else if (edad < 18) {
    IO.println("Eres menor de edad.");
} else if (edad < 65) {
    IO.println("Eres adulto en edad laboral.");
} else {
    IO.println("Eres jubilado.");
}

Aquí combinamos:

  1. Una lectura de un String con IO.readln().
  2. Una conversión a int con Integer.parseInt(...).
  3. Una validación de rango (edad entre 0 y 120).
  4. Una clasificación por rangos (menor, adulto, jubilado).

Fíjate que el orden importa: primero descartamos los valores imposibles, luego clasificamos. Si invertimos el orden y ponemos if (edad < 18) primero, ¿qué pasa con edad = -5? Que entraría en "menor de edad", lo cual es absurdo para una edad negativa. Primero valida, luego clasifica.

Si el usuario escribe 'abc', el programa se cae

En este ejemplo, si el usuario escribe "abc", Integer.parseInt("abc") lanza NumberFormatException y el programa termina con error. Hasta la sección 8 (excepciones con try-catch) no sabremos evitarlo. Mientras tanto, en los ejemplos básicos asumiremos que el usuario escribe cosas razonables.

2.7.3 Validación de menú: opción entre 1 y 4

Otro patrón clásico: menú de opciones.

IO.println("--- Calculadora básica ---");
IO.println("1. Sumar");
IO.println("2. Restar");
IO.println("3. Multiplicar");
IO.println("4. Dividir");
IO.print("Elige una opción (1-4): ");

int opcion = Integer.parseInt(IO.readln());

if (opcion < 1 || opcion > 4) {
    IO.println("Opción no válida. Tienes que elegir entre 1 y 4.");
} else {
    IO.print("Primer operando: ");
    double a = Double.parseDouble(IO.readln().replace(',', '.'));
    IO.print("Segundo operando: ");
    double b = Double.parseDouble(IO.readln().replace(',', '.'));

    if (opcion == 1) {
        IO.println("Resultado: " + (a + b));
    } else if (opcion == 2) {
        IO.println("Resultado: " + (a - b));
    } else if (opcion == 3) {
        IO.println("Resultado: " + (a * b));
    } else if (opcion == 4) {
        // Aquí falta comprobar división por cero. Lo veremos en sección 8 (excepciones),
        // pero por ahora podemos evitarlo con un if.
        if (b == 0) {
            IO.println("Error: no se puede dividir por cero.");
        } else {
            IO.println("Resultado: " + (a / b));
        }
    }
}

Observa:

  • Validamos la opción al principio. Si no es válida, no pedimos operandos. No tiene sentido pedir operandos si el usuario eligió una opción inexistente.
  • Para los operandos decimales, usamos IO.readln().replace(',', '.'): leemos y, si el usuario ha escrito coma como separador decimal, se la cambiamos por punto antes de pasársela a Double.parseDouble. Es una de las grandes ventajas de leer como String: podemos normalizar la entrada a nuestro antojo. Lo verás en detalle en 2.7.5.
  • Cuidado con la división por cero. Java no lanza error de compilación; lanza ArithmeticException (para enteros) o devuelve Infinity (para dobles). Con un if (b == 0) lo evitamos.

2.7.4 Validación de texto: "Sí" / "No"

IO.print("¿Estás seguro? (s/n): ");
String respuesta = IO.readln().trim();

if (respuesta.equalsIgnoreCase("s") || respuesta.equalsIgnoreCase("si") || respuesta.equalsIgnoreCase("sí")) {
    IO.println("Operación confirmada.");
} else if (respuesta.equalsIgnoreCase("n") || respuesta.equalsIgnoreCase("no")) {
    IO.println("Operación cancelada.");
} else {
    IO.println("Respuesta no válida. Escribe 's' o 'n'.");
}

Detalles a observar:

  1. IO.readln().trim(): lee toda la línea y quita espacios en blanco por los lados. Si el usuario escribe " sí ", se queda "sí". Con IO.readln() no hay buffer residual: lo que se lee es exactamente la línea completa, sin sorpresas.
  2. equalsIgnoreCase en vez de equals: acepta "S", "s", "SÍ", "sí", "Si", "sI". Más amable con el usuario.
  3. Cubrimos variantes: "s" y "sí" como sinónimos; "n" y "no" como sinónimos. Si el usuario escribe otra cosa, le damos un mensaje claro.
  4. else final para el caso de error: en validación, siempre tiene que haber un "camino" para cuando el dato no encaje.

Este patrón —IO.readln() seguido de .trim() y .equalsIgnoreCase()— es tan limpio que vas a usarlo en casi todos los programas que pidan confirmación al usuario. Acostúmbrate a escribirlo de carrerilla.

2.7.5 La trampa del separador decimal

En España, el separador decimal es la coma (,). En Java, el separador decimal siempre es el punto (.). Y eso da lugar a una trampa:

1
2
3
IO.print("Introduce tu altura en metros: ");
double altura = Double.parseDouble(IO.readln().trim());
IO.println("Tu altura es: " + altura);

Si el usuario escribe 1,75 (con coma, como le han enseñado toda la vida), Double.parseDouble("1,75") lanza NumberFormatException y el programa se cae. Tú sabes que es un decimal, pero Java no.

La solución es normalizar la entrada antes de parsear: cambiar la coma por punto con replace(',', '.'):

1
2
3
4
IO.print("Introduce tu altura en metros (puedes usar coma o punto): ");
String entrada = IO.readln().trim().replace(',', '.');
double altura = Double.parseDouble(entrada);
IO.println("Tu altura es: " + altura);

Ahora el usuario puede escribir 1,75, 1.75, 1.75 o 1,7500: el .trim() quita los espacios y el .replace(',', '.') convierte la coma en punto. Double.parseDouble recibe siempre un texto con punto y funciona.

Patrón recomendado para leer decimales:

// Plantilla estándar para leer un double del usuario
double valor = Double.parseDouble(IO.readln().trim().replace(',', '.'));

Memoriza esta línea: la vas a usar en cada programa que pida un precio, una altura, un peso o cualquier decimal.

2.7.6 Ejemplo completo: pedir y validar edad

Vamos a juntar todo lo aprendido en un ejemplo completo: pedir la edad al usuario, validarla y mostrar un mensaje según el rango.

public class ValidarEdad {

    public static void main(String[] args) {

        final int EDAD_MINIMA = 0;
        final int EDAD_MAXIMA = 120;
        final int MAYORIA_EDAD = 18;
        final int EDAD_JUBILACION = 65;

        IO.print("¿Cuántos años tienes? ");
        String entrada = IO.readln().trim();

        // 1. Convertir a int
        int edad = Integer.parseInt(entrada);

        // 2. Validar rango
        if (edad < EDAD_MINIMA || edad > EDAD_MAXIMA) {
            IO.println("Error: la edad debe estar entre " + EDAD_MINIMA + " y " + EDAD_MAXIMA + ".");
            return;
        }

        // 3. Clasificar según edad
        if (edad < MAYORIA_EDAD) {
            IO.println("Eres menor de edad.");
        } else if (edad < EDAD_JUBILACION) {
            IO.println("Eres adulto en edad laboral.");
        } else {
            IO.println("Estás en edad de jubilación.");
        }

        // 4. Mensaje extra según si está en edad laboral
        boolean enEdadLaboral = edad >= MAYORIA_EDAD && edad < EDAD_JUBILACION;
        String mensaje = enEdadLaboral
                ? "Puedes trabajar legalmente."
                : "No estás en edad laboral.";
        IO.println(mensaje);
    }
}

Salidas posibles según la entrada:

Entrada Salida
"17" Eres menor de edad.
No estás en edad laboral.
"35" Eres adulto en edad laboral.
Puedes trabajar legalmente.
"70" Estás en edad de jubilación.
No estás en edad laboral.
"-5" Error: debes escribir un número entero positivo. (no pasa el matches)
"200" Error: la edad debe estar entre 0 y 120.

2.8 Ejemplos resueltos paso a paso

Vamos a hacer cuatro ejemplos completos, cada uno con un enfoque distinto. Te recomiendo escribirlos tú mismo en IntelliJ y jugar con los valores para ver cómo cambia la salida.

2.8.1 Ejemplo 1: calculadora simple con menú

public class CalculadoraSimple {

    public static void main(String[] args) {

        final int OP_SUMAR = 1;
        final int OP_RESTAR = 2;
        final int OP_MULTIPLICAR = 3;
        final int OP_DIVIDIR = 4;

        IO.println("=== Calculadora ===");
        IO.println("1. Sumar");
        IO.println("2. Restar");
        IO.println("3. Multiplicar");
        IO.println("4. Dividir");
        IO.print("Elige opción (1-4): ");

        String entrada = IO.readln().trim();
        int opcion = Integer.parseInt(entrada);

        IO.print("Operando A: ");
        double a = Double.parseDouble(IO.readln().trim().replace(',', '.'));
        IO.print("Operando B: ");
        double b = Double.parseDouble(IO.readln().trim().replace(',', '.'));

        if (opcion == OP_SUMAR) {
            IO.println("A + B = " + (a + b));
        } else if (opcion == OP_RESTAR) {
            IO.println("A - B = " + (a - b));
        } else if (opcion == OP_MULTIPLICAR) {
            IO.println("A * B = " + (a * b));
        } else if (opcion == OP_DIVIDIR) {
            if (b == 0) {
                IO.println("Error: no se puede dividir por cero.");
            } else {
                IO.println("A / B = " + (a / b));
            }
        }
    }
}

Puntos a observar:

  1. Constantes para las opciones del menú: OP_SUMAR, etc. El código se autodocumenta.
  2. replace(',', '.') en los operandos: si el usuario escribe 3,14 (con coma), lo convertimos a 3.14 (con punto) antes de parsear. Truco útil para aceptar el formato español sin tener que pelearse con locales.
  3. División por cero controlada con un if (más adelante, en sección 8, verás try-catch).
  4. No hace falta import ni close(): java.lang.IO ya está disponible por defecto en Java 25 y no hay que cerrar ningún recurso. El código queda más limpio que con Scanner.

2.8.2 Ejemplo 2: tarifa de taxi

Una empresa de taxis cobra una tarifa base de 2,50 €, más 1,10 € por kilómetro hasta 10 km, y 0,80 € por kilómetro a partir de ahí. Vamos a calcular la tarifa.

public class TarifaTaxi {

    public static void main(String[] args) {

        final double TARIFA_BASE = 2.50;
        final double TARIFA_HASTA_10 = 1.10;
        final double TARIFA_DESDE_10 = 0.80;
        final double KM_UMBRAL = 10.0;

        IO.print("Kilómetros recorridos: ");
        // Normalizamos la coma a punto antes de validar y parsear
        String entrada = IO.readln().trim().replace(',', '.');
        double km = Double.parseDouble(entrada);

        if (km < 0) {
            IO.println("Los kilómetros no pueden ser negativos.");
            return;
        }

        double precio;
        if (km <= KM_UMBRAL) {
            precio = TARIFA_BASE + (km * TARIFA_HASTA_10);
        } else {
            precio = TARIFA_BASE + (KM_UMBRAL * TARIFA_HASTA_10)
                    + ((km - KM_UMBRAL) * TARIFA_DESDE_10);
        }

        IO.println(String.format("Tarifa: %.2f €", precio));
    }
}

Pruebas:

km Tarifa
0 2,50 €
5 8,00 €
10 13,50 €
20 21,50 €
50 45,50 €

Puntos a observar:

  1. Constantes con final: si la empresa cambia las tarifas, solo cambias una línea.
  2. String.format("%.2f", precio): imprime el número con dos decimales. %.2f significa "número con 2 decimales".
  3. replace(',', '.') antes de validar: así el usuario puede escribir 5,5 o 5.5 y ambos funcionan.
  4. if con else (sin else if) porque solo hay dos casos: por debajo o por encima del umbral.

2.8.3 Ejemplo 3: determinar estación del año

Dado un número de mes (1–12), decir qué estación del año es. Esta es la versión con if encadenado (en la sección 3 verás una versión mucho más elegante con switch).

public class EstacionAño {

    public static void main(String[] args) {

        IO.print("Mes (1-12): ");
        String entrada = IO.readln().trim();
        int mes = Integer.parseInt(entrada);

        String estacion;
        if (mes == 12 || mes == 1 || mes == 2) {
            estacion = "Invierno";
        } else if (mes == 3 || mes == 4 || mes == 5) {
            estacion = "Primavera";
        } else if (mes == 6 || mes == 7 || mes == 8) {
            estacion = "Verano";
        } else {
            estacion = "Otoño";
        }

        IO.println("Estación: " + estacion);
    }
}

Puntos a observar:

  1. Cada else if agrupa tres meses con ||: aquí el || es la forma natural, porque "es invierno si el mes es 12 O 1 O 2".
  2. El else final cubre septiembre, octubre, noviembre sin necesidad de escribirlos explícitamente: si las tres primeras condiciones son false, forzosamente es otoño.

2.8.4 Ejemplo 4: sistema de notas con letras

Las universidades anglosajonas usan letras (A, B, C, D, F). Vamos a convertir una nota numérica (0–10) en su letra equivalente.

public class NotaLetra {

    public static void main(String[] args) {

        final double NOTA_MIN = 0.0;
        final double NOTA_MAX = 10.0;

        IO.print("Tu nota (0-10): ");
        String entrada = IO.readln().trim().replace(',', '.');
        double nota = Double.parseDouble(entrada);

        if (nota < NOTA_MIN || nota > NOTA_MAX) {
            IO.println("Nota fuera de rango (0-10).");
            return;
        }

        String letra;
        if (nota >= 9.0) {
            letra = "A — Sobresaliente";
        } else if (nota >= 7.0) {
            letra = "B — Notable";
        } else if (nota >= 5.0) {
            letra = "C — Aprobado";
        } else if (nota >= 3.0) {
            letra = "D — Insuficiente";
        } else {
            letra = "F — Muy deficiente";
        }

        IO.println("Tu calificación: " + letra);
    }
}

Puntos a observar:

  1. Validación de rango antes de clasificar: si la nota está fuera de [0, 10], ni siquiera intentamos clasificarla.
  2. else if encadenado de mayor a menor: las condiciones se evalúan en orden, así que nota >= 7.0 significa "entre 7.0 y 8.999..." (porque las anteriores ya eran false). No hace falta escribir nota >= 7.0 && nota < 9.0.
  3. else final para "F": si ninguna de las anteriores se cumple, es "F". Esa es la lógica del "por descarte".

2.9 Buenas prácticas y reglas de oro

2.9.1 Las 10 reglas del if profesional

Aquí van las diez reglas que nos autoimponemos en este curso para escribir if como profesionales. Si las sigues, tu código será limpio, legible y mantenible.

  1. Siempre llaves, aunque el bloque tenga una sola línea. Sin excepciones.
  2. Indenta con 4 espacios. IntelliJ lo hace solo (Ctrl+Alt+L).
  3. else if y else se alinean con if. No los indentes.
  4. Una sola sentencia por línea. No juntes if (x) { y(); z(); } en una sola línea.
  5. Constantes para magic numbers. if (edad >= 18)if (edad >= MAYORIA_EDAD).
  6. Paréntesis en condiciones compuestas.
  7. == para comparar; = para asignar. Si IntelliJ subraya en rojo, mira dos veces.
  8. .equals() para String, nunca ==. Y si quieres defender contra null, pon el literal primero: "admin".equals(rol).
  9. Prefiere early return a pirámides de if. Si tienes más de 3 niveles de anidamiento, refactoriza.
  10. Valida la entrada antes de procesarla. Primero comprueba que el dato es válido, luego opera con él.

2.9.2 Cuándo refactorizar (cambiar la estrcutura sin cambiar el funcionamiento) un if

Síntoma Refactoriza a
Más de 3 niveles de anidamiento. Early return o extrae un método (UD4).
5+ else if comparando el mismo valor. switch expression (sección 3).
Condiciones con dobles negaciones. De Morgan + pensar en positivo.
if/else solo para asignar a una variable. Operador ternario (si es legible).
Condiciones duplicadas en varios sitios. Variable booleana con nombre.
Condiciones enormes con &&/|| mezclados. Variables booleanas intermedias.

2.9.3 Comentar el if

Comentarios buenos en un if explican por qué, no qué:

// MAL: explica lo que ya dice el código
if (edad >= 18) {     // si la edad es mayor o igual que 18
    IO.println("Mayor");
}

// BIEN: explica por qué 18 es el umbral
if (edad >= MAYORIA_EDAD) {   // RD 450/2010: mayoría de edad en España
    IO.println("Mayor");
}

// BIEN: explica una decisión de negocio poco obvia
if (km > UMBRAL_NOCTURNO && esFinDeSemana) {
    // La tarifa nocturna + fin de semana solo aplica a partir de 5 km
    // para no encarecer trayectos cortos (decisión de producto, 2025)
    tarifa = tarifa * 1.5;
}

El comentario no debe repetir el código; debe dar contexto que el código no puede expresar.


2.10 Resumen y mapa conceptual

2.10.1 Mapa conceptual

mindmap
  root((Sección 2 - if))
    Sintaxis
      if simple
      if con else
      if con else if
    Anatomía
      Condición entre paréntesis
      Bloque entre llaves
      else if y else alineados
    Anidamiento
      if anidados
      Pirámide de la muerte
      Early return
      Cláusulas de guarda
    Operador ternario
      Para asignar
      Como return
      No anidar
      No efectos secundarios
    Trampas
      = vs ==
      Bloques sin llaves
      Dangling else
      String con ==
      Magic numbers
    Validación
      Rango
      Menú
      Texto con equals
      IO.readln + trim
      Separador decimal
      matches para enteros

2.10.2 Las 12 ideas que no debes olvidar

  1. El if evalúa una condición booleana entre paréntesis. Sin paréntesis, no compila.
  2. if simple: se ejecuta o no se ejecuta. No tiene else.
  3. if con else: una de las dos ramas se ejecuta siempre. Nunca las dos.
  4. else if encadenado: solo se ejecuta la primera rama true. Las demás se saltan.
  5. El else final es opcional. Si no lo pones y ninguna condición se cumple, no se ejecuta nada.
  6. Las llaves siempre, aunque el bloque tenga una sola línea. Es la regla del bug "goto fail" de Apple.
  7. Compara String con .equals(), nunca con ==. Y mejor "admin".equals(rol) que rol.equals("admin") para defender contra null.
  8. El operador ternario es if/else que produce un valor. Úsalo para asignar, no para ejecutar.
  9. Evita la pirámide de la muerte con early return: comprueba lo negativo y sal, en vez de anidar lo positivo.
  10. Valida primero, procesa después. Primero comprueba que el dato es válido, luego opera con él.
  11. IO.readln() siempre devuelve un String limpio: no hay buffer residual ni InputMismatchException. Tú decides cómo convertirlo con Integer.parseInt, Double.parseDouble y replace(',', '.').

2.11 Actividades propuestas

Cinco actividades graduadas, en orden de dificultad creciente. Hazlas en este orden. Las soluciones se publicarán en Aules cuando el profesor las entregue.

2.11.1 Actividad 1 — Clasificador de edad

Objetivo: practicar if / else if / else con validación de rango.

Enunciado: Escribe un programa ClasificadorEdad que pida al usuario su edad y muestre uno de estos mensajes:

Edad Mensaje
0 a 12 "Infancia"
13 a 17 "Adolescencia"
18 a 29 "Juventud"
30 a 64 "Adultez"
65 a 120 "Vejez"
Fuera de [0, 120] "Edad no válida"

Requisitos:

  • Usa constantes final int para los umbrales.
  • Lee la entrada con IO.readln().
  • Usa else if encadenado para clasificar (no anidamiento).
  • No hace falta import ni close(): java.lang.IO está disponible por defecto.

Entrega: archivo ClasificadorEdad.java en el paquete jams.programacion.ud3.

Criterios evaluados: CE 3.1, CE 3.5, CE 3.7.

2.11.2 Actividad 2 — Refactorización de una pirámide

Objetivo: practicar early return y cláusulas de guarda.

Enunciado: Te dan el siguiente código (con pirámide de la muerte):

if (usuarioRegistrado) {
    if (correoVerificado) {
        if (!cuentaSuspendida) {
            if (edad >= 18) {
                IO.println("Acceso permitido.");
            } else {
                IO.println("Debes ser mayor de edad.");
            }
        } else {
            IO.println("Cuenta suspendida.");
        }
    } else {
        IO.println("Correo no verificado.");
    }
} else {
    IO.println("Usuario no registrado.");
}

Tareas:

  1. Reescríbelo con cláusulas de guarda (early return) de modo que la indentación no crezca.
  2. Define constantes con final para los mensajes y la edad mínima.
  3. Compara las dos versiones: ¿cuál es más legible? Escribe dos líneas justificando tu elección al final del archivo, en un comentario.

Entrega: archivo RefactorAcceso.java en el paquete jams.programacion.ud3, con la versión original en un comentario de bloque /* ... */ y la versión refactorizada activa.

Criterios evaluados: CE 3.1, CE 3.5, CE 3.7.

2.11.3 Actividad 3 — Calculadora de tarifa de envío

Objetivo: combinar if/else if, constantes y validación con IO.readln.

Enunciado: Una empresa de paquetería cobra:

  • 3,50 € de tarifa base.
  • 0,90 € por kg hasta 5 kg.
  • 1,20 € por kg desde 5 kg hasta 20 kg.
  • No se aceptan paquetes de más de 20 kg.
  • Si el destino es internacional (preguntar s/n al usuario), se aplica un recargo del 30 % sobre el total.

Requisitos:

  • Pide el peso en kg (decimal) y si el envío es internacional (s/n).
  • Valida el peso con un rango [0, 20].
  • Valida la respuesta internacional con .equalsIgnoreCase("s") y .equalsIgnoreCase("n"). Si la respuesta no es válida, muestra error y sale.
  • Muestra el precio final con dos decimales usando String.format("%.2f", precio).

Entrega: archivo CalculadoraEnvio.java en el paquete jams.programacion.ud3. Incluye en comentarios al final tres ejemplos de ejecución con sus resultados esperados.

Criterios evaluados: CE 3.1, CE 3.5, CE 3.7.

2.11.4 Actividad 4 — Detector de caracteres

Objetivo: practicar if encadenado con char y String.

Enunciado: Escribe un programa DetectorCaracter que pida al usuario un carácter (un solo carácter, leído con IO.readln().charAt(0)). El programa debe clasificarlo en una de estas categorías:

Categoría Criterio
"Vocal minúscula" c == 'a', 'e', 'i', 'o', 'u'
"Vocal mayúscula" c == 'A', 'E', 'I', 'O', 'U'
"Consonante minúscula" Letra minúscula que no sea vocal
"Consonante mayúscula" Letra mayúscula que no sea vocal
"Dígito" c >= '0' && c <= '9'
"Otro" Cualquier otra cosa (espacio, símbolo, etc.)

Requisitos:

  • Valida que el usuario haya escrito al menos un carácter (si no, error y sale).
  • Usa else if encadenado, no anidamiento.
  • Para comprobar si es letra, puedes usar (c >= 'a' && c <= 'z') y (c >= 'A' && c <= 'Z'). (El método Character.isLetter(c) se verá en UD2.)

Entrega: archivo DetectorCaracter.java en el paquete jams.programacion.ud3.

Criterios evaluados: CE 3.1, CE 3.5, CE 3.7.

2.11.5 Actividad 5 — Cuestionario de repaso

Responde por escrito (no hace falta compilar):

  1. ¿Cuál es la diferencia entre if simple, if con else y if con else if?
  2. ¿Por qué decimos que las llaves deben ser obligatorias, aunque el bloque tenga una sola línea?
  3. ¿Qué es el dangling else y cómo se evita?
  4. ¿Por qué if (rol == "admin") puede no funcionar aunque el contenido sea "admin"?
  5. Escribe con operador ternario: "asigna a descuento el valor 0.20 si esVip es true, 0.05 si no".
  6. ¿Cuándo refactorizarías un if anidado con early return?
  7. ¿Por qué if (edad < 0 || edad > 120) va antes que if (edad < 18) en un programa que clasifica edades?
  8. Nombra dos reglas de oro del if profesional que aplicarás a partir de hoy.

Entrega: archivo cuestionario_seccion2.md con tus respuestas.

Criterios evaluados: CE 3.1, CE 3.7.