カプセル化 (private と getter)

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

カプセル化 とは

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 フィールド、gettersetter の組み合わせと、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(); // ← OK

a.balance = -999999; を書こうとした瞬間、コンパイラから balance has private access in Account のようなエラーが出ます。「触れない」のではなく「触らせない」 ─ これがカプセル化の入り口です。

public private protected のように、誰がアクセスできるかを指定するキーワードを アクセス修飾子 (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 は「今の balanceint で返す」だけのメソッド、setBalance は「渡された intbalance に代入する」だけのメソッドです。外側からは次のように使います。

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 という古くからの規約で、SpringJackson などの主要フレームワークも、この命名を前提に自動的にフィールドを認識してくれます。最初から守っておくと、後で大規模なフレームワークに乗り換えるときに無駄な書き換えが減ります。

setter で validation を行う

カプセル化の真価が出てくるのが、setter の中で 「変な値は受け付けない」と決めておけるところ です。たとえば「balance0 円以上でなければいけない」「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 をそのまま返し、deposit0 以上なら initial + deposit を返す、というだけのシンプルなロジックですが、「変な入力を境界で弾く」というカプセル化の哲学 を体験する練習問題になっています。

validation のロジックを呼び出し側ではなくクラスの中に置く、というのはとても大事な設計判断です。同じチェックを呼び出し側 10 か所に書いていると、1 か所修正を忘れただけでバグが復活します。クラスの中に 1 か所だけ書いておけば、修正もそこ 1 か所で済みます。これはカプセル化が 保守性 に直接効いてくる典型例です。

図で見る ─ public な外側と private な内側

カプセル化を絵で表すと、Account クラスのまわりに薄い壁があって、public メソッド (getBalance, deposit) だけがその壁を貫通している、というイメージになります。外側のコードからは public 出口を通る経路しか見えません。

diagram (will load when visible)

Caller (呼び出し側) から private int balance への破線は「触ろうとしてもコンパイラが止める」という意味です。public の口を通ってもらえば、その中で validation や副作用を仕掛けることができます。これが、ただの構造体ではなく オブジェクト としてクラスを設計する強みです。

動きを追ってみる ─ safeAccount の場合

課題の safeAccount(int initial, int deposit) を例に、内部でどんな処理を組むか頭の中で追ってみましょう。やることは次の通りです。

  1. 引数 initial を「最初の残高」として受け取る
  2. 引数 deposit0 以上なら initial + deposit を最終残高とする
  3. deposit が負なら、initial をそのまま最終残高とする
  4. 最終残高を intreturn する

直感的に書くなら次のような形になります。

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 @SetterJava 16 以降の record で短く書く、というテクニックもありますが、まずは「カプセル化は手間に見合った価値がある」と感じてもらえる程度の手書きに慣れることが大事です

もうひとつ要注意なのが、private にしたから絶対安全」ではない という点です。private はあくまで「他のクラスから直接アクセスさせない」ためのもので、reflectionserialization のような仕組みを使えば中身を覗ける場面はあります。それでも 99% のバグはコンパイル時の private チェックで防げるので、まずは普通のフィールドを真面目に private にする、というところから始めれば十分です。

セキュリティの世界では「壁を 1 枚作っただけで完全に守れることはない」とよく言われます。カプセル化もそれと似ていて、private入口を狭めて間違いを起こしにくくする ための仕組みであって、絶対不可侵の盾ではありません。それでも、入口を狭めるだけで多くのバグを未然に防げるのが private の偉いところです。

やってみよう

それでは右側のエディタで、Solution.safeAccount(int initial, int deposit) メソッドを完成させましょう。手順は次の通りです。

  1. safeAccount の中で、まず int balance = initial; と書いて、初期残高をローカル変数に入れる
  2. if (deposit >= 0) のときだけ balance = balance + deposit; で入金処理を行う
  3. return balance; で最終残高を返す
  4. 「実行」ボタンを押して、safeAccount(1000, 500)1500safeAccount(1000, -200)1000safeAccount(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() で残高取得、という流れになります。同じロジックでもコードの形が変わると、「balanceAccount のもの」「Account だけがそれを書き換えられる」という関係がはっきりして、責任の所在が見えやすくなるはずです。

カプセル化は、Java でクラスを書く以上ずっと付き合っていく考え方です。private でデータを守り、public な窓口だけを開けておく ─ この習慣を今のうちから染み込ませておくと、後で SpringAndroid のような大きなフレームワークを触ったときも、「なるほど、ここは外に出したくないから private なんだな」と読み解けるようになります。

次のレッスン

次は Person クラスを作る で、Person クラスを作る を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. カプセル化 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. カプセル化 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. クラス名は Solution、メソッド名は safeAccount にすること
  2. 引数は int initialint deposit の 2 つ、戻り値の型は int にすること
  3. deposit0 以上のときは 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

ヒント

main.java
main.java
学習モード

メモ

カプセル化 (private と getter)

⌘S で保存