Hi! Today we'll talk about working with files and directories. You already know how to manage file contents: we've dedicated a lot of lessons to this :) I think you find it easy to remember a few classes used for these purposes. Files, Path - 1 In today's lesson, we'll talk specifically about file management: creating, renaming, etc. Before Java 7, all such operations were performed using the File class. You can read about it here. But in Java 7, the language's creators decided to change how we work with files and directories. This happened because the File class had several drawbacks. For example, it did not have the copy() method, which would let you copy a file from one location to another (a seemingly essential ability). In addition, the File class had quite a few methods that returned boolean values. When there is an error, such a method returns false. It does not throw an exception, making it very difficult to identify errors and diagnose their causes. In the place of the single File class, 3 classes appeared: Paths, Path, and Files. Well, to be precise, Path is an interface, not a class. Let's figure out how they differ from each other and why we need each of them. Let's start with the simplest: Paths.

Paths

Paths is a very simple class with a single static method: get(). It was created solely to get a Path object from the passed string or URI. It has no other functionality. Here is an example of it at work:
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");
   }
}
Not the most complex class, right? :) Well, we've also got this Path type. Let's figure out what Path is and why it is needed :)

Path

Path, by and large, is a redesigned analogue of the File class. It is much easier to work with than File. First, many utility (static) methods were taken out and moved to the Files class. Second, order was imposed on the return values of the methods of the Path interface. In the File class, methods returned either a String, or a boolean, or a File. It wasn't easy to figure it out. For example, there was a getParent() method that returned a string representing the parent path of the current file. But there was also a getParentFile() method, which returned the same thing but in the form of a File object! This is clearly redundant. Accordingly, in the Path interface, the getParent() method and other methods for working with files simply return a Path object. No pile of options — everything is easy and simple. What are some of the useful methods that Path has? Here are some of them and examples of how they work:
  • getFileName(): returns the file name from the path;

  • getParent(): returns the "parent" directory of the current path (in other words, the directory located immediately above in the directory tree);

  • getRoot(): returns the "root" directory, i.e. the directory at the top of the directory tree;

  • startsWith(), endsWith(): check whether the path starts/ends with the passed 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 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);
       }
    }

    Console output:

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

    Pay attention to how the endsWith() method works. It checks whether the current path ends with the passed path. Specifically, whether it is in the path, not in the passed string.

    Compare the results of these two calls:

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

    Console output:

    false
    true

    The endsWith() method must be passed a genuine path, not just a set of characters: otherwise, the result will always be false, even if the current path really ends with that sequence of characters (as is the case with "estFile.txt" in the example above).

    In addition, Path has a group of methods that simplifies working with absolute (full) and relative paths.

Let's look at these methods:
  • boolean isAbsolute() returns true if the current path is absolute:

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

    Console output:

    true
  • Path normalize(): "normalizes" the current path, removing unnecessary elements from it. You may know that in popular operating systems the symbols "." (current directory) and ".." (parent directory) are often used to designate paths. For example, "./Pictures/dog.jpg" means that the the current directory has a "Pictures" folder, which in turn contains a "dog.jpg" file.

    Look here. If a path using "." or ".." appears in your program, the normalize() method will remove them and produce a path that does not contain them:

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

    Console output:

    C:\Users\Java\examples
    C:\Users\examples
  • Path relativize(): computes the relative path between the current and the passed path.

    For example:

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

    Console output:

    Username\Desktop\testFile.txt

The complete list of Path methods is quite long. You can find them all in the Oracle documentation. Now we'll move on to consider Files.

Files

