пятница, 17 августа 2018 г.

Java IO - часть 2

Доброго времени суток, дорогой объектно-ориентированный друг!
В предыдущей части мы научились работать с файлами, как с неделимыми единицами файловой системы. Теперь зададимся вопросом - как же работать с содержимым файлов? Работа с содержимым файлов является частным случаем работы с потоками данных. Практически любая программа рано или поздно должна что-то откуда-то прочитать и куда-то потом сохранить. Поэтому владеть средствами чтения и записи информации - архинужно и архиважно! Вперед, товарищи!


Потоки
Источники и приемники информации так разнообразны, что вряд-ли хватит десяти таких статей, чтобы все их перечислить. Поэтому многолетняя эволюция программистской мысли привела к возникновению понятия поток - stream - универсального программного интерфейса для работы с данными. На текущий момент, практически все языки программирования общего назначения поддерживают эту концепцию. И Java не является исключением.

Источник информации называется входным потоком или потоком ввода - input stream.
При использовании входных потоков информация "входит" в программу из некоего источника, как показано на рисунке ниже.

Поток вывода - это приемник информации, куда мы "выводим" данные из программы:
Работа с потоками имеет следующие особенности:
  1. Работа с байтами идет строго последовательно, один за другим. Нельзя одновременно читать из нескольких позиций потока;
  2. Все операции с потоком - блокирующие. На время операций чтения и записи выполнение вашей программы в текущей нити приостанавливается;
  3. После работы с потоком его нужно обязательно закрыть. Нарушение этого правила может нанести урон как вашей программе, так и операционной системе в целом;
Все потоки делятся на 2 большие группы - байтовые, которые оперируют с байтами, и символьные, которые оперируют символами. 
В этой статье рассмотрим байтовые потоки.
Байтовые потоки ввода
Для работы с входными байтовыми потоками в Java существует класс java.io.InputStream. Он является родительским классом для всех входных байтовых потоков.

Рассмотрим методы класса InputStream:

/**
 * Читает один байт из входного потока.
 *
 * @return     прочитанный байт в виде целого числа от 0 до 255 или -1, 
 *             если байтов больше нет
 * @exception  IOException  ошибка ввода-вывода.
 */
public int read() throws IOException;
/**
 * Метод читает несколько байтов из потока и сохраняет их в переданный массив байтов b.
 * @param      b массив байтов, в котором будут храниться прочитанные байты.
 * @return     количество прочитанных байтов или -1, если байтов в потоке больше нет.
 * @exception  IOException  если невозможно прочитать следующий байт или если поток уже закрыт 
 *             или любая другая ошибка ввода-вывода.
 * @exception  NullPointerException  если b равен null.
 */
public int read(byte b[]) throws IOException;
/**
 * Метод читает несколько байтов из потока и сохраняет их в указанную область массива b.
 *
 * @param      b     массив, в котором будут храниться прочитанные байты.
 * @param      off   стартовая позиция в массиве b, куда будут читаться данные.
 * @param      len   максимальное число байтов, которые будут прочитаны из потока.
 * @return     реальное число прочитанных из потока байтов или -1, если не байтов больше нет.
 * @exception  IOException  если невозможно прочитать следующий байт или если поток уже закрыт 
 *             или любая другая ошибка ввода-вывода.
 * @exception  NullPointerException если b равен null.
 * @exception  IndexOutOfBoundsException если off отрицательное число 
 *                                       или если len - отрицательное число или если len > b.length - off
 */
public int read(byte b[], int off, int len) throws IOException;
/**
 * Метод пропускает n байтов из входного потока.
 *
 * @param      n   число байтов, которое нужно пропустить.
 * @return     число реально пропущенных байтов
 * @exception  IOException  если поток не поддерживает пропуск байтов 
 *                 или если возникла любая другая ошибка ввода-вывода.
 */
public long skip(long n) throws IOException;
/**
 * Метод возвращает число доступных байтов для чтения на текущий момент. 
 * Это число не всегда совпадает с полным числом байтов в потоке.
 *
 * @return     число доступных для чтения байтов.
 * @exception  IOException если возникла ошибка ввода-вывода
 */
