FechaVersiónDescripción
07/02/20221.0.0Versión inicial
08/02/20252.0.0Incorporación funcionalidad PF
11/02/20252.0.1Modificación ficheros

Unidad 7. Programación funcional. Ficheros. WebServices.

1. Introducción

 

2. Características principales

2.1 Transparencia referencial e inmutabilidad

Si llamamos repetidamente a esta función con el parámetro 1, cada vez producirá un resultado distinto (3, 4, 5 ...)

2.2 Imperativo VS Declarativo

Veamos en Java como podemos obtener una sublista de personas adultas, utilizando un modelo Imperativo y un modelo Declarativo


Clase Persona

Imperativa

Declarativa

La programación declarativa es más compacta y menos propensa a errores. Como se puede observar en el ejemplo, hemos realizado una composición de funciones.

 

3. Introducción a las funciones Lambda

Ejemplo en Java

Ordenación de la lista con Comparator

Clase persona

Clase que implementa el comparador de personas

Implementación con Lambdas

Estructura de una función Lambda

estructura_lambda

 

 

4. Gestión de colecciones con streams en Java

 

4.1 Operaciones Intermedias streams

4.1.1 Filtrado

El método filter es una operación intermedia que permite quedarnos con los datos de una colección que cumplan el criterio indicado como parámetro.

"Aquellas personas p de la colección que cumplen determinada condición"

 

filter recibe como parámetro una interfaz Predicate, cuyo método test recibe como parámetro un objeto y devuelve si ese objeto cumple o no una determinada condición.

4.1.2 Mapeo

El método map es una operación intermedia que permite transformar la colección original para quedarnos con cierta parte de la información o crear otros datos.

"Las edades de aquellas personas p de la colección"

map recibe como parámetro una interfaz Function, cuyo método apply recibe como parámetro un objeto y devuelve otro objeto diferente, normalmente derivado del parámetro.

 

4. 1. 3 Combinar

Se pueden combinar operaciones intermedias (composición de funciones) para producir resultados más complejos. Por ejemplo, obtener las edades de las personas adultas:

4.1.4 Ordenar

El método sorted es una operación intermedia que permite ordenar los elementos de una colección según cierto criterio. Por ejemplo, ordenar las personas adultas por edad:

"Para cada pareja de personas p1 y p2, ordénalas en función de la resta de la edad de p1 menos la edad de p2"

sorted recibe como parámetro una interfaz Comparator, que ya conocemos.

 

4,2 Operaciones finales streams

4.2.1 Collect

El método collect es una operación final que te permite obtener algún tipo de colección a partir de los datos procesados por las operaciones intermedias. Por ejemplo, una lista con las edades de las personas adultas:

4.2.2 Cadena

El método collect también permite obtener una cadena de texto que una los elementos resultantes, a través de un separador común. En la función Collectors.joining se puede indicar también un prefijo y un sufijo para el texto.

Por ejemplo, los nombres de las personas adultas:

4.2.3 forEach

El método forEach permite recorrer cada elemento del stream resultante, y hacer lo que necesite con él. Por ejemplo sacar por pantalla en líneas separadas los nombres de las personas adultas:

4.2.4 Media

El método average permite, junto a la operación intermedia mapToint, obtener la media de un stream que haya producido una colección resultante numérica. Por ejemplo, la media de edades de las personas adultas.

4.3 Miscelánea con ejemplos

Clase Persona

Ejemplos:

 

 

5. Ficheros

5.1 Introducción

Cuando desarrollas programas, en la mayoría de ellos los usuarios pueden pedirle a la aplicación que realice cosas y pueda suministrarle datos con los que se quiere hacer algo. Una vez introducidos los datos y las órdenes, se espera que el programa manipule de alguna forma esos datos, para proporcionar una respuesta a lo solicitado.

Además, normalmente interesa que el programa guarde los datos que se le han introducido, de forma que si el programa termina, los datos no se pierdan y puedan ser recuperados en una sesión posterior. La forma más normal de hacer esto es mediante la utilización de ficheros, que se guardarán en un dispositivo de memoria no volátil (normalmente un disco).

También podemos necesitar acceder a datos que nos proporcionan en un fichero:

