2014-05-23 7 views
16

Ich möchte eine java.util.streams.Stream um eine InputStream wickeln, um ein Byte oder ein Zeichen zu einem Zeitpunkt zu verarbeiten. Ich habe keinen einfachen Weg gefunden, dies zu tun.Wie kann ich Java 8 Streams mit einem InputStream verwenden?

Betrachten Sie die folgende Übung: Wir möchten zählen, wie oft jeder Buchstabe in einer Textdatei angezeigt wird. Wir können dies in einem Array speichern, so dass tally[0] speichert, wie oft a in der Datei erscheint, tally[1] speichert die Anzahl der Zeit, die b erscheint, und so weiter. Da ich keine Möglichkeit, Streaming die Datei direkt finden konnte, habe ich dies:

int[] tally = new int[26]; 
Stream<String> lines = Files.lines(Path.get(aFile)).map(s -> s.toLowerCase()); 
Consumer<String> charCount = new Consumer<String>() { 
    public void accept(String t) { 
     for(int i=0; i<t.length(); i++) 
     if(Character.isLetter(t.charAt(i)) 
      tall[t.charAt(i) - 'a' ]++; 
    } 
}; 
lines.forEach(charCount); 

Gibt es eine Möglichkeit, dies zu erreichen, ohne die lines Methode? Kann ich jedes Zeichen direkt als Stream oder Stream verarbeiten, anstatt Strings für jede Zeile in der Textdatei zu erstellen.

Kann ich java.io.InputStream in java.util.Stream.stream direkter umwandeln?

+1

Vorsicht! "Character.isLetter" gibt für mehr als nur "a-z", z. 'ä' oder' π'. – Holger

+0

Richtig, ich dachte zuerst Umwandlung in Kleinbuchstaben würde dafür sorgen. Vielleicht möchte ich .isLowerCase? – Thorn

+2

Nein, der Punkt ist, dass Java Unicode verwendet und es gibt weit mehr als 26 Buchstaben. Die Umwandlung in Kleinbuchstaben wird das Richtige für sie tun, z.B. wandeln '' Ä'' in '' ä'' und '' '' in '' π'' um. Wenn Sie jedoch die 26 Werte zwischen "a" und "z" zählen möchten, sollten Sie die Suche nach diesem Bereich filtern (wie in meiner Antwort), anstatt "isLetter" zu verwenden. ''Ä'' und'' π'' * sind * Kleinbuchstaben ... – Holger

Antwort

17

Zuerst müssen Sie Ihre Aufgabe neu definieren. Sie lesen Zeichen, daher möchten Sie nicht eine InputStream sondern eine Reader in eine Stream konvertieren.

Sie können die Zeichensatzkonvertierung, die z. in einem InputStreamReader, mit Stream Operationen, da es n: m Mappings zwischen den byte s der und der resultierenden char s gibt.

Einen Stream aus einem Reader erstellen ist ein bisschen schwierig. Sie werden einen Iterator brauchen eine Methode für das Erhalten eines Elements und eine Endebedingung zu spezifizieren:

PrimitiveIterator.OfInt it=new PrimitiveIterator.OfInt() { 
    int last=-2; 
    public int nextInt() { 
     if(last==-2 && !hasNext()) 
      throw new NoSuchElementException(); 
     try { return last; } finally { last=-2; } 
    } 
    public boolean hasNext() { 
     if(last==-2) 
     try { last=reader.read(); } 
     catch(IOException ex) { throw new UncheckedIOException(ex); } 
     return last>=0; 
    } 
}; 

Sobald Sie den Iterator haben, können Sie einen Stream mit dem Umweg eines spliterator erstellen und die gewünschte Operation auszuführen:

int[] tally = new int[26]; 
StreamSupport.intStream(Spliterators.spliteratorUnknownSize(
    it, Spliterator.ORDERED | Spliterator.IMMUTABLE | Spliterator.NONNULL), false) 
// now you have your stream and you can operate on it: 
    .map(Character::toLowerCase) 
    .filter(c -> c>='a'&&c<='z') 
    .map(c -> c-'a') 
    .forEach(i -> tally[i]++); 

Beachten sie, dass während Iteratoren mehr vertraut sind, die Umsetzung der neuen Spliterator Schnittstelle direkt den Betrieb vereinfacht, da es nicht Zustand zwischen zwei Methoden zu halten erfordert, die in beliebiger Reihenfolge aufgerufen werden könnte. Stattdessen haben wir nur eine tryAdvance Methode, die direkt mit einem read() Anruf zugeordnet werden können:

Spliterator.OfInt sp = new Spliterators.AbstractIntSpliterator(1000L, 
    Spliterator.ORDERED | Spliterator.IMMUTABLE | Spliterator.NONNULL) { 
     public boolean tryAdvance(IntConsumer action) { 
      int ch; 
      try { ch=reader.read(); } 
      catch(IOException ex) { throw new UncheckedIOException(ex); } 
      if(ch<0) return false; 
      action.accept(ch); 
      return true; 
     } 
    }; 
StreamSupport.intStream(sp, false) 
// now you have your stream and you can operate on it: 
… 

Beachten Sie jedoch, dass, wenn Sie Ihre Meinung ändern und sind bereit, Files.lines verwenden Sie ein viel leichteres Leben haben kann :

int[] tally = new int[26]; 
Files.lines(Paths.get(file)) 
    .flatMapToInt(CharSequence::chars) 
    .map(Character::toLowerCase) 
    .filter(c -> c>='a'&&c<='z') 
    .map(c -> c-'a') 
    .forEach(i -> tally[i]++); 
+1

Der letzte Teil Ihrer Antwort ist genau das, was ich gesucht habe.Ich habe nicht gesehen, wie man jeden String in einer Zeile mit Streams iteriert. – Thorn