Bab sebelumnya memakai extension function Kotlin di java.io.File. Bab ini beralih ke API NIO.2 modern: java.nio.file.Path dan kelas utilitas java.nio.file.Files. NIO.2 diperkenalkan di Java 7 (2011) dan direkomendasikan untuk kode baru karena fitur yang lebih kaya, dukungan symbolic link, atomic move, serta exception yang lebih informatif. Kotlin tidak menyembunyikan API ini — ia tetap dapat dipakai langsung, dan banyak operasi NIO.2 lebih ringkas daripada versi java.io.File.

Path vs Paths.get #

Ada dua cara membuat Path:

  • Path.of("data/file.txt") — diperkenalkan di Java 11, lebih ringkas.
  • Paths.get("data/file.txt") — versi lama, masih kompatibel.

Keduanya menghasilkan objek Path yang sama. Output Tes 1 membuktikan pathA == pathB mengembalikan true.

kotlin
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
import java.nio.charset.StandardCharsets

const val DEMO_DIR = "/tmp/kotlin-tutorial-java-nio"

fun main() {
    val dir = Paths.get(DEMO_DIR)
    if (Files.exists(dir)) Files.walk(dir).sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) }
    Files.createDirectories(dir)

    println("=== 1. Path.of vs Paths.get ===")
    val pathA: Path = Path.of("data/contoh.txt")
    val pathB: Path = Paths.get("data/contoh.txt")
    println("Path.of(...) = $pathA")
    println("Paths.get(...) = $pathB")
    println("pathA == pathB = ${pathA == pathB}")

    println()
    println("=== 2. Files.createDirectories: buat dir bertingkat ===")
    val subDir = dir.resolve("a/b/c")
    Files.createDirectories(subDir)
    println("subDir.exists = ${Files.exists(subDir)}")
    println("subDir.fileName = ${subDir.fileName}")

    println()
    println("=== 3. Files.newBufferedWriter: tulis teks (NIO.2) ===")
    val fileA = dir.resolve("catatan.txt")
    Files.newBufferedWriter(fileA, StandardCharsets.UTF_8).use { w ->
        w.write("baris satu")
        w.newLine()
        w.write("baris dua")
    }
    println("fileA.size = ${Files.size(fileA)} bytes")

    println()
    println("=== 4. Files.newBufferedReader: baca teks (NIO.2) ===")
    val isiDibaca = Files.newBufferedReader(fileA, StandardCharsets.UTF_8).use { r ->
        r.readText()
    }
    println("isi =")
    println(isiDibaca.prependIndent("  "))

    println()
    println("=== 5. Files.readAllLines: List<String> seluruh baris ===")
    val baris: List<String> = Files.readAllLines(fileA, StandardCharsets.UTF_8)
    println("jumlah baris = ${baris.size}")
    baris.forEachIndexed { i, b -> println("  baris[$i] = $b") }

    println()
    println("=== 6. Files.readString: baca seluruh file (Java 11+) ===")
    val seluruh: String = Files.readString(fileA)
    println("readString.length = ${seluruh.length}")

    println()
    println("=== 7. Files.writeString: tulis teks (Java 11+) ===")
    val fileB = dir.resolve("baru.txt")
    Files.writeString(fileB, "konten baru\nbaris kedua\n")
    println("fileB.exists = ${Files.exists(fileB)}")
    println("fileB.size = ${Files.size(fileB)} bytes")

    println()
    println("=== 8. Files.write dengan StandardOpenOption ===")
    // Append, CREATE (buat jika belum ada)
    Files.write(fileB, listOf("baris ketiga", "baris keempat"),
        StandardCharsets.UTF_8,
        StandardOpenOption.APPEND)
    val semuaBaris = Files.readAllLines(fileB)
    println("jumlah baris setelah append = ${semuaBaris.size}")

    println()
    println("=== 9. Files.lines: stream baris lazily (Java 8+) ===")
    val hasilFilter = Files.lines(fileB)
        .filter { it.contains("baris") }
        .map { it.uppercase() }
        .toList()
    println("baris yang cocok + upper = $hasilFilter")

    println()
    println("=== 10. Files.exists, Files.delete, Files.copy ===")
    val fileC = dir.resolve("salinan.txt")
    Files.copy(fileB, fileC)
    println("fileB.exists = ${Files.exists(fileB)}")
    println("fileC (copy dari fileB).exists = ${Files.exists(fileC)}")
    println("fileC.size = ${Files.size(fileC)} bytes")

    println()
    println("=== 11. Files.move: ganti nama atau pindah ===")
    val fileD = dir.resolve("dipindah.txt")
    Files.move(fileC, fileD)
    println("fileC.exists setelah move = ${Files.exists(fileC)}")
    println("fileD.exists setelah move = ${Files.exists(fileD)}")

    println()
    println("=== 12. Bersih-bersih ===")
    Files.walk(dir).sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) }
    println("dir.exists setelah hapus = ${Files.exists(dir)}")
}

Output yang Dihasilkan:

text
=== 1. Path.of vs Paths.get ===
Path.of(...) = data/contoh.txt
Paths.get(...) = data/contoh.txt
pathA == pathB = true

