Java / LESSON 09 / JV09

Java:カプセル化

1学習の目的

2基礎解説

JV08の Product は、p.stock = -100; と書けてしまいました。在庫がマイナスの商品が作れてしまうのは、設計として明らかにおかしい。カプセル化はこの穴を塞ぐ仕組みです。

修飾子アクセス範囲使いどころ
privateそのクラスの中だけフィールドは基本これ
publicどこからでもメソッドの多く
(なし)同じパッケージ内限定的な公開
protected同パッケージ+子クラス継承で使う(JV10)
✅ 覚えるべき重要ポイント
  1. フィールドは private、メソッドは public が基本。データは隠し、操作する手段だけを公開する——これがカプセル化の原則。
  2. getter は値を取り出すメソッドgetName())。setter は値を設定するメソッドsetPrice())で、ここに検証を書けるのが最大の利点。
  3. 不正な値は throw で拒否するIllegalArgumentException を投げれば、おかしな値が入る前に処理を止められる
  4. コンストラクタからも setter を呼ぶと、生成時にも同じ検証が働く。どの経路から来ても不正な値を通さない状態になる。
  5. setter を作らない選択もある。「商品名は後から変わらない」なら getter だけにする。変更できないほうが安全な場合は多い。

現場使用例:金額・在庫・年齢など取りうる範囲が決まっている値、パスワードのように直接触らせたくない値、変更時に他の処理が必要な値。

public class Main {

    static class Product {
        // ① フィールドは private で隠す
        private String name;
        private int price;
        private int stock;

        Product(String name, int price, int stock) {
            this.name = name;
            setPrice(price);      // ② コンストラクタでも検証を通す
            setStock(stock);
        }

        // ③ getter(取り出す)
        public String getName()  { return name; }
        public int getPrice()    { return price; }
        public int getStock()    { return stock; }

        // ④ setter(設定する・検証つき)
        public void setPrice(int price) {
            if (price < 0) {
                throw new IllegalArgumentException("価格は0以上にしてください");
            }
            this.price = price;
        }

        public void setStock(int stock) {
            if (stock < 0) {
                throw new IllegalArgumentException("在庫は0以上にしてください");
            }
            this.stock = stock;
        }

        // ⑤ 意味のある操作をメソッドにする
        public void addStock(int n) {
            stock += n;
        }

        public boolean reduceStock(int n) {
            if (n > stock) return false;    // 在庫不足なら失敗
            stock -= n;
            return true;
        }

        public int getStockValue() {
            return price * stock;
        }
    }

    public static void main(String[] args) {
        Product p = new Product("ノートPC", 128000, 5);

        // p.stock = -100;   ← ⚠ コンパイルエラー(privateなので触れない)

        p.addStock(3);              // 8個に
        p.reduceStock(2);           // 6個に
        p.reduceStock(100);         // false(在庫不足で変化なし)

        System.out.println(p.getStock());   // 6
    }
}

3基本ドリル(10問)

JV09-D01 フィールドを private にした Product クラスを作り、getter 経由で name を取得して出力せよ。 コーディング★☆☆無料
期待される結果

出力 == ノートPC

ヒント

private String name; と public String getName() { return name; } を書く。

模範解答
public class Main {

    static class Product {
        private String name;

        Product(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        Product p = new Product("ノートPC");

        System.out.println(p.getName());
    }
}
解説

private にすると外から直接触れなくなる。取り出したいなら getter を通す——これがカプセル化の基本形。

JV09-D02 フィールドに付けるべき修飾子として推奨されるのはどれか。
A. public B. private C. static D. final
選択★☆☆無料
期待される結果

解答 == B

ヒント

データは隠すのが原則。

模範解答
B
解説

フィールドは private、メソッドは publicが基本。データを直接触らせず、操作する手段だけを公開する。

JV09-D03 setter を使って価格を変更できる Product を作れ(初期値128000、setPrice(150000)で変更後、getPrice で出力)。 コーディング★☆☆無料
期待される結果

出力 == 150000

ヒント

public void setPrice(int price) { this.price = price; } と書く。

模範解答
public class Main {

    static class Product {
        private int price;

        Product(int price) {
            this.price = price;
        }

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }
    }

    public static void main(String[] args) {
        Product p = new Product(128000);

        p.setPrice(150000);

        System.out.println(p.getPrice());
    }
}
解説

setter を通すことで、変更に処理を挟める。次の問題では、ここに検証を入れて不正な値を弾く。

JV09-D04 フィールドを外から隠す修飾子を補ってコードを完成させよ。 穴埋め★☆☆無料
コード
public class Main {

