Apuntes DAM
Volver al inicio

Fichero de productos con registros de tamaño fijo y acceso directo

Ejercicio de JavaMuy difícilUnos 90 minutos

Un pequeño gestor de productos sobre un fichero binario con RandomAccessFile: registros de 77 bytes, acceso directo por código con seek, altas en huecos, cambios de un solo campo, bajas lógicas con una marca y un informe del fichero. Ficheros binarios, punteros y tamaños de cada tipo.

  • RandomAccessFile
  • seek y length
  • writeInt, writeChars, writeDouble…
  • Registros de tamaño fijo
  • Acceso directo por posición
  • Borrado lógico

Enunciado

Antes de las bases de datos, los programas de gestión guardaban sus datos en ficheros binarios con registros de tamaño fijo. Todavía hoy es la forma de entender cómo trabaja un gestor de bases de datos por dentro: si cada registro ocupa siempre lo mismo, la posición del registro número N se calcula con una multiplicación y se puede ir directamente a él, sin leer los anteriores.

En este fichero cada producto ocupa 77 bytes: el código (int, 4 bytes), el nombre (30 caracteres char de 2 bytes, 60 en total, rellenando con el carácter \0), el precio (double, 8), el stock (int, 4) y una marca de borrado (boolean, 1). El producto con código N vive en el byte (N − 1) × 77: es acceso directo, y por eso puede haber huecos entre productos.

Borrar un registro desplazando todos los siguientes sería lentísimo en un fichero grande, así que las bajas son lógicas: se pone la marca de borrado a true y el hueco queda libre para un alta futura con ese código.

Qué tiene que hacer el programa

  1. El programa crea un fichero temporal vacío y ejecuta una orden por línea (la orden puede ir en minúsculas; las líneas vacías se ignoran; una orden desconocida escribe Orden desconocida).
  2. ALTA código;nombre;precio;stock: el código es un entero de 1 a 9999, el nombre no puede estar vacío, el precio es un número no negativo con hasta dos decimales y el stock un entero no negativo (si no, Datos no válidos). Si ya hay un producto activo con ese código, Ya existe el producto N. Si no, se escribe el registro en su posición y se escribe Alta del producto N en el byte B, añadiendo (nombre recortado a 30 caracteres) si el nombre era más largo.
  3. PRECIO código precio cambia solo el precio, sin tocar el resto del registro: Precio del producto N: antes → después (o Datos no válidos / No existe el producto N).
  4. BAJA código pone la marca de borrado: Baja lógica del producto N, o No existe el producto N si no está activo. VER código escribe N · nombre · precio · S uds o No existe el producto N.
  5. LISTAR escribe Productos activos: A y, con dos espacios delante, cada producto activo por orden de código con el formato de VER.
  6. INFORME escribe Fichero: T bytes, H huecos de registro, A activos, L libres o borrados · valor del stock X, donde H es el tamaño del fichero entre 77 y el valor es la suma de precio × stock de los activos. Los precios se escriben con dos decimales, coma decimal y €.
  7. Un registro está activo si el fichero llega hasta él, el código guardado en él es el suyo (los huecos que se crean al escribir más allá del final están llenos de ceros) y no tiene la marca de borrado.

Entrada

Una orden por línea: ALTA, PRECIO, BAJA, VER, LISTAR o INFORME.

Datos de referencia

Estructura de un registro (77 bytes)
CampoTipoBytesDesplazamiento
códigoint40
nombre30 × char604
preciodouble864
stockint472
borradoboolean176

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Altas, cambios y bajas

Entrada

ALTA 1;Ratón inalámbrico;9.95;20
ALTA 3;Teclado mecánico;49.90;5
ALTA 1;Repetido;1;1
LISTAR
PRECIO 3 44.50
BAJA 1
VER 1
VER 3
VER 2
INFORME

Salida por consola

Alta del producto 1 en el byte 0
Alta del producto 3 en el byte 154
Ya existe el producto 1
Productos activos: 2
  1 · Ratón inalámbrico · 9,95 € · 20 uds
  3 · Teclado mecánico · 49,90 € · 5 uds
Precio del producto 3: 49,90 € → 44,50 €
Baja lógica del producto 1
No existe el producto 1
3 · Teclado mecánico · 44,50 € · 5 uds
No existe el producto 2
Fichero: 231 bytes, 3 huecos de registro, 1 activos, 2 libres o borrados · valor del stock 222,50 €

Datos que no valen

Entrada

ALTA 0;Cero;1;1
ALTA 10;;1;1
ALTA 10;Monitor;-5;1
ALTA 10;Monitor;99.999;1
ALTA abc
PRECIO 7 10
BAJA 7
listar
BORRAR 1

Salida por consola

Datos no válidos
Datos no válidos
Datos no válidos
Datos no válidos
Datos no válidos
No existe el producto 7
No existe el producto 7
Productos activos: 0
Orden desconocida

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. La posición de cada registro

Todo el ejercicio gira en torno a una fórmula: el registro del código N empieza en el byte (N - 1) * 77, y cada campo, en ese byte más su desplazamiento (tabla de arriba). Escríbela en un método y úsala siempre.

