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
Reader
Запись данных
OutputStream
Writer

Практическое применение

Сами классы InputStream, OutputStream, Reader и Writer в явном виде никто не использует: они не присоединены ни к каким внешним объектам, из которых можно читать данные (или в которые можно писать данные). Однако у этих четырех классов много классов-наследников, которые умеют очень многое.


2. Класс InputStream

Класс InputStream интересен тем, что является классом-родителем для сотен классов-наследников. В нем самом нет никаких данных, однако у него есть методы, которые есть у всех его классов-наследников.

Объекты-потоки вообще редко хранят в себе данные. Поток — это инструмент чтения/записи данных, но не хранения. Хотя бывают и исключения.

Методы класса InputStream и всех его классов-наследников:

Методы Описание
int read()
Читает один байт из потока
int read(byte[] buffer)
Читает массив байт из потока
byte[] readAllBytes()
Читает все байты из потока
long skip(long n)
Пропускает n байт в потоке (читает и выкидывает)
int available()
Проверяет, сколько байт еще осталось в потоке
void close()
Закрывает поток

Вкратце пройдемся по этим методам:

Метод 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(). Пример:

Код Примечание
String src = "c:\\projects\\log.txt";
String dest = "c:\\projects\\copy.txt";

try(FileInputStream input = new FileInputStream(src);
FileOutputStream output = new FileOutputStream(dest))
{
   byte[] buffer = new byte[65536]; // 64Kb
   while (input.available() > 0)
   {
      int real = input.read(buffer);
      output.write(buffer, 0, real);
   }
}



InputStream для чтения из файла
OutputStream для записи в файл

Буфер, в который будем считывать данные
Пока данные есть в потоке

Считываем данные в буфер
Записываем данные из буфера во второй поток

В этом примере мы использовали два класса: FileInputStream — наследник InputStream для чтения данных из файла, и класс FileOutputStream — наследник OutputStream для записи данных в файл. О втором классе расскажем немного позднее.

Еще один интересный момент — это переменная real. Когда из файла будет читаться последний блок данных, легко может оказаться, что его длина меньше 64Кб. Поэтому в output нужно тоже записать не весь буфер, а только его часть: первые real байт. Именно это и делается в методе write().



3. Класс Reader

Класс Reader — это полный аналог класса InputStream, с одним только отличием: он работает с символами — char, а не с байтами. Класс Reader, так же, как и класс InputStream самостоятельно нигде не используется: он является классом-родителем для сотен классов-наследников и задает для них всех общие методы.

Методы класса Reader (и всех его классов-наследников):

Методы Описание
int read()
Читает один char из потока
int read(char[] buffer)
Читает массив char’ов из потока
long skip(long n)
Пропускает n char’ов в потоке (читает и выбрасывает)
boolean ready()
Проверяет, что в потоке еще что-то осталось
void close()
Закрывает поток

Методы очень похожи на методы класса InputStream, хотя есть и небольшие отличия.

Метод int read()

Это метод читает из потока один char и возвращает его. Тип char расширяется до типа int, но первые два байта результата всегда нули.

Метод int read(char[] buffer)

Это вторая модификация метода read(). Он позволяет считать из Reader сразу массив символов. Массив для символов нужно передать в качестве параметра. Метод возвращает число — количество реально прочитанных символов.

Метод skip(long n)

Этот метод позволяет пропустить n первых символов из объекта Reader. Работает точно так же, как аналогичный метод класса InputStream. Возвращает число символов, которые были реально пропущены.

Метод boolean ready()

Возвращает true, если в потоке есть еще не прочитанные байты.

Метод void close()

Метод close() закрывает поток данных и освобождает связанные с ним внешние ресурсы. После закрытия потока данные из него читать больше нельзя.

Давайте для сравнения напишем программу, которая копирует текстовый файл:

Код Примечание
String src = "c:\\projects\\log.txt";
String dest = "c:\\projects\\copy.txt";

try(FileReader reader = new FileReader(src);
FileWriter writer = new FileWriter(dest))
{
   char[] buffer = new char[65536]; // 64Kb
   while (reader.ready())
   {
      int real = reader.read(buffer);
      writer.write(buffer, 0, real);
   }
}



Reader для чтения из файла
Writer для записи в файл

Буфер, в который будем считывать данные
Пока данные есть в потоке

Читаем данные в буфер
Записываем данные из буфера во второй поток