Тема 25. Работа с файлами - BelyiZ/JavaCourses GitHub Wiki
- Потоки ввода и вывода
- Try-with-resources
- Чтение файла
- Запись в файл
- Создание и удаление файлов
- Временные файлы
- Список литературы/курсов
Отличительной чертой многих языков программирования является работа с файлами и потоками.
В Java
основной функционал работы с потоками сосредоточен в классах из пакета java.io
.
Ключевым понятием здесь является понятие потока.
Хотя понятие "поток" в программировании довольно перегружено и может обозначать множество различных концепций.
В данном случае применительно к работе с файлами и вводом-выводом мы будем говорить о потоке (stream
), как об абстракции, которая используется для чтения или записи информации (файлов, сокетов, текста консоли и т.д.).
Поток связан с реальным физическим устройством с помощью системы ввода-вывода Java
.
У нас может быть определен поток, который связан с файлом и через который мы можем вести чтение или запись файла.
Это также может быть поток, связанный с сетевым сокетом, с помощью которого можно получить или отправить данные в сети.
Все эти задачи: чтение и запись различных файлов, обмен информацией по сети, ввод-ввывод в консоли мы будем решать в Java
с помощью потоков.
Объект, из которого можно считать данные, называется потоком ввода, а объект, в который можно записывать данные, - потоком вывода. Например, если надо считать содержание файла, то применяется поток ввода, а если надо записать в файл - то поток вывода.
В основе всех классов, управляющих потоками байтов, находятся два абстрактных класса: InputStream
(представляющий потоки ввода) и OutputStream
(представляющий потоки вывода)
Но поскольку работать с байтами не очень удобно, то для работы с потоками символов были добавлены абстрактные классы Reader
(для чтения потоков символов) и Writer
(для записи потоков символов).
Все остальные классы, работающие с потоками, являются наследниками этих абстрактных классов.
Пакет java.io
содержит почти каждый класс, который может потребоваться для совершения ввода и вывода в Java
.
Все данные потоки представлены потоком ввода и адресом вывода.
Поток в пакете java.io
осуществляет поддержку различных данных, таких как примитивы, объекты, локализованные символы и т.д.
Потоки в Java
определяются в качестве последовательности данных.
Существует два типа потоков:
InPutStream
– поток ввода используется для считывания данных с источника.
OutPutStream
– поток вывода используется для записи данных по месту назначения.
Java
предоставляет сильную, но гибкую поддержку в отношении ввода/вывода, связанных с файлами и сетями, однако в данном руководстве рассмотрены лишь базовые функции, связанные с потоками и вводом/выводом.
Рассмотрим далее по порядку наиболее распространенные примеры.
Байтовый поток
Потоки байтов в Java используются для осуществления ввода и вывода 8-битных байтов.
Не смотря на множество классов, связанных с потоками байтов, наиболее распространено использование следующих классов: FileInputStream
и FileOutputStream
.
Ниже рассмотрен пример, иллюстрирующий использование данных двух классов для копирования из одного файла в другой.
Примечание по примеру: чтобы скопировать файл, необходимо в папке проекта создать файл file.txt
с любым или пустым содержимым.
import java.io.*;
public class FileCopy {
public static void main(String args[]) throws IOException {
FileInputStream fileIn = null;
FileOutputStream fileOut = null;
try {
fileIn = new FileInputStream("file.txt");
fileOut = new FileOutputStream("copied_file.txt");
int a;
// Копирование содержимого файла file.txt
while ((a = fileIn.read()) != -1) {
fileOut.write(a); // Чтение содержимого файла file.txt и запись в файл copied_file.txt
}
}finally {
if (fileIn != null) {
fileIn.close();
}
if (fileOut != null) {
fileOut.close();
}
}
}
}
Теперь рассмотрим файл file.txt
со следующим содержимым:
Содержимое файла file.txt
В качестве следующего шага необходимо скомпилировать java
-программу и выполнить ее, что позволит создать файл copied_file.txt
с тем же содержимым, что имеется в file.txt
.
Таким образом, разместим обозначенный код в файле FileCopy.java
и выполним следующее действие:
$javac FileCopy.java
$java FileCopy
Символьные потоки
Потоки байтов в Java
позволяют произвести ввод и вывод 8-битных байтов, в то время как потоки символов используются для ввода и вывода 16-битного юникода. Не смотря на множество классов, связанных с потоками символов, наиболее распространено использование следующих классов: FileReader и FileWriter. Не смотря на тот факт, что внутренний FileReader использует FileInputStream, и FileWriter использует FileOutputStream, основное различие состоит в том, что FileReader производит считывание двух байтов в конкретный момент времени, в то время как FileWriter
производит запись двух байтов за то же время.
Мы можем переформулировать представленный выше пример, в котором два данных класса используются для копирования файла ввода (с символами юникода) в файл вывода.
Примечание по примеру: чтобы скопировать файл, необходимо в папке проекта создать файл file.txt
с любым или пустым содержимым.
import java.io.*;
public class FileCopy {
public static void main(String args[]) throws IOException {
FileReader fileIn = null;
FileWriter fileOut = null;
try {
fileIn = new FileReader("file.txt");
fileOut = new FileWriter("copied_file.txt");
int a;
while ((a = fileIn.read()) != -1) {
fileOut.write(a);
}
} finally {
if (fileIn != null) {
fileIn.close();
}
if (fileOut != null) {
fileOut.close();
}
}
}
}
Теперь рассмотрим файл file.txt
со следующим содержимым:
Содержимое файла file.txt
В качестве следующего шага необходимо скомпилировать программу и выполнить ее, что позволит создать файл copied_file.txt
с тем же содержимым, что имеется в file.txt
.
Таким образом, разместим обозначенный код в файле FileCopy.java
и выполним следующее действие:
$javac FileCopy.java
$java FileCopy
Стандартные потоки
Все языки программирования обеспечивают поддержку стандартного ввода/вывода, где программа пользователя может произвести ввод посредством клавиатуры и осуществить вывод на экран компьютера.
Java
предоставляет следующие три стандартных потока:
- Стандартный ввод – используется для перевода данных в программу пользователя, клавиатура обычно используется в качестве стандартного потока ввода, представленного в виде
System.in
. - Стандартный вывод – производится для вывода данных, полученных в программе пользователя, и обычно экран компьютера используется в качестве стандартного потока вывода, представленного в виде
System.out
. - Стандартная ошибка – используется для вывода данных об ошибке, полученной в программе пользователя, чаще всего экран компьютера служит в качестве стандартного потока сообщений об ошибках, представленного в виде
System.err
.
Ниже представлена простая программа, которая создает InputStreamReader
для чтения стандартного потока ввода, до введения пользователем "q":
Пример
import java.io.*;
public class ReadConsole {
public static void main(String args[]) throws IOException {
InputStreamReader inStRe = null;
try {
inStRe = new InputStreamReader(System.in);
System.out.println("Введите символы, символ 'q' для выхода.");
char a;
do {
a = (char) inStRe.read();
System.out.print(a);
} while(a != 'q');
}finally {
if (inStRe != null) {
inStRe.close();
}
}
}
}
Разместим представленный выше код в файле ReadConsole.java
и попробуем скомпилировать и выполнить его согласно тому, как это представлено в следующей программе.
Данная программа продолжает чтение и вывод одного и того же символа до нажатия 'q':
$javac ReadConsole.java
$java ReadConsole
Введите символы, 'q' для выхода.
proglang.su
proglang.su
q
q
Иногда в процессе работы Java-программа взаимодействует с объектами вне Java-машины. Например, с файлами на диске. Такие объекты принято называть внешними ресурсами. Внутренние ресурсы — это объекты, созданные внутри Java-машины.
Обычно взаимодействие происходит по такой схеме:
Учет ресурсов
Операционная система ведет строгий учет доступных ресурсов, а также контролирует совместный доступ разных программ к ним. Например, если одна программа меняет какой-то файл, другая программа не может изменить (или удалить) этот файл. Это касается не только файлов, но на их примере понятнее всего.
У операционной системы есть функции (API), которые позволяют программе захватить какой-либо ресурс и/или освободить его. Если ресурс занят, с ним может работать только та программа, которая его захватила. Если ресурс свободен, любая программа может захватить его.
Представьте, что у вас в офисе есть общие кружки. Если кто-то взял кружку, другой уже не может взять ее. Но если ей попользовались, помыли и поставили на место, ее снова может брать кто угодно. Ну или места в метро или в маршрутке. Если место свободно — любой может его занять. Если место занято — им распоряжается тот, кто занял.
Захват внешних ресурсов
Каждый раз, когда ваша Java-программа начинает работать с каким-то файлом на диске, Java-машина запрашивает у операционной системы монопольный доступ к нему. Если ресурс свободен, его захватывает Java-машина.
Но после того, как вы закончили работать с файлом, этот ресурс (файл) нужно освободить: уведомить операционную систему, что он вам больше не нужен. Если вы этого не сделаете, ресурс будет продолжать числиться за вашей программой.
Для каждой запущенной программы операционная система ведет список занятых ресурсов. Если ваша программа превысит разрешенный ей лимит ресурсов, новые ресурсы операционная система вам уже не даст.
Хорошая новость в том, что если ваша программа завершилась, все ресурсы автоматически освобождаются (это делает сама операционная система).
Плохая же новость в том, что если вы пишете серверное приложение (а очень много серверных приложений пишутся на Java), ваш сервер должен работать днями, неделями, месяцами без остановки. И если вы в день открываете 100 файлов и не закрываете их, через пару недель ваше приложение исчерпает свой лимит и упадет. Не очень-то похоже на месяцы стабильной работы.
try-with-resources - это оператор, который создан для того, чтобы решать проблему с обязательным вызовом метода close(). В общем случае выглядит он довольно просто:
try (Класс имя = new Класс())
{
Код, который работает с переменной имя
}
Это еще одна разновидность оператора try
.
После ключевого слова try нужно добавить круглые скобки, а внутри них — создать объекты с внешними ресурсами.
Для объекта, указанного в круглых скобках, компилятор сам добавит секцию finally
и вызов метода close()
.
Два эквивалентных примера:
FileOutputStream output = null;
try
{
output = new FileOutputStream(path);
output.write(1);
}
finally
{
if (output != null)
output.close();
}
try(FileOutputStream output = new FileOutputStream(path))
{
output.write(1);
}
Код с использованием try-with-resources значительно короче и легче читается. А чем меньше кода, тем меньше шансов сделать опечатку или ошибку.
Кстати, у оператора try-with-resources можно дописывать блоки catch
и finally
.
А можно и не добавлять, если в них нет необходимости.
#Чтение файла
Абстрактный класс Reader
предоставляет функционал для чтения текстовой информации.
Рассмотрим его основные методы:
-
absract void close(): закрывает поток ввода
-
int read(): возвращает целочисленное представление следующего символа в потоке. Если таких символов нет, и достигнут конец файла, то возвращается число -1
-
int read(char[] buffer): считывает в массив buffer из потока символы, количество которых равно длине массива buffer. Возвращает количество успешно считанных символов. При достижении конца файла возвращает -1
-
int read(CharBuffer buffer): считывает в объект CharBuffer из потока символы. Возвращает количество успешно считанных символов. При достижении конца файла возвращает -1
-
absract int read(char[] buffer, int offset, int count): считывает в массив buffer, начиная со смещения offset, из потока символы, количество которых равно count
-
long skip(long count): пропускает количество символов, равное count. Возвращает число успешно пропущенных символов
Чтение файлов и класс FileInputStream
Для считывания данных из файла предназначен класс FileInputStream
, который является наследником класса InputStream
и поэтому реализует все его методы.
Для создания объекта FileInputStream
мы можем использовать ряд конструкторов. Наиболее используемая версия конструктора в качестве параметра принимает путь к считываемому файлу:
FileInputStream(String fileName) throws FileNotFoundException
Если файл не может быть открыт, например, по указанному пути такого файла не существует, то генерируется исключение FileNotFoundException
.
Считаем данные из ранее записанного файла и выведем на консоль:
import java.io.*;
public class Program {
public static void main(String[] args) {
try(FileInputStream fin=new FileInputStream("C://SomeDir//notes.txt"))
{
System.out.printf("File size: %d bytes \n", fin.available());
int i=-1;
while((i=fin.read())!=-1){
System.out.print((char)i);
}
}
catch(IOException ex){
System.out.println(ex.getMessage());
}
}
}
В данном случае мы считываем каждый отдельный байт в переменную i:
while((i=fin.read())!=-1){
Когда в потоке больше нет данных для чтения, метод возвращает число -1.
Затем каждый считанный байт конвертируется в объект типа char и выводится на консоль.
Подобным образом можно считать данные в массив байтов и затем производить с ним манипуляции:
byte[] buffer = new byte[fin.available()];
// считаем файл в буфер
fin.read(buffer, 0, fin.available());
System.out.println("File data:");
for(int i=0; i<buffer.length;i++){
System.out.print((char)buffer[i]);
}
Совместим оба класса и выполним чтение из одного и запись в другой файл:
import java.io.*;
public class Program {
public static void main(String[] args) {
try(FileInputStream fin=new FileInputStream("C://SomeDir//notes.txt");
FileOutputStream fos=new FileOutputStream("C://SomeDir//notes_new.txt"))
{
byte[] buffer = new byte[fin.available()];
// считываем буфер
fin.read(buffer, 0, buffer.length);
// записываем из буфера в файл
fos.write(buffer, 0, buffer.length);
}
catch(IOException ex){
System.out.println(ex.getMessage());
}
}
}
Класс Writer определяет функционал для всех символьных потоков вывода. Его основные методы:
-
Writer append(char c): добавляет в конец выходного потока символ c. Возвращает объект Writer
-
Writer append(CharSequence chars): добавляет в конец выходного потока набор символов chars. Возвращает объект Writer
-
abstract void close(): закрывает поток
-
abstract void flush(): очищает буферы потока
-
void write(int c): записывает в поток один символ, который имеет целочисленное представление
-
void write(char[] buffer): записывает в поток массив символов
vabsract void write(char[] buffer, int off, int len) : записывает в поток только несколько символов из массива buffer. Причем количество символов равно len, а отбор символов из массива начинается с индекса off
-
void write(String str): записывает в поток строку
-
void write(String str, int off, int len): записывает в поток из строки некоторое количество символов, которое равно len, причем отбор символов из строки начинается с индекса off
-
Класс FileOutputStream предназначен для записи байтов в файл. Он является производным от класса OutputStream, поэтому наследует всю его функциональность.
Через конструктор класса FileOutputStream задается файл, в который производится запись. Класс поддерживает несколько конструкторов:
FileOutputStream(String filePath)
FileOutputStream(File fileObj)
FileOutputStream(String filePath, boolean append)
FileOutputStream(File fileObj, boolean append)
Файл задается либо через строковый путь, либо через объект File. Второй параметр - append задает способ записи: eсли он равен true, то данные дозаписываются в конец файла, а при false - файл полностью перезаписывается
Например, запишем в файл строку:
import java.io.*;
public class Program {
public static void main(String[] args) {
String text = "Hello world!"; // строка для запис
try(FileOutputStream fos=new FileOutputStream("C://SomeDir//notes.txt"))
{
// перевод строки в байты
byte[] buffer = text.getBytes();
fos.write(buffer, 0, buffer.length);
}
catch(IOException ex){
System.out.println(ex.getMessage());
}
System.out.println("The file has been written");
}
}
Для создания объекта FileOutputStream
используется конструктор, принимающий в качестве параметра путь к файлу для записи.
Если такого файла нет, то он автоматически создается при записи.
Так как здесь записываем строку, то ее надо сначала перевести в массив байтов.
И с помощью метода write строка записывается в файл.
Для автоматического закрытия файла и освобождения ресурса объект FileOutputStream
создается с помощью конструктции try...catch
.
При этом необязательно записывать весь массив байтов. Используя перегрузку метода write()
, можно записать и одиночный байт:
fos.write(buffer[0]); // запись первого байта
- Создайте файл с классом
java.io.File
Вам нужно использовать метод File.createNewFile()
.
Этот метод возвращает логическое значение:
- true, если файл выполнен.
- false, если файл уже существует или операция по какой-то причине не открывается.
Этот метод также генерирует исключение
java.io.IOException
, когда он не может создать файл.
Когда мы создаем объект File
, передавая имя файла, он может быть с абсолютным путем, или мы можем предоставить только имя файла, или мы можем предоставить относительный путь.
Для неабсолютного пути объект File пытается найти файлы в корневом каталоге проекта.
Если мы запустим программу из командной строки, для неабсолютного пути объект File попытается найти файлы из текущего каталога. Экземпляры класса File являются неизменяемыми; то есть, после создания абстрактный путь, представленный объектом File, никогда не изменится.
Теперь давайте рассмотрим небольшой пример и разберемся, как он работает.
File file = new File("c://temp//testFile1.txt");
//create the file.
if (file.createNewFile()){
System.out.println("File is created!");
}
else{
System.out.println("File already exists.");
}
//write content
FileWriter writer = new FileWriter (file);
writer.write("Test data");
writer.close();
Пожалуйста, обратите внимание, что этот метод будет только создавать файл, но не записывать в него никакого содержимого. Теперь давайте двигаться дальше и рассмотрим следующий метод.
- Создайте файл с классом
java.io.FileOutputStream
Если вы хотите создать новый файл и в то же время, если хотите записать в него некоторые данные, вы можете использовать метод записи FileOutputStream
.
В Java FileOutputStream
является классом потока байтов.
Чтобы записать данные в файл, вы должны преобразовать данные в байты, а затем сохранить их в файл.
Например:
String data = "Test data";
FileOutputStream out = new FileOutputStream("c://temp//testFile2.txt");
out.write(data.getBytes());
<span>out.close();
Класс FileOutputStream
хранит данные в виде отдельных байтов.
Может использоваться для создания текстовых файлов.
Файл представляет собой хранилище данных на втором носителе, таком как жесткий диск или компакт-диск.
Метод FileOutputStream.write()
автоматически создает новый файл и записывает в него содержимое.
- Создайте файл с помощью
Java.nio.file.Files
– Java NIO
Files.write() – лучший способ создать файл, и он должен быть вашим предпочтительным подходом в будущем, если вы его еще не используете. Это хороший вариант, потому что нам не нужно беспокоиться о закрытии ресурсов ввода-вывода. Каждая строка представляет собой последовательность символов и записывается в файл последовательно, каждая строка заканчивается разделителем строк платформы.
Метод:
public static Path createFile(Path path, FileAttribute<?>... attrs) throws IOException
Создает новый и пустой файл, и если файл уже существует, то будет ошибка.
Параметры:
путь – путь для создания файла.
attrs – необязательный список атрибутов файла, устанавливаемых атомарно при создании.
Например:
String data = "Test data";
Files.write(Paths.get("c://temp//testFile3.txt");
data.getBytes());
//or
List<String> lines = Arrays.asList("1st line", "2nd line");
Files.write(Paths.get("file6.txt");
lines,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
Далее, давайте посмотрим на создание временного файла.
Java также может создавать временные файлы
Создание временного файла в java может потребоваться во многих сценариях, но в основном это происходит во время модульных тестов, где вы не хотите сохранять результаты. Как только тестовый пример закончен, вас не волнует содержимое файла.
Создание временного файла с использованием java.io.File.createTempFile()
Public class TemporaryFileExample{
Public static void main(string[] args){
try{
final path path = Files.createTempFile("myTempFile",".txt");
System.out.println("Temp file : " + path);
// delete file on exist.
path.toFile().deleteonExit();
} catch (IOException e){
e.printStackTrace();
}
}
}
с использованием NIO
Public class TemporaryFileExample{
Public static void main(string[] args){
File temp;
try{
temp = File.createTempFile("myTempFile" , ".txt");
System.out.println("Temp file created : " +
temp.getAbsolutePath());
} catch (IOException e){
e.printStackTrace();
}
}
}
Для создания временного файла используются следующие два метода.
-
createTempFile(Path, String, String, FileAttribute<?>… attrs)
– создает файл tmp в указанном каталоге.
Вышеуказанный метод принимает четыре аргумента.
Путь -> указать каталог, в котором будет создан файл.
Строка -> чтобы упомянуть префикс имени файла. Используйте ноль, чтобы избежать префикса.
Строка -> чтобы упомянуть суффикс имени файла. т.е. расширение файла. Используйте null, чтобы использовать .tmp в качестве расширения.
attrs -> Это необязательно, чтобы упоминать список атрибутов файла, чтобы установить атомарно при создании файла
Например. Files.createTempFile(path,null, null); – создает временный файл с расширением .tmp по указанному пути
-
createTempFile(String, String, FileAttribute<?>)
– создает временный файл во временном каталоге по умолчанию системы / сервера.
Например: Files.createTempFile (null, null) – создает временный файл во временной папке по умолчанию в системе. В Windows временная папка может быть C: UsersusernameAppDataLocalTemp, где username – ваш идентификатор входа в Windows.
Тема 24. Исключения | Оглавление | Тема 26. Работа с консолью и логами