🍇

8.1 スタティックなメンバー(スタティックフィールド、スタティックメソッド、ユーティリティ、ライフサイクルなど)~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

8.1 スタティックなメンバー

チャプターの概要

このチャプターでは、クラスそのものが直接保持するメンバーである、スタティックメンバーについて学びます。

8.1.1 インスタンスメンバーとスタティックメンバー

スタティックメンバーとは

チャプター7.1~7.3ではクラスのメンバーについて取り上げましたが、これらのメンバーは生成したインスタンスが保持するものであり「インスタンスメンバー」と呼ばれています。
それに対して、インスタンスではなくクラスそのものが直接保持するメンバーを「スタティックメンバー」と呼びます。スタティックメンバーはクラスメンバーや静的メンバーと呼ばれることもありますが、本コースではスタティックメンバーで統一します。スタティックメンバーには、インスタンスメンバーと同様にフィールド、メソッドといった種類がありますが、コンストラクタはありません。またその他のメンバーに、スタティック初期化子があります。

インスタンスメンバーとスタティックメンバーの比較

インスタンスメンバーは、生成したインスタンスが保持するメンバーなので、インスタンスを生成しない限り、それらにアクセスすることはできません。一方スタティックメンバーは、クラスが直接保持するメンバーなので、インスタンスを生成することなくアクセス可能です。インスタンスメンバーはインスタンス単位に存在しますが、スタティックメンバーはクラス単位に存在します。インスタンスはいくつでも生成して増やすことができますが、クラス(スタティックメンバー)はJavaランタイムの中で1つしか存在しません。
このようなインスタンスメンバーとスタティックメンバーの関係は、以下の図のような関係になります。

【図8-1-1】インスタンスメンバーとスタティックメンバーの関係
image.png

クラスのライフサイクル

ここではスタティックメンバーへの理解を深めるために、クラスのライフサイクルについて説明します。
あるクラスが実行されると、JVMは、実行対象のクラスや、その中からアクセスされるその他のクラスのクラスファイル(.classファイル)をファイルシステムから検索し、メモリにロードします。このようにクラスファイルを検索し、メモリにロードする仕組みをクラスローダーと呼びます。クラスローダーがクラスファイルを検索するときに起点となるパスは、モジュールシステムが導入されている場合はモジュールパス、そうでない場合はクラスパスになりますが、詳細はここでは割愛します。クラスローダーやモジュールシステムの詳細については、『Java Advanced編』を参照してください。
クラスローダーによって読み込まれたクラス情報(一種のDNA設計情報)は、「スタティック領域」というメモリ空間にロードされます。ひとたびクラス情報がメモリ空間にロードされると、基本的には、再度同じクラス情報が読み込まれることはありません。このようにしてクラスがロードされると、後述するスタティック初期化が呼び出されます。

【図8-1-2】クラスのライフサイクル
image.png

この時点でスタティックなメンバーには、すでにアクセス可能になっています。
次にこのクラスのインスタンスが生成されると、生成されたインスタンスは「ヒープ領域」というメモリ空間に格納されます。1つのクラスからインスタンスはいくつでも生成することができますが、都度、生成されたインスタンスはヒープ領域に格納されていきます。インスタンスメンバーには、この時点で初めてアクセス可能になります。
このようにクラスのライフサイクルでは、まず「ロード」が行われ、次の段階として「インスタンス生成」が行われます。
なおヒープ領域上のインスタンスは、「もう使われない」と見なされるとマークが付き、特定のタイミングでJavaランタイムによって自動的に解放されます。このようなJavaランタイムの仕組みを、ガベージコレクションと呼びます。

以上から、クラスのライフサイクルを整理すると、以下のような3つの段階があることになります。

  1. ロード(クラスローダーによってスタティック領域へ)
  2. インスタンス生成(ヒープ領域へ)
  3. 廃棄(ガベージコレクションによってヒープから廃棄)

インスタンスメンバーとスタティックメンバー間のアクセス

インスタンスメンバーからスタティックメンバーにはアクセスが可能ですが、その逆、つまりスタティックメンバーからインスタンスメンバーに対してはアクセスすることはできません。
この理由は、前述したクラスのライフサイクルによって説明することができます。
インスタンスが生成された時点では、自身の生成元となったクラスは必ずメモリ上に存在するため、アクセス可能です。
一方でクラスがロードされたばかりの時点では、インスタンスが生成されているとは限りませんし、1つのクラスから複数のインスタンスを生成できる点を考えても、スタティックメンバーから特定のインスタンスメンバーにアクセスができないのは自明です。

