воскресенье, 19 августа 2018 г.

Java IO - часть 3

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


Символьные потоки ввода
Для работы с символьными потоками ввода существует класс java.io.Reader. Его методы:
/**
 * Метод читает символы и сохраняет их в символьном буфере target.
 *
 * @param target буфер для хранения символов
 * @return число сохраненных в буфере символов или -1, если символы в источнике закончились
 *
 * @throws IOException если произошла ошибка ввода-вывода
 * @throws NullPointerException если target равен null
 * @throws java.nio.ReadOnlyBufferException если target предназначен только для чтения
 */
public int read(CharBuffer target) throws IOException;
/**
 * Метод читает из потока код одного символа.
 *
 * @return     возвращает код одного символа в виде целого числа или -1,
 *             если символов больше нет во входном потоке
 *
 * @exception  IOException при возникновении ошибки ввода-вывода
 */
public int read() throws IOException;
/**
 * Метод читает символы и сохраняет их в массив.
 *
 * @param       cbuf массив, в котором будут сохранены символы
 *
 * @return      число прочитанных символов или -1, 
 *              если символов больше нет во входном потоке
 *
 * @exception   IOException при возникновении ошибки ввода-вывода
 */
public int read(char cbuf[]) throws IOException;
/**
 * Читает символы и сохраняет их в определенную область массива.
 *
 * @param      cbuf  массив, в котором будут сохранены символы
 * @param      off   позиция относительно начала массива, 
 *             в которую будут сохраняться прочитанные символы
 * @param      len   максимальное число символов, которые будут прочитаны
 *
 * @return     число прочитанных символов или -1, 
 *             если символов больше нет во входном потоке
 *
 * @exception  IOException  при возникновении ошибки ввода-вывода
 */
public int read(char cbuf[], int off, int len) throws IOException;
/**
 * Метод пропускает символы в потоке.
 *
 * @param  n  число символов, которое нужно пропустить
 *
 * @return    число реально пропущенных символов
 *
 * @exception  IllegalArgumentException  если n - отрицательное число
 * @exception  IOException  при возникновении ошибки ввода-вывода
 */
public long skip(long n) throws IOException;
/**
 * Готов ли поток к чтению данных.
 *
 * @return true если поток готов к чтению
 *
 * @exception  IOException при возникновении ошибки ввода-вывода
 */
public boolean ready() throws IOException;
/**
 * Метод для определения, поддерживает ли данный поток механизм меток. 
 *
 * @return true, если метки поддерживаются и false - если нет
 */
public boolean markSupported();
/**
 * Метод ставит метку на текущей позиции в потоке, 
 * чтобы иметь возможность потом к ней вернуться. 
 * Не все потоки поддерживают механизм меток.
 *
 * @param  readAheadLimit  максимальное количество символов,  
 *                         прочитанных после установки метки, 
 *                         чтобы метка оставалась актуальной.
 *                         Если будет прочитано большее количество символов - 
 *                         метка становится неактуальной
 *                         и метод reset завершится с ошибкой.
 *
 * @exception  IOException если механизм меток не поддерживается или 
 *                         в случае любой другой ошибки ввода-вывода
 */
public void mark(int readAheadLimit) throws IOException;
/**
 * Перемещает чтение потока в позицию, ранее помеченную меткой.
 *
 * @exception  IOException  Если метка не была установлена или
 *                          если метка стала неактуальной или,
 *                          если поток не поддерживает метод reset() или
 *                          при возникновении ошибки ввода-вывода
 */
public void reset() throws IOException;
/**
 * Закрывает поток.
 *
 * @exception  IOException при возникновении ошибки ввода-вывода
 */
public void close() throws IOException;
Как и в случае байтовых потоков, класс Reader - абстрактный класс, поэтому для создания конкретных потоков используются его подклассы. Наиболее популярные из них:

/**
 * Класс для чтения текста из файла.
 */
java.io.FileReader;
/**
 * Класс, превращающий входной поток байтов во входной поток символов.
 */
