Control de flujo en Java — práctica con un registro, la ventanilla de secretaría y un adivino de tres intentos (y las contraseñas que no coincidían aunque fueran iguales)

La primera vez que hice un formulario de registro en Java estaba convencido de que lo tenía todo bien. Escribía la contraseña, la repetía letra por letra, pulsaba Intro… y el programa me contestaba, muy tranquilo: «no coinciden». Lo probé cinco veces. Copié y pegué la contraseña para no equivocarme. Nada. El fallo no estaba en lo que yo tecleaba, sino en una sola línea: había comparado los dos textos con ==, como hacía con los números. En Java eso compila, se ejecuta y casi siempre da false. Esta práctica empieza justo ahí, con ese programa arreglado y con el error provocado a propósito para que lo veas con tus propios ojos.

Si vienes de la teoría de control de flujo en Java, ya conoces las piezas: Scanner y la trampa del buffer, if/else if/else, los operadores &&, || y !, y el switch en sus tres formas. Hoy las juntamos en tres programas de nivel medio con Laura, la estudiante de FP2 que nos acompaña desde la sintaxis: su alta en el campus virtual (validar cada dato y comparar textos bien), la ventanilla de secretaría (un trámite por ejecución, con el switch de flechas, el switch que devuelve un valor y un fall-through hecho a propósito) y un juego de adivinar el número con tres intentos y sin bucles. Los datos se parecen a los de verdad: edades fuera de rango, correos sin punto, notas con coma decimal y opciones que no existen.

Una advertencia antes de empezar: aquí no hay ni un solo bucle ni un solo array. No es un descuido: llegan en el tema siguiente. Y el tercer programa está hecho así a propósito, para que veas con tus propios ojos por qué hacen falta.

Antes de empezar

Conviene tener claro:

  • Cómo leer del teclado con Scanner: nextLine() para textos y nextInt()/nextDouble() para números, y por qué después de un nextInt() hace falta un nextLine() vacío (la trampa del buffer).
  • Que en un equipo en español nextDouble() espera la coma: se teclea 7,4, no 7.4.
  • Cómo se encadenan if, else if y else: se ejecuta la primera rama cuya condición sea verdadera, y ninguna más.
  • La diferencia entre && (las dos cosas a la vez) y || (basta con una).

Si alguno te suena dudoso, repasa la teoría antes de seguir.

Crea una carpeta practica_control_flujo con un .java por programa (el archivo se llama como la clase: RegistroUsuario.java). Compila y ejecuta con javac RegistroUsuario.java y java RegistroUsuario. Las salidas de este artículo son reales, de una única ejecución con Java 21 en un equipo en español: lo que ves detrás de cada pregunta es lo que se tecleó. También puedes probarlos en el entorno de Java de la web: lo que teclearías va en la pestaña Entrada, una línea por dato.

Programa 1: Validador de datos de usuario

Laura se da de alta en el campus virtual. El programa le pide el nombre, la edad, el correo, la contraseña dos veces y si acepta las normas, y comprueba cada dato por separado. Lo que lo complica: hay que decir todo lo que está mal de una vez (no parar en el primer fallo), comparar textos sin caer en ==, y no cortar el correo antes de saber que tiene una @.

La idea

El programa tiene tres fases bien separadas:

  1. Leer todo primero, con la trampa del buffer resuelta justo después de nextInt().
  2. Comprobar cada dato con su propio if/else. No son ramas de una misma cadena: son seis preguntas independientes, así que van seis if seguidos. Una variable todoBien, que empieza en true, se pone a false en cuanto algo falla.
  3. Decidir al final con un único if (todoBien).

Antes de programar, piensa en cada dato: ¿qué condición tiene que cumplir para ser válido? Escríbela en positivo («edad entre 16 y 99») y luego tradúcela: «entre» son dos condiciones a la vez, así que &&.

El código

import java.util.Scanner;

/**
 * Alta en el campus virtual: cada dato se valida con if/else y los textos se comparan con equals().
 * Datos de Laura inventados.
 */
public class RegistroUsuario {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("=== ALTA EN EL CAMPUS VIRTUAL ===");

        System.out.print("Nombre y apellido: ");
        String nombre = sc.nextLine().trim();      // trim() quita los espacios de los bordes

        System.out.print("Edad: ");
        int edad = sc.nextInt();
        sc.nextLine();                             // se lleva el Intro que deja nextInt()

        System.out.print("Correo: ");
        String correo = sc.nextLine().trim();

        System.out.print("Contraseña: ");
        String clave = sc.nextLine();              // sin trim(): un espacio también es parte de la clave

        System.out.print("Repite la contraseña: ");
        String clave2 = sc.nextLine();

