Связка new BufferedReader(new InputStreamReader(System.in)) собирается из трех слоев. System.in дает поток байтов с клавиатуры, InputStreamReader превращает байты в символы, а BufferedReader копит данные в буфере и добавляет метод readLine() для чтения целыми строками. Тот же BufferedReader читает не только клавиатуру, но и файлы, и данные из сети.

Кратко

  • System.in отдает байты: для буквы «Й» метод read() вернет 208 и 153, а не сам символ.
  • InputStreamReader собирает байты в символы: та же «Й» превращается в один код 1049.
  • BufferedReader копит данные в буфере, поэтому обращений к источнику становится в разы меньше.
  • Главный бонус BufferedReader это readLine(): он возвращает целую строку, а не отдельные числа.
  • Тот же BufferedReader читает файл и страницу по URL, меняется только то, что передали в InputStreamReader.
  • System.out это обычная статическая переменная типа PrintStream. Напрямую ее не присвоить, поле помечено final, но заменить объект можно методом System.setOut().
Привет! Сегодняшняя лекция будет разделена на две условные части. Мы повторим кое-что из старых тем, которых уже касались ранее, и рассмотрим некоторые новые фичи :) Подсвеченная клавиатура крупным планом, клавиши QWE и ASD: разбираем чтение данных с клавиатурыНачнем с первого. Повторение это мать учения :) Ты уже не раз пользовался таким классом как BufferedReader. Эту команду, надеюсь, ты еще не успел забыть:

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
Прежде чем читать дальше, попробуй вспомнить за что отвечает каждая составляющая (System.in, InputStreamReader, BufferedReader) и для чего они нужны. Получилось? Если нет, не страшно :) Если к этому моменту ты о чем-то позабыл, перечитай еще раз эту лекцию, посвященную ридерам.

Из чего состоит цепочка чтения с клавиатуры

Вкратце вспомним, что умеет каждый из них.

System.in читает байты

System.in это поток для получения данных с клавиатуры. В принципе, чтобы реализовать логику чтения текста, нам хватило бы и его одного. Но, как ты помнишь, System.in умеет считывать только байты, а не символы:

public class Main {

   public static void main(String[] args) throws IOException {

       while (true) {
           int x = System.in.read();
           System.out.println(x);
       }
   }
}
Если мы выполним этот код и введем в консоли букву «Й», вывод будет таким:

Й
208
153
10
Символы кириллицы в кодировке UTF-8 занимают 2 байта, которые и выводятся на экран (а число 10 это байтовое представление переноса строки, т.е. нажатия Enter).

InputStreamReader превращает байты в символы

Чтение байтов это такое себе удовольствие, поэтому использовать System.in в чистом виде будет неудобно. Для того, чтобы считывать понятные всем кириллические (и не только) буквы, мы используем InputStreamReader в качестве обертки:

public class Main {

   public static void main(String[] args) throws IOException {

       InputStreamReader reader = new InputStreamReader(System.in);
       while (true) {
           int x = reader.read();
           System.out.println(x);
       }
   }
}
Если мы введем в консоль ту же букву «Й», результат в этот раз будет иным:

Й
1049
10
InputStreamReader преобразовал два считанных байта (208, 153) к единому числу 1049. Это и есть считывание по символам. 1049 соответствует букве «Й», в чем можно легко убедиться:

public class Main {

   public static void main(String[] args) throws IOException {

       char x = 1049;
       System.out.println(x);
   }
}
Вывод в консоль:

Й

BufferedReader: буфер и readLine()

Ну а что касается BufferedReader’a (да и вообще BufferedЧегоУгодно), буферизированные классы используются для оптимизации производительности. Обращение к источнику данных (файлу, консоли, ресурсу в Сети) это достаточно дорогая в плане производительности операция. Поэтому чтобы сократить число таких обращений, BufferedReader считывает и накапливает данные в специальном буфере, откуда потом мы можем их получить. В результате количество обращений к источнику данных сокращается в разы или даже десятки раз! Еще одна дополнительная фича BufferedReader’a и его преимущество над обычным InputStreamReader’ом это крайне полезный метод readLine(), который считывает данные целыми строками, а не отдельными числами. Это, конечно, сильно прибавляет удобства при реализации, например, большого текста. Вот как будет выглядеть считывание строки:

public class Main {

   public static void main(String[] args) throws IOException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String s = reader.readLine();
       System.out.println("Пользователь ввел следующий текст:");
       System.out.println(s);
       reader.close();
   }
}

BufferedReader+InputStreamReader работает быстрее, чем просто InputStreamReader
Пользователь ввел следующий текст:
BufferedReader+InputStreamReader работает быстрее, чем просто InputStreamReader

System.in, InputStreamReader и BufferedReader: сводка

Соберем все три слоя цепочки в одну таблицу:
КлассЧто отдает при чтенииЗачем нужен в цепочке
System.inбайты: для буквы «Й» это 208 и 153базовый поток ввода, без оберток читать неудобно
InputStreamReaderсимволы: та же «Й» это один код 1049собирает байты в символы, понимает кириллицу
BufferedReaderсимволы и целые строки через readLine()копит данные в буфере и сокращает число обращений к источнику
Рука набирает текст на клавиатуре за рабочим столом

BufferedReader не только для клавиатуры

Разумеется, BufferedReader это очень гибкий механизм, и позволяет работать не только с клавиатурой.

Чтение данных из сети по URL

Считывать данные можно, например, напрямую из Сети, просто передав ридеру нужный URL:

