はじめに
株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
この記事は、新人〜2年目のJavaエンジニア向けに 「良いコードと悪いコードの違い」 を、現場でよく見る具体例とともに解説していくシリーズの第5回です。
| 回 | テーマ |
|---|---|
| #1 | 命名 |
| #2 | コメントの書き方 |
| #3 | マジックナンバー・定数化 |
| #4 | Null処理 |
| #5(本記事) | 早期リターン |
| #6 | メソッド分割 |
| #7 | ループ処理 |
| #8 | 例外処理 |
| #9 | ログ出力 |
| #10 | クラス設計 |
第5回は 早期リターン(Early Return) です。前回(#4 Null処理)でも触れた手法ですが、null以外にも 業務ルールでの除外 や バリデーション など、早期リターンが効くシーンは多くあります。ネストの深いコードを構造的に浅くする ための原則を整理します。
この記事のゴール
この記事を読み終わると、以下ができるようになります。
- 「ネストが深いコード」がなぜ読みにくいかを説明できる
- ガード節を使ってネストを平坦化できる
-
elseを消して、正常系を最後に書く構造を作れる - ループ内で
continue/breakを使い分けられる
「ネストが深いコード」の本当のコスト
新人のコードによく見られるのが、こんな構造です。
public String process(Order order) {
if (order != null) {
if (order.isLoggedIn()) {
if (order.isInStock()) {
if (!order.isCanceled()) {
// 本当にやりたい処理(ようやくここ)
int finalPrice = (int) (order.getPrice() * 0.9);
return "発送処理開始(最終価格: " + finalPrice + "円)";
} else {
return "キャンセル済みです";
}
} else {
return "在庫切れです";
}
} else {
return "ログインしてください";
}
} else {
return "注文がありません";
}
}
書いた本人は順番に if を重ねただけのつもりです。
ですが、読み手は 「いま自分がどの分岐の中にいるのか」を頭の中でスタックに積みながら 読まなければなりません。
ネストが深いコードには、3つのコストがあります。
- 読解コスト:「今どの条件下にいるか」を頭の中で常に追う必要がある
- 保守コスト:分岐を1つ追加するだけで、ネストがさらに深くなる
-
バグコスト:
elseの対応関係を読み間違えて、想定外のパスを通る
特に厄介なのが バグコスト です。
ネストが4段、5段と深くなると、「この else はどの if に対応しているのか」を読み違える事故が起こります。
押さえるべきは3原則
新人〜2年目がまず身につけるべき早期リターンの原則は、以下の3つです。
- ガード節で前提条件を最初に弾く
- else を消す(早期リターンで分岐を平坦化)
- ループ内では continue / break で異常系を弾く
順番に見ていきます。
原則① ガード節で前提条件を最初に弾く
ガード節(Guard Clause) とは、メソッドの冒頭で 「処理を続行できないケース」を先に return する パターンです。
悪い例(ネストが4段)
public String process(Order order) {
if (order != null) {
if (order.isLoggedIn()) {
if (order.isInStock()) {
if (!order.isCanceled()) {
int finalPrice = (int) (order.getPrice() * 0.9);
return "発送処理開始(最終価格: " + finalPrice + "円)";
} else {
return "キャンセル済みです";
}
} else {
return "在庫切れです";
}
} else {
return "ログインしてください";
}
} else {
return "注文がありません";
}
}
良い例(ガード節)
public String process(Order order) {
if (order == null) {
return "注文がありません";
}
if (!order.isLoggedIn()) {
return "ログインしてください";
}
if (!order.isInStock()) {
return "在庫切れです";
}
if (order.isCanceled()) {
return "キャンセル済みです";
}
int finalPrice = (int) (order.getPrice() * 0.9);
return "発送処理開始(最終価格: " + finalPrice + "円)";
}
メソッドの長さはほぼ同じですが、読みやすさは劇的に違います。
ガード節の特徴:
- ネストが0段 に保たれる
- 「メソッドの上から順に 「失格条件」を並べていく」という単純な構造
- 最後に 「正常系の処理」 だけが残る
ガード節の典型パターン
| 弾く対象 | ガード節の例 |
|---|---|
| null引数 | if (user == null) return defaultValue; |
| 不正な引数 | if (price < 0) throw new IllegalArgumentException(...); |
| 業務ルールでの除外 | if (!user.isActive()) return "停止中のユーザーです"; |
| 必要な権限の不足 | if (!user.hasPermission(...)) return "権限がありません"; |
| 必須フィールドの不足 | if (order.getCoupon() == null) return order.getPrice(); |
ガード節で「正常系以外」をすべて弾ききると、メソッドの最後には 本当にやりたい処理だけが残ります。
原則② else を消す(早期リターンで分岐を平坦化)
if と else の片方で return している場合、else は 書く必要がありません。
悪い例(不要なelse)
public String classifyAge(int age) {
if (age < 0) {
return "不正な年齢";
} else {
if (age < 20) {
return "未成年";
} else {
if (age < 65) {
return "成人";
} else {
return "高齢者";
}
}
}
}
if ブロックの中で return しているので、その後のコードは else を書かなくても実行されません。
それなのに else を書くと、ネストが無駄に深くなります。
良い例(elseを削除)
public String classifyAge(int age) {
if (age < 0) {
return "不正な年齢";
}
if (age < 20) {
return "未成年";
}
if (age < 65) {
return "成人";
}
return "高齢者";
}
ネストが0段 になり、上から順に読むだけで分類ルールが理解できます。
特に 最後の return "高齢者" は else の中ではなく、メソッド直下に書く のがポイントです。
三項演算子も有効
シンプルな2択なら、三項演算子で1行にしても良いです。
// if-else を使った冗長な書き方
String label;
if (user.isAdmin()) {
label = "管理者";
} else {
label = "一般";
}
// 三項演算子で1行に
String label = user.isAdmin() ? "管理者" : "一般";
ただし、条件が複雑な場合は無理に三項演算子にしない ようにしましょう。三項演算子のネストは、if-else のネストよりも読みにくくなります。
原則③ ループ内では continue / break で異常系を弾く
ループ内でも、ガード節と同じ考え方が使えます。continue や break で異常系を弾けば、ネストを浅く保てます。
悪い例(ループ内のネスト)
public int sumPositive(List<Integer> nums) {
int total = 0;
for (Integer n : nums) {
if (n != null) {
if (n > 0) {
total += n;
}
}
}
return total;
}
ループ内で if が2段ネストしています。
処理対象が増えると、さらにネストが深くなります。
良い例(continueで弾く)
public int sumPositive(List<Integer> nums) {
int total = 0;
for (Integer n : nums) {
if (n == null) {
continue;
}
if (n <= 0) {
continue;
}
total += n;
}
return total;
}
continue で異常系を弾くと、ループ本体には「本当にやりたい処理」だけ が残ります。
これは「ガード節のループ版」と言える書き方です。
continue / break / return の使い分け
| 使う場面 | キーワード | 効果 |
|---|---|---|
| 「この要素はスキップして次の要素へ」 | continue |
ループの次のイテレーションに進む |
| 「ループ自体を抜ける」 | break |
ループから抜けて次の処理へ |
| 「メソッド自体を抜ける」 | return |
メソッド全体を終了する |
ループ内で 「条件を満たす要素を1つでも見つけたら処理を抜けたい」 ようなケースでは、break や return も活用します。
public boolean hasAdmin(List<User> users) {
for (User user : users) {
if (user.isAdmin()) {
return true; // 見つかった瞬間にメソッドを抜ける
}
}
return false;
}
ここで「全員チェックしてから return」ではなく、最初に1人見つけた時点で return するのが効率的かつ読みやすいパターンです。
動作確認:3原則を全部適用したサンプル
3つの原則をすべて適用したコード例です。コピペでそのまま動かせます。
import java.util.List;
import java.util.ArrayList;
public class EarlyReturnDemo {
public static void main(String[] args) {
// 原則①:ガード節で前提条件を弾く
System.out.println(applyDiscount(new Order(1000, true, false)));
System.out.println(applyDiscount(new Order(1000, false, false)));
System.out.println(applyDiscount(new Order(1000, true, true)));
System.out.println(applyDiscount(null));
System.out.println("---");
// 原則②:elseを消して早期リターン
System.out.println(classifyAge(25));
System.out.println(classifyAge(15));
System.out.println(classifyAge(70));
System.out.println("---");
// 原則③:ループ内でcontinue
List<Integer> nums = new ArrayList<>();
nums.add(1);
nums.add(2);
nums.add(-3);
nums.add(4);
nums.add(0);
System.out.println("正の合計: " + sumPositive(nums));
}
static int applyDiscount(Order order) {
if (order == null) {
return 0;
}
if (!order.isLoggedIn()) {
return order.getPrice();
}
if (order.isCanceled()) {
return 0;
}
return (int) (order.getPrice() * 0.9);
}
static String classifyAge(int age) {
if (age < 0) {
return "不正な年齢";
}
if (age < 20) {
return "未成年";
}
if (age < 65) {
return "成人";
}
return "高齢者";
}
static int sumPositive(List<Integer> nums) {
int total = 0;
for (Integer n : nums) {
if (n == null) {
continue;
}
if (n <= 0) {
continue;
}
total += n;
}
return total;
}
}
class Order {
private final int price;
private final boolean isLoggedIn;
private final boolean isCanceled;
Order(int price, boolean isLoggedIn, boolean isCanceled) {
this.price = price;
this.isLoggedIn = isLoggedIn;
this.isCanceled = isCanceled;
}
int getPrice() { return price; }
boolean isLoggedIn() { return isLoggedIn; }
boolean isCanceled() { return isCanceled; }
}
期待する出力
900
1000
0
0
---
成人
未成年
高齢者
---
正の合計: 7
よくある反論:「メソッドの出口は1つにすべき」のは本当?
新人向けの書籍では「メソッドの出口は1つにすべき(return文は1個だけ書くべき)」という古典的なルールを見かけます。これは C言語時代の「リソース解放のため」のルール で、現代のJavaにはほぼ当てはまりません。
Javaでは以下の理由から、早期リターン(複数return)が推奨されます。
| 観点 | 単一出口の問題 | 早期リターンの利点 |
|---|---|---|
| リソース解放 | C言語では goto cleanup が必要だった |
Javaは try-with-resources が解決済み |
| 読みやすさ | フラグ変数 + 巨大なif-elseで複雑化 | 上から順に「失格条件」を読める |
| ネスト | どんどん深くなる | 0段に保てる |
| 認知負荷 | 「今どの条件下にいるか」を追う必要 | フラットなので不要 |
リーダブルコードや Effective Java でも、早期リターンは推奨される書き方 です。新人向けの教材で「return は1つ」と書かれていても、現代のJavaでは積極的に複数returnを使いましょう。
半日溶かした実話:ネスト7段のメソッド
過去最大に絡まったコードに出会った時の話です。
public Result process(Request request) {
if (request != null) {
if (request.getUser() != null) {
if (request.getUser().isActive()) {
if (request.getOrder() != null) {
if (request.getOrder().getItems() != null) {
if (!request.getOrder().getItems().isEmpty()) {
if (request.getOrder().getTotalAmount() > 0) {
// ようやく本処理
return doProcess(request);
}
// 各else節がここから下に延々と並ぶ
}
}
}
}
}
}
return null;
}
ネストが7段 ありました。
このメソッドのあるバグを直す依頼が来たのですが、調査だけで 半日以上 かかりました。
問題だったのは:
- どの
ifがどのelseに対応しているか、目で追うのが困難 - 修正箇所を見つけても、影響範囲(他の
elseブロック)が想像できない - テストを書こうにも、何パターンあるのか分からない
最終的に ガード節への書き換え を提案して、ネストを0段に直しました。
書き換え後のコードは行数が増えるどころか 減りました。else を書いていた分のインデントとブロック括弧が消えたからです。
修正のリスクも下がりました。ガード節なら、「ここを通る条件」を1つずつ消していくだけ で全パスを網羅できます。
ネストはコストです。書いた瞬間の楽さと、保守する地獄を引き換えにします。
演習問題
難易度の見方
| マーク | 難易度 | 目安 |
|---|---|---|
| ⭐ | 基本 | 原則を覚えれば解ける |
| ⭐⭐ | 応用 | 複数の原則を組み合わせる |
まずは自分で考えてから、模範解答を見てください!
問題1:ネストをガード節に書き換える ⭐
次のメソッドを、ガード節を使ってネストを浅く書き直してください。
public class Sample {
public static void main(String[] args) {
System.out.println(getDiscountRate(new Customer(true, 5)));
System.out.println(getDiscountRate(new Customer(true, 0)));
System.out.println(getDiscountRate(new Customer(false, 5)));
System.out.println(getDiscountRate(null));
}
static double getDiscountRate(Customer customer) {
if (customer != null) {
if (customer.isMember()) {
if (customer.getPurchaseCount() > 0) {
return 0.10;
} else {
return 0.0;
}
} else {
return 0.0;
}
} else {
return 0.0;
}
}
}
class Customer {
private final boolean isMember;
private final int purchaseCount;
Customer(boolean isMember, int purchaseCount) { this.isMember = isMember; this.purchaseCount = purchaseCount; }
boolean isMember() { return isMember; }
int getPurchaseCount() { return purchaseCount; }
}
模範解答
public class Exercise01 {
public static void main(String[] args) {
System.out.println(getDiscountRate(new Customer(true, 5)));
System.out.println(getDiscountRate(new Customer(true, 0)));
System.out.println(getDiscountRate(new Customer(false, 5)));
System.out.println(getDiscountRate(null));
}
static double getDiscountRate(Customer customer) {
if (customer == null) {
return 0.0;
}
if (!customer.isMember()) {
return 0.0;
}
if (customer.getPurchaseCount() == 0) {
return 0.0;
}
return 0.10;
}
}
class Customer {
private final boolean isMember;
private final int purchaseCount;
Customer(boolean isMember, int purchaseCount) { this.isMember = isMember; this.purchaseCount = purchaseCount; }
boolean isMember() { return isMember; }
int getPurchaseCount() { return purchaseCount; }
}
期待する出力
0.1
0.0
0.0
0.0
ポイント:
- 「失格条件」を上から順に並べる
- ネスト3段が0段になり、上から順に読むだけで条件が分かる
- 正常系の
return 0.10はメソッドの最後に1行残るだけ
問題2:バリデーションをガード節で書く ⭐
次のメソッドは、パスワードのバリデーションを行います。
ガード節を使って、エラーメッセージを返すバリデーション関数を書いてください。
仕様:
- nullなら「パスワードがnullです」
- 8文字未満なら「パスワードは8文字以上にしてください」
- 大文字を含まなければ「大文字を1文字以上含めてください」
- 数字を含まなければ「数字を1文字以上含めてください」
- 上記すべてOKなら「OK」
public class Sample {
public static void main(String[] args) {
System.out.println(validatePassword("abc"));
System.out.println(validatePassword("abcdefgh"));
System.out.println(validatePassword("ABCDEFGH"));
System.out.println(validatePassword("Abcdefgh1"));
}
static String validatePassword(String password) {
// ここに実装する
return null;
}
}
模範解答
public class Exercise02 {
public static void main(String[] args) {
System.out.println(validatePassword("abc"));
System.out.println(validatePassword("abcdefgh"));
System.out.println(validatePassword("ABCDEFGH"));
System.out.println(validatePassword("Abcdefgh1"));
}
static String validatePassword(String password) {
if (password == null) {
return "パスワードがnullです";
}
if (password.length() < 8) {
return "パスワードは8文字以上にしてください";
}
if (!password.matches(".*[A-Z].*")) {
return "大文字を1文字以上含めてください";
}
if (!password.matches(".*[0-9].*")) {
return "数字を1文字以上含めてください";
}
return "OK";
}
}
期待する出力
パスワードは8文字以上にしてください
大文字を1文字以上含めてください
数字を1文字以上含めてください
OK
ポイント:
- バリデーション関数は ガード節の典型例
- 条件を1つずつ順に弾いていく
- if-else でネストを作らないので、検査項目を追加するときも1ブロック足すだけで済む
問題3:ループ内でcontinueを使う ⭐
次のメソッドは、商品リストから「在庫があり、価格が0以上で、商品名がnullでない」商品の価格を合計します。
continue を使ってネストを浅く書き直してください。
import java.util.List;
import java.util.ArrayList;
public class Sample {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("apple", 100, true));
products.add(new Product(null, 200, true));
products.add(new Product("banana", -50, true));
products.add(new Product("cherry", 300, false));
products.add(new Product("date", 400, true));
int total = sumValidProductPrices(products);
System.out.println("有効商品の合計: " + total);
}
static int sumValidProductPrices(List<Product> products) {
int total = 0;
for (Product product : products) {
if (product != null) {
if (product.getName() != null) {
if (product.getPrice() >= 0) {
if (product.isInStock()) {
total += product.getPrice();
}
}
}
}
}
return total;
}
}
class Product {
private final String name;
private final int price;
private final boolean isInStock;
Product(String name, int price, boolean isInStock) { this.name = name; this.price = price; this.isInStock = isInStock; }
String getName() { return name; }
int getPrice() { return price; }
boolean isInStock() { return isInStock; }
}
模範解答
import java.util.List;
import java.util.ArrayList;
public class Exercise03 {
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("apple", 100, true));
products.add(new Product(null, 200, true));
products.add(new Product("banana", -50, true));
products.add(new Product("cherry", 300, false));
products.add(new Product("date", 400, true));
int total = sumValidProductPrices(products);
System.out.println("有効商品の合計: " + total);
}
static int sumValidProductPrices(List<Product> products) {
int total = 0;
for (Product product : products) {
if (product == null) {
continue;
}
if (product.getName() == null) {
continue;
}
if (product.getPrice() < 0) {
continue;
}
if (!product.isInStock()) {
continue;
}
total += product.getPrice();
}
return total;
}
}
class Product {
private final String name;
private final int price;
private final boolean isInStock;
Product(String name, int price, boolean isInStock) { this.name = name; this.price = price; this.isInStock = isInStock; }
String getName() { return name; }
int getPrice() { return price; }
boolean isInStock() { return isInStock; }
}
期待する出力
有効商品の合計: 500
ポイント:
- ループ内のネストが4段から0段に
-
continueで「対象外の要素」を弾く - 加算処理がループ本体に1行だけ残る
- 検査条件を追加するときも、新しい
if-continueを1ブロック足すだけ
問題4:複合リファクタリング ⭐⭐
次のメソッドは「ネストの深さ」「不要なelse」「単一出口の強制」がすべて含まれています。
3原則を踏まえて、リファクタリングしてください。
public class Sample {
public static void main(String[] args) {
System.out.println(processOrder(new Order(true, true, 1000, false)));
System.out.println(processOrder(new Order(false, true, 1000, false)));
System.out.println(processOrder(new Order(true, false, 1000, false)));
System.out.println(processOrder(new Order(true, true, 1000, true)));
System.out.println(processOrder(null));
}
static String processOrder(Order order) {
String result;
if (order == null) {
result = "注文がありません";
} else {
if (order.isLoggedIn()) {
if (order.isInStock()) {
if (!order.isCanceled()) {
int finalPrice = (int) (order.getPrice() * 0.9);
result = "発送処理開始(最終価格: " + finalPrice + "円)";
} else {
result = "キャンセル済みです";
}
} else {
result = "在庫切れです";
}
} else {
result = "ログインしてください";
}
}
return result;
}
}
class Order {
private final boolean isLoggedIn;
private final boolean isInStock;
private final int price;
private final boolean isCanceled;
Order(boolean isLoggedIn, boolean isInStock, int price, boolean isCanceled) {
this.isLoggedIn = isLoggedIn; this.isInStock = isInStock; this.price = price; this.isCanceled = isCanceled;
}
boolean isLoggedIn() { return isLoggedIn; }
boolean isInStock() { return isInStock; }
int getPrice() { return price; }
boolean isCanceled() { return isCanceled; }
}
模範解答
public class Exercise04 {
public static void main(String[] args) {
System.out.println(processOrder(new Order(true, true, 1000, false)));
System.out.println(processOrder(new Order(false, true, 1000, false)));
System.out.println(processOrder(new Order(true, false, 1000, false)));
System.out.println(processOrder(new Order(true, true, 1000, true)));
System.out.println(processOrder(null));
}
static String processOrder(Order order) {
if (order == null) {
return "注文がありません";
}
if (!order.isLoggedIn()) {
return "ログインしてください";
}
if (!order.isInStock()) {
return "在庫切れです";
}
if (order.isCanceled()) {
return "キャンセル済みです";
}
int finalPrice = (int) (order.getPrice() * 0.9);
return "発送処理開始(最終価格: " + finalPrice + "円)";
}
}
class Order {
private final boolean isLoggedIn;
private final boolean isInStock;
private final int price;
private final boolean isCanceled;
Order(boolean isLoggedIn, boolean isInStock, int price, boolean isCanceled) {
this.isLoggedIn = isLoggedIn; this.isInStock = isInStock; this.price = price; this.isCanceled = isCanceled;
}
boolean isLoggedIn() { return isLoggedIn; }
boolean isInStock() { return isInStock; }
int getPrice() { return price; }
boolean isCanceled() { return isCanceled; }
}
期待する出力
発送処理開始(最終価格: 900円)
ログインしてください
在庫切れです
キャンセル済みです
注文がありません
改善ポイント
| 元のコード | 改善後 | 理由 |
|---|---|---|
String result の宣言 |
削除 | 早期リターンで不要に |
ネスト3段の if-else
|
ガード節4本 | ネスト0段に |
単一出口の return result;
|
各分岐で直接return | フラグ変数が消える |
else { if (...) } の連鎖 |
平坦な if-return の連鎖 |
読みやすさが格段に向上 |
ポイント:
- 「単一出口(return 1つ)」というルールを捨てる
- フラグ変数(
String result)を持たずに、各条件で直接return - 結果として 元のコードより行数が減り、読みやすさが大幅に向上
まとめ
新人〜2年目が押さえるべき早期リターンの3原則は、以下の3つです。
- ガード節で前提条件を最初に弾く:処理続行できないケースを上から順に return
-
else を消す:
ifの中で return しているならelseは不要 - ループ内では continue / break で異常系を弾く:ガード節のループ版
すべてに共通するのは、「異常系を先に処理して、正常系を最後に書く」 という設計です。
この順序を守るだけで、ネストの深いコードはほぼ消えます。
「単一出口」「return は1つだけ」という古典ルールは、現代のJavaでは当てはまりません。
ネストはコストです。早期リターンを使い倒しましょう。
次回予告
次回(#6)は 「メソッド分割」 を扱います。
- 「100行を超えるメソッド」がなぜダメか
- メソッドの責務を1つに保つ方法
- リファクタリング「メソッド抽出」の判断軸
を、Before / After 形式で解説していきます。
参考
- リーダブルコード 第7章「制御フローを読みやすくする」(オライリー・ジャパン)
- Martin Fowler - Refactoring: Replace Nested Conditional with Guard Clauses
- Java言語仕様 - The break / continue Statement
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!