        System.out.print("¿Aceptas las normas del campus? (s/n): ");
        String acepta = sc.nextLine().trim();

        System.out.println();
        System.out.println("--- Comprobación ---");
        boolean todoBien = true;                   // empieza en true: cualquier fallo lo pone a false

        // 1. El nombre no puede estar vacío
        if (nombre.isEmpty()) {
            System.out.println("Nombre: vacío");
            todoBien = false;
        } else {
            System.out.println("Nombre: correcto");
        }

        // 2. Edad entre 16 y 99: se tienen que cumplir las DOS cosas, así que &&
        if (edad >= 16 && edad <= 99) {
            System.out.println("Edad: correcta");
        } else {
            System.out.println("Edad: fuera de rango (16-99)");
            todoBien = false;
        }

        // 3. Correo: una @ que no esté al principio y un punto después de la @ (y no al final)
        int arroba = correo.indexOf('@');          // -1 si no hay ninguna
        int punto = correo.lastIndexOf('.');       // el último punto: el de .es, .com...
        if (arroba > 0 && punto > arroba + 1 && punto < correo.length() - 1) {
            System.out.println("Correo: correcto");
        } else {
            System.out.println("Correo: no válido");
            todoBien = false;
        }

        // 4. Contraseña: primero la longitud; si es larga, que no sea el propio nombre
        if (clave.length() < 8) {
            System.out.println("Contraseña: demasiado corta (" + clave.length() + " de 8)");
            todoBien = false;
        } else if (clave.equalsIgnoreCase(nombre)) {
            System.out.println("Contraseña: no puede ser tu nombre");
            todoBien = false;
        } else {
            System.out.println("Contraseña: correcta");
        }

        // 5. Las dos contraseñas: equals() compara las letras, una a una y con mayúsculas
        if (clave.equals(clave2)) {
            System.out.println("Repetición: coincide");
        } else {
            System.out.println("Repetición: no coincide");
            todoBien = false;
        }

        // 6. Vale «s» y «S»: equalsIgnoreCase no distingue mayúsculas de minúsculas
        if (acepta.equalsIgnoreCase("s")) {
            System.out.println("Normas: aceptadas");
        } else {
            System.out.println("Normas: sin aceptar");
            todoBien = false;
        }

        System.out.println();
        if (todoBien) {
            // Solo aquí es seguro cortar el correo: sabemos que la @ existe y no está en la posición 0
            String usuario = correo.substring(0, arroba);
            System.out.println("Alta hecha. Tu usuario es " + usuario + ".");
        } else {
            System.out.println("No se puede dar de alta: revisa lo que está mal.");
        }
        sc.close();
    }
}

Salida

Primero, Laura con todos los datos bien:

=== ALTA EN EL CAMPUS VIRTUAL ===
Nombre y apellido: Laura Santana
Edad: 19
Correo: laura.santana@ulpgc.es
Contraseña: fp2-Laura26
Repite la contraseña: fp2-Laura26
¿Aceptas las normas del campus? (s/n): s

--- Comprobación ---
Nombre: correcto
Edad: correcta
Correo: correcto
Contraseña: correcta
Repetición: coincide
Normas: aceptadas

Alta hecha. Tu usuario es laura.santana.

Las seis comprobaciones pasan y el usuario sale del correo: todo lo que hay antes de la @, es decir, laura.santana. Ahora la misma Laura, pero con prisa y con todo mal:

=== ALTA EN EL CAMPUS VIRTUAL ===
Nombre y apellido: Laura Santana
Edad: 15
Correo: laura@ulpgc
Contraseña: laura
Repite la contraseña: Laura
¿Aceptas las normas del campus? (s/n): n

--- Comprobación ---
Nombre: correcto
Edad: fuera de rango (16-99)
Correo: no válido
Contraseña: demasiado corta (5 de 8)
Repetición: no coincide
Normas: sin aceptar

No se puede dar de alta: revisa lo que está mal.

Fíjate en que el programa no se para en la edad: sigue comprobando y enseña los cinco fallos de una vez. Eso es lo que consigue tener seis if independientes en lugar de una cadena de else if. Comprobamos a mano cada línea:

DatoValorCondiciónResultado
Edad1515 >= 16 && 15 <= 99 → false && …fuera de rango
Correolaura@ulpgcpunto = lastIndexOf('.') = -1, y -1 > 5 + 1 es falseno válido
Contraseñalauralength() = 5, y 5 < 8demasiado corta (5 de 8)
Repeticiónlaura / Lauraequals distingue mayúsculasno coincide
Normasn"n".equalsIgnoreCase("s") → falsesin aceptar