Por tanto, vemos que el almacenamiento en variables es temporal, los datos se pierden en las variables cuando están fuera de su ámbito o cuando el programa termina. Las computadoras utilizan ficheros para guardar los datos, incluso después de que el programa termine su ejecución. Se suele denominar a los datos que se guardan en ficheros datos persistentes, porque existen, persisten más allá de la ejecución de la aplicación. Los ordenadores almacenan los ficheros en unidades de almacenamiento secundario como discos duros, discos ópticos, etc. En esta unidad veremos cómo hacer con Java estas operaciones de crear, actualizar y procesar ficheros.

Todas estas operaciones con ficheros suponen un flujo de información del programa con el exterior, con el dispositivo en el que está el fichero, y se las conoce como operaciones de Entrada/Salida (E/S).

Normalmente, distinguimos dos tipos de E/S:

Todas las operaciones de E/S en Java vienen proporcionadas por el paquete estándar del API de Java denominado java.io que incorpora interfaces, clases y excepciones para acceder a todo tipo de ficheros.

 

5.1.1 Clase File

En el paquete java.io se encuentra la clase File pensada para poder realizar operaciones de información sobre archivos. No proporciona métodos de acceso a los archivos, sino operaciones a nivel de sistema de archivos (listado de archivos, crear carpetas, borrar ficheros, cambiar nombre,...). Un objeto File representa un archivo o un directorio y sirve para obtener información (permisos, tamaño,…). También sirve para navegar por la estructura de archivos.

Documentación oficial:

Construcción de objetos de archivo

Utiliza como único argumento una cadena que representa una ruta en el sistema de archivo. También puede recibir, opcionalmente, un segundo parámetro con una ruta segunda que se define a partir de la posición de la primera.

El primer formato utiliza una ruta absoluta y el segundo una ruta relativa. En Java el separador de archivos tanto para Windows como para Linux es el símbolo /. Otra posibilidad de construcción es utilizar como primer parámetro un objeto File ya hecho. A esto se añade un segundo parámetro que es una ruta que cuenta desde la posición actual.

Si el archivo o carpeta que se intenta examinar no existe, la clase File no devuelve una excepción. Habrá que utilizar el método exists. Este método recibe true si la carpeta o archivo es válido (puede provocar excepciones SecurityException). También se puede construir un objeto File a partir de un objeto URI.

El problema en las rutas

Cuando se crean programas en Java hay que tener muy presente que no siempre sabremos qué sistema operativo utilizará el usuario del programa. Esto provoca que la realización de rutas sea problemática porque la forma de denominar y recorrer rutas es distinta en cada sistema operativo. Por ejemplo en Windows se puede utilizar la barra / o la doble barra invertida \ como separador de carpetas, en muchos sistemas Unix sólo es posible la primera opción. También se pueden utilizar las variables estáticas que posee File. Estas son:

PropiedadUso
static char separatorCharEl carácter separador de nombres de archivo y carpetas. En Linux/Unix es / y en Windows es \, que se debe escribir como \, ya que el carácter permite colocar caracteres de control, de ahí que haya que usar la doble barra. Pero Windows admite también la barra simple (/)
static String separatorComo el anterior pero en forma de String
static char pathSeparatorCharEl carácter separador de rutas de archivo quepermite poner más de un archivo en una ruta. En Linux/Unix suele ser “:”, en Windows es “;”
static String pathSeparatorComo el anterior, pero en forma de String

Para poder garantizar que el separador usado es el del sistema en uso:

Normalmente no es necesaria esta comprobación ya que Windows acepta también el carácter / como separador.

 

métodos generales

métodouso
toString()Para obtener la cadena descriptiva del objeto
boolean exists()Devuelve true si existe la carpeta o archivo.
boolean canRead()Devuelve true si el archivo se puede leer
boolean canWrite()Devuelve true si el archivo se puede escribir
boolean isHidden()Devuelve true si el objeto File es oculto
boolean isAbsolute()Devuelve true si la ruta indicada en el objeto
boolean equals(File f2)Compara f2 con el objeto File y devuelve
int compareTo(File f2)Compara basado en el orden alfabético del texto (sólo funciona bien si ambos archivos son de texto) f2 con el objeto File y devuelve cero si son iguales, un entero negativo si el orden de f2 es mayor y positivo si es menor
String getAbsolutePath()Devuelve una cadena con la ruta absoluta al Objeto File.
File getAbsoluteFile()Como la anterior pero el resultado es un objeto File
String getName()Devuelve el nombre del objeto File.
String getParent()Devuelve el nombre de su carpeta superior si la hay y si no null
File getParentFile()Como la anterior pero la respuesta se obtiene en forma de objeto File.
boolean setReadOnly()Activa el atributo de sólo lectura en la carpeta o archivo.
URL toURL() throws MalformedURLExceptionConvierte el archivo a su notación URL correspondiente
URI toURI()Convierte el archivo a su notación URI Correspondiente

 

