В Java для потоковой работы с файлами на уровне байтов используют три класса:
В прошлых лекциях мы коснулись BufferedReader’a, а также абстрактных классов InputStream & OutputStream и нескольких наследников. Сегодня рассмотрим 3 новых класса: FileInputStream, FileOutputStream и BufferedInputStream.
Кратко назначение каждого удобно свести в таблицу:
Давай проверим и запустим наш измененный код трижды:
Вот как будет выглядеть реализация чтения данных из файла при помощи
FileOutputStream записывает байты в файл, FileInputStream читает байты из файла, а BufferedInputStream ускоряет чтение, забирая данные не по одному байту, а блоками через буфер. Первые два наследуют абстрактные OutputStream и InputStream, а BufferedInputStream оборачивает поток чтения.
Привет! В сегодняшней лекции продолжим разговор о потоках ввода и вывода в Java, или сокращенно Java I/O («input-output»). Это не первая лекция на данную тему, и далеко не последняя :)
Кратко
FileOutputStreamзаписывает байты в файл; наследуетOutputStream.FileInputStreamчитает байты из файла; наследуетInputStream.BufferedInputStreamчитает данные блоками через буфер, ускоряя чтение (в примере в 2 раза).- Второй параметр конструктора
FileOutputStream(append = true) дозаписывает данные в конец файла вместо перезаписи. - После работы поток нужно закрывать методом
close(). - Все три класса работают с байтами, а не с символами.
В прошлых лекциях мы коснулись BufferedReader’a, а также абстрактных классов InputStream & OutputStream и нескольких наследников. Сегодня рассмотрим 3 новых класса: FileInputStream, FileOutputStream и BufferedInputStream.
Кратко назначение каждого удобно свести в таблицу:
| Класс | Назначение | Наследует |
|---|---|---|
FileOutputStream | запись байтов в файл | OutputStream |
FileInputStream | чтение байтов из файла | InputStream |
BufferedInputStream | чтение блоками через буфер (ускоряет ввод) | обертка над InputStream |
Класс FileOutputStream
Главное назначение класса FileOutputStream это запись байтов в файл. Ничего сложного :) FileOutputStream является одной из реализаций абстрактного класса OutputStream. В конструкторе объекты этого класса принимают либо путь к целевому файлу (в который и нужно записать байты), либо объект классаFile.
Рассмотрим оба примера:
public class Main {
public static void main(String[] args) throws IOException {
File file = new File("C:\\Users\\Username\\Desktop\\test.txt");
FileOutputStream fileOutputStream = new FileOutputStream(file);
String greetings = "Привет! Добро пожаловать на JavaRush - лучший сайт для тех, кто хочет стать программистом!";
fileOutputStream.write(greetings.getBytes());
fileOutputStream.close();
}
}
При создании объекта File мы указали в конструкторе путь, где он должен будет находиться. Создавать его заранее нет необходимости: если он не существует, программа создаст его сама.
Можно обойтись и без создания лишнего объекта, и просто передать строку с адресом:
public class Main {
public static void main(String[] args) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\test.txt");
String greetings = "Привет! Добро пожаловать на JavaRush - лучший сайт для тех, кто хочет стать программистом!";
fileOutputStream.write(greetings.getBytes());
fileOutputStream.close();
}
}
Результат в обоих случаях будет одинаковым. Мы можем открыть наш файл и увидеть там:
Привет! Добро пожаловать на JavaRush - лучший сайт для тех, кто хочет стать программистом!
Однако есть здесь один нюанс. Попробуй запустить код из примера выше несколько раз подряд, а потом загляни в файл, и ответь на вопрос: сколько записанных в него строк ты видишь?
Всего одну. Но ведь ты запускал код несколько раз. Однако при этом данные, оказывается, всякий раз перезаписывались, заменяя старые.
Что делать, если нас это не устраивает, и требуется последовательная запись? Что если мы хотим записать наше приветствие в файл три раза подряд?
Здесь все просто. Поскольку сам язык не может знать, какое именно поведение нам нужно в каждом случае, в конструктор FileOutputStream ты можешь передать дополнительный параметр, boolean append.
Если его значение true, данные будут дозаписаны в конец файла. Если false (а по умолчанию это значение и есть false), старые данные будут стерты, а новые записаны.
Коротко разница по параметру append:
| Значение append | Поведение |
|---|---|
true | новые данные дозаписываются в конец файла |
false (по умолчанию) | старые данные стираются, файл перезаписывается |
public class Main {
public static void main(String[] args) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\test.txt", true);
String greetings = "Привет! Добро пожаловать на JavaRush - лучший сайт для тех, кто хочет стать программистом!\r\n";
fileOutputStream.write(greetings.getBytes());
fileOutputStream.close();
}
}
Результат в файле:
Привет! Добро пожаловать на JavaRush - лучший сайт для тех, кто хочет стать программистом!
Привет! Добро пожаловать на JavaRush - лучший сайт для тех, кто хочет стать программистом!
Привет! Добро пожаловать на JavaRush - лучший сайт для тех, кто хочет стать программистом!
Другое дело!
Не забывай об этой особенности при использовании классов ввода-вывода. В свое время и мне приходилось часами сидеть над задачами, чтобы понять, куда деваются из файлов мои старые данные :)
Ну и конечно, как и в случае с другими классами I/O, не забываем об освобождении ресурсов через метод close().
Класс FileInputStream
У классаFileInputStream назначение противоположное, чтение байтов из файла. Так же как FileOutputStream наследует OutputStream, этот класс происходит от абстрактного класса InputStream.
Запишем в наш текстовый «test.txt» несколько строк текста:
«So close no matter how far
Couldn't be much more from the heart
Forever trusting who we are
And nothing else matters»
Вот как будет выглядеть реализация чтения данных из файла при помощи FileInputStream:
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\test.txt");
int i;
while((i=fileInputStream.read())!= -1){
System.out.print((char)i);
}
}
}
Мы считываем из файла по одному байту, преобразуем считанные байты в символы и выводим их в консоль.
А вот и результат в консоли:
«So close no matter how far
Couldn't be much more from the heart
Forever trusting who we are
And nothing else matters»
Класс BufferedInputStream
Думаю, учитывая знания из прошлых лекций, ты легко сможешь сказать, зачем нужен классBufferedInputStream и какие преимущества у него есть по сравнению с FileInputStream :)
Мы уже встречались с буферизированными потоками, поэтому попробуй предположить (или вспомнить), прежде чем продолжить чтение :)
Буферизированные потоки нужны прежде всего для оптимизации ввода-вывода.
Обращение к источнику данных, например, чтение из файла, дорогостоящая в плане производительности операция. И каждый раз обращаться к файлу для чтения по одному байту расточительно.
Поэтому BufferedInputStream считывает данные не по одному байту, а блоками и временно хранит их в специальном буфере. Это позволяет нам оптимизировать работу программы за счет того, что мы уменьшаем количество обращений к файлу.
Давай посмотрим, как это выглядит:
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\test.txt");
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream, 200);
int i;
while((i = bufferedInputStream.read())!= -1){
System.out.print((char)i);
}
}
}
Здесь мы создали объект BufferedInputStream. Он принимает на вход объект InputStream или любого его наследника, так что предыдущий FileInputStream подойдет.
В качестве дополнительного параметра он принимает размер буфера в байтах. Теперь благодаря этому данные будут считываться из файла не по одному байту, а по 200! Представь, насколько мы сократили количество обращений к файлу.
Для сравнения производительности ты можешь взять какой-нибудь большой текстовый файл размером несколько мегабайт и сравнить, сколько займет его чтение и вывод в консоль в миллисекундах с использованием FileInputStream и BufferedInputStream.
Вот оба варианта кода для примера:
public class Main {
public static void main(String[] args) throws IOException {
Date date = new Date();
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\textBook.rtf");
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
int i;
while((i = bufferedInputStream.read())!= -1){
System.out.print((char)i);
}
Date date1 = new Date();
System.out.println((date1.getTime() - date.getTime()));
}
}
public class Main {
public static void main(String[] args) throws IOException {
Date date = new Date();
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\26951280.rtf");
int i;
while((i = fileInputStream.read())!= -1){
System.out.print((char)i);
}
Date date1 = new Date();
System.out.println((date1.getTime() - date.getTime()));
}
}
При чтении файла размером 1,5 Мб на моем компьютере FileInputStream выполнил работу за ~3500 миллисекунд, а вот BufferedInputStream за ~1700 миллисекунд. Как видишь, буферизированный поток оптимизировал работу программы в 2 раза! :)
Мы еще продолжим изучать классы ввода-вывода. Больше о байтовых потоках можно почитать в документации Oracle (Byte Streams).
Читайте также
- Абстрактные классы и интерфейсы: разбор
InputStreamиOutputStream, от которых наследуются эти классы. - Потоки ввода-вывода и строки: базовое устройство потоков в Java.
- System.out.println: как устроен стандартный поток вывода
out.
Вопросы и ответы
Для чего нужен FileOutputStream?
Для записи байтов в файл. В конструктор передают путь к файлу или объектFile; если файла нет, он создается автоматически. Метод write() записывает байты, close() освобождает ресурс.
Как дописать данные в файл, а не перезаписать?
Передать в конструкторFileOutputStream второй параметр append = true. Тогда новые данные добавятся в конец файла. По умолчанию (false) файл перезаписывается.
Для чего нужен FileInputStream?
Для чтения байтов из файла. Методread() возвращает очередной байт, а значение -1 означает конец файла.
Чем BufferedInputStream лучше FileInputStream?
Он читает данные не по одному байту, а блоками, складывая их в буфер. Это уменьшает число обращений к файлу и ускоряет чтение (в примере статьи примерно в 2 раза на файле 1,5 Мб).Как создать BufferedInputStream?
Обернуть им объектInputStream, например FileInputStream: new BufferedInputStream(fileInputStream). Вторым параметром можно задать размер буфера в байтах.
Нужно ли закрывать поток?
Да. После работы поток закрывают методомclose(), чтобы освободить системные ресурсы.
