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.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- Cómo leer del teclado con
Scanner:nextLine()para textos ynextInt()/nextDouble()para números, y por qué después de unnextInt()hace falta unnextLine()vacío (la trampa del buffer). - Que en un equipo en español
nextDouble()espera la coma: se teclea7,4, no7.4. - Cómo se encadenan
if,else ifyelse: 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:
- Leer todo primero, con la trampa del buffer resuelta justo después de
nextInt(). - Comprobar cada dato con su propio
if/else. No son ramas de una misma cadena: son seis preguntas independientes, así que van seisifseguidos. Una variabletodoBien, que empieza entrue, se pone afalseen cuanto algo falla. - 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:
| Dato | Valor | Condición | Resultado |
|---|---|---|---|
| Edad | 15 | 15 >= 16 && 15 <= 99 → false && … | fuera de rango |
| Correo | laura@ulpgc | punto = lastIndexOf('.') = -1, y -1 > 5 + 1 es false | no válido |
| Contraseña | laura | length() = 5, y 5 < 8 | demasiado corta (5 de 8) |
| Repetición | laura / Laura | equals distingue mayúsculas | no coincide |
| Normas | n | "n".equalsIgnoreCase("s") → false | sin 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é:
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 comoifsuelto o comoelse ifdel correo? - Cambia el
equalsde 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:
- El
switchcon flechas para el menú: cada trámite hace lo suyo y no cae en el siguiente. - El
switchcomo expresión para el precio del certificado: cada caso devuelve un valor y el resultado se guarda en una variable. - El
switchclásico sinbreak, 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:
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 queApostilladocon mayúscula también funciona. - Cambia el
switchde los plazos para que en lugar de sumar, cuente cuántos plazos faltan con la misma idea de caer de uncaseal 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
Randomcon una semilla fija (42): con la misma semilla,nextInt(100) + 1da 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/elsecon cuatro caminos: fuera de rango, acierto, más alto o más bajo. - Si aciertas,
return;terminamainen ese mismo instante. Así no hace falta meter el intento 2 dentro delelsedel 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:
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
Stringcon==. Compila y dafalsecon textos leídos del teclado aunque sean iguales. Siempreequals()oequalsIgnoreCase(). - Usar una cadena
else ifcuando las comprobaciones son independientes. Solo se ejecuta una rama, así que dejas de comprobar el resto. Para validar varios datos, unifpor dato. - Ordenar mal las condiciones. La primera verdadera gana: lo imposible (fuera de rango, formato desconocido) va antes que las reglas.
- Olvidar el
breaken unswitchclá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)conarroba = -1revienta: primero compruebas, después operas. - Teclear
7.4en un equipo en español.nextDouble()lanzaInputMismatchException: 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:
- Al llegar al
switch, la flecha roja salta directamente acase 1:: no pasa por elcase 0:ni lo evalúa. - Tras
pendiente += 18040no sale delswitch: la siguiente línea es la decase 2:. Mira cómopendientepasa por 18040, 36080 y 54120. - En el
breaksalta fuera delswitch, sin tocardefault. - Al crear
claveyrepetida, Python Tutor dibuja dos cajas deStringen el montón, cada una con su flecha: es el dibujo de esta práctica. - Fíjate en los valores:
mismoObjetoesfalseymismasLetrasestrue.
Resumen y siguiente paso
- Para validar varios datos, un
ifpor dato y una variabletodoBienque empieza entrue: así enseñas todos los fallos de una vez. - Los textos se comparan con
equals()(oequalsIgnoreCase());==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 (condefaultobligatorio) y clásico sinbreakcuando los casos se acumulan a propósito. return;enmaintermina 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
ifsueltos y cuándo una cadenaelse 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 →




Simulacro del examen práctico de IC2 — diagrama de flujo, fork e hilos en C (y las capturas que no pueden faltar)