Métodos de Carpetas (Directorios)

métodouso
boolean isDirectory()Devuelve true si el objeto File es una carpeta y false si es un archivo o si no existe.
boolean mkdir()Intenta crear una carpeta y devuelve true si fue posible hacerlo
boolean mkdirs()Usa el objeto para crear una carpeta con la ruta creada para el objeto y si hace falta crea toda la estructura de carpetas necesaria para crearla.
boolean delete()Borra la carpeta y devuelve true si puedo hacerlo
String[] list()Devuelve la lista de archivos de la carpeta representada en el objeto File.
static File[] listRoots()Devuelve un array de objetos File, donde cada objeto del array representa la carpeta raíz de una unidad de disco.
File[] listfiles()Igual que la anterior, pero el resultado es un array de objetos File.

Métodos de archivos

métodouso
boolean isFile()Devuelve true si el objeto File es un archivo y false si es carpeta o si no existe.
boolean renameTo(File f2)Cambia el nombre del archivo por el que posee el archivo pasado como argumento. Devuelve true si se pudo completar la operación.
boolean delete()Borra el archivo y devuelve true si puedo hacerlo long length() Devuelve el tamaño del archivo en bytes (en el caso del texto devuelve los caracteres del archivo)
boolean createNewFile() throws IOExceptionCrea un nuevo archivo basado en la ruta dada al objeto File. Hay que capturar la excepción IOException que ocurriría si hubo error crítico al crear el archivo. Devuelve true si se hizo la creación del archivo vacío y false si ya había otro archivo con ese nombre.
static File createTempFile(String prefijo, String sufijo) throws IOExceptionCrea un objeto File de tipo archivo temporal con el prefijo y sufijo indicados. Se creará en la carpeta de archivos temporales por defecto del sistema. El prefijo y el sufijo deben de tener al menos tres caracteres (el sufijo suele ser la extensión), de otro modo se produce una excepción del tipo IllegalArgumentsException Requiere capturar la excepción IOException que se produce ante cualquier fallo en la creación del archivo
static File createTempFile( String prefijo, String sufijo, File directorio)Igual que el anterior, pero utiliza el directorio indicado.
void deleteOnExit()Borra el archivo cuando finaliza la ejecución del programa

 

Más información aquí: https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/io/File.html#method-detail

 

5.1.2 Clases para la entrada y la salida

Java.io

 

Java se basa en las secuencias de datos para dar facilidades de entrada y salida. Una secuencia es una corriente de datos entre un emisor y un receptor de datos en cada extremo. Normalmente las secuencias son de bytes, pero se pueden formatear esos bytes para permitir transmitir cualquier tipo de datos.

Los datos fluyen en serie, byte a byte. Se habla entonces de un stream (corriente de datos, o mejor dicho, corriente de bytes). Pero también podemos utilizar streams que transmiten caracteres Java (tipo char Unicode, de dos bytes), se habla entonces de un reader (si es de lectura) o un writer (escritura).

En el caso de las excepciones, todas las que provocan las excepciones de E/S son derivadas de IOException o de sus derivadas. Además son habituales ya que la entrada y salida de datos es una operación crítica porque con lo que la mayoría de operaciones deben ir inmersas en un try.

 

Corrientes de bytes. InputStream/ OutputStream

Los Streams de Java son corrientes de datos binarios accesibles byte a byte. Estas dos clases abstractas, definen las funciones básicas de lectura y escritura de una secuencia de bytes pura (sin estructurar). Estas corrientes de bits, no representan ni textos ni objetos, sino datos binarios puros. Poseen numerosas subclases; de hecho casi todas las clases preparadas para la lectura y la escritura, derivan de estas.

Los métodos más importantes son read (leer) y write (escribir), que sirven para leer un byte del dispositivo de entrada o escribir un byte respectivamente.

 

Métodos de InputStream

Métodouso
int available()Devuelve el número de bytes de entrada
void close()Cierra la corriente de entrada. Cualquier acceso posterior generaría una IOException.
void mark(int bytes)Marca la posición actual en el flujo de datos de entrada. Cuando se lea el número de bytes indicado, la marca se elimina.
boolean markSupported()Devuelve verdadero si en la corriente de entrada es posible marcar mediante el método mark.
int read()Lee el siguiente byte de la corriente de entrada y le almacena en formato de entero. Devuelve -1 si estamos al final del fichero
int read(byte[] búfer)Lee de la corriente de entrada hasta llenar el array búfer.
void reset()Coloca el puntero de lectura en la posición marcada con mark.
long skip()Se salta de la lectura el número de bytes indicados

 

Métodos de OutputStream

Métodouso
void close()Cierra la corriente de salida. Cualquier acceso posterior generaría una IOException.
void flush()Vacía los búferes de salida de la corriente de datos
void write(int byte)Escribe un byte en la corriente de salida
void write(byte[] bufer)Escribe todo el array de bytes en la corriente de salida
void write( byte[] buffer, int posInicial, int numBytes )Escribe el array de bytes en la salida, pero empezando por la posición inicial y sólo la cantidad indicada por numBytes.

5.1.3 Reader/Writer

Clases abstractas que definen las funciones básicas de escritura y lectura basada en texto Unicode. Se dice que estas clases pertenecen a la jerarquía de lectura/escritura orientada a caracteres, mientras que las anteriores pertenecen a la jerarquía orientada a bytes. Aparecieron en la versión 1.1 y no substituyen a las anteriores. Siempre que se pueda es más recomendable usar clases que deriven de estas.

Poseen métodos read y write adaptados para leer arrays de caracteres.

Métodos reader

Métodouso
void close()Cierra la corriente de entrada. Cualquier acceso posterior generaría una IOException.
void mark(int bytes)Marca la posición actual en el flujo de datos de entrada. Cuando se lea el número de bytes indicado, la marca se elimina.
boolean markSupported()Devuelve verdadero si en la corriente de entrada es posible marcar mediante el método mark.
int read()Lee el siguiente byte de la corriente de entrada y le almacena en formato de entero. Devuelve -1 si estamos al final del fichero
int read(byte[] búfer)Lee de la corriente de entrada bytes y les almacena en el búfer. Lee hasta llenar el búfer.
int read( byte[] bufer, int posInicio, int despl)Lee de la corriente de entrada bytes y les almacena en el búfer. La lectura la almacena en el array pero a partir de la posición indicada, el número máximo de bytes leídos es el tercer parámetro
boolean ready()Devuelve verdadero si la corriente de entrada está lista.
void reset()Coloca el puntero de lectura en la posición marcada con mark.
long skip()Se salta de la lectura el número de bytes indicados.

Métodos de Writer

Métodouso
void close()Cierra la corriente de salida. Cualquier acceso posterior generaría una IOException.
void flush()Vacía los búferes de salida de la corriente de datos.
void write(int byte)Escribe un byte en la corriente de salida
void write(byte[] bufer)Escribe todo el array de bytes en la corriente de salida.
void write( byte[] buffer, int posInicial, int numBytes )Escribe el array de bytes en la salida, pero empezando por la posición inicial y sólo la cantidad indicada por numBytes.
void write(String texto)Escribe los caracteres en el String en la corriente de salida.
void write( String buffer, int posInicial, int numBytes )Escribe el String en la salida, pero empezando por la posición inicial y sólo la cantidad indicada por numBytes.

 

5.1.4 InputStreamReader/ OutputStreamWriter

Son clases que sirven para adaptar la entrada y la salida. La razón es que las corrientes básicas de E/S son de tipo Stream. Estas clases consiguen adaptarlas a corrientes Reader/Writer.

Puesto que derivan de las clases Reader y Writer, ofrecen los mismos métodos que éstas.

Para ello poseen un constructor que permite crear objetos InputStreamReader pasando como parámetro una corriente de tipo y objetos OutputStreamWriter partiendo de objetos OutputStream.

5.1.5 DataInputStream/DataOutputStream