java
static long posicion(int codigo) {
    return (long) (codigo - 1) * TAM;
}
2. Escribir un registro completo

Sitúate con f.seek(posicion(codigo)) y escribe los campos en orden: writeInt, writeChars, writeDouble, writeInt y writeBoolean. Para que el nombre ocupe siempre 60 bytes, ajústalo a 30 caracteres: StringBuilder.setLength(30) recorta o rellena con \0.

3. Saber si un registro está activo

Comprueba que posicion + 77 <= f.length(), lee el código guardado y salta al último byte para leer la marca de borrado. Si escribes el producto 5 en un fichero vacío, los huecos 1 a 4 se rellenan con ceros: su código guardado es 0 y no coincide.

4. Cambiar un solo campo

Para el precio no hace falta leer el registro entero: el precio está en el byte 64 del registro. seek ahí, readDouble para el valor anterior, otra vez seek (leer ha movido el puntero) y writeDouble.

5. Recorrer el fichero

El número de huecos es f.length() / 77. Para listar, recorre los códigos de 1 a ese número y muestra los activos: no hace falta un índice aparte porque la posición es el código.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

☕JavaFichero de productos con registros de tamaño fijo y acceso directoMuy difícil

Ejemplo

Entrada (lo que se escribe por teclado)
ALTA 1;Ratón inalámbrico;9.95;20
ALTA 3;Teclado mecánico;49.90;5
ALTA 1;Repetido;1;1
LISTAR
PRECIO 3 44.50
BAJA 1
VER 1
VER 3
VER 2
INFORME
Salida esperada
Alta del producto 1 en el byte 0
Alta del producto 3 en el byte 154
Ya existe el producto 1
Productos activos: 2
  1 · Ratón inalámbrico · 9,95 € · 20 uds
  3 · Teclado mecánico · 49,90 € · 5 uds
