カプセル化でデータを守る

docs

Javaのオブジェクト指向プログラミングにおいて、カプセル化は非常に重要な考え方です。前回の記事ではクラスとオブジェクトの基本的な概念について説明しましたね。

クラスとオブジェクトの概念 – オブジェクト指向の入り口 | ToolDocs

今回は、そのクラスとオブジェクトを使ってデータを安全に扱うための「カプセル化」について、具体例を交えながら解説していきます。


皆さんは大事なものを誰にも見られないように、鍵のかかる箱に入れたりしますよね? カプセル化は、まさにそんなイメージです。クラスの中にデータ(変数)と、そのデータを操作するメソッドを一つにまとめ、外部から直接データに触れられないようにする仕組みです。これにより、データの不正な操作を防ぎ、プログラムの信頼性を高めることができます。

なぜカプセル化が必要なの?

カプセル化をしない場合、クラスの外部から直接データにアクセスできてしまいます。たとえば、ある人の銀行口座の残高が、誰でも勝手に変更できるとしたらどうでしょう? とても危険ですよね。

プログラムでも同じです。もし、クラスの重要なデータが外部から自由に書き換えられてしまうと、意図しないバグが発生したり、プログラム全体がおかしくなったりする可能性があります。カプセル化は、こうしたリスクを未然に防ぐための防御策なんです。

カプセル化のやり方

Javaでカプセル化を実現するには、主に以下の2つのステップを踏みます。

  1. フィールド(データ)をprivateにする
  2. データの操作用にpublicなメソッド(getter/setter)を用意する

それぞれ詳しく見ていきましょう。

1. フィールドをprivateにする

privateは、そのフィールドが同じクラス内からしかアクセスできないようにするアクセス修飾子です。これにより、外部から直接フィールドにアクセスして値を変更したり、参照したりすることができなくなります。

Java

public class BankAccount {
    private String accountNumber; // 口座番号(privateなので外部から直接アクセス不可)
    private double balance;       // 残高(privateなので外部から直接アクセス不可)

    // ... その他のメソッド
}

このBankAccountクラスでは、accountNumberbalanceprivateに設定されています。これにより、たとえば他のクラスからbankAccount.balance = -10000;のように直接残高をマイナスの値に書き換えるような操作はできなくなります。

2. publicなメソッド(getter/setter)を用意する

フィールドをprivateにしてしまうと、外部から一切データにアクセスできなくなってしまいます。これでは不便ですよね。そこで、データを安全に操作するための窓口として、publicなメソッドを用意します。これがgettersetterです。

  • getter: フィールドの値を取得するためのメソッドです。「get + フィールド名」という命名規則が一般的です。
  • setter: フィールドの値を設定するためのメソッドです。「set + フィールド名」という命名規則が一般的です。必要に応じて、値のチェック(バリデーション)を行うこともできます。

先ほどのBankAccountクラスにgetterとsetterを追加してみましょう。

Java

public class BankAccount {
    private String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        // 初期残高を設定する際に、setterを利用してバリデーションを行う
        setBalance(initialBalance);
    }

    // 口座番号を取得するgetter
    public String getAccountNumber() {
        return accountNumber;
    }

    // 残高を取得するgetter
    public double getBalance() {
        return balance;
    }

    // 残高を設定するsetter
    public void setBalance(double newBalance) {
        if (newBalance >= 0) { // 残高が0以上の場合のみ設定を許可
            this.balance = newBalance;
        } else {
            System.out.println("エラー:残高はマイナスにできません。");
        }
    }

    // 入金処理
    public void deposit(double amount) {
        if (amount > 0) {
            this.balance += amount;
            System.out.println(amount + "円入金されました。現在の残高:" + this.balance + "円");
        } else {
            System.out.println("エラー:0円以下の金額は入金できません。");
        }
    }

    // 出金処理
    public void withdraw(double amount) {
        if (amount > 0 && this.balance >= amount) {
            this.balance -= amount;
            System.out.println(amount + "円出金されました。現在の残高:" + this.balance + "円");
        } else if (amount <= 0) {
            System.out.println("エラー:0円以下の金額は出金できません。");
        } else {
            System.out.println("エラー:残高が不足しています。");
        }
    }
}

これで、BankAccountクラスのaccountNumberbalanceは外部から直接操作できなくなりました。代わりに、getAccountNumber(), getBalance(), setBalance(), deposit(), withdraw()といったメソッドを通じて、安全にデータにアクセスしたり、操作したりできるようになります。

カプセル化のメリット

カプセル化には、以下のようなメリットがあります。

  1. データの保護と整合性の維持 外部からの不正なアクセスを防ぎ、データの状態を健全に保つことができます。setBalance()のように、setterメソッド内で条件チェックを行うことで、不正な値が設定されるのを防げます。
  2. 保守性の向上 クラス内部の実装を変更しても、外部に公開しているgetter/setterメソッドのインターフェースが変わらなければ、外部のクラスに影響を与えません。たとえば、balanceの型をdoubleからBigDecimalに変更しても、getter/setterの返り値や引数の型を変えずに内部で変換処理をすることで、外部のコードを修正する必要がなくなります。
  3. 再利用性の向上 カプセル化されたクラスは、データの操作ロジックがそのクラス内に完結しているため、他のプログラムでも安心して再利用できます。
  4. わかりやすさの向上 クラスがどのようなデータを持っていて、どのように操作できるのかが、公開されたメソッドを見れば明確になります。

