カプセル化 (private と getter)
カプセル化 とは
privateフィールドとpublicな getter / setter を使って、データを安全に守る「カプセル化」を学ぼう。負の入金を弾くsafeAccount(int initial, int deposit)を書く。
カプセル化 ─ クラスの中身を守る防壁
前のレッスンまでで、Java のクラスにはフィールド (データ) とメソッド (動き) があり、new でインスタンスを作って . でアクセスする、という基本を見てきました。けれど、今のままだとひとつ大きな弱点があります。外側から account.balance = -999999; のように直接フィールドを書き換えられてしまう ことです。残高が突然マイナスになるような銀行アプリは、誰も使いたくないはずです。
この問題を防ぐために Java に用意されている考え方が カプセル化 (encapsulation) です。クラスの内部データを private で隠して、外側からは限定された窓口 (メソッド) 経由でしか触れないようにする ─ いわば、お店のレジカウンターと裏のバックヤードを分けるイメージです。お客さんはカウンター越しに商品を受け渡しできるけれど、勝手にバックヤードに入って商品を漁ることはできない。これがクラス設計でいうカプセル化のメンタルモデルです。
カプセル化は、
Javaを含むほとんどのオブジェクト指向言語が大事にしている 3 本柱 (継承 / ポリモーフィズム / カプセル化) のひとつです。プログラムが大きくなるほど、「どこから誰がこのデータを触っているのか」を把握するコストが跳ね上がります。privateで入り口を絞っておけば、何かバグが起きても疑う場所が一気に減って、後から自分を助けてくれます。
このレッスンでは、銀行口座を題材にした safeAccount(int initial, int deposit) メソッドを書きながら、private フィールド、getter、setter の組み合わせと、setter の中で validation (入力の検証) を行うパターンを身につけていきます。
private フィールドの意義
まずはカプセル化されていない「素のクラス」と、private で守った「カプセル化されたクラス」を見比べます。
Java
// Before: フィールドが public で誰でも書き換えられる
class Account {
public int balance;
}
Account a = new Account();
a.balance = 1000;
a.balance = -999999; // これも通ってしまう。残高がマイナスに...上のように public でフィールドを公開してしまうと、外側のコードから何でもできてしまいます。balance が -999999 になっても、Java のコンパイラは止めてくれません。なぜなら、文法的にはまったく正しい操作だからです。
一方、private でフィールドを隠すと、外からの直接アクセスは即コンパイルエラーになります。
Java
// After: private で隠して、メソッド経由でしか触れないようにする
class Account {
private int balance;
public int getBalance() {
return balance;
}
}
Account a = new Account();
// a.balance = -999999; // ← コンパイルエラー
int now = a.getBalance(); // ← OKa.balance = -999999; を書こうとした瞬間、コンパイラから balance has private access in Account のようなエラーが出ます。「触れない」のではなく「触らせない」 ─ これがカプセル化の入り口です。
publicprivateprotectedのように、誰がアクセスできるかを指定するキーワードを アクセス修飾子 (access modifier) と呼びます。privateは「自分のクラスの中だけ」、publicは「どこからでも」、protectedは「同じパッケージか継承先から」を意味します。最初のうちはフィールドはprivate、外に出したいメソッドだけpublic、と覚えてしまえば十分です。
getter と setter ─ 公式の窓口を用意する
private で隠したフィールドにまったく触れないままだと、それはそれで困ります。「残高をいくらにしたい」「今いくらか知りたい」というのは普通のニーズです。そこで Java では getter (値を取り出すメソッド) と setter (値を入れるメソッド) を用意して、外側からはこれを通じてフィールドを触ってもらう、というのが慣習になっています。
Java
class Account {
private int balance;
public int getBalance() {
return balance;
}
public void setBalance(int newBalance) {
balance = newBalance;
}
}getBalance は「今の balance を int で返す」だけのメソッド、setBalance は「渡された int を balance に代入する」だけのメソッドです。外側からは次のように使います。
Java
Account a = new Account();
a.setBalance(1000);
int now = a.getBalance(); // 1000ここまでだと「public フィールドにするのと何が違うんだ?」と感じるかもしれません。違いが効いてくるのは、次の節で見る setter で validation をかける 場面です。窓口 (メソッド) を一段はさんだことで、入ってくる値を検査して弾くチャンスが生まれるわけです。
命名の慣習として、
Javaの getter はgetXxx()、setter はsetXxx(...)、booleanを返す getter だけはisXxx()の形にする、というルールがほぼ業界標準になっています。これはJavaBeansという古くからの規約で、SpringやJacksonなどの主要フレームワークも、この命名を前提に自動的にフィールドを認識してくれます。最初から守っておくと、後で大規模なフレームワークに乗り換えるときに無駄な書き換えが減ります。
setter で validation を行う
カプセル化の真価が出てくるのが、setter の中で 「変な値は受け付けない」と決めておけるところ です。たとえば「balance は 0 円以上でなければいけない」「deposit (入金) は負の値は無視する」というルールを、クラスの中に書き込んでおけます。
Java
class Account {
private int balance;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
if (amount < 0) {
return; // 負の入金は無視
}
balance = balance + amount;
}
}deposit は厳密には setter ではありませんが、balance を書き換える窓口メソッドという意味では同じ役割です。「amount が負だったら無視する」というルールがクラスの中に閉じ込められている のがポイントです。呼び出し側のコードは、いちいち if (amount >= 0) を書かなくても、a.deposit(amount) を呼ぶだけで安心して使えます。
Java
Account a = new Account();
a.deposit(1000); // balance = 1000
a.deposit(-500); // 負なので無視。balance = 1000 のまま
a.deposit(300); // balance = 1300
System.out.println(a.getBalance()); // 1300このレッスンの課題 safeAccount(int initial, int deposit) も同じ発想です。initial を初期残高、deposit を入金額として、deposit が負なら無視して initial をそのまま返し、deposit が 0 以上なら initial + deposit を返す、というだけのシンプルなロジックですが、「変な入力を境界で弾く」というカプセル化の哲学 を体験する練習問題になっています。
validation のロジックを呼び出し側ではなくクラスの中に置く、というのはとても大事な設計判断です。同じチェックを呼び出し側 10 か所に書いていると、1 か所修正を忘れただけでバグが復活します。クラスの中に 1 か所だけ書いておけば、修正もそこ 1 か所で済みます。これはカプセル化が 保守性 に直接効いてくる典型例です。
図で見る ─ public な外側と private な内側
カプセル化を絵で表すと、Account クラスのまわりに薄い壁があって、public メソッド (getBalance, deposit) だけがその壁を貫通している、というイメージになります。外側のコードからは public 出口を通る経路しか見えません。
Caller (呼び出し側) から private int balance への破線は「触ろうとしてもコンパイラが止める」という意味です。public の口を通ってもらえば、その中で validation や副作用を仕掛けることができます。これが、ただの構造体ではなく オブジェクト としてクラスを設計する強みです。
動きを追ってみる ─ safeAccount の場合
課題の safeAccount(int initial, int deposit) を例に、内部でどんな処理を組むか頭の中で追ってみましょう。やることは次の通りです。
- 引数
initialを「最初の残高」として受け取る - 引数
depositが0以上ならinitial + depositを最終残高とする depositが負なら、initialをそのまま最終残高とする- 最終残高を
intでreturnする
直感的に書くなら次のような形になります。
Java
public static int safeAccount(int initial, int deposit) {
int balance = initial;
if (deposit >= 0) {
balance = balance + deposit;
}
return balance;
}balance という一時変数を作って、initial をスタート地点にし、deposit がプラスなら足し込み、マイナスならスキップ、最後に balance を返します。クラスを丸ごと作らずに static メソッド 1 つにまとめている形ですが、考え方は前述の Account.deposit(int) と同じです。境界で変な値を弾く という発想がそのまま入っています。
よくある間違い
カプセル化まわりは、頭で分かっていても手が滑ってしまうポイントが多いところです。代表的な落とし穴を 3 つ整理します。
- フィールドをついつい
publicにしてしまう — 書き始めのうちは、public int balance;のほうがコード量が少なくて楽に感じます。けれどそれは「これから先、誰かが変な値を入れても止められない」という時限爆弾を埋めている状態です。フィールドは原則private、必要に応じて getter / setter を足す という順番を体に染み込ませてください。Javaの世界では「privateが当たり前、publicフィールドは要相談」が共通認識です - getter / setter を機械的に全フィールドに作る —
IDEの自動生成でフィールドの数だけ getter / setter が並ぶと、見た目はリッチですが、実質的にはpublicフィールドと同じ「中身を素通しできるクラス」になりがちです。setter は「外から自由に書き換えてほしい場合だけ」用意する、それ以外は読み取り専用にする (getter だけ作る)、という判断をするとカプセル化が一段強くなります。balanceのように外から直接いじらせたくない値には、deposit(int)withdraw(int)のような 意図のあるメソッド を用意するのが理想です - ボイラープレートだらけのクラスになって本質が埋もれる —
privateフィールドと getter / setter を律儀に書いていくと、Javaのコードはどうしても縦に長くなります。意味のあるロジックが 1 行なのに、その周りに 50 行のボイラープレートがついている、ということもよくあります。これをLombokの@Getter@SetterやJava 16以降のrecordで短く書く、というテクニックもありますが、まずは「カプセル化は手間に見合った価値がある」と感じてもらえる程度の手書きに慣れることが大事です
もうひとつ要注意なのが、「private にしたから絶対安全」ではない という点です。private はあくまで「他のクラスから直接アクセスさせない」ためのもので、reflection や serialization のような仕組みを使えば中身を覗ける場面はあります。それでも 99% のバグはコンパイル時の private チェックで防げるので、まずは普通のフィールドを真面目に private にする、というところから始めれば十分です。
セキュリティの世界では「壁を 1 枚作っただけで完全に守れることはない」とよく言われます。カプセル化もそれと似ていて、
privateは 入口を狭めて間違いを起こしにくくする ための仕組みであって、絶対不可侵の盾ではありません。それでも、入口を狭めるだけで多くのバグを未然に防げるのがprivateの偉いところです。
やってみよう
それでは右側のエディタで、Solution.safeAccount(int initial, int deposit) メソッドを完成させましょう。手順は次の通りです。
safeAccountの中で、まずint balance = initial;と書いて、初期残高をローカル変数に入れるif (deposit >= 0)のときだけbalance = balance + deposit;で入金処理を行うreturn balance;で最終残高を返す- 「実行」ボタンを押して、
safeAccount(1000, 500)→1500、safeAccount(1000, -200)→1000、safeAccount(0, 100)→100が全部緑になることを確認する
動いたら、いくつか実験してみてください。たとえば次のように balance を使わずに return 文を 2 つに分けても、まったく同じ動きになります。
Java
public static int safeAccount(int initial, int deposit) {
if (deposit < 0) {
return initial;
}
return initial + deposit;
}どちらの書き方が読みやすいかは好みもありますが、if の中で return を 1 回だけ書く前者と、早期 return で意図を強調する後者、それぞれの「読み味」の違いを感じてみてください。
余裕があれば、本物のクラスベースで書き直してみるのもおすすめです。次のような Account クラスを別に作って、deposit メソッドの中で同じ validation をしてみると、「カプセル化された状態」と「static メソッド 1 つで済ませた状態」の違いがよく分かります。
Java
class Account {
private int balance;
public Account(int initial) {
balance = initial;
}
public void deposit(int amount) {
if (amount < 0) {
return;
}
balance = balance + amount;
}
public int getBalance() {
return balance;
}
}クラスにすると、new Account(1000) で残高 1000 の口座を作って、account.deposit(500) で入金、account.getBalance() で残高取得、という流れになります。同じロジックでもコードの形が変わると、「balance は Account のもの」「Account だけがそれを書き換えられる」という関係がはっきりして、責任の所在が見えやすくなるはずです。
カプセル化は、Java でクラスを書く以上ずっと付き合っていく考え方です。private でデータを守り、public な窓口だけを開けておく ─ この習慣を今のうちから染み込ませておくと、後で Spring や Android のような大きなフレームワークを触ったときも、「なるほど、ここは外に出したくないから private なんだな」と読み解けるようになります。
次のレッスン
次は Person クラスを作る で、Person クラスを作る を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- カプセル化 の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. カプセル化 とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- クラス名は
Solution、メソッド名はsafeAccountにすること - 引数は
int initialとint depositの 2 つ、戻り値の型はintにすること depositが0以上のときはinitial + depositを返し、depositが負のときはinitialをそのまま返すこと
入出力例
test-cases.txt
safeAccount(1000, 500) → 1500
safeAccount(1000, -200) → 1000
safeAccount(0, 100) → 100
safeAccount(500, 0) → 500
safeAccount(0, -50) → 0
safeAccount(123, 877) → 1000