JavaRush /Java блог /Random UA /Java Files, Path

Java Files, Path

Стаття з групи Random UA
Вітання! Сьогодні ми поговоримо про роботу з файлуми та каталогами. Ти вже знаєш, як керувати вмістом файлів: у нас було чимало занять, присвячених цьому:) Думаю, ти легко зможеш згадати кілька класів, які потрібні для цих цілей. На сьогоднішній лекції ми поговоримо саме про управління файлуми — про створення, перейменування тощо. До появи Java 7 усі подібні операції проводабося за допомогою класу File. Про його роботу ти можеш прочитати тут . Але в Java 7 творці мови вирішабо змінити роботу з файлуми та каталогами. Це сталося через те, що клас Fileмав низку недоліків. Наприклад, у ньому не було методу copy(), який дозволив би скопіювати файл з одного місця в інше (начебто, явно необхідна функція). Крім того, у класіFileбуло досить багато методів, які повертали booleanзначення. При помилці такий метод повертає false , а чи не викидає виняток, що робить діагностику помилок і встановлення причин дуже непростим справою. Замість єдиного класу Fileз'явабося цілих 3 класи: Paths, Pathі Files. Ну а якщо бути точним, Pathце інтерфейс, а не клас. Давай розберемося, чим вони один від одного відрізняються і навіщо потрібний кожен із них. Почнемо з найлегшого - Paths.

Paths

Paths- Це дуже простий клас з єдиним статичним способом get(). Його створабо виключно для того, щоб з переданого рядка або URI отримати об'єкт типу Path. Іншої функціональності в нього немає. Ось приклад його роботи:
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, давай розбиратися, що це за Pathтакий і навіщо він потрібен :)

Path

Path, за великим рахунком, це перероблений аналог класу File. Працювати з ним значно простіше, ніж File. По-перше , з нього прибрали багато утилітних (статичних) методів, і перенесли їх у клас Files. По-друге , Pathбули впорядковані значення методів, що повертаються. У класі Fileметоди повертали то String, то boolean, то Fileрозібратися було непросто. Наприклад, був метод getParent(), який повертав батьківський шлях для поточного файлу у вигляді рядка. Але при цьому був метод getParentFile(), який повертав те саме, але у вигляді об'єкта File! Це явно надмірно. Тому в інтерфейсі Pathметод getParent()та інші методи роботи з файлуми повертають просто об'єкт.Path. Жодної купи варіантів - все легко і просто. Які ж корисні методи є у 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);
       }
    }

    Виведення в консоль:

    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"));
       }
    }

    Виведення в консоль:

    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());
       }
    }

    Виведення в консоль:

    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());
       }
    }

    Виведення в консоль:

    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));
       }
    }

    Виведення в консоль:

    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 {

       //Створення файлу
       Path testFile1 = Files.createFile(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt"));
       System.out.println("Чи був успішно створений файл?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt")));

       //Створення директорії
       Path testDirectory = Files.createDirectory(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory"));
       System.out.println("Чи була директорія успішно створена?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory")));

       //Переміщаємо файл з робочого столу в директорію testDirectory. Переміщати треба із зазначенням імені файлу в папці!
       testFile1 = Files.move(testFile1, Paths.get("C:\\Users\\Username\\Desktop\\testDirectory\\testFile111.txt"), REPLACE_EXISTING);

       System.out.println("Чи залишився наш файл на робочому столі?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt")));

       System.out.println("Чи був наш файл переміщений до testDirectory?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory\\testFile111.txt")));

       //Видалення файлу
       Files.delete(testFile1);
       System.out.println("Файл все ще існує?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory\\testFile111.txt")));
   }
}
Тут ми спочатку створюємо файл (метод Files.createFile()) на робочому столі, далі створюємо там папку (метод Files.createDirectory()). Після цього ми переміщуємо файл (метод Files.move()) з робочого столу в цю нову папку, а в кінці видаляємо файл (метод Files.delete()). Висновок у консоль: Чи був файл успішно створено? true Чи була директорія успішно створена? true Чи залишився файл на робочому столі? false Чи був наш файл переміщений до testDirectory? true Чи все ще існує файл? 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 {

       //Створення файлу
       Path testFile1 = Files.createFile(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt"));
       System.out.println("Чи був успішно створений файл?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt")));

       //Створення директорії
       Path testDirectory2 = Files.createDirectory(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory2"));
       System.out.println("Чи була директорія успішно створена?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory2")));

       //Копіюємо файл з робочого столу в директорію testDirectory2.
       testFile1 = Files.copy(testFile1, Paths.get("C:\\Users\\Username\\Desktop\\testDirectory2\\testFile111.txt"), REPLACE_EXISTING);

       System.out.println("Чи залишився наш файл на робочому столі?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testFile111.txt")));

       System.out.println("Чи наш файл був скопійований у testDirectory?");
       System.out.println(Files.exists(Paths.get("C:\\Users\\Username\\Desktop\\testDirectory2\\testFile111.txt")));
   }
}
Висновок у консоль: Чи був файл успішно створено? true Чи була директорія успішно створена? true Чи залишився файл на робочому столі? true Чи був файл скопійований у testDirectory? true Тепер ти вмієш копіювати файли програмно! :) Але клас Filesдозволяє не лише керувати самими файлуми, а й працювати з його вмістом. Для запису даних у файл він має метод write(), а читання — цілих 3: 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);
       }
   }
}
Висновок у консоль: Я пам'ятаю чудову мить: Переді мною з'явилася ти, Як швидкоплинне бачення, Як геній чистої краси. Дуже зручно! :) Така можливість з'явилася ще Java 7. У версії Java 8 з'явився Stream API , який додав Java деякі елементи функціонального програмування. У тому числі багатші можливості роботи з файлуми. Уяви, що ми маємо завдання: знайти у файлі всі рядки, які починаються зі слова «Як», привести їх до UPPER CASE і вивести в консоль. Як виглядало б рішення з використанням класу FilesJava 7? Приблизно так:
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("Як")) {
               String upper = s.toUpperCase();
               result.add(upper);
           }
       }

       for (String s: result) {
           System.out.println(s);
       }
   }
}
Висновок в консоль: ЯК МИМОЛІТНЕ БАЧЕННЯ, ЯК ГЕНІЙ ЧИСТОЇ КРАСИ. Ми начебто впоралися, але чи не здається тобі, що для такого простого завдання наш код вийшов трохи...багатослівним? З використанням 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("Як"))
               .map(String::toUpperCase)
               .collect(Collectors.toList());
       result.forEach(System.out::println);
   }
}
Ми досягли того ж результату, але з набагато меншим обсягом коду! Причому не можна сказати, що ми втратабо у «читабельності». Думаю, ти легко зможеш прокоментувати, що робить цей код, навіть не будучи знайомим із Stream API. Але якщо коротко, Stream - це послідовність елементів, над якими можна виконувати різні функції. Ми отримуємо об'єкт Stream з методу Files.lines(), після чого застосовуємо до нього 3 функції:
  1. За допомогою методу filter()відбираємо ті рядки з файлу, які починаються з «Як».

  2. Проходимося по всіх відібраних рядках за допомогою методу та наводимо кожен з них до UPPER CASE.map()

  3. Об'єднуємо всі рядки, що вийшли, Listза допомогою методу collect().

На виході ми отримуємо той же результат: ЯК МИМОЛІТНЕ БАЧЕННЯ, ЯК ГЕНІЙ ЧИСТОЇ КРАСИ. Якщо тобі буде цікаво дізнатися більше про можливості цієї бібліотеки, рекомендуємо прочитати цю статтю . Ми ж повернемося до наших баранів, тобто файлів:) Остання можливість, яку ми сьогодні розглянемо - це прохід по дереву файлів . Файлова структура в сучасних операційних системах найчастіше має вигляд дерева: він має корінь і є гілки, яких можуть відділятися інші гілки тощо. Роль кореня та гілок виконують директорії. Наприклад, у ролі кореня може виступати директорія " С:// ". Від нього відходять дві гілки: " C://Downloads " і " C://Users”. Від кожної з цих гілок відходять ще по 2 гілки: " C://Downloads/Pictures ", " C://Downloads/Video ", " C://Users/JohnSmith ", " C://Users/Pudge2005 " . Від цих гілок відходять інші гілки тощо. - так і виходить дерево. У Linux це виглядає приблизно так само, тільки там у ролі кореня виступає директорія / Files, Path - 2 Тепер уяви, що у нас є завдання: знаючи кореневий каталог, ми повинні пройтися по ньому, заглянути в папки всіх рівнів і знайти файли з потрібним нам вмістом. Ми шукатимемо файли, що містять рядок «This is the file we need!» Нашим кореневим каталогом буде папка testFolder, яка лежить на робочому столі. Усередині у неї такий вміст: Files, Path - 3Усередині папок level1-a і level1-b теж є папки: Files, Path - 4Files, Path - 5Усередині цих «папок другого рівня» папок вже немає, тільки окремі файли: Files, Path - 6Files, Path - 73 файли з потрібним нам вмістом ми спеціально позначимо зрозумілими назвами - FileWeNeed1.txt, FileWeNeed2.txt, File 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("Потрібний файл виявлено!");
               System.out.println(file.toAbsolutePath());
               break;
           }
       }

       return FileVisitResult.CONTINUE;
   }
}
У разі наш клас успадковується від SimpleFileVisitor. Це клас, реалізуючий FileVisitor, у якому необхідно перевизначити лише один метод: visitFile(). Тут ми і описуємо, що потрібно робити з кожним файлом у кожній директорії. Якщо тобі потрібна складніша логіка обходу, варто написати свою реалізацію FileVisitor. Там знадобиться реалізувати ще 3 методи:
  • 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());
   }
}
Висновок: Потрібний файл виявлено! C:\Users\Username\Desktop\testFolder\FileWeNeed1.txt Потрібний файл виявлено! C:\Users\Username\Desktop\testFolder\level1-a\level2-aa\FileWeNeed2.txt Потрібний файл виявлено! C:\Users\Username\Desktop\testFolder\level1-b\level2-bb\FileWeNeed3.txt Відмінно, у нас все вийшло! :) Якщо тобі хочеться дізнатися більше про walkFileTree(), рекомендую тобі цю статтю . Також ти можеш виконати невелике завдання – замінити SimpleFileVisitorна звичайний FileVisitor, реалізувати всі 4 методи та придумати призначення для цієї програми. Наприклад, можна написати програму, яка логуватиме всі свої дії: виводити в консоль назву файлу або папки до/після входу до них. На цьому все – до зустрічі! :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