- Kotlin
Kotlin Sequenceの使い方|Listとの違いと速さを実測
Sequence は、要素を1件ずつ、必要になったときに作って流す「遅延評価」のコレクション処理です。 List の map・filter は段ごとに新しいリストを作りますが、asSequence() を付けると、1件ずつ全部の段を通し、途中のリストを作りません。first() や take() で途中で止めると、残りの要素は処理されません。
val data = listOf(1, 2, 3, 4)
data.map { it * 10 }.filter { it > 15 }.first() // List
data.asSequence().map { it * 10 }.filter { it > 15 }.first() // Sequence
map と filter にログを入れて動かした結果です。
== List == Sequence
map(1) map(1)
map(2) filter(10)
map(3) map(2)
map(4) filter(20)
filter(10) result=20
filter(20)
filter(30)
filter(40)
result=20
List は「map を全部 → filter を全部」の段ごと、Sequence は「1件目を map → filter、2件目を…」の1件ずつでした。 Sequence は 20 が見つかった時点で止まり、3件目・4件目は処理していません。
測った速さ(中央値)の要約です。詳しい表と測り方は後半にあります。
| 処理(100万件) | List | Sequence | 結果 |
|---|---|---|---|
| map → filter → 最初の10件 | 33.5 ms | 0.0002 ms | Sequence が圧倒的に速い |
| map → filter → sum(全部) | 27.5 ms | 4.6 ms | Sequence が速い |
| map を5段つなぐ → sum | 47.7 ms | 15.3 ms | Sequence が速い |
| map だけ → toList | 14.8 ms | 35.0 ms | List が速い |
確認環境:Kotlin コンパイラ kotlinc 2.4.20、OpenJDK 21.0.10、Linux(2コアの仮想マシン、メモリ 7GB)で、この記事のコードをコンパイル・実行しました。速さは JMH(Java のベンチマーク用ツール)を使わず、System.nanoTime() で何度も測った中央値です(測り方は後述)。Sequence の仕様は kotlinlang.org のドキュメントで確かめました(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin Sequenceの使い方|Listとの違いと速さを実測」
Sequence の作り方
| 作り方 | 例 | 向いている場面 |
|---|---|---|
asSequence() |
list.asSequence() |
今ある List・Set・Range から |
sequenceOf() |
sequenceOf(1, 2, 3) |
要素を直接書く |
generateSequence() |
generateSequence(1) { it * 2 } |
前の値から次の値を作る |
sequence { yield() } |
sequence { yield(1); yieldAll(list) } |
複雑な手順で1件ずつ作る |
Iterator.asSequence() |
iterator.asSequence() |
Iterator しか無いとき(1回しか回せない) |
Stream.asSequence() |
stream.asSequence() |
Java の Stream から |
generateSequence:前の値から次を作る
println(generateSequence(1) { it * 2 }.take(10).toList())
println(generateSequence(1) { if (it < 5) it + 1 else null }.toList())
var n = 3
println(generateSequence { if (n > 0) n-- else null }.toList()) // 種なし:毎回この関数を呼ぶ
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
[1, 2, 3, 4, 5]
[3, 2, 1]
- 第1引数が最初の値(種)、ラムダが「前の値から次の値」を作る
- ラムダが
nullを返すと終わり。nullを返さなければ無限に続くので、take()・takeWhile()・first()で止める - そのため、Sequence の要素に
nullは入れられない(generateSequenceの場合)
sequence { yield() }:手順を書いて1件ずつ返す
fun fib() = sequence {
var a = 0L; var b = 1L
while (true) {
yield(a) // 1件返して、次を求められるまで止まる
val t = a + b; a = b; b = t
}
}
println(fib().take(10).toList())
println(sequence { yield(1); yieldAll(listOf(2, 3)); yieldAll(generateSequence(10) { it + 10 }.take(2)) }.toList())
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
[1, 2, 3, 10, 20]
kotlinlang.org の説明では、yield() は要素を1つ渡して、次の要素が求められるまで sequence {} の処理を止めます。yieldAll() には Iterable・Iterator・別の Sequence を渡せます。無限の Sequence を yieldAll() に渡すと、その後ろの処理は動かないので、最後に置きます。
ページ単位の API を、必要な分だけ読む例です。
fun pages(): Sequence<List<Int>> = sequence {
var page = 0
while (true) {
println(" fetch page=$page")
val items = if (page < 3) List(2) { page * 2 + it } else emptyList() // API の代わり
if (items.isEmpty()) return@sequence
yield(items)
page++
}
}
println(pages().flatten().take(3).toList())
println(pages().flatten().toList())
fetch page=0
fetch page=1
[0, 1, 2]
fetch page=0
fetch page=1
fetch page=2
fetch page=3
[0, 1, 2, 3, 4, 5]
3件だけ欲しいときは、2ページ目までしか読みませんでした。 全部読むときは、空のページ(page=3)が来たところで終わりました。sequence {} の中は普通の関数と同じように書け、return@sequence で終わります。
sequence {} の中で使えるのは yield・yieldAll だけで、delay() などの suspend 関数は呼べません。非同期で1件ずつ流すときは Flow を使います(関連記事「Kotlin Flowの使い方 StateFlowとSharedFlowの違い」で扱っています)。
中間操作と終端操作:終端操作が無いと何も動かない
val s = data.asSequence().map { println(" map($it)"); it * 10 }
println("s = $s")
s = kotlin.sequences.TransformingSequence@448139f0
map のログは1行も出ませんでした。 Sequence の map は「あとで map する」という予約を作るだけで、s は結果ではなく処理の手順です。toList()・sum()・first() などの終端操作を呼んだときに、初めて動きます。
| 種類 | 戻り値 | 主なもの |
|---|---|---|
| 中間操作 | Sequence |
map・filter・take・drop・distinct・sorted・onEach・flatMap・chunked・windowed |
| 終端操作 | 値・コレクション | toList・toSet・first・firstOrNull・sum・sumOf・count・forEach・joinToString・groupBy・any |
ログを出したいだけなら onEach { … }(中間操作)、全部に何かしたいなら forEach { … }(終端操作)です。onEach だけで終えると、何も起きません。
同じ Sequence を2回使うと、2回とも最初から動く
println(s.count()); println(s.count())
map(1)
map(2)
map(3)
map(4)
4
map(1)
map(2)
map(3)
map(4)
4
Sequence は結果を覚えていないので、終端操作のたびに最初から処理し直しました。重い処理を何度も使うときは、一度 toList() してから使います。count() だけなのに map が動いている点にも注意します(map は件数を変えないので、本当は動かさなくても数は分かります)。
1回しか回せない Sequence
constrainOnce() を2回 → java.lang.IllegalStateException: This sequence can be consumed only once.
iterator.asSequence() を2回 → java.lang.IllegalStateException: This sequence can be consumed only once.
useLines { } の中で2回 → java.lang.IllegalStateException: This sequence can be consumed only once.
Java の Stream を2回 → java.lang.IllegalStateException: stream has already been operated upon or closed
Iterator から作った Sequence や、ファイルの行を読む useLines { } の Sequence は、1回しか回せませんでした。kotlinlang.org の Sequence の説明では、Sequence は何度でも回せるのが基本で、1回しか回せないものはドキュメントにそう書かれている、とされています。種(最初の値)を渡さない generateSequence { … } も、1回しか回せない例として挙げられています。
sorted は全件がそろうまで待つ
data.asSequence()
.map { println(" map($it)"); it }
.sortedDescending()
.map { println(" after sort($it)"); it }
.take(1).toList()
map(1)
map(2)
map(3)
map(4)
after sort(4)
result=[4]
sorted の前は全件が流れ、後ろは1件だけでした。kotlinlang.org では、map・filter のように1件ずつ処理できる操作を stateless(状態を持たない)、sorted・distinct のように件数に比例した状態が要る操作を stateful(状態を持つ)と呼んでいます。stateful な操作の前では、Sequence でも全件を処理します。
速さの実測:件数別
4種類の処理を、List・Sequence・Java の Stream で比べました。数字は1回の処理にかかった時間の中央値(ミリ秒)で、カッコ内は1回で確保したメモリの量です。
val data = (1..n).toList()
// full:map → filter → sum(全部たどる)
data.map { it * 2 }.filter { it % 3 == 0 }.sumOf { it.toLong() }
data.asSequence().map { it * 2 }.filter { it % 3 == 0 }.sumOf { it.toLong() }
// first:map → filter → 最初の10件
data.map { it * 2 }.filter { it % 3 == 0 }.take(10).sumOf { it.toLong() }
data.asSequence().map { it * 2 }.filter { it % 3 == 0 }.take(10).sumOf { it.toLong() }
// one:map だけ → toList
data.map { it * 2 }
data.asSequence().map { it * 2 }.toList()
// five:map・filter を5段
data.map { it + 1 }.filter { it % 2 == 0 }.map { it * 3 }.filter { it % 5 != 0 }.map { it - 1 }.sumOf { it.toLong() }
map → filter → sum(全部たどる)
| 件数 | List | Sequence | Stream |
|---|---|---|---|
| 10 | 0.0007 ms(256B) | 0.0003 ms(152B) | 0.0005 ms(440B) |
| 1,000 | 0.017 ms(23KB) | 0.010 ms(15KB) | 0.012 ms(15KB) |
| 100,000 | 1.6 ms(2.6MB) | 0.50 ms(1.6MB) | 1.5 ms(1.6MB) |
| 1,000,000 | 27.5 ms(24MB) | 4.6 ms(16MB) | 10.5 ms(16MB) |
| 10,000,000 | 1039 ms(249MB) | 91 ms(160MB) | 90 ms(160MB) |
map → filter → 最初の10件(途中で止める)
| 件数 | List | Sequence | Stream |
|---|---|---|---|
| 10 | 0.0008 ms | 0.0005 ms | 0.0014 ms |
| 1,000 | 0.017 ms(24KB) | 0.0004 ms(224B) | 0.0010 ms(608B) |
| 100,000 | 1.0 ms(2.6MB) | 0.0002 ms(56B) | 0.0005 ms(552B) |
| 1,000,000 | 33.5 ms(24MB) | 0.0002 ms(56B) | 0.0005 ms(552B) |
| 10,000,000 | 1001 ms(249MB) | 0.0002 ms(56B) | 0.0009 ms(552B) |
map だけ → toList
| 件数 | List | Sequence | Stream |
|---|---|---|---|
| 10 | 0.0002 ms(56B) | 0.0003 ms(176B) | 0.0005 ms(288B) |
| 1,000 | 0.011 ms(19KB) | 0.016 ms(30KB) | 0.007 ms(19KB) |
| 100,000 | 0.51 ms(2.0MB) | 0.66 ms(2.9MB) | 0.81 ms(2.0MB) |
| 1,000,000 | 14.8 ms(20MB) | 35.0 ms(31MB) | 36.3 ms(20MB) |
| 10,000,000 | 719 ms(200MB) | 1232 ms(326MB) | 366 ms(200MB) |
map・filter を5段つなぐ → sum
| 件数 | List | Sequence | Stream |
|---|---|---|---|
| 10 | 0.0012 ms | 0.0010 ms | 0.0026 ms |
| 1,000 | 0.033 ms(49KB) | 0.015 ms(28KB) | 0.012 ms(28KB) |
| 100,000 | 3.0 ms(5.2MB) | 1.5 ms(3.0MB) | 1.3 ms(3.0MB) |
| 1,000,000 | 47.7 ms(51MB) | 15.3 ms(30MB) | 12.7 ms(30MB) |
| 10,000,000 | 1360 ms(503MB) | 182 ms(304MB) | 154 ms(304MB) |
結果から分かること
- 途中で止める処理(
first・take・any・find)は、件数が多いほど Sequence が圧倒的に速い。必要な分しか処理しないので、件数が増えても時間とメモリがほぼ変わらなかった - 段が多いほど Sequence が有利。List は段ごとに途中のリストを作るので、5段では確保したメモリが Sequence の約1.7倍になった
- 1段だけで
toList()するなら List の方が速かった。List のmapは最初から件数が分かる大きさのリストを作るが、Sequence のtoList()は件数が分からないので、リストを伸ばしながら詰める(確保したメモリも約1.5〜1.6倍) - 10件・1,000件では、どれも 0.03 ms 以下。少ない件数では、どちらを選んでも差は体感できない
- Sequence と Java の Stream は、多くの場面で近い結果だった
kotlinlang.org でも、Sequence は途中の結果を作らない分速くなる一方、遅延処理のための手間(オーバーヘッド)があり、少ない件数や単純な処理では、その手間が目立つことがある、と説明されています。
測り方と注意
fun measure(rounds: Int, warm: Int, block: () -> Long): Double {
repeat(warm) { sink += block() } // ウォームアップ(JIT コンパイルを済ませる)
val times = LongArray(rounds)
for (i in 0 until rounds) {
val t0 = System.nanoTime()
sink += block() // 結果を捨てない(最適化で消されないように)
times[i] = System.nanoTime() - t0
}
times.sort()
return times[rounds / 2] / 1_000_000.0 // 中央値(ミリ秒)
}
| 件数 | ウォームアップ | 測った回数 |
|---|---|---|
| 10〜1,000 | 20,000回 | 201回 |
| 100,000 | 300回 | 51回 |
| 1,000,000 | 30回 | 21回 |
| 10,000,000 | 5回 | 11回 |
メモリは com.sun.management.ThreadMXBean.getThreadAllocatedBytes() で、1回の処理の前後の差を取りました。JVM のオプションは -Xms2g -Xmx2g です。
- 同じプログラムをもう一度動かすと、数字は変わった。たとえば full の100万件は、2回目は List 41.6 ms・Sequence 5.4 ms・Stream 7.0 ms だった。1,000万件の Stream は 90 ms → 291 ms だった。GC(使わなくなったメモリの回収)が入るかどうかで大きく変わる。倍率の目安として読み、細かい差は気にしない
- 1,000万件の List が遅いのは、途中のリスト(1段ごとに数十MB)を作り、そのたびに GC が動くため
Intの List なので、要素はIntegerオブジェクト(箱に入った数)。map { it * 2 }のたびに新しいIntegerを作る。プリミティブのIntArrayやIntStreamを使うと、結果は大きく変わる- 正確に測るなら JMH を使う。
System.nanoTime()での自作の計測は、JIT(実行中の最適化)・GC・ほかのプロセスの影響を受けやすい
Java での時間の測り方は、関連記事「Java 処理時間の計測|nanoTimeとcurrentTimeMillis」で扱っています。
使うべき場面の判断表
| 場面 | 選ぶもの | 理由 |
|---|---|---|
first・take・any・find で途中で止める |
Sequence | 残りを処理しない |
| 何十万件以上を、2段以上つなぐ | Sequence | 途中のリストを作らない |
| 無限に続く値・ページ単位の読み込み | Sequence(generateSequence・sequence {}) |
List では表せない |
| ファイルを1行ずつ読む | Sequence(useLines { }) |
全行をメモリに載せない |
| 数十件〜数千件の画面用データ | List | 差は小さく、List の方が読みやすい |
1段だけ(map だけ・filter だけ) |
List | Sequence の方が遅かった |
| 結果を何度も使う | List(または一度 toList()) |
Sequence は毎回やり直す |
sorted・groupBy が中心 |
List | 全件そろうのを待つので、Sequence の利点が小さい |
| 並列に処理したい | Java の parallelStream() かコルーチン |
Sequence に並列の機能は無い |
迷ったら List で書き、件数が多い処理や途中で止める処理だけ asSequence() を足す、という順番が書きやすい形です。List の操作一覧は、関連記事「Kotlin List・Mapの使い方とコレクション操作一覧【早見表】」で扱っています。
Java Stream との対応表
Java の Stream も、1件ずつ処理する遅延評価のしくみです。書き方の対応です。
| やりたいこと | Kotlin Sequence | Java Stream |
|---|---|---|
| List から作る | list.asSequence() |
list.stream() |
| 変換・絞り込み | map・filter |
map・filter |
| 先頭 n 件・n 件飛ばす | take(n)・drop(n) |
limit(n)・skip(n) |
| 条件の間だけ | takeWhile・dropWhile |
takeWhile・dropWhile(Java 9 以降) |
| 平らにする | flatMap・flatten |
flatMap |
| 途中で覗く | onEach |
peek |
| 最初の1件 | first()・firstOrNull() |
findFirst()(Optional) |
| リストにする | toList() |
toList()(Java 16 以降)・collect(Collectors.toList()) |
| 合計 | sumOf { } |
mapToLong(…).sum() |
| グループ分け | groupBy { } |
collect(Collectors.groupingBy(…)) |
| 前の値から次を作る | generateSequence(seed) { } |
Stream.iterate(seed, f) |
| 手順を書いて作る | sequence { yield() } |
対応なし(Stream.generate・Spliterator で作る) |
| まとめて区切る | chunked(3)・windowed(3) |
標準には無い(Java 24 以降の Gatherers で書ける) |
| 2回使う | 多くの Sequence は使える(毎回やり直す) | 使えない(IllegalStateException) |
| 並列 | 無い | parallel() |
| 相互の変換 | seq.asStream() |
stream.asSequence()(Kotlin 側の関数) |
import kotlin.streams.asSequence
import kotlin.streams.asStream
println(listOf(3, 1, 2).asSequence().asStream().sorted().toList())
println(java.util.stream.Stream.of(1, 2, 3).asSequence().map { it + 1 }.toList())
[1, 2, 3]
[2, 3, 4]
chunked・windowed などの Sequence 独自の操作の結果です。
(1..7).asSequence().chunked(3) → [[1, 2, 3], [4, 5, 6], [7]]
(1..5).asSequence().windowed(3) → [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
generateSequence(1) { it + 1 }.map { it * it }.takeWhile { it < 50 } → [1, 4, 9, 16, 25, 36, 49]
Java の Stream の書き方は、関連記事「Java Stream APIの使い方|for文からの書き換え例と注意点」で扱っています。
よくある質問
Q. List の拡張関数と Sequence の拡張関数は、名前が同じですか。
A. ほぼ同じです。kotlinlang.org でも「Sequence は Iterable と同じ関数を持ち、処理の進め方が違う」と説明されています。違いは、Sequence の map・filter が Sequence を返す点(List では List を返す)です。そのため、Sequence の最後には toList() などの終端操作が要ります。
Q. (1..1_000_000) の Range にも asSequence() は要りますか。
A. Range(IntRange)は Iterable なので、map を呼ぶと List を作ります。途中で止める処理や段の多い処理では、Range にも asSequence() を付けると途中のリストを作らずに済みます。
Q. Sequence はスレッドセーフですか。
A. Sequence は1つのスレッドで順に処理するしくみで、並列の機能はありません。複数のスレッドで同じ Iterator 由来の Sequence を回す使い方は避けます。並列処理は関連記事「Kotlinコルーチン入門|launch・async・runBlocking」で扱っています。
Q. ログを入れたら処理の順番が変わって見えます。 A. Sequence では、各段のラムダが要素ごとに交互に呼ばれます。上のログのように「map(1) → filter(10) → map(2) …」と並ぶのは正しい動きです。List と同じ順番を期待していると、ログの読み違いが起きます。
まとめ
- Sequence は1件ずつ全段を通す遅延評価。List は段ごとに途中のリストを作る
- 終端操作(
toList・first・sumなど)を呼ぶまで、何も動かない。2回使うと2回やり直す - 作り方は
asSequence()・generateSequence()・sequence { yield() }。generateSequenceはnullで終わる - 途中で止める処理は、100万件で List 33.5 ms・Sequence 0.0002 ms。段の多い処理でも Sequence が速かった
- 1段だけで
toList()するなら List の方が速かった。少ない件数では差は小さい sorted・distinctの前では、Sequence でも全件を処理する- Java Stream とほぼ同じ考え方。違いは、Sequence は再利用でき、並列が無く、
chunked・sequence {}がある点
参考・出典
確認日はいずれも 2026年10月13日です。
- Kotlin「Sequences」:https://kotlinlang.org/docs/sequences.html
- Kotlin API「Sequence」:https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.sequences/-sequence/
- Kotlin API「generateSequence」:https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.sequences/generate-sequence.html
- Kotlin API「sequence」:https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.sequences/sequence.html
- Java SE 21「java.util.stream」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/package-summary.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin Sequenceの使い方|Listとの違いと速さを実測」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。