Работа с файлами в Java

🔥 🚀 Важно для всех, кто работает с Java! 🔥
На JavaRocks ты найдешь уникальные туториалы, практические задачи и редкие книги, которых не найти в свободном доступе. Присоединяйся к нашему Telegram-каналу JavaRocks — стань частью профессионального сообщества!

В этой статье разберемся в работе с файлами и директориями и сосредоточимся именно на управлении файлами: создании, переименовании и т.д. До появления Java 7 все эти операции выполнялись с помощью класса File.

Но начиная с Java 7 создатели языка решили изменить подход к работе с файлами и директориями. Это произошло потому, что у класс File имелось несколько недостатков. Например, в нем не было метода copy(), который позволял бы копировать файл из одного места в другое (казалось бы, базовая способность).

Кроме того, в классе File было довольно много методов, возвращающих boolean. При ошибке такой метод просто возвращал false вместо того, чтобы выбросить исключение. Из-за этого выявлять ошибки и понимать их причины было крайне неудобно.

Вместо единственного класса File появилось сразу три: Paths, Path и Files. Если быть точным, то Path — это интерфейс, а не класс.

Разберемся, чем они отличаются друг от друга и зачем нужен каждый из них.

Начнем с самого простого — Paths.

Paths

Paths — это очень простой класс с единственным статическим методом: get(). Он создан исключительно для того, чтобы получить объект Path из переданной строки или URI.

Других функций у него нет.

Вот как это работает:

import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {

   public static void main(String[] args) {

       Path testFilePath = Paths.get("C:\\Users\\Username\\Desktop\\testFile.txt");
   }
}
Run Code

Ничего сложного, правда? 🙂

Теперь появился тип Path. Разберемся, что это и зачем он нужен 🙂

Path

Path, по большому счету, является переработанным аналогом класса File. Работать с ним гораздо проще, чем с File.

Во-первых, многие вспомогательные (статические) методы вынесли в отдельный класс — Files.

Во-вторых, навели порядок с возвращаемыми значениями методов интерфейса Path. В классе File методы могли вернуть что угодно: String, boolean или объект File.

Разобраться в этом было нелегко. Например, был метод getParent(), который возвращал строку, представляющую путь к родительскому каталогу текущего файла. Но был также метод getParentFile(), который возвращал то же самое, но в виде объекта File. Это явно лишнее.

В интерфейсе Path таких дублирований нет: метод getParent() и другие методы для работы с файлами просто возвращают объект Path. Никакой путаницы — все легко и просто.

Читайте также: 11 полезных фишек для Java

Какими полезными методами обладает Path?

Вот некоторые из них и примеры того, как они работают:

  • getFileName() — возвращает имя файла из пути;
  • getParent()— возвращает “родительский” каталог текущего пути (другими словами, каталог, расположенный уровнем выше в дереве каталогов);
  • getRoot()— возвращает “корневой” каталог, т.е. самый верхний каталог в дереве каталогов;
  • startsWith(), endsWith()— проверяют, начинается/заканчивается ли путь переданным путем:
import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {

   public static void main(String[] args) {

       Path testFilePath = Paths.get("C:\\Users\\Username\\Desktop\\testFile.txt");

       Path fileName = testFilePath.getFileName();
       System.out.println(fileName);

       Path parent = testFilePath.getParent();
       System.out.println(parent);

       Path root = testFilePath.getRoot();
       System.out.println(root);

       boolean endWithTxt = testFilePath.endsWith("Desktop\\testFile.txt");
       System.out.println(endWithTxt);

       boolean startsWithLalala = testFilePath.startsWith("lalalala");
       System.out.println(startsWithLalala);
   }
}
Run Code

Вывод в консоли:

testFile.txt
C:\Users\Username\Desktop
C:\
true
false

Обратите внимание на то, как работает метод endsWith(). Он проверяет, действительно ли путь заканчивается указанным путем. То есть сравнение идет по структуре пути, а не по простой подстроке.

Сравните эти два вызова:

import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {

   public static void main(String[] args) {

       Path testFilePath = Paths.get("C:\\Users\\Username\\Desktop\\testFile.txt");

       System.out.println(testFilePath.endsWith("estFile.txt"));
       System.out.println(testFilePath.endsWith("Desktop\\testFile.txt"));
   }
}
Run Code

Вывод в консоли:

false
true

Методу endsWith() должен быть передан именно путь, а не просто набор символов: в противном случае результат всегда будет false, даже если строка действительно совпадает по окончанию пути (как в примере с "estFile.txt").