    static class Item {
        ____ int value;

        Item(int value) { this.value = value; }

        public int getValue() { return value; }
    }

    public static void main(String[] args) {
        System.out.println(new Item(42).getValue());
    }
}
期待される結果

出力 == 42

ヒント

「私的な」を意味する7文字の修飾子。

模範解答
private
解説

private はそのクラスの中だけからアクセスできる。外部のコードからは見えないので、勝手に書き換えられない。

JV09-D05 setter に検証を書く利点はどれか。
A. 処理が速くなる B. 不正な値が入るのを防げる C. コードが短くなる D. メモリを節約できる
選択★☆☆無料
期待される結果

解答 == B

ヒント

値を設定する唯一の入口になる。

模範解答
B
解説

setter が唯一の入口なので、そこで検証すれば必ず通る。フィールドが public だと、どこから何を入れられるか分からない。

JV09-D06 検証つき setter を作れ(在庫は0以上のみ許可、負の値なら IllegalArgumentException を投げる)。-1を設定しようとして拒否されることを確認すること。 コーディング★★☆広告解放
期待される結果

出力 == 在庫は0以上にしてください

ヒント

throw new IllegalArgumentException("メッセージ") を使い、try/catch で受ける。

模範解答
public class Main {

    static class Product {
        private int stock;

        Product(int stock) {
            setStock(stock);
        }

        public void setStock(int stock) {
            if (stock < 0) {
                throw new IllegalArgumentException("在庫は0以上にしてください");
            }
            this.stock = stock;
        }

        public int getStock() {
            return stock;
        }
    }

