1. Класс InputStreamReader

Еще одной интересной особенностью потоков является возможность объединять несколько потоков в цепочки. Поток может читать данные не только из источника данных, который их хранит, но из другого потока.

Это очень мощный механизм в Java, который позволил конструировать сложные сценарии чтения данных, соединяя одни потоки с другими. Выглядит такая схема примерно так:

Класс InputStreamReader

Когда программа читает данные из потока данных, он в свою очередь читает их из своего источника данных: другого потока данных или из файла, например.

При этом каждый поток данных не просто читает и отдает данные, а еще может преобразовывать их или выполнять над ними различные операции. Хорошим примером такого «промежуточного потока» данных является класс InputStreamReader.

Мы уже знаем класс, который называется FileReader, и это Reader, который читает данные из файла. А откуда читает данные класс InputStreamReader? Правильно: из потока InputStream.

Чтобы создать объект типа InputStreamReader, в него нужно передать объект типа InputStream, или его класса наследника. Пример:

String src = "c:\\projects\\log.txt";
FileInputStream input = new FileInputStream(src);
InputStreamReader reader = new InputStreamReader(input);

У класса InputStreamReader есть все методы, которые есть у класса Reader, причем они работают точно так же.

Основное отличие классов InputStreamReader и, например, FileReader в том, откуда они читают данные. FileReader читает данные из файла (поэтому он и называется FileReader), а InputStreamReader читает данные из потока InputStream.

Когда вы читаете символ из объекта FileReader с помощью метода read(), он в свою очередь читает из файла на диске два байта и возвращает их вам как char.

Когда вы читаете символ из объекта InputStreamReader с помощью метода read(), он в свою очередь читает два байта из переданного в него объекта FileInputStream, который в свою очередь читает данные из файла. Получается такая цепочка вызовов методов read().


2. Класс BufferedReader

Есть еще один интересный класс, который вы, скорее всего, будете часто использовать — BufferedReader. Это тоже «промежуточный поток», который читает данные из другого потока.

Класс BufferedReader, как видно из его названия, является классом-наследником от Reader и позволяет читать символы. Однако, что самое интересное, в качестве источника данных в него тоже нужно передать поток, из которого можно читать символы – поток-наследник от класса Reader.

В чем же смысл? В отличие от InputStreamReader’а, класс BufferedReader не преобразовывает байты в символы: он вообще ничего не преобразовывает. Вместо этого он буферизует данные.

Класс BufferedReader

Когда программа читает из объекта BufferedReader один символ, он читает из своего потока-источника сразу большой массив символов. И сохраняет их у себя внутри.

При чтении следующего символа из объекта BufferedReader, он просто возьмет очередной символ из своего внутреннего массива-буфера и отдаст его, не обращаясь при этом к потоку-источнику данных. И только когда все символы в буфере закончатся, он снова прочитает большой массив символов.

Еще у класса BufferedReader есть очень полезный метод — String readLine(), который позволяет читать данные из потока-источника сразу строками. С помощью этого метода можно, например, прочитать какой-то файл и вывести его содержимое на экран построчно. Пример:

Мы специально записали код в компактном формате, чтобы вы увидели, как это может быть удобно. Можно записать данный код и немного более детально.

String src = "c:\\projects\\log.txt";

try(FileReader in = new FileReader(src);
BufferedReader reader = new BufferedReader(in))
{
   while (reader.ready())
   {
      String line = reader.readLine();
      System.out.println(line);
   }
}


Создаем объект FileReader, источник данных — файл.
Создаем объект BufferedReader, источник данных — объект FileReader;
пока в reader еще есть данные

Прочитать одну линию
Вывести линию на экран
Важный момент:

Если вы соединили несколько потоков в цепочку, метод close() достаточно вызвать только у одного из них: он вызовет его у своего источника данных и т.д., пока не дойдут до финального потока с данными.



3. Чтение с консоли

И еще один интересный факт: класс Scanner — ничто иное как входящий промежуточный поток данных, который читает их из потока System.in — тоже потока данных.

Вот два способа чтения строки с консоли:

Класс Scanner Классы BufferedReader и InputStreamReader
InputStream stream = System.in;
Scanner console = new Scanner(stream);
String line = console.nextLine();
InputStream stream = System.in;
InputStreamReader reader = new InputStreamReader(stream);
BufferedReader buff = new BufferedReader(reader);
String line = buff.readLine();

Наш знаменитый System.in — это ничто иное как статическая переменная in класса System. Тип ее — InputStream, а имя — in.

Так что практически с самого начала изучения Java на JavaRush вы работаете с потоками данных и строите из них цепочки. Только теперь вы будете это делать более осознанно.