Кроме того, в Path есть группа методов, упрощающих работу с абсолютными и относительными путями.

Рассмотрим эти методы:

  • boolean isAbsolute() возвращает true, если текущий путь является абсолютным:
import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {

   public static void main(String[] args) {

       Path testFilePath = Paths.get("C:\\Users\\Username\\Desktop\\testFile.txt");

       System.out.println(testFilePath.isAbsolute());
   }
}
Run Code

Вывод в консоли:

true
  • Path normalize() “нормализует” текущий путь, удаляя из него ненужные элементы. Возможно, вы знаете, что в популярных операционных системах часто встречаются обозначения . (текущая директория) и .. (родительская директория). Например, “./Pictures/dog.jpg” означает, что в текущем каталоге есть папка “Pictures”, которая, в свою очередь, содержит файл “dog.jpg”.

Если в программе встречается путь с использованием . или .., метод normalize() удалит их и вернет упрощенный путь:

import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {

   public static void main(String[] args) {


       Path path5 = Paths.get("C:\\Users\\Java\\.\\examples");

       System.out.println(path5.normalize());

       Path path6 = Paths.get("C:\\Users\\Java\\..\\examples");
       System.out.println(path6.normalize());
   }
}
Run Code

Вывод в консоли:

C:\Users\Java\examples
C:\Users\examples
  • Path relativize(): строит относительный путь от текущего до переданного.

Например:

import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {

   public static void main(String[] args) {

       Path testFilePath1 = Paths.get("C:\\Users\\Users\\Users\\Users");
       Path testFilePath2 = Paths.get("C:\\Users\\Users\\Users\\Users\\Username\\Desktop\\testFile.txt");

       System.out.println(testFilePath1.relativize(testFilePath2));
   }
}
Run Code

Вывод в консоли:

Username\Desktop\testFile.txt

Полный список методов Path довольно длинный. Вы можете найти их все в документации Oracle.

Теперь перейдем к Files.

Files

Files — это утилитный класс, куда вынесли статические методы из класса File. Files можно сравнить с Arrays или Collections. Разница в том, что работает он с файлами, а не с массивами или коллекциями 🙂

Его основное назначение — управление файлами и каталогами. Используя статические методы класса Files, можно создавать, удалять и перемещать файлы и каталоги.

Эти операции выполняются с помощью методов createFile() (для каталогов — createDirectory()), move() и delete().

Вот как их использовать:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

public class Main {

   public static void main(String[] args) throws IOException {

       // Create a file
       Path testFile1 = Files.createFile(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt"));
       System.out.println("Was the file created successfully?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt")));

       // Create a directory
       Path testDirectory = Files.createDirectory(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory"));
       System.out.println("Was the directory created successfully?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory")));

       // Move the file from the desktop to the testDirectory directory. When you move a folder, you need to indicate its name in the folder!
       testFile1 = Files.move(testFile1, Paths.get("C:\\Users\\Username\\Desktop\\testDirectory\\testFile111.txt"), REPLACE_EXISTING);

       System.out.println("Did our file remain on the desktop?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt")));

       System.out.println("Has our file been moved to testDirectory?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory\\testFile111.txt")));

       // Delete a file
       Files.delete(testFile1);
       System.out.println("Does the file still exist?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory\\testFile111.txt")));
   }
}
Run Code

Здесь сначала создается файл (метод Files.createFile()) на рабочем столе. Затем в том же месте создается папка (метод Files.createDirectory()). После этого перемещаем файл (метод Files.move()) с рабочего стола в эту новую папку и, наконец, удаляем файл (метод Files.delete()).

Консольный вывод:

Was the file created successfully?
true
Was the directory created successfully?
true
Did our file remain on the desktop?
false
Has our file been moved to testDirectory?
true
Does the file still exist?
false

Обратите внимание: как и методы интерфейса Path, многие методы класса Files возвращают объект Path.

Большинство методов класса Files также принимают в качестве входных данных объекты Path. Здесь метод Paths.get() будет вашим верным помощником — используйте его с пользой.

Что еще интересного есть в Files?
Чего действительно не хватало старому классу File, так это метода copy()! Мы упоминали об этом в начале статьи, а теперь пришло время с ним познакомиться.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

public class Main {

   public static void main(String[] args) throws IOException {

       // Create a file
       Path testFile1 = Files.createFile(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt"));
       System.out.println("Was the file created successfully?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt")));

