Как я могу использовать потоки Java 8 с InputStream?

Я хотел бы обернуть java.util.streams.Stream вокруг InputStream для обработки одного байта или одного символа за раз. Я не нашел простого способа сделать это.

Рассмотрим следующее упражнение. Мы хотим подсчитать, сколько раз каждая буква встречается в текстовом файле. Мы можем сохранить это в массиве, так что tally[0] будет хранить количество раз, когда a появляется в файле, tally[1] хранит количество раз, когда b появляется и так далее. Поскольку я не мог найти способ прямой потоковой передачи файла, я сделал следующее:

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

Есть ли способ сделать это без использования метода lines? Могу ли я просто обрабатывать каждый символ непосредственно как поток или поток вместо создания строк для каждой строки в текстовом файле.

Могу ли я более прямо преобразовать java.io.InputStream в java.util.Stream.stream ?


person Thorn    schedule 23.05.2014    source источник
comment
Остерегаться! Character.isLetter возвращает true больше, чем просто a-z, например. ä или π.   -  person Holger    schedule 23.05.2014
comment
Верно, я думал, что первое преобразование в нижний регистр позаботится об этом. Может быть, я хочу .isLowerCase?   -  person Thorn    schedule 24.05.2014
comment
Нет, дело в том, что в Java используется Unicode и там гораздо больше 26 букв. Преобразование в нижний регистр будет для них правильным, например. преобразовать 'Ä' в 'ä' и 'Π' в 'π'. Но если вы хотите подсчитать только 26 значений между 'a' и z, вам следует отфильтровать проверку для этого диапазона (как я сделал в своем ответе), а не использовать isLetter. 'ä' и 'π' являются строчными буквами…   -  person Holger    schedule 26.05.2014


Ответы (1)


Во-первых, вы должны переопределить свою задачу. Вы читаете символы, поэтому не хотите преобразовывать InputStream, а Reader в Stream.

Вы не можете повторно реализовать преобразование кодировки, которое происходит, например. в InputStreamReader с Stream операциями, так как может быть n: m сопоставлений между bytes InputStream и результирующими chars.

Создать поток из Reader немного сложно. Вам понадобится итератор, чтобы указать метод получения элемента и конечное условие:

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

Когда у вас есть итератор, вы можете создать поток, используя обход разделителя, и выполнить желаемую операцию:

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

Обратите внимание, что хотя итераторы более привычны, реализация нового интерфейса Spliterator напрямую упрощает операцию, поскольку не требует сохранения состояния между двумя методами, которые можно вызывать в произвольном порядке. Вместо этого у нас есть только один метод tryAdvance, который можно напрямую сопоставить с вызовом read():

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:
…

Однако обратите внимание, что если вы передумаете и захотите использовать Files.lines, ваша жизнь может быть намного проще:

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]++);
person Holger    schedule 23.05.2014
comment
Последняя часть вашего ответа - это именно то, что я искал. Я не видел, как перебирать каждую строку в одной строке с помощью потоков. - person Thorn; 24.05.2014