Y una tercera ejecución para la rama que falta: la contraseña es larga, pero es el propio nombre (con otras mayúsculas).

=== ALTA EN EL CAMPUS VIRTUAL ===
Nombre y apellido: Laura Santana
Edad: 19
Correo: laura.santana@ulpgc.es
Contraseña: laura santana
Repite la contraseña: laura santana
¿Aceptas las normas del campus? (s/n): S

--- Comprobación ---
Nombre: correcto
Edad: correcta
Correo: correcto
Contraseña: no puede ser tu nombre
Repetición: coincide
Normas: aceptadas

No se puede dar de alta: revisa lo que está mal.

"laura santana".equalsIgnoreCase("Laura Santana") da true, así que entra en el else if de la contraseña. Fíjate también en la respuesta S mayúscula de las normas: equalsIgnoreCase la acepta igual que s.

Preguntas que te puedes hacer

¿Por qué seis if sueltos y no una cadena de else if? Porque una cadena se queda con una rama y se salta las demás. Si la edad fallara en un if y el correo estuviera en su else if, el correo ni se comprobaría. Aquí queremos las seis respuestas, así que cada pregunta va en su propio if. La cadena else if sí aparece dentro de la contraseña: ahí las preguntas sí dependen unas de otras (si es corta, no tiene sentido mirar si coincide con el nombre).

¿Por qué todoBien empieza en true? Es el patrón de «inocente hasta que se demuestre lo contrario»: empieza en true y cualquier comprobación que falle lo baja a false. Ninguna lo vuelve a subir. Si empezara en false, tendrías que poner todoBien = true en algún sitio y acabarías dándolo por bueno con que pasara una sola comprobación.

¿Qué hace exactamente la condición del correo? Zoom a ese trozo:

// 3. Correo: una @ que no esté al principio y un punto después de la @ (y no al final)
int arroba = correo.indexOf('@');          // -1 si no hay ninguna
int punto = correo.lastIndexOf('.');       // el último punto: el de .es, .com...
if (arroba > 0 && punto > arroba + 1 && punto < correo.length() - 1) {
    System.out.println("Correo: correcto");
} else {
    System.out.println("Correo: no válido");
    todoBien = false;
}

indexOf('@') devuelve la posición de la primera @ (o -1 si no hay). arroba > 0 exige que exista y que no sea el primer carácter. punto > arroba + 1 exige al menos una letra entre la @ y el punto (laura@.es no vale). punto < correo.length() - 1 impide que el punto sea lo último (laura@ulpgc. tampoco). Con laura.santana@ulpgc.es: arroba = 13, punto = 19 y length() = 22, así que 13 > 0, 19 > 14 y 19 < 21: las tres se cumplen. No es una validación perfecta (para eso están las expresiones regulares, con su propio tema), pero atrapa los errores de verdad.

¿Por qué el substring está dentro del if (todoBien)? Porque correo.substring(0, arroba) con arroba = -1 lanza StringIndexOutOfBoundsException. Dentro del if (todoBien) sabemos que el correo pasó la comprobación, así que la @ existe. Es la idea de validar antes de usar: primero te aseguras de que el dato tiene sentido y después operas con él.

¿Y qué pasa si comparo las contraseñas con ==? Lo mismo que me pasó a mí. Este programa corto lee las dos contraseñas y las compara de las dos formas:

import java.util.Scanner;

/** La misma comprobación de la repetición, con == y con equals(). */
public class RepeticionMal {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Contraseña: ");
        String clave = sc.nextLine();
        System.out.print("Repite la contraseña: ");
        String clave2 = sc.nextLine();

        // == pregunta: «¿son el MISMO objeto en memoria?». Cada nextLine() crea un String nuevo.
        if (clave == clave2) {
            System.out.println("Con ==: coinciden");
        } else {
            System.out.println("Con ==: no coinciden");
        }
        // equals() pregunta: «¿tienen las mismas letras?»
        if (clave.equals(clave2)) {
            System.out.println("Con equals(): coinciden");
        } else {
            System.out.println("Con equals(): no coinciden");
        }
        sc.close();
    }
}

Salida, tecleando exactamente lo mismo dos veces:

Contraseña: fp2-Laura26
Repite la contraseña: fp2-Laura26
Con ==: no coinciden
Con equals(): coinciden

El dibujo explica por qué:

Por qué == dice que las contraseñas no coincidenCada nextLine() crea un objeto String nuevo. clave y clave2 apuntan a dos objetos distintos con las mismas letras fp2-Laura26: == compara las flechas y da false; equals() compara las letras y da true.VariablesMontón: dos objetos Stringclavefp2–Laura26objeto 1clave2fp2–Laura26objeto 2clave == clave2¿la misma flecha? → falseclave.equals(clave2)¿las mismas letras? → true
Dos objetos con las mismas letras: == mira si las flechas llegan al mismo sitio; equals(), si dicen lo mismo.

