limit と skip

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

limit と skip とは

Stream API の中間操作 limit と skip を使い、要素を先頭から数件だけ取り出したり、先頭の数件を飛ばしたりする方法を学んで、配列のスライス処理を Stream で書いてみよう。

ストリームを「ちょうど良い長さ」に切りそろえる

Stream API を使いこなすうえで、filtermap と並んで早い段階で覚えておきたいのが limitskip です。どちらも Stream中間操作 で、流れてくる要素を「先頭から何件まで」「先頭の何件を捨てて」という形で切り出してくれます。データ件数を制御できると、ページ送り (ページネーション) や試験的な抜き取り、無限ストリームの打ち切りなど、現場でよくある場面を一気にこなせるようになります。

中間操作Stream を別の Stream に変換する操作で、filter map sorted distinct limit skip などが該当します。最後に countcollect などの 終端操作 を呼ぶまで実際の計算は走りません。これを 遅延評価 と呼びます。

たとえば「成績データの上位 3 件だけ表示したい」「最初の 10 件は古いので飛ばして、それ以降を集計したい」といった処理は、配列と for を組み合わせて書くと意外と面倒です。Stream なら stream.skip(10).limit(20) の 1 行で表現できます。今回のレッスンでは、この 2 つの操作の使い分けと、つまずきやすいポイントを Java の流儀でしっかり押さえていきましょう。

diagram (will load when visible)

limit(n) — 先頭 n 件だけ残す

limit(n) は「Stream の先頭から最大 n 件だけ通過させて、それ以降は捨てる」という中間操作です。データベースの LIMIT 句や、SQLTOP n と同じイメージで使えます。

Java