=== 2. Files.createDirectories: buat dir bertingkat ===
subDir.exists = true
subDir.fileName = c

=== 3. Files.newBufferedWriter: tulis teks (NIO.2) ===
fileA.size = 20 bytes

=== 4. Files.newBufferedReader: baca teks (NIO.2) ===
isi =
  baris satu
  baris dua

=== 5. Files.readAllLines: List<String> seluruh baris ===
jumlah baris = 2
  baris[0] = baris satu
  baris[1] = baris dua

=== 6. Files.readString: baca seluruh file (Java 11+) ===
readString.length = 20

=== 7. Files.writeString: tulis teks (Java 11+) ===
fileB.exists = true
fileB.size = 24 bytes

=== 8. Files.write dengan StandardOpenOption ===
jumlah baris setelah append = 4

=== 9. Files.lines: stream baris lazily (Java 8+) ===
baris yang cocok + upper = [BARIS KEDUA, BARIS KETIGA, BARIS KEEMPAT]

=== 10. Files.exists, Files.delete, Files.copy ===
fileB.exists = true
fileC (copy dari fileB).exists = true
fileC.size = 51 bytes

=== 11. Files.move: ganti nama atau pindah ===
fileC.exists setelah move = false
fileD.exists setelah move = true

=== 12. Bersih-bersih ===
dir.exists setelah hapus = false

Mari kita bedah bagian-bagian penting:

  1. Bagian 1 (Path.of vs Paths.get): keduanya menghasilkan objek Path yang setara. Path.of lebih baru (Java 11) dan lebih disukai untuk kode baru. Paths.get tetap kompatibel untuk library lama.
  2. Bagian 2 (Files.createDirectories): membuat direktori beserta semua parent yang belum ada. Berbeda dengan File.mkdirs() yang mengembalikan Boolean, Files.createDirectories melempar IOException jika gagal — pesan error lebih informatif.
  3. Bagian 3 (Files.newBufferedWriter): membuka BufferedWriter langsung tanpa dekorasi manual. Method .use { } memastikan writer ditutup. w.newLine() menulis newline sesuai sistem (tidak hardcode \n).
  4. Bagian 4 (Files.newBufferedReader + readText): extension function Kotlin BufferedReader.readText() membaca seluruh isi. .use { } menjamin resource ditutup.
  5. Bagian 5 (Files.readAllLines): setara dengan File.readLines() di Kotlin. Mengembalikan List<String>. File diseluruhnya dibaca ke memori.
  6. Bagian 6 (Files.readString): shortcut Java 11+ untuk membaca seluruh file menjadi satu String. Output menunjukkan length 20 — 9 byte “baris satu” + 1 newline + 9 byte “baris dua” = 20. Cocok untuk file kecil.
  7. Bagian 7 (Files.writeString): shortcut Java 11+ untuk menulis teks. Default charset UTF-8. Menghapus isi sebelumnya (overwrite).
  8. Bagian 8 (Files.write dengan StandardOpenOption.APPEND): lebih fleksibel daripada writeString. Menerima Iterable<CharSequence> sehingga bisa menulis List<String> sekaligus. StandardOpenOption.APPEND menambah tanpa menghapus.
  9. Bagian 9 (Files.lines): mengembalikan Stream<String> (Java Stream) — baris dibaca lazily. Kita dapat memfilter dan memetakan tanpa memuat seluruh file ke memori. .toList() di akhir mengkonsumsi stream dan menutup resource.
  10. Bagian 10 (Files.copy): menyalin file. Default tidak menimpa file tujuan jika sudah ada — gunakan StandardCopyOption.REPLACE_EXISTING untuk menimpa.
  11. Bagian 11 (Files.move): mengganti nama atau memindah file. Default atomik di sistem file yang sama; tambahkan StandardCopyOption.ATOMIC_MOVE untuk memaksakan atomisitas.

StandardOpenOption #

Enum StandardOpenOption mengontrol cara file dibuka:

OpsiEfek
READBuka untuk baca (default untuk read)
WRITEBuka untuk tulis
APPENDTambah di akhir, bukan timpa
CREATEBuat file jika belum ada
CREATE_NEWBuat file baru; gagal jika sudah ada
TRUNCATE_EXISTINGHapus isi file yang ada saat dibuka
DELETE_ON_CLOSEHapus file saat stream ditutup

Files.lines vs Files.readAllLines #

Perbedaan penting:

AspekFiles.readAllLinesFiles.lines
MengembalikanList<String>Stream<String>
MemoriSeluruh file dimuatSatu baris pada satu waktu
SumberEagerLazy
Cocok untukFile kecilFile besar (gigabyte)
PenutupanOtomatisWajib via .use { } atau try-with-resources

Referensi Lanjutan #

Selanjutnya #

Bab berikutnya, Membaca File Teks, akan membahas lebih dalam operasi baca teks dengan fokus pada pola aman resource: File.readText(), File.readLines(), dan File.useLines { }. Kita akan melihat kapan memakai yang mana berdasarkan ukuran file dan kebutuhan pemrosesan.