8.1.2 スタティックフィールド

スタティックフィールドとは

スタティックフィールドとはクラスが直接保持する変数で、Javaランタイムの中において1つだけ存在します。
スタティックフィールドは、static修飾子を付与して、以下の構文のように宣言します。

【構文】スタティックフィールド宣言
static データ型 フィールド名;

また以下の構文のように、宣言と同時に初期値を設定することも可能です。

【構文】スタティックフィールド宣言と初期値設定
static データ型 フィールド名 =;

static修飾子を付与する以外は、インスタンスフィールドと同じです。

スタティックフィールドへのアクセス

スタティックフィールドには、以下のようにクラス名を起点にアクセスします。

【構文】スタティックフィールドへのアクセス
クラス名.フィールド

このように(インスタンス名ではなく)直接クラス名の後ろに.を記述し、アクセス対象のスタティックフィールドを指定します。存在しないスタティックフィールドを指定すると、コンパイルエラーになります。
それでは先に取り上げた「人物」を題材に、スタティックフィールドのみを持つPersonクラスを作成してみましょう。

Person
class Person {
    static String name; // 名前
    static int age; // 年齢
}

次にこのクラスの外側で、2つのスタティックフィールド、nameとageに値を代入します。

snippet_1 (Main_Person)
Person.name = "Alice";
Person.age = 25;

このように、Person.nameと記述すれば、Personクラスのスタティックなnameフィールドにアクセス可能です。スタティックフィールドに対しては、インスタンスフィールドと同じように、代入演算子=を使って値を代入します。
この2つのスタティックフィールドは、以下のようにして参照します。

snippet_2 (Main_Person)
System.out.println("name => " + Person.name); // "Alice"
System.out.println("age => " + Person.age); // 25

スタティックフィールドの用途

スタティックフィールドは、Javaランタイム内おいて1つだけしか存在しないので、1つのアプリケーションが数多くのクラスから構成される場合、どのクラスからでもアクセス可能な変数として利用することができます。このような変数を「グローバル変数」と呼びます。例えば4つのクラス、Foo、Bar、Baz、Quxがあり、Fooクラスがスタティックなフィールドとしてvalueを持っているものとします。このフィールドに対してBarクラスで何らかの値を設定すると、その値はBazクラスでもQuxクラスでも参照可能です。

【図8-1-3】グローバル変数へのアクセス
image.png

これは一見便利そうに見えますが、グローバル変数を多用すると、処理の流れや構造が把握しにくい見通しの悪いコード(俗に言う「スパゲッティコード」)になる可能性があります。「スパゲッティコード」の発生を抑え、プログラムの拡張性や保守性を確保するためには、極力グローバル変数の使用を控えるようにした方が良いでしょう。
また実際のJavaアプリケーションは、複数の処理が並列で実行されるマルチスレッド環境で動作するケースが少なくありませんが、複数の処理が同時に1つのグローバル変数にアクセスすることにより、意図しない更新が行われ、不具合を引き起こす危険性がある点にも注意が必要です。
以上の理由から、スタティックフィールドの主な用途は「定数」(チャプター8.2参照)になります。または後述するスタティック初期化子によって一度だけ値を設定したら、アクセス修飾子(チャプター10.1参照)によって可視性を制御し、参照専用の変数として利用するケースもあります。更新可能なグローバス変数として利用するケースもゼロではありませんが、マルチスレッド環境では、値の更新時に不正な処理が行われないように対策が必要です[1]

8.1.3 スタティックメソッドとスタティック初期化子

スタティックメソッドとは

スタティックメソッドは、static修飾子を付与して、以下の構文のように宣言します。

【構文】メソッド宣言
static 戻り値型 メソッド名(1 引数名1,2 引数名2, ....) {
    ....メソッド本体の処理....
}

static修飾子を付与する以外は、インスタンスメソッドと同じです。

スタティックメソッドへのアクセス

スタティックメソッドは、別クラスから呼び出すこともあれば、同一クラスの他メソッドから呼び出すこともあります。
インスタンスメンバーからスタティックメンバーへのアクセスは可能なため、呼び出し元は、インスタンスメソッドでもスタティックメソッドでも、どちらでも構いません。
別クラスから呼び出す場合は、以下の構文のように、クラス名を起点に呼び出します。

【構文】別クラスからのスタティックメソッド呼び出し
クラス名.メソッド名(引数1, 引数2, ....);

また同一クラスから呼び出す場合は、以下の構文のように直接メソッドを呼び出します。

【構文】同一クラスからのスタティックメソッド呼び出し
メソッド名(引数1, 引数2, ....);