java.io.InputStreamReader;
/**
 * Класс, который добавляет возможность буферизации и чтения строками.
java.io.BufferedReader;
Символьные потоки вывода
Теперь познакомимся с символьными потоками вывода. Базовым классом для всех потоков вывода в Java является класс java.io.Writer. Его методы:
/**
 * Метод записывает в поток один символ.
 *
 * @param  c целочисленное представление символа
 *
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public void write(int c) throws IOException;
/**
 * Записывает массив символов в поток.
 *
 * @param  cbuf массив символов
 *
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public void write(char cbuf[]) throws IOException;
/**
 * Записывает область массива символов в поток.
 *
 * @param  cbuf массив символов
 * @param  off начало области массива, которую надо записать в поток
 * @param  len количество символов, которые надо записать в поток
 *
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public void write(char cbuf[], int off, int len) throws IOException;
/**
 * Записывает строку в поток.
 *
 * @param  str строка
 *
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public void write(String str) throws IOException;
/**
 * Записывает часть строки в поток
 *
 * @param  str строка
 * @param  off индекс первого символа подстроки, которую надо записать
 * @param  len число символов, которые надо записать
 *
 * @throws  IndexOutOfBoundsException если off < 0 или 
                                      len < 0 или off + len < 0 или 
                                      off + len > str.length()
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public void write(String str, int off, int len) throws IOException;
/**
 * Дописывает символьную последовательность в поток.
 *
 * @param  csq символьная последовательность
 *
 * @return  текущий объект Writer
 *
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public Writer append(CharSequence csq) throws IOException;
/**
 * Дописывает в поток часть символьной последовательности.
 *
 * @param  csq символьная последовательность, часть которой нужно дописать в поток
 *
 * @param  start индекс первого символа, который нужно записать в поток
 * @param  end индекс последнего символа, который нужно записать в поток
 *
 * @return  текущий объект Writer
 *
 * @throws  IndexOutOfBoundsException если start - отрицательное число или 
 *                                    end отрицательное число или 
 *                                    start > end или 
 *                                    end > csq.length()
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public Writer append(CharSequence csq, int start, int end) throws IOException;
/**
 * Дописывает в поток один символ.
 *
 * @param  c символ, который нужно дописать в поток
 *
 * @return  текущий объект Writer
 *
 * @throws  IOException при возникновении ошибки ввода-вывода
 *
 */
public Writer append(char c) throws IOException;
/**
 * Метод записывает всю информацию из буфера в приемник.
 *
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public void flush() throws IOException;
/**
 * Закрывает поток, предварительно записав в приемник всю информацию из буфера.
 *
 * @throws  IOException при возникновении ошибки ввода-вывода
 */
public void close() throws IOException;
Чаще всего используются следующие подклассы класса Writer:

/**
 * Класс, для записи текста в файл.
 */
java.io.FileWriter;
/**
 * Класс, который добавляет буферизацию при записи информации в поток.
 */
java.io.BufferedWriter;
/**
 * Класс, который предоставляет множество методов для удобной записи различной информации в поток.
 */
java.io.PrintWriter;
Особая роль PrintStream
В пакете java.io есть удивительный класс PrintStream, который является наследником OutputStream, но, при этом, имеет множество методов записи текста в поток. Если сравнить класс PrintWriter и PrintStream, можно заметить, что методы для записи чисел, символов и т.п. - просто совпадают. 
Зачем же наследовать от класса OutputStream, отвечающего за байтовый вывод, класс, который отвечает за символьный вывод?
Ответ на самом деле довольно прост. Класс PrintStream был добавлен в Java в версии 1.0, а класс Writer - в версии 1.1. То есть на момент добавления PrintStream базовых классов, отвечающих за символьный ввод и вывод просто не существовало. 
Но существует и более интересная причина такой долгой жизни класса PrintStream и она связана с так называемыми стандартными потоками.
Стандартные потоки
Стандартные потоки - это потоки данных, которые всегда открываются при запуске процесса в операционной системе. Таких потоков обычно три:
  1. Стандартный поток ввода - stdin - предназначен для чтения команд пользователя. По умолчанию связан с устройством ввода текстовой информации - клавиатурой;
  2. Стандартный поток вывода - stdout - предназначен для отображения текстовой информации пользователю. По умолчанию связан с устройством вывода текстовой информации - монитором;
  3. Стандартный поток вывода ошибок - stderr - предназначен для вывода ошибок и диагностической информации. Обычно связан с тем же устройством, что и stdout, но, в отличии от stdout, вывод в stderr не буферизируется.
Java по умолчанию всегда автоматически инициализирует и открывает стандартные потоки:
  1. Стандартный поток ввода - System.in - объект класса InputStream;
  2. Стандартный поток вывода - System.out - объект класса PrintStream;
  3. Стандартный поток вывода ошибок - System.err - объект класса PrintStream;
Теперь понятно, что поскольку стандартный поток вывода и ошибок имеют класс PrintStream, от этого класса нельзя избавиться, не разрушив обратной совместимости.
К тому же, аргументом в пользу PrintStream является то, что стандартные потоки не всегда оперируют символьной информацией. 
Иногда - хотя и довольно редко - возникает необходимость передать через стандартные потоки массивы байтов. Тут и пригодится тот факт, что PrintStream является наследником OutputStream и способен одинаково хорошо работать и с текстом и с байтами.
Утилитный класс Scanner
Для того, чтобы сделать чтение текста удобнее в Java 1.5 в пакет java.util был добавлен класс Scanner. Он имеет множество удобных методов для чтения не только текста, но и чисел, логических констант и многого другого.
Для демонстрации возможностей этого класса, напишем небольшую программу, которая запрашивает у пользователя два целых числа, складывает их, и выдает пользователю результат:
import java.util.Scanner;