Leen corrientes de datos de entrada en forma de byte, pero adaptándola a los tipos simples de datos (int, short, byte,..., String). Tienen varios métodos read y write para leer y escribir datos de todo tipo.

Ambas clases construyen objetos a partir de corrientes InputStream y OutputStream respectivamente.

Métodos de DataInputStream

Métodouso
boolean readBoolean()Lee un valor booleano de la corriente de entrada. Puede provocar excepciones de tipo IOException o excepciones de tipo EOFException, esta última se produce cuando se ha alcanzado el final del archivo y es una excepción derivada de la anterior, por lo que, si se capturan ambas, ésta debe ir en un catch anterior (de otro modo, el flujo del programa entraría siempre en la IOException).
byte readByte()Idéntica a la anterior, pero obtiene un byte. Las excepciones que produce son las mismas readChar(), readShort(), readLong(), readFloat(), readDouble() Como las anteriores pero devolviendo el tipo de
String readLine()Lee de la entrada caracteres hasta llegar a un salto de línea o al fin del fichero y el resultado le obtiene en forma de String
String readUTF()Lee un String en formato UTF (codificación norteamericana). Además de las excepciones comentadas antes, puede ocurrir una excepción del tipo UTFDataFormatException (derivada de IOException) si el formato del texto no está en UTF.

Métodos de OutputStreamWriter

La idea es la misma, los métodos son: writeBoolean, writeByte, writeBytes (para Strings), writeFloat, writeShort, writeUTF, writeInt, writeLong.

Todos poseen un argumento que son los datos a escribir (cuyo tipo debe coincidir con la función).

 

5.1.6 ObjectInputStream/ObjectOutputStream

 

Filtros de secuencia que permiten leer y escribir objetos de una corriente de datos orientada a bytes. Sólo tiene sentido si los datos almacenados son objetos. Tienen los mismos métodos que la anterior, pero aportan un nuevo método de lectura:

La clase ObjectOutputStream posee el método de escritura de objetos writeObject al que se le pasa el objeto a escribir. Este método podría dar lugar en caso de fallo a excepciones IOException, NotSerializableException o InvalidClassException.

 

5.1.7 BufferedInputStream/BufferedOutputStream/ BufferedReader/BufferedWriter

La palabra buffered hace referencia a la capacidad de almacenamiento temporal en la lectura y escritura. Los datos se almacenan en una memoria temporal antes de ser realmente leídos o escritos. Se trata de cuatro clases que trabajan con métodos distintos pero que suelen trabajar con las mismas corrientes de entrada que podrán ser de bytes (Input/OutputStream) o de caracteres (Reader/Writer).

La clase BufferedReader aporta el método readLine que permite leer caracteres hasta la presencia de null o del salto de línea.

5.1.8 PrintWriter

Clase pensada para secuencias de datos orientados a la impresión de textos. Es una clase escritora de caracteres en flujos de salida, que posee los métodos print y println, que otorgan gran potencia a la escritura.

5.1.9 PipedInputStream/PipedOutputStream

Permiten realizar canalizaciones entre la entrada y la salida; es decir lo que se lee se utiliza para una secuencia de escritura o al revés.

 

5.2 lectura y escritura en archivos

5.2.1 Ficheros con la clase Scanner

Si cuando creamos nuestro objeto de la clase Scanner, en vez de System.in le pasamos como parámetro una instancia de la clase File, podremos conectar con el fichero referenciado y leer su contenido. Los métodos que usaremos serán los mismo que hemos utilizado hasta ahora para leer del teclado: next(),nextInt(),nextDouble() y nextLine().

Por último, cuando hayamos acabado de trabajar con el fichero, cerraremos la conexión con él mediante el método close de la clase Scanner:

Los objetos de la clase Scanner, leen elementos separados por los delimitadores por defecto. Estos son los espacios en blanco y los saltos de línea pero podemos configurar los que queramos mediante el método delimiter().

Si integramos todo lo visto hasta ahora para crear un programa que cuente las palabras de un fichero de texto, el resultado sería:

Recuerda que:

Si queremos leer números de tipo enteros o double o líneas enteras de caracteres, usaremos los métodos nextInt(), nextDouble() y nextLine() respectivamente.

Cuando leemos un número de tipo double, dependiendo de la configuración de nuestro sistema operativo, buscará un punto (US o EN) o una coma (ES) para identificar los decimales.