       // Create a directory
       Path testDirectory2 = Files.createDirectory(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory2"));
       System.out.println("Was the directory created successfully?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory2")));

       // Copy the file from the desktop to the testDirectory2 directory.
       testFile1 = Files.copy(testFile1, Paths.get("C:\\Users\\Username\\Desktop\\testDirectory2\\testFile111.txt"), REPLACE_EXISTING);

       System.out.println("Did our file remain on the desktop?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt")));

       System.out.println("Was our file copied to testDirectory?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory2\\testFile111.txt")));
   }
}
Run Code

Консольный вывод:

Was the file created successfully?
true
Was the directory created successfully?
true
Did our file remain on the desktop?
true
Was our file copied to testDirectory?
true

Теперь вы знаете, как копировать файлы программно! 🙂

Конечно, класс Files позволяет не только управлять самим файлом, но и работать с его содержимым.

В нем есть метод write() для записи данных в файл и целых три метода для чтения данных: read(), readAllBytes() и readAllLines().

Подробно остановимся на последнем. Почему именно на нем?

Потому что его возвращаемый тип очень интересен: List<String>. То есть метод возвращает список строк файла. Это делает работу с содержимым невероятно удобной: можно, например, вывести весь файл построчно в консоль при помощи обычного цикла for.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

import static java.nio.charset.StandardCharsets.UTF_8;

public class Main {

   public static void main(String[] args) throws IOException {

       List<String> lines = Files.readAllLines(Paths.get("C:\\Users\\Username\\Desktop\\pushkin.txt"), UTF_8);

       for (String s: lines) {
           System.out.println(s);
       }
   }
}
Run Code

Консольный вывод:

I still recall the wondrous moment:
When you appeared before my sight,
As though a brief and fleeting omen,
Pure phantom in enchanting light.

Очень удобно! 🙂

Эта возможность появилась в Java 7.

Stream API появился в Java 8. Он добавил в язык элементы функционального программирования, включая более продвинутые возможности работы с файлами.

Представьте такую задачу: надо найти все строки, начинающиеся со слова “As”, преобразовать их в верхний регистр и вывести в консоль.

Как бы это выглядело в Java 7 с использованием класса Files?

Примерно вот так:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import static java.nio.charset.StandardCharsets.UTF_8;

public class Main {

   public static void main(String[] args) throws IOException {

       List<String> lines = Files.readAllLines(Paths.get("C:\\Users\\Username\\Desktop\\pushkin.txt"), UTF_8);

       List<String> result = new ArrayList<>();

       for (String s: lines) {
           if (s.startsWith("As")) {
               String upper = s.toUpperCase();
               result.add(upper);
           }
       }

       for (String s: result) {
           System.out.println(s);
       }
   }
}
Run Code

Консольный вывод:

AS THOUGH A BRIEF AND FLEETING OMEN,
PURE PHANTOM IN ENCHANTING LIGHT.

Задача выполнена, но не кажется ли вам, что для такой простой задачи код получился слишком громоздким?

В Java 8 с Stream API решение выглядит куда изящнее:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {

   public static void main(String[] args) throws IOException {

       Stream<String> stream = Files.lines(Paths.get("C:\\Users\\Username\\Desktop\\pushkin.txt"));

       List<String> result  = stream
               .filter(line -> line.startsWith("As"))
               .map(String::toUpperCase)
               .collect(Collectors.toList());
       result.forEach(System.out::println);
   }
}
Run Code

Результат тот же, но кода гораздо меньше! Более того, читабельность нисколько не пострадала — даже без знакомства со Stream API легко понять, что делает программа.

Если вкратце, то Stream — это последовательность элементов, над которой можно выполнять различные операции. Мы получаем объект Stream с помощью метода Files.lines(), а затем применяем к нему три функции:

  1. Метод filter() используется, чтобы выбрать из файла только те строки, которые начинаются с “As”.
  2. Мы проходим по всем выделенным строкам с помощью метода map() и преобразуем каждую из них в верхний регистр.
  3. Мы используем метод collect(), чтобы собрать все полученные строки в список (List).

Результат остается тем же:

AS THOUGH A BRIEF AND FLEETING OMEN,
PURE PHANTOM IN ENCHANTING LIGHT.

А теперь вернемся к основам, то есть к файлам 🙂

Последняя возможность, которую мы сегодня рассмотрим, — это перемещение по дереву файлов.

В современных операционных системах файловая структура чаще всего выглядит как дерево: есть корень, от которого идут ветви, которые могут иметь другие ветви и т.д.

Корень и ветви — это директории, или каталоги.

Например, каталог “С://” может быть корневым.