Precio del producto 3: 49,90 € → 44,50 €
Baja lógica del producto 1
No existe el producto 1
3 · Teclado mecánico · 44,50 € · 5 uds
No existe el producto 2
Fichero: 231 bytes, 3 huecos de registro, 1 activos, 2 libres o borrados · valor del stock 222,50 €
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
java
1import java.io.File;
2import java.io.IOException;
3import java.io.RandomAccessFile;
4import java.util.Locale;
5import java.util.Scanner;
6
7public class Main {
8    static final int NOMBRE = 30;                          // caracteres del nombre
9    static final int TAM = 4 + NOMBRE * 2 + 8 + 4 + 1;     // código + nombre + precio + stock + borrado = 77 bytes
10
11    static RandomAccessFile f;
12
13    /** Acceso directo: el registro del código N empieza en el byte (N - 1) × 77. */
14    static long posicion(int codigo) {
15        return (long) (codigo - 1) * TAM;
16    }
17
18    /** Un registro está activo si existe, guarda ese código y no tiene la marca de borrado. */
19    static boolean activo(int codigo) throws IOException {
20        if (posicion(codigo) + TAM > f.length()) return false;
21        f.seek(posicion(codigo));
22        int guardado = f.readInt();
23        f.seek(posicion(codigo) + TAM - 1);
24        return guardado == codigo && !f.readBoolean();
25    }
26
27    static void escribir(int codigo, String nombre, double precio, int stock) throws IOException {
28        f.seek(posicion(codigo));
29        f.writeInt(codigo);
30        StringBuilder sb = new StringBuilder(nombre);
31        sb.setLength(NOMBRE);                              // recorta o rellena con '\0' hasta 30 caracteres
32        f.writeChars(sb.toString());
33        f.writeDouble(precio);
34        f.writeInt(stock);
35        f.writeBoolean(false);
36    }
37
38    /** Lee el registro del código (que debe estar activo) como texto. */
39    static String leer(int codigo) throws IOException {
40        f.seek(posicion(codigo) + 4);
41        StringBuilder nombre = new StringBuilder();
42        for (int i = 0; i < NOMBRE; i++) {
43            char c = f.readChar();
44            if (c != '\0') nombre.append(c);
45        }
46        double precio = f.readDouble();
47        int stock = f.readInt();
48        return codigo + " · " + nombre + " · " + euros(precio) + " · " + stock + " uds";
49    }
50
51    static String euros(double x) {
52        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";
53    }
54
55    static void orden(String linea) throws IOException {
56        String[] p = linea.split("\\s+", 2);
57        String args = p.length > 1 ? p[1] : "";
58        switch (p[0].toUpperCase()) {
59            case "ALTA" -> {
60                String[] c = args.split(";", -1);
61                if (c.length != 4 || !c[0].matches("\\d{1,4}") || Integer.parseInt(c[0]) == 0 || c[1].isBlank()
62                        || !c[2].matches("\\d+(\\.\\d{1,2})?") || !c[3].matches("\\d+")) {
63                    System.out.println("Datos no válidos");
64                    return;
65                }
66                int codigo = Integer.parseInt(c[0]);
67                if (activo(codigo)) {
68                    System.out.println("Ya existe el producto " + codigo);
69                    return;
70                }
71                String nombre = c[1].trim();
72                escribir(codigo, nombre, Double.parseDouble(c[2]), Integer.parseInt(c[3]));
73                System.out.println("Alta del producto " + codigo + " en el byte " + posicion(codigo)
74                        + (nombre.length() > NOMBRE ? " (nombre recortado a " + NOMBRE + " caracteres)" : ""));
75            }
76            case "PRECIO" -> {
77                String[] c = args.split("\\s+");
78                if (c.length != 2 || !c[0].matches("\\d{1,4}") || !c[1].matches("\\d+(\\.\\d{1,2})?")) {
79                    System.out.println("Datos no válidos");
80                } else if (!activo(Integer.parseInt(c[0]))) {
81                    System.out.println("No existe el producto " + c[0]);
82                } else {
83                    // Solo se reescriben los 8 bytes del precio: no hace falta leer ni escribir el resto del registro
84                    long donde = posicion(Integer.parseInt(c[0])) + 4 + NOMBRE * 2;
85                    f.seek(donde);
86                    double antes = f.readDouble();
87                    f.seek(donde);
88                    f.writeDouble(Double.parseDouble(c[1]));
89                    System.out.println("Precio del producto " + c[0] + ": " + euros(antes) + " → " + euros(Double.parseDouble(c[1])));
90                }
91            }
92            case "BAJA" -> {
93                if (!args.matches("\\d{1,4}") || !activo(Integer.parseInt(args))) {
94                    System.out.println("No existe el producto " + args);
95                } else {
96                    f.seek(posicion(Integer.parseInt(args)) + TAM - 1);
97                    f.writeBoolean(true);                    // borrado lógico: el registro sigue ahí, marcado
98                    System.out.println("Baja lógica del producto " + args);
99                }
100            }
101            case "VER" -> System.out.println(args.matches("\\d{1,4}") && activo(Integer.parseInt(args)) ? leer(Integer.parseInt(args)) : "No existe el producto " + args);
102            case "LISTAR" -> {
103                int total = (int) (f.length() / TAM);
104                int activos = 0;
105                StringBuilder sb = new StringBuilder();
106                for (int codigo = 1; codigo <= total; codigo++) {
107                    if (activo(codigo)) {
108                        activos++;
109                        sb.append("  ").append(leer(codigo)).append('\n');
110                    }
111                }
112                System.out.print("Productos activos: " + activos + "\n" + sb);
113            }
114            case "INFORME" -> {
115                int total = (int) (f.length() / TAM);
116                int activos = 0;
117                double valor = 0;
118                for (int codigo = 1; codigo <= total; codigo++) {
119                    if (!activo(codigo)) continue;
120                    activos++;
121                    f.seek(posicion(codigo) + 4 + NOMBRE * 2);
122                    double precio = f.readDouble();
123                    valor += precio * f.readInt();
124                }
125                System.out.println("Fichero: " + f.length() + " bytes, " + total + " huecos de registro, " + activos
126                        + " activos, " + (total - activos) + " libres o borrados · valor del stock " + euros(valor));
127            }
128            default -> System.out.println("Orden desconocida");
129        }
130    }
131
132    public static void main(String[] args) throws IOException {
133        File fichero = File.createTempFile("productos", ".dat");
134        fichero.deleteOnExit();
135        try (RandomAccessFile raf = new RandomAccessFile(fichero, "rw")) {
136            f = raf;
137            Scanner sc = new Scanner(System.in);
138            while (sc.hasNextLine()) {
139                String linea = sc.nextLine().trim();
140                if (!linea.isEmpty()) orden(linea);
141            }
142        }
143    }
144}

El tamaño fijo es lo que hace posible el acceso directo: con una multiplicación se sabe dónde está cualquier registro, sin leer los anteriores. Es el mismo principio que usan los gestores de bases de datos al organizar los datos en páginas de tamaño fijo.

RandomAccessFile tiene un puntero que avanza con cada lectura o escritura; seek lo coloca donde se quiera. Por eso cambiar el precio son dos seek: uno para leer el valor anterior y otro para volver a la misma posición antes de escribir.

El borrado lógico cambia un solo byte en lugar de reorganizar el fichero. El precio a pagar es que el fichero no encoge: con el tiempo se llena de huecos, y por eso los sistemas reales tienen operaciones de compactación o reutilizan los huecos (aquí, un alta con el mismo código).

Los huecos que se crean al escribir más allá del final contienen ceros. Guardar el propio código dentro del registro permite distinguir un registro real de un hueco sin una tabla aparte.

Para ir más allá

  • Añade COMPACTAR: crea un fichero nuevo solo con los activos y un índice código → posición en otro fichero.
  • Guarda los nombres en UTF-8 con writeUTF y explica por qué entonces los registros dejan de tener tamaño fijo.
  • Haz una versión con FileChannel y ByteBuffer y compara el código.

Dónde se explica