Cada nextLine() crea un objeto String nuevo. Las dos variables apuntan a dos objetos distintos que, por casualidad, tienen las mismas letras. == pregunta si las dos flechas llegan al mismo objeto (no), y equals() pregunta si los dos objetos dicen lo mismo (sí).

Comparar textos con ==. Compila sin avisos y con literales escritos en el código a veces «funciona» (Java reutiliza los literales iguales), lo que lo hace todavía más traicionero: en cuanto el texto viene del teclado, da false. Con String, siempre equals() (o equalsIgnoreCase() si las mayúsculas dan igual).

clave.equals(clave2) lanzaría NullPointerException si clave fuera null. Con Scanner no pasa (nextLine() devuelve como mucho una cadena vacía), pero cuando compares con un texto fijo puedes darle la vuelta: "s".equalsIgnoreCase(acepta) nunca falla, aunque acepta sea null.

Pruébalo tú

  • Añade una séptima comprobación: el correo tiene que acabar en @ulpgc.es (pista: endsWith). ¿Va como if suelto o como else if del correo?
  • Cambia el equals de la repetición por ==, ejecuta con los datos buenos y mira qué pasa con el alta. Después déjalo como estaba.

Programa 2: La ventanilla de secretaría con switch

Laura va a la ventanilla de secretaría. Un menú le ofrece tres trámites: pedir un certificado (gratis en digital, 6,50 € en papel, 12,75 € urgente), cambiar de grupo (se concede si trabaja o si tiene un 7 de media) y ver cuánto le queda por pagar de la matrícula (4 plazos de 180,40 €). Los precios y las normas son inventados. Cada ejecución hace un trámite: sin bucle no se puede volver al menú (eso llegará con do-while).

La idea

Un menú numerado es el sitio natural del switch: cada número es un case. Aquí salen las tres formas que viste en la teoría, cada una donde mejor encaja:

  1. El switch con flechas para el menú: cada trámite hace lo suyo y no cae en el siguiente.
  2. El switch como expresión para el precio del certificado: cada caso devuelve un valor y el resultado se guarda en una variable.
  3. El switch clásico sin break, a propósito, para sumar los plazos que faltan: es el único sitio de toda la práctica donde el fall-through nos viene bien.

Antes de programar, pregúntate en cada switch: ¿quiero hacer algo distinto en cada caso (flechas), calcular un valor según el caso (expresión) o que los casos se acumulen unos sobre otros (clásico sin break)?

El código

import java.util.Scanner;

/**
 * Ventanilla de secretaría: un trámite por ejecución, con las tres formas de switch.
 * Precios y plazos inventados.
 */
public class Ventanilla {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        final int PLAZO = 18040;                   // cada plazo de la matrícula, en céntimos (180,40 €)

        System.out.println("=== VENTANILLA DE SECRETARÍA ===");
        System.out.println("1. Pedir un certificado");
        System.out.println("2. Cambiar de grupo");
        System.out.println("3. Ver lo que queda por pagar");
        System.out.println("0. Salir");
        System.out.print("Trámite: ");
        int tramite = sc.nextInt();
        sc.nextLine();                             // fuera el Intro: después se leen líneas

        // switch con flechas (Java 14+): cada caso hace lo suyo y no cae en el siguiente
        switch (tramite) {
            case 1 -> {
                System.out.print("Formato (digital/papel/urgente): ");
                String formato = sc.nextLine().trim().toLowerCase();
                // switch como EXPRESIÓN: cada caso devuelve un precio y el resultado se guarda
                double precio = switch (formato) {
                    case "digital" -> 0.0;
                    case "papel" -> 6.50;
                    case "urgente" -> 12.75;
                    default -> -1;                 // marca de «formato desconocido»
                };
                if (precio < 0) {
                    System.out.println("Formato desconocido: " + formato);
                } else if (precio == 0) {
                    System.out.println("Certificado digital: gratis, te llega al correo.");
                } else {
                    System.out.printf("Certificado en %s: %.2f €%n", formato, precio);
                }
            }
            case 2 -> {
                System.out.print("Grupo actual (mañana/tarde): ");
                String actual = sc.nextLine().trim().toLowerCase();
                System.out.print("¿Trabajas? (s/n): ");
                boolean trabaja = sc.nextLine().trim().equalsIgnoreCase("s");
                System.out.print("Nota media: ");
                double media = sc.nextDouble();    // en un equipo en español, con coma: 7,4
                // Primero se descarta lo imposible; después, la norma: trabajar O tener un 7 de media
                if (!actual.equals("mañana") && !actual.equals("tarde")) {
                    System.out.println("Grupo no válido: " + actual);
                } else if (trabaja || media >= 7) {
                    String nuevo = actual.equals("mañana") ? "tarde" : "mañana";
                    System.out.println("Cambio concedido: pasas al grupo de " + nuevo + ".");
                } else {
                    System.out.println("Cambio denegado: hay que trabajar o tener un 7 de media.");
                }
            }
            case 3 -> {
                System.out.print("Plazos pagados (0-4): ");
                int pagados = sc.nextInt();
                int pendiente = 0;
                // switch clásico SIN break a propósito: desde el caso de entrada, cada plazo suma el siguiente
                switch (pagados) {
                    case 0:
                        pendiente += PLAZO;        // falta el plazo 1...
                    case 1:
                        pendiente += PLAZO;        // ...el 2...
                    case 2:
                        pendiente += PLAZO;        // ...el 3...
                    case 3:
                        pendiente += PLAZO;        // ...y el 4
                        break;                     // aquí sí: que no siga hasta el default
                    case 4:
                        break;                     // todo pagado: pendiente sigue en 0
                    default:
                        pendiente = -1;            // dato imposible
                }
                if (pendiente < 0) {
                    System.out.println("Dato no válido: la matrícula tiene 4 plazos.");
                } else if (pendiente == 0) {
                    System.out.println("Matrícula pagada. Nada pendiente.");
                } else {
                    System.out.printf("Te quedan %d plazos: %.2f €%n", 4 - pagados, pendiente / 100.0);
                }
            }
            case 0 -> System.out.println("Hasta luego.");
            default -> System.out.println("Trámite no válido: " + tramite);
        }
        sc.close();
    }
}

Salida

Cinco ejecuciones, una por camino. Un certificado en papel:

=== VENTANILLA DE SECRETARÍA ===
1. Pedir un certificado
2. Cambiar de grupo
3. Ver lo que queda por pagar
0. Salir
Trámite: 1
Formato (digital/papel/urgente): papel
Certificado en papel: 6,50 €

"papel" entra en el case "papel" -> 6.50, y como 6,50 no es menor que 0 ni igual a 0, va al else y se imprime con %.2f, que en un equipo en español usa coma. Cambio de grupo con un 7,4 de media (sin trabajar):

=== VENTANILLA DE SECRETARÍA ===
1. Pedir un certificado
2. Cambiar de grupo
3. Ver lo que queda por pagar
0. Salir
Trámite: 2
Grupo actual (mañana/tarde): mañana
¿Trabajas? (s/n): n
Nota media: 7,4
Cambio concedido: pasas al grupo de tarde.

trabaja || media >= 7 es false || true, es decir, true. Y con un 6,9 desde la tarde:

=== VENTANILLA DE SECRETARÍA ===
1. Pedir un certificado
2. Cambiar de grupo
3. Ver lo que queda por pagar
0. Salir
Trámite: 2
Grupo actual (mañana/tarde): tarde
¿Trabajas? (s/n): n
Nota media: 6,9
Cambio denegado: hay que trabajar o tener un 7 de media.

false || false da false: denegado. Ahora los plazos, con uno ya pagado:

=== VENTANILLA DE SECRETARÍA ===
1. Pedir un certificado
2. Cambiar de grupo
3. Ver lo que queda por pagar
0. Salir
Trámite: 3
Plazos pagados (0-4): 1
Te quedan 3 plazos: 541,20 €

Este es el switch que merece un dibujo:

El fall-through que suma los plazos que faltanCon pagados = 1 el switch entra en case 1 y, como no hay break, sigue por case 2 y case 3: pendiente pasa por 18040, 36080 y 54120 céntimos. El break de case 3 lo saca antes de case 4 y default. 54120 / 100.0 son 541,20 euros.switch (pagados) con pagados = 1pendientecase 0:pendiente += PLAZO;case 1:pendiente += PLAZO;18040case 2:pendiente += PLAZO;36080case 3:pendiente += PLAZO; break;54120case 4:break;default:pendiente = -1;entra aquísale54120 / 100.0 → 541,20 €Sin break, cada case cae en el siguiente:aquí es justo lo que queremos.
Se entra por el caso de los plazos pagados y se baja sumando hasta el break.

Con pagados = 1, Java salta directamente a case 1: y empieza a bajar: suma el plazo 2 (18040), cae en case 2: y suma el 3 (36080), cae en case 3: y suma el 4 (54120) y ahí encuentra el break. Comprobación: 3 × 18040 = 54120 céntimos, y 54120 / 100.0 = 541,20 €. Por último, una opción que no existe:

