Java:カプセル化
1学習の目的
- private でフィールドを隠し、getter/setter で出入り口を作れるようになる。不正な値が入ることを構造的に防げるようになる。
- 「データを守る」という設計の考え方を理解する。JV08で残った穴——外から自由に書き換えられる問題——を塞ぐ。
2基礎解説
JV08の Product は、p.stock = -100; と書けてしまいました。在庫がマイナスの商品が作れてしまうのは、設計として明らかにおかしい。カプセル化はこの穴を塞ぐ仕組みです。
| 修飾子 | アクセス範囲 | 使いどころ |
|---|---|---|
| private | そのクラスの中だけ | フィールドは基本これ |
| public | どこからでも | メソッドの多く |
| (なし) | 同じパッケージ内 | 限定的な公開 |
| protected | 同パッケージ+子クラス | 継承で使う(JV10) |
- フィールドは private、メソッドは public が基本。データは隠し、操作する手段だけを公開する——これがカプセル化の原則。
- getter は値を取り出すメソッド(getName())。setter は値を設定するメソッド(setPrice())で、ここに検証を書けるのが最大の利点。
- 不正な値は throw で拒否する。IllegalArgumentException を投げれば、おかしな値が入る前に処理を止められる。
- コンストラクタからも setter を呼ぶと、生成時にも同じ検証が働く。どの経路から来ても不正な値を通さない状態になる。
- 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問)
出力 == ノート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 を通す——これがカプセル化の基本形。
A. public B. private C. static D. final 選択★☆☆無料
解答 == B
データは隠すのが原則。
B
フィールドは private、メソッドは publicが基本。データを直接触らせず、操作する手段だけを公開する。
出力 == 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 を通すことで、変更に処理を挟める。次の問題では、ここに検証を入れて不正な値を弾く。
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 はそのクラスの中だけからアクセスできる。外部のコードからは見えないので、勝手に書き換えられない。
A. 処理が速くなる B. 不正な値が入るのを防げる C. コードが短くなる D. メモリを節約できる 選択★☆☆無料
解答 == B
値を設定する唯一の入口になる。
B
setter が唯一の入口なので、そこで検証すれば必ず通る。フィールドが public だと、どこから何を入れられるか分からない。
出力 == 在庫は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 がそのまま入って後の計算が壊れていた。
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の例外処理で扱う。
A. 速くなる B. 生成時にも同じ検証が働く C. コードが減る D. 特にない 選択★★☆広告解放
解答 == B
検証を1箇所にまとめられる。
B
どの経路から値が入っても検証を通る。コンストラクタで直接代入すると、生成時だけ検証を素通りしてしまう。
出力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());
}
}失敗しても在庫は変わらない。もし先に引いてから判定していたら、一瞬でもマイナスの在庫が存在してしまう。
出力 == ノート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問)
出力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; と書けてしまい、銀行システムとして成立しない。
出力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());
}
}
}上限と下限の両方を検証する。|| でつなげば、どちらか一方でも外れていれば拒否できる。
出力 == 価格は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 で別々に書くと、片方の修正を忘れて抜け道ができる。
出力 == 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 と呼ぶ——「情報を取り出す」役割が同じだから。
出力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仕上げ課題
Product クラス
・フィールドはすべて private:name(final)、price、stock
・コンストラクタ: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箇所でもそんなコードがあれば、原因を探すのに何時間もかかる。
reduceStock が boolean を返す設計にも意味がある。「失敗するかもしれない操作」であることが、戻り値の型に現れている。呼び出す側は結果を確認せざるをえず、在庫不足を見逃せない。もし void だったら、失敗しても気づかないまま処理が進んでしまう。
private final String name という宣言も重要だ。商品名は生成後に変わらない——この業務ルールが、コードそのものに刻まれている。setter を作らなかったのは手抜きではなく、意図した設計だ。
次章では継承を学ぶ。似たクラスが増えてきたとき、共通部分をまとめる方法を身につける。