なお存在しないスタティックメソッドを指定すると、コンパイルエラーになります。

ユーティリティ

スタティックメソッドの主な目的の1つに、汎用性の高い機能を「ユーティリティ」として提供する、というものがあります。特にユーティリティとしてスタティックメソッドだけを集めたクラスを、「ユーティリティクラス」と呼びます。ユーティリティクラスは、以下の図のように、処理を共通化するために利用します。この図では、ユーティリティクラスに定義されたスタティックメソッドA~Cは、Foo、Bar、Bazといった様々なクラスへの共通的な機能を提供します。

【図8-1-4】ユーティリティクラス
image.png

Java SEのクラスライブラリの中では、例えばMathクラスは、数値計算に関するユーティリティが集められた「ユーティリティクラス」です。
例えばMathクラスのround()メソッドは四捨五入するためのユーティリティで、以下のように呼び出します。

snippet
long x = Math.round(3.6); // 4が返る 

ユーティリティクラスの具体例

チャプター7.2で登場したCalculatorクラスは、足し算、引き算といった汎用的な機能を提供するもので、フィールド(状態)は保持していません。ここで、Calculatorクラスのコードを再掲載します。

Calculator
class Calculator {
    int add(int x, int y) {
        int result = x + y;
        return result;
    }
    int subtract(int x, int y) {
        int result = x - y;
        return result;
    }
}

これを一種のユーティリティと考え、このクラスをユーティリティクラスにすることもできます。そのように修正したクラス(StaticCalculatorクラス)のコードを、以下に示します。

StaticCalculator
class StaticCalculator {
    static int add(int x, int y) { //【1】
        int result = x + y;
        return result;
    }
    static int subtract(int x, int y) { //【2】
        int result = x - y;
        return result;
    }
}

修正点は、add()メソッド【1】とsubtract()メソッド【2】に、static修飾子を付与しただけです。Calculatorクラスではインスタンスを生成してからメソッドを呼び出す必要がありましたが、StaticCalculatorクラスの場合はその必要はありません。例えば以下のようにしてクラス名を起点に直接add()メソッドを呼び出せば、足し算の結果を受け取ることができます。

snippet (Main_StaticCalculator)
int answer = StaticCalculator.add(30, 10);

このように状態を持つ必要がない汎用的な機能を、ユーティリティクラスとして提供するケースは多々あるでしょう。
ただしクラスの責務を歪めてまで、安易にユーティリティにしてしまうことがないように留意してください。例えばPersonクラスのisAdult()メソッド(成人かどうかを判定するメソッド)は、Personクラスが保持する状態(年齢)に紐づいた判定処理なので、Personクラスのインスタンスメソッドとして定義するのが正しい設計です。繰り返しになりますが、クラスは、その責務に応じて属性と振る舞いを保持するように設計することが肝要です。

スタティック初期化子とは

スタティック初期化子とは、スタティックメンバーの一種で、クラスがロードされた時点で一度だけ呼び出されるブロックです。
スタティック初期化子は、以下の構文のように宣言します。

【構文】スタティック初期化子
static {
    ....スタティック初期化子本体の処理....
}

このようにスタティック初期化子は、static修飾子の後ろに直接ブロックを記述し、その中に処理を実装します。
スタティック初期化子の目的を、コンストラクタと比較して説明します。コンストラクタは前述したように、インスタンス生成時に呼び出され、主にインスタンスフィールドを初期化するために実装されます。それに対してスタティック初期化子は、クラスがロードされた時に呼び出され、主にスタティックフィールドを初期化するために実装します。具体的には、何らかの外部リソースを読み込んで、その情報をもとにスタティックフィールドを初期化するケースが多いでしょう。
なおスタティック初期化子の中からは、スタティックフィールドやスタティックメソッドにはアクセスが可能ですが、必然的にインスタンスフィールドやインスタンスメソッドには、アクセスできません。

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

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

  1. スタティックメンバーの特徴や、インスタンスメンバーとの違いについて。
  2. クラスのライフサイクル(ロード、インスタンス生成、破棄)や、クロスローダー・ガベージコレクションの機能について。
  3. スタティックフィールドの宣言やアクセス方法について。
  4. グローバル変数の仕組みや課題について。
  5. スタティックメソッドを宣言する方法やユーティリティの目的について。
  6. スタティック初期化子の特徴や記述方法について。
脚注
  1. マルチスレッド環境では、スタティックフィールドへの更新処理を「同期化」することで、不正な更新を回避する必要がある。詳細は『Java Advanced編』チャプター3.1を参照。 ↩︎

Discussion