カプセル化とは
-
使って欲しい機能に関係ないものは見せない
- ex: テレビ。中の回路はブラックボックスにして、利用者側には電源、画面などの機能のみ見えている
- 無関係な部分に不用意に触ることがないようにする
- クラスの機能を実現するための内部的な機能は隠し、使って欲しい機能のみ示す
アクセス修飾子
- 特定のメンバーを見せるかどうか管理
- 要件を満たす範囲でできるだけ強い制約を課すこと
- パッケージ外からのアクセスを意図してない限りパッケージプライベートを基本(Java9以降モジュールがpublic/privateの間を埋めた)
- public:全クラスからアクセス可能
- protected:現クラスと派生クラス、同じパッケージクラスのみアクセス可
- なし:現クラスと同じパッケージクラスからのみアクセス可能
- private:現クラスからのみアクセス可
フィールドのアクセス権限
- クラスのデータをフィールドで外部に公開
- インスタンスフィールドはpublic宣言するべきではない
-
読み書き許可/禁止制御できない
- フィールドはオブジェクト状態を管理するための変数
- 値の取得はしても変更は制限したい
- フィールドは単に変数なので修飾子でアクセスの可否を決めるとその値を取得変更できる
- 不変クラス:インスタンス化のタイミングで内部状態固定し変更できないクラス
- 値の妥当性を検証できない
-
内部状態の変更に左右
- ageフィールドがintではなくdoubleに変更された場合など、ageフィールドを参照する全コードが影響する
-
読み書き許可/禁止制御できない
- →アクセサーメソッドを使う!
//よくない例
public class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String show(){
return String.format("%s(%d歳)\n", this.name, this.age);
}
}
Accessor Method(アクセサーメソッド)
- オブジェクトの内部状態(=フィールド)は外部から直接アクセスできないようにして、取得も変更もメソッドを介する仕組みを実現する
- アリクサーメソッドの名前はget/set+フィールド先頭を大文字
- メソッドなので例外処理も可能(throw命令など)
- 取得時に値加工可能
- getter/setterのみ用意してフィールドを読み取り/書き込み専用にできる(読み取り専用推奨)
public class Person {
//フィールドはprivate
private String name;
private int age;
//nameフィールドのゲッター
public String getName() {
return this.name;
}
//nameフィールドのセッター
public void setName(String name) {
this.name = name;
}
//ageフィールドのゲッター
public int getAge() {
return this.age;
}
//ageフィールドのセッター
public void setAge(int age) {
if (age <= 0) {
throw new IllegalArgumentException("年齢は正数で指定してください。");
}
this.age = age;
}
public String show() {
return String.format("%s(%d歳)です。", getName(), getAge());
}
}
public class AccessorBasic {
public static void main(String[] args) {
var p = new Person();
p.setName("イチロー");
p.setAge(47);
System.out.println(p.show()); //イチロー(47歳)です。
p.setAge(-30); //エラー
}
}
不変クラス
- オブジェクトを最初に生成したところから一切値(フィールド)が変化しないクラス(推奨)
//クラスそのものをfinal宣言し拡張しない
public final class Person {
//全部private final(再代入禁止)
private final String name;
private final int age;
//コンストラクタでprivate finalフィールド初期化
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//ゲッター
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
public class ImmutableBasic {
public static void main(String[] args) {
var p = new Person("サトシ", 10);
System.out.println(p.getName()); //サトシ
}
}
- 不変性が破れてしまう例
- 参照型の代入は参照そのものを渡すのみ
- 実引数/戻り値で受け渡した値を変更すると内容はフィールドにも影響
- 例外としてString型は不変型
import java.util.Date;
public final class Person {
private final String name;
private final int age;
private final Date birth;
public Person(String name, int age, Date birth) {
this.name = name;
this.age = age;
this.birth = birth;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public Date getBirth() {
return this.birth;
}
}
//インスタンスに渡した値を後から変更する例
import java.util.Date;
public class Main {
public static void main(String[] args) {
var birth = new Date();
var p = new Person("サトシ", 10, birth);
System.out.println(p.getBirth()); //Tue Nov 03 11:26:33 UTC 2020
//インスタンス化の際に渡したオブジェクトを更新
birth.setDate(15);
System.out.println(p.getBirth()); //Sun Nov 15 11:26:33 UTC 2020
//インスタンスから取得した値を変更する例
// var p = new Person("サトシ", 10, new Date());
// System.out.println(p.getBirth()); //Tue Nov 03 11:29:05 UTC 2020
// var birth = p.getBirth();
//
// birth.setDate(15);
// System.out.println(p.getBirth()); //Sun Nov 15 11:29:05 UTC 2020
}
}
不変性が破れないようにする方法
-
引数/戻り値に受け渡す際に防御的コピーする
- オブジェクトをそのまま受け渡すのではなく複製したオブジェクトを受け渡す
-
戻り値として返す際に不変型に変換
- StringBuilder型のフィールドを外部に引き渡す際にString型に変換する
//コンストラクタ引数birth,birthフィールドからgetTmeメソッドでタイムスタンプ値をとりだし新オブジェクト生成
import java.util.Date;
public final class Person {
private final String name;
private final int age;
private final Date birth;
public Person(String name, int age, Date birth) {
this.name = name;
this.age = age;
//防御的コピー
this.birth = new Date(birth.getTime());
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public Date getBirth() {
//防御的コピー
return new Date(this.birth.getTime());
}
}