Podemos aprovechar los metodos locale() y useLocale de la clase Scanner para identificar la configuración que va a usar y cambiarla si nos interesa:

Leer ficheros línea a línea

Hasta ahora hemos leído el contenido de un fichero elemento a elemento, dependiendo del tipo de dato que era. Sin embargo, en algunos casos puede resultar útil leer cada una de las líneas del fichero y luego procesarlas. Todas tendrán el mismo formato y por tanto tendrán que ser procesadas de la misma manera, como en el siguiente fichero en el que en cada línea se almacena el identificador, el nombre y todas las calificaciones de un alumno o alumna.

En este caso, el main podría quedar como sigue y podría utilizarse para más de una aplicación:

La diferencia estaría en el nombre del fichero y en lo que hace el método procesarLinea().

Si quisiéramos leer el fichero anterior y mostrar la información con el siguiente formato:

El programa quedaría:

En este caso, al método procesarLinea se le pasa como parámetro la línea que se ha leído del fichero y se utiliza para crear un nuevo objeto de la clase Scanner que nos permite leer el texto de esa línea como si viniera del teclado o de un fichero.

Errores frecuentes

Al principio nos puede ocurrir, que cuando juntamos las líneas que crean el objeto de la clase File y el objeto de clase Scanner para leer un fichero, se nos olvide crear la instancia de la clase File y en vez de escribir:

Escribamos:

La línea parece correcta y Java no nos da error al compilar. Crea el objeto a partir de la cadena de caracteres "fichero.txt" y por tanto cuenta una única palabra.

Código alternativo y mejorado. En este vamos a controlar el acceso a los datos. Desde revisar la existencia del fichero hasta en la gestión de problemas de lectura con try-with-resources y try-catch tradicional:

 

Ahora veamos como podemos realizar el proceso inverso, la escritura. Para ello haremos uso de FileWriter que nos va a permitir escribir en los ficheros y añadirle el método Append de tal manera que añadamos al final.

 

5.2.2 clases FileInputStream y FileOutputStream

Se trata de las clases que manipulan archivos. Son herederas de Input/OutputStream, por lo que manejan corrientes de datos en forma de bytes binarios. La diferencia es que se construyen a partir de objetos de tipo File.

5.2.3 lectura y escritura byte a byte de un archivo

Para leer necesitamos un archivo del que dispongamos permisos de escritura y su ruta o bien un objeto File que le haga referencia. Con ello creamos una corriente de tipo FileInputStream:

 

La construcción de objetos FileOutputStream se hace igual, pero además se puede indicar un parámetro más de tipo booleano que con valor true permite añadir más datos al archivo (normalmente al escribir se borra el contenido del archivo, valor false y por defecto).

Estos constructores intentan abrir el archivo, generando una excepción del tipo FileNotFoundException si el archivo no existiera u ocurriera un error en la apertura. Los métodos de lectura y escritura de estas clases son los heredados de las clases InputStream y OutputStream; fundamentalmente los métodos read y write son los que permiten leer y escribir. El método read devuelve -1 en caso de llegar al final del archivo.

Este método lee el archivo de forma absolutamente binaria los archivos y sólo es válido cuando deseamos leer toda la información del archivo.

Ejemplo de lectura:

Ejemplo de escritura

5.2.4 lectura y escritura de archivos de texto

Como ocurría con la entrada estándar, se puede convertir un objeto FileInputStream o FileOutputStream a forma de Reader o Writer mediante las clases InputStreamReader y OutputStreamWriter. Y esto es más lógico cuando manejamos archivos de texto.

Existen además dos clases que manejan caracteres en lugar de bytes (lo que hace más cómodo su manejo), son FileWriter y FileReader.

La construcción de objetos del tipo FileReader se hace con un parámetro que puede ser un objeto File o un String que representarán a un determinado archivo.

La construcción de objetos FileWriter se hace igual sólo que se puede añadir un segundo parámetro booleano que, en caso de valer true, indica que se abre el archivo para añadir datos; en caso contrario se abriría para grabar desde cero (se borraría su contenido).

Para escribir se utiliza write que es un método void que recibe como parámetro lo que se desea escribir en formato int, String o en forma de array de caracteres.

Por ejemplo este el código de un programa que lee por teclado texto hasta que el usuario deja vacía la línea y todo lo escrito lo vuelca en un archivo llamado salida.txt:

 

