ITI0011RUS:IO
Обратно на страницу предмета.
Взаимодействие программы с внешним миром происходит через т.н. вввод-вывод, а соответствующие операции называются операциями ввода-вывода (Input/Output Operations) или просто IO. Ввод отвечает за поступление в программу исходных данных, а вывод отвечает за то, что делать с результатами работы программы.
Типично по умолчанию вводом для программы в настольном компьютере устройством ввода является клавиатура, а устройством вывода - монитор. В смартфонах, например, устройством ввода и устройством вывода является сенсорный экран (тачпад). Ввод и вывод можно осуществлять не только из устройства, но также из файла - благодаря этому наши программы могут читать и писать файлы, это же позволяет записать результат работы программы в файл, а также вести лог ошибок, куда программа записывает информацию, связанную с нештатной работой программы.
В Java независимо от того откуда осуществляется ввод/вывод (из устройства, или из файла), все данные записываются в т.н. потоки. Эти потоки являются объектами Java, и с потоками мы работаем, когда нам нужно обработать ввод или записать что-то в вывод. У каждого процесса в системе есть 3 связанных с ним т.н. стандартных потока:
- Стандартный поток ввода - STDIN (Standard Input). В Java - объект System.in
- Стандартный поток вывода - STDOUT (Standard Output). В Java - объект System.out
- Стандартный поток ошибок - STDERR (Standard Error). В Java - объект System.err
По умолчанию стандартный поток ввода связан с клавиатурой, стандартный поток вывода и стандартный поток ошибок связан с монитором. Отличие этих "стандартных" потоков от остальных потоков в том, что они открываются операционной системой при старте процесса, и закрываются при завершении процесса.
Программист также может создавать произвольное количество своих потоков (максимальное количество потоков в системе, конечно же, не бесконечно, но оно достаточно большое, чтобы не задумываться об этом). Например, для того, чтобы прочитать файл, программист может создать поток чтения из файла. Поток, который создан не системой, а программистом - находится полностью под ответственностью программиста. После открытия потока, например, задачачей программиста является корректно закрыть поток после окончания работы с ним. Стандартные потоки операционная система открывает и закрывает сама - нам не следует об этом беспокоиться. Нужно только помнить, что не следует открывать стандартные потоки, поскольку эти 3 потока уже открыты системой для каждого процесса.
Работа с потоками в Java
Для работы с потоками в Java предусмотрены т.н. читатели (объекты Reader), и писатели (объекты Writer). Вся работа с потоками в сущности сводится к созданию соответствующих объектов Reader либо Writer и работе с ними.
Существует различные типы потоков для различных типов данных. Потоки грубо говоря делятся на бинарные и символьные. Бинарные потоки воспринимают ввод на уровне байтов и из-за этого эти потоки позволяют нам сделать с ними ограниченное количество действий - например, считать один байт, или считать n байт в массив. Такие потоки предназначены для чтения бинарных данных и последующей машинной обработки - они не предназначены для восприятия человеком. Символьные потоки воспринимают информацию уровнем выше - для них ввод это не просто последовательность байт, а осмысленные символы, которые можно прочитать. Символы также подразделяются на печатные и непечатные. Строки состоят из печатных символов, а символ переноса строки, например, непечатный символ. Символьные потоки могут читать потоки по-символьно (например, InputStreamReader). По-символьная обработка ввода не совсем удобна, поэтому в Java существуют специальные обёртки вокруг символьных потоков (такие как BufferedReader) которые могут рассмотреть в непрерывном потоке символов строки (как последовательность символов, которая оканчивается символами окончания строки), что позволяет нам работать с таким потоком на уровне строк - прочитать строку, либо прочитать все строки из потока.
Чтение пользовательского ввода с клавиатуры
Для чтения пользовательского ввода с клавиатуры используется читатель символьного потока, который называется BufferedReader. Этот читатель, однако, не является самодостаточным. По сути, это обёртка вокруг читателя символьных потоков InputStreamReader, который позволяет нам читать поток только по-символьно.
Создание объекта BufferedReader по причинам, описанным выше, происходит в два этапа - сначала создается читатель символьных потоков InputStreamReader, который передается объекту BufferedReader.
<source lang="java"> import java.io.BufferedReader; import java.io.InputStreamReader;
InputStreamReader is = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(is); </source>
То же самое можно сделать одной строкой: <source lang="java"> import java.io.BufferedReader; import java.io.InputStreamReader;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); </source>
Объект BufferedReader позволяет нам читать символьные данные построчно. Для этого в нашем распоряжении есть метод readLine(). Этот метод может выкинуть исключение ввода-вывода IOException, которое следует корректно обработать, поместив код в блок try ... catch:
<source lang="java"> import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader;
InputStreamReader is = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(is); try {
System.out.print("What is your name? "); String name = br.readLine(); System.out.println("Hello, " + name + "!");
} catch(IOException e) {
System.err.println("IO Operation failed");
} </source>
Преобразование строки в число
BufferedReader позволяет нам читать из потока только строки. Часто в программах необходимо чтобы пользователь ввел число. Это число мы получаем в виде строки, которую необходимо преобразовать к соответствующему числовому типу int, float, либо double. Для преобразования строки в число можно пользоваться двумя методами: valueOf() и parseX(), где Х - тип соотвестующих данных Int, Float, либо Double. Оба метода в качестве параметра принимают строку. Если в сроке описано корректное число, например, "10", то функции преобразуют строку к числовому типу без проблем. Если в строке описано нечто, что не является числом, например, "xyz", то преобразование такой строки к числовому типу невозможно, и функции выкидывают исключение NumberFormatException, которое следует обработать поместив код преобразования строки в число в блок try .. catch:
<source lang="java"> try {
int i; float f; double d;
a = Integer.valueOf("10"); // a = 10 a = Integer.valueOf("hello"); // Invalid format a = Integer.parseInt("10"); // a = 10 a = Integer.parseInt("hello"); // Invalid format
f = Float.valueOf("1.57"); // f = 1.57 f = Float.valueOf("hello"); // Invalid format f = Float.parseFloat("1.57"); // f = 1.57 f = Float.parseFloat("hello"); // Invalid format
d = Double.valueOf("0.75"); // d = 0.75 d = Double.valueOf("hello"); // Invalid format d = Double.parseDouble("0.75"); // d = 0.75 d = Double.parseDouble("hello"); // Invalid format
} catch (NumberFormatException e) {
System.err.println("Invalid format");
} </source>
Объект Scanner
Для чтения пользовательского ввода можно также пользоваться объектом Scanner. Преимущества этого объекта в сравнении с объектом BufferedReader заключается в том, Scanner производит обработку исключений внутри себя, и нам не нужно об этом заботиться. Другое преимущество заключается в том, что объект Scanner также умеет определить тип данных в потоке и предоставить нам эти данные в соответствующем формате. Если на вводе число с плавающей точкой, то мы можем получить эти данные в виде float или double. Если на вводе целое число, мы можем получить его в виде переменной типа int. Помимо прочего, Scanner заключает в себе функции токенайзера (tokenizer). В этом одновременно заключается и его достоинство, и скрытая опасность - если не понимать как работает деление ввода на токены и их обработка, работа со Scanner может превратиться в кошмар, в котором в результате работы вы будете получать совсем не то, что ожидаете получить.
Поэтому давайте и начнем с обсуждения что же такое токен, и как происходит деление на токены. Любой ввод можно условно разделить на части, которые несут какую-то смысловую нагрузку (они и называются токенами) и разделители, которые отделяют эти части друг от друга. Другими словами любой ввод можно редставить в виде последовательности токенов, разделенных разделителями. Если взять как пример обычное предложение, то токенами могут быть слова, а разделителями - пробелы. Если например взять текст, и в качестве разделителя взять символ окончания / переноса строки, то токенами будут целые предложения. Если взять формат даты например 10/11/2012 и в качестве разделителя взять символ /, то числа 10, 11 и 2012 будут являться токенами. Разделение токены полезно при форматированном вводе, например в формате CSV (формат, в котором данные разделены запятыми). Указывая соответствующий разделитель под конкретный тип ввода мы можем получить последовательность токенов для обработки.
Теперь, когда мы разобрались с токенами, давайте поговорим непосредственно об объекте Scanner. Scanner, как и InputStreamReader при создании требует указать ему бинарный поток, с которым ему предстоит работать. Поскольку нашей задачей является чтение ввода с клавиатуры, в качестве такого потока имеет смысл передать стандартный поток ввода - System.in.
<source lang="java"> import java.util.Scanner;
Scanner s = new Scanner(System.in); </source>
В примере выше мы создали переменную s типа Scanner и проинициализировали ее объектом Scanner который обрабатывает стандартный поток ввода System.in.
В самом простейшем случае мы можем использовать Scanner точно так же как мы использовали BufferedReader для чтения строк:
<source lang="java"> import java.util.Scanner;
Scanner s = new Scanner(System.in); while(s.hasNextLine()) {
String line = s.nextLine(); System.out.println("User input: " + line);
} </source>
Если пользователь введет строку "hello world", то на экране увидит
User input: hello world
Мы можем разделить ввод на токены и обрабатывать каждый токен в отдельности.
<source lang="java"> import java.util.Scanner;
Scanner s = new Scanner(System.in); while(s.hasNext()) {
String line = s.next(); System.out.println("User input: " + line);
} </source>
Если пользователь введет строку "hello world", то на экране увидит две строки
User input: hello User input: world
Это произошло потому, что по умолчанию в качестве разделителя Scanner использует символ пробела, поэтому в строке "hello world" присутствуют два токена - "hello" и "world". Поскольку каждый токен обрабатывается в отдельности, на экране мы увидели две строки с соответствующими токенами.
Следующими функциями можно проверять что собирается обработать сканер:
- hasNext() - возвращает true, если на входе есть следующий токен (любого типа).
- hasNextInt() - возвращает true, если в потоке присутствует следующий токен, который может быть преобразован к переменной типа int.
- hasNextBoolean() - возвращает true, если в потоке присутствует следующий токен, который может быть преобразован к переменной типа boolean.
- hasNextFloat() - возвращает true, если в потоке присутствует следующий токен, который может быть преобразован к переменной типа float.
- hasNextDouble() - возвращает true, если в потоке присутствует следующий токен, который может быть преобразован к переменной типа double.
- hasNextLine() - возвращает true, если в потоке присутствуют необработанные участки пользовательского ввода.
- ... и т.д (подобных функций целое множество).
Этими функциями следует пользоваться для того, чтобы проверить, есть ли в потоке что-то что можно обработать.
Обработка токена выполняется аналогичным набором функций:
- next() - возвращает следующий токен в виде переменной типа String.
- nextInt() - возвращает значение в виде переменной типа int.
- nextBoolean() - возвращает значение в виде переменной типа boolean.
- hasNextFloat() - возвращает значение в виде переменной типа float.
- hasNextDouble() - возвращает значение в виде переменной типа double.
- hasNextLine() - возвращает все необработанные токены в виде переменной типа String.
Пример: <source lang="java"> import java.util.Scanner;
Scanner s = new Scanner(System.in);
System.out.println("Enter something"); while(s.hasNext()){ if(s.hasNextInt()) { System.out.println(s.next() + " -- That's an integer!"); } else if(s.hasNextDouble()) { System.out.println(s.next() + " -- That's a double precision floating point value!"); } else if(s.hasNextLine()) { System.out.println(s.next() + " -- Oh, what a dissapointment, it's just a string :("); } } s.close(); </source>
Если пользователь введет строку "a 10 12.5 zz" то в результате на экране увидим следующий вывод:
a -- Oh, what a dissapointment, it's just a string :( 10 -- That's an integer! 12.5 -- That's a double precision floating point value! zz -- Oh, what a dissapointment, it's just a string :(