具体例でさらに理解を深めよう

例1: ユーザー情報クラス

ユーザーの名前や年齢を管理するUserクラスを考えてみましょう。年齢はマイナスになったり、異常に大きな値になったりしないように制御したいです。

Java

public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        setAge(age); // 初期化時にもsetterでバリデーション
    }

    // 名前を取得するgetter
    public String getName() {
        return name;
    }

    // 名前のsetter (今回は変更を許可しない場合、setterは用意しない選択肢もある)
    public void setName(String name) {
        this.name = name;
    }

    // 年齢を取得するgetter
    public int getAge() {
        return age;
    }

    // 年齢を設定するsetter
    public void setAge(int age) {
        if (age >= 0 && age <= 150) { // 年齢は0から150歳の範囲で許可
            this.age = age;
        } else {
            System.out.println("エラー:無効な年齢です。");
        }
    }

    public void displayUserInfo() {
        System.out.println("名前: " + name + ", 年齢: " + age);
    }
}

// 別のクラスでの利用例
public class UserApp {
    public static void main(String[] args) {
        User user1 = new User("田中", 30);
        user1.displayUserInfo(); // 名前: 田中, 年齢: 30

        user1.setAge(-5);       // エラー:無効な年齢です。
        user1.setAge(200);      // エラー:無効な年齢です。
        user1.setAge(35);
        user1.displayUserInfo(); // 名前: 田中, 年齢: 35

        // 直接ageにアクセスしようとするとコンパイルエラーになる
        // user1.age = -10;
    }
}

この例では、ageフィールドはprivateに設定され、setAge()メソッドを通じてのみ変更可能です。setAge()メソッド内で年齢が0から150の範囲内にあるかチェックすることで、不正な年齢が設定されるのを防いでいます。

vscodeで実行した結果

例2: 商品クラス

商品の価格を設定するProductクラスです。価格は常に正の値であるべきです。

Java

public class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        setPrice(price); // 初期化時にもsetterでバリデーション
    }

    // 商品名を取得するgetter
    public String getName() {
        return name;
    }

    // 価格を取得するgetter
    public double getPrice() {
        return price;
    }

    // 価格を設定するsetter
    public void setPrice(double price) {
        if (price > 0) { // 価格は0より大きい値のみ許可
            this.price = price;
        } else {
            System.out.println("エラー:価格は0より大きい値に設定してください。");
        }
    }

    public void displayProductInfo() {
        System.out.println("商品名: " + name + ", 価格: " + price + "円");
    }
}

// 別のクラスでの利用例
public class ProductApp {
    public static void main(String[] args) {
        Product item1 = new Product("Java入門書", 3500.0);
        item1.displayProductInfo(); // 商品名: Java入門書, 価格: 3500.0円

        item1.setPrice(-100.0); // エラー:価格は0より大きい値に設定してください。
        item1.setPrice(0.0);    // エラー:価格は0より大きい値に設定してください。
        item1.setPrice(4000.0);
        item1.displayProductInfo(); // 商品名: Java入門書, 価格: 4000.0円
    }
}

このProductクラスでも、priceフィールドはprivateで保護され、setPrice()メソッドを通じてのみ変更できます。setPrice()内で価格が正の値であるかをチェックすることで、誤った価格設定を防いでいます。

vscodeで実行した結果

前回の記事とのつながり

前回の記事では、クラスが「設計図」で、オブジェクトがその「実体」であると説明しました。この設計図の中に、データ(フィールド)と、そのデータを操作する手順(メソッド)を一緒に記述するのがクラスでしたね。

カプセル化は、この設計図をより強固なものにするための技術です。クラスという箱の中に、必要なものをきちんとしまい込み、外部からは決められた手順(getter/setterやその他のpublicメソッド)でしか触れないようにすることで、オブジェクトの状態を安全に保つことができるのです。

これにより、プログラムが大規模になったり、複数の人が開発に関わったりしても、データの整合性を保ちやすくなり、より堅牢なシステムを構築するための土台となります。


まとめ

今回はJavaのカプセル化について学びました。

  • カプセル化は、データ(フィールド)と、そのデータを操作するメソッドをクラス内にまとめ、外部からの直接アクセスを制限するオブジェクト指向の重要な概念です。
  • フィールドをprivateにし、publicgetter(データの取得)とsetter(データの設定)メソッドを通じてデータの操作を許可することで実現します。
  • カプセル化には、データの保護保守性の向上再利用性の向上といった多くのメリットがあります。

カプセル化を意識してコードを書くことで、より安全で保守しやすい、そして拡張性の高いプログラムを作れるようになります。最初は少し難しく感じるかもしれませんが、意識して練習することで自然と身についていきますよ!


前回の記事: クラスとオブジェクトの概念 – オブジェクト指向の入り口 | ToolDocs

コメント

タイトルとURLをコピーしました