Он включает в себя две ветви: “C://Downloads” и “C://Users”.

Каждая из этих веток имеет в свою очередь еще две ветки: “C://Downloads/Pictures”, “C://Downloads/Video”, “C://Users/JohnSmith”, “C://Users/Pudge2005”.

Эти ветви, в свою очередь, могут иметь другие ветви и т.д., собственно, поэтому такая структура и называется деревом.

В Linux структура аналогична, но в качестве корня — каталог /.

структура Linux

Теперь представьте следующую задачу: начиная с корневого каталога, обойти все его папки и подпапки и найти файлы с определенным содержимым. Например, те, где есть строка: “This is the file we need!”

В качестве корневого каталога возьмем папку “testFolder”, которая находится на рабочем столе.

Вот ее содержимое:

содержимое папки testFolder

Папки level1-a и level1-b содержат вложенные папки:

содержимое папки level1-a
содержимое папки level1-b

Эти “папки второго уровня” не содержат других папок, только отдельные файлы:

содержимое папки level2-b-b
содержимое папки level2-a-a

Три файла с нужным нам содержимым специально названы “говорящими” именами: FileWeNeed1.txt, FileWeNeed2.txt, FileWeNeed3.txt.

Именно эти файлы нам нужно найти с помощью Java. Как это сделать?

На помощь приходит мощный метод для обхода файлового дерева — Files.walkFileTree().

Вот что нам нужно сделать.

Во-первых, нам понадобится FileVisitor.

FileVisitor — это специальный интерфейс, в котором описаны методы для обхода дерева файлов. Именно здесь мы реализуем логику чтения содержимого файлов и проверки, есть ли в них нужный нам текст. Вот как будет выглядеть наш FileVisitor:

import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;

public class MyFileVisitor extends SimpleFileVisitor<Path> {

   @Override
   public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

       List<String> lines = Files.readAllLines(file);
       for (String s: lines) {
           if (s.contains("This is the file we need")) {
               System.out.println("We found a file we need!");
               System.out.println(file.toAbsolutePath());
               break;
           }
       }

       return FileVisitResult.CONTINUE;
   }
}
Run Code

В данном случае наш класс наследуется от SimpleFileVisitor — это готовая реализация FileVisitor, где нам нужно переопределить всего один метод: visitFile(). Здесь мы описываем, что именно делать с каждым файлом в каждой директории.

Если требуется более сложная логика для обхода файловой структуры, можно реализовать свой FileVisitor, переопределив еще три метода:

  • preVisitDirectory()— выполняется перед входом в папку;
  • visitFileFailed()— выполняется, если к файлу нет доступа;
  • postVisitDirectory()— выполняется после выхода из каталога.

В нашем случае все это не нужно, поэтому хватает SimpleFileVisitor.

Логика внутри метода visitFile() предельно проста: читаем все строки файла, проверяем наличие искомой строки и при успехе печатаем абсолютный путь.

Единственная строка, которая может вызвать затруднения:

return FileVisitResult.CONTINUE;

На самом деле, все очень просто. Здесь мы просто описываем, что должна сделать программа после посещения файла и выполнения всех необходимых операций. В нашем случае нужно продолжить обход дерева, поэтому используется значение CONTINUE.

Но задача может быть другой: вместо того чтобы найти все файлы, содержащие надпись “This is the file we need”, найти только один такой файл. После этого программа должна завершиться. В этом случае код будет выглядеть точно так же, но вместо break будет:

return FileVisitResult.TERMINATE;

Запустим программу и проверим, как это работает:

import java.io.IOException;
import java.nio.file.*;

public class Main {

   public static void main(String[] args) throws IOException {

       Files.walkFileTree(Paths.get("C:\\Users\\Username\\Desktop\\testFolder"), new MyFileVisitor());
   }
}
Run Code

Консольный вывод:

We found a file we need!
C:\Users\Username\Desktop\testFolder\FileWeNeed1.txt
We found a file we need!
C:\Users\Username\Desktop\testFolder\level1-a\level2-a-a\FileWeNeed2.txt
We found a file we need!
C:\Users\Username\Desktop\testFolder\level1-b\level2-b-b\FileWeNeed3.txt

Отлично! Программа работает! 🙂

В качестве небольшого упражнения можно заменить SimpleFileVisitor на “чистый” FileVisitor, переопределить все четыре метода и придумать для программы новое назначение. Например, реализовать логирование: выводить имя файла или папки до и после входа в них.
На этом все!

Перевод статьи «Java Files, Path».

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх