🍇

10.1 カプセル化とアクセス修飾子(public、protected、private)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

10.1 カプセル化とアクセス修飾子

チャプターの概要

このチャプターでは、アクセス修飾子と、それによって実現される「カプセル化」の概念について学びます。

10.1.1 アクセス修飾子

アクセス修飾子とは

アクセス修飾子とは、クラスやそのメンバーに対する「可視性」、言い方を替えると「公開範囲」を指定するための修飾子です。アクセス修飾子にはpublic、protected、privateの3種類がありますが、「アクセス修飾子を付与しない状態」も可視性の一種と考えられるため、4つに分類するものとします。
それぞれのアクセス修飾子ごとの公開範囲や、そのメンバーに対してアクセス可能な要素を以下の表に示します。

公開範囲 修飾子 アクセス可能な要素(from what?)
広い public すべてのクラスからアクセス可能
protected 自クラスおよび同一パッケージのクラスと、 自身の子クラスからアクセス可能
なし 自クラスおよび同一パッケージのクラスからアクセス可能
※換言すると、異なるパッケージからはアクセス不可
狭い private 自クラスのみからアクセス可能
※換言すると、他クラスからはアクセス不可

公開範囲は、publicが最も広く、protected、「アクセス修飾子なし」、privateの順に狭まります。「アクセス修飾子なし」の状態よりも、protectedを付与した方が(直感とは異なり)可視性の範囲は広くなりますので、注意してください。
まずpublicが付与された要素に対しては、すべてのクラスからアクセスが可能です(すべてのクラスに公開される)。
次にprotectedが付与された要素に対しては、自クラスおよび同一パッケージのクラスからしかアクセスができません。ただし自クラスを継承(チャプター11.1参照)した子クラスに限っては、異なるパッケージであってもアクセスが可能です。
「アクセス修飾子なし」の要素に対しては、自クラスおよび同一パッケージのクラスからしかアクセスができません。
privateが付与された要素に対しては、他クラスからは一切アクセスができません。

アクセス修飾子を付与可能な要素

ここでは、要素ごとに付与することが可能なアクセス修飾子を整理します。
まずクラスに対して付与可能なアクセス修飾子は、publicのみです。一方、クラスのメンバーであるフィールド、メソッド、コンストラクタには、すべての修飾子が付与可能です。

修飾子 付与可能な要素
クラス publicのみ
フィールド public、protected、private
メソッド public、protected、private
コンストラクタ public、protected、private

クラスに対するアクセス修飾子と、メンバーに対するアクセス修飾子が競合する場合は、可視性が狭い方が優先されます。つまり例えばクラスが「アクセス修飾子なし」の場合は、メンバーのアクセス修飾子がpublicであったとしても、「アクセス修飾子なし」の可視性が優先されます。(クラスにアクセスできない時点で、必然的にメンバーにもアクセスできない)
以上から、クラスにはpublicを付与し、メンバー毎の可視性の制御はメンバー単位に行う、というのが基本的な方針になります。

アクセス修飾子の具体例

ここでは、具体例をもとにアクセス修飾子の仕様を説明します。
以下のように、異なるパッケージに所属するFooクラスとBarクラスがあるものとします。

  • pro.kensait.pkg1.Foo
  • pro.kensait.pkg2.Bar

Fooクラスのコードを次に示します。

pro.kensait.pkg1.Foo
package pro.kensait.pkg1;
public class Foo {
    private int x;
    public Foo(int x) {
        this.x = x;
    }
    public int getX() {
        return x;
    }
    int pow() {
        return x * x;
    }
}

このようにFooクラスでは、まずクラスそのものにpublicを付与しています。メンバーについては、xフィールドにはprivateを、コンストラクタとgetX()メソッドにはpublicを付与しています。またpow()メソッドには、アクセス修飾子を付与していません。
このFooクラスに対して、パッケージが異なるBarクラス内からのアクセスは、以下のようになります。

snippet (pro.kensait.pkg2.Bar)
Foo foo = new Foo(10); //【1】OK
int val1 = foo.x; //【2】NG
int val2 = foo.getX(); //【3】OK
int val3 = foo.pow(); //【4】NG

まずFooクラスそのものにpublicを付与しているので、基本的にこのクラスはすべてのクラスに公開されます。またコンストラクタにもpublicを付与しているので、すべてのクラスで、このクラスのインスタンスを生成可能です。従って【1】のようにインスタンスを生成可能です。
xフィールドにはprivateを付与しているので、Barクラスからxフィールドにはアクセスできません(不可視)。従って【2】のようにBarクラス内でfoo.xと記述すると、コンパイルエラーになります。
getX()メソッドにはpublicを付与しているので、すべてのクラスからアクセス可能です。従って【3】のようにgetX()メソッドは呼び出し可能です。
pow()メソッドは「アクセス修飾子なし」なので、異なるパッケージからはアクセスできません(不可視)。従って【4】のようにBarクラス内でpow()メソッドを呼び出すと、コンパイルエラーになります。

10.1.2 カプセル化

カプセル化とは

Javaでは、アクセス修飾子によってクラスが保持する属性(フィールド)を外部から隠蔽したり、ローカル変数によってデータをメソッド内に閉じ込めたりすることができます。このようにオブジェクト指向型言語において、クラスに実装された処理の独立性を高めることを「カプセル化」と呼びます。従来の手続き型プログラミング言語では、グローバル変数が引き起こす「スパゲッティコード」が課題でしたが、Javaではカプセル化によって、処理の流れや構造が分かりやすいコードを記述することが可能になります。

アクセス修飾子とカプセル化の戦略

プログラムは、開発者自身のためだけに作成するとは限りません。クラスがリポジトリにコミットされ、プロジェクトチーム内で共有され始めると、そのクラスを「他の開発者にどのように公開するか」「どのように使われるのか」を意識しなければなりません。このような点を意識しないと、当該クラスを後で修正したとき、思わぬところで不具合が発生してしまう可能性があるためです。
そのためには各メンバーに対する公開範囲を必要以上に広げすぎないよう、適切にアクセス修飾子を付与したり、カプセル化によって内部情報を隠蔽したりすることが必要です。また詳細はチャプター11.1で触れますが、finalキーワードによって、クラスとメソッドの継承やオーバーライドを制御することも重要です。

カプセル化とアクセサメソッド

ここでは、カプセル化を実現するための一般的な方法について説明します。
まずフィールドにpublicを付与することは、原則として回避します。このようにする理由は、フィールドはあくまでもクラスが内部的に保持する属性なので、外部から自由に参照したり更新したりすることができると、カプセル化が崩れてしまうためです。よってフィールドにはprivateを付与し、外部からは不可視にします。
では外部からフィールド値を参照したり更新したりする場合は、どのようにしたらよいのでしょうか。それはアクセサメソッドと呼ばれるメソッドを経由して行います。アクセサメソッドとはフィールドにアクセスするためのメソッドで、ゲッターとセッターの2種類があります。ゲッターとはフィールド値を参照するためのメソッド、セッターとは更新するためのメソッドですが、外部からのアクセスを可能にするためにアクセス修飾子はpublicにします。
アクセサメソッドは、例えばnameフィールドが対象の場合、ゲッターは"getName"、セッターは"setName"というネーミング規約(get、set直後の文字は大文字)に従う必要があります。ここでゲッターおよびセッターから、get、setを取り除き、先頭を小文字化したもの(先の例では"name")を、プロパティと呼びます。基本的にはフィールド名とプロパティ名は一致させますが、nameフィールドを参照するためのゲッターをgetFullName()メソッドにする(この場合プロパティは"fullName")ことも技術的には可能です。

カプセル化の具体例

ここでは、「人物」を表すPersonクラス(レッスン7.1.2参照)を、カプセル化するケースを考えてみましょう。
以下のコードを見てください。

pro.kensait.java.lsn_10_1_1.Person
public class Person {
    // フィールド
    private String name; // 名前
    private int age; // 年齢
    // コンストラクタ
    public Person() {
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // アクセサメソッド
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

このようにフィールドはprivateにし、フィールド値を参照・更新するためのアクセサメソッドはpublicにするのが、カプセル化の基本パターンです。
ここでこのPersonクラスのように、ゲッターとセッターが、単純にフィールド値を参照したり更新したりするだけであれば、フィールドをpublicにしても同じなのではないか、と感じるかもしれませんが、それはその通りです。
ただしアクセサメソッドがあることによって、フィールドに対する外部からのアクセスをコントロール可能になるため、以下のようなことを実現可能です。

  • 参照専用フィールドを作る。publicなゲッターのみを宣言(セッターなし)すれば、コンストラクタで初期値を代入後、フィールドを参照のみ可能(更新不可)にすることができる。
  • フィールドが後から仕様変更になった場合の影響を極小化する。例えばフルネームを表すnameフィールドが、後から苗字(lastNameフィールド)と名前(fisrtNameフィールド)に分割されたケースを考える。nameフィールドを不可視にさえしておけば、このような仕様変更があっても、公開されたgetName()メソッドの内容を「苗字と名前を文字列連結して返す」ように修正するだけで済むため、外部への影響は極小化できる。
  • フィールドをセッターを通して更新させることで、必要に応じて、設定される値の有効性を検証する。例えば「年齢(age)の有効範囲は0以上」という要件がある場合は、セッターの中で有効性を検証を行い、設定される値がマイナスの場合はエラーにする。なおこのようなチェックを「事前条件」と呼ぶ[1]

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. アクセス修飾子とは公開範囲を指定するための修飾子であり、public、protected、指定なし、privateの4段階があること。
  2. アクセス修飾子は、クラス、フィールド、メソッド、コンストラクタに付与可能であること。
  3. カプセル化とは、クラスに実装された処理の独立性を高めること。
  4. アクセサメソッドにはゲッター、セッターがあり、カプセル化を実現するためのものであること。
脚注
  1. 事前条件では、if文でメソッド引数の有効性を検証し、エラーの場合はIllegalArgumentException例外を送出するのが一般的(チャプター19.2参照)。ただし実際のJavaアプリケーションでは、有効性の検証ロジックは比較的複雑になったり、ユーザーにエラーメッセージ返却が必要になったりするケースが多いため、セッターで値を設定するより前段階で、別の方法で検証が行われるケースが大半。 ↩︎

Discussion