Almacenando Información en Java (Ficheiros)

De Wiki do Ciclo ASIR do IES de Rodeira
Saltar á navegación Saltar á procura

Conceptos Básicos

Soporte, Organización e Acceso

Os Soportes

Un sistema informático xestiona a información almacenada na súa memoria RAM en formato binario, sendo procesada pola CPU para producir información nova que é almacenada de volta na memoria RAM do sistema. A memoria RAM é volátil, polo que si desconectamos o equipo a información almacenada se perde. Polo tanto é necesario almacenar a información binaria en algún tipo de soporte de información permanente.

Boxinfo info.png
Se entende por soporte calquera dispositivo que almacena información. Poden ser soportes volátiles (como a RAM) ou permanentes (como un SSD ou unha cinta magnética)

Os soportes de almacenamento permanente máis utilizados son os soportes magnéticos, que nos últimos anos están sendo substituídos polas memorias SSD. En canto ao xeito en que permiten almacenar e recuperar a información, podemos clasificar os soportes de almacenamento en:

  • Soportes Secuenciais: A información se almacena unha detrás de outra segundo vai chegando, non é posible retroceder nin avanzar a unha posición de escritura concreta sin pasar por enriba de información previa. Entre estes tipos de soporte podemos destacar as cintas magnéticas.
  • Soportes de Acceso Aleatorio: Permiten acceder a posicións concretas do soporte de xeito directo para realizar operacións de lectura e escritura. De este tipo de soportes podemos destacar os Discos Magnéticos (HDD) ou as memorias de estado sólido (SDD).

Os dispositivos máis comúns de almacenamento utilizados polos programas son soportes de acceso aleatorio, principalmente HDD e SDD. O xeito de organizar a información en estes tipos de soporte é similar. Dende o punto de vista da programación o concepto máis importante é o de bloque físico.

Boxinfo info.png
Un bloque físico é a cantidade de información mínima que se lee ou escribe no soporte de información. Por exemplo, si utilizamos un bloque físico de 4K isto quere dicir que a información se intercambia entre o disco e a RAM en "anacos" de 4K

O Sistema de Arquivos

Os soportes nos dan a posibilidade de almacenar información, pero para poder gardala e recuperala de un xeito eficiente necesitamos organizala dun xeito "comprensible". O xeito en que se organiza a información nun soporte para poder acceder á mesma con facilidade se coñece como sistema de arquivos.

Sistemas de arquivos de uso común son FAT32, VFAT, NTFS, EXT4, ZFS, BTRFS ... etc, cada un de eles con diferentes características e limitacións. Sen embargo, teñen algo en común: Almacenan a información en Ficheiros (ou Arquivos) e a organizan en Directorios (ou "carpetas"),

Boxinfo info.png
O concepto de carpeta non é máis que unha metáfora para referirnos a un lugar no que agrupamos un conxunto de arquivos, e se coñece así porque nas contornas gráficas de usuario se representan coa icona dunha carpeta. O concepto se denomina realmente directorio

Un arquivo é o lugar onde almacenamos información en formato binario pertencente a un "concepto" concreto. Por exemplo podemos ter información dunha factura, un documento de texto, un vídeo ou unha fotografía. Podemos distinguir entre dous tipos de arquivos diferentes:

  • arquivos de texto: Nos arquivos de texto a información binaria almacenada (nun arquivo informático e nos sistemas informáticos toda a información está sempre en binario, e decir representando únicamente valores 0 e valores 1) se interpreta como caracteres. Estes caracteres poden ser interpretados segundo varias codificacións (ISO, UTF-8, UTF-16) e algúns deles teñen un significado "especial" (como o salto de liña). Para interpretar correctamente un arquivo de texto é necesario coñecer que codificación de caracteres utiliza.
  • arquivos binarios: Nos arquivos binarios a información non ten unha interpretación concreta. Son arquivos que conteñen bits que poden representar absolutamente calquera cousa: Un programa, unha canción, unha fotografía, .... etc. O modo en que se almacena a información se coñece como "formato" do arquivo. Formatos comúns son: class, EXE, PDF, MP4, AVI, MP3, OGG, DOC, MKV, .... etc. Habitualmente para que unha persoa recoñeza de inmediato o tipo de arquivo (o seu formato) se utiliza unha "extensión" para o nome do arquivo que consiste nun punto seguido de 3 letras. Os programas poden ter en conta esa extensión ou non facelo.

Os sistemas de arquivos organizan os arquivos en distintos directorios formando unha árbore na que o raíz ou inicio é o directorio / (\ en Windows). A partir de ahí se van creando directorios e subdirectorios formando unha árbore do tamaño que se desexe. En todos os directorios existen dous directorios "especiais". O directorio . que representa a posición actual, e o directorio .. que representa o directorio anterior ao actual.

Para referirnos a un directorio ou arquivo do sistema concreto necesitamos especificar o seu camiño ou ruta (path). O path pode ser:

  • Relativo: Se van listando os directorios polos que necesitamos pasar ata alcanzar o destino contando dende a nosa posición actual. Se separará un directorio de outro utilizando o caracter / si utilizamos un sistema UNIX/Linux ou \ si utilizamos un sistema Windows.
  • Absoluto: Se van listando os directorios polos que necesitamos pasar ata alcanzar o destino contando dende o raíz (e polo tanto o path absoluto sempre comeza por / ou por \ si estamos en Windows).
Boxinfo info.png
En Windows existe un concepto que non se utiliza nos sistemas Linux/Unix, que é o concepto de "unidade". Baixo Windows o espazo de almacenamento está distribuído en unha ou varias unidades que se nomean cunha letra. Para crear o path é necesario antepoñer a letra da unidade seguida de dous puntos: Por exemplo C:\Documentos\Java\Titorial.doc sería o camiño ao ficheiro Titorial.doc dende a carpeta raíz (path absoluto) da unidade C:. Nos sistemas UNIX/Linux non se utiliza o concepto de "unidade", polo que o camiño sería /Documentos/Java/Titorial.doc
Boxinfo info.png
Tamén é importante o concepto de URI (Uniform Resource Identifier) e de URL (Uniform Resource Locator). Unha URI identifica un recurso de xeito que o podemos distinguir de outros recursos. Un camiño a un ficheiro, por exemplo, é unha URI. Existen dous tipos de URI:
  • URN: Os URN son "persistentes" no sentido que sempre identificarán o mesmo recurso.
  • URL: Os URL non son necesariamente "persistentes" e ademáis de identificar un recurso proporcionan información sobre como acceder ao mesmo normalmente indicando o protocolo de acceso antes do identificador (file://, http:// ftp://)

Uso do sistema de arquivos en Java: A clase File

A clase File do API de Java nos permite todas as accións posibles sobre un sistema de arquivos, as principais son:

  • Crear un Arquivo: createNewFile
  • Crear unha Carpeta: mkdir / mkdirs
  • Determinar si un arquivo ou carpeta existe: exists
  • Eliminar un Arquivo ou Carpeta: delete
  • Listar os arquivos dunha carpeta: list
  • Renomear un arquivo ou unha carpeta: renameTo
  • Obter atributos dun arquivo: isDirectory, isFile, isHidden, length, lastModified, canExecute, canRead, canWrite
EXERCICIO
Escribir un programa Java que amose un prompt path a carpeta actual > e nos permita movernos mediante o comando cd, crear directorios co comando md, crear arquivos co comando create listar os arquivos mediante o comando ls, renomear arquivos co comando ren e eliminar arquivos co comando rm. Tamén debe permitir obter o tamaño e a data de modificación dun arquivo ou directorio co comando info

A organización: Campos, Rexistros e Bloques

Cando almacenamos información dentro dun arquivo o modo de recuperala depende de varios factores, sendo os máis importantes o tipo de soporte no que se almacena a información (secuencial ou directo) e como se organiza a información dentro do propio ficheiro. A información que se almacena nun arquivo se divide habitualmente en rexistros. Un rexistro é un bloque de información con unha relación moi forte entre sí. Podemos comparalo ao conxunto de atributos de unha clase. Por exemplo si almacenamos nun ficheiro os nomes e notas dunha serie de alumnos o rexistro estaría composto do nome do alumno e da súa nota.

Cada unha das distintas informacións que compoñen o rexistro se coñece como campo. Facendo un símil, si consideramos que un rexistro é similar a un obxecto, un campo ten relación cun atributo.

Cando almacenamos datos nun ficheiro, os organizamos en modo de rexistros, que están compostos de campos.

Un bloque é o conxunto de rexistros que lemos ou escribimos de unha soa vez. Un bloque pode ser un único rexistro ou máis de un. O óptimo é que o tamaño do bloque coincida co tamaño do bloque físico que utilice o soporte.

Boxinfo info.png
Non en todos os ficheiros ten sentido falar de rexistros e campos. Por exemplo nun vídeo en formato mp4 ou un arquivo de música mp3 o concepto de "rexistro" e "campo" é moito menos claro. Nos ficheiros de texto en todo caso poderíamos falar de liñas e non de rexistros

Dependendo como sexan os rexistros podemos distinguir entre:

  • Ficheiros con rexistro de lonxitude fixa: Todos os rexistros teñen o mesmo tamaño. Isto permite unha xestión máis simple e efectiva dos datos xa que podemos coñecer con antelación onde comeza cada rexistro almacenado no ficheiro.
  • Ficheiros con rexistro de lonxitude variable: Cada rexistro pode ocupar un número distinto de bytes. Isto complica a xestión e o acceso directo a un rexistro concreto, pero aforra espazo no disco.

Dependendo do tipo de soporte que utilicemos podemos organizar a información de varios xeitos:

  • Organización Secuencial: E o xeito de organizar a información mais simple. Se pode levar a cabo sobre soportes secuenciais ou directos. Os datos se escriben secuencialmente, un detrás de outro segundo van chegando.
  • Organización Directa: Os rexistros se almacenan nunha posición concreta derivada a partir do seu contido (hashing). É necesario un soporte de acceso directo e permite recuperar rapidamente o rexistro desexado. O problema principal desta organización é o potencial tamaño do arquivo (o ficheiro é tan grande como a posición do último rexistro) e a posibles colisións (cando a distintos rexistros se lles asigna a mesma posición)
  • Organización Indexada: Se trata de gardar os datos nun soporte de acceso directo e crear un índice no que se asocia cada rexistro (normalmente mediante un valor único dun campo do rexistro denominado "chave primaria") coa posición en que está gravado permitindo localizar moi rápido a posición do rexistro buscado. O índice pode organizarse de varios modos, sendo o máis común a organización secuencial ou de acceso aleatorio en forma de arbore B+
Boxinfo info.png
Nos soportes directos é posible establecer outro tipo de organizacións complexas para facilitar a inserción e borrado de información, como poden ser as listas enlazadas e dobremente enlazadas ou as árbores

O acceso

Cando falamos de acceso nos referimos ao xeito de recuperar e xestionar a información almacenada. Do mesmo modo que a organización está limitada polo tipo de soporte, o modo en que podemos acceder a información depende directamente de como estea organizada.

O xeito de realización das operacións típicas sobre os datos (inserción, actualización e borrado) depende tamén de como estean organizados os datos.

Para acceder os datos nun ficheiro (leer ou escribir) é necesario abrir o arquivo. A operación de apertura consiste en crear as estruturas en memoria para realizar o acceso ao dispositivo físico, sendo de particular importancia a caché do ficheiro e o punteiro do ficheiro. A caché do ficheiro permite acelerar as operacións facendo que a modificación se realice na memoria RAM (máis rápida) sendo volcada logo en segundo plano ao dispositivo (máis lento). O punteiro do ficheiro indica en todo momento o punto do ficheiro no que se vai a operar. Cada operación de lectura ou escritura fai que ese punteiro se actualice a nova posición automáticamente.

Si queremos non perder datos é necesario pechar o ficheiro cando xa non operemos máis con él. O peche asegura que os datos da caché se reflexen no dispositivo evitando así perdas accidentais de datos además de liberar todos os recursos ocupados polo acceso ao ficheiro.

Boxinfo info.png
E moi importante asegurarse que todos os ficheiros que se abren se pechan. E común para eso utilizar a cláusula finally nun bloque try ou o uso de try with resources

En canto o acceso, podemos falar de:

  • Acceso Secuencial: Sempre é posible acceder ós datos de xeito secuencial. O acceso secuencial consiste en ir lendo datos secuencialmente, dende a posición actual ata rematar todos os datos. Non é posible avanzar nin retroceder.
As distintas operacións están limitadas por este modo de acceso:
  • Para ler un rexistro concreto é necesario ler os anteriores
  • Eliminar/borrar información é moi complexo e costoso, xa que é necesario leer todos os datos anteriores ao punto de modificación.
  • So é posible engadir nova información ao final do arquivo. Insertar información noutro punto precisa da creación dun novo arquivo.
  • Acceso Directo: So é posible acceder mediante acceso directo si a información está organizada de xeito directo, o que implica que a posición de cada rexistro se calcula a partir da información almacenada. Si non é así, dado que o soporte ten que ser obrigatoriamente de acceso directo sigue sendo posible acceder directamente a rexistros arbitrarios si coñecemos a súa posición (si os rexistros son de lonxitude fixa coñeceremos a posición de comezo de cada rexistro), isto se coñece como acceso aleatorio.
  • Acceso Indexado: Dispoñemos dun "índice" (que pode estar organizado de diversos modos) que nos indica a posición de cada rexistro en función dun valor de campo especial que coñecemos como "chave"
Boxinfo info.png
A eliminación de información dun arquivo non é posible. Para "borrar" información é necesaria a creación dun novo arquivo omitindo a información a eliminar, o que resulta unha operación moi custosa en termos de rendimento. Para evitar esto se recurre a marcar o rexistro como eliminado mediante un valor especial nun dos seus campos ou reservando ao principio de cada rexistro un área de información sobre o contido do rexistro (metadatos) onde ademais de indicar si o rexistro é válido ou non podemos subministrar outra información como o número de bytes que ocupa o rexistro. Este sistema cando o volume de borrados é moi alto fai que o arquivo ocupe demasiado espazo de xeito innecesario, polo que se recurre a desfragmentación do arquivo creando un novo arquivo que omite os rexistros borrados. Unha boa estratexia na inserción de novos rexistros (aproveitando o espazo dos rexistros borrados) tamén reduce este problema. A organización en forma de lista tamén facilita a inserción e borrado. Por exemplo, mantendo unha lista de rexistros válidos e unha lista de rexistros borrados

Serialización, A interface Serializable

Os ficheiros almacenan bytes, é dicir, secuencias de 0 e 1. Sen embargo, a información que almacenamos e recuperamos é "mais complexa", como liñas de facturas, ventas anuais, clientes, vehículos, contas bancarias.... etc. Como sabemos, a información que almacenamos en ficheiros a organizamos en rexistros (ou liñas si son ficheiros de texto), e os rexistros están a súa vez divididos en campos. Polo tanto, é necesario un medio de convertir esa información (atributos dun obxecto) a bytes e viceversa. Eses procesos se coñecen como serialización e deserializacion respectivamente.

Mentres que serializar os tipos de datos primitivos (byte, short, int, long, float, double, boolean, char) e as súas clases correspondentes (Byte,Short,Integer,Long,Float,Double,Boolean,Char) é simple (xa que o número de bytes que ocupan é fixo e coñecido) a serialización de clases máis complexas non é tan simple. Para solucionar este "problema" Java proporciona unha interface implementada na maioría das clases do API denominada Serializable.

A interface Serializable non define ningún método, simplemente "activa" os seguintes métodos da clase Object e que podemos sobrepoñer para variar o seu comportamento:

private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;

Os fluxos de bytes (Streams) se verán nun tema posterior

Boxinfo info.png
Calquera clase que implements Serializable pode convertir os seus obxectos nun fluxo de bytes e viceversa (a partir dun fluxo de bytes reconstruír o obxecto). Para que un obxecto se poda serializar correctamente, todos os seus atributos deben ser Serializable
Boxinfo info.png
A clase String dispon dun método byte[] getBytes() que permite obter o array de byte que conforma o String e dun constructor String(byte[] data) que permite crear un String a partir dun array de bytes que o representa

A clase RandomAccessFile

A clase do API Java RandomAccessFile permite almacenar información nun ficheiro de acceso aleatorio no disco. Cando creamos un obxecto de este tipo se creará a estrutura na memoria necesaria para ler e escribir información no disco e o buffer de memoria correspondente. O almacenamento se realizará sobre un soporte de acceso aleatorio, pero para poder acceder de xeito directo aos datos é precisa tamén unha organización que o permita

Boxinfo info.png
Un concepto fundamental para a correcta xestión dos RandomAccessFile é o Punteiro do Ficheiro. O punteiro do ficheiro indica en todo momento a posición do ficheiro na que se levará a cabo a seguinte operación de lectura ou escritura, e se actualiza de xeito automático segundo lemos ou escribimos. RandomAccessFile nos proporciona métodos para coñecer o valor deste punteiro e para "movelo" á posición que desexemos.

Algúns métodos que nos proporcionará esta clase son:

 long length();  // Devolve o número de bytes que ocupa o ficheiro
 long getFilePointer(); // Indica a posición actual do punteiro do ficheiro
 void seek(long pos); // Coloca o punteiro do ficheiro na posición pos
 void close(); // Pecha o RandomAccessFile liberando todos os recursos ocupados e volcando o buffer a disco

Ver RandomAccessFile na documentación de Java

Boxinfo info.png
RandomAccessFile implementa Closeable o que permite o uso de try with resources para asegurar o correcto peche do ficheiro

ORM (Object Relational Mapping

Si empregamos unha base de datos relacional para almacenar os obxectos os atributos se almacenarían como campos en táboas da base de datos, correspondendo habitualmente cada clase a unha táboa específica. Habitualmente se empregan librarías de clases deseñadas para xestionar o almacenamento e recuperación de datos de xeito automático denominadas sistemas ORM (Object-Relational Mapping).

Un ORM é un conxunto de clases que permiten almacenar e recuperar obxectos nunha base de datos relacional de xeito transparente e simple evitando a dependencia de sistemas de bases de datos concretos e incluso sen necesidade de uso das linguaxes de consulta do xestor da base de datos (como SQL). O ORM se encarga de almacenar os atributos dos obxectos nas táboas cando “gardamos un obxecto” e de ler os atributos e reconstruír o obxecto cando “lemos un obxecto”. Un dos ORM máis empregados en Java é Hibernate. Hibernate pode facer uso de annotations (instruccións de preprocesamento que comezan por @) para asociar clases co almacenamento relacional e xestionar a carga e descarga de obxectos de xeito transparente.

import javax.persistence.*;

@Entity
@Table(name = "EMPLOYEE")
public class Employee {
  @Id @GeneratedValue
  @Column(name = "id")
  private int id;

  @Column(name = "first_name")
  private String firstName;

  @Column(name = "last_name")
  private String lastName;

  @Column(name = "salary")
  private int salary;

  public Employee() {}
.... getters y setters ...

podes ver aquí máis información sobre Hibernate

Sen embargo non é imprescindible o uso de ORM para almacenar e recuperar información dunha base de datos relacional, pode facerse mediante unha serie de clases que nos proporcionan unha API de acceso á base de datos dende Java, como JDBC (Java DataBase Connectivity). Si as necesidades de almacenamento e recuperación non son tan complexas, tamén se pode facer uso de simples ficheiros no noso sistema de almacenamento principal. O almacenamento de datos en ficheiros pode facerse en ficheiros secuenciais facendo uso de Streams de datos ou en ficheiros de acceso aleatorio mediante RandomAccessFile.

EXERCICIO
Precisamos de dous ficheiros de datos Traballadores.dat que almacena o nif, nomes e apelidos dos traballadores da empresa, e "Salarios.dat" que almacena o salario de cada un de eles nun número double. Se pide:

1.- Escribir un programa que cando se inicia carge todos os traballadores do arquivo de traballadores nun ArrayList e visualice un menú coas seguintes opcións:

1.- Listar Traballadores --> Visualizará na pantalla todos os traballadores indicando DNI, nome e apelidos.
2.- Borrar Traballador --> "Eliminará" o traballador do array e do disco.
3.- Engadir Traballador --> Se solicitará o DNI, nome, apelidos e salario e se engadirá aos ficheiros Traballadores.dat e Salarios.dat
4.- Modificar Salario --> Se solicitará o DNI, e si o traballador existe, se visualizarán os seus datos e se pedirá o novo salario actualizando o ficheiro Salarios.dat
5.- Ver Salario --> Solicitará o DNI e visualizará nome, apelidos e salario consultando o ficheiro Salarios.dat
6.- Sair

NOTA: A clase Traballador NON almacena o salario, se debe consultar/modificar no ficheiro Salarios.dat. O Borrado se realizará mediante unha "marca" no rexistro que nos indicará si é válido ou non é válido. Como os salarios están na posición correspondente ao traballador (o salario 5 corresponde ao traballador 5) os traballadores "borrados" supoñen un problema. Para evitalo, cada obxecto Traballador terá un atributo idx que indicará o número de traballador dentro do ficheiro tendo en conta os borrados. Si un traballador está "borrado" se entenderá que o seu salario no ficheiro de salarios tamén o está.

EXERCICIO
Un periódico quere crear unha páxina web na que salga a portada, de xeito que cando o usuario faga click no titular se visualice todo o corpo da noticia. Para facer iso, se crea un ficheiro "News.dat" organizado secuencialmente que contén o titular, a entradilla e a posición no ficheiro NewsDetails.dat no que está o corpo principal da noticia. Se pide escribir un programa que visualice o seguinte menú:

1.- Engadir Noticia ---> Se solicitará titular, entradilla e corpo 2.- Ver Noticias ---> Se visualizarán as noticias e as entradillas, permitindo elixir unha de elas. Cando se elixa, se visualizarán os detalles da mesma 3.- Saír


EXERCICIO
Imos crear unha organización en forma de lista enlazada mediante un ficheiro de acceso directo que nos optimice a inserción e borrado de rexistros, e un índice organizado secuencialmente que nos vai a permitir localizar directamente un rexistro concreto a partir do valor dun campo do rexistro (chave primaria).
A organización do ficheiro de datos será a seguinte:
Posicion Primeiro Rexistro Datos (Long) Posición Último Rexistro Datos (Long) Posición Primeiro Rexistro Borrados (Long) Posición Ultimo Rexistro Borrados (Long) Datos Datos ...
Cada Dato se almacenarán do seguinte xeito
Lonxitude(integer) Posición seguinte (Long) Posición anterior (Long) Rexistro Datos ....
A organización do ficheiro índice será a seguinte:
Dirty (byte) Lonxitude (int) Chave Posicion(Long) Lonxitude (int) Chave ....

Como non coñecemos os datos específicos a gardar e recuperar (dependerá da clase do obxecto que queramos gardar), definimos unha interface coa funcionalidade que necesitamos. Será posible almacenar en ficheiros organizados de este xeito calquera obxecto que implemente a interface Rexistrable:

public interface Rexistrable<K> {
     boolean isDeleted(); // Nos indica si este elemento Rexistrable está marcado como borrado ou non
     K getKey(); // Devolve o valor de chave primaria do Rexistrable
     void readRecord(RandomAccessFile f) throws LinkedFileError;  // Lee na posición actual do RandomAccessFile e garda a información nos atributos do obxecto
     void writeRecord() throws LinkedFileError; // Almacena os valores dos atributos do obxecto na posición actual do RandomAccessFile
}