1. Клас OutputStream

З потоками вводу ми щойно розібралися. Тепер саме час поговорити про потоки виводу.

Клас OutputStream є батьківським класом для всіх класів, які підтримують байтове виведення. Це абстрактний клас, який сам нічого не робить: для цього є класи-спадкоємці на всі випадки.

Дещо складно, чи не так? Якщо простіше, цей клас працює з байтами, а не, скажімо, із символами чи іншими типами даних. А термін «абстрактний клас» означає, що зазвичай використовується не цей клас, а класи-спадкоємці, наприклад FileOutputStream і подібні.

Але повернемося до класу OutputStream. Цей клас має методи, що їх зобов'язані реалізовувати всі його класи-спадкоємці. Перелічимо основні з них:

Методи Опис
void write(int b)
Записує один байт (не int) у потік.
void write(byte[] buffer)
Записує масив байтів у потік
void write(byte[] buffer, off, len)
Записує частину масиву байтів у потік
void flush()
Записує в потік усі дані, які є в буфері
void close()
Закриває потік

Під час створення об'єкта класу-спадкоємця InputStream зазвичай указується об'єкт-джерело, з якого InputStream читає дані. Під час створення об'єкта класу-спадкоємця OutputStream також зазвичай указується цільовий об'єкт або цільовий потік, в який будуть записуватися дані.

Побіжно перегляньмо всі методи класу OutputStream:

Метод write(int b)

Цей метод записує в потік виводу один байт (не int). Передане значення перетворюється на байтовий тип, три перші байти відкидаються.

Метод write(byte[] buffer)

Записує в потік виводу переданий масив байтів.

Метод write(byte[] buffer, int offset, int length)

Записує в потік виводу частину переданого масиву байтів. Змінна offset задає номер першого елемента масиву, length — довжину записуваного фрагмента.

Метод flush()

Метод flush() використовується для примусового запису в цільовий потік таких даних, які можна кешувати в цьому потоці. Застосовується в разі використання буферизації та/або декількох об'єктів-потоків, об'єднаних у ланцюжок.

Метод close()

Записує в цільовий об'єкт усі незаписані дані. У разі використання оператора try-with-resources метод close() можна не викликати.

Приклад: копіювання файлу

Код Примітка
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 для запису у файл

Буфер, в який зчитуються дані
Доки в потоці є дані

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

2. Клас Writer

Клас Writer — це повний аналог класу OutputStream, тільки він працює не з байтами, а із символами — char.

Це абстрактний клас: об'єкти класу Writer створити неможливо. Його головне призначення — бути єдиним батьківським класом для сотень класів-спадкоємців і передати їм спільні методи роботи із символьними потоками.

Методи класу Writer (і всіх його класів-спадкоємців):

Методи Опис
void write(int b)
Записує один символ (не int) у потік.
void write(char[] buffer)
Записує масив символів у потік
void write(char[] buffer, off, len)
Записує частину масиву символів у потік
void write(String str)
Записує рядок у потік
void write(String str, off, len)
Записує частину рядка в потік
void flush()
Записує в потік усі дані, які є в буфері
void close()
Закриває потік

Методи дуже схожі на методи класу OutputStream, тільки працюють не з байтами, а із символами.

Короткий опис методів:

Метод write(int b)

Цей метод записує в потік виводу один символ char (не int). Передане значення перетворюється на тип char, два перші байти відкидаються.

Метод write(char[] buffer)

Записує в потік виводу переданий масив символів.

Метод write(char[] buffer, int offset, int length)

Записує в потік виводу частину переданого масиву символів. Змінна offset задає номер першого елемента масиву, length — довжину записуваного фрагмента.

Метод write(String str)

Записує в потік виводу переданий рядок.

Метод write(String str, int offset, int length)

Записує в потік виводу частину переданого рядка й перетворює рядок на масив символів. Змінна offset задає номер першого елемента масиву, length — довжину записуваного фрагмента.

Метод flush()

