1.2. Consulta y manipulación de contenidos#
Para acceder a los contenidos de los archivos, antes tenemos que tener claro con qué tipos de información almacenada podemos encontrarnos. Básicamente, podemos clasificarla en estos tipos:
Información accesible a través de un SGBD (o sea, una base de datos), que es el modo en que debe almacenarse la información cuando es abundante y necesitamos acceder a ella de forma eficiente. Gran parte del curso tratará de ella, así que, por ahora, no profundizaremos más.
Información en archivos, que pueden ser de distinto tipo:
Archivos de texto:
Sin formato específico, con lo que no tendremos una biblioteca especializada que nos permita rescatar la información, y deberemos acceder a ella a través de los métodos genéricos de lectura de archivos.
Con formato específico, para el cual sí dispondremos de biblioteca especializada. Formatos típicos son CSV, INI, JSON, YAML o XML. Dejaremos el acceso a algunos de estos formatos para la próxima unidad.
Archivos binarios, esto es, ilegibles directamente por un humano y que requieren forzosamente de algún método para su acceso. Entre ellos cabe destacar:
Los producidos por la serialización directa de objetos.
Los que responden a un formato específico para el cual, como en el caso de archivos de texto, necesitaremos una biblioteca especializada para su tratamiento.
Nuestro plan de estudios será el siguiente:
Dedicaremos el resto de la unidad a ver cómo acceder a archivos de texto sin formato y binarios generados por serialización.
En la siguiente unidad trataremos algunos formatos típicos de archivos de texto. No haremos lo propio con formatos de archivos binarios.
El resto de las unidades se dedicarán al acceso a bases de datos de distinto tipo con distintas técnicas.
1.2.1. Archivos de texto#
En este tipo de archivos el acceso es secuencial, de manera que no hay modo de acceder a una parte concreta de la información: simplemente se abre el archivo y se genera un flujo. Para su manipulación (tanto lectura como escritura) basta con utilizar algunos de los métodos del ya mencionado Files:
1.2.1.1. Lectura#
Hay varios métodos que pueden ayudarnos a leer un archivo, pero nos decantamos por:
Path archivo = Path.of(System.getProperty("user.home"), ".bashrc");
InputStream st = Files.newInputStream(archivo); // Si no puede abrirse, genera un error.
ya que genera un objeto InputStream
, que es lo mismo que se obtiene al
extraer contenido de una URL mediante este código:
URL url = new URI("https://etc...").toURL();
InputStream st = url.openStream();
y que también coincide con el tipo de System.in
:
InputStream st = System.in;
En consecuencia, a partir de este punto podemos tratar estas tres entradas diferentes de un mismo modo (p.e. usando la clase Scanner). Si el archivo lo vamos a manejar por líneas lo más adecuado es hacer lo siguiente:
InputStreamReader sr = new InputStreamReader(st, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(sr);
Nota
En las siguientes unidades, para el tratamiento de archivos en distintos formatos de intercambio de información conocidos intentaremos utilizar un método que nos permita convertir la entrada en un objeto InputStream, aunque pueda existir algún otro menos verborreico que permita utilizarlo directamente a partir de su nombre. Eso nos asegurará que si la entrada no es un archivo sino el teclado o una URL externa, sabremos cómo tratarlo.
Nota
Cuando establecemos el lector a partir del flujo de entrada, debemos indicar qué codificación se usa. Si no se especifica, se sobreentiende que la predeterminada de la JVM que coincide con la del sistema operativo, que en los modernos suele ser UTF-8, de modo que a partir de ahora, no volveremos a indicarlo:
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
Charset.defaultCharset() == StandardCharsets.UTF_8; // true
InputStreamReader sr = new InputStreamReader(st); // UTF-8
Al objeto resultado podemos tratarlo como prefiramos:
String line;
while((line = br.readLine()) != null) {
// Tratamiento de cada línea.
}
o bien:
br.lines().forEach(line -> {
// Tratamiento de cada línea.
});
o bien:
for(String line: br.lines().toList()) {
// Tratamiento de cada línea.
}
o, incluso, si no se quiere agotar el flujo de primeras:
// Convertimos el stream en un iterable.
for(String line: (Iterable<String>) br.lines()::iterator) {
// Tratamiento de la línea
}
Finalmente, habría que cerrar el flujo:
br.close(); // El resto se cierra en cascada.
Nota
El método lines()
devuelve un flujo por líneas (Stream<String>
)
al que se pueden aplicar estrategias de programación funcional.
Poniendo todo junto y usando try para el tratamiento de errores y el autocierre:
Path archivo = Path.of(System.getProperty("user.home"), ".bashrc");
try (
InputStream st = Files.newInputStream(archivo);
BufferedReader br = new BufferedReader(new InputStreamReader(st))
) {
for(String line: br.lines().toList()) {
// Tratamiento de cada línea.
}
}
catch (IOException err) {
err.printStackTrace();
}
1.2.1.2. Escritura#
En este tipo de archivos, obviamente, tenemos que escribir texto, o sea, cadenas, pero no directamente caracteres, sino bytes. Por lo demás, basta con utilizar otro método de Files para abrir un flujo de salida:
String contenido = "Este es el texto del archivo";
Path archivo = Path.of(System.getProperty("java.io.tmpdir"), "caca.txt");
try (OutputStream st = Files.newOutputStream(archivo)) {
st.write(contenido.getBytes(StandardCharset.UTF_8));
}
catch (IOException err) {
err.printStackTrace();
}
En este caso, se ha abierto el archivo para incluir en él la información
suministrada sin respetar la que ya pudiera haber. Sin embargo, pueden añadirse
a Files.newOutputStream
argumentos adicionales para incluir una o varias
opciones que modifiquen este comportamiento (véase
StandardOpenOption). Por ejemplo:
// Se añade contenido, por lo que se respeta el que pudiera haber.
OutputStream st = Files.newOutputStream(archivo, StandardOpenOption.APPEND);
Por otro lado, para transformar la cadena en bytes es necesario especificar la codificación usada (StandardCharsets.UTF-8), aunque si no se indica se sobreentiende la predeterminada de la JVM (muy probablemente UTF-8, que es lo habitual en los sistemas modernos). Una alternativa, es utilizar un escritor de flujo:
String contenido = "Este es el texto del archivo";
Path archivo = Path.of(System.getProperty("java.io.tmpdir"), "caca.txt");
try (
OutputStream st = Files.newOutputStream(archivo)
OutputStreamWriter sw = new OutputStreamWriter(st, StandardCharsets.UTF-8);
) {
sw.write(contenido); // Cuidado que no incluye salto de línea.
}
catch (IOException err) {
err.printStackTrace();
}
De nuevo, si la codificación es UTF-8, podemos prescindir de indicarla explícitamente.
1.2.2. Serialización de objetos#
El otro mecanismo de acceso a archivos es el aleatorio, implementado mediante la clase java.io.RandomAccessFile y gracias al cual se puede acceder a bytes concretos y avanzar o retroceder dentro de él, tanto para leer como para escribir.
Este mecanismo nos permite escribir distintos tipos de datos e incluso objetos completos, pero es tedioso (véase almacenar objetos en archivos de acceso aleatorio).
En vez de ello, sale más a cuenta serializar objetos y almacenarlos en disco para poderlos rescatar posteriormente. Eso sí, antes debemos definir el concepto. La serialización es el proceso de convertir datos en una secuencia de bytes, cuya lectura permite posteriormente recuperar los datos originales. Como los archivos son precisamente eso mismo, secuencias de bytes, es un mecanismo apropiado para almacenar datos en disco.
Antes de dar un ejemplo, no obstante, es preciso establecer varias premisas:
En cada archivo sólo podemos serializar un objeto, por lo que si queremos serializar varios tendremos que incluirlos dentro de una lista o una estructura parecida.
Para que un objeto sea serializable debe implementar la interfaz java.io.Serializable.
Escribimos y leemos el archivo de una tacada. Esto es un problema si la cantidad de datos es grande, pero en ese caso, deberíamos haber usado una base de datos.
Para ilustrar cómo se serializan objetos definamos una clase muy simple:
public class Persona implements Serializable {
private String nombre;
private int edad;
Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public String getNombre() {
return nombre;
}
public int getEdad() {
return edad;
}
@Override
public String toString() {
return String.format("%s, %d", nombre, edad);
}
@Override
public boolean equals(Object o) {
Persona otra = (Persona) o;
return edad == otra.edad && nombre.equals(otra.nombre);
}
}
1.2.2.1. Escritura#
Para escribir en disco varios objetos «Persona», podemos hacer lo siguiente:
Path ruta = Path.of(System.getProperty("java.io.tmpdir"), "personas.bin");
// Con List es igual ya que, como los arrays, es serializable.
Persona[] personas = new Persona[] {
new Persona("Manolo", 15),
new Persona("Pablo", 10)
};
try (
OutputStream os = Files.newOutputStream(ruta);
ObjectOutputStream oss = new ObjectOutputStream(os)
) {
oss.writeObject(personas);
}
catch(IOException err) {
err.printStackTrace();
}
Y listo, tendremos en personas.bin
la lista de personas serializadas.
1.2.2.2. Lectura#
Para recuperar un objeto serializado, hay que hacer el proceso inverso. Para ilustrarlo añadamos el siguiente código detrás del anterior:
Persona[] personasLeidas = null;
try (
InputStream is = Files.newInputStream(ruta);
ObjectInputStream ois = new ObjectInputStream(is);
) {
personasLeidas = (Persona[]) ois.readObject();
}
catch (IOException err) {
err.printStackTrace();
}
System.out.printf("¿Es el array leído el mismo que habíamos escrito? %b",
Arrays.equals(personas, personasLeidas)); // Debe ser true.