public class URLReader {
   public static void main(String[] args) throws Exception {

       URL oracle = URI.create("https://www.oracle.com/index.html").toURL();
       BufferedReader in = new BufferedReader(
               new InputStreamReader(oracle.openStream()));

       String inputLine;
       while ((inputLine = in.readLine()) != null)
           System.out.println(inputLine);
       in.close();
   }
}
Обрати внимание, как здесь создается URL. Конструктор new URL("...") помечен устаревшим начиная с Java 20, поэтому объект получают через URI.create(...).toURL().

Чтение данных из файла

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

public class Main {
   public static void main(String[] args) throws Exception {

       FileInputStream fileInputStream = new FileInputStream("testFile.txt");
       BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));

       String str;

       while ((str = reader.readLine()) != null)   {
           System.out.println (str);
       }

       reader.close();
   }
}

Подмена System.out

Теперь давай рассмотрим одну интересную возможность, которую мы ранее не затрагивали. Как ты наверняка помнишь, в классе System есть статические поля System.in и System.out. Эти братья-близнецы являются объектами классов-потоков. System.in это объект абстрактного класса InputStream, а System.out объект класса PrintStream. Сейчас мы поговорим именно о System.out. Если мы зайдем в исходный код класса System, увидим вот что:

public final class System {

……………...

public final static PrintStream out = null;

  …………

}
Итак, System.out это просто обычная статическая переменная класса System. Никакой магии в ней нет :) Переменная out относится к классу PrintStream. Вот интересный вопрос: а почему при выполнении кода System.out.println() вывод производится именно в консоль, а не куда-то еще? И можно ли это как-то изменить? Например, мы хотим считывать данные из консоли, и записывать их в текстовый файл. Можно ли как-то реализовать такую логику, не используя дополнительные классы ридеров и райтеров, а просто пользуясь System.out? Еще как можно :) И хотя переменная System.out обозначена модификатором final, мы все равно можем это сделать! Ледолаз в красной куртке с ледорубами поднимается по ледяной стенеИтак, что нам для этого нужно? Во-первых, нужен новый объект класса PrintStream вместо нынешнего. Текущий объект, установленный в классе System по умолчанию, нам не подходит: он указывает на консоль. Надо создать новый, который будет указывать на текстовый файл в качестве «места назначения» для наших данных. Во-вторых, нужно понять, как присвоить новое значение переменной System.out. Просто так этого не сделать, ведь она помечена final. Начнем с конца. В классе System как раз есть нужный нам метод setOut(). Он принимает на вход объект PrintStream и устанавливает его в качестве точки вывода. Как раз то, что нам нужно! Осталось только создать объект PrintStream. Это сделать тоже несложно:

PrintStream filePrintStream = new PrintStream(new File("C:\\Users\\Username\\Desktop\\test.txt"));
Код целиком будет выглядеть так:

public class SystemRedirectService {

   public static void main(String arr[]) throws FileNotFoundException
   {
       PrintStream filePrintStream = new PrintStream(new File("C:\\Users\\Username\\Desktop\\test.txt"));

       /*Сохраним текущее значение System.out в отдельную переменную, чтобы потом
       можно было переключиться обратно на вывод в консоль*/
       PrintStream console = System.out;

       // Присваиваем System.out новое значение
       System.setOut(filePrintStream);
       System.out.println("Эта строка будет записана в текстовый файл");

       // Возвращаем System.out старое значение
       System.setOut(console);
       System.out.println("А эта строка - в консоль!");
   }
}
В результате первая строка будет записана в текстовый файл, а вторая выведена в консоль :) Ты можешь скопировать этот код в свою IDE и запустить. Открыв текстовый файл, ты увидишь, что нужная строка успешно туда записалась :) На этом лекция подходит к концу. Сегодня мы вспомнили, как работать с потоками и ридерами, восстановили в памяти, чем они отличаются друг от друга и узнали о новых возможностях System.out, которой пользовались чуть ли не в каждом уроке :) До встречи на следующих лекциях!

Вопросы и ответы

Зачем оборачивать System.in в InputStreamReader?

System.in отдает отдельные байты: для кириллической буквы это будут два числа. InputStreamReader собирает байты обратно в символы, поэтому та же буква приходит одним кодом, который уже можно превратить в char.

Зачем нужен BufferedReader, если есть InputStreamReader?

Ради скорости и удобства. BufferedReader копит данные в буфере, поэтому обращений к источнику становится в разы меньше. И он добавляет метод readLine(), который возвращает целую строку, а не отдельные числа.

Почему System.in.read() возвращает числа, а не буквы?

Потому что это поток байтов, и read() возвращает значение очередного байта. Чтобы увидеть символ, число нужно привести к типу char или читать через InputStreamReader.

Как прочитать файл построчно через BufferedReader?

Обернуть FileInputStream в InputStreamReader, а его в BufferedReader, и читать в цикле while, пока readLine() не вернет null.

Как перенаправить System.out в файл?

Создать объект PrintStream, указывающий на файл, и передать его в System.setOut(). Чтобы потом вернуть вывод в консоль, прежнее значение System.out стоит заранее сохранить в переменную.

Почему System.out можно заменить, если поле объявлено final?

Присвоить значение напрямую действительно нельзя, компилятор не даст. Но в классе System есть метод setOut(), который подменяет это поле средствами самой виртуальной машины, в обход обычных правил. Источник: Oracle: класс System.

Читайте также