- Kotlin
Kotlin 標準入力の書き方 readln・split と高速入力
Kotlin の標準入力は readln() で1行を文字列として読み、toInt() や split(" ") で数値やリストに変換するのが基本です。入力の終わりまで読むときは、null を返す readlnOrNull() を使います。
この記事では、入力の形ごとのコピペ用コードを早見表にまとめ、3つの読み方(readln+split、BufferedReader+StringTokenizer、自前のバイト読み取り)を同じ100万行の入力で測った時間と、出力のまとめ方による時間の差を載せます。空行や末尾の空白で出る例外も、実際の文言と直し方をセットで示します。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10(2コア)。readln と readlnOrNull は Kotlin 1.6 から使える関数です。
Kotlin 標準入力の早見表
| 入力の形 | 入力例 | コード |
|---|---|---|
| 1行に整数が1個 | 3 |
val n = readln().toInt() |
| 1行に文字列が1個 | hello |
val s = readln() |
| 1行に整数が2個 | 10 20 |
val (a, b) = readln().split(" ").map { it.toInt() } |
| 1行に整数が複数 | 1 2 3 |
val xs = readln().split(" ").map { it.toInt() } |
| 型が混ざった1行 | apple 5 |
val (name, countText) = readln().split(" ") のあと countText.toInt() |
| N 行に1個ずつ | 3行の数 | val rows = List(n) { readln().toLong() } |
| N 行 M 列の数の表 | 2行3列 | val grid = List(h) { readln().split(" ").map { it.toInt() } } |
| N 行の文字の盤面 | #.# など |
val board = List(h) { readln().toCharArray() } |
| 個数不明(EOF まで) | 何行でも | val rest = generateSequence(::readlnOrNull).toList() |
| 大きな数 | 3000000000 |
readln().toLong() |
| 小数 | 3.14 |
readln().toDouble() |
| 関数 | 入力が終わっているとき | 使いどころ |
|---|---|---|
readln() |
例外(ReadAfterEOFException) | 行があると分かっている入力 |
readlnOrNull() |
null を返す | 入力の終わりまで読むループ |
readLine() |
null を返す | Kotlin 1.6 より前から使われている書き方。2.4 のコンパイラは readln / readlnOrNull への置き換えを案内する |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin 標準入力の書き方 readln・split と高速入力」
入力の形ごとの完成形コード
早見表のコードを1つのプログラムにまとめました。shapes.kt として保存します。
fun main() {
// 1. 1行に整数が1個
val n = readln().toInt()
// 2. 1行に文字列が1個
val s = readln()
// 3. 1行に整数が2個(空白区切り)
val (a, b) = readln().split(" ").map { it.toInt() }
// 4. 1行に整数が複数(個数は n)
val xs = readln().split(" ").map { it.toInt() }
// 5. 型が混ざった1行(名前と数)
val (name, countText) = readln().split(" ")
val count = countText.toInt()
// 6. N 行に1個ずつ
val rows = List(n) { readln().toLong() }
// 7. N 行 M 列(数の表)
val (h, w) = readln().split(" ").map { it.toInt() }
val grid = List(h) { readln().split(" ").map { it.toInt() } }
// 8. N 行の文字の盤面
val board = List(h) { readln().toCharArray() }
// 9. 個数が分からず、入力の終わり(EOF)まで読む
val rest = generateSequence(::readlnOrNull).toList()
println("n=$n s=$s a=$a b=$b")
println("xs=$xs sum=${xs.sum()}")
println("name=$name count=$count")
println("rows=$rows")
println("h=$h w=$w grid=$grid")
println("board=" + board.map { it.concatToString() } + " board[1][2]=" + board[1][2])
println("rest=$rest")
}
入力ファイル in1.txt の中身です。
3
hello
10 20
1 2 3
apple 5
100
200
300
2 3
1 2 3
4 5 6
#.#
..#
foo
bar
baz
コンパイルして、ファイルを標準入力に流し込みます。-Dstdout.encoding=UTF-8 は日本語の出力を UTF-8 で表示するための指定です。
kotlinc shapes.kt -include-runtime -d shapes.jar
java -Dstdout.encoding=UTF-8 -jar shapes.jar < in1.txt
n=3 s=hello a=10 b=20
xs=[1, 2, 3] sum=6
name=apple count=5
rows=[100, 200, 300]
h=2 w=3 grid=[[1, 2, 3], [4, 5, 6]]
board=[#.#, ..#] board[1][2]=#
rest=[foo, bar, baz]
読み方の考え方は3段階です。
- readln() で1行を String として受け取る。
- 数値なら toInt()・toLong()・toDouble() で変換する。 複数あるなら split(" ") でリストにしてから map で変換する。
- 行数が決まっていれば
List(n) { ... }で繰り返す。 ブロックが n 回呼ばれ、結果がリストになります。
val (a, b) = ... は分解宣言という書き方で、リストの先頭から順に変数へ入れます。5個まで並べられます。
個数が分からない入力を EOF まで読む
行数が先に与えられない入力は、readlnOrNull() が null を返すまで読みます。
fun main() {
// 方法1:while と readlnOrNull
var sum = 0L
var lines = 0
while (true) {
val line = readlnOrNull() ?: break
if (line.isBlank()) continue
sum += line.trim().split(Regex("\\s+")).sumOf { it.toLong() }
lines++
}
println("行数=$lines 合計=$sum")
}
fun main() {
// 方法2:generateSequence で行の Sequence にする
val total = generateSequence(::readlnOrNull)
.filter { it.isNotBlank() }
.flatMap { it.trim().split(Regex("\\s+")) }
.sumOf { it.toLong() }
println("合計=$total")
// 方法3:System.in を最後まで読む
// val all = System.`in`.bufferedReader().readText()
// val lines = System.`in`.bufferedReader().readLines()
}
printf '1 2 3\n\n10 20\n5\n' をパイプで渡した結果です(2行目は空行、3行目は空白が3つ)。
行数=3 合計=41
合計=41
a ?: break は「null なら break」という書き方です。キーボードから入力しているときは、Linux や macOS では Ctrl+D が入力の終わりの合図になります。
空行・末尾の空白で出る例外と対処
入力に空行や余分な空白があると、変換のところで止まります。実際の例外文を並べます。
fun show(label: String, block: () -> Any?) {
try {
println("$label -> ${block()}")
} catch (e: Exception) {
println("$label\n ${e::class.qualifiedName}: ${e.message}")
}
}
fun main() {
// 入力は「空行」「末尾に空白」「空白が2つ」「先頭に空白」「数字以外」「桁あふれ」の6行
show("空行を toInt") { readln().toInt() }
show("末尾に空白 split(\" \")") { readln().split(" ").map { it.toInt() } }
show("空白が2つ split(\" \")") { readln().split(" ").map { it.toInt() } }
show("先頭に空白 toInt") { readln().toInt() }
show("数字以外 toInt") { readln().toInt() }
show("桁あふれ toInt") { readln().toInt() }
show("入力が終わった後の readln") { readln() }
show("入力が終わった後の readlnOrNull") { readlnOrNull() }
show("入力が終わった後の readLine()!!") { readLine()!! }
println("RuntimeException の一種か: " + (runCatching { readln() }.exceptionOrNull() is RuntimeException))
}
入力 in_err.txt は次の6行です(1行目は空行、2行目は末尾に空白、3行目は空白が2つ、4行目は先頭に空白)。
printf '\n1 2 3 \n1 2\n 42\n12a\n3000000000\n' > in_err.txt
java -Dstdout.encoding=UTF-8 -jar err.jar < in_err.txt
空行を toInt
java.lang.NumberFormatException: For input string: ""
末尾に空白 split(" ")
java.lang.NumberFormatException: For input string: ""
空白が2つ split(" ")
java.lang.NumberFormatException: For input string: ""
先頭に空白 toInt
java.lang.NumberFormatException: For input string: " 42"
数字以外 toInt
java.lang.NumberFormatException: For input string: "12a"
桁あふれ toInt
java.lang.NumberFormatException: For input string: "3000000000"
入力が終わった後の readln
kotlin.io.ReadAfterEOFException: EOF has already been reached
入力が終わった後の readlnOrNull -> null
入力が終わった後の readLine()!!
java.lang.NullPointerException: null
RuntimeException の一種か: true
catch しない場合、readln の例外は次のように表示されます(shapes.jar に空の入力 < /dev/null を渡したときの出力)。
Exception in thread "main" kotlin.io.ReadAfterEOFException: EOF has already been reached
at kotlin.io.ConsoleKt.readln(Console.kt:152)
at ShapesKt.main(shapes.kt:3)
at ShapesKt.main(shapes.kt)
| 例外文 | 原因 | 対処 |
|---|---|---|
| NumberFormatException: For input string: "" | 空行を変換した、または split(" ") の結果に空文字が混ざった | trim() してから分ける、split(Regex("\\s+")) を使う、空の要素を filter で除く |
| NumberFormatException: For input string: " 42" | 前後に空白が付いている | trim().toInt() |
| NumberFormatException: For input string: "12a" | 数字以外が混ざっている | toIntOrNull() で null を受けて既定値にする |
| NumberFormatException: For input string: "3000000000" | Int の範囲(約±21億)を超えた | toLong() |
| ReadAfterEOFException: EOF has already been reached | 行が残っていないのに readln() を呼んだ | 読む回数を見直す、readlnOrNull() に替える |
| NullPointerException | readLine()!! が null を受けた |
readln() か readlnOrNull() に替える |
ReadAfterEOFException は RuntimeException の一種です(最後の行の出力で確認できます)。readLine()!! を含むコードを Kotlin 2.4 でコンパイルすると、次の案内が表示されます。
err.kt:19:38: warning: 'fun readLine(): String?' is deprecated. Use `readln()` instead of `readLine()!!`, and `readlnOrNull()` instead of `readLine()`.
直したコードと、同じ入力での結果です。
fun main() {
// 入力は err.kt と同じ6行
readln() // 空行は読み飛ばす
println(readln().trim().split(" ").map { it.toInt() }) // 末尾の空白は trim
println(readln().trim().split(Regex("\\s+")).map { it.toInt() }) // 連続する空白は \s+
println(readln().trim().toInt()) // 先頭の空白も trim
println(readln().toIntOrNull() ?: -1) // 数字以外は toIntOrNull
println(readln().toLong()) // 大きな数は Long
println(readlnOrNull() ?: "(入力の終わり)") // EOF は readlnOrNull
// split() に空白文字の判定を渡す書き方でも、空の要素を除ける
println(" 1 2 3 ".split(" ").filter { it.isNotEmpty() }.map { it.toInt() })
}
[1, 2, 3]
[1, 2]
42
-1
3000000000
(入力の終わり)
[1, 2, 3]
迷ったら readln().trim().split(Regex("\\s+")) と書いておくと、前後の空白と連続する空白の両方に対応できます。
高速入力:3つの方式を100万行で測る
入力が数十万行を超えると、読み方で時間に差が出ます。1行に整数が2個ある行を100万行(約 20 MB)用意し、全部の合計を求める処理で比べました。
入力の生成(Python 3.11):
import random
random.seed(1)
N=1_000_000
with open('big.txt','w') as f:
f.write(f"{N}\n")
for _ in range(N):
f.write(f"{random.randint(-10**9,10**9)} {random.randint(-10**9,10**9)}\n")
import java.io.BufferedReader
import java.io.DataInputStream
import java.io.InputStreamReader
import java.util.StringTokenizer
import kotlin.time.measureTimedValue
// 方式1:readln + split
fun byReadln(): Long {
val n = readln().toInt()
var sum = 0L
repeat(n) {
val (a, b) = readln().split(" ").map { it.toInt() }
sum += a.toLong() + b
}
return sum
}
// 方式2:BufferedReader + StringTokenizer
fun byTokenizer(): Long {
val br = BufferedReader(InputStreamReader(System.`in`))
val n = br.readLine().toInt()
var sum = 0L
repeat(n) {
val st = StringTokenizer(br.readLine())
sum += st.nextToken().toLong() + st.nextToken().toInt()
}
return sum
}
// 方式3:自前のバイト読み取り
class FastReader(stream: java.io.InputStream) {
private val input = DataInputStream(stream)
private val buf = ByteArray(1 shl 16)
private var len = 0
private var ptr = 0
private fun readByte(): Int {
if (ptr == len) {
len = input.read(buf, 0, buf.size)
ptr = 0
if (len <= 0) return -1
}
return buf[ptr++].toInt()
}
fun nextLong(): Long {
var c = readByte()
while (c != -1 && c <= ' '.code) c = readByte() // 空白と改行を読み飛ばす
val negative = c == '-'.code
if (negative) c = readByte()
var x = 0L
while (c >= '0'.code && c <= '9'.code) {
x = x * 10 + (c - '0'.code)
c = readByte()
}
return if (negative) -x else x
}
fun nextInt(): Int = nextLong().toInt()
}
fun byBytes(): Long {
val fr = FastReader(System.`in`)
val n = fr.nextInt()
var sum = 0L
repeat(n) { sum += fr.nextLong() + fr.nextLong() }
return sum
}
fun main(args: Array<String>) {
val (sum, time) = measureTimedValue {
when (args[0]) {
"readln" -> byReadln()
"tokenizer" -> byTokenizer()
else -> byBytes()
}
}
println("${args[0]}: sum=$sum time=${time.inWholeMilliseconds} ms")
}
kotlinc bench.kt -include-runtime -d bench.jar
java -jar bench.jar readln < big.txt
java -jar bench.jar tokenizer < big.txt
java -jar bench.jar bytes < big.txt
1回の実行の出力例です。3方式とも同じ合計になっています。
readln: sum=-1287932221238 time=2926 ms
tokenizer: sum=-1287932221238 time=1151 ms
bytes: sum=-1287932221238 time=297 ms
3回ずつ実行した結果です。
| 方式 | 1回目 | 2回目 | 3回目 | 特徴 |
|---|---|---|---|---|
| readln + split | 2926 ms | 3139 ms | 2817 ms | いちばん短く書ける。10万行くらいまでなら十分 |
| BufferedReader + StringTokenizer | 1151 ms | 1666 ms | 690 ms | 行ごとのリストを作らないぶん速い。書き方も覚えやすい |
| 自前のバイト読み取り | 297 ms | 435 ms | 275 ms | 文字列を作らずに数値へ変換する。コードは長い |
この環境では、readln+split に対して StringTokenizer がおよそ2〜4倍、バイト読み取りがおよそ7〜10倍速い結果でした。 数値はマシンや JVM の状態で変わるので、順番と桁の違いを目安にしてください。
選び方の目安です。
| 入力の量 | おすすめ |
|---|---|
| 数千行まで | readln + split(読みやすさを優先) |
| 10万〜100万行 | BufferedReader + StringTokenizer |
| 100万行を超える、制限時間が厳しい | 自前のバイト読み取り(FastReader をひな形として貼る) |
BufferedReader と readln を同じプログラムで混ぜると、BufferedReader が先読みした分を readln 側が受け取れなくなります。1つのプログラムでは読み方を1種類に揃えます。
高速出力:println の繰り返しとまとめ書きを比べる
出力も、1行ずつ println するか、まとめて出すかで時間が変わります。0 から 999999 までの100万行を出力して測りました。
import java.io.BufferedWriter
import java.io.OutputStreamWriter
import java.io.PrintWriter
import kotlin.time.measureTime
fun main(args: Array<String>) {
val n = 1_000_000
val time = measureTime {
when (args[0]) {
// 方式1:println を n 回
"println" -> repeat(n) { println(it) }
// 方式2:buildString でまとめてから1回だけ出す
"buildString" -> print(buildString { repeat(n) { appendLine(it) } })
// 方式3:joinToString でまとめる
"joinToString" -> println((0 until n).joinToString("\n"))
// 方式4:PrintWriter(バッファ付き)に書いて最後に flush
else -> {
val out = PrintWriter(BufferedWriter(OutputStreamWriter(System.out)))
repeat(n) { out.println(it) }
out.flush()
}
}
}
// 計測結果は標準エラーに出す(標準出力はファイルへ向けるため)
System.err.println("${args[0]}: ${time.inWholeMilliseconds} ms")
}
kotlinc outbench.kt -include-runtime -d outbench.jar
java -jar outbench.jar println > /dev/null
java -jar outbench.jar buildString > /dev/null
java -jar outbench.jar joinToString > /dev/null
java -jar outbench.jar printWriter > /dev/null
| 方式 | 1回目 | 2回目 | 3回目 |
|---|---|---|---|
| println を100万回 | 3046 ms | 3280 ms | 3365 ms |
| buildString でまとめて print | 318 ms | 384 ms | 568 ms |
| joinToString("\n") でまとめて println | 532 ms | 765 ms | 672 ms |
| PrintWriter(バッファ付き)+ flush | 404 ms | 523 ms | 390 ms |
println の繰り返しに比べて、まとめ書きは4〜10倍ほど速くなりました。 println は呼ぶたびに出力先へ書き出すため、回数が多いほど差が開きます。
| 状況 | 書き方 |
|---|---|
| リストを1行ずつ出す | println(list.joinToString("\n")) |
| 空白区切りで1行に出す | println(list.joinToString(" ")) |
| ループの中で条件ごとに出す | buildString { ... appendLine(x) ... } を最後に print |
| 出力がとても多い | PrintWriter に println して、最後に flush() |
PrintWriter を使うときは、最後の flush() を忘れると出力がバッファに残ったまま終わります。 out.use { ... } で囲むか、main の最後に flush() を置きます。
よくある質問
Q. readLine() と readln() はどちらを使えばよいですか?
A. Kotlin 1.6 以降は readln() と readlnOrNull() が用意されています。readLine()!! は readln()、readLine() は readlnOrNull() に置き換えられます。Kotlin 2.4 のコンパイラも同じ置き換えを案内します。
Q. split(" ") と split(Regex("\s+")) はどう違いますか?
A. split(" ") は空白1個ごとに分けるので、空白が2つ続くと間に空文字が入ります。Regex("\\s+") は連続する空白やタブをまとめて1つの区切りとして扱います。先頭や末尾の空白は trim() で落としておきます。
Q. 数を Int と Long のどちらで読めばよいですか? A. 値が約±21億に収まるなら Int、超える可能性があるなら Long です。入力は Int に収まっても、合計や積が超えることがあるので、足し合わせる変数は Long にしておくと安心です。
Q. IDE や手元で入力を試すにはどうすればよいですか?
A. 入力をファイルに保存し、java -jar a.jar < input.txt のようにリダイレクトで渡すと、同じ入力で何度でも試せます。短い入力は printf '3\n1 2 3\n' | java -jar a.jar のようにパイプでも渡せます。
まとめ
- 基本は readln()。数値は toInt() / toLong()、複数の値は
split(" ").map { it.toInt() }で読みます。 - N 行は
List(n) { readln() ... }、個数不明は readlnOrNull() が null を返すまで読みます。 - 空行・余分な空白は NumberFormatException: For input string: "" になります。
trim()とsplit(Regex("\\s+"))で対応できます。 - 行が無いのに readln() を呼ぶと ReadAfterEOFException(RuntimeException の一種)。readlnOrNull() に替えます。
- 100万行の入力では、readln+split が約2.8〜3.1秒、StringTokenizer が約0.7〜1.7秒、バイト読み取りが約0.3〜0.4秒でした。
- 出力は println を繰り返すより、buildString・joinToString・PrintWriter でまとめると4〜10倍ほど速くなりました。
参考・出典
- Kotlin 標準ライブラリ API リファレンス kotlin.io(readln・readlnOrNull・readLine・println の定義、対応バージョン): https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin 標準入力の書き方 readln・split と高速入力」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。