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 для записи в файлБуфер, в который будем считывать данные Пока данные есть в потоке Читаем данные в буфер Записываем данные из буфера во второй поток |
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