1. Потоки данных
Любая программа редко существует сама по себе. Обычно она как-то взаимодействует с «внешним миром». Это может быть считывание данных с клавиатуры, отправка сообщений, загрузка страниц из интернета или, наоборот, загрузка файлов на удалённый сервер.
Все эти вещи мы можем назвать одним словом — процесс обмена данными между программой и внешним миром. Хотя это уже не одно слово.
Сам процесс обмена данными можно разделить на два типа: получение данных и отправка данных. Например, вы считываете данные с клавиатуры с помощью объекта Scanner — это получение данных. И выводите данные на экран с помощью команды System.out.println() — это отправка данных.
Для описания процесса обмена данными в программировании используется термин поток. Откуда вообще взялось такое название?
В реальной жизни им может быть поток воды или поток людей (людской поток). В программировании же под потоком подразумевают поток данных.
Потоки — это универсальный инструмент. Они позволяют программе получать данные откуда угодно (входящие потоки) и отправляют данные куда угодно (исходящие потоки). Делятся на два вида:
- Входящий поток (Input): используется для получения данных
- Исходящий поток (Output): используется для отправки данных
Чтобы потоки можно было «потрогать руками», разработчики Java написали два класса: InputStream и OutputStream.
У класса InputStream есть метод read(), который позволяет читать из него данные. А у класса OutputStream есть метод write(), который позволяет записывать в него данные. У них есть и другие методы, но об этом после.
Байтовые потоки
Что же это за данные и в каком виде их можно читать? Другими словами, какие типы данных поддерживаются этими классами?
О, это универсальные классы, и поэтому они поддерживают самый распространённый тип данных — byte. В OutputStream можно записывать байты (и массивы байт), а из объекта InputStream можно читать байты (или массивы байт). Все — никакие другие типы данных они не поддерживают.
Поэтому такие потоки еще называют байтовыми потоками.
Особенность потоков в том, что данные из них можно читать (писать) только последовательно. Вы не можете прочитать данные из середины потока, не прочитав все данные перед ними.
Именно так работает чтение с клавиатуры через класс Scanner: вы читаете данные с клавиатуры последовательно: строка за строкой. Прочитали строку, прочитали следующую строку, прочитали следующую строку и т.д. Поэтому метод чтения строки и называется nextLine() (дословно — «следующая строка»).
Запись данных в поток OutputStream тоже происходит последовательно. Хороший пример — вывод на экран. Вы выводите строку, за ней еще одну и еще одну. Это последовательный вывод. Вы не можете вывести 1-ю строку, затем 10-ю, а затем вторую. Все данные записываются в поток вывода только последовательно.
Символьные потоки
Недавно вы изучали, что строки — второй по популярности тип данных, и это действительно так. Очень много информации передается в виде символов и целых строк. Компьютер отлично бы передавал все в виде байт, но люди не настолько идеальны.
Java-программисты учли этот факт и написали еще два класса: Reader и Writer. Класс Reader — это аналог класса InputStream, только его метод read() читает не байты, а символы — char. Класс Writer соответствует классу OutputStream, и так же, как и класс Reader, работает с символами (char), а не байтами.
Если сравнить эти четыре класса, мы получим такую картину:
| Байты (byte) | Символы (char) | |
|---|---|---|
| Чтение данных | |
|
| Запись данных | |
|
Практическое применение
Сами классы InputStream, OutputStream, Reader и Writer в явном виде никто не использует: они не присоединены ни к каким внешним объектам, из которых можно читать данные (или в которые можно писать данные). Однако у этих четырех классов много классов-наследников, которые умеют очень многое.
2. Класс InputStream
Класс InputStream интересен тем, что является классом-родителем для сотен классов-наследников. В нем самом нет никаких данных, однако у него есть методы, которые есть у всех его классов-наследников.
Объекты-потоки вообще редко хранят в себе данные. Поток — это инструмент чтения/записи данных, но не хранения. Хотя бывают и исключения.
Методы класса InputStream и всех его классов-наследников:
| Методы | Описание |
|---|---|
|
Читает один байт из потока |
|
Читает массив байт из потока |
|
Читает все байты из потока |
|
Пропускает n байт в потоке (читает и выкидывает) |
|
Проверяет, сколько байт еще осталось в потоке |
|
Закрывает поток |
Вкратце пройдемся по этим методам:
Метод read()
Метод read() читает один байт из потока и возвращает его. Вас может сбить тип результата — int, однако так было сделано, потому что тип int — это стандарт всех целых чисел. Три первые байта типа int будут равны нулю.
Метод read(byte[] buffer)
Это вторая модификация метода read(). Он позволяет считать из InputStream сразу массив байт. Массив для сохранения байт нужно передать в качестве параметра. Метод возвращает число — количество реально прочитанных байт.
Допустим у вас буфер на 10 килобайт, и вы читаете данные из файла с помощью класса FileInputStream. Если файл содержит всего 2 килобайта, все данные будут помещены в массив-буфер, а метод вернет число 2048 (2 килобайта).
Метод readAllBytes()
Очень хороший метод. Просто считывает все данные из InputStream, пока они не закончатся, и возвращает их в виде единого массива байт. Очень удобен для чтения небольших файлов. Большие файлы могут физически не поместиться в память, и метод кинет исключение.
Метод skip(long n)
Этот метод позволяет пропустить n первых байт из объекта InputStream. Поскольку данные читаются строго последовательно, этот метод просто вычитывает n первых байт из потока и выбрасывает их.
Возвращает число байт, которые были реально пропущены (если поток закончился раньше, чем прокрутили n байт).
Метод int available()
Метод возвращает количество байт, которое еще осталось в потоке
Метод void close()
Метод close() закрывает поток данных и освобождает связанные с ним внешние ресурсы. После закрытия потока данные из него читать больше нельзя.
Давайте напишем пример программы, которая копирует очень большой файл. Его нельзя весь считать в память с помощью метода readAllBytes(). Пример:
| Код | Примечание |
|---|---|
|
InputStream для чтения из файлаOutputStream для записи в файлБуфер, в который будем считывать данные Пока данные есть в потоке Считываем данные в буфер Записываем данные из буфера во второй поток |
В этом примере мы использовали два класса: FileInputStream — наследник InputStream для чтения данных из файла, и класс FileOutputStream — наследник OutputStream для записи данных в файл. О втором классе расскажем немного позднее.
Еще один интересный момент — это переменная real. Когда из файла будет читаться последний блок данных, легко может оказаться, что его длина меньше 64Кб. Поэтому в output нужно тоже записать не весь буфер, а только его часть: первые real байт. Именно это и делается в методе write().
3. Класс Reader
Класс Reader — это полный аналог класса InputStream, с одним только отличием: он работает с символами — char, а не с байтами. Класс Reader, так же, как и класс InputStream самостоятельно нигде не используется: он является классом-родителем для сотен классов-наследников и задает для них всех общие методы.
Методы класса Reader (и всех его классов-наследников):
| Методы | Описание |
|---|---|
|
Читает один char из потока |
|
Читает массив char’ов из потока |
|
Пропускает n char’ов в потоке (читает и выбрасывает) |
|
Проверяет, что в потоке еще что-то осталось |
|
Закрывает поток |
Методы очень похожи на методы класса InputStream, хотя есть и небольшие отличия.
Метод int read()
Это метод читает из потока один char и возвращает его. Тип char расширяется до типа int, но первые два байта результата всегда нули.
Метод int read(char[] buffer)
Это вторая модификация метода read(). Он позволяет считать из Reader сразу массив символов. Массив для символов нужно передать в качестве параметра. Метод возвращает число — количество реально прочитанных символов.
Метод skip(long n)
Этот метод позволяет пропустить n первых символов из объекта Reader. Работает точно так же, как аналогичный метод класса InputStream. Возвращает число символов, которые были реально пропущены.
Метод boolean ready()
Возвращает true, если в потоке есть еще не прочитанные байты.
Метод void close()
Метод close() закрывает поток данных и освобождает связанные с ним внешние ресурсы. После закрытия потока данные из него читать больше нельзя.
Давайте для сравнения напишем программу, которая копирует текстовый файл:
| Код | Примечание |
|---|---|
|
Reader для чтения из файлаWriter для записи в файлБуфер, в который будем считывать данные Пока данные есть в потоке Читаем данные в буфер Записываем данные из буфера во второй поток |
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