Serializa un almacén a fichero con Serializable y transient, y recupéralo con readObject reconstruyendo el valor total calculado.
Actualizado el
Código de Examen Java Programación orientada a objetos Nivel examen Código probado

Una ferretería quiere guardar el estado de su almacén en un fichero de trabajo llamado almacen.dat, situado en el directorio de ejecución. Debes escribir el programa Main con dos clases: Producto (nombre, cantidad y precio unitario en céntimos, como enteros) y Almacen (lista de productos), ambas serializables.
Almacen mantiene además un campo valorTotal con la suma de cantidad * precio de todos los productos. Ese campo es una caché derivada y no debe escribirse en el fichero: decláralo transient y reconstrúyelo al cargar mediante un readObject personalizado. Define también serialVersionUID en las dos clases.
El programa lee órdenes de la entrada estándar, una por línea, hasta el final de la entrada. Parte de un almacén vacío. Las órdenes son: ALTA nombre cantidad precio, VENTA nombre cantidad, VALOR, GUARDAR, CARGAR y LISTAR. Los nombres no contienen espacios y las cantidades y precios son enteros no negativos.
ALTA crea el producto o, si ya existe, suma la cantidad y sustituye el precio por el nuevo; imprime ALTA nombre. VENTA resta unidades y, si todo va bien, imprime VENTA nombre restante; si el producto no existe imprime ERROR producto desconocido, y si no hay unidades suficientes imprime ERROR stock insuficiente, sin modificar nada en ambos casos.
VALOR imprime VALOR total. GUARDAR serializa el almacén completo en el fichero e imprime GUARDADO n, siendo n el número de productos distintos. CARGAR sustituye el almacén en memoria por el leído del fichero e imprime CARGADO n; si el fichero no existe imprime ERROR sin fichero y deja el almacén como estaba. LISTAR imprime una línea por producto, en orden de alta, con el formato nombre cantidad precio.
Al terminar, el programa debe borrar almacen.dat si existe. Una línea en blanco se ignora. Si la entrada está vacía no se imprime nada.
Entrada de ejemplo
ALTA tuerca 10 25
ALTA tornillo 4 100
VALOR
GUARDAR
VENTA tuerca 4
VALOR
CARGAR
VALOR
LISTARSalida esperada
ALTA tuerca
ALTA tornillo
VALOR 650
GUARDADO 2
VENTA tuerca 6
VALOR 550
CARGADO 2
VALOR 650
tuerca 10 25
tornillo 4 100- Clases Producto y Almacen correctamente declaradas como Serializable, con serialVersionUID
- 0,5
- Campo valorTotal marcado transient y mantenido de forma incremental en ALTA y VENTA
- 0,75
- Guardado y carga con ObjectOutputStream/ObjectInputStream, con try-with-resources
- 0,75
- readObject personalizado que reconstruye el campo transitorio tras la carga
- 0,75
- Salida exacta, errores de stock y de producto desconocido, y gestión de fichero inexistente
- 0,25
Pistas
Pista 1 · ¿Qué debe guardarse y qué no?
Piensa qué información es primaria y cuál se puede recalcular. La lista de productos es el estado real; valorTotal se deduce de ella. Un campo transient vuelve a su valor por defecto (0 en un long) al deserializar, así que alguien tiene que rellenarlo.
Pista 2 · ¿Cómo se engancha el código a la carga?
Una clase Serializable puede declarar un método privado private void readObject(ObjectInputStream in) que la JVM invoca por reflexión. Dentro, llama primero a in.defaultReadObject() para leer los campos no transitorios y después recalcula lo que falte.
Pista 3 · ¿Y la gestión de recursos y errores?
Abre los flujos con try-with-resources para que se cierren siempre. Antes de cargar, comprueba con Files.exists o captura NoSuchFileException/FileNotFoundException. Mantén valorTotal de forma incremental en ALTA y VENTA para no recorrer la lista en cada VALOR.
Solución
Solución explicada
La solución separa tres responsabilidades: el modelo (Producto y Almacen), la persistencia (guardar y cargar) y el intérprete de órdenes en main. La clave del ejercicio es que valorTotal está marcado transient: nunca viaja al fichero y, al leerlo de nuevo, readObject lo reconstruye a partir de la lista.
import java.io.*;
import java.nio.file.*;
import java.util.*;
public class Main {
static class Producto implements Serializable {
private static final long serialVersionUID = 1L;
final String nombre;
long cantidad;
long precio;
Producto(String nombre, long cantidad, long precio) {
this.nombre = nombre;
this.cantidad = cantidad;
this.precio = precio;
}
}
static class Almacen implements Serializable {
private static final long serialVersionUID = 1L;
private final List<Producto> productos = new ArrayList<>();
private transient long valorTotal = 0;
private Producto buscar(String nombre) {
for (Producto p : productos) {
if (p.nombre.equals(nombre)) return p;
}
return null;
}
void alta(String nombre, long cantidad, long precio) {
Producto p = buscar(nombre);
if (p == null) {
p = new Producto(nombre, cantidad, precio);
productos.add(p);
valorTotal += cantidad * precio;
} else {
valorTotal -= p.cantidad * p.precio;
p.cantidad += cantidad;
p.precio = precio;
valorTotal += p.cantidad * p.precio;
}
}
String venta(String nombre, long cantidad) {
Producto p = buscar(nombre);
if (p == null) return "ERROR producto desconocido";
if (p.cantidad < cantidad) return "ERROR stock insuficiente";
p.cantidad -= cantidad;
valorTotal -= cantidad * p.precio;
return "VENTA " + nombre + " " + p.cantidad;
}
long valor() {
return valorTotal;
}
int tamano() {
return productos.size();
}
List<Producto> lista() {
return productos;
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
valorTotal = 0;
for (Producto p : productos) {
valorTotal += p.cantidad * p.precio;
}
}
}
static final Path FICHERO = Paths.get("almacen.dat");
static void guardar(Almacen a) throws IOException {
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(Files.newOutputStream(FICHERO)))) {
out.writeObject(a);
}
}
static Almacen cargar() throws IOException, ClassNotFoundException {
try (ObjectInputStream in = new ObjectInputStream(
new BufferedInputStream(Files.newInputStream(FICHERO)))) {
return (Almacen) in.readObject();
}
}
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Almacen almacen = new Almacen();
StringBuilder sb = new StringBuilder();
String linea;
while ((linea = br.readLine()) != null) {
linea = linea.trim();
if (linea.isEmpty()) continue;
String[] t = linea.split("\\s+");
switch (t[0]) {
case "ALTA" -> {
almacen.alta(t[1], Long.parseLong(t[2]), Long.parseLong(t[3]));
sb.append("ALTA ").append(t[1]).append('\n');
}
case "VENTA" -> sb.append(almacen.venta(t[1], Long.parseLong(t[2]))).append('\n');
case "VALOR" -> sb.append("VALOR ").append(almacen.valor()).append('\n');
case "GUARDAR" -> {
guardar(almacen);
sb.append("GUARDADO ").append(almacen.tamano()).append('\n');
}
case "CARGAR" -> {
if (!Files.exists(FICHERO)) {
sb.append("ERROR sin fichero\n");
} else {
almacen = cargar();
sb.append("CARGADO ").append(almacen.tamano()).append('\n');
}
}
case "LISTAR" -> {
for (Producto p : almacen.lista()) {
sb.append(p.nombre).append(' ').append(p.cantidad)
.append(' ').append(p.precio).append('\n');
}
}
default -> { }
}
}
System.out.print(sb);
Files.deleteIfExists(FICHERO);
}
}Producto y Almacen implementan Serializable y declaran serialVersionUID: así la compatibilidad de versiones es explícita y no depende de un hash calculado por la JVM. En Almacen, el campo productos se serializa y valorTotal, al ser transient, se omite del flujo de bytes.
alta y venta mantienen valorTotal de forma incremental. En alta sobre un producto existente se resta primero su aportación antigua y se suma la nueva, porque el precio se sustituye: en el test «Alta repetida» (clavo 10 a 5 y luego 5 más a 8) el total pasa de 50 a 15·8 = 120, y no a 50 + 40.
El corazón del ejercicio es readObject. La JVM lo invoca al deserializar; llamamos a defaultReadObject() para restaurar productos y después recorremos la lista para recalcular valorTotal. Sin ese método, el campo quedaría en 0 tras CARGAR. El test «Ejemplo del enunciado» lo comprueba: tras GUARDAR el valor es 650, tras una venta de 4 tuercas baja a 550, y tras CARGAR vuelve a 650, porque se recupera el estado guardado y el campo transitorio se reconstruye a partir de él.
El método guardar usa try-with-resources con ObjectOutputStream sobre un flujo con búfer, de modo que el fichero se cierra y se vuelca aunque ocurra una excepción. cargar hace lo propio con ObjectInputStream y convierte el resultado con un cast a Almacen. Antes de cargar, main comprueba Files.exists; el test «Cargar sin guardar» muestra ERROR sin fichero y el almacén sigue vacío (VALOR 0).
El test «Guardar vacío y recargar» demuestra que la carga sustituye el almacén en memoria: se guarda vacío, se añade un producto (valor 1), y CARGAR devuelve 0 productos y valor 0. Los errores de VENTA (test «Ventas erróneas») no alteran el estado: el valor sigue en 1500. Por último, main acumula la salida en un StringBuilder y borra el fichero de trabajo al terminar, para que ejecuciones sucesivas sean independientes.
Casos de prueba
| Caso | Entrada | Salida esperada | Salida real | Resultado |
|---|---|---|---|---|
| Ejemplo del enunciado | ALTA tuerca 10 25
ALTA tornillo 4 100
VALOR
GUARDAR
VENTA tuerca 4
VALOR
CARGAR
VALOR
LISTAR | ALTA tuerca
ALTA tornillo
VALOR 650
GUARDADO 2
VENTA tuerca 6
VALOR 550
CARGADO 2
VALOR 650
tuerca 10 25
tornillo 4 100 | ALTA tuerca
ALTA tornillo
VALOR 650
GUARDADO 2
VENTA tuerca 6
VALOR 550
CARGADO 2
VALOR 650
tuerca 10 25
tornillo 4 100 | OK |
| Ventas erróneas | ALTA cable 3 500
VENTA cable 5
VENTA fusible 1
VALOR | ALTA cable
ERROR stock insuficiente
ERROR producto desconocido
VALOR 1500 | ALTA cable
ERROR stock insuficiente
ERROR producto desconocido
VALOR 1500 | OK |
| Cargar sin guardar | CARGAR
VALOR
LISTAR | ERROR sin fichero
VALOR 0 | ERROR sin fichero
VALOR 0 | OK |
| Alta repetida | ALTA clavo 10 5
ALTA clavo 5 8
VALOR
LISTAR | ALTA clavo
ALTA clavo
VALOR 120
clavo 15 8 | ALTA clavo
ALTA clavo
VALOR 120
clavo 15 8 | OK |
| Guardar vacío y recargar | GUARDAR
ALTA a 1 1
VALOR
CARGAR
VALOR
LISTAR | GUARDADO 0
ALTA a
VALOR 1
CARGADO 0
VALOR 0 | GUARDADO 0
ALTA a
VALOR 1
CARGADO 0
VALOR 0 | OK |
| Entrada vacía | (vacía) | (vacía) | (vacía) | OK |
Salidas reales: código compilado con Java 21.0.12.1 (Temurin) y ejecutado en un contenedor aislado el 14 de octubre de 2026.
Complejidad
Sea p el número de productos y g el número de órdenes. buscar es una búsqueda lineal, O(p), y la usan alta y venta. Por tanto el bucle de órdenes cuesta O(g·p) en el peor caso. Mantener valorTotal de forma incremental no cambia esa cota: solo evita que VALOR recorra la lista, de modo que esa orden pasa de O(p) a O(1). Para bajar alta y venta a O(1) esperado habría que indexar los nombres con un HashMap.
GUARDAR, CARGAR (incluido el recálculo de readObject) y LISTAR son O(p) en tiempo, proporcional al tamaño del grafo de objetos que se escribe o se lee. En memoria el coste es O(p) para la lista; la serialización añade un búfer temporal y los metadatos de clase que Java escribe una sola vez por flujo.
Errores típicos
- Olvidar
implements SerializableenProducto:Almacensí lo implementa, pero al escribir la lista se lanzaNotSerializableExceptionen el primer producto. - Marcar
valorTotalcomotransienty no reconstruirlo: trasCARGARel valor sería 0 aunque los productos estén ahí. Eltransientexige siempre un plan para recuperar el dato. - Llamar a
readObjectsin invocar antesin.defaultReadObject(): la lista quedaría sin leer y la deserialización fallaría o dejaría el objeto incoherente. - En
altasobre un producto existente, sumar solo la cantidad nueva por el precio nuevo avalorTotal, sin restar la aportación antigua ni valorar las unidades que ya había al nuevo precio. - No cerrar los flujos (sin
try-with-resources): conBufferedOutputStreamlos últimos bytes pueden no llegar al fichero y la carga posterior falla conEOFException. - Comparar nombres con
==en lugar deequals: funciona con literales, pero tras leer del fichero o de la entrada son objetos distintos y la búsqueda falla.
Variantes
Guardar solo si hay cambios
El profesor puede pedir que GUARDAR no escriba si el almacén no se ha modificado desde la última carga o guardado. Basta un campo transient boolean sucio que se pone a true en alta y venta y a false tras guardar o cargar. Como es transient, al deserializar vale false por defecto, que es justo el valor correcto.
Control de versiones con serialVersionUID
Si añades un campo nuevo a Producto (por ejemplo categoria) y mantienes serialVersionUID = 1L, Java sigue leyendo ficheros antiguos y deja el campo nuevo a su valor por defecto. Si cambias el UID, la lectura lanza InvalidClassException. Con un readObject propio puedes rellenar un valor razonable para el campo ausente.
Externalizable en lugar de Serializable
Implementando Externalizable debes escribir writeExternal y readExternal a mano y la clase necesita un constructor público sin argumentos. Controlas el formato por completo, y el campo derivado ni siquiera se plantea: simplemente no se escribe y se recalcula en readExternal.
En los temarios
La persistencia de objetos es un contenido habitual de la programación orientada a objetos avanzada. El curso ISIS1209 APO II Honores de la Universidad de los Andes (Colombia) incluye en su temario el estudio de mecanismos de reutilización de clases y de definición de contratos, base sobre la que se apoya una clase serializable: Serializable es en sí un contrato que el compilador no verifica pero la JVM sí exige en tiempo de ejecución.
Historial de cambios
- Reescrito con la plantilla de Código de Examen: enunciado de examen, pistas, solución explicada, casos de prueba y código compilado y ejecutado; versión en inglés enlazada.
- Publicación original.
Artículo generado mediante AI.larebelion · Revisado por Jose Santamaria


Publicar un comentario