public int available() throws IOException;
/**
 * Метод закрывает поток.
 *
 * @exception  IOException  если возникла ошибка ввода-вывода
 */
public void close() throws IOException;
/**
 * Метод ставит метку на текущей позиции в потоке, чтобы иметь возможность потом к ней вернуться. 
 * Не все потоки поддерживают механизм меток.
 *
 * @param   readlimit   максимальное количество символов,  прочитанных после установки метки, 
 *                      чтобы метка оставалась актуальной.
 *                      Если будет прочитано большее количество символов - метка становится неактуальной
 *                      и метод reset завершится с ошибкой.
 */
public void mark(int readlimit);
/**
 * Перемещает чтение потока в позицию, ранее помеченную меткой.
 *
 * @exception  IOException  Если метка не была установлена или
 *                          если метка стала неактуальной или
 *                          если поток не поддерживает метод reset() или
 *                          в случае возникновения любой другой ошибки ввода-вывода
 */
public void reset() throws IOException;
/**
 * Метод для определения, поддерживает ли данный поток механизм меток. 
 *
 * @return true, если метки поддерживаются и false - если нет
 */
public boolean markSupported();
InputStream - абстрактный класс и мы не можем его использовать для непосредственно создания потока.
Поэтому на практике используется один из его подклассов. Чаще всего используются:
/**
 * Класс для чтения массива байтов в виде потока.
 */
java.io.ByteArrayInputStream
/**
 * Класс для чтения байтов из файла.
 */
java.io.FileInputStream
/**
 * Класс, который добавляет возможность буферизации данных при чтении из другого потока.
 */
java.io.BufferedInputStream
Байтовые потоки вывода
Для работы с выходными потоками существует класс java.io.OutputStream.
Этот класс содержит следующие методы:
/**
 * Метод записывает один байт в поток вывода.
 *
 * @param      b   значение байта.
 * @exception  IOException  если поток закрыт или в случае любая другая ошибка ввода-вывода.
 */
public void write(int b) throws IOException;
/**
     * Метод записывает массив байтов в поток вывода.
     *
     * @param      b   массива байтов.
     * @exception  IOException  в случае любой ошибки ввода-вывода.
     */
public void write(byte b[]) throws IOException;
/**
 * Метод записывает в поток вывода часть массива байтов.
 * 
 * @param      b     массив байтов, часть которого будет записана в поток.
 * @param      off   начальная позиция в массиве, с которой начинается блок информации.
 * @param      len   число байтов, которые будут записаны в поток.
 *
 * @exception  IOException               если поток закрыт или в случае любой другой ошибки ввода-вывода.
 * @exception  NullPointerException      если b равен null
 * @exception  IndexOutOfBoundsException если off - отрицательное число 
 *                                       или len - отрицательное число или off+len > b.length
 */
public void write(byte b[], int off, int len) throws IOException;
/**
 * Вся информация в буфере потока насильно записывается в поток вывода.
 *
 * @exception  IOException в случае любой ошибки ввода-вывода
 */
public void flush() throws IOException;
/**
 * Метод закрывает поток.
 *
 * @exception  IOException в случае любой ошибки ввода-вывода.
 */
public void close() throws IOException;
Наиболее часто используемые подклассы OutputStream:
/**
 * Класс для записи данных в массив байтов в памяти.
 */
java.io.ByteArrayOutputStream
/**
 * Класс для записи байтов в файл.
 */
java.io.FileOutputStream
/**
 * Класс, который добавляет буферизацию при записи данных в поток.
 */
java.io.BufferedOutputStream

Особенности архитектуры библиотеки ввода-вывода
В системе ввода-вывода Java активно используется такой шаблон проектирования, как декоратор. То есть, для добавления дополнительной функциональности к уже существующему потоку, этот поток "вкладывается" в другой поток, который реализует требуемую функциональность.
Например, открытие потока для чтения файла выглядит так:
InputStream input = new FileInputStream(new File("/tmp/1.txt"));
Если вы хотите добавить буферизацию при чтении данных из файла, то поток выше оборачивается в BufferedInputStream:
InputStream input = new BufferedInputStream(new FileInputStream(new File("/tmp/1.txt")));
Пример 1 - чтение файла
Допустим, у нас есть текстовый файл на диске - C:/1.txt, в котором хранится текст - "Hello, world". Прочитаем этот файл и распечатаем текст в консоль. 
Для того, чтобы прочитать данные из файла, нам понадобится входной поток FileInputStream. Всю прочитанную информацию мы должны сохранить в памяти, чтобы потом сконструировать из нее строку. Полученную строку мы распечатаем в консоль и увидим содержимое файла.
Для хранения байтов в памяти нужно использовать ByteArrayOutputStream. 
При этом помним, что потоки нужно закрывать после использования. Итого, получаем:
import java.io.*;

public class CopyFile {
    public static void main(String[] args) throws IOException {
        //Создаем объект File
        File file = new File("C:/1.txt");

        //Входной поток - для чтения информации из файла
        InputStream input = null;
        //Выходной поток - для хранения прочитанного в памяти
        ByteArrayOutputStream output = null;
        try {
            //Инициализируем входной поток
            input = new FileInputStream(file);
            //Инициализируем выходной поток
            output = new ByteArrayOutputStream();

            //b - прочитанный байт
            int b;
            //Читаем из входного потока по одному байту до тех пор, пока не встретим -1
            while ((b = input.read()) != -1) {
                //Каждый прочитанный байт записываем в поток вывода
                output.write(b);
            }
        } finally {
            //Закрываем входной поток
            if (input != null) {
                input.close();
            }
            
            //Закрываем выходной поток
            if(output != null) {
                output.close();
            }
        }

        byte[] bytes = output.toByteArray();
        System.out.println(new String(bytes, "UTF-8"));
    }
}
Код кажется многословным, но, к счастью, его можно упростить.
Во-первых, все потоки реализуют интерфейс AutoCloseable, что означает, что мы можем воспользоваться конструкцией try-with-resources.
Во-вторых - нет необходимости закрывать поток ByteArrayOutputStream, так как все, что он делает - хранит данные в памяти. Если вы посмотрите исходный код класса ByteArrayOutputStream, то увидите, что метод close() ничего не делает.
В итоге, код будет выглядеть так:
import java.io.*;

public class CopyFile {
    public static void main(String[] args) throws IOException {
        //Создаем объект File
        File file = new File("C:/1.txt");

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try (InputStream input = new FileInputStream(file)) {           
            //b - прочитанный байт
            int b;
            //Читаем из входного потока по одному байту до тех пор, пока не встретим -1
            while ((b = input.read()) != -1) {
                //Каждый прочитанный байт записываем в поток вывода
                output.write(b);
            }
        }

        //Массив байтов в памяти, которые мы прочитали из файла
        byte[] bytes = output.toByteArray();
        //Создаем строку из массива байтов
        String string = new String(bytes, "UTF-8");  
        //Распечатываем строку в консоль     
        System.out.println(string);
    }
}

Нужно отметить, что вообще говоря, копирование по одному байту - не самая оптимальная реализация. Создадим буфер в памяти и будем копировать по несколько байтов одновременно. В этом случае, код будет выглядит так:
import java.io.*;