Files is a utility class that holds the static methods taken out of the File class. Files is comparable to Arrays or Collections. The difference is that it works with files, not arrays or collections :) It focuses on managing files and directories. Using the static methods of the Files class, we can create, delete, and move files and directories. These operations are performed using the createFile() (for directories, createDirectory()), move(), and delete() methods. Here's how to use them:
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")));
   }
}
Here we first create a file (Files.createFile() method) on the desktop. Then we create a folder in the same location (Files.createDirectory() method). After that, we move the file (Files.move()method) from the desktop to this new folder, and finally we delete the file (Files.delete() method). Console output:
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
Note: like the methods of the Path interface, many methods of the Files class return a Path object. Most of the methods of the Files class also take Path objects as inputs. Here the Paths.get() method will be your faithful assistant — make good use of it. What else is interesting in Files? What the old File class really lacked is a copy() method! We talked about it at the beginning of this lesson. Now it's time to meet it!
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")));
   }
}
Console output:
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
Now you know how to copy files programmatically! :) Of course, the Files class lets you not only manage a file itself, but also work with its contents. It has the write() method for writing data to a file, and all of 3 methods for reading data: read(), readAllBytes(), and readAllLines() We will dwell in detail on the last one. Why that one? Because it has a very interesting return type: List<String>! That is, it returns us a list of all the lines in the file. Of course, this makes it very convenient to work with the file contents, because the entire file, line by line, can, for example, be displayed on the console using an ordinary for loop:
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);
       }
   }
}
Console output:
I still recall the wondrous moment:
When you appeared before my sight,
As though a brief and fleeting omen,
Pure phantom in enchanting light.
Super convenient! :) This ability appeared in Java 7. The Stream API appeared in Java 8. It adds some elements of functional programming to Java. Including richer file handling capabilities. Imagine that we have the following task: find all the lines that begin with the word "As", convert them to UPPERCASE, and display them on the console. What would a solution using the Files class look like in Java 7? Something like this:
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);
       }
   }
}
Console output:
AS THOUGH A BRIEF AND FLEETING OMEN,
PURE PHANTOM IN ENCHANTING LIGHT.
Mission accomplished, but don't you think that for such a simple task our code turned out to be a little... verbose? Using Java 8's Stream API, the solution looks much more elegant:
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);
   }
}
We achieved the same result, but with much less code! What's more, no one can say that we've lost "readability". I think you can easily comment on what this code does, even without being familiar with the Stream API. In short, a Stream is a sequence of elements, over which you can perform various operations. We get a Stream object from the Files.lines() method, and then apply 3 functions to it:
  1. We use the filter() method to select only those lines from the file that begin with "As".

  2. We walk through all the selected lines using the map() method and convert each of them to UPPERCASE.

  3. We use the collect() method to gather all of the received lines into a List.

We get the same output:
AS THOUGH A BRIEF AND FLEETING OMEN,
PURE PHANTOM IN ENCHANTING LIGHT.
Now let's return to our bread and butter, that is, files :) The last capability that we will consider today is walking through a file tree. In modern operating systems, the file structure most often looks like a tree: it has a root and there are branches, which can have other branches, etc. The root and branches are directories. For example, the directory "С://" may be the root. It includes two branches: "C://Downloads" and "C://Users". Each of these branches has two branches: "C://Downloads/Pictures", "C://Downloads/Video", "C://Users/JohnSmith", "C://Users/Pudge2005". And these branches in turn have other branches, etc. and this is why we call it a tree. On Linux, the structure is similar, but the /home directory Files, Path - 2is the root. Now imagine that we need to start at the root directory, walk through all of its folders and subfolders, and find files that have some particular content. We will search for files that begin with the line "This is the file we need!" We'll take the "testFolder" folder, which is on the desktop, as the root directory. Here are its contents: Files, Path - 3The level1-a and level1-b folders also contain folders: Files, Path - 4Files, Path - 5There are no folders in these "second level folders", only individual files: Files, Path - 6Files, Path - 7The 3 files with the contents we need are deliberately given explanatory names: FileWeNeed1.txt, FileWeNeed2.txt, FileWeNeed3.txt. These are precisely the files we need to find using Java. How do we do this? A very powerful method for traversing a file tree comes to our aid: Files.walkFileTree (). Here's what we need to do. First, we need a FileVisitor. FileVisitor is a special class that encapsulates the entire logic for traversing a file tree. In particular, that's where we will put the logic for reading the contents of a file and checking whether it contains the text we need. Here's what our FileVisitor looks like:
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());
           }
       }

       return FileVisitResult.CONTINUE;
   }
}
In this case, our class inherits SampleFileVisitor. This is a simplified version of FileVisitor, in which we need to override just one method: visitFile(). Here we define what needs to be done with each file in each directory. If you need more complex logic for traversing the file structure, you should inherit FileVisitor. You would need to implement 3 more methods in that class:
  • preVisitDirectory(): the logic to execute before entering a folder;

  • visitFileFailed(): the logic to execute if a file cannot be visited (no access, or for other reasons);

  • postVisitDirectory(): the logic to execute after entering a folder.

We don't need any such logic executed, so we instead inherit a simpler version. The logic inside the visitFile() method is quite simple: read all the lines in the file, check if they contain the content we need, and if so, print the absolute path on the console. The only line that might cause you difficulty is this one:
return FileVisitResult.CONTINUE;
Actually, this is very simple. Here we are simply describing what the program should do after the file is visited and all the necessary operations have been performed. In our case, we want to continue traversing the tree, so we choose the CONTINUE option. But, alternatively, we might have a different objective: instead of finding all files that contain "This is the file we need", find only one such file. After that, the program should terminate. In this case, our code would look exactly the same, but the return value would be:
return FileVisitResult.TERMINATE;
Well, let's run our code and see if it works.
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());
   }
}
Console output:
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
Excellent! It worked! :) You could also accept this small challenge: replace SimpleFileVisitor with an ordinary FileVisitor, override all 4 methods, and come up with your own purpose for the program. For example, you could write a program that logs all its actions: display the name of the file or folder before or after entering them. That's all for now. See you soon! :)