Estrutura de dados - Arquivos
Fonte: Material cedido por Jacques Saúvé e tutorial da Sun (I/O)
• Aprender a acessar depósitos de dados persistentes chamados "arquivos"
• Aprender a manipular arquivos de várias formas, incluindo para a serialização de objetos
• Usaremos um conceito muito genérico para acessar arquivos: o Stream (fluxo)
• Um stream é uma sequência ordenada de dados com uma fonte e um destino
• O que é importante sobre o stream é que o acesso aos dados é sequencial
• Não é como acessar objetos na memória
› Neste caso, acessamos qualquer objeto diretamente na memória usando a referência
• Se houver 1000 pedaços de informação num stream, acessamos cada pedaço, mas seqüencialmente, um de cada vez: "Me dê o próximo... me dê o próximo..."
• A noção de stream é importante, pois permite acessar vários tipos de "depósitos de informação"
›
Arquivos
›
Conexões
de rede
› Dispositivos (teclado, tela, impressora, fita de backup, câmera de vídeo, ...)
• Um stream pode ser caracterizado em várias dimensões
• Direção
›
Streams
de entrada
i) Em Java, possuem a palavra "Input" na classe (ex. InputStream)
ii)O destino da informação é seu programa
iii) Isto é, seu programa lê informação do stream
›
Streams
de saída
i) Em Java, possuem a palavra "Output" na classe (ex. OutputStream)
ii)A fonte da informação é seu programa
iii) Isto é, seu programa grava informação no stream
›
Streams
bidirecionais
i) Em Java, possuem a palavra "RandomAccess" na classe (ex. RandomAccessFile)
ii)Seu programa lê e grava informação do/no stream
• Tipo de informação lida
› Streams de bytes (caracterizados pela palavra Stream)
i) Leitura/gravação de bytes crus (nenhuma palavra adicional)
ii)Leitura/gravação de tipos nativos (palavra Data - DataInputStream)
iii) Leitura/gravação de objetos inteiros (palavra Object - ObjectOutputStream)
› Streams de caracteres Unicode (caracterizados pela palavra Reader ou Writer)
• Fontes e destinos (de onde vem/para onde vai a informação)
›
Arquivo
(palavra File)
i) Para ter persistência de informação depois que seu programa termina
› Array de bytes (palavra ByteArray)
› Array de caracteres (palavra CharArray)
›
String
(palavra String)
›
Outro
processo (palavra Piped)
• Processamento intermediário realizado na informação
›
Bufferização
(palavra Buffered)
i) Na leitura, como o disco é lento, é melhor trazer muita informação de uma vez dentro de um lugar da memória (um array) que chamamos buffer
ii)O acesso é feito diretamente à informação no buffer
iii) Quando o buffer esvazia, é cheio novamente com uma única operação de leitura do disco
iv)
Idem
para a saída
›
Compressão
(palavra ZIP)
i) Para ler/escrever arquivo zip, por exemplo
› Para guardar o número da linha enquanto lê informação (palavra LineNumber)
› Para quebrar a informação em pedaços ou "tokens" (palavra Tokenizer)
• É por causa dessa grande variedade de opções que o sistema de entrada/saída (E/S ou do inglês I/O – input/output) do Java, baseado em streams, é meio chato de usar (mas não complicado)
› São muitas opções e é preciso conhecê-las para tomar boas decisões
› Ainda bem que podemos ter uma visão geral que nos dá um bom embasamento para essas decisões
› Nos deteremos mais aqui à persistência
• Todas as classes usadas para entrada/saída (leitura/escrita) de bytes de 8 bits são descendentes de InputStream e OutputStream
• Vejamos um exemplo a seguir, que lê e escreve bytes de arquivos (FileInputStream e FileOutputStream)
package p2.exemplos; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class CopyBytes { public static void main(String[]
args) throws IOException { FileInputStream
in = null; FileOutputStream
out = null; try { in
= new FileInputStream("proverbiochines.txt"); out
= new FileOutputStream("copiadeproverbiochines.txt"); int c; while ((c = in.read()) != -1) { out.write(c); } }
finally { if (in != null) { in.close(); } if (out != null) { out.close(); } } } } |
• É a stream mais primitiva
› Para ler caracteres, como no caso acima, seria mais indicado usar uma stream de caracteres e não de bytes
• As outras streams são construídas sobre as streams de bytes, por isso é importante conhecê-las
› É como se pegássemos uma stream de bytes e a embrulhássemos (padrão wrapper) com uma capa que a torna uma stream de outro tipo de dado, como, por exemplo, caracteres
› Usar o framework de I/O é basicamente um exercício de embrulhar da forma mais adequada uma stream de bytes
i) Por isso vocês verão a seguir muito aninhamento de news…
• Também muito simples
• Todas as classes usadas para entrada/saída (leitura/escrita) de caracteres são descendentes de Reader e Writer
› Note o padrão de nomes
i) InputStream e OutputStream para bytes e Reader e Writer para caracteres
ii)File para indicar que os dados vêm ou vão para um arquivo
• Vejamos um exemplo a seguir, que lê e escreve caracteres de arquivos (FileReader e FileWriter)
package p2.exemplos; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class
CopyCharacters { public static void main(String[]
args) throws IOException { FileReader
inputStream = null; FileWriter
outputStream = null; int numeroDeCaracteres = 0; try { inputStream
= new FileReader("proverbiochines.txt"); outputStream
= new FileWriter("copiacaracteres.txt"); int c; while ((c = inputStream.read()) != -1) { numeroDeCaracteres++; outputStream.write(c); } }
finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } System.out.println(numeroDeCaracteres + " caracteres copiados para
o arquivo copiacaracteres.txt."); } } } |
• Note que ambas usam uma variável inteira para ler/escrever em arquivos
› A diferença é que no CopyBytes existe um byte nos 8 últimos bits do inteiro e no CopyCharacters existe um caractere nos 16 últimos bits do valor inteiro
• Quando queremos usar um stream, perguntamos primeiro:
› Em que direção
a stream deve funcionar?
› Que tipo de
informação será tratada?
› Qual é a fonte
(na leitura) ou destino (na gravação) da informação?
› Qual
processamento(s) intermediário(s) deve(m) ser feito(s) na informação?
•
|
• A beleza do sistema de E/S
Java (e sua fonte de complexidade) é que podemos montar um stream juntando
vários pedaços (como num jogo de Lego) › Com algumas exceções, podemos tratar as 4 dimensões de forma independente, isto é, tomando decisões individuais para cada dimensão e juntando a solução para cada dimensão para fazer o stream final |
• A montagem do stream é feita juntando objetos de várias classes através de um esquema de "wrapping" (empacotamento)
• Exemplo: Ler caracteres de um arquivo com “bufferização”
›
Direção:
Ler
›
Tipo:
Caracteres (Reader)
›
Fonte:
Arquivo (File)
›
Processamento:
Bufferização (Buffered)
BufferedReader in = new BufferedReader( new FileReader("dados.dat") ); |
•
O
stream que montamos é o seguinte:
Lê |
Grava |
Bytes crus |
Tipos nativos |
Objetos |
Carac- |
Arquivo |
Array de bytes |
Outro processo |
Array de carac- |
String |
Bufferi- |
Compressão |
Número |
Tokeniza |
|
FileOutputStream |
X |
X |
X |
X |
X |
||||||||||
ByteArrayOutputStream |
X |
X |
X |
X |
X |
||||||||||
PipedOutputStream |
X |
X |
X |
X |
X |
||||||||||
BufferedOutputStream |
X |
X |
X |
X |
X |
||||||||||
ObjectOutputStream |
X |
X |
|||||||||||||
DataOutputStream |
X |
X |
|||||||||||||
ZIPOutputStream |
X |
X |
X |
X |
X |
||||||||||
GZIPOutputStream |
X |
X |
X |
X |
X |
||||||||||
FileInputStream |
X |
X |
X |
X |
X |
||||||||||
ByteArrayInputStream |
X |
X |
X |
X |
X |
||||||||||
PipedInputStream |
X |
X |
X |
X |
X |
||||||||||
BufferedInputStream |
X |
X |
X |
X |
X |
||||||||||
ObjectInputStream |
X |
X |
|||||||||||||
DataInputStream |
X |
X |
|||||||||||||
ZIPInputStream |
X |
X |
X |
X |
X |
||||||||||
GZIPInputStream |
X |
X |
X |
X |
X |
||||||||||
RandomAccessFile |
X |
X |
X |
X |
|||||||||||
FileWriter |
X |
X |
X |
||||||||||||
PipedWriter |
X |
X |
X |
||||||||||||
CharArrayWriter |
X |
X |
X |
||||||||||||
StringWriter |
X |
X |
X |
||||||||||||
BufferedWriter |
X |
X |
X |
||||||||||||
PrintWriter |
X |
X |
|||||||||||||
FileReader |
X |
X |
X |
||||||||||||
PipedReader |
X |
X |
X |
||||||||||||
CharArrayReader |
X |
X |
X |
||||||||||||
StringReader |
X |
X |
X |
||||||||||||
BufferedReader |
X |
X |
X |
||||||||||||
LineNumberReader |
X |
X |
X |
||||||||||||
StreamTokenizer |
X |
X |
X |
• Outro exemplo: queremos gravar dados em arquivo zipado
›
Direção:
Escrever (Output)
›
Tipo:
Bytes (Stream)
›
Fonte:
Arquivo (File)
› Processamento: Buferização (Buffered) + Compressão zip (Zip)
BufferedOutputStream out = new BufferedOutputStream(
new
ZipOutputStream( new FileOutputStream("dados.zip") ) ); |
• Mais um exemplo: queremos gravar objetos em um arquivo
ObjectOutputStream out = new ObjectOutputStream(
new
FileOutputStream("agencia.txt"))
); |
• Arquivos são uma abstração que deu certo!
• Arquivos são usados devido à sua persistência
› Isto é, o conteúdo não desaparece quando o programa termina
› Isso não é o caso para objetos manipulados pelo programa
•
Outro lugar para guardar informação persistente
é um Banco
de Dados
› É uma "camada" em cima de arquivos para dar um acesso mais rápido quando há uma quantidade muito grande de informação
› O acesso seqüencial proporcionado pelo arquivo seria muito lento
› O Banco de Dados fornece outros serviços, além de rapidez, tais como:
i) Robustez
ii)Agrupamento de operações (transações)
iii) Controle de concorrência (quando vários usuários acessam os dados ao mesmo tempo)
iv) etc.
• Ao usar "new FileReader(nomeArquivo)", o sistema operacional "abre" o arquivo
› A "abertura" do arquivo tem vários propósitos:
i) Verificar se o arquivo existe
ii)Verificar se você tem permissão de usar o arquivo da maneira que está pedindo (ler ou gravar, por exemplo)
iii) Preparar o sistema operacional para receber pedidos de acesso ao arquivo feitos pelo programa com a maior rapidez possível
(1) Para atender a isso, o sistema operacional guarda informação do arquivo na memória para ter acesso mais rápido
• No final, é necessário avisar o sistema operacional que o arquivo não será mais usado, por enquanto
› Isso é feito com a operação de fechamento de arquivo (close)
i) O sistema só pode abrir um certo número limitado de arquivos, devido a limitações de recursos internos do sistema operacional (veja ulimit no Unix/Linux)
• Arquivos são freqüentemente organizados por registro
› Exemplo: se um arquivo contiver o cadastro de alunos, um registro conteria o cadastro de um único aluno
› O registro consiste de campos (nome, matrícula, data de nascimento, ...)
› O registro pode ser de tamanho fixo (tamanho fixo para cada campo)
› O registro pode ser de tamanho variável (usando um separador especial entre campos)
› Às vezes, o arquivo pode ser organizado de outras formas e não conter apenas uma seqüência de registros do mesmo tipo
i) Exemplos: arquivo HTML, arquivo XML, etc., etc.
› É possível em muitas linguagens realizar acesso direto a registros específicos
java.io class RandomAccessFile
public void seek(long pos) throws IOException Sets
the file-pointer offset, measured from the beginning of this file, at which
the next read or write occurs. The offset may be set beyond the end of the
file. Setting the offset beyond the end of the file does not change the file
length. The file length will change only by writing after the offset has been
set beyond the end of the file. Parameters:
Throws:
|
• O que é “bufferizar”?
› Os exemplos mostrados não têm bufferização
• Por que “bufferizar”?
• Para reduzir custo, Java implementa streams de E/S “bufferizadas”
› Streams de entrada bufferizadas lêem dados de uma área de memória chamada “buffer”;
› A API nativa de Java para leitura em arquivo é chamada apenas quando o buffer está vazio;
› Streams de saída bufferizadas escrevem dados em uma área de memória chamada buffer; as chamadas nativas da API para efetivamente escrever no arquivo ocorrem apenas quando o buffer está cheio.
• Podemos converter streams não “bufferizadas” em streams bufferizadas
› Empacotamento de streams
• Exitem 4 embrulhos para tornar streams não “bufferizadas” em streams “bufferizadas”
› BufferedInputStream
e BufferedOutputStream
criam streams “bufferizadas” de bytes
BufferedReader
e BufferedWriter
criam streams “bufferizadas” de caracteres• Também muito simples
• É muito comum que a leitura/escrita de caracteres seja feita, de fato, considerando uma coleção de caracteres que forma uma linha
› Uma linha é uma string de caracteres com um terminador de linha no fim
›
O finalizador de linha pode ser uma seqüência de
carriage-return/line-feed ("\r\n"
), a single carriage-return ("\r"
), ou
um simples line-feed ("\n"
).
• É o caso em que há processamento; neste caso, bufferização
•
Vamos modificar o exemplo CopyCharacters
para aplicar I/O dirigida a linhas
›
Teremos que usar duas classes que não usamos
ainda: BufferedReader
e PrintWriter
.
package p2.exemplos; import java.io.FileReader; import java.io.FileWriter; import java.io.BufferedReader; import java.io.PrintWriter; import java.io.IOException; public class CopyLines { public static void main(String[]
args) throws IOException { BufferedReader
inputStream = null; PrintWriter
outputStream = null; try { inputStream
= new BufferedReader(new FileReader( "proverbiochines.txt")); outputStream
= new PrintWriter(new FileWriter("linesoutput.txt")); String
line; while ((line = inputStream.readLine()) != null) { outputStream.println(line); } }
finally { if (inputStream != null) { inputStream.close(); } if (outputStream != null) { outputStream.close(); } } } } |
• O readLine retorna uma linha com o finalizador de linha do sistema operacional onde a aplicação está sendo executada, mesmo que seja diferente do finalizador de linha encontrado no arquivo
•
(seção para estudar
em casa!)
• As classes que definem streams em Java possuem vários métodos de acesso
› Já usamos o método read() do BufferedReader, acima
• Resumimos aqui alguns dos métodos mais importantes de algumas classes
› Veja a documentação da API Java para ver a lista completa de métodos
• InputStream
› Todos os InputStreams podem:
i) Ler um byte: read()
ii)Ler vários bytes: read(byte[] b)
iii) Observe que a leitura de um byte retorna int
(1) Motivo: queremos retornar qualquer valor de byte e ainda ter o valor -1 para indicar o fim do stream
› O ObjectInputStream ainda pode:
i) Ler um Objeto qualquer: readObject()
› O DataInputStream ainda pode:
i) Ler dados de tipos básicos: readBoolean(), readDouble(), readInt(), ...
• OutputStream
› Todos os OutputStreams podem:
i) Gravar um byte: write(int b)
ii)Gravar vários bytes: write(byte[] b)
› O ObjectOutputStream ainda pode:
i) Gravar um Objeto qualquer: writeObject(Object obj)
› O DataInputStream ainda pode:
i) Gravar dados de tipos básicos: writeBoolean(), writeDouble(), writeInt(), ...
• FileReader
› Todos os FileReaders podem:
i) Ler um caractere: read()
(1) Retorna -1 no fim de arquivo
› O BufferedReader ainda pode:
i) Ler uma linha inteira: readLine()
› O LineNumberReader ainda pode:
i) Retornar o número da linha: getLineNumber()
• FileWriter
› Todos os FileWriters podem:
i) Gravar um caractere: write(int c)
› O BufferedWriter ainda pode:
i) Gravar um caractere de nova-linha (separador de linhas): newLine()
› O PrintWriter ainda pode:
Gravar dados de tipos básicos de forma formatada: print(int), print(double), ...
Streams de dados
• Apóia entrada/saída de tipos primitivos e strings
›
boolean, char, byte, short, int, long, float, e double
• Na raiz dessas streams estão as interfaces DataInput e DataOutput
• Vejamos a seguir exemplos do uso de DataInputStream e DataOutputStream, que são implementações bastante usadas de DataInput e Dataoutput
package p2.exemplos; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; public class
GravaRegistros { public static void main(String[]
args) { String dataFile = "registros.txt"; escreveDados(dataFile); leDados(dataFile); } private static void
escreveDados(String dataFile) { double[] prices = { 19.99, 9.99, 15.99, 3.99,
4.99 }; int[] units = { 12, 8, 13, 29, 50 }; String[] descs = { "Camiseta Java", "Caneca
Java", "Boneco Java", "Broche Java", "Chaveiro
Java " }; DataOutputStream
out = null; try { out
= new DataOutputStream(new BufferedOutputStream( new FileOutputStream(dataFile))); for (int i = 0; i <
prices.length; i++) { out.writeDouble(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); } }
catch (FileNotFoundException e) { e.printStackTrace(); }
catch (IOException e) { e.printStackTrace(); }
finally { if (out != null) { try { out.close(); }
catch (IOException e) { e.printStackTrace(); } } } } private static void
leDados(String dataFile) { DataInputStream
in = null; double price; int unit; String
desc; double total = 0.0; try { in
= new DataInputStream(new BufferedInputStream( new FileInputStream(dataFile))); try { while (true) { price
= in.readDouble(); unit
= in.readInt(); desc = in.readUTF(); System.out.format("Voce
pediu %d unidades de %s por $%.2f%n", unit, desc,
price); total
+= unit * price; } }
catch (EOFException e) { }
catch (IOException e) { e.printStackTrace(); } }
catch (FileNotFoundException e) { e.printStackTrace(); }
finally { if (in != null) { try { in.close(); }
catch (IOException e) { e.printStackTrace(); } } } System.out.format("Valor total a pagar: $%.2f%n", total); } } |
• Observe que o programador tem que controlar o tipo de dado a ser lido
› Para cada write teve um read associado
• Observe o uso de EOFException para detectar o fim de arquivo
Streams do objetos
• A unidade que circula da stream é “objeto”
• As classes usadas são ObjectInputStream e ObjectOutputStream
› Implementam ObjectInput e ObjectOutput, que por sua vez herdam de DataInput e DataOutput respectivamente
i) Assim, streams de objetos também são capazes de lidar com tipos primitivos e strings
• Programa idêntico ao anterior, mas que lida com quantias monetárias usando objetos da classe BigDecimal
package p2.exemplos; import java.io.*; import java.math.BigDecimal; import java.util.Calendar; public class ObjectStreams
{ static final String dataFile = "objetos.dat"; static final BigDecimal[] prices = { new BigDecimal("19.99"), new BigDecimal("9.99"), new BigDecimal("15.99"), new BigDecimal("3.99"), new BigDecimal("4.99") }; static final
int[] units = { 12, 8, 13, 29, 50 }; static final
String[] descs = { "Camiseta
Java", "Caneca Java", "Boneco
Java", "Broche Java", "Chaveiro
Java" }; public static void main(String[] args) throws IOException, ClassNotFoundException
{ writeObjects(); readObjects(); } private static void
writeObjects() throws IOException, FileNotFoundException
{ ObjectOutputStream
out = null; try { out
= new ObjectOutputStream(new BufferedOutputStream( new FileOutputStream(dataFile))); out.writeObject(Calendar.getInstance()); for (int i = 0; i <
prices.length; i++) { out.writeObject(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); } }
finally { out.close(); } } private static void readObjects()
throws IOException, FileNotFoundException,
ClassNotFoundException { ObjectInputStream
in = null; try { in
= new ObjectInputStream(new BufferedInputStream( new FileInputStream(dataFile))); Calendar
date = null; BigDecimal
price; int unit; String
desc; BigDecimal
total = new
BigDecimal(0); date
= (Calendar) in.readObject(); System.out.format("%tA,
%<tB %<te, %<tY:%n", date); try { while (true) { price
= (BigDecimal) in.readObject(); unit
= in.readInt(); desc
= in.readUTF(); System.out.format("Você pediu %d unidades de %s por
$%.2f%n", unit, desc,
price); total
= total.add(price.multiply(new
BigDecimal(unit))); } } catch (EOFException e) { } System.out.format("Valor
TOTAL da compra: $%.2f%n", total); } finally { in.close(); } } } |
• Se um objeto tiver atributos que são referências a outros objetos, então uma escrita de um objeto vai resultar, de fato, na escrita de vários objetos
› E se dois objetos referenciarem o mesmo objeto?
Serialização
• Anda lado a lado com streams de objetos que empacotam streams de dados, que empacotam streams de bytes!
• A serialização significa gravar, seqüencialmente e byte-por-byte, os atributos de um objeto
• Qual é o tempo máximo de vida de um objeto?
› Que tal ter objetos que ficam em um estado dormente, mas com todo o seu estado mantido, mesmo depois que o programa termina?
› Na próxima vez que o programa for executado, o objeto terá a mesma informação/estado que ele tinha quando o programa foi executado pela última vez
› Surgiu para permitir: RMI (Remote Method Invovation) e JavaBeans
• Como tornar um objeto persistente?
› Implementando a interface Serializable
i) É uma interface de rotulação (“tagging”) e não contém métodos a serem implementados
› O objeto pode ser transformado em uma seqüência de bytes (que será armazenado em um arquivo) e que pode depois ter seu estado restaurado
› O mecanismo de serialização está preparado para lidar com diferenças de representação entre diferentes sistemas
• É preciso “serializar” e “desserializar” os objetos explicitamente gravando-os em arquivo como vimos no exemplo anterior
• Para serializar criamos um ObjectOutputStream que empacota um OutputStream e chamamos o método writeObject()
• Para desserializar fazemos o processo inverso: criamos um objeto ObjectInputStream que empacota um OutputStream e usamos readObject()
› Deve haver o cuidado ao fazer o downcast…
• Vejamos um exemplo
package p2.exemplos; import java.io.*; import java.math.BigDecimal; public class Serializavel implements Serializable { private static final long serialVersionUID = 1L; private Serializavel instancia; private String string; private BigDecimal bigDecimal; public Serializavel(int numInstances) { bigDecimal = new BigDecimal(numInstances); string = "serializavel
numero " + numInstances; if (--numInstances > 0) instancia = new Serializavel(numInstances); else instancia = null; } public Serializavel getOtherInstance() { return instancia; } public Serializavel getInstance() { return this; } public String getString() { return string; } public BigDecimal getBigDecimal() { return bigDecimal; } @Override public String toString() { if (instancia != null) return string + "; " + bigDecimal.toPlainString()
+ "; " +
instancia; return string + "; " + bigDecimal.toPlainString()
+ ";"; } public static void main(String[]
args) throws
FileNotFoundException, IOException { int profundidade = 6; Serializavel serializavel = new Serializavel(profundidade); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream( "serializavel.ser")); out.writeObject(serializavel); out.close(); } } |
package p2.exemplos; import java.io.*; public class
LeSerializavel { public static void main(String[] args) throws FileNotFoundException, IOException,
ClassNotFoundException { ObjectInputStream
in = new ObjectInputStream(new FileInputStream("serializavel.ser")); Serializavel serializavel = (Serializavel)
in.readObject(); System.out.println(serializavel); in.close(); } } |
Outras questões mais avançadas
• Controle de versões usando serialVersionUID
• Atributos transientes
› Palavra chave transient
• A interface Externalizable que herda de Serializable
› writeExternal()
› readExternal()
Detalhes sobre serialização
• http://java.sun.com/javase/6/docs/platform/serialization/spec/version.html
• http://blog.caelum.com.br/2008/04/01/entendendo-o-serialversionuid/