La forma más eficiente de crear InputStream a partir de OutputStream

84

Esta página: http://blog.ostermiller.org/convert-java-outputstream-inputstream describe cómo crear un InputStream desde OutputStream:

new ByteArrayInputStream(out.toByteArray())

Otras alternativas son usar PipedStreams y nuevos hilos, lo cual es engorroso.

No me gusta la idea de copiar muchos megabytes en una nueva matriz de bytes de memoria. ¿Existe una biblioteca que haga esto de manera más eficiente?

EDITAR:

Siguiendo el consejo de Laurence Gonsalves, probé PipedStreams y resultó que no son tan difíciles de manejar. Aquí está el código de muestra en 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
fuente

Respuestas:

72

Si no desea copiar todos los datos en un búfer en la memoria de una vez, entonces tendrá que tener su código que usa OutputStream (el productor) y el código que usa InputStream (el consumidor ) se alternan en el mismo hilo o funcionan simultáneamente en dos hilos separados. Hacer que operen en el mismo hilo es probablemente mucho más complicado que usar dos hilos separados, es mucho más propenso a errores (deberá asegurarse de que el consumidor nunca bloquee la espera de entrada, o se bloqueará efectivamente) y necesitaría tener al productor y al consumidor funcionando en el mismo bucle, lo que parece estar demasiado unido.

Entonces usa un segundo hilo. Realmente no es tan complicado. La página a la que vinculó tenía un ejemplo perfecto:

  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
fuente
Creo que también necesitas crear un nuevo PipedInputStream para cada hilo de consumidor. Si lee de Pipe desde otro hilo, le dará un error.
Denis Tulskiy
@Lawrence: No entiendo su razón de ser para usar 2 hilos ... A MENOS QUE sea un requisito que todos los caracteres leídos del InputStream se escriban en el OutputStream de manera oportuna.
Stephen C
8
Stephen: no puedes leer algo hasta que esté escrito. Entonces, con solo un hilo, debe escribir todo primero (creando una gran matriz en la memoria que Vagif quería evitar) o debe alternarlos teniendo mucho cuidado de que el lector nunca bloquee la espera de entrada (porque si lo hace , el escritor nunca podrá ejecutar tampoco).
Laurence Gonsalves
1
¿Es esta sugerencia segura de usar en un entorno JEE donde el contenedor probablemente esté ejecutando muchos de sus propios subprocesos?
Toskan
2
@Toskan si new Threadno es apropiado en su contenedor por cualquier motivo, entonces vea si hay un grupo de subprocesos que pueda usar.
Laurence Gonsalves
14

Hay otra biblioteca de código abierto llamada EasyStream que se ocupa de las tuberías y los hilos de forma transparente. Eso no es complicado si todo va bien. Los problemas surgen cuando (mirando el ejemplo de Laurence Gonsalves)

class1.putDataOnOutputStream (fuera);

Lanza una excepción. En ese ejemplo, el hilo simplemente se completa y la excepción se pierde, mientras que el exteriorInputStream podría truncarse.

Easystream se ocupa de la propagación de excepciones y otros problemas desagradables que he estado depurando durante aproximadamente un año. (Soy el encargado de la biblioteca: obviamente mi solución es la mejor;)) Aquí hay un ejemplo de cómo usarla:

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)
 }
};

También hay una buena introducción donde se explican todas las otras formas de convertir un OutputStream en un InputStream. Vale la pena echarle un vistazo.

Charla
fuente
1
El tutorial para usar su clase está disponible en code.google.com/p/io-tools/wiki/Tutorial_EasyStream
koppor
9

Una solución simple que evita copiar el búfer es crear un 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);
    }
}

Escriba en el flujo de salida anterior según sea necesario, luego llame toInputStreampara obtener un flujo de entrada sobre el búfer subyacente. Considere el flujo de salida como cerrado después de ese punto.

Eron Wright
fuente
7

Creo que la mejor manera de conectar InputStream a OutputStream es a través de flujos canalizados , disponibles en el paquete java.io, de la siguiente manera:

// 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.*/

En mi opinión, hay dos ventajas principales para este código:

1 - No hay consumo adicional de memoria a excepción del búfer.

2 - No necesita manejar la cola de datos manualmente

Mostafa Abdellateef
fuente
1
Esto sería genial, pero los javadocs dicen que si los lees y escribes en el mismo hilo, podrías llegar a un punto muerto. ¡Ojalá hubieran actualizado esto con NIO!
Nate Glenn
1

Por lo general, trato de evitar la creación de un subproceso separado debido a la mayor posibilidad de interbloqueo, la mayor dificultad para comprender el código y los problemas de lidiar con excepciones.

Aquí está mi solución propuesta: un ProducerInputStream que crea contenido en fragmentos mediante llamadas repetidas a produceChunk ():

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
fuente