public class IntSum {
    public static void main(String[] args) {
        //Используем Scanner для чтения информации введенной пользователем через консоль
        //В данном случае закрывать scanner не нужно, так как он
        //связан со стандартным потоком ввода System.in, которым управляет сама Java
        Scanner scanner = new Scanner(System.in);
        System.out.println("Введите целое число: ");
        //Читаем первое число. Если пользователь введет не число - возникнет ошибка
        int value = scanner.nextInt();

        System.out.println("Введите еще одно целое число: ");
        //Читаем второе число. Если пользователь введет не число - возникнет ошибка
        int value2 = scanner.nextInt();

        int result = value + value2;
        //Выводим результат в консоль, используя метод printf, 
        //который умеет распознавать шаблоны (%d, %s и т.п.) и заменять их числами, строками и т.п.
        System.out.printf("Сумма чисел %d и %d равна %d", value, value2, result);
    }
}
Теперь, когда мы основательно изучили теорию - рассмотрим несколько примеров.
Пример 1 - из консоли в файл
Пусть мы хотим всю информацию, введенную пользователем в консоль - сохранять в файл. 
Главное, о чем нужно помнить при работе с текстом - это кодировка символов. В этой программе - мы читаем и пишем в кодировке UTF-8:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Scanner;

public class LogMe {
    public static void main(String[] args) throws IOException {
        //Файл, куда будем писать все введенное с клавиатуры
        File file = new File("D:/log.txt");

        //Используем Scanner для чтения того, что введет пользователь
        //В данном случае закрывать scanner не нужно, так как он
        //связан со стандартным потоком ввода System.in, которым управляет сама Java
        Scanner scanner = new Scanner(System.in, "UTF-8");
        //Используем PrintWriter для того, чтобы писать в файл
        try(PrintWriter writer = new PrintWriter(
                new OutputStreamWriter(
                        new FileOutputStream(file), "UTF-8"))) {
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                if(line != null) {
                    //Если пользователь ввел exit - завершаем программу
                    if("exit".equals(line)) {
                        return;
                    }

                    //Пишем в файл то, что ввел пользователь
                    writer.println(line);
                }
            }
        }
    }
}
Если вы запустите программу и введете пару строк, то после ввода exit - в файл запишется все, что вы ввели. Единственный нюанс в том, что если вы запустите программу несколько раз - она будет каждый раз перезаписывать содержимое файла.
Если же вы хотите каждый раз дописывать информацию - нужно немного изменить код:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Scanner;

public class LogMe {
    public static void main(String[] args) throws IOException {
        //Файл, куда будем писать все введенное с клавиатуры
        File file = new File("D:/log.txt");

        //Используем Scanner для чтения того, что введет пользователь.
        //В данном случае закрывать scanner не нужно, так как он
        //связан со стандартным потоком ввода System.in, которым управляет сама Java
        Scanner scanner = new Scanner(System.in, "UTF-8");
        //Используем PrintWriter для того, чтобы писать в файл
        try(PrintWriter writer = new PrintWriter(
                new OutputStreamWriter(
                        //Второй параметр true - значит не перезаписываем содержимое файла, 
                        //а дописываем в него
                        new FileOutputStream(file, true), "UTF-8"))) {
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                if(line != null) {
                    //Если пользователь ввел exit - завершаем программу
                    if("exit".equals(line)) {
                        return;
                    }

                    //Пишем в файл то, что ввел пользователь
                    writer.println(line);
                }
            }
        }
    }
}
Пример 2 - из файла в консоль
Рассмотрим обратную задачу - прочитаем содержимое файла и выведем его в консоль:
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class FileToConsole {
    public static void main(String[] args) throws FileNotFoundException {
        //Файл, содержимое которого мы будем читать
        File file = new File("D:/1.txt");

        //Используем Scanner, чтобы построчно читать содержимое файла в кодировке UTF-8
        //В данном случае - обязательно закрыть за собой поток, так как мы сами его и открыли
        try(Scanner scanner = new Scanner(file, "UTF-8")) {
            //Читаем файл построчно до тех пор, пока есть строки
            while (scanner.hasNextLine()) {
                //Читаем очередную строку из файла
                String line = scanner.nextLine();
                if(line != null) {
                    //Распечатываем строку из файла
                    System.out.println(line);
                }
            }
        }
    }
}
Вместо заключения
Думаете это все? О, нет, конечно не все! Нас впереди ждет еще одна очень увлекательная тема - непотоковая работа с данными. 


1 комментарий: