La entrada y salida en Java es uno de esos temas que al principio parece sencillo, pero que termina siendo clave para construir programas útiles de verdad. Un programa que no recibe datos, no muestra resultados y no guarda información se queda en algo muy limitado. En cuanto empezamos a pedir datos al usuario, imprimir resultados, leer ficheros, guardar registros o crear una pequeña ventana con botones, ya estamos trabajando con entrada y salida en Java.
Cuando explico este tema en programación, me gusta partir de una idea muy simple: un programa no vive aislado. Necesita comunicarse con el exterior. A veces esa comunicación se hace con la consola, otras veces con un fichero y otras mediante una interfaz gráfica. La lógica es siempre parecida: entra información, el programa la procesa y después produce una salida.
En esta guía vamos a ver entrada y salida en Java desde una perspectiva práctica: consola, teclado, formato de salida, ficheros de texto, rutas, excepciones, interfaces gráficas simples y eventos. La idea no es memorizar clases sueltas, sino entender cuándo usar cada herramienta y qué errores conviene evitar.
Qué significa entrada y salida en programación
La entrada es la información que recibe un programa para poder trabajar. Puede venir del teclado, de un ratón, de una pantalla táctil, de un fichero, de una base de datos, de una red o de una interfaz gráfica. La salida, en cambio, es la información que el programa produce, muestra, guarda o envía.
Por ejemplo, una aplicación de calificaciones puede recibir como entrada el nombre de un alumno y su nota. Después puede calcular una media y mostrarla por pantalla. Si además guarda esos datos en un fichero para recuperarlos más adelante, entonces ya estamos trabajando con una entrada y salida en Java bastante completa: teclado, consola, procesamiento y persistencia.
En programación suele usarse la abreviatura E/S para hablar de entrada/salida. En Java, esta entrada y salida puede aparecer en muchos niveles. En programas sencillos usamos Scanner para leer desde teclado y System.out.println para mostrar mensajes. En programas algo más completos usamos Files, Path, BufferedReader o BufferedWriter para leer y escribir ficheros. Y cuando empezamos con interfaces gráficas, entran en juego clases como JFrame, JTextField, JButton, JLabel o JOptionPane.
Lo importante es no ver cada parte como algo separado. La entrada y salida en Java es un hilo común que conecta todo: consola, ficheros e interfaces. Si el usuario escribe algo en un campo de texto, eso es entrada. Si el programa muestra una etiqueta con el resultado, eso es salida. Si se guarda un registro en un archivo, también es salida. Y si más tarde se lee ese archivo, vuelve a ser entrada.
En mi caso, cuando trabajo estos conceptos con alumnado de FP Informática, intento que no se queden solo con “este método sirve para esto”. Prefiero que entiendan el flujo completo: qué dato entra, cómo lo representamos, dónde lo guardamos, cómo lo recuperamos y cómo se lo mostramos al usuario de forma clara.
Salida por consola en Java
La salida por consola es la forma más directa de mostrar información en un programa Java. Aunque pueda parecer básica, es fundamental. De hecho, antes de crear ventanas, botones o ficheros, conviene dominar bien cómo se muestran mensajes, resultados, errores e instrucciones.
En Java, lo más habitual es usar System.out.print, System.out.println y System.out.printf.
print muestra texto sin saltar a la línea siguiente. Es útil cuando queremos pedir un dato y dejar el cursor en la misma línea:
System.out.print("Introduce tu edad: ");
println muestra texto y añade un salto de línea al final:
System.out.println("Programa finalizado");
printf permite mostrar datos con formato, por ejemplo decimales con dos cifras o columnas alineadas:
System.out.printf("Nota final: %.2f%n", 8.75);
Una parte importante de la entrada y salida en Java es que la salida no sea solo correcta, sino también comprensible. No es lo mismo mostrar esto:
System.out.println(nombre + edad + nota);
que mostrar algo como:
System.out.println("Alumno: " + nombre);
System.out.println("Edad: " + edad + " años");
System.out.printf("Nota final: %.2f%n", nota);
El segundo ejemplo se entiende mucho mejor. Y eso importa. En programación no solo escribimos código para que la máquina lo ejecute; también lo escribimos para que otras personas lo puedan leer, probar y mantener.
Diferencia entre print, println y printf
La diferencia básica es esta:
| Método | Qué hace | Cuándo usarlo |
|---|---|---|
print | Escribe sin salto de línea final | Para pedir datos en la misma línea |
println | Escribe y baja a la línea siguiente | Para mensajes completos |
printf | Escribe usando formato | Para decimales, columnas, importes o tablas |
En una guía sobre entrada y salida en Java, printf merece especial atención porque ayuda mucho cuando queremos mostrar datos de forma profesional. Por ejemplo, si tenemos una tabla de productos, podemos alinear columnas y controlar los decimales:
System.out.printf("%-15s %8s %10s%n", "Producto", "Unid.", "Precio");
System.out.printf("%-15s %8d %10.2f%n", "Ratón", 2, 12.50);
System.out.printf("%-15s %8d %10.2f%n", "Monitor", 1, 149.99);
System.out.printf("%-15s %8d %10.2f%n", "Cable HDMI", 3, 7.25);
Aquí aparecen varios especificadores de formato:
| Especificador | Significado |
|---|---|
%s | Texto |
%d | Número entero |
%f | Número decimal |
%.2f | Decimal con dos cifras |
%n | Salto de línea |
%-15s | Texto alineado a la izquierda en 15 caracteres |
%10.2f | Decimal alineado en 10 espacios con dos cifras |
Este tipo de salida es especialmente útil cuando el programa tiene que mostrar listados, importes, notas, totales o resultados tabulados.
String.format para reutilizar textos formateados
String.format funciona de forma parecida a printf, pero en lugar de imprimir directamente, devuelve una cadena. Esto viene muy bien cuando queremos preparar un texto antes de mostrarlo, guardarlo en un fichero o colocarlo en una interfaz gráfica.
String linea = String.format("%-10s %.2f euros", "Total", 45.90);
System.out.println(linea);
Este detalle conecta muy bien con la entrada y salida en Java porque no siempre queremos mostrar la salida en consola. A veces queremos generar una línea ya formateada para escribirla en un fichero o para mostrarla en una etiqueta de Swing.
Entrada por teclado con Scanner
Para leer datos desde teclado, una de las clases más usadas en Java es Scanner. Se crea un objeto asociado a System.in y después se utilizan métodos como nextLine, nextInt, nextDouble o nextBoolean.
Ejemplo básico:
import java.util.Scanner;
public class EntradaBasica {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Introduce tu nombre: ");
String nombre = sc.nextLine();
System.out.print("Introduce tu edad: ");
int edad = sc.nextInt();
System.out.println("Hola, " + nombre + ". Tienes " + edad + " años.");
sc.close();
}
}
Este programa trabaja con entrada y salida en Java de forma muy clara. Primero muestra mensajes para pedir datos. Después recibe información del usuario. Finalmente muestra una respuesta personalizada.
Los métodos más habituales de Scanner son:
| Método | Qué lee | Ejemplo |
|---|---|---|
nextLine() | Una línea completa | Nombre completo |
next() | Una palabra hasta el primer espacio | Usuario |
nextInt() | Un entero | Edad |
nextDouble() | Un decimal | Precio |
nextBoolean() | true o false | Confirmación lógica |
Ahora bien, aquí aparece uno de los errores clásicos de entrada y salida en Java: mezclar métodos numéricos con nextLine.
El problema de nextLine después de nextInt
Cuando usamos nextInt o nextDouble, el número se lee correctamente, pero el salto de línea que queda al pulsar Enter puede quedarse pendiente. Si después llamamos a nextLine, ese salto se consume como si fuera una línea vacía.
Ejemplo del problema típico:
Scanner sc = new Scanner(System.in);
System.out.print("Edad: ");
int edad = sc.nextInt();
System.out.print("Nombre completo: ");
String nombre = sc.nextLine();
En este caso, nombre puede quedar vacío porque nextLine recoge el salto de línea pendiente.
La solución clásica es consumir ese salto antes de leer el texto:
Scanner sc = new Scanner(System.in);
System.out.print("Edad: ");
int edad = sc.nextInt();
sc.nextLine();
System.out.print("Nombre completo: ");
String nombre = sc.nextLine();
En clase suelo insistir mucho en este punto porque no es un error “raro”: aparece constantemente cuando se empieza a trabajar con entrada por teclado. Entenderlo ayuda a perderle miedo a Scanner y a escribir programas más previsibles.
Leer todo como texto y convertir después
Una alternativa muy recomendable consiste en leer siempre líneas completas con nextLine y convertir después al tipo que necesitemos. Este enfoque facilita la validación y el control de errores.
Scanner sc = new Scanner(System.in);
System.out.print("Introduce una edad: ");
String textoEdad = sc.nextLine();
int edad = Integer.parseInt(textoEdad);
System.out.println("Edad registrada: " + edad);
El problema es que Integer.parseInt puede lanzar una NumberFormatException si el usuario escribe algo que no se puede convertir en número. Por eso conviene combinar este enfoque con try-catch.
Scanner sc = new Scanner(System.in);
int edad = -1;
boolean datoValido = false;
while (!datoValido) {
try {
System.out.print("Introduce una edad entre 0 y 120: ");
edad = Integer.parseInt(sc.nextLine());
if (edad >= 0 && edad <= 120) {
datoValido = true;
} else {
System.out.println("La edad debe estar entre 0 y 120.");
}
} catch (NumberFormatException e) {
System.out.println("Debes introducir un número entero.");
}
}
System.out.println("Edad aceptada: " + edad);
Esta forma de trabajar resume una buena práctica esencial en entrada y salida en Java: no confiar ciegamente en que el usuario escribirá bien los datos. Un programa robusto valida antes de usar la información.
Librerías y clases de entrada/salida en Java
Java ofrece muchas herramientas para trabajar con entrada y salida. No se usa la misma clase para leer una línea de teclado, recorrer un directorio, escribir un fichero de texto o leer bytes de una imagen.
Por eso conviene tener una visión general:
| Necesidad | Clases o herramientas habituales |
|---|---|
| Leer teclado de forma sencilla | Scanner, BufferedReader |
| Mostrar texto por consola | System.out.print, println, printf |
| Leer y escribir ficheros de texto | Files, Path, BufferedReader, BufferedWriter |
| Leer y escribir bytes | InputStream, OutputStream, Files.readAllBytes |
| Trabajar con rutas y directorios | Path, Paths, Files |
| Crear ventanas simples | JOptionPane, JFrame, JPanel, JLabel, JTextField, JButton |
| Responder a acciones del usuario | ActionListener, eventos de Swing |
Cuando has trabajado en proyectos reales, te das cuenta de que elegir bien la herramienta importa. No todo se resuelve con Scanner, ni todo se resuelve con Files.readString. Cada clase tiene su contexto.
Flujos de bytes y flujos de caracteres
Un flujo es una secuencia de datos que entra o sale del programa. En Java se suele distinguir entre flujos de bytes y flujos de caracteres.
Los flujos de bytes trabajan con datos binarios: imágenes, audio, PDF, ficheros comprimidos o cualquier archivo que no sea texto plano. Aquí aparecen clases como InputStream y OutputStream.
Los flujos de caracteres trabajan con texto: archivos .txt, .csv, logs o configuraciones. Aquí entran clases como Reader, Writer, BufferedReader y BufferedWriter.
Un fichero de texto también se guarda físicamente como bytes, pero Java puede interpretarlo como caracteres usando una codificación como UTF-8. Esta idea es importante porque ayuda a entender por qué no todos los ficheros se leen igual.
Files y Path
Para trabajar con rutas y ficheros, las clases Path y Files son especialmente cómodas.
Path representa una ruta:
Path ruta = Path.of("datos/alumnos.txt");
Files permite hacer operaciones sobre esa ruta:
if (Files.exists(ruta)) {
System.out.println("El fichero existe.");
} else {
System.out.println("El fichero no existe.");
}
La entrada y salida en Java se vuelve mucho más práctica cuando entiendes que Path representa dónde está el fichero y Files permite actuar sobre él: leer, escribir, copiar, mover, borrar o comprobar si existe.
Ficheros en Java: datos, registros y rutas
Un fichero permite almacenar información de forma persistente. Persistente significa que los datos no se pierden cuando el programa termina, siempre que se hayan guardado correctamente.
Esto marca una diferencia enorme. Un programa que solo usa variables pierde la información al cerrarse. En cambio, un programa que guarda datos en un fichero puede recuperarlos más tarde.
Por ejemplo:
Ana;1DAW;8.5
Luis;1DAW;6.75
Marta;1DAM;9.10
En este fichero, cada línea puede considerarse un registro. Dentro de cada registro hay varios campos separados por punto y coma.
| Concepto | Significado | Ejemplo |
|---|---|---|
| Fichero | Unidad de almacenamiento con nombre y ubicación | alumnos.txt |
| Dato | Valor individual | Ana, 8.5, 1DAW |
| Registro | Conjunto de datos relacionados | Ana;1DAW;8.5 |
| Campo | Cada parte del registro | nombre, grupo, nota |
Este formato es muy útil para empezar porque se entiende a simple vista. No hace falta una base de datos para practicar persistencia. Con un fichero de texto bien estructurado podemos trabajar lectura, escritura, validación, separación de campos y listados.
Rutas absolutas y rutas relativas
Una ruta indica dónde está un fichero o directorio. Puede ser absoluta o relativa.
Una ruta absoluta indica la ubicación completa desde la raíz del sistema:
C:\datos\alumnos.txt
/home/usuario/datos/alumnos.txt
Una ruta relativa se interpreta desde la carpeta de trabajo del programa:
datos/alumnos.txt
En ejercicios de programación, casi siempre recomiendo usar rutas relativas. Si usas una ruta absoluta de tu ordenador, el programa puede fallar en cuanto lo lleves a otro equipo. En cambio, una ruta relativa dentro del proyecto hace que el código sea más portable y fácil de entregar.
Este tipo de decisión parece pequeña, pero forma parte de escribir buen código. La entrada y salida en Java no consiste solo en leer y escribir: también consiste en organizar bien los datos para que el programa sea mantenible.
Cómo escribir información en ficheros Java
Escribir en un fichero significa enviar información desde el programa hacia un archivo. Esa escritura puede sobrescribir el contenido anterior o añadir información al final.
Para textos pequeños, Files.writeString es una opción sencilla:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class EscribirTexto {
public static void main(String[] args) {
Path ruta = Path.of("mensaje.txt");
try {
Files.writeString(ruta, "Hola desde Java");
System.out.println("Fichero escrito correctamente.");
} catch (IOException e) {
System.out.println("Error al escribir el fichero: " + e.getMessage());
}
}
}
Aquí aparece otro concepto importante: IOException. Las operaciones con ficheros pueden fallar. La ruta puede no existir, puede no haber permisos, el disco puede estar lleno o el fichero puede estar bloqueado. Por eso conviene controlar los errores.
En entrada y salida en Java, ignorar las excepciones suele terminar en programas frágiles. Un buen programa no solo funciona cuando todo va bien; también responde de forma comprensible cuando algo falla.
Escribir varias líneas
Si queremos escribir varias líneas, podemos usar una lista:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class EscribirLineas {
public static void main(String[] args) {
Path ruta = Path.of("alumnos.txt");
List<String> lineas = List.of(
"Ana;1DAW;8.5",
"Luis;1DAW;6.75",
"Marta;1DAM;9.10"
);
try {
Files.write(ruta, lineas);
System.out.println("Datos guardados.");
} catch (IOException e) {
System.out.println("No se pudieron guardar los datos.");
}
}
}
Este enfoque es cómodo para guardar colecciones sencillas. Cada elemento de la lista se convierte en una línea del fichero.
Añadir contenido al final del fichero
Muchas veces no queremos sobrescribir el fichero completo. Por ejemplo, si estamos registrando alumnos, productos o incidencias, lo normal es añadir un nuevo registro al final.
Para eso podemos usar StandardOpenOption.APPEND:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class AnadirLinea {
public static void main(String[] args) {
Path ruta = Path.of("alumnos.txt");
String nuevaLinea = "Carlos;1DAW;7.25" + System.lineSeparator();
try {
Files.writeString(
ruta,
nuevaLinea,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
);
System.out.println("Registro añadido.");
} catch (IOException e) {
System.out.println("Error al añadir datos.");
}
}
}
StandardOpenOption.CREATE permite crear el fichero si no existe. StandardOpenOption.APPEND añade el contenido al final. System.lineSeparator() añade el salto de línea adecuado para el sistema operativo.
Este ejemplo resume muy bien una parte práctica de la entrada y salida en Java: convertir objetos o datos en líneas de texto y guardarlas de forma ordenada.
Cerrar recursos con try-with-resources
Cuando trabajamos con recursos como ficheros, es importante cerrarlos. try-with-resources permite que Java cierre automáticamente el recurso al terminar el bloque.
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class EscrituraConBuffer {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("log.txt"))) {
bw.write("Inicio del programa");
bw.newLine();
bw.write("Proceso finalizado correctamente");
} catch (IOException e) {
System.out.println("Error de escritura: " + e.getMessage());
}
}
}
BufferedWriter es útil cuando queremos escribir línea a línea o trabajar con ficheros más grandes. Files.writeString es muy cómodo para textos pequeños, pero BufferedWriter da más control.
Cómo leer información desde ficheros Java
Leer un fichero significa recuperar información almacenada para usarla en el programa. La lectura puede hacerse completa, línea a línea o mediante otros métodos según el tamaño y el formato.
Para leer todo el contenido como texto:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class LeerTexto {
public static void main(String[] args) {
Path ruta = Path.of("mensaje.txt");
try {
String contenido = Files.readString(ruta);
System.out.println(contenido);
} catch (IOException e) {
System.out.println("No se pudo leer el fichero.");
}
}
}
Este enfoque es cómodo cuando el fichero es pequeño, por ejemplo una configuración sencilla o un mensaje.
Leer línea a línea
Cuando cada línea representa un registro, es muy habitual leer línea a línea:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class LeerLineas {
public static void main(String[] args) {
Path ruta = Path.of("alumnos.txt");
try {
List<String> lineas = Files.readAllLines(ruta);
for (String linea : lineas) {
System.out.println(linea);
}
} catch (IOException e) {
System.out.println("Error al leer alumnos.txt");
}
}
}
Este ejemplo es perfecto para entender la entrada y salida en Java con ficheros de texto. El fichero entra en el programa como una lista de líneas. Después, cada línea puede procesarse, mostrarse, validarse o transformarse.
Separar campos de un registro con split
Si cada línea contiene campos separados por un carácter, podemos usar split.
String linea = "Ana;1DAW;8.5";
String[] campos = linea.split(";");
String nombre = campos[0];
String grupo = campos[1];
double nota = Double.parseDouble(campos[2]);
System.out.println(nombre + " - " + grupo + " - " + nota);
Aquí hay que tener cuidado. Antes de acceder a campos[0], campos[1] o campos[2], conviene comprobar que la línea tiene el número esperado de campos. Si no lo hacemos, puede aparecer un ArrayIndexOutOfBoundsException.
Una versión más segura sería:
String linea = "Ana;1DAW;8.5";
String[] campos = linea.split(";");
if (campos.length == 3) {
String nombre = campos[0];
String grupo = campos[1];
double nota = Double.parseDouble(campos[2]);
System.out.printf("%s - Grupo: %s - Nota: %.2f%n", nombre, grupo, nota);
} else {
System.out.println("Registro con formato incorrecto.");
}
Esta forma de pensar viene mucho de la práctica profesional: no basta con que el caso ideal funcione. Hay que pensar también en datos incompletos, líneas mal escritas y errores previsibles.
Lectura con BufferedReader
BufferedReader permite leer texto de forma eficiente, normalmente línea a línea:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class LecturaConBuffer {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("alumnos.txt"))) {
String linea;
while ((linea = br.readLine()) != null) {
System.out.println(linea);
}
} catch (IOException e) {
System.out.println("Error de lectura: " + e.getMessage());
}
}
}
Este patrón es muy común: abrir recurso, leer mientras haya líneas, procesar cada línea y cerrar automáticamente con try-with-resources.
Métodos de acceso y formatos de fichero
El acceso al contenido de los ficheros puede hacerse de varias maneras. En proyectos iniciales, lo más habitual es usar acceso secuencial.
El acceso secuencial consiste en leer o escribir desde el principio hacia el final. Por ejemplo, procesar un fichero de alumnos línea a línea.
También existe la carga completa en memoria, donde todo el contenido se lee como una cadena o como una lista. Es útil para ficheros pequeños.
El acceso aleatorio permite ir directamente a una posición concreta, aunque suele usarse más en ficheros binarios estructurados o escenarios más avanzados.
Y el acceso por registros consiste en tratar cada línea o bloque como una entidad. Por ejemplo, una línea por producto en un CSV.
| Método de acceso | Descripción | Ejemplo |
|---|---|---|
| Secuencial | Se lee o escribe de principio a fin | Procesar alumnos línea a línea |
| Completo en memoria | Se carga todo el contenido | Leer una configuración pequeña |
| Aleatorio | Se accede a una posición concreta | Modificar un registro estructurado |
| Por registros | Cada línea representa una entidad | Una línea por producto en CSV |
En los primeros proyectos, recomiendo usar ficheros de texto con registros sencillos. Es una forma muy buena de practicar entrada y salida en Java antes de pasar a bases de datos.
Texto plano, CSV y ficheros binarios
No todos los ficheros tienen el mismo formato.
El texto plano es fácil de leer y modificar con cualquier editor, aunque puede ser poco estructurado si no definimos bien el formato.
El CSV es adecuado para tablas simples y compatible con hojas de cálculo. Eso sí, hay que controlar separadores, comillas y saltos de línea.
El binario puede ser más compacto y adecuado para ciertos datos, pero no se entiende directamente con un editor de texto.
Para empezar con entrada y salida en Java, el texto plano y los registros separados por punto y coma son una opción muy clara.
Crear, copiar, mover y borrar ficheros en Java
Además de leer y escribir, un programa puede comprobar si existe una ruta, crear directorios, listar contenidos, copiar, mover o eliminar ficheros.
Por ejemplo, para crear una carpeta:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class CrearDirectorio {
public static void main(String[] args) {
Path carpeta = Path.of("datos");
try {
Files.createDirectories(carpeta);
System.out.println("Carpeta preparada.");
} catch (IOException e) {
System.out.println("No se pudo crear la carpeta.");
}
}
}
Para copiar un fichero:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class GestionFicheros {
public static void main(String[] args) {
Path origen = Path.of("datos/alumnos.txt");
Path copia = Path.of("copias/alumnos_copia.txt");
try {
Files.createDirectories(Path.of("copias"));
Files.copy(origen, copia, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copia realizada.");
} catch (IOException e) {
System.out.println("No se pudo copiar el fichero.");
}
}
}
Algunas operaciones habituales con Files son:
| Operación | Método habitual |
|---|---|
| Comprobar existencia | Files.exists(ruta) |
| Crear directorio | Files.createDirectories(ruta) |
| Copiar fichero | Files.copy(origen, destino) |
| Mover o renombrar | Files.move(origen, destino) |
| Borrar fichero | Files.delete(ruta) o Files.deleteIfExists(ruta) |
| Listar directorio | Files.list(ruta) |
Buenas prácticas al trabajar con ficheros
Estas son algunas buenas prácticas que conviene aplicar desde el principio:
- Usar rutas relativas en proyectos de clase siempre que sea posible.
- Comprobar si una carpeta existe antes de escribir dentro de ella.
- Controlar
IOExceptioncon mensajes comprensibles. - No borrar archivos sin confirmar o sin tener claro qué ruta se está usando.
- Separar los datos de prueba en una carpeta clara, por ejemplo
datos/. - Usar nombres de fichero descriptivos.
- No guardar contraseñas o datos sensibles en texto plano sin medidas de seguridad.
La entrada y salida en Java se aprende mucho mejor cuando se trabaja con proyectos pequeños pero completos. Por ejemplo, un registro de productos, una agenda de contactos, un listado de incidencias o un gestor de notas.
Caso práctico de consola: registro de productos
Vamos a ver una idea sencilla: un programa que permite introducir productos por consola y guardarlos en un fichero. Cada producto tendrá nombre, precio y unidades.
El fichero podría tener este formato:
Monitor;149.99;2
Teclado;19.99;5
Ratón;12.50;10
Primero podemos crear una clase Producto:
public class Producto {
private String nombre;
private double precio;
private int unidades;
public Producto(String nombre, double precio, int unidades) {
this.nombre = nombre;
this.precio = precio;
this.unidades = unidades;
}
public double calcularTotal() {
return precio * unidades;
}
public String toLineaFichero() {
return nombre + ";" + precio + ";" + unidades;
}
@Override
public String toString() {
return String.format("%s - %.2f euros x %d = %.2f euros",
nombre, precio, unidades, calcularTotal());
}
}
Después, un repositorio sencillo para guardar:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class RepositorioProductos {
private static final Path RUTA = Path.of("datos/productos.txt");
public static void guardar(Producto producto) {
try {
Files.createDirectories(RUTA.getParent());
Files.writeString(
RUTA,
producto.toLineaFichero() + System.lineSeparator(),
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
);
} catch (IOException e) {
System.out.println("No se pudo guardar el producto.");
}
}
}
Y un programa principal:
import java.util.Scanner;
public class AppProductos {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Nombre del producto: ");
String nombre = sc.nextLine();
System.out.print("Precio: ");
double precio = Double.parseDouble(sc.nextLine());
System.out.print("Unidades: ");
int unidades = Integer.parseInt(sc.nextLine());
Producto producto = new Producto(nombre, precio, unidades);
RepositorioProductos.guardar(producto);
System.out.println("Producto guardado: " + producto);
sc.close();
}
}
Este ejemplo reúne muchas piezas de entrada y salida en Java: pide datos por teclado, convierte números, crea un objeto, genera una línea de fichero, crea la carpeta datos, añade el registro al final y muestra una salida por consola.
Como mejora, convendría validar que el precio no sea negativo, que las unidades sean mayores que cero, que el nombre no esté vacío y que el fichero se pueda leer para listar los productos guardados.
Interfaces gráficas simples en Java
Una interfaz gráfica de usuario permite interactuar con el programa mediante ventanas, botones, campos de texto, etiquetas, menús y otros componentes visuales. En lugar de escribir instrucciones en consola, el usuario realiza acciones sobre elementos gráficos.
En Java, una forma clásica de empezar con interfaces gráficas es Swing. Para una primera aproximación no hace falta crear aplicaciones visuales complejas. Basta con entender cuatro ideas: ventana, componentes, eventos y controladores.
| Elemento | Descripción |
|---|---|
| Ventana | Área principal donde se muestran componentes, normalmente JFrame |
| Componente | Elemento visual como etiqueta, botón o campo de texto |
| Contenedor | Elemento que agrupa componentes, como JPanel |
| Layout | Forma de colocar componentes |
| Evento | Acción que ocurre en la interfaz |
| Controlador | Código que se ejecuta cuando ocurre un evento |
La entrada y salida en Java también existe en una GUI. Un JTextField recoge entrada. Un JLabel muestra salida. Un JButton dispara una acción. Un ActionListener permite responder al clic del usuario.
JOptionPane: la interfaz gráfica más sencilla
JOptionPane permite mostrar cuadros de diálogo de forma rápida:
import javax.swing.JOptionPane;
public class SaludoGrafico {
public static void main(String[] args) {
String nombre = JOptionPane.showInputDialog("Introduce tu nombre:");
JOptionPane.showMessageDialog(null, "Hola, " + nombre);
}
}
Aquí la entrada se recoge con showInputDialog y la salida se muestra con showMessageDialog.
No sustituye a una interfaz completa, pero es una forma muy buena de introducir la entrada y salida gráfica.
Crear una ventana con JFrame
JFrame representa una ventana principal:
import javax.swing.JFrame;
public class VentanaBasica {
public static void main(String[] args) {
JFrame ventana = new JFrame("Mi primera ventana");
ventana.setSize(400, 250);
ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ventana.setLocationRelativeTo(null);
ventana.setVisible(true);
}
}
La llamada setVisible(true) debe aparecer cuando la ventana ya está preparada. Si no se llama, la ventana existe como objeto, pero no se muestra.
Componentes básicos de Swing
Una ventana puede incluir etiquetas, campos de texto y botones:
import javax.swing.*;
import java.awt.FlowLayout;
public class FormularioSimple {
public static void main(String[] args) {
JFrame ventana = new JFrame("Formulario");
ventana.setSize(350, 150);
ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ventana.setLayout(new FlowLayout());
JLabel etiqueta = new JLabel("Nombre:");
JTextField campoNombre = new JTextField(15);
JButton boton = new JButton("Saludar");
ventana.add(etiqueta);
ventana.add(campoNombre);
ventana.add(boton);
ventana.setLocationRelativeTo(null);
ventana.setVisible(true);
}
}
Este ejemplo todavía no hace nada al pulsar el botón, pero ya muestra la estructura básica de una interfaz.
Cuando he pasado de desarrollar software a explicarlo en el aula, una de las cosas que más he confirmado es que conviene construir las ideas poco a poco. Primero una ventana. Después componentes. Después eventos. Después validación. Si se intenta hacer todo a la vez, el código se vuelve una bola difícil de entender.
Eventos y controladores en Swing
Un evento es algo que ocurre durante la ejecución del programa y que puede requerir una respuesta. En una interfaz gráfica, los eventos más habituales son pulsar un botón, escribir en un campo, seleccionar una opción o cerrar una ventana.
El controlador de eventos es el código que se ejecuta cuando sucede ese evento. En Swing, para responder a un botón se usa con frecuencia un ActionListener.
import javax.swing.*;
import java.awt.FlowLayout;
public class EventoBoton {
public static void main(String[] args) {
JFrame ventana = new JFrame("Evento de botón");
ventana.setSize(350, 150);
ventana.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ventana.setLayout(new FlowLayout());
JTextField campoNombre = new JTextField(15);
JButton boton = new JButton("Saludar");
JLabel resultado = new JLabel("Esperando nombre...");
boton.addActionListener(e -> {
String nombre = campoNombre.getText();
resultado.setText("Hola, " + nombre);
});
ventana.add(new JLabel("Nombre:"));
ventana.add(campoNombre);
ventana.add(boton);
ventana.add(resultado);
ventana.setLocationRelativeTo(null);
ventana.setVisible(true);
}
}
Cuando el usuario pulsa el botón, se ejecuta el código que hay dentro de addActionListener. Ese código lee el texto del campo y actualiza la etiqueta de resultado.
Esto también es entrada y salida en Java: la entrada llega desde el campo de texto y la salida aparece en una etiqueta.
Separar la interfaz en una clase propia
Cuando la interfaz crece, no conviene escribir todo dentro del método main. Una solución sencilla es crear una clase propia para la ventana.
import javax.swing.*;
import java.awt.FlowLayout;
public class VentanaSaludo extends JFrame {
private JTextField campoNombre;
private JLabel resultado;
public VentanaSaludo() {
setTitle("Saludo");
setSize(350, 150);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new FlowLayout());
setLocationRelativeTo(null);
campoNombre = new JTextField(15);
JButton boton = new JButton("Saludar");
resultado = new JLabel("Introduce un nombre");
boton.addActionListener(e -> saludar());
add(new JLabel("Nombre:"));
add(campoNombre);
add(boton);
add(resultado);
}
private void saludar() {
String nombre = campoNombre.getText();
resultado.setText("Hola, " + nombre);
}
}
Y después:
public class AppVentana {
public static void main(String[] args) {
VentanaSaludo ventana = new VentanaSaludo();
ventana.setVisible(true);
}
}
Separar la creación de la interfaz ayuda a mantener el código más limpio. Además, permite explicar mejor qué parte crea la ventana, qué parte responde al evento y qué parte contiene la lógica.
Validación en interfaces gráficas
Una interfaz gráfica también necesita validación. Un campo de texto permite escribir cualquier cosa, aunque esperemos un número.
private void calcularDoble() {
try {
int numero = Integer.parseInt(campoNumero.getText());
int doble = numero * 2;
resultado.setText("Doble: " + doble);
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(
this,
"Introduce un número entero válido.",
"Error de entrada",
JOptionPane.ERROR_MESSAGE
);
}
}
La validación es uno de los puntos donde más se nota la diferencia entre un ejemplo que solo funciona en el caso ideal y un programa un poco más cuidado.
La entrada y salida en Java no termina cuando conseguimos leer un dato. También hay que comprobar que el dato tiene sentido, que se puede convertir y que el usuario recibe un mensaje claro si algo no va bien.
Caso práctico gráfico: calculadora de descuento
Un ejemplo sencillo de interfaz gráfica es una calculadora de precio final. Recibe un precio y un porcentaje de descuento, calcula el resultado y lo muestra.
import javax.swing.*;
import java.awt.GridLayout;
public class CalculadoraDescuentoGUI extends JFrame {
private JTextField campoPrecio;
private JTextField campoDescuento;
private JLabel resultado;
public CalculadoraDescuentoGUI() {
setTitle("Calculadora de descuento");
setSize(350, 200);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setLayout(new GridLayout(4, 2, 5, 5));
campoPrecio = new JTextField();
campoDescuento = new JTextField();
resultado = new JLabel("Resultado pendiente");
JButton botonCalcular = new JButton("Calcular");
botonCalcular.addActionListener(e -> calcular());
add(new JLabel("Precio:"));
add(campoPrecio);
add(new JLabel("Descuento (%):"));
add(campoDescuento);
add(botonCalcular);
add(resultado);
}
private void calcular() {
try {
double precio = Double.parseDouble(campoPrecio.getText());
double descuento = Double.parseDouble(campoDescuento.getText());
double finalPrecio = precio - (precio * descuento / 100);
resultado.setText(String.format("Final: %.2f euros", finalPrecio));
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(this, "Datos numéricos no válidos.");
}
}
public static void main(String[] args) {
new CalculadoraDescuentoGUI().setVisible(true);
}
}
Este ejemplo reúne varios conceptos:
| Parte | Responsabilidad |
|---|---|
JTextField | Recoger precio y descuento |
JButton | Lanzar el cálculo |
ActionListener | Controlar el evento |
JLabel | Mostrar el resultado |
try-catch | Evitar fallos si los datos no son válidos |
Es una buena muestra de entrada y salida en Java aplicada a interfaces gráficas: entrada desde campos, procesamiento interno, salida en etiqueta y control de errores mediante mensajes.
Errores frecuentes al trabajar con entrada, salida, ficheros y GUI
Hay errores que se repiten mucho cuando se empieza con entrada y salida en Java. Conocerlos ahorra tiempo y frustración.
| Error frecuente | Consecuencia | Cómo evitarlo |
|---|---|---|
| Olvidar cerrar recursos de fichero | Puede quedar información sin guardar o recursos bloqueados | Usar try-with-resources |
| Usar rutas absolutas del ordenador propio | El programa falla en otros equipos | Usar rutas relativas dentro del proyecto |
No controlar IOException | El programa termina con error ante problemas de fichero | Usar try-catch y mensajes claros |
Mezclar nextInt y nextLine sin limpiar salto | Se leen cadenas vacías inesperadas | Consumir el salto con sc.nextLine() |
| No validar números en campos de texto | La interfaz falla si el usuario escribe letras | Usar parse y capturar NumberFormatException |
Escribir toda la GUI en main | Código difícil de mantener | Crear clases para las ventanas |
| No separar lógica y presentación | Aplicación rígida | Crear métodos para cálculos y dejar la ventana para interactuar |
De todos estos errores, hay dos que considero especialmente importantes: no validar y no separar responsabilidades.
No validar significa confiar demasiado en el usuario. Y el usuario puede escribir letras donde esperamos números, dejar campos vacíos, introducir notas fuera de rango o poner importes negativos.
No separar responsabilidades significa mezclarlo todo: lectura, cálculo, interfaz, guardado y mensajes en el mismo sitio. Al principio puede parecer más rápido, pero en cuanto el programa crece se vuelve difícil de mantener.
En mi experiencia, pensar con estructura es una de las habilidades que más ayuda al aprender programación. Analizar el problema, dividirlo en partes y construir soluciones mantenibles no es solo una buena práctica profesional; también facilita muchísimo el aprendizaje.
Ejemplo final: gestor de notas con consola, fichero e interfaz gráfica
Un buen mini proyecto para practicar entrada y salida en Java es un gestor de notas en fichero.
La idea sería desarrollar una aplicación sencilla que permita registrar alumnos y notas, guardar la información en un fichero y recuperar los datos para mostrarlos por pantalla o en una interfaz gráfica simple.
Requisitos básicos:
| Requisito | Descripción |
|---|---|
| Entrada | Nombre del alumno, grupo y nota |
| Salida | Listado de alumnos y nota media del grupo |
| Persistencia | Guardar datos en datos/notas.txt |
| Formato | nombre;grupo;nota |
| Validación | La nota debe estar entre 0 y 10 |
| Consola | Debe existir una versión con menú de texto |
| Interfaz gráfica | Debe existir una ventana simple para añadir y listar datos |
| Eventos | Los botones deben tener controladores asociados |
Este proyecto permite trabajar casi todo el tema: consola, formato de salida, librerías de entrada/salida, ficheros, métodos de acceso, interfaz gráfica, eventos y validación.
Una posible línea del fichero sería:
Ana;1DAW;8.5
El programa debería poder:
- Pedir nombre, grupo y nota.
- Validar que la nota esté entre 0 y 10.
- Guardar el registro en
datos/notas.txt. - Leer los registros guardados.
- Mostrar un listado claro.
- Calcular una media.
- Ofrecer una versión de consola.
- Ofrecer una ventana simple para añadir y listar.
Este tipo de proyecto es ideal porque conecta la teoría con algo tangible. No se queda en “aprender Scanner” o “aprender JFrame”. Obliga a unir piezas: entrada, salida, ficheros, validación y eventos.
Y eso es justo lo que debería conseguir una buena explicación de entrada y salida en Java: que la persona que aprende vea el sentido completo, no una colección de métodos sueltos.
Tabla rápida: qué usar según lo que quieras hacer
| Quiero hacer esto | Puedes usar |
|---|---|
| Mostrar un mensaje simple | System.out.println |
| Pedir un dato por consola | Scanner |
| Mostrar decimales con dos cifras | printf o String.format |
| Leer un fichero pequeño completo | Files.readString |
| Leer varias líneas | Files.readAllLines o BufferedReader |
| Escribir un texto pequeño | Files.writeString |
| Escribir varias líneas | Files.write |
| Añadir registros al final | Files.writeString con APPEND |
| Crear una carpeta | Files.createDirectories |
| Comprobar si existe un fichero | Files.exists |
| Crear una ventana simple | JFrame |
| Pedir texto en una GUI | JTextField o JOptionPane.showInputDialog |
| Mostrar un resultado en una GUI | JLabel o JOptionPane.showMessageDialog |
| Responder a un botón | ActionListener |
Conclusión
La entrada y salida en Java es mucho más que imprimir mensajes por consola. Es la base para que un programa pueda comunicarse con el usuario, recibir datos, mostrar resultados, guardar información y recuperarla más adelante.
Primero conviene dominar la consola: print, println, printf y Scanner. Después, hay que aprender a trabajar con ficheros mediante Path, Files, BufferedReader, BufferedWriter y control de excepciones. Finalmente, las interfaces gráficas simples permiten llevar esa misma lógica a ventanas, campos, botones, etiquetas y eventos.
Si tuviera que resumirlo en una idea, sería esta: aprender entrada y salida en Java es aprender a construir programas que interactúan con el mundo exterior. Programas que no solo calculan, sino que preguntan, responden, guardan, leen y ayudan al usuario a trabajar mejor.
La clave está en practicar con ejemplos pequeños pero completos: una calculadora, un registro de productos, una agenda, un listado de incidencias o un gestor de notas. Ahí es donde la entrada y salida en Java deja de ser teoría y empieza a convertirse en programación útil.
Preguntas frecuentes sobre entrada y salida en Java
¿Qué significa entrada y salida en Java?
La entrada es la información que recibe un programa Java, por ejemplo datos introducidos por teclado, información leída desde un fichero o texto escrito en un campo de una interfaz gráfica. La salida es la información que el programa muestra, guarda o comunica, por ejemplo un mensaje en consola, un registro escrito en un fichero o un resultado mostrado en una ventana.
¿Qué diferencia hay entre print, println y printf?
print escribe texto sin salto de línea final. println escribe texto y baja a la línea siguiente. printf permite mostrar texto con formato, por ejemplo decimales con dos cifras, columnas alineadas o importes.
¿Para qué sirve Scanner en Java?
Scanner sirve para leer datos desde una fuente de entrada, como el teclado. Permite leer líneas completas, palabras, números enteros, decimales o valores booleanos. Es una de las clases más usadas al empezar con entrada y salida en Java por consola.
¿Por qué falla nextLine después de nextInt?
Porque nextInt lee el número, pero puede dejar pendiente el salto de línea que se genera al pulsar Enter. Si después llamamos a nextLine, puede leer ese salto como una línea vacía. Para evitarlo, se puede añadir un sc.nextLine() después de nextInt o leer todo como texto y convertir después.
¿Qué clases se usan para leer y escribir ficheros en Java?
Para leer y escribir ficheros de texto en Java se usan habitualmente Files, Path, BufferedReader y BufferedWriter. Files y Path son muy cómodas para operaciones sencillas, mientras que BufferedReader y BufferedWriter son útiles para lectura y escritura línea a línea.
¿Qué es un registro dentro de un fichero?
Un registro es un conjunto de datos relacionados que representan una entidad. Por ejemplo, en la línea Ana;1DAW;8.5, el registro representa a una alumna con su nombre, grupo y nota. Cada parte del registro es un campo.
¿Qué diferencia hay entre ruta absoluta y ruta relativa?
Una ruta absoluta indica la ubicación completa del fichero desde la raíz del sistema. Una ruta relativa se interpreta desde la carpeta de trabajo del programa. En ejercicios y proyectos de clase suele ser mejor usar rutas relativas porque hacen que el proyecto sea más fácil de mover entre ordenadores.
¿Qué es una interfaz gráfica en Java?
Una interfaz gráfica permite interactuar con un programa mediante ventanas, botones, campos de texto, etiquetas y otros componentes visuales. En Java se puede empezar con Swing usando clases como JFrame, JPanel, JLabel, JTextField, JButton y JOptionPane.
¿Qué es un evento en Swing?
Un evento es una acción que ocurre en la interfaz, como pulsar un botón o escribir en un campo. Para responder a esos eventos se usan controladores, por ejemplo un ActionListener asociado a un JButton.
¿Por qué es importante validar datos en entrada y salida en Java?
Porque el usuario puede escribir datos incorrectos, dejar campos vacíos o introducir texto donde esperamos números. Validar evita errores y permite mostrar mensajes comprensibles. La validación es importante tanto en consola como en interfaces gráficas.


