Java Tips | コレクション:ページング

Java Java
スポンサーリンク

ページングは「長い一覧を“ちょうどいい一枚”に切り分ける」技

業務システムでは、「検索結果が1万件あります」とか普通に起こります。
でも画面に1万件は出せませんよね。

だから「1ページ20件」「3ページ目を表示」みたいに、
長い一覧を“ページ”という単位に切り分けて扱います。

これが「ページング」です。
ここでは「メモリ上にある List を、きれいにページングするユーティリティ」をテーマにします。


ページングの基本概念をコードに落とす

必要な情報は「ページ番号」「ページサイズ」「全件数」

ページングを考えるとき、最低限この3つを意識します。

ページ番号(page):何ページ目か(0始まり or 1始まりを決める)
ページサイズ(size):1ページに何件出すか
全件数(total):全部で何件あるか

そして、「そのページに含まれる要素の範囲」を計算します。

0始まりで考えると、
startIndex = page * size
endIndex = 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として保持している」ことです。
これにより、「ページング結果を誰かが勝手に書き換える」ことを防げます。

三つ目は、「totalPagesceil(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

ここでの重要ポイントは二つです。

一つ目は、「skiplimit を使えば、“Stream 上でページング”ができる」ことです。
skip(page * size) で先頭を飛ばし、limit(size) で必要な件数だけ取ります。

二つ目は、「全件数や全ページ数を知りたい場合は、別途 count() などで数える必要がある」ことです。
DB 連携などでは、「件数取得クエリ」と「ページングクエリ」を分けるのと同じ発想になります。


まとめ:ページングユーティリティで身につけてほしい感覚

ページングは、
単に「subList の使い方を覚える」話ではなく、
「長い一覧を“人間が扱える単位”に切り分ける設計」です。

ページ番号・ページサイズ・全件数から、開始位置と終了位置を正しく計算する。
subList がビューであることを理解し、「独立した List が欲しいときはコピーする」判断をする。
Page<T> のようなクラスに「中身+メタ情報」をまとめて、画面/API側をシンプルにする。
Stream では skiplimit でページングしつつ、全件数は別途数える、という構造を意識する。

あなたのコードのどこかに、
「for 文でインデックスをゴリゴリ計算してページングしている」箇所があれば、
それを一度「ページングユーティリティ+Pageクラス」に置き換えられないか眺めてみてください。

その小さな整理が、
「データ量が増えても落ち着いて“ページ単位”で扱えるエンジニア」への、
確かな一歩になります。

タイトルとURLをコピーしました