=== VENTANILLA DE SECRETARÍA ===
1. Pedir un certificado
2. Cambiar de grupo
3. Ver lo que queda por pagar
0. Salir
Trámite: 7
Trámite no válido: 7

Preguntas que te puedes hacer

¿Por qué los plazos en céntimos (int) y el certificado en double? Porque los plazos se suman: tres sumas de 180.40 en double pueden dejar restos como 541.1999999…, que printf taparía pero que estarían ahí. En céntimos todo es exacto y solo se divide entre 100.0 al imprimir. El precio del certificado no se suma con nada: se imprime tal cual, así que el double no da problemas.

¿Por qué default -> -1 en el switch del precio? Porque un switch que devuelve un valor tiene que devolver algo en todos los casos. Si quitas el default, javac se queja: con un String puede llegar cualquier texto. El -1 es una marca imposible para un precio, y el if (precio < 0) de después la detecta.

¿Por qué toLowerCase() al leer el formato? Porque el switch con String compara con equals(), que distingue mayúsculas. Sin toLowerCase(), Papel no entraría en case "papel" y acabaría en el default. Pasar todo a minúsculas al leer es más corto que poner case "papel", "Papel", "PAPEL".

¿Por qué se descarta antes el grupo no válido que la norma? Por el orden de la cadena: primero lo imposible, después la regla. Si la norma fuera primero, alguien que escribiera «noche» con un 8 de media recibiría un «Cambio concedido» a un grupo que no existe. Fíjate además en el !actual.equals("mañana") && !actual.equals("tarde"): «no es mañana y no es tarde». Con || sería siempre verdadero (nada puede ser mañana y tarde a la vez), un error que verás en los ejercicios.

¿Y el case 4: break; vacío? Sin él, pagados = 4 caería en el default y pondría pendiente = -1, como si el dato fuera imposible. Ese break dice «aquí no hay nada que sumar, pero el dato es válido».

Olvidar un break en un switch clásico cuando no quieres fall-through. Compila sin avisos y los casos se pisan unos a otros: verás uno en los ejercicios con los días de préstamo de la biblioteca. Si no necesitas que los casos se acumulen, usa las flechas: con -> el fall-through no existe.

javac -Xlint:fallthrough Ventanilla.java avisa de cada case que cae en el siguiente. Aquí lo hacemos a propósito, así que el aviso sobra, pero en un programa tuyo te puede ahorrar una tarde.

Pruébalo tú

  • Añade el formato "apostillado" a 21,00 € y comprueba que Apostillado con mayúscula también funciona.
  • Cambia el switch de los plazos para que en lugar de sumar, cuente cuántos plazos faltan con la misma idea de caer de un case al siguiente.

Programa 3: Adivina el número con tres intentos (sin bucles)

El clásico: el programa piensa un número entre 1 y 100 y tú tienes tres intentos. En cada fallo te dice si es más alto o más bajo; si te sales del rango, pierdes el intento. Lo que lo complica: no podemos usar bucles, así que el mismo bloque de código se copia tres veces. Al terminar vas a odiar ese copiar y pegar, y eso es exactamente lo que queremos.

La idea

  • El número secreto sale de Random con una semilla fija (42): con la misma semilla, nextInt(100) + 1 da siempre el mismo número. Así se puede probar y las salidas de este artículo son repetibles. En un juego de verdad se quita la semilla y cambia en cada partida.
  • Cada intento es una cadena if/else if/else con cuatro caminos: fuera de rango, acierto, más alto o más bajo.
  • Si aciertas, return; termina main en ese mismo instante. Así no hace falta meter el intento 2 dentro del else del intento 1 (y el 3 dentro del 2): el código se queda plano.

Antes de programar, ordena las condiciones: ¿qué tiene que comprobarse primero, el rango o el acierto? Si el rango fuera después, un 150 caería en «Más bajo» y el jugador no sabría que se ha salido.

El código

import java.util.Random;
import java.util.Scanner;

/**
 * Adivina el número en 3 intentos, SIN bucles: el mismo bloque copiado tres veces.
 * Cuando acabes, cuenta las líneas repetidas: por eso existen los bucles.
 */
public class Adivina {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        // Semilla fija (42): el número secreto sale siempre igual, así se puede probar. Sin semilla, cambia cada vez.
        Random azar = new Random(42);
        int secreto = azar.nextInt(100) + 1;       // nextInt(100) da de 0 a 99; con +1, de 1 a 100

        System.out.println("=== ADIVINA EL NÚMERO (1-100) ===");
        System.out.println("Tienes 3 intentos.");