public class CopyFile {
    public static void main(String[] args) throws IOException {
        //Создаем объект File
        File file = new File("C:/1.txt");

        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try (InputStream input = new FileInputStream(file)) {
            //len - количество прочитанных в буфер байтов 
            int len;
            //Читаем из входного потока до тех пор, пока не встретим -1
            byte[] bytes = new byte[1024];
            while ((len = input.read(bytes)) != -1) {
                //Записываем в поток вывода len байтов, начиная с начала буфера bytes
                output.write(bytes, 0, len);
            }
        }

        //Массив байтов в памяти, которые мы прочитали из файла
        byte[] bytes = output.toByteArray();
        //Создаем строку из массива байтов
        String string = new String(bytes, "UTF-8");
        //Распечатываем строку в консоль     
        System.out.println(string);
    }
}
Пример 2 - HTTP запросы
Для каждого приемника или источника информации или есть отдельный класс потока или существует метод, который возвращает объект потока.
Допустим, мы хотим прочитать исходный код страницы, открываемой по ссылке https://ya.ru.
Для этого нужно послать HTTP GET запрос серверу ya.ru, после этого открыть поток и прочитать содержимое, которое отправляет нам сервер.

Воспользуемся для этого классом java.net.URL, с помощью которого можно посылать HTTP-запросы.
Итоговый код программы выглядит так:
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class HttpGet {
    public static void main(String[] args) throws IOException {
        //Конструируем ссылку на сайт
        URL url = new URL("https://ya.ru/");

        //Открываем соединение до сервера
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        //Указываем, что мы собираемся читать данные с сервера
        connection.setDoInput(true);
        //Указываем, что мы собираемся послать GET-запрос
        connection.setRequestMethod("GET");

        //Отправляем запрос на сервер и читаем код состояния. 
        int responseCode = connection.getResponseCode();
        //Если сервер вернул код 200 - OK - значит запрос корректный и сервер, обработав его, вернул данные
        if (responseCode == HttpURLConnection.HTTP_OK) {
            //Буфер в памяти, куда будем сохранять данные с сервера
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            //открываем поток ввода и читаем его, копируя данные в output
            try (InputStream input = connection.getInputStream()) {
                //Переносим данные из input в output порциями по 100 байт
                byte[] bytes = new byte[100];
                int read;
                while ((read = input.read(bytes)) != -1) {
                    output.write(bytes, 0, read);
                }
            }

            //Все байты, прочитанные с сервера конвертируем в строку и распечатываем в консоль
            String response = output.toString("UTF-8");
            System.out.println(response);
        }
    }
}
Результатом работы этой программы будет исходный код поисковой страницы Yandex, распечатанный в консоль.
Пример 3 - создание архива
Подклассы потоков могут иметь специализированные методы, которые нужны для создания необходимой структуры данных в приемнике. 
Например, при создании zip-архива нужно использовать класс java.util.zip.ZipOutputStream и его методы:

/**
 * Метод сообщает потоку о начале записи в архив нового элемента содержимого.
 * 
 * @param entry элемент для записи в архив
 * @exception ZipException если структура zip-файла нарушена
 * @exception IOException в случае любой ошибки ввода-вывода
 */
public void putNextEntry(ZipEntry entry) throws IOException;
/**
 * Сообщает потоку о конце записи элемента содержимого.
 * 
 * @exception ZipException если структура zip-файла нарушена
 * @exception IOException в случае любой ошибки ввода-вывода
 */
public void closeEntry() throws IOException;
Исходный код программы создания архива выглядит так:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZipArchive {
    public static void main(String[] args) throws IOException {
        try(ZipOutputStream output = new ZipOutputStream(new FileOutputStream(new File("D:/1.zip")))) {
            ZipEntry entry = new ZipEntry("TEST.txt");
            output.putNextEntry(entry);
            output.write("ABC".getBytes("UTF-8"));
            output.closeEntry();
        }
    }
}
После выполнения этой программы на диске D: будет создан файл 1.zip, содержимое которого выглядит так:
Заключение
Итак, друзья, мы рассмотрели основные принципы работы с байтовыми потоками в Java. 
Если у вас возникла задача чтения или хранения данных - ищите нужный поток.
А если такого потока нет - не бойтесь создавать свой=)

P.S.
Мы научились работать с байтами, но в реальности, намного чаще приходится работать с текстом. К счастью, Java позволяет легко это сделать, поэтому в следующий раз рассмотрим символьные потоки и их широкое применение.
До новых встреч!

Комментариев нет:

Отправить комментарий