1. Iterating over files in a directory
Getting a list of files: Files.list and Files.walk
In Java, there are two very useful methods for working with directory contents:
- Files.list(Path) — returns a stream (Stream<Path>) of all top-level files and directories in the given directory.
- Files.walk(Path) — returns a stream of all files and directories in the directory and all its subdirectories (recursively).
Example: print all files and directories in the data directory
import java.nio.file.*;
import java.io.IOException;
public class ListFilesExample {
public static void main(String[] args) throws IOException {
Path dir = Paths.get("data");
// Get a stream of all top-level files and directories
try (var stream = Files.list(dir)) {
stream.forEach(System.out::println);
}
}
}
If you want to traverse all nested directories (recursively), use Files.walk:
try (var stream = Files.walk(dir)) {
stream.forEach(System.out::println);
}
Warning:
Files.walk can return a huge number of files if the structure is large—don’t run it at the root of a drive unless you want to see the “list of the entire universe”!
Visualization: difference between list and walk
| Method | What it returns |
|---|---|
|
Only the contents of the current directory |
|
All files and directories, including nested ones |
2. Filtering files
One of the main advantages of working with streams (Stream<Path>) is the ability to filter files by any criterion: extension, name, size, creation date, etc.
Filtering by extension
Suppose we need only .txt files. You can filter them like this:
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
Filtering by name
We want all files whose name starts with "report":
stream
.filter(path -> path.getFileName().toString().startsWith("report"))
.forEach(System.out::println);
Filtering by size
Get only large files (greater than 1 MB):
stream
.filter(path -> {
try {
return Files.size(path) > 1_000_000;
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
Filtering by date
For example, files modified in the last 7 days:
import java.time.*;
import java.nio.file.attribute.*;
stream
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
All together: combined filtering
You can combine filters “à la carte”: for example, all .log files older than a month and larger than 10 KB:
stream
.filter(path -> path.toString().endsWith(".log"))
.filter(path -> {
try {
return Files.size(path) > 10_000;
} catch (IOException e) {
return false;
}
})
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
3. Bulk copying and deleting files
Bulk operations are simple: get a stream of files, then apply the desired function to each element. The main thing is not to forget about possible errors (for example, a file is locked or already exists).
Copying all files from one directory to another
import java.nio.file.StandardCopyOption;
Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");
try (var stream = Files.list(sourceDir)) {
stream.forEach(path -> {
Path targetPath = targetDir.resolve(path.getFileName());
try {
Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copied: " + path.getFileName());
} catch (IOException e) {
System.out.println("Error copying " + path.getFileName() + ": " + e.getMessage());
}
});
}
Deleting all temporary files (*.tmp)
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("Deleted: " + path.getFileName());
} catch (IOException e) {
System.out.println("Error deleting " + path.getFileName() + ": " + e.getMessage());
}
});
}
Using the Stream API to process collections of paths
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.sorted((p1, p2) -> {
try {
return Long.compare(Files.size(p2), Files.size(p1)); // by size, descending
} catch (IOException e) {
return 0;
}
})
.limit(5) // only the 5 largest
.forEach(System.out::println);
}
4. Practical tasks
Finding and deleting files older than a certain date
Delete all files that have not been modified for more than a year:
try (var stream = Files.list(dir)) {
stream
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
} catch (IOException e) {
return false;
}
})
.forEach(path -> {
try {
Files.delete(path);
System.out.println("Deleted: " + path.getFileName());
} catch (IOException e) {
System.out.println("Error deleting " + path.getFileName() + ": " + e.getMessage());
}
});
}
Batch renaming files by pattern
Suppose you need to add the prefix "old_" to every .txt file:
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
Path newPath = path.resolveSibling("old_" + path.getFileName());
try {
Files.move(path, newPath);
System.out.println("Renamed: " + path.getFileName() + " -> " + newPath.getFileName());
} catch (IOException e) {
System.out.println("Error renaming " + path.getFileName() + ": " + e.getMessage());
}
});
}
Copying all files from all subdirectories (recursively)
Use Files.walk to iterate over all files in all subdirectories:
try (var stream = Files.walk(sourceDir)) {
stream
.filter(Files::isRegularFile)
.forEach(path -> {
Path relative = sourceDir.relativize(path);
Path targetPath = targetDir.resolve(relative);
try {
Files.createDirectories(targetPath.getParent());
Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copied: " + path + " -> " + targetPath);
} catch (IOException e) {
System.out.println("Error copying " + path + ": " + e.getMessage());
}
});
}
5. Important considerations and nuances
Error handling in bulk operations
In bulk operations, you almost always encounter “problem” files: no access, the file is locked, the path is too long, the file already exists, etc. If you don’t handle exceptions inside the loop, the program may crash on the very first failure. So always use try-catch inside forEach to “swallow” the error for one file but continue processing the rest.
Impact of recursion on performance and the call stack
When using Files.walk on large directory trees, you can end up with a very long traversal if you implement recursion manually. Fortunately, the method itself is implemented efficiently and does not cause stack overflows. But if you write your own recursive function for deleting/copying directories, be careful: for huge nested structures, the stack may overflow.
Working with large directories
- Use Files.walk with care: if a directory has thousands of files, it can take time and consume a lot of memory.
- If you only need to process the top level, use Files.list.
- To search for files by mask, use name-based filtering (endsWith, matches, etc.).
Example: “Clean a directory of temporary files”
Path tempDir = Paths.get("data/tmp");
try (var stream = Files.walk(tempDir)) {
stream
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("Deleted: " + path);
} catch (IOException e) {
System.out.println("Error deleting " + path + ": " + e.getMessage());
}
});
}
Key methods for bulk operations
| Operation | Method/approach | Notes |
|---|---|---|
| Get all files | |
walk — recursive, list — top level only |
| Filter by extension | |
Can be combined with other filters |
| Copy a group of files | |
Verify the target directory exists |
| Delete a group of files | |
Prefer try-catch for each operation |
| Bulk rename | |
Renaming is move with a new name |
6. Common mistakes in bulk operations
Mistake #1: Unhandled exceptions inside forEach.
A very common issue: if you don’t wrap each operation in try-catch, the program will crash on the first problematic file, and the rest won’t be processed.
Mistake #2: Trying to delete/copy a directory as a file (or vice versa).
The methods Files.delete and Files.copy handle files and directories differently. Don’t mix them up! For example, attempting to delete a non-empty directory with the standard method will cause an error.
Mistake #3: Incorrectly building the destination path when copying.
If you build the destination path without preserving structure (for example, not using relativize), you can overwrite files or end up with the wrong structure in the archive.
Mistake #4: Opening too many files at the same time.
If you open streams for reading/writing inside a loop, don’t forget to close them! Prefer try-with-resources.
Mistake #5: Recursive traversal without a depth limit.
When using Files.walk on very large and deep directory trees, you can run into performance and memory issues. If you need to limit the depth, use the overload with a depth parameter: Files.walk(dir, depth).
Mistake #6: Implicitly ignoring hidden/system files.
If your application works with user files, you can accidentally touch hidden or system files. To filter them out, use the method Files.isHidden(path).
GO TO FULL VERSION