Связка 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().
Привет!
Сегодняшняя лекция будет разделена на две условные части. Мы повторим кое-что из старых тем, которых уже касались ранее, и рассмотрим некоторые новые фичи :)
Начнем с первого. Повторение это мать учения :)
Ты уже не раз пользовался таким классом как 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.
Главный архитектор программного обеспечения в Tribunal de Justiça da Paraíba
Джесси - инженер-программист, увлеченный поиском решений, которые помогают людям жить лучше. Он работает с технологиями с 2008 год ...
[Читать полную биографию]
Прочитывает 2 байта, что соответствует 1 char-у. Кодировка в JVM по умолчанию стоит - UTF-16.
Если интересно, как работают кодировки в Java для I/O, рекомендую посмотреть эту (большую и подробную) лекцию.
Головач. I/O. Начало. Кодировки.
Для кодировок с фиксированным количеством байтов на символ (например, ASCII или ISO-8859-1), каждый байт представляет собой один символ. Для кодировок с фиксированным количеством байтов подход другой. В UTF-8 первый байт определяет, сколько байтов будет использоваться для символа. Например, если первый байт имеет форму 0xxxxxxx, это один байт; если форма 110xxxxx - это начало многобайтового символа, и так далее. В UTF-16 символы могут занимать 2 или 4 байта. Самые часто используемые символы занимают 2 байта, остальные - 4 байта.
Понял эту фигню через многократное пропечатывание кода замены в задачках с этого уровня. Плюс каменты писал. И по итогу весь процесс уложился в понимании.
Спасибо вам огромное за статью. В обычной лекции про подмену вообще ничего непонятно. напишу туда, чтобы вас сразу читали, а не мучались там по 10 раз напрасно перечитывая
InputStreamReader преобразовал два считанных байта (208, 153) к единому числу 1049
кто-нибудь может подсказать как получить этот результат? у меня какая-то жесть выходит, когда сдвигаю первую часть на 1 байт.
Не претендую на точность, но предполагаю, что разработчики Java просто так решили, что по умолчанию в статической переменной System.out хранится поток вывода в консоль.
помню гдето тут когдато писали что хороший программер это чей код читабелен и понятен. вот нахрена нужно мучить этот System.out если пользоваться им не явно для вывода в консоль а какуюто чушь придумывать чтоб кудато вместо этого записывало?!?!?! если надо чтоб кудато записало то так и накодировать чтобы всем было понятно
Подскажите пожалуйста - если System.in и System.out являются static final, то как тогда работают методы System.setIn() и System.setOut()? Это реализовано с помощью рефлексии?
хм, видимо я очень сильно туплю. Мы считываем символы - и как показано выше это байты (цифры которые мы записываем в int), я не понимаю какой магией, они следующей строчкой оказываются в String уже строкой с буквами, а не цифрами..? надеюсь я понятно написал суть своего непонимания.
Когда мы передаем переменную типа int в конструкцию System.out.println() происходит следующее:
- переменная типа int автоматически упаковывается в обертку типа Integer;
- у полученного объекта вызывается метод .toString() .
Читаю-читаю,
System.in - final переменная класса типа InputStream
и не врубаюсь....
Ну скажите же по-крестьянски, для тупых: при запуске программы java ищет условную точку входа в операционной системе, где происходит чтение с клавиатуры, запоминает её в переменную, которую потом нельзя поменять. Пусть она для джавы станет равна System@35487. Все потоки ввода по умолчанию обращаются к этому адресу.
Захотели подменить - создали новый объект типа InputStream. В процессе создания у него будет совсем другой адрес в памяти, например InputStream@654987)
Запишем наш новый адрес в программу методом System.setIn( InputStream@654987 )
Теперь при создании любых адаптеров для потоков ввода они будут смотреть не в изначальный адрес, а в наш.
То же самое для потоков вывода out, но в обратную сторону.
Т.е. мы для джавы методами System.setIn() и System.setOut() просто подменяем системные указатели ввода/вывода на созданные нами и классы-адаптеры ввода-вывода об этом не догадываются
BufferedReader reader1 = new BufferedReader(new InputStreamReader(System.in));
reader1.close();
BufferedReader reader2 = new BufferedReader(new InputStreamReader(System.in));
reader2.readLine();
когда я запущу этот код, то прога упадёт, выбросив IOException
почему?
да, я закрыл поток, но я же закрывал совершенно другой ридер!
не тот, что я попытался использовать
Если мы выполним этот код и введем в консоли букву «Й», вывод будет таким:
Й
208
153
10
Символы кириллицы занимают в памяти 2 бита,
которые и выводятся на экран (а число 10 — это битовое представление
переноса строки, т.е. нажатия Enter).
Кириллица занимает 2 байта.
10 это представление в десятичной системе счисления двоичного 1010.
Потому что в рамках одного уровня необходимо двигаться не от первого урока к последнему, а наоборот 🙂 Наверное всё таки так необходимо курсом пользоваться 😏
Иногда мне кажется, что развернутая лекция в конце - это для тупых, которые не разобрались самостоятельно с помощью задач и интернетов..🤔
Ps И если уж профессор Ханц Нуделес - это закос под немца, то он должен быть Hans Nudeln. SPRICH DEUTSCH DU.... ну вы поняли.🤣
А в чём преимущество использования такой конструкции
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("Эта строка будет записана в текстовый файл");
перед
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\test.txt");
String string = "Эта строка будет записана в текстовый файл";
byte[] bytes = string.getBytes();
fileOutputStream.write(bytes);
fileOutputStream.close();
while (true) {
int x = System.in.read();
System.out.println(x);
}
В лекции утверждается:
Если мы выполним этот код и введем в консоли букву «Й», вывод будет таким:
Й
208
153
10
Откуда взялось 208 и 153? Это что?
На самом деле:
Как видим, "Й" в байтах - это 201.
Но что за 208 и 153 в лекции?
"Символы кириллицы занимают в памяти 2 бита, которые и выводятся на экран (а число 10 — это битовое представление переноса строки, т.е. нажатия Enter)."
Исправьте 2 бита на 2 байта.
InputStreamReader преобразовал два считанных байта (208, 153) к единому числу 1049.
А можно узнать, как именно реализована эта магия?
Ведь 208 это 1101 0000,
а 153 это 1001 1001
что вместе даст 1101 0000 1001 1001, равное в инте 53401, а не 1049. Как решается на самом деле?
Насколько я понимаю, по умолчанию InputStreamReader использует кодировку UTF-8. Для нее есть свои правила кодирования байтов в символы и наоборот
Вот тут нормально объясняется на примере буквы Ж
http://i.voenmeh.ru/kafi5/Kam.loc/inform/UTF-8.htm
почему при System.in.read() у меня английские буквы считываются по юникоду, а русские нет, не понятно что за номера, а при InputStreamReader.read английские буквы считываются так же по юникоду, и русские то же
для примера взял "П" и "Z"
System.in.read()
InputStreamReader.read()
Потому что System.in - байтовый поток - то-есть считывает по 1 байту.
В кодировке Unicode (2 байта) содержится кодировки ASCII (1 байт).
В кодировке ASCII содержатся все латинские буквы и цифры.
Так что считывая System.in.read() латинские буквы и цифры, ты будешь считывать их корректно , но русские буквы не содержатся в ASCII, поэтому считать их байтовым потоком не выйдет.
В отличие от байтовых потоков, символьные потоки в Java считывают char (2 байта) - Unicode символ, а в юникоде есть, в том числе и русские буквы, поэтому символьные потоки считывают любой символ корректно.
я понял что систем ин он читает хорошо только кодировку ASCKII которая содержится в юникоде
далее InputStream...он читает 2 байта, то есть и саму кодировку юникод (чар)
только не понял одного...
по байтовому потоку буква "П" загланая = 207, а по символьному потоку = 1055, так вот, что это за цифра 207, где ее найти или что она значит...
или
И
char stream: 1048
И
byte stream: 200
Как я понял, в переменную in заносятся данные только после Enter, а только после этого метод read() начинает данные считывать в int x. Так если ввести один символ и нажать Enter то в 1 итерации х == коду символа, во второй коду переноса строки -10, в третьей снова запрос на ввод. если ввести сразу три и более символов то считаются первые 3 байта.
Надеюсь более опытные товарищи лучше объяснят.
Спасибо за пример.
бит это тумблер - либо ноль либо 1. Символы кириллицы ни как не смогут влезть в два бита. Ну максимум 3 символа...))))) блин прикольный вопрос.... не все знают чем бит от байта отличается.
почему при System.in.read() у меня английские буквы считываются по юникоду, а русские нет, не понятно что за номера, а при InputStreamReader.read английские буквы считываются так же по юникоду, и русские то же
для примера взял "П" и "Z"
System.in.read()
InputStreamReader.read()
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
SPRICH DEUTSCH DU....ну вы поняли.🤣