Para leer se utiliza el método read que devuelve un int y que puede recibir un array de caracteres en el que se almacenarían los caracteres leídos. Ambos métodos pueden provocar excepciones de tipo IOException.

No obstante sigue siendo un método todavía muy rudimentario. Por ello lo ideal es convertir el flujo de las clases File en clases de tipo BufferedReader y BufferedWriter vistas anteriormente . Su uso sería:

 

Escritura mejorada con BufferedWriter. Tiene incorporado el método Append, que es un booleano:

 

La escritura se realiza con el método write que permite grabar caracteres, Strings y arrays de caracteres. BufferedWriter además permite utilizar el método newLine que escriba un salto de línea en el archivo; lo que arregla el problema de la compatibilidad entre plataformas por que los caracteres para el cambio de párrafo son distintos según cada sistema operativo (o incluso por diferentes circunstancias).

5.2.5 archivos binarios

Para archivos binarios se suelen utilizar las clases DataInputStream y DataOutputStream. Estas clases están mucho más preparadas para escribir datos de todo tipo.

escritura en archivos binarios

El proceso sería:

  1. Crear un objeto FileOutputStream a partir de un objeto File que posea la ruta al archivo que se desea escribir (para añadir usar el segundo parámetro del constructor indicando true)

  2. Crear un objeto DataOutputStream asociado al objeto anterior. Esto se realiza mediante el constructor de DataOutputStream.

  3. Usar el objeto del punto 2 para escribir los datos mediante los métodos writeTipo donde tipo es el tipo de datos a escribir (int, double, ...). A este método se le pasa como único argumento los datos a escribir.

  4. Se cierra el archivo mediante el método close del objeto DataOutputStream.

 

Ejemplo:

Lectura de este y muestra por pantalla:

No obstante, haciendo uso de FileInputStream/DataInputStream para escritura y FileOutputStream/DataOutputStream para lectura no son los métodos más eficientes ya que se realizan numerosos accesos a discto.

Para mejorar este se puede hacer uso de BufferedOutputStream que reduce estos accesos al disco y escribe en bloques.

 

Ejemplo de escritura y lectura:

Puedes observar que hay una funcionalidad del Try /Catch que no habíamos trabajado previamente.

Se trata del Try with resources. Este nos va a facilitar el cierre de recursos cuando trabajamos con ficheros y conexiones.

Podemos ver un poco más de información aquí:

https://keepcoding.io/blog/que-es-try-with-resources-en-java/

MétodoVelocidadUso de memoriaComentario
FileOutputStream y FileInputStreamLentoBajoMuchas operaciones de E/S individuales
DataOutputStreamyDataInputStreamMedioMedioEscribe y lee tipos primitivos de manera estructurada
BufferedOutputStreamyBufferedInputStreamRápidoMedio-AltoReduce accesos al disco, ideal para archivos grandes

 

5.2.6 Archivos de acceso aleatorio

Hasta ahora los archivos se están leyendo secuencialmente. Es decir desde el inicio hasta el final. Pero es posible leer datos de una zona concreta del archivo.

Por supuesto esto implica necesariamente dominar la estructura del archivo, pero además permite crear programas muy potentes para manejar archivos de datos binarios.

 

Archivos de acceso aleatorio

Hasta ahora los archivos se están leyendo secuencialmente. Es decir desde el inicio hasta el final. Pero es posible leer datos de una zona concreta del archivo.

Por supuesto esto implica necesariamente dominar la estructura del archivo, pero además permite crear programas muy potentes para manejar archivos de datos binarios.

 

RandomAccessFile

Esta clase permite leer archivos en forma aleatoria. Es decir, se permite leer cualquier posición del archivo en cualquier momento. Los archivos anteriores son llamados secuenciales, se leen desde el primer byte hasta el último.

Esta es una clase primitiva que implementa las interfaces DataInput y DataOutput y sirve para leer y escribir datos. La construcción requiere de una cadena que contenga una ruta válida a un archivo o de un archivo File. Hay un segundo parámetro obligatorio que se llama modo. El modo es una cadena que puede contener una r (lectura), w (escritura) o ambas, rw.

Como ocurría en las clases anteriores, hay que capturar la excepción FileNotFound cuando se ejecuta el constructor para el caso de que haya problemas al crear el objeto File.

