ページングは「長い一覧を“ちょうどいい一枚”に切り分ける」技
業務システムでは、「検索結果が1万件あります」とか普通に起こります。
でも画面に1万件は出せませんよね。
だから「1ページ20件」「3ページ目を表示」みたいに、
長い一覧を“ページ”という単位に切り分けて扱います。
これが「ページング」です。
ここでは「メモリ上にある List を、きれいにページングするユーティリティ」をテーマにします。
ページングの基本概念をコードに落とす
必要な情報は「ページ番号」「ページサイズ」「全件数」
ページングを考えるとき、最低限この3つを意識します。
ページ番号(page):何ページ目か(0始まり or 1始まりを決める)
ページサイズ(size):1ページに何件出すか
全件数(total):全部で何件あるか
そして、「そのページに含まれる要素の範囲」を計算します。
0始まりで考えると、startIndex = page * sizeendIndex = min(startIndex + size, total)
という形になります。
この計算を毎回手書きするのは面倒なので、ユーティリティに閉じ込めます。
List をページングするユーティリティの基本形
subList を使って「切り出す」
まずは、List<T> をページングするシンプルなユーティリティです。
import java.util.Collections;
import java.util.List;
public final class Paging {
private Paging() {}
public static <T> List<T> page(List<T> list, int page, int size) {
if (list == null || list.isEmpty()) {
return List.of();
}
if (size <= 0) {
return List.of();
}
int total = list.size();
int fromIndex = page * size;
if (fromIndex >= total || page < 0) {
return List.of(); // そのページには何もない
}
int toIndex = Math.min(fromIndex + size, total);
return list.subList(fromIndex, toIndex);
}
}
Java使い方の例です。
List<Integer> numbers = List.of(1,2,3,4,5,6,7,8,9,10);
List<Integer> page0 = Paging.page(numbers, 0, 3); // [1,2,3]
List<Integer> page1 = Paging.page(numbers, 1, 3); // [4,5,6]
List<Integer> page3 = Paging.page(numbers, 3, 3); // [10]
List<Integer> page4 = Paging.page(numbers, 4, 3); // []
Javaここでの重要ポイントは三つです。
一つ目は、「ページ番号を0始まりで扱っている」ことです。
内部計算がシンプルになるので、ユーティリティ内部は0始まりにしておくのが楽です(画面表示は1始まりでもOK)。
二つ目は、「fromIndex が全件数以上なら“そのページには何もない”として空 List を返している」ことです。
これにより、「存在しないページを指定しても例外にならず、自然に“空ページ”として扱えます。
三つ目は、「subList(from, to) を使って“ビュー”を返している」ことです。subList は元の List と中身を共有するので、「ビューでいいのか」「独立した List が欲しいのか」を次で深掘りします。
subList の注意点と「独立したページList」が欲しい場合
subList はビュー(元の List と連動する)
subList が返すのは「ビュー」です。
つまり、元の List を変更するとページ側も変わりますし、
ページ側で要素を削除すると元の List にも影響します。
List<Integer> numbers = new ArrayList<>(List.of(1,2,3,4,5));
List<Integer> page0 = numbers.subList(0, 2); // [1,2]
numbers.set(0, 99);
System.out.println(page0); // [99,2]
Java業務的に「ページング結果は“その時点のスナップショット”として扱いたい」ことが多いので、
独立した List にコピーして返す方が安全なことが多いです。
import java.util.ArrayList;
import java.util.List;
public static <T> List<T> pageCopy(List<T> list, int page, int size) {
List<T> view = page(list, page, size);
return new ArrayList<>(view); // 中身をコピー
}
Javaここでの重要ポイントは、
「ビューでいい場面(読み取り専用)と、独立した List が欲しい場面(後で変更するかも)を意識して使い分ける」ことです。
ページ情報をまとめたクラスを用意する
「中身だけ」ではなく「メタ情報」も一緒に返す
実務では、「ページの中身」だけでなく、
「全件数」「全ページ数」「今何ページ目か」などの情報も一緒に欲しくなります。
そこで、ページング結果を表すクラスを用意します。
import java.util.List;
public final class Page<T> {
private final List<T> content;
private final int page; // 0始まり
private final int size;
private final int totalElements;
private final int totalPages;
public Page(List<T> content, int page, int size, int totalElements) {
this.content = List.copyOf(content);
this.page = page;
this.size = size;
this.totalElements = totalElements;
this.totalPages = size == 0 ? 0 :
(int) Math.ceil((double) totalElements / size);
}
public List<T> getContent() { return content; }
public int getPage() { return page; }
public int getSize() { return size; }
public int getTotalElements() { return totalElements; }
public int getTotalPages() { return totalPages; }
public boolean hasNext() { return page + 1 < totalPages; }
public boolean hasPrevious() { return page > 0; }
}
Javaユーティリティ側でこれを返すようにします。
public static <T> Page<T> paginate(List<T> list, int page, int size) {
if (list == null) {
list = List.of();
}
List<T> content = pageCopy(list, page, size);
return new Page<>(content, page, size, list.size());
}
Java使い方のイメージです。
Page<Integer> p = Paging.paginate(numbers, 1, 3);
System.out.println(p.getContent()); // [4,5,6]
System.out.println(p.getTotalElements()); // 10
System.out.println(p.getTotalPages()); // 4
System.out.println(p.hasNext()); // true
Javaここでの重要ポイントは三つです。
一つ目は、「ページングに必要なメタ情報(totalElements, totalPages, hasNext など)を一箇所にまとめている」ことです。
画面側やAPIレスポンス側は、この Page だけ見ればページング情報が揃います。
二つ目は、「List.copyOf で中身をコピーし、不変Listとして保持している」ことです。
これにより、「ページング結果を誰かが勝手に書き換える」ことを防げます。
三つ目は、「totalPages を ceil(total / size) で計算している」ことです。
10件を3件ずつなら、10 / 3 = 3.333... → 切り上げて4ページ、という計算になります。
Stream と組み合わせたページング
「全部 List にしてから切る」か「Stream の段階で絞る」か
メモリ上にすでに List があるなら、ここまでのやり方で十分です。
一方、「Stream で処理している途中でページングしたい」こともあります。
例えば、「ID 昇順にソートした結果のうち、3ページ目だけ欲しい」など。
List<Integer> page =
numbers.stream()
.sorted()
.skip((long) page * size)
.limit(size)
.toList();
Javaここでの重要ポイントは二つです。
一つ目は、「skip と limit を使えば、“Stream 上でページング”ができる」ことです。skip(page * size) で先頭を飛ばし、limit(size) で必要な件数だけ取ります。
二つ目は、「全件数や全ページ数を知りたい場合は、別途 count() などで数える必要がある」ことです。
DB 連携などでは、「件数取得クエリ」と「ページングクエリ」を分けるのと同じ発想になります。
まとめ:ページングユーティリティで身につけてほしい感覚
ページングは、
単に「subList の使い方を覚える」話ではなく、
「長い一覧を“人間が扱える単位”に切り分ける設計」です。
ページ番号・ページサイズ・全件数から、開始位置と終了位置を正しく計算する。subList がビューであることを理解し、「独立した List が欲しいときはコピーする」判断をする。Page<T> のようなクラスに「中身+メタ情報」をまとめて、画面/API側をシンプルにする。
Stream では skip+limit でページングしつつ、全件数は別途数える、という構造を意識する。
あなたのコードのどこかに、
「for 文でインデックスをゴリゴリ計算してページングしている」箇所があれば、
それを一度「ページングユーティリティ+Pageクラス」に置き換えられないか眺めてみてください。
その小さな整理が、
「データ量が増えても落ち着いて“ページ単位”で扱えるエンジニア」への、
確かな一歩になります。