Метод flush() використовується для примусового запису в цільовий потік таких даних, які можна кешувати в цьому потоці. Застосовується в разі використання буферизації та/або декількох об'єктів-потоків, об'єднаних у ланцюжок.

Метод close()

Записує в цільовий об'єкт усі незаписані дані. У разі використання оператора try-with-resources метод 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]; // 128Kb
   while (reader.ready())
   {
      int real = reader.read(buffer);
      writer.write(buffer, 0, real);
   }
}



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

Буфер, в який зчитуються дані
Доки в потоці є дані

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

Класс StringWriter

StringWriter — це ще один цікавий клас-спадкоємець класу Writer. Він містить змінюваний рядок — об'єкт StringBuffer. І щоразу, коли ви щось «пишете» в об'єкт StringWriter, текст просто додається у внутрішній буфер.

Приклад:

Код Примітка
StringWriter writer = new StringWriter();
writer.write("Hello");
writer.write(String.valueOf(123));

String result = writer.toString();
Створюється цільовий символьний потік StringWriter
Рядок записується в буфер усередині StringWriter
Рядок записується в буфер усередині StringWriter

Перетворюємо вміст об'єкта на рядок

У цьому коді клас StringWriter по суті є обгорткою класу StringBuffer, однак клас StringWriter — це спадкоємець класу-потоку Writer і може використовуватися в ланцюжках з об'єктів-потоків. Доволі корисна властивість на практиці.



3. Клас PrintStream

Класи потокового виводу теж можна об'єднувати в ланцюжки з використанням потоків-посередників, які записують дані в переданий їм цільовий потік. Загальна схема взаємодії цих потоків така:

Клас PrintStream

PrintStream — це найцікавіший з усіх проміжних потоків виводу, який до того ж має найбільше функцій. Він має кілька десятків методів і аж 12 конструкторів.

Клас PrintStream є спадкоємцем класу FilterOutputStream, а той — спадкоємцем класу OutputStream. Тому клас PrintStream має всі методи батьківських класів і до того ж свої власні. Наведемо найцікавіші з них:

Методи Опис
void print(obj)
Перетворює переданий об'єкт на рядок і виводить у цільовий потік.
void println(obj)
Перетворює переданий об'єкт на рядок і виводить у цільовий потік. В кінці додає символ розриву рядка.
void println()
Виводить у цільовий потік символ розриву рядка.
PrintStream format(String format, args...)
Конструює та виводить рядок на основі рядка-шаблону й переданих аргументів подібно до методу String.format().

А де ж кілька десятків методів, запитаєте ви?

Річ у тім, що цей клас має багато варіантів методів print() і println() з різними аргументами, які вже описано в таблиці вище.

Ми навіть не розглядатимемо ці методи, тому що ви їх і так вже добре знаєте. Здогадуєтеся, до чого я хилю?

Пам'ятаєте команду System.out.println()? Її можна записати у два рядки:

Код Виведення на екран
PrintStream stream = System.out;
stream.println("Hello!");
Hello!

Наша улюблена команда System.out.println() — це виклик методу println() для статичної змінної out класу System. І ця змінна має тип PrintStream.

На всіх попередніх рівнях ви майже в кожній задачі викликали методи класу PrintStream і навіть не здогадувалися про це!

Практичне використання

У мові Java є один цікавий клас — ByteArrayOutputStream, успадкований від OutputStream, і цей клас, власне, є масивом байтів, що динамічно збільшується.

Об'єкт ByteArrayOutputStream і об'єкт PrintStream можна об'єднати в такий ланцюжок:

Код Опис
ByteArrayOutputStream baos = new ByteArrayOutputStream();

try(PrintStream stream = new PrintStream(baos))
{
   stream.println("Hello");
   stream.println(123);
}

String result = baos.toString();

System.out.println(result);
Створили в пам'яті буфер для запису

Обгорнули буфер об'єктом PrintStream

Записали дані як у консоль



Перетворили масив на рядок!

Виведення на екран:
Hello!
123