    public static void main(String[] args) {
        Product p = new Product(5);

        try {
            p.setStock(-1);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}
解説

不正な値は入る前に止められる。もしフィールドが public なら、-1 がそのまま入って後の計算が壊れていた。

JV09-D07 例外を投げるキーワードを補ってコードを完成させよ。 穴埋め★★☆広告解放
コード
public class Main {

    static void check(int n) {
        if (n < 0) {
            ____ new IllegalArgumentException("負の値です");
        }
        System.out.println("OK");
    }

    public static void main(String[] args) {
        try {
            check(-1);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}
期待される結果

出力 == 負の値です

ヒント

「投げる」を意味する5文字のキーワード。

模範解答
throw
解説

throw で処理を中断し、呼び出し元にエラーを伝える。詳しくはJV13の例外処理で扱う。

JV09-D08 コンストラクタの中から setter を呼ぶ利点はどれか。
A. 速くなる B. 生成時にも同じ検証が働く C. コードが減る D. 特にない
選択★★☆広告解放
期待される結果

解答 == B

ヒント

検証を1箇所にまとめられる。

模範解答
B
解説

どの経路から値が入っても検証を通る。コンストラクタで直接代入すると、生成時だけ検証を素通りしてしまう。

JV09-D09 在庫を減らすメソッド reduceStock を作れ(在庫不足なら false を返して変更しない)。在庫5から2個・100個を順に減らそうとし、結果と残数を2行出力すること。 コーディング★★☆広告解放
期待される結果

出力2行。true 3 / false 3

ヒント

if (n > stock) return false; で先に判定する。

模範解答
public class Main {

    static class Product {
        private int stock;

        Product(int stock) {
            this.stock = stock;
        }

        public int getStock() {
            return stock;
        }

        public boolean reduceStock(int n) {
            if (n > stock) {
                return false;
            }
            stock -= n;
            return true;
        }
    }

    public static void main(String[] args) {
        Product p = new Product(5);

        System.out.println(p.reduceStock(2) + " " + p.getStock());
        System.out.println(p.reduceStock(100) + " " + p.getStock());
    }
}
解説

失敗しても在庫は変わらない。もし先に引いてから判定していたら、一瞬でもマイナスの在庫が存在してしまう。

JV09-D10 読み取り専用のフィールドを作れ(商品名は setter を作らず、getter のみ)。名前を取得して出力すること。 コーディング★★☆広告解放
期待される結果

出力 == ノートPC

ヒント

setter を書かなければ、外から変更できない。final を付けるとさらに厳密になる。

模範解答
public class Main {

    static class Product {
        private final String name;

        Product(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        Product p = new Product("ノートPC");

        System.out.println(p.getName());
    }
}
解説

変更する必要がないなら setter を作らないfinal を付ければ、クラスの中からも変更できなくなり、より安全になる。

4実践シナリオ(5問)

JV09-S01 【銀行口座】Account クラスを作れ(balance は private)。入金 deposit と出金 withdraw(残高不足なら false)を持たせ、10000円から3000円出金・50000円出金を試して2行出力すること。 コーディング★★☆広告解放
期待される結果

出力2行。true 7000 / false 7000

ヒント

withdraw は残高不足なら何もせず false を返す。

模範解答
public class Main {

    static class Account {
        private int balance;

        Account(int balance) {
            this.balance = balance;
        }

        public int getBalance() {
            return balance;
        }

        public void deposit(int amount) {
            balance += amount;
        }

        public boolean withdraw(int amount) {
            if (amount > balance) {
                return false;
            }
            balance -= amount;
            return true;
        }
    }

    public static void main(String[] args) {
        Account a = new Account(10000);

        System.out.println(a.withdraw(3000) + " " + a.getBalance());
        System.out.println(a.withdraw(50000) + " " + a.getBalance());
    }
}
解説

残高を private にする最大の理由がこれ。もし public なら a.balance = 999999; と書けてしまい、銀行システムとして成立しない。

JV09-S02 【年齢の検証】Person クラスの setAge に検証を入れよ(0〜150以外は拒否)。25歳は成功、-5歳は拒否されることを確認し2行出力すること。 コーディング★★☆広告解放
期待される結果

出力2行。25 / 年齢が不正です

ヒント

範囲外なら IllegalArgumentException を投げる。

模範解答
public class Main {

    static class Person {
        private int age;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            if (age < 0 || age > 150) {
                throw new IllegalArgumentException("年齢が不正です");
            }
            this.age = age;
        }
    }

    public static void main(String[] args) {
        Person p = new Person();

        p.setAge(25);
        System.out.println(p.getAge());

        try {
            p.setAge(-5);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}
解説

上限と下限の両方を検証する|| でつなげば、どちらか一方でも外れていれば拒否できる。

JV09-S03 【コンストラクタでも検証】Product のコンストラクタから setter を呼び、生成時にも不正な値を弾くことを確認せよ(価格-100で生成を試み、拒否メッセージを出力)。 コーディング★★☆広告解放
期待される結果

出力 == 価格は0以上にしてください

ヒント

コンストラクタで this.price = price; ではなく setPrice(price); と書く。

模範解答
public class Main {

    static class Product {
        private int price;

        Product(int price) {
            setPrice(price);
        }

        public void setPrice(int price) {
            if (price < 0) {
                throw new IllegalArgumentException("価格は0以上にしてください");
            }
            this.price = price;
        }
    }

    public static void main(String[] args) {
        try {
            new Product(-100);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}
解説

検証が1箇所にまとまる。コンストラクタと setter で別々に書くと、片方の修正を忘れて抜け道ができる。

JV09-S04 【計算結果を返す getter】Product に getStockValue()(price × stock)と getLabel()(在庫状態)を作れ。ノートPC/128000/5 で「640,000円(残りわずか)」と出力すること。 コーディング★★★広告解放
期待される結果

出力 == 640,000円(残りわずか)

ヒント

フィールドを持たない getter も作れる。計算結果を返すだけでよい。

模範解答
public class Main {

    static class Product {
        private String name;
        private int price;
        private int stock;

        Product(String name, int price, int stock) {
            this.name = name;
            this.price = price;
            this.stock = stock;
        }

        public int getStockValue() {
            return price * stock;
        }

        public String getLabel() {
            if (stock == 0) return "品切れ";
            if (stock < 10) return "残りわずか";
            return "在庫あり";
        }
    }

    public static void main(String[] args) {
        Product p = new Product("ノートPC", 128000, 5);

        System.out.println(String.format("%,d", p.getStockValue())
                + "円(" + p.getLabel() + ")");
    }
}
解説

getter は必ずしもフィールドを返すとは限らない。計算結果や判定結果を返すものも getter と呼ぶ——「情報を取り出す」役割が同じだから。

JV09-S05 【入荷と出荷の一連の流れ】在庫5の商品に対し、①3個入荷 ②2個出荷 ③100個出荷(失敗)を順に行い、各操作後の在庫を3行出力せよ。 コーディング★★★広告解放
期待される結果

出力3行。8 / 6 / 6

ヒント

addStock と reduceStock を作り、順に呼び出す。

模範解答
public class Main {

    static class Product {
        private int stock;

        Product(int stock) {
            this.stock = stock;
        }

        public int getStock() {
            return stock;
        }

        public void addStock(int n) {
            stock += n;
        }

        public boolean reduceStock(int n) {
            if (n > stock) {
                return false;
            }
            stock -= n;
            return true;
        }
    }

    public static void main(String[] args) {
        Product p = new Product(5);

        p.addStock(3);
        System.out.println(p.getStock());

        p.reduceStock(2);
        System.out.println(p.getStock());

        p.reduceStock(100);
        System.out.println(p.getStock());
    }
}
解説

失敗した操作では在庫が変わらないstock -= n を先に書いていたら、100個引かれてマイナスになっていた。

5仕上げ課題

JV09-FINAL 【安全な在庫管理クラス】カプセル化を徹底した Product クラスを作れ。

Product クラス
・フィールドはすべて privatename(final)、pricestock
・コンストラクタ:setter を経由して検証を通す
getName() … 読み取り専用(setterなし)
setPrice(int) … 負なら「価格は0以上にしてください」で拒否
setStock(int) … 負なら「在庫は0以上にしてください」で拒否
addStock(int n) … 入荷
boolean reduceStock(int n) … 在庫不足なら false(変更しない)
getStockValue() … 在庫金額
toString() … 「ノートPC:128,000円 × 6個 = 768,000円」の形式

main での操作
① ノートPC/128000/5 を生成して出力
② 3個入荷して出力
③ 2個出荷して出力
④ 100個出荷を試みる(失敗)→「出荷失敗:在庫不足」と出力
⑤ 価格を -1 に設定しようとする → 拒否メッセージを出力
⑥ 在庫 -5 で新規生成を試みる → 拒否メッセージを出力

期待される出力(6行)
ノートPC:128,000円 × 5個 = 640,000円
ノートPC:128,000円 × 8個 = 1,024,000円
ノートPC:128,000円 × 6個 = 768,000円
出荷失敗:在庫不足
価格は0以上にしてください
在庫は0以上にしてください
総合★★★広告解放
期待される結果

出力6行が完全一致

ヒント

コンストラクタで setPrice と setStock を呼ぶことで、生成時にも検証が働く。reduceStock の戻り値を if で判定して失敗メッセージを出す。try/catch で例外メッセージを受け取る。

模範解答
public class Main {

    static class Product {
        private final String name;
        private int price;
        private int stock;

        Product(String name, int price, int stock) {
            this.name = name;
            setPrice(price);
            setStock(stock);
        }

        public String getName() {
            return name;
        }

        public int getPrice() {
            return price;
        }

        public int getStock() {
            return stock;
        }

        public void setPrice(int price) {
            if (price < 0) {
                throw new IllegalArgumentException("価格は0以上にしてください");
            }
            this.price = price;
        }

        public void setStock(int stock) {
            if (stock < 0) {
                throw new IllegalArgumentException("在庫は0以上にしてください");
            }
            this.stock = stock;
        }

        public void addStock(int n) {
            stock += n;
        }

        public boolean reduceStock(int n) {
            if (n > stock) {
                return false;
            }
            stock -= n;
            return true;
        }

        public int getStockValue() {
            return price * stock;
        }

        @Override
        public String toString() {
            return name + ":" + String.format("%,d", price)
                    + "円 × " + stock + "個 = "
                    + String.format("%,d", getStockValue()) + "円";
        }
    }

    public static void main(String[] args) {
        Product p = new Product("ノートPC", 128000, 5);
        System.out.println(p);

        p.addStock(3);
        System.out.println(p);

        p.reduceStock(2);
        System.out.println(p);

        if (!p.reduceStock(100)) {
            System.out.println("出荷失敗:在庫不足");
        }

        try {
            p.setPrice(-1);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }

        try {
            new Product("不正商品", 1000, -5);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}
解説

この Product は、不正な状態になることができない。価格を負にしようとすれば拒否され、在庫不足の出荷は実行されず、生成時ですら検証を通る——どの経路から操作しても、常に正しい状態が保たれる

JV08の Product と比べてほしい。あちらは p.stock = -100; と1行書くだけで壊れた。データを公開するということは、あらゆる場所から壊される可能性を許すということだ。1万行のプログラムの中に1箇所でもそんなコードがあれば、原因を探すのに何時間もかかる。

reduceStockboolean を返す設計にも意味がある。「失敗するかもしれない操作」であることが、戻り値の型に現れている。呼び出す側は結果を確認せざるをえず、在庫不足を見逃せない。もし void だったら、失敗しても気づかないまま処理が進んでしまう。

private final String name という宣言も重要だ。商品名は生成後に変わらない——この業務ルールが、コードそのものに刻まれている。setter を作らなかったのは手抜きではなく、意図した設計だ。

次章では継承を学ぶ。似たクラスが増えてきたとき、共通部分をまとめる方法を身につける。