新人エンジニア研修で知っておきたいカプセル化の使い方
なぜ、カプセル化の理解が重要なのか、その理由。
この記事では、弊社の新人エンジニア研修の参考にJavaを解説します。
前回は継承(拡張)について解説しました。今回はカプセル化について解説します。
カプセル化はデータ隠蔽とセットなのでデータ隠蔽についてもお話しします。これらもオブジェクト指向特有の概念です。
カプセル化とは、関連する情報と処理を1つにまとめることです。オブジェクト指向はプログラムを部品と考えて、その部品の組み合わせで大きなプログラムを作るのです。物理的なモノの世界ではエンジン廻りだけを取り替える、デスクトップパソコンの電源ボックスだけを取り替えるということが可能です。例えば、車でもパソコンでも関連する部品は近くに配置されていますね。物理的なモノの場合は、距離の問題がありますから、関連の近いものを、近いところに並べるというのは自然です。しかし、情報の世界はそうではありませんから、気をつけないとグチャグチャになりがちです。そこでカプセル化が重要になります。
データ隠蔽とは、クラスのメンバ(フィールドやメソッド)の公開範囲をできるだけ限定することです。私たちは個々の部品、車やパソコンの中身(やそのデータ)に直接触れることができるでしょうか?もちろん、強引にやれば可能ですが(良い子の皆さんはぜひ試してみてください)保証適用外になりますね。逆に言えば、中身に直接触れさせないことによって部品と更には皆さんを守っていると言うこともできます。このイメージがデータ隠蔽です。
この記事では、カプセル化にデータ隠蔽の意味も含めて解説します。
1.カプセル化の意義
カプセル化とは「関連する」情報と処理を一つのクラスにまとめることでした。
「今まで作ったクラスはどれもメンバとしてフィールドとメソッドを持っているのでカプセル化ができている」新人エンジニアであるあなたは、そう思ったかもしれません。
ポイントは“関連する”というところです。
例えば次のような一台のノートパソコンを表すクラスはどうでしょうか?
class LaptopPC{
int id;
String name;
String companyName;
String companyAdress;
String companyTelephoneNumber;
・・・
}
IDと名前までは良いとしても会社名や会社住所、会社電話番号などはLaptopPCクラスが持つべきデータではないですね。Companyクラスを別途作るべきでしょう。
クラスの設計の考え方には単一責任の原則というものがあります。
この原則は、
変更する理由が同じものは一つのクラスに集める
変更する理由が違うものは別のクラスに分ける
一つのクラスはそのクラスを利用する人に対してだけ責務を負う
という考え方です。
上記の例では会社の住所や電話番号を取得するにもLaptopPCクラスに問い合わせるということになってしまいます。また、社名が変更になった場合や会社が引っ越した場合にはLaptopPCクラスを修正することになってしまいますね。
2.データ隠蔽の意義
データ隠蔽の意義は、不用意にデータに触らせないことにありました。
例によって、Javaの標準APIにその範を求めましょう。なお、新人エンジニア研修でこの hashCode()メソッド の仕組みを理解することは求めませんのでリラックスして聞いてください。
前回、StringクラスのhashCode()メソッドまで遡りましたね。StringクラスのhashCode()メソッドを使ってこんなことができます。
package chap11;
public class Example01 {
public static void main(String[] args) {
System.out.println("A".hashCode());
System.out.println("AA".hashCode());
}
}
<実行結果>
65 2080 |
“65”は文字Aの文字コードを10進表記したものです。では、“2080”はどこから来たのでしょうか?
IDEを使ってこのStringクラスのhashCode()メソッドの定義をさかのぼりましょう。以下のコードにあるhashはStringクラスの172行目に以下のようにあります。
Java17標準API/** Cache the hash code for the string */ private int hash; // Default to 0
デフォルトは0ですね。
また、valueは以下156行目の通りStringのインスタンスを格納するchar型の配列です。どちらも"private"で宣言されていることが今回のテーマである情報隠蔽と関連していますので心に留めてください。
Java17標準API/** The value is used for character storage. */ private final char value[];
このあともStringクラスのソースを辿ると“2080”の謎は明らかになります。余裕のある研修会場では講師と一緒に追求してみてください。
ここで本題のカプセル化に戻ります。
hashは172行目で以下のように"private"で宣言されていましたね。(再掲)
Java17標準API/** Cache the hash code for the string */ private int hash; // Default to 0
つまり、非公開ということで、「このクラスからのみアクセスできるフィールドである」という宣言です。他クラスからのアクセスを拒否するという意味です。ですから、以下のようなことはできません。
package chap11;
public class Example02 {
public static void main(String[] args) {
"A".hash = 1;
}
}
上記のプログラムにはエラーが出ているはずです。
強引に実行すれば、
<実行結果>
Exception in thread "main" java.lang.Error: Unresolved compilation problem: フィールド String.hash は不可視です |
と出力されます。
ハッシュ値の性質からそれを強引に書き換えられたとしたらセキュリティ問題や整合性の問題が発生することは容易に想像できるのではないでしょうか?
つまり、このように「フィールドの値を更新するときには必ずメソッドを使って更新し、フィールドには直接アクセスできなくすること」がデータ隠蔽です。公開すべきものと非公開にすべきものを明確に分け、公開したものだけを使ってプログラミングをするようにプログラマに促しているのです。
3.アクセス修飾子
private 修飾詞は、他のクラスからアクセスできない(不可視)という意味です。このように、他のクラスからインスタンス変数を隠すことを「データ隠蔽」といいました。また、修飾子とは、クラス、フィールド、メソッドの性質を指定するものをいうのでした。その中でも特に、アクセスを制御するためのものをアクセス修飾子と呼びます。
フィールドとメソッドのアクセス修飾子を公開範囲の広いものから狭いものの順に並べて一覧にすると下表11.1のようになります。
アクセス修飾子 | 公開範囲 | クラス図での表記 |
public | どのクラスからもアクセスできる | + |
protected | 現在のクラスとそのサブクラスからのみアクセスできる | # |
なし(package-private) | 現在のクラスと同じパッケージのクラスからのみアクセスできる | ~ |
private | 現在のクラスからのみアクセスできる | - |
下図は、クラスAの4つのフィールドに対して、どのクラスのメソッド(doSomething)からアクセス可能かを矢印(➞)を使って示しています。
分かりやすいのはどこからでもアクセス可能なpublicと自クラスからのみのprivateです。同じクラスのメソッドからは全てのフィールドにアクセス可能です。
アクセス修飾子が“なし”のフィールドは同じパッケージであれば他のクラス(下図のB)のメソッドからもアクセス可能です。パッケージプライベート【 package-private 】とも呼ばれます。
やや判断に迷いが生じるのは“protected”で、同じパッケージに加えて、サブクラスからも(サブクラス自身のフィールドとして)アクセス可能です。このことはpublicとprotectedのメンバは継承されるということをしっかり押さえればそれほど難しくはありません。
なお、 protectedはWebアプリケーションでサーブレットを学ぶときに出てきます。公開範囲外からのアクセスはコンパイルエラーとなります。
ここでは、オリジナルなクラスにデータ隠蔽を施してみましょう。
例えば、カジノゲームを作成するとします。プレイヤー(Player)は最初にチップの残高(balance)を1,000持ってゲームスタートです。プレイヤーはこの残高からチップを引き出すことができますが、残高がマイナスになることはできません。
それを表現したのが以下のExample03です。
package chap11;
public class Player {
private int balance = 1000;
public void withdraw(int amount) {
if ((balance - amount) < 0) {
System.out.println("残高不足です");
} else {
balance -= amount;
System.out.println("現在の残高です:" + balance);
}
}
}
package chap11;
public class Example03 {
public static void main(String[] args) {
Player p1 = new Player();
p1.withdraw(200);
p1.withdraw(1000);
}
}
<実行結果>
現在の残高です:800 残高不足です |
このようにデータ隠蔽を使ってフィールドの値を守ることができるのです。
他のプログラマ(含む将来のあなた自身)に設計者が意図しない操作をさせない、それがデータ隠蔽の効果です。
また、フィールドにアクセスするためのメソッドをアクセサメソッド【accessor method】といいます。
Javaの基本的なクラス設計には、フィールドはprivate、メソッドはアクセサメソッドとして、publicにするというものがあります。
アクセサメソッドを使ったサンプルプログラムとして、足し算電卓のクラスを作成してみます。あくまで説明のためのサンプルですので冗長でまどろこしい処理ですがご容赦ください。
以下のCalculatorはフィールドはprivate、メソッドはpublicにしている例です。
package chap11;
public class Calculator {
private int num1, num2;
public int add() {
return this.num1 + this.num2;
}
public int getNum1() {
return num1;
}
public void setNum1(int num1) {
this.num1 = num1;
}
public int getNum2() {
return num2;
}
public void setNum2(int num2) {
this.num2 = num2;
}
}
アクセサメソッドもIDEで簡単に挿入できますので試してみてください。後でWebアプリケーションを学ぶ際に活躍しますので、お楽しみに。
※ここでは、メソッドは無条件にpublicにすべきとしているように感じられるかもしれません。しかし、本当は違います。メソッドをprivateにした方が良い場合も存在します。例えば、同じクラスからしか使わないメソッドはprivateにします。メソッドをpublicにするデメリットは、そのメソッドを使う他クラスがあった場合にメソッドのインプット・アウトプットを変更しづらくなってしまうということが挙げられます(依存性の問題)。余計な公開はしないというのがデータ隠蔽の考え方です。また、アクセサメソッドはデータ隠蔽を無効にする作用があります。むやみに作るべきではありません。
※また、クラスの外から値を参照はできるが変更はされたくないフィールドには、getterのみ実装し、setterは実装しない、ということもよくあります。このお話は、またWebアプリケーションのところで出てきます。
4.クラスとインタフェースのアクセス修飾子について
クラスやインタフェース(未出:13章参照)もアクセス修飾子を使ってパッケージ外からのアクセスを制御することができます。
公開するか公開しないかの2択ですので分かりやすいですね。(下表11.2)
アクセス修飾子 | 意味 |
public | 全てのパッケージからアクセス可能 |
なし | 同じパッケージからのみアクセス可能 |
ここでの注意点としては、クラスのアクセス修飾子はフィールドとメソッドのアクセス修飾子より優先されるということです。つまり、例えばフィールドやメソッドをpublic宣言してもクラスがpublic宣言されていないと、そのフィールドやメソッドは他のパッケージからはアクセスできません。そもそも、クラスにアクセスできないわけですから当然といえば当然ですが。
また、public修飾子をつけられるクラスは1つのjavaファイルの中に1つだけです。さらに、public修飾子をつけたクラス名とファイル名は一致する必要があります。これまでのサンプルプログラムもそうなっていたことに気づかれましたか?
5.final修飾子とは
※この話題は研修時間の都合によりスキップすることがあります。
では、次にカプセル化とは話がずれるのですが、final修飾子についてお話ししたいと思います。これまでもちょくちょく出てきたとは思うのですが、ここでまとめです。finalは「最後の」という意味ですが、まさにそんな感じです。
これまでの学習では、クラスMathのPI(標準API)がありました。
public static final double PI = 3.14159265358979323846;
Java17 標準API
初期化したら変更できない定数になるのでしたね。また、先のStringクラスのところで出てきた文字列を格納するvalueというchar配列がfinalでしたね。(再掲)
/** The value is used for character storage. */
Java17標準API
private final char value[];
文字列はイミュータブルで変更できないというのはこのように宣言されているからです。
また、クラスにfinalをつけるとサブクラスを作れなくなります。finalなクラスの代表例として、String(標準API)が挙げられます。リンクから標準APIに移動してみてください。(あるいはIDEでStringクラスの140行目を見てください)
Java17標準APIpublic final class String
となっていますね。つまり、Stringを拡張してオリジナルなサブクラスは作れないのです。誰もが勝手に自分仕様の文字列クラスを作成すると混乱するからでしょうね。
ちなみに以前でてきたMathもfinalクラスです。
さらに、メソッドにfinalをつけるとサブクラスでオーバーライドできないメソッドになります。クラスは継承させたいが、メソッドはオーバーライドさせたくないケースということになります。具体的にはオーバーライドされたらまずいメソッドにfinal宣言をするケースはあり得ると思います。しかし、新人エンジニア研修の間は特に気にしなくてよいでしょう。
これをまとめると下表11.3のようになります。
finalクラス | 継承できない |
final変数(フィールド、ローカル問わず) | 定数(再代入できない) |
finalメソッド | オーバーライドできない |
できない、できない、できない、とまさにfinalですね。
カプセル化することでオブジェクトは堅牢になるのでした。
前回見た、継承とポリモーフィズム、そして今回のカプセル化の3つをあわせてオブジェクト指向の三大要素と呼ばれることがあります。
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「カプセル化と情報隠蔽で部品の完成度を高める」方法について見てきました。
次回は、「インタフェースで直接関係のないクラスもグループとして扱う」です。
IT企業向け新人研修おすすめ資料 無料公開中 (saycon.co.jp)