Maneira mais eficiente de criar InputStream a partir de OutputStream

84

Esta página: http://blog.ostermiller.org/convert-java-outputstream-inputstream descreve como criar um InputStream a partir de OutputStream:

new ByteArrayInputStream(out.toByteArray())

Outras alternativas são usar PipedStreams e novos threads, o que é complicado.

Não gosto da ideia de copiar muitos megabytes para um novo array de bytes de memória. Existe uma biblioteca que faz isso de forma mais eficiente?

EDITAR:

A conselho de Laurence Gonsalves, experimentei o PipedStreams e descobri que não são tão difíceis de lidar. Aqui está o código de amostra em clojure:

(defn #^PipedInputStream create-pdf-stream [pdf-info]
  (let [in-stream (new PipedInputStream)
        out-stream (PipedOutputStream. in-stream)]
    (.start (Thread. #(;Here you write into out-stream)))
    in-stream))
Vagif Verdi
fonte

Respostas:

72

Se você não quiser copiar todos os dados em um buffer na memória de uma só vez, você terá que ter seu código que usa o OutputStream (o produtor) e o código que usa o InputStream (o consumidor ) alterne no mesmo encadeamento ou opere simultaneamente em dois encadeamentos separados. Tê-los operando no mesmo thread é provavelmente muito mais complicado do que usar dois threads separados, é muito mais propenso a erros (você precisará se certificar de que o consumidor nunca bloqueie a espera pela entrada, ou você efetivamente entrará em conflito) e necessitaria tendo o produtor e o consumidor rodando no mesmo loop, que parece estar fortemente acoplado.

Portanto, use um segundo tópico. Realmente não é tão complicado. A página que você vinculou tinha um exemplo perfeito:

  PipedInputStream in = new PipedInputStream();
  PipedOutputStream out = new PipedOutputStream(in);
  new Thread(
    new Runnable(){
      public void run(){
        class1.putDataOnOutputStream(out);
      }
    }
  ).start();
  class2.processDataFromInputStream(in);
Laurence Gonsalves
fonte
Acho que você também precisa criar um novo PipedInputStream para cada thread de consumidor. Se você ler do Pipe de outro tópico, ocorrerá um erro.
Denis Tulskiy
@ Lawrence: Não entendo sua razão para usar 2 threads ... A MENOS que seja um requisito que todos os caracteres lidos do InputStream sejam gravados no OutputStream em tempo hábil.
Stephen C
8
Stephen: você não pode ler algo até que esteja escrito. Assim, com apenas um segmento você precisa escrever tudo primeiro (criando um grande array na memória que Vagif queria evitar) ou você precisa ter eles alternativos, tomando muito cuidado para que o leitor nunca bloqueie esperando pela entrada (porque se ele o fizer , o escritor também nunca conseguirá executar).
Laurence Gonsalves
1
essa sugestão é segura para uso em um ambiente JEE onde o contêiner provavelmente está executando muitos de seus próprios threads?
Toskan
2
@Toskan se new Threadnão for apropriado em seu contêiner por qualquer motivo, então veja se há um pool de threads que você pode usar.
Laurence Gonsalves
14

Existe outra biblioteca Open Source chamada EasyStream que lida com tubos e threads de forma transparente. Isso não é realmente complicado se tudo correr bem. Os problemas surgem quando (olhando para o exemplo de Laurence Gonsalves)

class1.putDataOnOutputStream (out);

Lança uma exceção. Nesse exemplo, o encadeamento simplesmente é concluído e a exceção é perdida, enquanto o externo InputStreampode ser truncado.

Easystream lida com propagação de exceções e outros problemas desagradáveis ​​que venho depurando há cerca de um ano. (Eu sou o mantenedor da biblioteca: obviamente minha solução é a melhor;)) Aqui está um exemplo de como usá-la:

final InputStreamFromOutputStream<String> isos = new InputStreamFromOutputStream<String>(){
 @Override
 public String produce(final OutputStream dataSink) throws Exception {
   /*
    * call your application function who produces the data here
    * WARNING: we're in another thread here, so this method shouldn't 
    * write any class field or make assumptions on the state of the outer class. 
    */
   return produceMydata(dataSink)
 }
};

Há também uma boa introdução onde todas as outras maneiras de converter um OutputStream em InputStream são explicadas. Vale a pena dar uma olhada.

Gab
fonte
1
O tutorial para usar a classe está disponível em code.google.com/p/io-tools/wiki/Tutorial_EasyStream
koppor
9

Uma solução simples que evita copiar o buffer é criar um propósito especial ByteArrayOutputStream:

public class CopyStream extends ByteArrayOutputStream {
    public CopyStream(int size) { super(size); }

    /**
     * Get an input stream based on the contents of this output stream.
     * Do not use the output stream after calling this method.
     * @return an {@link InputStream}
     */
    public InputStream toInputStream() {
        return new ByteArrayInputStream(this.buf, 0, this.count);
    }
}

Grave no fluxo de saída acima conforme necessário e, em seguida, chame toInputStreampara obter um fluxo de entrada no buffer subjacente. Considere o fluxo de saída como fechado após esse ponto.

Eron Wright
fonte
7

Acho que a melhor maneira de conectar InputStream a OutputStream é por meio de streams piped - disponível no pacote java.io, da seguinte maneira:

// 1- Define stream buffer
private static final int PIPE_BUFFER = 2048;

// 2 -Create PipedInputStream with the buffer
public PipedInputStream inPipe = new PipedInputStream(PIPE_BUFFER);

// 3 -Create PipedOutputStream and bound it to the PipedInputStream object
public PipedOutputStream outPipe = new PipedOutputStream(inPipe);

// 4- PipedOutputStream is an OutputStream, So you can write data to it
// in any way suitable to your data. for example:
while (Condition) {
     outPipe.write(mByte);
}

/*Congratulations:D. Step 4 will write data to the PipedOutputStream
which is bound to the PipedInputStream so after filling the buffer
this data is available in the inPipe Object. Start reading it to
clear the buffer to be filled again by the PipedInputStream object.*/

Na minha opinião, existem duas vantagens principais para este código:

1 - Não há consumo adicional de memória, exceto para o buffer.

2 - Você não precisa lidar com o enfileiramento de dados manualmente

Mostafa Abdellateef
fonte
1
Isso seria incrível, mas os javadocs dizem que se você ler e escrever neles no mesmo tópico, poderá ocorrer um impasse. Eu gostaria que eles tivessem atualizado isso com o NIO!
Nate Glenn
1

Geralmente, tento evitar a criação de um thread separado por causa do aumento da chance de conflito, da maior dificuldade de compreensão do código e dos problemas de lidar com exceções.

Esta é a minha solução proposta: um ProducerInputStream que cria conteúdo em partes por chamadas repetidas paraproduzirChunk ():

public abstract class ProducerInputStream extends InputStream {

    private ByteArrayInputStream bin = new ByteArrayInputStream(new byte[0]);
    private ByteArrayOutputStream bout = new ByteArrayOutputStream();

    @Override
    public int read() throws IOException {
        int result = bin.read();
        while ((result == -1) && newChunk()) {
            result = bin.read();
        }
        return result;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int result = bin.read(b, off, len);
        while ((result == -1) && newChunk()) {
            result = bin.read(b, off, len);
        }
        return result;
    }

    private boolean newChunk() {
        bout.reset();
        produceChunk(bout);
        bin = new ByteArrayInputStream(bout.toByteArray());
        return (bout.size() > 0);
    }

    public abstract void produceChunk(OutputStream out);

}
Marca
fonte