Al guardar el archivo en disco, no se puede leer un carajo... porque NO es un archivo
de texto.
Es un archivo
de bytes, lo que guardamos es una copia del "lenguaje máquina" tal cuál está en la memoria RAM.
Por eso en el programa lo puse para que se guardase con extensión
.bin (binary code).
Se le puede poner cualquier extensión, sus datos internos son los mismos. Pero si le ponemos .txt, se puede dar a entender que es un archivo de texto. Y no lo es.
La ventaja de guardar los datos como bytes, es que el código para hacer esto es muy sencillo.
Básicamente necesitamos una instrucción para escribir, y otra para leer. Punto pelota.
La desventaja, que si luego queremos poder leer su contenido abriéndolo como un archivo de texto, nos vamos a encontrar un galimatías.
Se puede guardar también como archivo de texto plano, es decir, un verdadero y genuino .txt
El proceso es muy parecido, pero implica hacer más (muchas más) operaciones extra, además de decidir cómo nos conviene que se escriban los datos.
Porque según como los escribamos, luego será más fácil o más difícil leerlos.
Me explico:
Un Producto tiene cuatro atributos: referencia(int), nombre(String), precio(double) y descripcion(String)
Supongamos que tenemos 5 Productos registrados en un ArrayList y queremos guardarlos en un archivo de texto plano.
Tendremos que recorrer el ArrayList con un bucle para obtener los tres atributos de cada Producto y escribirlos en el archivo de texto (esto como decía, ya supone realizar varias instrucciones de más comparado con antes).
Si simplemente vamos escribiendo en el archivo de texto, según vamos obteniendo valores de atributos, tendremos un archivo de texto como este:
1Coca-Cola1.25Bebida refrescante azucarada2Coca-Cola Light1.30Bebida refrescante ligera en azucares3Coca-Cola Zero1.30Bebida refrescante sin azucar4Fanta Limon1.25Bebida refrescante sabor limon5Fanta Naranja1.25Bebida refrescante sabor naranja
Vale, ahora si tenemos texto que entiende un humano, pero esta todo apelotonado en una sola línea.
No solo es feo a nuestros ojos, esto es lo de menos.
El auténtico problema es que luego para recuperar los datos en nuestro programa, tenemos que saber que palabras y números corresponden a cada atributo, para podre reconstruir los objetos Producto que habíamos guardado de la sesión anterior.
¿Entiendes por donde voy?
Antes, serializando todo en un archivo de bytes, era muy sencillo porque los objetos Producto se guardan tal cuál estaban en memoria RAM. Luego al leerlos no hay que reconstruir nada, porque permanecen "construidos" en el archivo de bytes.
Esto no es posible hacerlo en el archivo de texto plano, aquí solo podemos guardar los valores de los atributos de los productos, pero no los objetos Producto tal cual.
Y luego, hay que recuperar esos valores y separarlos, y utilizarlos para volver a construir otra vez los Productos (habrá que hacer otra vez new Producto() para cada uno y agregarlos otra vez al array de Producte[] )
Así que para facilitar esta tarea, primero hay que decidir una forma más adecuada para escribir los valores en el txt.
Lo habitual es hacer una línea para cada Producto.
1Coca-Cola1.25Bebida refrescante azucarada
2Coca-Cola Light1.30Bebida refrescante ligera en azucares
3Coca-Cola Zero1.30Bebida refrescante sin azucar
4Fanta Limon1.25Bebida refrescante sabor limon
5Fanta Naranja1.25Bebida refrescante sabor naranja
Bien, ahora ya sabemos donde comienza y termina cada Producto. Cada línea es un Producto.
Pero no sabemos bien donde comienzan y terminan cada valor de los atributos.
Tenemos que decidir alguna forma óptima de separarlos.
Así después, leeremos las líneas una por una. Y cada línea la partiremos en trozos allá donde detectemos lo que separa un valor de otro.
¿Y con qué separamos un valor de otro?
No podemos usar espacios en blanco, porque por ejemplo los nombres de producto o el texto de la descripción contienen espacios en blanco. Así que esto no nos va a servir para saber con certeza donde comienza un valor y termina otro
Podemos usar una coma para separar cada valor
1,Coca-Cola,1.25,Bebida refrescante azucarada
2,Coca-Cola Light,1.30,Bebida refrescante ligera en azucares
3,Coca-Cola Zero,1.30,Bebida refrescante sin azucar
4,Fanta Limon,1.25,Bebida refrescante sabor limon
5,Fanta Naranja,1.25,Bebida refrescante sabor naranja
Esta solución puede servir, si estamos seguros de que ni el nombre ni la descripción van a tener comas.
Sin embargo es arriesgado no solo por esos atributos, si no también por el precio. Yo lo he puesto con punto decimal, pero en realidad, cuando se guarde en archivo es posible que se guarden con coma decimal, esto puede variar según el sistema operativo y la configuración regional del usuario.
De hecho, luego al leer los datos, el que corresponde al precio habrá que parsearlo a double porque al leer del txt lo vamos a obtener como String, pero el atributo es un double.
Y habrá que controlar que
no nos llegue con coma decimal, porque entonces el parseo a double fallará...solo es posible parsear a double si el texto leído tiene un punto decimal, no coma decimal.
Así que por todo esto, mejor usar otro símbolo para separar los productos. Para curarnos en salud, lo mejor es usar un conjunto de símbolos, por ejemplo dos guiones seguidos con espacio en blanco a cada lado.
1 -- Coca-Cola -- 1.25 -- Bebida refrescante azucarada
2 -- Coca-Cola Light -- 1.30 -- Bebida refrescante ligera en azucares
3 -- Coca-Cola Zero -- 1.30 -- Bebida refrescante sin azucar
4 -- Fanta Limon -- 1.25 -- Bebida refrescante sabor limon
5 -- Fanta Naranja -- 1.25 -- Bebida refrescante sabor naranja
Así se lee mejor a simple vista, y lo mejor de todo es que luego raro sería que ocurriesen errores al trocear las líneas correctamente en cuatro valores, uno para cada atributo.
Disculpa si me extiendo mucho con la explicación, pero es para que quede claro que guardar los valores como texto plano, supone ciertas complicaciones.
Ahora veremos como resolver todo esto con código Java.
De nuevo trabajaremos con los Productos y usaremos métodos específicos para guardar como texto plano, sin quitar lo que habíamos hecho antes. Así tendremos las dos versiones para practicar.
Lo primero que haremos, para facilitar las cosas, es crear un método en la clase Producto que nos construya una línea de texto con los atributos, tal y como queremos que se guarden en el archivo txt.
Algo parecido a lo que hacemos con el método toString(), pero este lo usaremos para escribir el Producto en una línea de texto.
Dicho método, puede ser como este:
public String toTxt() {
return String.format("%d -- %s -- %.2f -- %s", refProducte, nom, preu, descripcio);
}
Eso ya nos devuelve una línea de texto, con los atributos separados por los dobles guiones.
También conviene reiniciar a 0 el contador interno de Productos creados que tiene la clase Producte.
Este contador se usa para establecer el atributo de la referencia cuando creamos un Producto nuevo. Como al leer líneas de texto, vamos a crear Productos nuevos (con valores guardados de antes) para evitar inconsistencias con las referencias, es mejor que comience desde 0 otra vez.
Así que le añadimos también este método a la clase Producte (ha de ser estatico):
public static void resetTotalProductes() {
totalDeProductes = 0;
}
Esto no lo tuvimos en cuenta la vez anterior, y creo que también habría sido importante hacerlo.
Bien, pues ahora en la clase principal, añadimos nuevo método para guardar productos, esta vez en un archivo de texto plano.
Para esto usaremos la clase BufferedWriter, que a su vez necesita un FileWriter, que a su vez necesita un File.
El proceso es parecido a la forma anterior, pero fíjate que antes escribíamos tal cual el array de Productos.
Ahora, hay que recorrer el array y a cada Producto, pedirle que nos retorne la línea de texto, que será lo que guardaremos en el archivo.
private static void guardarProductesTXT() {
File fitxerProductes = new File("productes.txt");
if (!fitxerProductes.exists()) {
try {
fitxerProductes.createNewFile();
} catch (IOException e) {
System.out.println("\n-- ERROR. No s'ha pogut crear: " + fitxerProductes.getAbsolutePath());
return; //Posem fi a l'operació. No es pot guardar en aquesta ubicació.
}
}
try {
BufferedWriter bw = new BufferedWriter(new FileWriter(fitxerProductes));
//Per cada Producte de l'array, escriurem una linia
for (int i = 0; i < comptadorDeProductes; i++) {
bw.write(productes[i].toTxt());
bw.newLine(); //Fen una nova linia per al següent producte
}
bw.close();
} catch (IOException e) {
System.out.println("\n-- ERROR. No es pot accedir a fitxer: " + fitxerProductes.getAbsolutePath());
}
}
Y ahora otro método para el proceso opuesto, leer las líneas de texto, separar los valores, parsear a int la referencia, parsear a double el precio, y crear nuevos Productos mediante estos valores.
Para leer usaremos la clase BufferedReader, que a su vez necesita un FileReader, que a su vez necesita un File...
Las líneas que vamos a leer las recibimos como String, y String tiene el método split() con el que podemos ordenar que "trocee" la línea allá donde encuentre los dobles guiones que hemos usado para separar los valores.
Al trocear la linea, vamos a recibir un array que contiene cada "trozo" obtenido. Estos "trozos" son los valores de los atributos, que usaremos para reconstruir los Productos.
El primer "trozo" (posicion 0 del array que nos da split()) es la referencia, pero como este valor se autoasigna de forma interna con el contador que tiene la clase
Producte, no lo usaremos para nada.
Por eso antes hicimos el método para resetear este contador.
Con el resto de trozos, crearemos un Producto, lo añadimos al array, y pasamos a la siguiente línea.
public static void recuperarProductesTXT() {
File fitxerProductes = new File("productes.txt");
if (!fitxerProductes.exists())
System.out.println("No hi ha productes guardats per recuperar.");
else {
try {
//Reiniciem l'array actual de Productes. Si tenía Productes, ¡¡s'esborrarán!!
productes = new Producte[MAXIM_PRODUCTES];
comptadorDeProductes = 0;
//També el comptador intern de la clase Producte
Producte.resetTotalProductes();
//Declarem el Reader
BufferedReader br = new BufferedReader(new FileReader(fitxerProductes));
//Obtenim primera linia
String linia = br.readLine();
do {
//Dividim linia per separar valors de atributs
String[] atributs = linia.split(" -- ");
//Reconstruim producte, alguns valors requereixen ser parsejats.
//atributs[0] es la referencia, pero aquest valor s'encarrega el comptador intern de Producte
String nom = atributs[1];
//El preu segurament tindrà coma decimal. Cal canviar-lo per un punt decimal.
atributs[2] = atributs[2].replaceAll(",", ".");
double preu = Double.parseDouble(atributs[2]);
String descrip = atributs[3];
productes[comptadorDeProductes] = new Producte(nom, preu, descrip);
comptadorDeProductes++;
//Producte reconstruir, llegim linia següent
linia = br.readLine();
}while (linia != null); //Repetim procés fins que no quedin linies
//Ja no quedan linies, procés finalitzat
br.close();
} catch (FileNotFoundException e) {
System.out.println("\n-- ERROR. No es troba fitxer: " + fitxerProductes.getAbsolutePath());
} catch (IOException e) {
System.out.println("\n-- ERROR. No es pot accedir a fitxer: " + fitxerProductes.getAbsolutePath());
}
}
}
Importante comprender que cada vez que recuperamos Productos, ya sea del archivo binario o de texto, los posibles Productos creados en ese momento en el array de Productos se borrarán.
Y bueno, ya por último, para poder escoger una forma u otra tanto para guardar como para leer, he cambiado el método menu() añadiendo una variable char para que el usuario elija como quiere hacerlo.
Marco en negrita los cambios:
public static void menu(){
Scanner sc = new Scanner(System.in);
boolean sortir = false;
char opcioArxiu = ' ';
do{
System.out.println("***** Gestió de factures *****");
System.out.println("Dades emissor: " + Emissor.getString());
System.out.println();
System.out.println("--- Menú d'usuari ---");
System.out.println("\t1) Crear nova factura");
System.out.println("\t2) Visualitzar totes les factures");
System.out.println("\t3) Cercar factura");
System.out.println("\t4) Eliminar factura");
System.out.println("\t5) Crear client");
System.out.println("\t6) Visualitzar client");
System.out.println("\t7) Crear producte");
System.out.println("\t8) Visualitzar producte");
System.out.println("\t9) Canviar emissor");
System.out.println("\t10) Guardar emissor");
System.out.println("\t11) Guardar productes");
System.out.println("\t12) Guardar clients");
System.out.println("\t13) Guardar factures");
System.out.println("\t14) Recuperar productes");
System.out.println("\t15) Sortir");
System.out.print("La teva opció:");
int opcio = 0;
try{
opcio = sc.nextInt();
}catch(InputMismatchException ex){
System.out.print("Entrada no vàlida. No és un número");
}finally{
sc.nextLine();
}
try{
switch(opcio){
case 1: crearNovaFactura();
break;
case 2: visualitzarFactures();
break;
case 3: cercarFactura();
break;
case 4: eliminarFactura();
break;
case 5: crearClient();
break;
case 6: visualitzarClients();
break;
case 7: crearProducte();
break;
case 8: visualitzarProductes();
break;
case 9: canviarEmissor();
break;
case 10: guardarEmissor();
break;
case 11:
System.out.println("[ b ] - Desar com arxiu de bytes");
System.out.println("[ t ] - Desar com arxiu de text");
System.out.print("Opcio: ");
opcioArxiu = sc.nextLine().toLowerCase().charAt(0);
if (opcioArxiu == 'b')
guardarProductes();
else
guardarProductesTXT();
break;
case 12: guardarClients();
break;
case 13: guardarFactures();
break;
case 14:
System.out.println("[ b ] - Recuperar de l'arxiu de bytes");
System.out.println("[ t ] - Recuperar de l'arxiu de text");
System.out.print("Opcio: ");
opcioArxiu = sc.nextLine().toLowerCase().charAt(0);
if (opcioArxiu == 'b')
recuperarProductes();
else
recuperarProductesTXT();
break;
case 15: sortir = true;
break;
default:
System.out.println("Opció no vàlida!");
}
}catch(Exception ex){
System.out.println("S'ha produit un error:");
System.out.println(ex.getMessage());
}
}while(!sortir);
}
Y ya está, ahora se pueda guardar y recuperar de un archivo de texto plano. Archivo que si lo abrimos con el bloc de notas, ahora si lo puede leer un humano.
Como has visto, este proceso requiere de más pasos, pero bueno, tampoco es muy distinto de la forma anterior.
Revísalo bien y pregunta si algo no lo entiendes. Un saludo.