はじめに
株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
この記事は、新人〜2年目のJavaエンジニア向けに 「良いコードと悪いコードの違い」 を、現場でよく見る具体例とともに解説していくシリーズの第7回です。
| 回 | テーマ |
|---|---|
| #1 | 命名 |
| #2 | コメントの書き方 |
| #3 | マジックナンバー・定数化 |
| #4 | Null処理 |
| #5 | 早期リターン |
| #6 | メソッド分割 |
| #7(本記事) | ループ処理 |
| #8 | 例外処理 |
| #9 | ログ出力 |
| #10 | クラス設計 |
第7回は ループ処理 です。for (int i = 0; i < list.size(); i++) を新人時代に教わった人ほど、現代Javaに移行できずに 古い書き方を量産 してしまいがちです。拡張for文・Stream APIへの切り替え方を整理します。
この記事のゴール
この記事を読み終わると、以下ができるようになります。
- 古典的な
for文と拡張for文を適切に使い分けられる - 「ループの中で複数のことをやる」アンチパターンを避けられる
- Stream APIで意図を明確に表現できる
「悪いループ」の本当のコスト
新人〜2年目のコードによく見られるのが、こんな構造です。
public Result analyzeScores(List<Score> scores) {
int total = 0;
int max = Integer.MIN_VALUE;
int count = 0;
List<String> failedNames = new ArrayList<>();
String topPerformer = null;
for (int i = 0; i < scores.size(); i++) {
Score score = scores.get(i);
total += score.getValue();
if (score.getValue() > max) {
max = score.getValue();
topPerformer = score.getName();
}
if (score.getValue() < 60) {
failedNames.add(score.getName());
}
count++;
}
return new Result(total, max, count, failedNames, topPerformer);
}
このループには 5つの全く違う処理 が同居しています。
- 合計を計算する
- 最大値を見つける
- 件数をカウントする
- 60点未満の名前を集める
- トップの名前を記録する
書いた本人は「1ループで全部済ませて効率的」と思っているかもしれません。
ですが、このコードには3つのコストがあります。
- 読解コスト:ループの中で何が起きているか把握するのに時間がかかる
- 変更コスト:「平均も出したい」と言われたら、また別の変数が増える
- バグコスト:複数の処理が密結合し、片方の修正が他方に影響する
押さえるべきは3原則
新人〜2年目がまず身につけるべきループの原則は、以下の3つです。
- 拡張for文を優先する
- ループ内の責務を1つに絞る
- Stream APIで宣言的に書く
順番に見ていきます。
原則① 拡張for文を優先する
インデックスが不要なら、拡張for文(拡張 for-each) を使います。
悪い例(古典的なfor)
List<String> names = List.of("田中", "佐藤", "鈴木");
for (int i = 0; i < names.size(); i++) {
System.out.println("こんにちは、" + names.get(i) + "さん");
}
このループでは i は 「何番目か」を表示するためではなく、要素を取得するためだけ に使われています。実質、i 自体には意味がありません。
良い例(拡張for)
List<String> names = List.of("田中", "佐藤", "鈴木");
for (String name : names) {
System.out.println("こんにちは、" + name + "さん");
}
「リストの各要素 name について処理する」という意図がそのままコードに表れています。
インデックス変数 i というノイズが消える だけで、可読性が大きく上がります。
古典的なforを使うべきケース
ただし、以下のような場合は古典的な for が適しています。
| ケース | 例 |
|---|---|
| インデックスそのものを使う | for (int i = 0; i < list.size(); i++) { System.out.println(i + ": " + list.get(i)); } |
| 逆順に処理する | for (int i = list.size() - 1; i >= 0; i--) |
| 2要素ずつ進める | for (int i = 0; i < list.size(); i += 2) |
| 配列の特定範囲だけ処理 | for (int i = 5; i < 10; i++) |
「インデックスが本当に必要か?」を考えて、不要なら拡張forを選びましょう。
原則② ループ内の責務を1つに絞る
ループの中では 1つの処理だけ をします。複数の集計を同時にしない、というルールです。
悪い例(1ループで複数のこと)
int total = 0;
int max = Integer.MIN_VALUE;
List<String> failedNames = new ArrayList<>();
for (Score score : scores) {
total += score.getValue();
if (score.getValue() > max) {
max = score.getValue();
}
if (score.getValue() < 60) {
failedNames.add(score.getName());
}
}
「1回のループで効率的に集計できる」と思いがちですが、現代のJVMは賢く最適化してくれます。可読性を犠牲にして手動で詰め込むメリットはほとんどありません。
良い例(責務ごとにループを分ける)
int total = calculateTotal(scores);
int max = findMax(scores);
List<String> failedNames = findFailedNames(scores);
private int calculateTotal(List<Score> scores) {
int total = 0;
for (Score score : scores) {
total += score.getValue();
}
return total;
}
private int findMax(List<Score> scores) {
int max = Integer.MIN_VALUE;
for (Score score : scores) {
if (score.getValue() > max) {
max = score.getValue();
}
}
return max;
}
private List<String> findFailedNames(List<Score> scores) {
List<String> result = new ArrayList<>();
for (Score score : scores) {
if (score.getValue() < 60) {
result.add(score.getName());
}
}
return result;
}
各メソッドが 1つのループで1つのこと だけをしています。
それぞれを 単体テストでき、再利用でき、読み手が処理を理解しやすい という3拍子が揃います。
「パフォーマンスが落ちるのでは?」への回答
「ループを3回に分けたら3倍遅くなるのでは?」と心配する新人がいます。
実際には:
- 数千件程度のコレクションなら、人間が体感できる差は出ない
- 本当にパフォーマンスが問題なら、まず計測する(推測で最適化しない)
- 可読性を犠牲にした最適化は、最後の手段
「早すぎる最適化は諸悪の根源」(Donald Knuth)という有名な言葉があります。
新人〜2年目のうちは、まず読みやすさを優先 しましょう。
原則③ Stream APIで宣言的に書く
Java 8 以降は Stream API が使えます。「リストを変換・抽出・集計する」処理は、Streamで書くと意図が明確になります。
Stream APIの基本パターン
| パターン | メソッド | 例 |
|---|---|---|
| 絞り込み | filter |
条件に合う要素だけ取り出す |
| 変換 | map |
各要素を別の値に変換する |
| 集約 |
reduce、sum、count
|
合計・件数などにまとめる |
| 並び替え | sorted |
順序を変える |
| リスト化 | collect(toList()) |
結果をListに戻す |
悪い例(命令的:何をしているか分かりにくい)
List<Integer> evenSquares = new ArrayList<>();
for (Integer n : numbers) {
if (n % 2 == 0) {
evenSquares.add(n * n);
}
}
「リストを用意して」「ループで」「if で絞って」「* で変換して」「add で詰める」という 手順 を書いています。読み手は手順を追って意図を逆算する必要があります。
良い例(宣言的:意図がそのまま見える)
List<Integer> evenSquares = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
「偶数だけを抽出(filter)して、二乗に変換(map)して、リストに集める(collect)」という 意図そのもの がコードになっています。
Stream API でよく使うイディオム
// 合計
int total = list.stream().mapToInt(Item::getPrice).sum();
// 平均
double average = list.stream().mapToInt(Item::getScore).average().orElse(0.0);
// 件数
long count = list.stream().filter(Item::isActive).count();
// 最大
int max = list.stream().mapToInt(Item::getValue).max().orElse(0);
// 条件に合う要素を全て取得
List<Item> filtered = list.stream()
.filter(item -> item.getPrice() > 1000)
.collect(Collectors.toList());
// 各要素を文字列に変換してカンマ結合
String csv = list.stream()
.map(Item::getName)
.collect(Collectors.joining(", "));
これらを覚えておくと、ほとんどのループ処理が 数行で済みます。
Stream API を使わない方がよい場面
ただし、以下のケースでは従来のforループの方が読みやすいです。
| ケース | 理由 |
|---|---|
| 副作用(DB更新、メール送信)を伴う処理 |
forEach で副作用は推奨されない |
| 例外を投げる可能性のある処理 | ラムダ内で throws できない |
| ループの途中でループ外の状態を変えたい | Stream は不変前提 |
| パフォーマンスがクリティカル | プリミティブ配列の場合、for のほうが速いことも |
新人〜2年目のうちは、「コレクションを変換・抽出・集計するならStream、それ以外は拡張for」 という指針で十分です。
動作確認:3原則を全部適用したサンプル
3つの原則をすべて適用したコード例です。コピペでそのまま動かせます。
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class LoopDemo {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Java本", 2500, true));
products.add(new Product("SQL本", 3000, false));
products.add(new Product("Git本", 1800, true));
products.add(new Product("Linux本", 4000, true));
// 原則①:拡張for文
System.out.println("--- 全商品 ---");
for (Product product : products) {
System.out.println(product.getName() + ": " + product.getUnitPrice() + "円");
}
// 原則②:ループ内の責務を1つに(合計と最大を別ループで)
int total = calculateTotal(products);
int max = findMaxPrice(products);
System.out.println("合計: " + total + "円");
System.out.println("最高価格: " + max + "円");
// 原則③:Stream API
List<String> inStockNames = products.stream()
.filter(Product::isInStock)
.map(Product::getName)
.collect(Collectors.toList());
System.out.println("在庫あり商品: " + inStockNames);
int inStockTotal = products.stream()
.filter(Product::isInStock)
.mapToInt(Product::getUnitPrice)
.sum();
System.out.println("在庫あり合計: " + inStockTotal + "円");
}
static int calculateTotal(List<Product> products) {
int total = 0;
for (Product product : products) {
total += product.getUnitPrice();
}
return total;
}
static int findMaxPrice(List<Product> products) {
int max = Integer.MIN_VALUE;
for (Product product : products) {
if (product.getUnitPrice() > max) {
max = product.getUnitPrice();
}
}
return max;
}
}
class Product {
private final String name;
private final int unitPrice;
private final boolean isInStock;
Product(String name, int unitPrice, boolean isInStock) { this.name = name; this.unitPrice = unitPrice; this.isInStock = isInStock; }
String getName() { return name; }
int getUnitPrice() { return unitPrice; }
boolean isInStock() { return isInStock; }
}
期待する出力
--- 全商品 ---
Java本: 2500円
SQL本: 3000円
Git本: 1800円
Linux本: 4000円
合計: 11300円
最高価格: 4000円
在庫あり商品: [Java本, Git本, Linux本]
在庫あり合計: 8300円
半日溶かした実話:絡まったループ
1つのループに5つの集計が詰め込まれたメソッドの話です。
public Result analyze(List<Order> orders) {
int total = 0;
int count = 0;
int maxAmount = 0;
String topCustomer = null;
Map<String, Integer> byCategory = new HashMap<>();
List<String> errors = new ArrayList<>();
for (Order order : orders) {
if (order.getStatus() == 1) {
total += order.getAmount();
count++;
if (order.getAmount() > maxAmount) {
maxAmount = order.getAmount();
topCustomer = order.getCustomerName();
}
byCategory.merge(order.getCategory(), order.getAmount(), Integer::sum);
} else if (order.getStatus() == 2) {
errors.add(order.getId() + ": キャンセル済み");
}
}
// ...
}
1つのループの中で5つの集計 をしていました。
このメソッドのバグ修正依頼が来たのですが、関係のない集計同士が 同じ変数を経由して絡まり、修正の影響範囲を特定するのに 半日以上 かかりました。
リファクタリング後はこうなりました(STATUS_COMPLETED は事前にenum化/定数化しておく前提です)。
public Result analyze(List<Order> orders) {
List<Order> completed = filterCompleted(orders);
return new Result(
sumAmount(completed),
completed.size(),
findTopCustomer(completed),
groupByCategory(completed),
findCancelErrors(orders)
);
}
private static final int STATUS_COMPLETED = 1; // 実際は enum 化が望ましい(#3参照)
private List<Order> filterCompleted(List<Order> orders) {
return orders.stream()
.filter(o -> o.getStatus() == STATUS_COMPLETED)
.collect(Collectors.toList());
}
// 各集計メソッドが続く...
行数は増えましたが、各集計が独立してテスト可能 になりました。
1つの集計を変更しても、他の集計には影響しません。
「1ループで全部やる」は、書く瞬間は楽でも、保守する瞬間に地獄を呼びます。
演習問題
難易度の見方
| マーク | 難易度 | 目安 |
|---|---|---|
| ⭐ | 基本 | 原則を覚えれば解ける |
| ⭐⭐ | 応用 | 複数の原則を組み合わせる |
まずは自分で考えてから、模範解答を見てください!
問題1:古典的forを拡張forに書き直す ⭐
次のコードを、拡張for文に書き直してください。
import java.util.List;
import java.util.ArrayList;
public class Sample {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("田中");
names.add("佐藤");
names.add("鈴木");
for (int i = 0; i < names.size(); i++) {
System.out.println("こんにちは、" + names.get(i) + "さん");
}
}
}
模範解答
import java.util.List;
import java.util.ArrayList;
public class Exercise01 {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("田中");
names.add("佐藤");
names.add("鈴木");
for (String name : names) {
System.out.println("こんにちは、" + name + "さん");
}
}
}
期待する出力
こんにちは、田中さん
こんにちは、佐藤さん
こんにちは、鈴木さん
ポイント:
- インデックス
iは使っていないので拡張forに変換できる -
names.get(i)がnameという意味のある変数になる
問題2:ループ内の責務を分離する ⭐
次のコードは1ループで「合計」と「最大値」を計算しています。
ループを2つのメソッドに分割してください。
import java.util.List;
import java.util.ArrayList;
public class Sample {
public static void main(String[] args) {
List<Integer> scores = new ArrayList<>();
scores.add(80);
scores.add(60);
scores.add(90);
scores.add(70);
int total = 0;
int max = Integer.MIN_VALUE;
for (int score : scores) {
total += score;
if (score > max) {
max = score;
}
}
System.out.println("合計: " + total);
System.out.println("最高点: " + max);
}
}
模範解答
import java.util.List;
import java.util.ArrayList;
public class Exercise02 {
public static void main(String[] args) {
List<Integer> scores = new ArrayList<>();
scores.add(80);
scores.add(60);
scores.add(90);
scores.add(70);
int total = calculateTotal(scores);
int max = findMax(scores);
System.out.println("合計: " + total);
System.out.println("最高点: " + max);
}
static int calculateTotal(List<Integer> scores) {
int total = 0;
for (int score : scores) {
total += score;
}
return total;
}
static int findMax(List<Integer> scores) {
int max = Integer.MIN_VALUE;
for (int score : scores) {
if (score > max) {
max = score;
}
}
return max;
}
}
期待する出力
合計: 300
最高点: 90
ポイント:
- 合計と最大値はそれぞれ独立した集計(責務)
- 各メソッドが単体テスト可能になる
-
mainメソッドが「業務フロー」を表すだけになり、抽象度が揃う
問題3:Stream APIで書き直す ⭐
次のコードを、Stream APIを使って書き直してください。
import java.util.List;
import java.util.ArrayList;
public class Sample {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
// 偶数だけを抽出して、二乗してリストにする
List<Integer> evenSquares = new ArrayList<>();
for (Integer n : numbers) {
if (n % 2 == 0) {
evenSquares.add(n * n);
}
}
System.out.println("偶数の二乗: " + evenSquares);
}
}
模範解答
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class Exercise03 {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
List<Integer> evenSquares = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("偶数の二乗: " + evenSquares);
}
}
期待する出力
偶数の二乗: [4, 16]
ポイント:
- 「偶数を抽出(filter)」「二乗に変換(map)」「リスト化(collect)」と意図がそのままコードに
- 一時変数
evenSquaresの宣言・初期化が消える -
ifのネストやaddの手順が不要になる
問題4:複合的なStream処理 ⭐⭐
次のコードを、Stream APIを使ってリファクタリングしてください。
仕様:
- 「開発部かつ年収500万以上」の社員名をソートして取得する
- 「営業部」の社員の平均年収を計算する
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
public class Sample {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("田中", 30, 5000000, "営業"));
employees.add(new Employee("佐藤", 25, 4500000, "開発"));
employees.add(new Employee("鈴木", 40, 7000000, "営業"));
employees.add(new Employee("高橋", 35, 6500000, "開発"));
employees.add(new Employee("山田", 28, 4000000, "営業"));
// 開発部・年収500万以上の名前リスト
List<String> highSalaryDevNames = new ArrayList<>();
for (Employee e : employees) {
if (e.getDepartment().equals("開発") && e.getSalary() >= 5000000) {
highSalaryDevNames.add(e.getName());
}
}
Collections.sort(highSalaryDevNames);
System.out.println("開発部・年収500万以上: " + highSalaryDevNames);
// 営業部の平均年収
int total = 0;
int count = 0;
for (Employee e : employees) {
if (e.getDepartment().equals("営業")) {
total += e.getSalary();
count++;
}
}
long avg = count == 0 ? 0 : total / count;
System.out.println("営業部の平均年収: " + avg + "円");
}
}
class Employee {
private final String name;
private final int age;
private final int salary;
private final String department;
Employee(String name, int age, int salary, String department) {
this.name = name; this.age = age; this.salary = salary; this.department = department;
}
String getName() { return name; }
int getAge() { return age; }
int getSalary() { return salary; }
String getDepartment() { return department; }
}
模範解答
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class Exercise04 {
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("田中", 30, 5000000, "営業"));
employees.add(new Employee("佐藤", 25, 4500000, "開発"));
employees.add(new Employee("鈴木", 40, 7000000, "営業"));
employees.add(new Employee("高橋", 35, 6500000, "開発"));
employees.add(new Employee("山田", 28, 4000000, "営業"));
List<String> highSalaryDevNames = employees.stream()
.filter(e -> e.getDepartment().equals("開発"))
.filter(e -> e.getSalary() >= 5000000)
.map(Employee::getName)
.sorted()
.collect(Collectors.toList());
System.out.println("開発部・年収500万以上: " + highSalaryDevNames);
double avgSalary = employees.stream()
.filter(e -> e.getDepartment().equals("営業"))
.mapToInt(Employee::getSalary)
.average()
.orElse(0.0);
System.out.println("営業部の平均年収: " + (long) avgSalary + "円");
}
}
class Employee {
private final String name;
private final int age;
private final int salary;
private final String department;
Employee(String name, int age, int salary, String department) {
this.name = name; this.age = age; this.salary = salary; this.department = department;
}
String getName() { return name; }
int getAge() { return age; }
int getSalary() { return salary; }
String getDepartment() { return department; }
}
期待する出力
開発部・年収500万以上: [高橋]
営業部の平均年収: 5333333円
改善ポイント
| 元のコード | 改善後 | 効果 |
|---|---|---|
for + if でフィルタ |
.filter() を2回 |
「絞り込み」が明示 |
名前リストを add
|
.map(Employee::getName) |
「変換」が明示 |
Collections.sort で別途ソート |
.sorted() |
一連の流れに統合 |
total / count で平均計算 |
.mapToInt().average() |
平均が標準API |
| 一時変数(total, count) | なし | 状態がフラットに |
ポイント:
- 仕様が「条件A and B でフィルタ → 名前を取り出す → 並び替え」という流れ通りにコードに表れる
- 営業部の平均年収は
average()の戻り値がOptionalDoubleなのでorElse(0.0)でデフォルト指定 - 平均は
(long)キャストで小数点以下を切り捨て
まとめ
新人〜2年目が押さえるべきループ処理の3原則は、以下の3つです。
-
拡張for文を優先する:インデックスが不要なら
for-each - ループ内の責務を1つに絞る:1ループで複数の集計をしない
- Stream APIで宣言的に書く:filter / map / collect で意図を明示
「1ループで効率的に」は錯覚です。
現代のJVMは賢く、可読性を犠牲にした手動最適化のメリットはほぼありません。
読みやすさを優先する ことが、結果として保守可能性とテスト可能性を高めます。
次回予告
次回(#8)は 「例外処理」 を扱います。
- catchで握り潰してはいけない理由
- 業務例外とシステム例外の区別
- try-with-resources の使い方
を、Before / After 形式で解説していきます。
参考
- Stream(Oracle公式API)
- Effective Java 第3版 - 項目45「Streamを賢明に使用する」(Joshua Bloch, ピアソン・エデュケーション)
- リーダブルコード 第8章「巨大な式を分割する」(オライリー・ジャパン)
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!