Najbardziej efektywny sposób tworzenia InputStream z OutputStream

84

Ta strona: http://blog.ostermiller.org/convert-java-outputstream-inputstream zawiera opis sposobu tworzenia InputStream z OutputStream:

new ByteArrayInputStream(out.toByteArray())

Inną alternatywą jest użycie PipedStreams i nowych wątków, co jest uciążliwe.

Nie podoba mi się pomysł kopiowania wielu megabajtów do nowej tablicy bajtów pamięci. Czy istnieje biblioteka, która robi to wydajniej?

EDYTOWAĆ:

Za radą Laurence'a Gonsalvesa wypróbowałem PipedStreams i okazało się, że nie są one takie trudne. Oto przykładowy kod w 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
źródło

Odpowiedzi:

72

Jeśli nie chcesz kopiować wszystkich danych do bufora w pamięci naraz, będziesz musiał mieć swój kod, który używa OutputStream (producenta) i kodu używającego InputStream (konsument ) naprzemiennie w tym samym wątku lub działają jednocześnie w dwóch oddzielnych wątkach. Posiadanie ich działających w tym samym wątku jest prawdopodobnie znacznie bardziej skomplikowane niż użycie dwóch oddzielnych wątków, jest znacznie bardziej podatne na błędy (musisz się upewnić, że konsument nigdy nie blokuje czekania na dane wejściowe lub skutecznie zablokujesz się) i wymagałoby posiadanie producenta i konsumenta działających w tej samej pętli, co wydaje się zbyt ściśle powiązane.

Więc użyj drugiego wątku. To naprawdę nie jest takie skomplikowane. Strona, do której utworzyłeś link, miała doskonały przykład:

  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
źródło
Myślę, że musisz również utworzyć nowy PipedInputStream dla każdego wątku konsumenckiego. Jeśli czytasz z Pipe z innego wątku, da ci to błąd.
Denis Tulskiy
@Lawrence: Nie rozumiem twojego uzasadnienia dla używania 2 wątków ... chyba że jest to wymaganie, aby wszystkie znaki odczytywane z InputStream były zapisywane do OutputStream w odpowiednim czasie.
Stephen C
8
Stephen: nie możesz czegoś przeczytać, dopóki nie zostanie napisane. Tak więc, mając tylko jeden wątek, musisz albo najpierw napisać wszystko (tworząc dużą tablicę w pamięci, której Vagif chciał uniknąć), albo musisz mieć je naprzemiennie, bardzo uważając, aby czytnik nigdy nie blokował czekania na wejście (ponieważ jeśli tak , autor też nigdy nie będzie mógł wykonać).
Laurence Gonsalves
1
Czy ta sugestia jest bezpieczna w użyciu w środowisku JEE, w którym kontener prawdopodobnie obsługuje wiele własnych wątków?
Toskan
2
@Toskan, jeśli new Threadz jakiegoś powodu nie jest odpowiedni w Twoim kontenerze, sprawdź, czy istnieje pula wątków, której możesz użyć.
Laurence Gonsalves
14

Istnieje inna biblioteka Open Source o nazwie EasyStream, która obsługuje rury i wątki w przejrzysty sposób. To nie jest naprawdę skomplikowane, jeśli wszystko pójdzie dobrze. Problemy pojawiają się, gdy (patrząc na przykład Laurence Gonsalves)

class1.putDataOnOutputStream (out);

Zgłasza wyjątek. W tym przykładzie wątek po prostu się kończy, a wyjątek zostaje utracony, podczas gdy zewnętrzna część InputStreammoże zostać obcięta.

Easystream radzi sobie z propagacją wyjątków i innymi nieprzyjemnymi problemami, które debuguję od około roku. (Jestem opiekunem biblioteki: oczywiście moje rozwiązanie jest najlepsze;)) Oto przykład, jak z niego korzystać:

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

Jest też fajne wprowadzenie, w którym wyjaśniono wszystkie inne sposoby konwersji OutputStream na InputStream. Warto rzucić okiem.

Gadanina
źródło
1
Samouczek dotyczący korzystania z ich klasy jest dostępny pod adresem code.google.com/p/io-tools/wiki/Tutorial_EasyStream
koppor
9

Prostym rozwiązaniem, które pozwala uniknąć kopiowania bufora, jest utworzenie specjalnego przeznaczenia 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);
    }
}

W razie potrzeby napisz do powyższego strumienia wyjściowego, a następnie wywołaj, toInputStreamaby uzyskać strumień wejściowy z bazowego buforu. Potraktuj strumień wyjściowy jako zamknięty po tym punkcie.

Eron Wright
źródło
7

Myślę, że najlepszym sposobem na połączenie InputStream z OutputStream są strumienie potokowe - dostępne w pakiecie java.io, w następujący sposób:

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

Moim zdaniem są dwie główne zalety tego kodu:

1 - Nie ma dodatkowego zużycia pamięci poza buforem.

2 - Nie musisz ręcznie obsługiwać kolejkowania danych

Mostafa Abdellateef
źródło
1
Byłoby to niesamowite, ale javadoc mówi, że jeśli czytasz i piszesz do nich w tym samym wątku, możesz wpaść w zakleszczenie. Szkoda, że ​​nie zaktualizowali tego za pomocą NIO!
Nate Glenn
1

Zwykle staram się unikać tworzenia oddzielnego wątku ze względu na zwiększoną szansę na zakleszczenie, zwiększoną trudność w zrozumieniu kodu i problemy z obsługą wyjątków.

Oto moje proponowane rozwiązanie: ProducerInputStream, który tworzy zawartość w kawałkach przez powtarzające się wywołania funkcji bringChunk ():

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

}
znak
źródło