KotlinとSequence<T>と巨大なファイル

  • 4
    Like
  • 0
    Comment

問題設定

 100万行の巨大なJSON Linesファイルがあります。

 これを1行ずつオブジェクトに変換し、ある条件を満たす最初の10件のオブジェクトを取得したいです。

ダメな例

 まずはダメな例を示します。

val lineList: List<String> = File("data.jsonlines").readLines()

val result = lineList
    .map { convertToData(it) } // 変換
    .filter { filterData(it) } // 選択
    .take(10) // 最初の10件のみ取得

result.forEach(::println)

 このコードは今回の問題設定ではよくないコードです。

 何がそしてどこがよくないかわかりますか?

ダメな理由

 冒頭のコードはJSON Linesのファイルを読み込んでいる次の部分がよくありません。

val lineList: List<String> = File("data.jsonlines").readLines()

 今回の問題設定では、読み込むファイルは100万行のファイルです。上記のコードでは、100万行のファイルすべてを読み込んでいます。

 100万行、すべてを読み込む必要があるのでしょうか?

 欲しいデータは10件でしたね。もしかしたら100万行の内、先頭1万行だけ読めば見つかるかもしれません。たかだか数10行読んだら見つかるってことだってあるかもしれません。

 巨大なファイルを最後まで読み切らずに、少しずつ必要な分だけファイルを読みこむことを検討しましょう。

SequenceそしてuseLinesを活用しよう

 少しずつ必要な分だけファイルを読みこむのは、次のコードで実現できます。

// 次のコードもよくない点があります。
// このコードをそのまま使わないでください。

val lineSequence: Sequence<String> = File("data.jsonlines")
    .bufferedReader() // FileクラスのbufferedReader拡張関数、BufferedReaderを作成
    .lineSequence()

val result = lineSequence
    .map { convertToData(it) }
    .filter { filterData(it) }
    .take(10)

result.forEach(::println)

 このコードのポイントは、BufferedReaderクラスの拡張関数、lineSequenceです。

 この関数はSequenceを返します。この関数を用いることで、100万行のファイルを最後まで読まず必要な分だけファイルを読み込み処理を行うことができます。

 コメントにも書きましたが、上のコードは良くない点があります。BufferedReaderのCloseable処理をしていないのです。

 bufferedReader().lineSequence()ではなくて、次のようにuseLinesメソッドをぜひ使ってください。

 useLinesメソッドは、Fileクラスの拡張関数で次のようなものです。

  • Sequence<String> -> T 型の関数リテラルを引数に取る
  • ファイルを1行ずつ読み込んだシーケンスSequence<String>、これに引数で渡した関数リテラルを適用した結果が返り値
  • 内部でCloseableの処理を行っている
val result: List<PersonData> = File("data.jsonlines")
    .useLines { lineSequences: Sequence<String> ->
        lineSequences
            .map { convertToData(it) }
            .filter { filterData(it) }
            .take(10)
            .toList()
    }

result.forEach(::println)

まとめ

 巨大なファイルを処理する際は、File#useLinesを活用しましょう。