- Kotlin
Kotlin ファイル読み込み・書き込み readText・追記
Kotlin のファイル読み書きは、java.io.File に用意された拡張関数で書けます。読み込みは readText(全体を1つの文字列)・readLines(行のリスト)・useLines / forEachLine(1行ずつ)、書き込みは writeText(上書き)・appendText(追記)が基本です。
この記事では、8つの関数と kotlin.io.path の同等関数を「全部メモリに載るか・自動で閉じるか・向く用途」で一覧にし、Shift_JIS(MS932)の文字化け、存在しないファイルの例外文、改行コードと BOM、use での閉じ方まで、すべて実行した出力と一緒に示します。コードはそのまま貼って動く完成形です。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。パスの区切りや例外文の括弧内(No such file or directory など)は OS が返す文言なので、ここでは Linux での表示を載せています。
Kotlin ファイル読み書きの早見表
読み込み
| 関数(java.io.File) | 戻り値 | 全部メモリに載るか | 自動で閉じるか | 向く用途 | kotlin.io.path の同等関数 |
|---|---|---|---|---|---|
readText() |
String | 載る | 閉じる | 設定ファイルや JSON など、小さなファイルを丸ごと扱う | Path.readText() |
readLines() |
List<String> |
載る | 閉じる | 行数が少ないファイルを、添字や size で扱う | Path.readLines() |
forEachLine { } |
Unit | 載らない(1行ずつ) | 閉じる | 大きなログを先頭から順に処理する | Path.forEachLine { } |
useLines { } |
ブロックの戻り値 | 載らない(Sequence) | 閉じる | filter・map・take などで絞り込み、結果を返す | Path.useLines { } |
bufferedReader() |
BufferedReader | 載らない | 閉じない(use が必要) | 先頭の数行だけ読む、読む量を自分で決める | Path.bufferedReader() |
readBytes() |
ByteArray | 載る | 閉じる | 画像など文字ではないデータ、BOM の確認 | Path.readBytes() |
書き込み
| 関数(java.io.File) | 動き | ファイルが無いとき | 自動で閉じるか | 向く用途 | kotlin.io.path の同等関数 |
|---|---|---|---|---|---|
writeText(text) |
上書き | 作る | 閉じる | 文字列を1回で書き出す | Path.writeText(text) |
appendText(text) |
末尾に追記 | 作る | 閉じる | ログを1行足す | Path.appendText(text) |
printWriter() |
上書きで開く | 作る | 閉じない(use が必要) | println で何行も書く | 一覧に無い(Path.bufferedWriter() や Path.writeLines() を使う) |
bufferedWriter() |
上書きで開く | 作る | 閉じない(use が必要) | write と newLine で大量に書く | Path.bufferedWriter() |
writeBytes(array) |
上書き | 作る | 閉じる | バイト列をそのまま書く | Path.writeBytes(array) |
どの関数も文字コードの既定値は UTF-8で、引数に Charset を渡すと切り替えられます。親フォルダが無い場合は、どの書き込み関数も例外になります(後半で出力を示します)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin ファイル読み込み・書き込み readText・追記」
まず動かす:読み込みと書き込みの最小コード
次のコードを basic.kt として保存します。
import java.io.File
fun main() {
val file = File("memo.txt")
// 書き込み(上書き)
file.writeText("1行目\n2行目\n")
// 追記
file.appendText("3行目\n")
// 全体を1つの文字列で読む
println(file.readText())
// 行のリストで読む
println(file.readLines())
// 1行ずつ処理する
file.forEachLine { println("行: $it") }
// Sequence で読み、終わったら自動で閉じる
val count = file.useLines { lines -> lines.count { it.isNotBlank() } }
println("行数: $count")
}
コンパイルと実行は次のとおりです。-Dstdout.encoding=UTF-8 は、日本語の出力を UTF-8 で表示するための指定です。
kotlinc basic.kt -include-runtime -d basic.jar
java -Dstdout.encoding=UTF-8 -jar basic.jar
1行目
2行目
3行目
[1行目, 2行目, 3行目]
行: 1行目
行: 2行目
行: 3行目
行数: 3
readText は末尾の改行も含めて返すので、println すると空行が1つ増えます。相対パスは、プログラムを起動したフォルダ(作業フォルダ)からの位置として解釈されます。
読み込み関数を使い分ける
5つの読み方を同じファイルで比べる
import java.io.File
fun main() {
val file = File("items.txt")
file.writeText("apple,120\nbanana,80\ncherry,300\n")
println("--- readText ---")
val text: String = file.readText()
println(text.length)
println("--- readLines ---")
val lines: List<String> = file.readLines()
println(lines)
println(lines.size)
println("--- forEachLine ---")
file.forEachLine { line -> println(line.uppercase()) }
println("--- useLines ---")
val expensive = file.useLines { seq ->
seq.map { it.split(",") }
.filter { it[1].toInt() >= 100 }
.map { it[0] }
.toList()
}
println(expensive)
println("--- bufferedReader ---")
file.bufferedReader().use { reader ->
val first = reader.readLine()
val second = reader.readLine()
println("$first / $second")
}
println("--- readBytes ---")
println(file.readBytes().size)
}
--- readText ---
31
--- readLines ---
[apple,120, banana,80, cherry,300]
3
--- forEachLine ---
APPLE,120
BANANA,80
CHERRY,300
--- useLines ---
[apple, cherry]
--- bufferedReader ---
apple,120 / banana,80
--- readBytes ---
31
| やりたいこと | 選ぶ関数 | 理由 |
|---|---|---|
| 中身を丸ごと文字列でほしい | readText | 1行で書け、閉じる処理も不要 |
| 行番号や件数を使いたい | readLines | List なので添字・size・reversed が使える |
| 1行ずつ順に処理するだけ | forEachLine | 行をためないので、大きなファイルでもメモリが増えにくい |
| 絞り込んで結果を返したい | useLines | Sequence なので filter・map・take が使え、ブロックの戻り値がそのまま返る |
| 先頭の数行で読むのをやめたい | bufferedReader + use | readLine を呼ぶ回数を自分で決められる |
大きなファイルでのメモリの差を測る
「全部メモリに載るか」の違いを、200万行・103 MB のログで測りました。
import java.io.File
import kotlin.time.measureTimedValue
fun usedMb(): Long {
System.gc()
val rt = Runtime.getRuntime()
return (rt.totalMemory() - rt.freeMemory()) / 1024 / 1024
}
fun main() {
val file = File("big.log")
file.printWriter().use { out ->
repeat(2_000_000) { i -> out.println("2026-10-05 12:00:00 INFO request id=$i status=${if (i % 1000 == 0) 500 else 200}") }
}
println("サイズ: ${file.length() / 1024 / 1024} MB")
val base = usedMb()
val (n1, t1) = measureTimedValue { file.useLines { seq -> seq.count { it.endsWith("status=500") } } }
println("useLines : $n1 件 ${t1.inWholeMilliseconds} ms 使用メモリの増加 ${usedMb() - base} MB")
var n2 = 0
val t2 = kotlin.time.measureTime { file.forEachLine { if (it.endsWith("status=500")) n2++ } }
println("forEachLine : $n2 件 ${t2.inWholeMilliseconds} ms 使用メモリの増加 ${usedMb() - base} MB")
val (lines, t3) = measureTimedValue { file.readLines() }
println("readLines : ${lines.count { it.endsWith("status=500") }} 件 ${t3.inWholeMilliseconds} ms 使用メモリの増加 ${usedMb() - base} MB")
}
サイズ: 103 MB
useLines : 2000 件 1266 ms 使用メモリの増加 -1 MB
forEachLine : 2000 件 922 ms 使用メモリの増加 -1 MB
readLines : 2000 件 2133 ms 使用メモリの増加 193 MB
useLines と forEachLine は使用メモリがほぼ増えず(-1 MB は測定の誤差)、readLines は 103 MB のファイルに対して 193 MB 増えました。 JVM の文字列は内部表現の分だけファイルより大きくなることがあります。時間は実行のたびに変わるので目安として見てください。数十 MB を超えるファイルは、useLines か forEachLine を選ぶと安心です。
書き込みと追記を使い分ける
import java.io.File
fun main() {
val file = File("out.txt")
file.writeText("A\n") // 上書き(無ければ作る)
file.writeText("B\n") // もう一度呼ぶと中身が置き換わる
println("writeText x2 -> " + file.readLines())
file.appendText("C\n") // 末尾に足す
file.appendText("D\n")
println("appendText -> " + file.readLines())
// 何行も書くなら printWriter / bufferedWriter を use で閉じる
file.printWriter().use { out ->
out.println("id,name")
for (i in 1..3) out.println("$i,item$i")
}
println("printWriter -> " + file.readLines())
file.bufferedWriter().use { w ->
w.write("x")
w.newLine()
w.write("y")
w.newLine()
}
println("bufferedWriter -> " + file.readLines())
// 追記モードの Writer は FileWriter(file, true) か FileOutputStream(file, true)
java.io.FileOutputStream(file, true).bufferedWriter().use { w ->
w.write("z")
w.newLine()
}
println("追記 Writer -> " + file.readLines())
// 行のリストをまとめて書く
file.writeText(listOf("p", "q", "r").joinToString("\n", postfix = "\n"))
println("joinToString -> " + file.readLines())
// 親フォルダが無いと書けないので先に作る
val nested = File("logs/2026/app.log")
nested.parentFile.mkdirs()
nested.appendText("started\n")
println("nested -> " + nested.readText().trim() + " / exists=" + nested.exists())
}
writeText x2 -> [B]
appendText -> [B, C, D]
printWriter -> [id,name, 1,item1, 2,item2, 3,item3]
bufferedWriter -> [x, y]
追記 Writer -> [x, y, z]
joinToString -> [p, q, r]
nested -> started / exists=true
押さえるポイントは4つです。
- writeText は毎回上書きです。2回呼ぶと、残るのは2回目の内容だけです。
- 追記は appendText。ファイルが無ければ新しく作られます。
- printWriter() と bufferedWriter() は上書きで開きます。 追記で開きたいときは
FileOutputStream(file, true)から bufferedWriter() を作ります。 - appendText は呼ぶたびにファイルを開いて閉じます。ループで何千行も足すなら、Writer を1回開いて use の中で書くほうが、開閉の回数が1回で済みます。
kotlin.io.path で同じことを書く
java.nio.file.Path を使う場合は、import kotlin.io.path.* で同じ名前の拡張関数が使えます。
import kotlin.io.path.*
import java.nio.file.StandardOpenOption
fun main() {
val dir = Path("work").createDirectories()
val path = dir / "note.txt"
path.writeText("one\ntwo\n")
path.appendText("three\n")
path.writeLines(listOf("four", "five"), Charsets.UTF_8, StandardOpenOption.APPEND)
println(path.readText().trimEnd())
println(path.readLines())
path.forEachLine { print("[$it]") }
println()
println(path.useLines { it.first() })
path.bufferedReader().use { println(it.readLine()) }
path.bufferedWriter(Charsets.UTF_8, 8192, StandardOpenOption.APPEND).use { it.write("six\n") }
println(path.readLines().size)
println("${path.name} ${path.extension} ${path.fileSize()} ${path.exists()}")
path.deleteIfExists()
dir.deleteIfExists()
println(path.exists())
}
one
two
three
four
five
[one, two, three, four, five]
[one][two][three][four][five]
one
one
6
note.txt txt 28 true
false
File 版との違いは次のとおりです。
| 観点 | java.io.File の拡張 | kotlin.io.path の拡張 |
|---|---|---|
| パスの作り方 | File("a/b.txt") |
Path("a/b.txt")、dir / "b.txt" |
| 行のまとめ書き | joinToString して writeText | writeLines(lines)・appendLines(lines) |
| 開き方の指定 | 追記は appendText か FileOutputStream(file, true) | StandardOpenOption(APPEND・CREATE_NEW など)を引数で渡せる |
| 無いファイルの例外 | java.io.FileNotFoundException | java.nio.file.NoSuchFileException |
| 追加の操作 | exists()・length()・mkdirs() | exists()・fileSize()・createDirectories()・deleteIfExists() など |
新しく書くコードで Path を受け渡ししているなら kotlin.io.path、既存のコードが File 中心なら File の拡張、という選び方で揃えると読みやすくなります。file.toPath() と path.toFile() で相互に変換できます。
Shift_JIS(MS932)の文字化けと Charset 指定
表計算ソフトから書き出した CSV など、UTF-8 以外のファイルを読むときは Charset を渡します。Windows で作られた Shift_JIS のファイルは、Java では MS932(windows-31j)という名前で扱うのが確実です。
import java.io.File
import java.nio.charset.Charset
fun main() {
val ms932 = Charset.forName("MS932")
val file = File("sjis.csv")
// Shift_JIS(MS932)で書く
file.writeText("商品名,数量\nりんご,3\n①髙橋,1\n", ms932)
println("バイト数: " + file.readBytes().size)
println("--- 文字コード指定なし(UTF-8 として読む) ---")
println(file.readText())
println("--- Charset を指定して読む ---")
println(file.readText(ms932))
println("--- Shift_JIS と MS932 の違い ---")
println(file.readText(Charset.forName("Shift_JIS")))
// 逆向き:UTF-8 のファイルを MS932 で読む
val utf8 = File("utf8.txt")
utf8.writeText("日本語のメモ\n")
println("--- UTF-8 のファイルを MS932 で読む ---")
println(utf8.readText(ms932))
// MS932 に無い文字を書くと ? になる
val emoji = File("emoji.txt")
emoji.writeText("OK👍\n", ms932)
println("--- MS932 に無い文字を書いた結果 ---")
println(emoji.readText(ms932))
// readLines / forEachLine / useLines / bufferedReader も同じ引数
println(file.readLines(ms932))
file.forEachLine(ms932) { print("<$it>") }
println()
println(file.useLines(ms932) { it.count() })
println(file.bufferedReader(ms932).use { it.readLine() })
file.appendText("みかん,5\n", ms932)
file.printWriter(ms932).use { it.println("上書き") }
println(file.readText(ms932))
println("default=" + Charset.defaultCharset() + " file.encoding=" + System.getProperty("file.encoding"))
}
バイト数: 30
--- 文字コード指定なし(UTF-8 として読む) ---
���i��,����
���,3
�@����,1
--- Charset を指定して読む ---
商品名,数量
りんご,3
①髙橋,1
--- Shift_JIS と MS932 の違い ---
商品名,数量
りんご,3
�@��橋,1
--- UTF-8 のファイルを MS932 で読む ---
譌・譛ャ隱槭�ョ繝。繝「
--- MS932 に無い文字を書いた結果 ---
OK?
[商品名,数量, りんご,3, ①髙橋,1]
<商品名,数量><りんご,3><①髙橋,1>
3
商品名,数量
上書き
default=UTF-8 file.encoding=UTF-8
出力から読み取れることを表にまとめます。
| 画面に出た形 | 起きていること | 直し方 |
|---|---|---|
���i�� のように � が並ぶ |
MS932 のファイルを UTF-8 として読んだ | readText(Charset.forName("MS932")) |
譌・譛ャ隱槭 のような漢字と半角カナの並び |
UTF-8 のファイルを MS932 として読んだ | Charset の引数を外す(既定の UTF-8 に戻す) |
| ① や 髙 だけが � になる | Shift_JIS を指定した(丸数字などは MS932 の拡張部分) | Shift_JIS ではなく MS932 を指定する |
| 絵文字が ? になる | 書き込み先の文字コードにその文字が無い | UTF-8 で書くか、書く前に文字を置き換える |
Charset の引数は、readText・readLines・forEachLine・useLines・bufferedReader・writeText・appendText・printWriter のどれにも同じように渡せます。 読むときと書くときで同じ Charset を使うのがコツです。
文字コードの取り違えを例外で受け取る
File.readText は、読めないバイトを置き換え文字(U+FFFD、画面では �)にして読み進めます。取り違えに早く気づきたいときは、デコーダーの動作を REPORT に切り替えます。
import java.io.File
import java.nio.charset.CodingErrorAction
import kotlin.io.path.Path
import kotlin.io.path.readLines
import kotlin.io.path.readText
fun main() {
val file = File("sjis2.txt")
file.writeText("りんご\n", charset("MS932"))
// File.readText は読めないバイトを U+FFFD に置き換えて読み進める
println("File.readText: U+FFFD を含む = " + file.readText().contains('\uFFFD'))
// デコーダーを REPORT にすると、文字コードの取り違えを例外で受け取れる
val decoder = Charsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
try {
java.io.InputStreamReader(file.inputStream(), decoder).use { it.readText() }
} catch (e: Exception) {
println("REPORT 指定: ${e::class.qualifiedName}: ${e.message}")
}
// kotlin.io.path 側の挙動
try {
println("Path.readText: U+FFFD を含む = " + Path("sjis2.txt").readText().contains('\uFFFD'))
} catch (e: Exception) {
println("Path.readText: ${e::class.qualifiedName}: ${e.message}")
}
try {
println("Path.readLines: " + Path("sjis2.txt").readLines())
} catch (e: Exception) {
println("Path.readLines: ${e::class.qualifiedName}: ${e.message}")
}
}
File.readText: U+FFFD を含む = true
REPORT 指定: java.nio.charset.MalformedInputException: Input length = 1
Path.readText: U+FFFD を含む = true
Path.readLines: java.nio.charset.MalformedInputException: Input length = 1
同じ kotlin.io.path でも、readText は置き換えて読み進め、readLines は MalformedInputException で止まりました。 関数によって動きが分かれるので、文字コードが分からないファイルを受け取る処理では、REPORT 指定のデコーダーで一度確かめる方法が役立ちます。
存在しないファイルで出る例外と対処
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import kotlin.io.path.Path
import kotlin.io.path.readText
import kotlin.io.path.writeText
fun show(label: String, block: () -> Unit) {
try {
block()
println("$label: 例外なし")
} catch (e: Exception) {
println("$label\n ${e::class.qualifiedName}: ${e.message}")
}
}
fun main() {
show("File.readText(無いファイル)") { File("nothing.txt").readText() }
show("File.readLines(無いファイル)") { File("nothing.txt").readLines() }
show("File.useLines(無いファイル)") { File("nothing.txt").useLines { it.count() } }
show("Path.readText(無いファイル)") { Path("nothing.txt").readText() }
show("File.readText(フォルダを指定)") { File(".").readText() }
show("File.writeText(親フォルダが無い)") { File("no_dir/a.txt").writeText("x") }
show("Path.writeText(親フォルダが無い)") { Path("no_dir/a.txt").writeText("x") }
show("File.appendText(無いファイル)") { File("new_append.txt").appendText("x\n") }
// 対処1:先に存在を確かめる
val f = File("nothing.txt")
val text1 = if (f.exists()) f.readText() else "(初期値)"
println(text1)
// 対処2:例外を受けて既定値にする
val text2 = try {
f.readText()
} catch (e: FileNotFoundException) {
"(見つからないので初期値)"
} catch (e: IOException) {
"(読み込み中に問題)"
}
println(text2)
// 対処3:runCatching で Result にする
val text3 = runCatching { f.readText() }.getOrElse { "(runCatching の既定値)" }
println(text3)
}
File.readText(無いファイル)
java.io.FileNotFoundException: nothing.txt (No such file or directory)
File.readLines(無いファイル)
java.io.FileNotFoundException: nothing.txt (No such file or directory)
File.useLines(無いファイル)
java.io.FileNotFoundException: nothing.txt (No such file or directory)
Path.readText(無いファイル)
java.nio.file.NoSuchFileException: nothing.txt
File.readText(フォルダを指定)
java.io.FileNotFoundException: . (Is a directory)
File.writeText(親フォルダが無い)
java.io.FileNotFoundException: no_dir/a.txt (No such file or directory)
Path.writeText(親フォルダが無い)
java.nio.file.NoSuchFileException: no_dir/a.txt
File.appendText(無いファイル): 例外なし
(初期値)
(見つからないので初期値)
(runCatching の既定値)
| 例外文 | 原因 | 対処 |
|---|---|---|
| FileNotFoundException: 名前 (No such file or directory) | 読むファイルが無い、または書き込み先の親フォルダが無い | 作業フォルダと相対パスを確かめる。書き込みは parentFile.mkdirs() を先に呼ぶ |
| FileNotFoundException: 名前 (Is a directory) | ファイルではなくフォルダを指定した | isFile で確かめる |
| NoSuchFileException: 名前 | kotlin.io.path 側で同じ状況になった | createDirectories() を先に呼ぶ、exists() で確かめる |
どちらの例外も java.io.IOException の子クラスなので、catch (e: IOException) でまとめて受けられます。 パスが合っているか迷ったら、println(File("memo.txt").absolutePath) で実際に見に行っている場所を表示すると、原因がすぐ分かります。
改行コードと BOM の扱い
Windows で作られたファイルは改行が CRLF(\r\n)で、先頭に BOM(UTF-8 であることを示す3バイト)が付いていることがあります。
import java.io.File
fun String.visible() = replace("\r", "\\r").replace("\n", "\\n").replace("\uFEFF", "<BOM>")
fun main() {
// Windows の改行(CRLF)と BOM 付き UTF-8 のファイルを作る
val bom = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte())
val file = File("win.csv")
file.writeBytes(bom + "id,name\r\n1,apple\r\n2,banana\r\n".toByteArray())
println("readText : " + file.readText().visible())
println("readLines : " + file.readLines().map { it.visible() })
println("split(\"\\n\") : " + file.readText().split("\n").map { it.visible() })
println("lines() : " + file.readText().lines().map { it.visible() })
val header = file.readLines().first()
println("先頭の列名は id か : " + (header.split(",")[0] == "id"))
println("先頭の文字コード : U+%04X".format(header[0].code))
// BOM を取り除く
val clean = file.readText().removePrefix("\uFEFF")
println("removePrefix 後 : " + (clean.lines().first().split(",")[0] == "id"))
// 末尾の改行があるかどうかで readLines の件数は変わらない
File("a.txt").writeText("x\ny\n")
File("b.txt").writeText("x\ny")
println("末尾改行あり: " + File("a.txt").readLines().size + " 件 / なし: " + File("b.txt").readLines().size + " 件")
println("split だと : " + File("a.txt").readText().split("\n").size + " 件")
// 書くときの改行
File("c.txt").printWriter().use { it.println("x") }
println("println の改行 : " + File("c.txt").readText().visible())
println("lineSeparator : " + System.lineSeparator().visible())
// BOM 付きで書く(表計算ソフトに UTF-8 と伝えたいとき)
File("bom_out.csv").writeText("\uFEFF" + "名前,数量\nりんご,3\n")
println("先頭3バイト : " + File("bom_out.csv").readBytes().take(3).joinToString(" ") { "%02X".format(it) })
}
readText : <BOM>id,name\r\n1,apple\r\n2,banana\r\n
readLines : [<BOM>id,name, 1,apple, 2,banana]
split("\n") : [<BOM>id,name\r, 1,apple\r, 2,banana\r, ]
lines() : [<BOM>id,name, 1,apple, 2,banana, ]
先頭の列名は id か : false
先頭の文字コード : U+FEFF
removePrefix 後 : true
末尾改行あり: 2 件 / なし: 2 件
split だと : 3 件
println の改行 : x\n
lineSeparator : \n
先頭3バイト : EF BB BF
| つまずく箇所 | 出力で分かること | こうするとよい |
|---|---|---|
| 行末に \r が残る | split("\n") は \r を残す |
readLines・useLines・forEachLine、または文字列の lines() を使う |
| 最後に空の要素が付く | readText を split や lines() で分けると、末尾の改行の後ろが空文字になる | 行で扱うなら最初から readLines を使う |
| 先頭の列名が一致しない | BOM(U+FEFF)が1文字目に残っている | removePrefix("") で取り除く |
| 書いた改行が環境で変わる | printWriter の println は System.lineSeparator() を使う(今回の環境では \n) | 改行を固定したいときは "\n" を自分で付けて write する |
readLines 系は \n・\r\n のどちらも行の区切りとして扱い、BOM は取り除きません。 見出し行を列名として使う CSV では、BOM の除去を1行入れておくと安定します。
use で閉じ忘れを防ぐ
readText・readLines・forEachLine・useLines・writeText・appendText は、関数の中でファイルを閉じてくれます。自分で閉じる必要があるのは、bufferedReader()・bufferedWriter()・printWriter()・inputStream()・outputStream() など、Reader / Writer / Stream を返す関数です。これらは use と組み合わせます。
import java.io.BufferedReader
import java.io.File
import java.io.Reader
// close されたかを表示する Reader
class LoggingReader(private val inner: Reader, private val name: String) : Reader() {
override fun read(cbuf: CharArray, off: Int, len: Int) = inner.read(cbuf, off, len)
override fun close() {
println(" [$name] close されました")
inner.close()
}
}
fun main() {
val file = File("data.txt")
file.writeText("10\n20\nabc\n40\n")
println("1. use を使う(途中で例外が出ても閉じる)")
try {
BufferedReader(LoggingReader(file.reader(), "use")).use { r ->
var sum = 0
while (true) {
val line = r.readLine() ?: break
sum += line.toInt()
}
println(sum)
}
} catch (e: NumberFormatException) {
println(" 例外: ${e.message}")
}
println("2. use を使わない(例外で close の行まで届かない)")
try {
val r = BufferedReader(LoggingReader(file.reader(), "手動"))
var sum = 0
while (true) {
val line = r.readLine() ?: break
sum += line.toInt()
}
r.close()
} catch (e: NumberFormatException) {
println(" 例外: ${e.message}")
}
println("3. useLines の外に Sequence を持ち出す")
val seq = file.useLines { it }
try {
println(seq.first())
} catch (e: Exception) {
println(" ${e::class.qualifiedName}: ${e.message}")
}
println("4. useLines の中で toList してから返す")
println(" " + file.useLines { it.take(2).toList() })
println("5. 書き込みも use で閉じると、バッファの中身が確実に書き出される")
val w = File("nouse.txt").bufferedWriter()
w.write("hello")
println(" close 前のサイズ: " + File("nouse.txt").length())
w.close()
println(" close 後のサイズ: " + File("nouse.txt").length())
}
1. use を使う(途中で例外が出ても閉じる)
[use] close されました
例外: For input string: "abc"
2. use を使わない(例外で close の行まで届かない)
例外: For input string: "abc"
3. useLines の外に Sequence を持ち出す
java.io.IOException: Stream closed
4. useLines の中で toList してから返す
[10, 20]
5. 書き込みも use で閉じると、バッファの中身が確実に書き出される
close 前のサイズ: 0
close 後のサイズ: 5
- 1 と 2 の差が use の役割です。use はブロックが正常に終わっても例外で抜けても close を呼びます。 2 では close の行に届かず、ファイルが開いたまま残ります。
- 3 のように、useLines のブロックから Sequence をそのまま返すと、使う時点ではもう閉じているので
IOException: Stream closedになります。ブロックの中で toList() や count() まで済ませて、結果だけを返します。 - 5 のとおり、BufferedWriter は close(または flush)するまでファイルに書き出しません。use で囲むと書き漏れを防げます。
よくある質問
Q. readText と readLines はどちらを使えばよいですか? A. 丸ごと1つの文字列として渡したいとき(JSON の解析など)は readText、行ごとに処理したいときは readLines です。どちらも全体をメモリに載せるので、大きなファイルは useLines か forEachLine を選びます。
Q. useLines と forEachLine の違いは何ですか? A. forEachLine は各行に処理をするだけで値を返しません。useLines は Sequence を受け取り、ブロックの最後の式が戻り値になります。「条件に合う行を数える」「先頭の10件だけ取る」のように結果がほしいときは useLines が向いています。
Q. 追記するとき、改行は自動で付きますか?
A. 付きません。appendText は渡した文字列をそのまま足すので、appendText("3行目\n") のように改行を含めます。Path なら appendLines(listOf("3行目")) で行の区切りを付けて追記できます。
Q. 文字化けしたら最初に何を確かめればよいですか? A. � が並ぶなら「UTF-8 以外のファイルを UTF-8 で読んでいる」、漢字と半角カナが並ぶなら「UTF-8 のファイルを MS932 で読んでいる」可能性が高いです。Charset の引数を、ファイルを作った側の文字コードに合わせます。
まとめ
- 小さなファイルは readText / readLines、大きなファイルは useLines / forEachLine。200万行のログでは、readLines だけ使用メモリが 193 MB 増えました。
- 上書きは writeText、追記は appendText。printWriter() と bufferedWriter() は上書きで開きます。
- 文字コードの既定は UTF-8。Shift_JIS のファイルは
Charset.forName("MS932")を読み書きの両方に渡します。 - 無いファイルは FileNotFoundException(Path は NoSuchFileException)。どちらも IOException で受けられ、親フォルダは mkdirs() / createDirectories() で先に作ります。
- readLines 系は CRLF を正しく分け、BOM は残します。
removePrefix("")を1行入れると列名の比較が安定します。 - Reader / Writer を返す関数は use で囲む。useLines の結果は、ブロックの中でリストや件数にしてから返します。
参考・出典
- Kotlin 標準ライブラリ API リファレンス kotlin.io(readText・readLines・useLines・forEachLine・writeText・appendText・printWriter などの定義と説明): https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/
- Kotlin 標準ライブラリ API リファレンス kotlin.io.path(Path の拡張関数の一覧): https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io.path/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin ファイル読み込み・書き込み readText・追記」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。