        // ---- Intento 1
        System.out.print("Intento 1: ");
        int n = sc.nextInt();
        if (n < 1 || n > 100) {
            System.out.println("Eso no está entre 1 y 100: pierdes el intento.");
        } else if (n == secreto) {
            System.out.println("¡A la primera! Era el " + secreto + ".");
            return;                                // return en main: el programa termina aquí
        } else if (n < secreto) {
            System.out.println("Más alto.");
        } else {
            System.out.println("Más bajo.");
        }

        // ---- Intento 2: el mismo bloque, otra vez
        System.out.print("Intento 2: ");
        n = sc.nextInt();
        if (n < 1 || n > 100) {
            System.out.println("Eso no está entre 1 y 100: pierdes el intento.");
        } else if (n == secreto) {
            System.out.println("¡A la segunda! Era el " + secreto + ".");
            return;
        } else if (n < secreto) {
            System.out.println("Más alto.");
        } else {
            System.out.println("Más bajo.");
        }

        // ---- Intento 3: el último ya no da pistas
        System.out.print("Intento 3: ");
        n = sc.nextInt();
        if (n == secreto) {
            System.out.println("¡Por los pelos! Era el " + secreto + ".");
        } else {
            System.out.println("Sin intentos. El número era el " + secreto + ".");
        }
        sc.close();
    }
}

Salida

El secreto con la semilla 42 resulta ser el 31. Una partida con búsqueda «por la mitad»:

=== ADIVINA EL NÚMERO (1-100) ===
Tienes 3 intentos.
Intento 1: 50
Más bajo.
Intento 2: 25
Más alto.
Intento 3: 31
¡Por los pelos! Era el 31.

Y una partida desastrosa, con un número fuera de rango en medio:

=== ADIVINA EL NÚMERO (1-100) ===
Tienes 3 intentos.
Intento 1: 50
Más bajo.
Intento 2: 150
Eso no está entre 1 y 100: pierdes el intento.
Intento 3: 20
Sin intentos. El número era el 31.

El dibujo enseña qué condición gana en cada intento de la primera partida:

Tres intentos sin bucle: tres copias del mismo ifCon secreto 31: el intento 1 (50) entra en el else y dice Más bajo; el 2 (25) entra en n < secreto y dice Más alto; el 3 (31) acierta. Cada intento repite el mismo bloque de condiciones.Secreto: 31. El mismo bloque, copiado tres vecesIntento 1: 50n < 1 || n > 100n == secreton < secretoelseMás bajo.Intento 2: 25n < 1 || n > 100n == secreton < secretoelseMás alto.Intento 3: 31n == secretoelseAcierto≈ 12 líneas por intento: con 10 intentos serían 120. En Bucles, un while lo hace en 10.
En coral, la condición que gana en cada intento. El código es el mismo tres veces: eso es un bucle esperando a nacer.

Intento 1: 50 está en rango, no es 31 y no es menor que 31, así que cae en el else: «Más bajo». Intento 2: 25 < 31, «Más alto». Intento 3: 31 == 31.

Preguntas que te puedes hacer

¿Por qué nextInt(100) + 1? nextInt(100) devuelve un número de 0 a 99 (el 100 queda fuera, igual que el final de range() en Python). Para que vaya de 1 a 100 se suma 1 a todo: el 0 pasa a ser el 1 y el 99, el 100.

¿Qué hace return; dentro de main? Termina el método en ese punto. Como main es el programa entero, el programa acaba: el intento 2 no llega a ejecutarse. Sin ese return, tras acertar a la primera el programa seguiría pidiendo el intento 2.

¿Por qué el intento 3 no comprueba el rango? Porque ya no hay pista que dar: si no acierta, se acaba igual esté dentro o fuera del rango. Menos ramas, menos que leer.

¿Por qué n se declara una vez y luego solo se reutiliza? Porque int n en el intento 2 daría error: ya existe una variable n en el mismo bloque. La segunda vez solo se le da un valor nuevo: n = sc.nextInt();.

¿Y si quiero diez intentos? Cuenta: unas 12 líneas por intento, así que 120 líneas casi iguales. Y si un día cambias el mensaje «Más alto.», lo cambias diez veces. Eso es justo lo que resuelven los bucles en Java: el mismo juego con un while cabe en una pantalla.

Poner la comprobación del rango después de las pistas: if (n == secreto) … else if (n < secreto) … else if (n > secreto) … else if (n < 1 || n > 100) …. Esa última rama no se ejecuta nunca: cualquier número distinto del secreto es menor o mayor que él, así que un 150 se queda en «Más bajo» y un -3 en «Más alto». La primera condición verdadera gana: lo más restrictivo, primero.