import java.util.stream.IntStream; public class LimitDemo { public static void main(String[] args) { int[] scores = {95, 88, 76, 64, 52, 40}; int sum = IntStream.of(scores) .limit(3) // 先頭 3 件 (95, 88, 76) だけ残す .sum(); System.out.println(sum); // 259 } }

limit(3) を通った時点で Stream の中身は 95, 88, 76 の 3 件になります。元の配列に 6 件あっても、4 件目以降は完全に無視されます。引数の n には 0 も指定でき、その場合は空の Stream になります。n が要素数より大きい場合は、すべての要素がそのまま通過します。

limit は「短絡操作」と呼ばれる性質を持っています。必要な数を取り終えた時点でそれ以降の要素は読み込まれません。後で扱う 無限 Stream を有限の世界に引き戻す重要な役割もこの性質のおかげです。

skip(n) — 先頭 n 件を捨てる

対する skip(n) は「Stream の先頭から n 件を読み飛ばして、それ以降だけを通過させる」操作です。SQLOFFSET と同じ役割で、limit と組み合わせてページネーションを作るときの相棒になります。

Java

import java.util.stream.IntStream; public class SkipDemo { public static void main(String[] args) { int[] scores = {95, 88, 76, 64, 52, 40}; int sum = IntStream.of(scores) .skip(2) // 95, 88 を飛ばす .sum(); // 残り 76+64+52+40 = 232 System.out.println(sum); // 232 } }

skip(2) を通った後の Stream76, 64, 52, 40 の 4 件です。n が要素数以上の場合、結果は空の Stream になります。たとえば 2 件しかないリストに skip(2) を掛けると、その後ろの操作 (limitsum) は何も処理する要素がない状態で動きます。sum() のような数値集計は空でも 0 を返してくれるので、余計な if を書かずに済むのが Stream の良いところです。

ページネーション ─ skiplimit の合わせ技

skiplimit を組み合わせると、「p ページ目の n 件」を取り出す典型的なページネーション処理が書けます。p ページ目 (1 始まり) の式は skip((p - 1) * n).limit(n) です。

Java

import java.util.List; public class PageDemo { public static void main(String[] args) { List<String> items = List.of("a", "b", "c", "d", "e", "f", "g"); int page = 2; int size = 3; items.stream() .skip((long) (page - 1) * size) // 3 件飛ばす .limit(size) // 次の 3 件 .forEach(System.out::println); // d, e, f } }

2 ページ目を取り出すには skip(3).limit(3) を呼びます。実務では Spring DataJPAPageable が裏でこのロジックを組み立てていますが、Stream だけでも十分書けるので、簡易ツールや CLI スクリプトでよく使われます。

注意したいのは skip の引数の型が long であることです。(page - 1) * size の計算結果が int の範囲を超えそうな場合はキャストや long リテラルが必要になります。普通のリストなら int で十分ですが、ビッグデータ寄りのコードでは意識すると安心です。

無限 Stream を有限に引き戻す

Stream.iterateStream.generate を使うと、終わりのない 無限 Stream を作れます。これらをそのまま forEach に渡すと永久に止まりません。ここで活躍するのが limit です。

Java

import java.util.stream.Stream; public class InfiniteDemo { public static void main(String[] args) { Stream.iterate(1, x -> x * 2) // 1, 2, 4, 8, 16, ... .limit(5) // 5 件で打ち切り .forEach(System.out::println); // 1, 2, 4, 8, 16 } }

iterate(1, x -> x * 2) は「1 から始めて 2 倍ずつ増やし続ける」無限の数列を作ります。これに limit(5) を掛けると、最初の 5 件 (1, 2, 4, 8, 16) で安全に止まります。limit がなければ JVM はメモリと CPU を食い尽くすまで動き続けてしまうので、無限 Stream には必ず limit か途中で短絡する終端操作 (findFirstanyMatch など) を組み合わせるのが鉄則です。

よくある間違い

limitskip は単純そうに見えて、初学者がよく混乱するポイントがいくつかあります。以下を頭の片隅に置いておくとデバッグが速くなります。

  • skip → limitlimit → skip の順序を取り違えるstream.skip(2).limit(3) は「先頭 2 件を捨ててから 3 件取る」(3, 4, 5 番目を取得)、stream.limit(3).skip(2) は「先頭 3 件に絞ってから 2 件捨てる」(結果は 1 件) と挙動が変わります。ページネーションは必ず skip → limit の順番で書きます
  • 無限 Stream に limit を付け忘れるStream.iterateStream.generate から作った無限 Stream を forEachcount に直接渡すと、プログラムが止まらなくなります。途中で必ず limittakeWhile などで打ち切りましょう
  • limit(-1) skip(-1) のように負の引数を渡すlimitskip も負の値はサポートしておらず、実行時に IllegalArgumentException を投げます。page - 1 を計算するときに、page0 や負の値になっていないかを必ず検証してください

もう 1 つ覚えておきたいのは、並列 Stream (parallelStream) で limit を使うと順序保証のために遅くなることがある、という性能上の落とし穴です。普通の Stream ではほぼ気にしなくて構いませんが、Spring Boot のバッチ処理などで大量データを並列処理するときは Stream の特性を確認しましょう。

IllegalArgumentException が出たときは、まず limit skip の引数が 0 以上になっているかを疑うのが定石です。値の出どころ (page、リクエストパラメータなど) を遡って null や負数を弾く バリデーション を入れましょう。

やってみよう

それでは今回の課題に挑戦してみましょう。Solution.windowSum(int[] arr) メソッドを完成させて、配列を skip(2) で 2 件読み飛ばし、limit(3) で 3 件に切り取り、その合計を int で返します。手順は次のとおりです。

  1. import java.util.stream.IntStream; をファイル先頭に書く
  2. IntStream.of(arr)int[] から IntStream を作る
  3. .skip(2) を呼んで先頭 2 件を捨てる
  4. .limit(3) を呼んで残った要素の先頭 3 件だけにする
  5. .sum() で合計を取り、その値を return する

windowSum(new int[]{10, 20, 30, 40, 50, 60}) の場合は 30 + 40 + 50 = 120windowSum(new int[]{1, 2, 3, 4, 5}) の場合は 3 + 4 + 5 = 12 になります。配列が短くて skip(2) の時点で要素が残らなければ、sum()0 を返します。int[]{1, 2}0int[]{1, 2, 3}3 になることを確かめてみてください。

余裕があれば、skiplimit の順番を入れ替えるとどう結果が変わるか、limit(0) を間に挟むとどうなるかも実験してみましょう。Stream の挙動を体で覚えると、次のレッスンで扱う sorteddistinct との組み合わせもすんなり理解できるはずです。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は 並列ストリーム で、並列ストリーム を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. limit/skip の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. limit/skip とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. ファイル先頭で import java.util.stream.IntStream; を書くこと
  2. IntStream.of(arr)int[] から IntStream を作り、.skip(2) .limit(3) の順で中間操作を呼ぶこと
  3. 終端操作 .sum() を呼んで合計を intreturn すること (自前の for ループで合計しないこと)

入出力例

test-cases.txt

windowSum([10,20,30,40,50,60])120 windowSum([1,2,3,4,5])12 windowSum([1,2])0 windowSum([1,2,3])3 windowSum([5,10,15,20,25,30,35,40])60

ヒント

main.java
main.java
学習モード

メモ

limit と skip

⌘S で保存