Los métodos fundamentales son:

 

6. Ficheros de objetos (Serialización)

El inconveniente de guardar la información en modo binario como hemos hecho antes es que es necesario leer en el mismo orden en el que hemos escrito los datos. Además, nosotros estamos trabajando ya en los objetos como unidad. Entonces lo que nos interesará es escribir en el fichero los distintos objetos y después leeremos objetos.

Para ello, los objetos necesitan ser serializados (lo podemos entender cómo poner todos los datos de un objeto en binario y en serie, unos detrás de otros). Simplemente debemos indicarlo en la definición de la clase. Así:

Y en la siguiente tabla tenemos las clases que hay que utilizar para utilizar los métodos de lectura y escritura sobre ficheros de objetos:

Clase o MétodoDescripción
ObjectOutputStreamIncorpora los métodos para escribir objetos en un archivo. La clase debe implementar Serializable
writeObject(Object o)Escribir el objeto o
close()Cerrar el archivo
writeTipo(Tipo t)Similar a los archivos binarios
ObjectInputStreamIncorpora los métodos para leer objetos de un archivo. La clase debe implementar Serializable
(ObjecteDesti) readObject()Lee un objeto. Debemos realizar un casting en el objeto de destino (Persona, Alumno, Coche, etc.)
Tipo readTipo()Similar a los archivos binarios
skipBytes()Salta una cierta cantidad de bytes

Para escribir objetos en el archivo llamaremos al método writeObject(), pasándole como parámetro el objeto que queremos escribir. La escritura es secuencial y destructiva (cada vez se creará un nuevo archivo).

Para leer del archivo de objetos llamaremos al método readObject(), que nos devuelve un objeto pero de la clase Object. Por tanto, deberemos hacerle un casting para convertirlo al tipo de objeto que estamos leyendo. Ahora bien, antes de leer objetos, habrá que asegurarse de que quedan por leer. Para ello, llamaremos al método available() (de la clase FileInputStream), el cual devuelve la cantidad de bytes que faltan por leer en el archivo.

Si el tipo de Objeto que intentamos guardar no implementa la interfaz Serializable, saltará la excepción java.io.NotSerializableException.

6.1 Añadir objetos. Problemática

Si queremos añadir objetos a un archivo ya dado (con ciertos objetos) debemos realizar unas pequeñas modificaciones, debido al siguiente problema.

Cuando escribimos objetos con writeObject(), la clase ObjectOutputStream escribe una cabecera con metainformación del objeto que va a escribir a continuación. Si queremos añadir objetos, lo habitual es abrir el archivo de nuevo, con la opción append a true y situarse al final del archivo y escribir los nuevos objetos.

Después si intentamos leer el archivo en el que hemos guardado la información nos encontraremos que tenemos varias cabeceras en medio del archivo y como no sabemos a priori la cantidad de objetos tenemos, entonces nos dará errores nuestro código, ya que intentamos leer un objeto y estaremos leyendo una cabecera.

AfegirObjectes

La solución pasa por evitar que se escriba la cabecera de los objetos en futuras adiciones. Por eso debemos hacer lo siguiente

Nos implementamos un ObjectOutputStream propio de forma que redefinimos el método encargado de escribir la cabecera que no haga nada. Entonces:

  1. Cuando creamos el archivo utilizaremos ObjectOutputStream.

  2. Cuando añadimos datos al archivo utilizaremos MiObjectOutputStream.

6.2 Manera de trabajar

Con programas con gran volumen de información, los datos inicialmente estarán en el disco duro dentro de un archivo. Nuestro programa, para utilizar y manipular esos datos, los llevará a memoria principal volcando los objetos del archivo en un vector de objetos.

A continuación, los datos podrán ser consultados, borrados, introducir nuevos objetos, etc.

Y, cuando queremos terminar, volcaremos el vector en el disco duro, escribiendo todos los objetos del vector en el archivo que teníamos.

Como alternativa, podemos leer y escribir todos los objetos de una manchada, puesto que, como un ArrayList o vector es un objeto también, podemos escribirlo todo entero con writeObject, en vez del típico bucle que va recorriendo el array y procesándolo uno a uno.

 

Ejemplo

 

Clase Empleado

 

Redifinición de ObjectOutputStream

Programa Principal