Random sin semilla (new Random()) usa una semilla distinta en cada ejecución. Con semilla, la secuencia es siempre la misma en cualquier ordenador con Java: por eso tu 42 también dará 31.

Pruébalo tú

  • Cambia la semilla a 7 y juega sin mirar el código: ¿cuál es el secreto? Compruébalo después imprimiéndolo al principio.
  • Añade una pista de «¡caliente!» cuando te quedes a 5 o menos del secreto (pista: Math.abs(n - secreto) <= 5). ¿En cuántos sitios tienes que añadirla?

Errores típicos en esta práctica

  • Comparar String con ==. Compila y da false con textos leídos del teclado aunque sean iguales. Siempre equals() o equalsIgnoreCase().
  • Usar una cadena else if cuando las comprobaciones son independientes. Solo se ejecuta una rama, así que dejas de comprobar el resto. Para validar varios datos, un if por dato.
  • Ordenar mal las condiciones. La primera verdadera gana: lo imposible (fuera de rango, formato desconocido) va antes que las reglas.
  • Olvidar el break en un switch clásico. Los casos se acumulan sin avisar. Si no lo buscas a propósito, usa las flechas.
  • Usar el dato antes de validarlo. substring(0, arroba) con arroba = -1 revienta: primero compruebas, después operas.
  • Teclear 7.4 en un equipo en español. nextDouble() lanza InputMismatchException: lo viste en la teoría y aquí se tecleó con coma.

Míralo paso a paso en Python Tutor

Python Tutor (pythontutor.com) ejecuta el código línea a línea y dibuja las variables y los objetos con sus flechas. Elige Java en el desplegable y pega este programa, que junta el fall-through de los plazos y las dos contraseñas (sin Scanner: Python Tutor no puede teclear, así que los datos van fijos y las contraseñas se crean con new String para que sean dos objetos, como con nextLine()).

public class TutorPlazos {
    public static void main(String[] args) {
        int pagados = 1;
        int pendiente = 0;
        switch (pagados) {
            case 0:
                pendiente += 18040;
            case 1:
                pendiente += 18040;
            case 2:
                pendiente += 18040;
            case 3:
                pendiente += 18040;
                break;
            default:
                pendiente = -1;
        }
        String clave = new String("fp2-Laura26");
        String repetida = new String("fp2-Laura26");
        boolean mismoObjeto = clave == repetida;
        boolean mismasLetras = clave.equals(repetida);
        System.out.println(pendiente + " " + mismoObjeto + " " + mismasLetras);
    }
}

Salida:

54120 false true

Qué observar paso a paso:

  1. Al llegar al switch, la flecha roja salta directamente a case 1:: no pasa por el case 0: ni lo evalúa.
  2. Tras pendiente += 18040 no sale del switch: la siguiente línea es la de case 2:. Mira cómo pendiente pasa por 18040, 36080 y 54120.
  3. En el break salta fuera del switch, sin tocar default.
  4. Al crear clave y repetida, Python Tutor dibuja dos cajas de String en el montón, cada una con su flecha: es el dibujo de esta práctica.
  5. Fíjate en los valores: mismoObjeto es false y mismasLetras es true.

Resumen y siguiente paso

  • Para validar varios datos, un if por dato y una variable todoBien que empieza en true: así enseñas todos los fallos de una vez.
  • Los textos se comparan con equals() (o equalsIgnoreCase()); == compara si son el mismo objeto.
  • En una cadena else if, la primera condición verdadera gana: lo imposible y lo más restrictivo van primero.
  • Las tres formas del switch: flechas para hacer cosas distintas, expresión para calcular un valor (con default obligatorio) y clásico sin break cuando los casos se acumulan a propósito.
  • return; en main termina el programa y evita anidar un bloque dentro de otro.
  • Copiar el mismo bloque tres veces funciona, pero no escala: para eso están los bucles.

Checklist: ¿lo has practicado?

  • ¿Has ejecutado los tres programas y comprobado las salidas?
  • ¿Sabes explicar por qué == dice que dos contraseñas iguales no coinciden?
  • ¿Sabrías decir, sin ejecutar, qué imprime la ventanilla con 0 plazos pagados?
  • ¿Sabes cuándo usar seis if sueltos y cuándo una cadena else if?
  • ¿Has intentado los «Pruébalo tú» sin mirar la teoría?

Si has respondido que sí a casi todo, estás listo para los ejercicios de control de flujo en Java, donde ya no te doy el código hecho. ¡Nos vemos allí!

Siguiente paso: ejerciciosControl de flujo en Java — ejercicios con la nota de acceso, la biblioteca y un cajero de tres intentos (y el 9,5 que salía «Suficiente»)

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →