Streams e Design Pattern Pipe & Filter

Streams

Diversas operações em Java podem ser realizadas através de mecanismos de fluxos de dados denominados stream. Streams representam fluxos de informação de entrada ou saída. Eles abstraem o envio e recuperação de dados para/de fontes externas (arquivos, dados pela rede etc.)

Os fluxos são representadas genericamente por duas classes abstratas:

  • Reader - stream de entrada
  • Writer - stream de saída

Há uma hierarquia de herdeiros de Writers de acordo com o propósito.

FileWriter

O FileWriter é um fluxo de saída que guarda os dados em um arquivo. O caminho do arquivo é indicado no seu construtor como pode ser visto abaixo.

Como o FileWriter é um fluxo em sua forma básica, ele não dispõe de operações mais alto nível para a gravação de dados. Portanto, caso se desejasse gravar a sequência "Tecodonte", seria necessário se gravar um byte de cada vez. Veja no código a seguir.

Em geral, fluxos precisam ser fechados com a operação close() quando se conclui a operação com eles. Operações com arquivo, por exemplo, podem envolver cash e bloqueio de recursos, que são liberados quando o fluxo é fechado.

Exceções de entrada/saída (IOException) geralmente são associadas a fluxos, pois podem haver erros, por exemplo, na leitura e gravação de dados.


In [2]:
import java.io.FileWriter;
import java.io.IOException;

FileWriter arquivo;

try {
    arquivo = new FileWriter("texto1.txt");

    arquivo.write('T');
    arquivo.write('e');
    arquivo.write('c');
    arquivo.write('o');
    arquivo.write('d');
    arquivo.write('o');
    arquivo.write('n');
    arquivo.write('t');
    arquivo.write('e');

    arquivo.close();

    System.out.println("Gravacao concluida com sucesso!");
} catch (IOException erro) {
    System.out.println("Nao consegui criar o arquivo =(");
    erro.printStackTrace();
}


Gravacao concluida com sucesso!

Resultados

Observe à esquerda que foi criado um arquivo chamado texto1.txt com o conteúdo gravado. Pode ser necessário aguardar um pouco até o Jupyter sincronizar a visualização.

FileReader

Tal como o FileWriter, o FileReader representa um fluxo de entrada de um arquivo em sua forma básica. O código a seguir mostra como o FileReader é usado para a leitura do que foi gravado antes, um byte de cada vez.


In [3]:
import java.io.FileReader;
import java.io.IOException;

try {
    FileReader arquivo = new FileReader("texto1.txt");

    int caractere = arquivo.read();
    while (caractere != -1) {
        System.out.println((char)caractere);
        caractere = arquivo.read();
    }

    arquivo.close();
} catch (IOException erro) {
    System.out.println("Nao consegui criar o arquivo =(");
    erro.printStackTrace();
}


T
e
c
o
d
o
n
t
e

Pipe & Filter

Design pattern usado pelo Java para concatenar fluxos.

Este pattern é bastante popular em sistemas operacionais UNIX-like. Trata-se de processo incremental em que, enquanto um elemento vai gerando um fluxo vai gerando os dados de saída, o elemento seguinte vai consumindo o fluxo sem esperar que a entrada de dados se complete.

Java Stream Pipe & Filter

Em Java os streams podem trabalhar sob a lógica de Pipe & Filter se conectando fluxos. Veja no slide a seguir uma ilustração de como o FileWriter pode se conectar ao PrintWriter para permitir operações de nível mais alto. O PrintWriter oferece operações de mais alto nível, como o println(). Essa operação recebe uma String e a decompõe em bytes que são entregues ao fluxo seguinte.

Como o PrintWriter não tem a funcionalidade de gravar em arquivos, os fluxos são conectados para trabalhar em colaboração:

No código a seguir é apresentada a sequência para se conectar fluxos. No slide a seguir é comentada a sequência de passos que estão no código:

Note que o resultado final é o mesmo da sequência anterior (o println acrescenta mais um enter no final do fluxo).


In [1]:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

FileWriter arquivo;
PrintWriter formatado;

try {
    arquivo = new FileWriter("texto2.txt");

    formatado = new PrintWriter(arquivo);

    formatado.println("Tecodonte");

    formatado.close();

    System.out.println("Gravacao realizada com sucesso!");
} catch (IOException erro) {
    System.out.println("Nao consegui criar o arquivo =(");
}


Gravacao realizada com sucesso!

Pipe & Filter com Reader

Da mesma forma que na gravação, é possível se realizar Pipe & Filter com o Reader. No exemplo a seguir, é usado o BufferedReader conectado ao FileReader. Nesse caso, o BufferedReader tem um papel equivalente ao PrintWriter, oferecendo operações de alto nível de leitura como o readLine que lê uma linha (String) completa.


In [2]:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

try {
    FileReader arquivo = new FileReader("texto2.txt");
    BufferedReader formatado = new BufferedReader(arquivo);

    String linha = formatado.readLine();
    while (linha != null)
    {
        System.out.println(linha);
        linha = formatado.readLine();
    }

    arquivo.close();
} catch (IOException erro) {
    erro.printStackTrace();
}


Tecodonte

Continuando a Gravação

Da forma que foi mostrado até agora, cada vez que você cria um arquivo ele reinicializa seu conteúdo, ou seja, se havia algum conteúdo anteriormente no arquivo, ele é apagado.

É possível sinalizar ao Java que você deseja gravar novos dados no final do arquivo; isso é feito acrescentando-se um parâmero true na criação do FileWriter:


In [2]:
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

FileWriter arquivo;
PrintWriter formatado;

try {
   // o segundo parametro indica se fara append ou nao
    arquivo = new FileWriter("texto2.txt", true);

    formatado = new PrintWriter(arquivo);

    formatado.println("amigo do Horacio");

    formatado.close();

    System.out.println("Gravacao realizada com sucesso!");
} catch (IOException erro) {
    System.out.println("Nao consegui criar o arquivo =(");
}


Gravacao realizada com sucesso!

Exercício

Retomando o exemplo do Empréstimo codificado a seguir (classe geral Emprestimo que abstrai duas subclasses: EmprestimoSimples e EmprestimoComposto), escreva duas rotinas:

  • serialização - grava o estado de empréstimos (simples ou composto) em arquivos texto;
  • deserialização - lê o estado de um empréstimo (simples ou composto) de um arquivo texto e reconstrói o objeto.

Nesse exercício, não pode ser usado o recurso nativo de serialização do Java. O método de serialização e deserialização deve ser implementado por você.


In [2]:
import java.lang.Math;

public abstract class Emprestimo {
    protected float s;
    protected int   n;
    protected float j;
    protected int   corrente;
    protected float p,
                    proxima;

    public Emprestimo(float s, int n, float j) {
        this.s = s;
        this.n = n;
        this.j = j;
        corrente = 1;
        this.p = -1;  // antes da primeira parcela
        this.proxima = s;
    }

    float getS() {
        return s;
    }

    int getN() {
        return n;
    }
    
    float getJ() {
        return j;
    }

    public float parcela() {
        return p;
    }
    
    public abstract float proximaParcela();
    
    public abstract float parcela(int numero);
}

class EmprestimoSimples extends Emprestimo {
    public EmprestimoSimples(float s, int n, float j) {
        super(s, n, j);
    }

    public float proximaParcela() {
        if (corrente <= n)
            p = s + ((corrente-1) * s * (j/100));
        else
            p = 0;
        corrente++;
        return p;
    }
    
    public float parcela(int numero) {
        float resultado = 0;
        if (numero <= n)
            resultado = s + ((numero-1) * s * (j/100));
        return resultado;
    }
}


class EmprestimoComposto extends Emprestimo {
    public EmprestimoComposto(float s, int n, float j) {
        super(s, n, j);
    }

    public float proximaParcela() {
        p = proxima;
        corrente++;
        if (corrente <= n)
            proxima += (proxima * (j/100));
        else
            proxima = 0;
        return p;
    }
    
    public float parcela(int numero) {
        float resultado = 0;
        if (numero <= n)
            resultado = s * (float)Math.pow(1 + j/100, numero-1);
        return resultado;
    }
}

// codigo principal

Emprestimo emprestimo1 = new EmprestimoSimples(500, 7, 2);
Emprestimo emprestimo2 = new EmprestimoComposto(500, 7, 2);

int i = 1;
emprestimo1.proximaParcela();
emprestimo2.proximaParcela();
while (emprestimo1.parcela() > 0 || emprestimo2.parcela() > 0) {
    if (emprestimo1.parcela() > 0) {
        System.out.println("Emprestimo 1: parcela " + i + " eh " + emprestimo1.parcela());
        System.out.println("              parcela " + i + " eh " + emprestimo1.parcela(i));
    }
    if (emprestimo2.parcela() > 0) {
        System.out.println("Emprestimo 2: parcela " + i + " eh " + emprestimo2.parcela());
        System.out.println("              parcela " + i + " eh " + emprestimo2.parcela(i));
    }
    emprestimo1.proximaParcela();
    emprestimo2.proximaParcela();
    i++;
}


Emprestimo 1: parcela 1 eh 500.0
              parcela 1 eh 500.0
Emprestimo 2: parcela 1 eh 500.0
              parcela 1 eh 500.0
Emprestimo 1: parcela 2 eh 510.0
              parcela 2 eh 510.0
Emprestimo 2: parcela 2 eh 510.0
              parcela 2 eh 510.0
Emprestimo 1: parcela 3 eh 520.0
              parcela 3 eh 520.0
Emprestimo 2: parcela 3 eh 520.2
              parcela 3 eh 520.19995
Emprestimo 1: parcela 4 eh 530.0
              parcela 4 eh 530.0
Emprestimo 2: parcela 4 eh 530.604
              parcela 4 eh 530.60394
Emprestimo 1: parcela 5 eh 540.0
              parcela 5 eh 540.0
Emprestimo 2: parcela 5 eh 541.21606
              parcela 5 eh 541.216
Emprestimo 1: parcela 6 eh 550.0
              parcela 6 eh 550.0
Emprestimo 2: parcela 6 eh 552.0404
              parcela 6 eh 552.04034
Emprestimo 1: parcela 7 eh 560.0
              parcela 7 eh 560.0
Emprestimo 2: parcela 7 eh 563.08124
              parcela 7 eh 563.0811

In [ ]:

Resoluções

Para testar a sua implementação, replique também o exemplo acima com EmprestimoSimples e EmprestimoComposto a seguir no seu código de tal forma que na primeira parte são criados os objetos e serializados, na segunda parte eles são lidos, deserializados e as parcelas dos empréstimos são impressas na tela.

Resolução Parte 1 - Instanciação e serialização dos empréstimos em um arquivo


In [ ]:

Resolução Parte 2 - Leitura, deserialização e impressão das parcelas no console


In [ ]: