1. Overview
From version 1.5, Kotlin standard library includes some java.nio.file.Path extension functions and operators. In this article, we’ll walk through a few examples of how to use these functions.
The complete list of extensions can be found in the official documentation.
These functions facilitate idiomatic usage of the class Path, which should be preferred over the class File. For further details, please check out our article: Java – Path vs File.
2. Interacting with the File System Using Path
In this section, we’ll explore how to implement basic operations with files and directories.
2.1. Basics Operations with Files and Directories
The Java class Path represents a directory or file path on the file system. The extension functions in the kotlin.io.path package enable us to perform basic operations on files and directories easily.
For example, we can obtain the list of files in a given directory as follow:
Path("~/Downloads").listDirectoryEntries()
The listDirectoryEntries() function returns a list of objects of type Path that represent all the files contained in the ~/Downloads directory.
If we need to copy files we can make use of the copyTo() extension function as follow:
Path("source.txt").copyTo(Path("destination.txt"))
If we want to move the file, instead of creating a copy, we can use the moveTo() as shown in the example below:
Path("source.txt").moveTo(Path("destination.txt"))
Many of the extension functions in the package leverage Kotlin features to provide an idiomatic way to use the methods of the Path class.
If we look deeper, for instance, at the implementation of moveTo:
public inline fun Path.moveTo(target: Path, overwrite: Boolean = false): Path {
val options = if (overwrite) arrayOf<CopyOption>(StandardCopyOption.REPLACE_EXISTING) else emptyArray()
return Files.move(this, target, *options)
}
We can see that the move functionality was already implemented in the Files class, but by using Kotlin’s default parameter, moveTo becomes straightforward to be used for different use cases.
Setting the overwrite parameter to true it’s more concise than providing the correspondent StandardCopyOption.REPLACE_EXISTING.
2.2. Referencing Nested Files and Directories
To explore how we can navigate the file system, let’s look at the softDelete() function. Instead of deleting a file, it will move it to a directory named .deleted.
Here’s the implementation:
fun softDelete(path: String) {
val fileToDelete = Path(path)
val destinationDirectory = fileToDelete.parent / ".deleted"
if (destinationDirectory.notExists()) {
destinationDirectory.createDirectory()
}
fileToDelete.moveTo(destinationDirectory / fileToDelete.name)
}
The div() operator, invoked using the / symbol, allows us to specify the path in different directories in a way that is very similar to the standard URI.
3. Dealing with File Content
In this section, we’ll explore how we can write and read text files using the Path class.
3.1. Writing Text Files
There are a few options for writing file content using the Path extensions. Let’s take a look:
Path("destination.txt").writeText("<file content>")
In this first example, we write the file content all in one go. The extension writeText(), by default, overrides the file if it already exists, but that behavior can be updated by providing different OpenOption parameters:
Path("destination.txt").writeText(
text = "<file content>",
charset = Charsets.UTF_8,
StandardOpenOption.CREATE_NEW
)
Passing the StandardOpenOption.CREATE_NEW parameter will make the function writeText() throw a FileAlreadyExistsException if the file already exists.
Another option for writing content to a text file is using an OutputStreamWriter or BufferedWriter provided respectively by the extension functions Path.writer() or Path.bufferedWriter().
Here’s an example of how to write content to a file line by line:
fun writeFileLineByLine(path: String) {
Path(path).bufferedWriter().use { writer ->
(1..10).forEach { i ->
writer.appendLine("Line #$i")
}
}
}
In the example above, we write 10 lines to a file, each line contains the string “Line #” followed by the line number.
3.2. Writing Text Files
The extensions for the Path class provide great flexibility in reading text files.
Here’s how we can read the whole file content as a string:
val content: String = Path("source.txt").readText()
If we need to process the file line by line the readLines() function can be a good fit. It allows us to read the whole file content as a list of strings:
val lines: List<String> = Path("source.txt").readLines()
In some cases, for instance, when processing large files, we may not want to load all the file content in memory.
Therefore a better approach could be processing the file line by line. We can use the idiomatic useLines() function to do that.
Let’s take a look at a function that counts the line in a text file to demonstrate that:
fun countLines(path: String): Int {
var lines = 0
Path(path).useLines {
it.forEach { _ -> // we don't need the line content
lines++
}
}
return lines
}
4. Handling Files Recursively
The package kotlin.io.path includes functions that facilitate dealing with files recursively. Some notable examples are:
- Path.walk(): for visiting directories recursively in an efficient manner
- Path.copyToRecursively(): for copying a whole directory and its content to another path
Note: These APIs are still experimental in the current version of Kotlin (1.8). Therefore, they may be subject to change in future versions. To use these functionalities, we have two options:
- Annotate the usage with @OptIn(ExperimentalPathApi::class)
- Use the compiler argument “-opt-in=kotlin.io.path.ExperimentalPathApi”
Alternatively, we can fallback the stable File extensions functions covered in our article: Listing Files Recursively in Kotlin.
5. Conclusion
In this article, we explored the usage of the Path class in Kotlin, and we saw that:
- Kotlin standard library includes extension functions and operators for java.nio.file.Path as stable from version 1.5
- These functions provide an idiomatic way to perform basic operations on files and directories, such as navigating directories, writing and reading files
- How these extension functions leverage Kotlin features to make working with the Path class more straightforward
As always, the code for these examples is available over on GitHub.