Operasi file tidak hanya membaca dan menulis — kita perlu memanipulasi path (menggabungkan, menormalkan, menghitung path relatif) dan menelusuri struktur direktori (listing, walking rekursif). Bab ini membahas operasi path dengan java.nio.file.Path dan operasi direktori dengan java.io.File serta Files.walk NIO.2. Pemahaman ini penting untuk utility yang memproses pohon direktori (mis. backup tool, static site generator, analisis log).
Operasi Path: resolve, relativize, normalize #
Tiga operasi path paling sering:
Path.resolve(other): menggabungkan path — jikaotherabsolut, ia menggantikan; jika relatif, ditambahkan.Path.relativize(other): menghasilkan path relatif dari path ini keother.Path.normalize(): membersihkan.(current dir) dan..(parent dir) dari path.
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.attribute.BasicFileAttributes
const val DEMO_DIR = "/tmp/kotlin-tutorial-path-dir"
fun main() {
File(DEMO_DIR).deleteRecursively()
File(DEMO_DIR).mkdirs()
println("=== 1. Path.resolve: gabungkan path ===")
val baseDir: Path = Paths.get(DEMO_DIR)
val file1 = baseDir.resolve("a.txt")
val file2 = baseDir.resolve("sub/b.txt")
println("baseDir = ${baseDir.fileName}")
println("file1 = ${file1.fileName}")
println("file2 = ${file2.fileName}")
println()
println("=== 2. Path.relativize: path relatif antara dua path ===")
val rel: Path = baseDir.relativize(file2)
println("relativize(file2) = $rel")
println("rel.parts = ${rel.toString()}")
println()
println("=== 3. Path.normalize: bersihkan . dan .. ===")
val berantakan = Paths.get(DEMO_DIR, "sub", "..", "a.txt")
println("berantakan = ${berantakan.fileName} (parent=${berantakan.parent.fileName})")
val rapi = berantakan.normalize()
println("setelah normalize = ${rapi.fileName} (parent=${rapi.parent.fileName})")
println()
println("=== 4. Path.toAbsolutePath dan toRealPath ===")
val pathRelatif = Paths.get("a.txt")
println("relatif = $pathRelatif")
println("absolute = ${pathRelatif.toAbsolutePath().fileName}")
println()
println("=== 5. Buat struktur direktori ===")
val sub1 = File("$DEMO_DIR/sub1")
val sub1a = File("$DEMO_DIR/sub1/a")
val sub2 = File("$DEMO_DIR/sub2")
sub1.mkdirs(); sub1a.mkdirs(); sub2.mkdirs()
listOf("file-a.txt", "file-b.txt", "file-c.txt").forEach { File(sub1, it).writeText("isi $it") }
listOf("file-d.txt").forEach { File(sub1a, it).writeText("isi $it") }
listOf("file-e.txt", "file-f.log").forEach { File(sub2, it).writeText("isi $it") }
println("struktur dibuat")
println()
println("=== 6. File.list: daftar isi direktori (nama saja) ===")
val isiSub1 = sub1.list()?.toList() ?: emptyList()
println("sub1.list = ${isiSub1.sorted()}")
val isiRoot = File(DEMO_DIR).list()?.toList() ?: emptyList()
println("DEMO_DIR.list = ${isiRoot.sorted()}")
println()
println("=== 7. File.listFiles: File[] dengan filter ===")
val hanyaTxt = sub1.listFiles { file -> file.extension == "txt" }?.map { it.name } ?: emptyList()
println("sub1 hanya .txt = ${hanyaTxt.sorted()}")
println()
println("=== 8. File.walk: traversing rekursif ===")
val semua = File(DEMO_DIR).walkTopDown().filter { it.isFile }.map { it.relativeTo(File(DEMO_DIR)).path }.toList()
println("semua file (rekursif, relatif):")
semua.sorted().forEach { println(" - $it") }
println()
println("=== 9. Files.walk (NIO.2): traversing dengan filter ===")
val walkStream = Files.walk(baseDir)
.filter { Files.isRegularFile(it) }
.map { baseDir.relativize(it).toString() }
.sorted()
.toList()
println("Files.walk sorted:")
walkStream.forEach { println(" - $it") }
println()
println("=== 10. File.walkTopDown vs walkBottomUp (urutan traversal) ===")
// walkTopDown: parent directory sebelum isinya (pre-order)
// walkBottomUp: isi directory sebelum parent-nya (post-order)
val semuaSub1 = sub1.walkTopDown().toList().map { it.relativeTo(File(DEMO_DIR)).path }
val semuaSub1BU = sub1.walkBottomUp().toList().map { it.relativeTo(File(DEMO_DIR)).path }
println("walkTopDown (parent dulu) = $semuaSub1")
println("walkBottomUp (child dulu) = $semuaSub1BU")
println()
println("=== 11. Files.isDirectory dan BasicFileAttributes ===")
val attrs = Files.readAttributes(baseDir, BasicFileAttributes::class.java)
println("isDirectory (NIO) = ${attrs.isDirectory}")
println("isRegularFile (NIO) = ${attrs.isRegularFile}")
println()
println("=== 12. Bersih-bersih ===")
File(DEMO_DIR).deleteRecursively()
println("demo dir dihapus = ${!File(DEMO_DIR).exists()}")
}Output yang Dihasilkan:
=== 1. Path.resolve: gabungkan path ===
baseDir = kotlin-tutorial-path-dir
file1 = a.txt
file2 = b.txt
=== 2. Path.relativize: path relatif antara dua path ===
relativize(file2) = sub/b.txt
rel.parts = sub/b.txt
=== 3. Path.normalize: bersihkan . dan .. ===
berantakan = a.txt (parent=..)
setelah normalize = a.txt (parent=kotlin-tutorial-path-dir)
=== 4. Path.toAbsolutePath dan toRealPath ===
relatif = a.txt
absolute = a.txt
=== 5. Buat struktur direktori ===
struktur dibuat
=== 6. File.list: daftar isi direktori (nama saja) ===
sub1.list = [a, file-a.txt, file-b.txt, file-c.txt]
DEMO_DIR.list = [sub1, sub2]
=== 7. File.listFiles: File[] dengan filter ===
sub1 hanya .txt = [file-a.txt, file-b.txt, file-c.txt]
=== 8. File.walk: traversing rekursif ===
semua file (rekursif, relatif):
- sub1/a/file-d.txt
- sub1/file-a.txt
- sub1/file-b.txt
- sub1/file-c.txt
- sub2/file-e.txt
- sub2/file-f.log
=== 9. Files.walk (NIO.2): traversing dengan filter ===
Files.walk sorted:
- sub1/a/file-d.txt
- sub1/file-a.txt
- sub1/file-b.txt
- sub1/file-c.txt
- sub2/file-e.txt
- sub2/file-f.log
=== 10. File.walkTopDown vs walkBottomUp (urutan traversal) ===
walkTopDown (parent dulu) = [sub1, sub1/a, sub1/a/file-d.txt, sub1/file-a.txt, sub1/file-b.txt, sub1/file-c.txt]
walkBottomUp (child dulu) = [sub1/a/file-d.txt, sub1/a, sub1/file-a.txt, sub1/file-b.txt, sub1/file-c.txt, sub1]
=== 11. Files.isDirectory dan BasicFileAttributes ===
isDirectory (NIO) = true
isRegularFile (NIO) = false
=== 12. Bersih-bersih ===
demo dir dihapus = trueMari kita bedah bagian-bagian penting:
- Bagian 1 (
resolve): menggabungkan path.baseDir.resolve("a.txt")menghasilkan path kea.txtdi dalambaseDir. Jika argumen absolut (mis./tmp/other.txt),resolvemengabaikanbaseDirdan mengembalikan path absolut baru. - Bagian 2 (
relativize): menghitung path relatif.baseDir.relativize(file2)menghasilkansub/b.txt— path yang jika di-resolve daribaseDirakan menghasilkanfile2. Berguna saat menampilkan path file tanpa path lengkap. - Bagian 3 (
normalize): membersihkan.dan..dari path.DEMO_DIR/sub/../a.txtdinormalkan menjadiDEMO_DIR/a.txt. Berguna saat menerima path dari input user yang mungkin berantakan. - Bagian 4 (
toAbsolutePath): mengubah path relatif menjadi absolut. Berguna saat harus menyimpan path absolut di konfigurasi.toRealPath()(tidak dipakai di sini) juga menyelesaikan symbolic link. - Bagian 5 (buat struktur): tiga sub-direktori dengan beberapa file. Struktur ini digunakan oleh demo berikutnya.
- Bagian 6 (
File.list): mengembalikanArray<String>nama file di direktori. Tidak rekursif.list()mengembalikannulljika bukan direktori atau jika terjadi I/O error — selalu cek null. - Bagian 7 (
File.listFiles): versilist()yang mengembalikanArray<File>. Menerima lambda filter opsional — di sini hanya file dengan extension.txtyang dipilih. - Bagian 8 (
File.walkTopDown): traversing rekursif. MengembalikanSequence<File>yang mencakup semua file dan direktori di dalam pohon. FilterisFilehanya menyimpan file;relativeTomembuat path relatif terhadap root untuk output yang ringkas. - Bagian 9 (
Files.walk): versi NIO.2. MengembalikanStream<Path>Java. Lebih fleksibel karena bisa di-chain dengan Java Stream API.Files.isRegularFile(it)adalah cek file reguler yang lebih akurat daripadaisFile(menghormati symbolic link). - Bagian 10 (
walkTopDownvswalkBottomUp): perbedaan urutan traversal.walkTopDown(pre-order) mengunjungi parent sebelum isinya —sub1muncul lebih dulu.walkBottomUp(post-order) mengunjungi isi sebelum parent —sub1muncul terakhir. Berguna saat ingin menghapus direktori: hapus file dulu, baru direktori (polawalkBottomUp). - Bagian 11 (
BasicFileAttributes): membaca atribut dasar file/direktori via NIO.2.isDirectory,isRegularFile,size,lastModifiedTime,creationTimesemuanya tersedia diBasicFileAttributes.
Pola Umum Traversing #
Beberapa pola traversing direktori yang sering muncul:
fun deleteRecursivelySafe(root: File) {
root.walkBottomUp().forEach { it.delete() }
}
// walkBottomUp penting: hapus file dulu, baru direktori parentval ktFiles = File("src").walkTopDown()
.filter { it.isFile && it.extension == "kt" }
.toList()val totalBytes = File("/var/log").walkTopDown()
.filter { it.isFile }
.map { it.length() }
.sum()File.list vs File.listFiles vs File.walk #
| Function | Mengembalikan | Rekursif? | Filter? |
|---|---|---|---|
list() | Array<String>? | Tidak | Tidak |
listFiles() | Array<File>? | Tidak | Tidak |
listFiles(filter) | Array<File>? | Tidak | Ya (lambda) |
walkTopDown() | Sequence<File> | Ya | Filter via .filter { } |
walkBottomUp() | Sequence<File> | Ya | Filter via .filter { } |
Files.walk(path) | Stream<Path> | Ya | Filter via .filter { } |
Referensi Lanjutan #
- Kotlin stdlib kotlin.io.path
— paket
kotlin.io.pathdengan extensionPathidiomatic Kotlin - Kotlin stdlib java.nio.file
— alias Kotlin stdlib untuk
java.nio.file.PathdanFiles - Java SE 21 Files
— dokumentasi
java.nio.file.FilesJava SE 21 untuk operasi direktori
Selanjutnya #
Bab berikutnya, File Attributes, akan membahas atribut file secara mendalam: BasicFileAttributes, PosixFileAttributes, Files.getAttribute, dan FileTime untuk membaca dan memodifikasi metadata file.