limit と skip
limit と skip とは
Stream API の中間操作 limit と skip を使い、要素を先頭から数件だけ取り出したり、先頭の数件を飛ばしたりする方法を学んで、配列のスライス処理を Stream で書いてみよう。
ストリームを「ちょうど良い長さ」に切りそろえる
Stream API を使いこなすうえで、filter や map と並んで早い段階で覚えておきたいのが limit と skip です。どちらも Stream の 中間操作 で、流れてくる要素を「先頭から何件まで」「先頭の何件を捨てて」という形で切り出してくれます。データ件数を制御できると、ページ送り (ページネーション) や試験的な抜き取り、無限ストリームの打ち切りなど、現場でよくある場面を一気にこなせるようになります。
中間操作はStreamを別のStreamに変換する操作で、filtermapsorteddistinctlimitskipなどが該当します。最後にcountやcollectなどの終端操作を呼ぶまで実際の計算は走りません。これを遅延評価と呼びます。
たとえば「成績データの上位 3 件だけ表示したい」「最初の 10 件は古いので飛ばして、それ以降を集計したい」といった処理は、配列と for を組み合わせて書くと意外と面倒です。Stream なら stream.skip(10).limit(20) の 1 行で表現できます。今回のレッスンでは、この 2 つの操作の使い分けと、つまずきやすいポイントを Java の流儀でしっかり押さえていきましょう。
limit(n) — 先頭 n 件だけ残す
limit(n) は「Stream の先頭から最大 n 件だけ通過させて、それ以降は捨てる」という中間操作です。データベースの LIMIT 句や、SQL の TOP 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 件を読み飛ばして、それ以降だけを通過させる」操作です。SQL の OFFSET と同じ役割で、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) を通った後の Stream は 76, 64, 52, 40 の 4 件です。n が要素数以上の場合、結果は空の Stream になります。たとえば 2 件しかないリストに skip(2) を掛けると、その後ろの操作 (limit や sum) は何も処理する要素がない状態で動きます。sum() のような数値集計は空でも 0 を返してくれるので、余計な if を書かずに済むのが Stream の良いところです。
ページネーション ─ skip と limit の合わせ技
skip と limit を組み合わせると、「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 Data や JPA の Pageable が裏でこのロジックを組み立てていますが、Stream だけでも十分書けるので、簡易ツールや CLI スクリプトでよく使われます。
注意したいのは
skipの引数の型がlongであることです。(page - 1) * sizeの計算結果がintの範囲を超えそうな場合はキャストやlongリテラルが必要になります。普通のリストならintで十分ですが、ビッグデータ寄りのコードでは意識すると安心です。
無限 Stream を有限に引き戻す
Stream.iterate や Stream.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 か途中で短絡する終端操作 (findFirst や anyMatch など) を組み合わせるのが鉄則です。
よくある間違い
limit と skip は単純そうに見えて、初学者がよく混乱するポイントがいくつかあります。以下を頭の片隅に置いておくとデバッグが速くなります。
skip → limitとlimit → skipの順序を取り違える —stream.skip(2).limit(3)は「先頭 2 件を捨ててから 3 件取る」(3, 4, 5番目を取得)、stream.limit(3).skip(2)は「先頭 3 件に絞ってから 2 件捨てる」(結果は 1 件) と挙動が変わります。ページネーションは必ずskip → limitの順番で書きます- 無限 Stream に
limitを付け忘れる —Stream.iterateやStream.generateから作った無限 Stream をforEachやcountに直接渡すと、プログラムが止まらなくなります。途中で必ずlimitかtakeWhileなどで打ち切りましょう limit(-1)skip(-1)のように負の引数を渡す —limitもskipも負の値はサポートしておらず、実行時にIllegalArgumentExceptionを投げます。page - 1を計算するときに、pageが0や負の値になっていないかを必ず検証してください
もう 1 つ覚えておきたいのは、並列 Stream (parallelStream) で limit を使うと順序保証のために遅くなることがある、という性能上の落とし穴です。普通の Stream ではほぼ気にしなくて構いませんが、Spring Boot のバッチ処理などで大量データを並列処理するときは Stream の特性を確認しましょう。
IllegalArgumentExceptionが出たときは、まずlimitskipの引数が0以上になっているかを疑うのが定石です。値の出どころ (page、リクエストパラメータなど) を遡ってnullや負数を弾くバリデーションを入れましょう。
やってみよう
それでは今回の課題に挑戦してみましょう。Solution.windowSum(int[] arr) メソッドを完成させて、配列を skip(2) で 2 件読み飛ばし、limit(3) で 3 件に切り取り、その合計を int で返します。手順は次のとおりです。
import java.util.stream.IntStream;をファイル先頭に書くIntStream.of(arr)でint[]からIntStreamを作る.skip(2)を呼んで先頭 2 件を捨てる.limit(3)を呼んで残った要素の先頭 3 件だけにする.sum()で合計を取り、その値をreturnする
windowSum(new int[]{10, 20, 30, 40, 50, 60}) の場合は 30 + 40 + 50 = 120、windowSum(new int[]{1, 2, 3, 4, 5}) の場合は 3 + 4 + 5 = 12 になります。配列が短くて skip(2) の時点で要素が残らなければ、sum() は 0 を返します。int[]{1, 2} で 0、int[]{1, 2, 3} で 3 になることを確かめてみてください。
余裕があれば、skip と limit の順番を入れ替えるとどう結果が変わるか、limit(0) を間に挟むとどうなるかも実験してみましょう。Stream の挙動を体で覚えると、次のレッスンで扱う sorted や distinct との組み合わせもすんなり理解できるはずです。
よくある質問
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 つができることを確認しましょう。
- limit/skip の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. limit/skip とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- ファイル先頭で
import java.util.stream.IntStream;を書くこと IntStream.of(arr)でint[]からIntStreamを作り、.skip(2).limit(3)の順で中間操作を呼ぶこと- 終端操作
.sum()を呼んで合計をintでreturnすること (自前の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