Java | 基礎文法:繰り返しのネスト

Java Java
スポンサーリンク

繰り返しのネストの全体像

「繰り返しのネスト」は、for/while/foreach などのループを入れ子にして、2次元以上のデータ(表、グリッド、組み合わせ)を処理する書き方です。外側のループが“行”、内側のループが“列”のように、階層ごとに役割を分けるのが基本です。強力ですが、複雑化しやすく計算量も増えるため、可読性の維持と早期終了の設計が重要になります。


基本パターンと考え方

二重ループの基本形

二重ループは「外側がひとまとまりの対象、内側がその中身」という役割分担で書きます。

int[][] grid = {
    {1, 2, 3},
    {4, 5, 6}
};
for (int r = 0; r < grid.length; r++) {         // 行
    for (int c = 0; c < grid[r].length; c++) {  // 列
        int v = grid[r][c];
        System.out.println("[" + r + "," + c + "]=" + v);
    }
}
Java

インデックスは「0 以上、サイズ未満」が大原則です。外側の要素ごとに内側の長さが違う(ジャグ配列)場合、必ず行ごとに列の長さを確認します。

foreach で安全に書く

インデックス管理を減らしたいなら foreach を使うと読みやすく、境界ミスも減ります。

for (int[] row : grid) {
    for (int v : row) {
        System.out.print(v + " ");
    }
    System.out.println();
}
Java

重要ポイントの深掘り:計算量と早期終了

計算量の見積もりと限界

二重ループは概ね O(n·m)、三重ループは O(n·m·k) になります。入力サイズが増えると処理時間は掛け算で膨らむため、「本当に全探索が必要か」「前処理で高速化できないか」を最初に考えます。例えば検索なら、ハッシュ構造(Map/Set)を使うことで O(1) 近似の照合に置き換えられます。

// 二重ループの検索(遅い)
for (String a : listA) {
    for (String b : listB) {
        if (a.equals(b)) { /* ... */ }
    }
}
// 高速化:Setへ前処理
var setB = new java.util.HashSet<>(listB);
for (String a : listA) {
    if (setB.contains(a)) { /* ... */ }
}
Java

早期終了とラベル付き break/continue

「条件を満たしたら即終了」できる設計にすると無駄が減ります。ネストを飛び越えるにはラベル付き break が有効です。

search:
for (int r = 0; r < grid.length; r++) {
    for (int c = 0; c < grid[r].length; c++) {
        if (grid[r][c] == 42) {
            System.out.println("found at " + r + "," + c);
            break search; // 外側ループごと終了
        }
    }
}
Java

continue は「今の反復をスキップして次へ」。条件分岐を浅く保つために活用します。


よくある落とし穴と回避策

役割が曖昧なネスト

外側・内側の責務が混ざると読みにくくなります。変数名に文脈を込め、処理を小さなメソッドに分けて「一階層=一責務」にします。

for (Order order : orders) {
    processOrderLines(order.lines());
}
static void processOrderLines(java.util.List<Line> lines) {
    for (Line line : lines) {
        applyDiscount(line);
    }
}
Java

ループ内の重い処理・重複計算

ループの中で毎回同じ計算や I/O を行うと爆発的に遅くなります。前計算(キャッシュ)や外出しを検討します。

// 悪い:毎回正規表現をコンパイル
for (String s : texts) {
    boolean ok = java.util.regex.Pattern.compile("[A-Z]+").matcher(s).matches();
}
// 良い:事前にコンパイル
var pattern = java.util.regex.Pattern.compile("[A-Z]+");
for (String s : texts) {
    boolean ok = pattern.matcher(s).matches();
}
Java

変更を伴う走査の破綻

リストを走査しながら削除すると、インデックスがずれてバグになります。Iterator の remove を使うか、末尾から削除します。

var it = list.iterator();
while (it.hasNext()) {
    if (shouldRemove(it.next())) it.remove();
}
Java

設計の引き出し:ネストを減らすテクニック

フィルタ→変換→集計の段階化

前段で対象を絞り、後段で処理する構造にするとネストが浅くなります。

var activeUsers = new java.util.ArrayList<User>();
for (User u : users) {
    if (!u.isActive()) continue;          // フィルタ
    activeUsers.add(u);
}
for (User u : activeUsers) {
    sendMail(u);                           // 処理
}
Java

マップ化・グルーピング

二重ループの結合条件は「キーでまとめる」ことで 1 ループに還元できます。

var byId = new java.util.HashMap<String, User>();
for (User u : users) byId.put(u.id(), u);
for (Order o : orders) {
    User u = byId.get(o.userId());
    if (u != null) link(o, u);
}
Java

事前ソートで二本指法(two-pointer)

両リストをソートして前から突き合わせると、二重ループでも O(n + m) に近づけられます。

var xs = new java.util.ArrayList<>(listA);
var ys = new java.util.ArrayList<>(listB);
java.util.Collections.sort(xs);
java.util.Collections.sort(ys);
int i = 0, j = 0;
while (i < xs.size() && j < ys.size()) {
    int cmp = xs.get(i).compareTo(ys.get(j));
    if (cmp == 0) { /* マッチ */ i++; j++; }
    else if (cmp < 0) i++; else j++;
}
Java

例題で身につける

例 1: 掛け算表(行×列)

public class Table {
    public static void main(String[] args) {
        for (int i = 1; i <= 9; i++) {
            for (int j = 1; j <= 9; j++) {
                System.out.print((i * j) + "\t");
            }
            System.out.println();
        }
    }
}
Java

例 2: 2D グリッドで隣接セルを数える

public class Neighbors {
    static int countNeighbors(int[][] g, int r, int c) {
        int count = 0;
        for (int dr = -1; dr <= 1; dr++) {
            for (int dc = -1; dc <= 1; dc++) {
                if (dr == 0 && dc == 0) continue;
                int nr = r + dr, nc = c + dc;
                if (nr < 0 || nc < 0 || nr >= g.length || nc >= g[nr].length) continue;
                if (g[nr][nc] == 1) count++;
            }
        }
        return count;
    }
}
Java

境界チェックを先に行い、早期 continue で分岐を浅く保っています。

例 3: 条件一致で早期終了(ラベル付き break)

public class Search {
    public static void main(String[] args) {
        int[][] g = {{1,2,3},{4,42,6}};
        found:
        for (int r = 0; r < g.length; r++) {
            for (int c = 0; c < g[r].length; c++) {
                if (g[r][c] == 42) {
                    System.out.println("found at " + r + "," + c);
                    break found;
                }
            }
        }
    }
}
Java

仕上げのアドバイス(重要部分のまとめ)

ネストは「外側=まとまり、内側=その中身」で役割を分け、インデックスは 0 以上サイズ未満の原則を守る。計算量は掛け算で増えるため、早期終了・前処理(Set/Map への変換、ソートと two-pointer)で無駄を削る。重い処理はループ外へ、削除は Iterator で安全に。読みにくくなったらメソッド分割で一階層=一責務へ——この型が身につけば、ネストは強力さを保ったまま、速く安全に扱えます。

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