[雑談] 試験の役に立たないJava講座 (9)
ということで引き続きインプレス社の「徹底攻略Java SE 11 Silver問題集」の章立てに沿って、少し雑談していきたいと思います。
以前よりお話ししているとおり問題集の内容に沿ったものではなく抜けているところも多々ありますので、認定資格を取得しようというのであれば問題集を購入して、そちらをしっかり勉強してください。
こちらは章立てに沿って適当に書き散らかしているものですので、試験の役には立ちません。
ということで今回は第6章、インスタンスとメソッドについて考えていきますが、ここは肝になるところなので、少し長くなるかもしれません。
オブジェクトについて
オブジェクトには二つの側面があると私は考えています。
一つは実世界のモデルとしてのオブジェクトで、オブジェクトはシステム化の対象となる実体、人やモノのふるまいをモデル化したものであるという考え方です。
オブジェクト指向の基本的な考え方としてシステム化のための分析や基本設計などがこの考え方の下に行われていると思っています。
そしてもう一つはプログラミングテクニックとしてのオブジェクトで、オブジェクトは状態と操作の集まりに過ぎず、プログラム要素の独立性を強化するものに過ぎないという考え方で、プログラミングの観点ではこの考え方が有用である場合が少なくないと考えています。
こういった2種類のとらえ方が錯綜しているためにオブジェクトは難しいと思われているのではないかというのが現時点での感想です。
プログラミングの観点でのオブジェクト
ここではプログラム要素としてのオブジェクトという考え方に絞ってオブジェクトを考えていきたいと思います。
最初にも書いたようにオブジェクトは状態と操作をまとめて一つの要素としたものになります。
オブジェクトを使うことの利点を「何とかペイ」といった電子マネーを例に考えていこうと思います。
カプセル化
例えば電子マネーではいろいろな特典、クーポンや曜日値引があったり、ユーザーステータスによってポイント付与率が違ったりといったものが用意されています。
毎回の支払いごとに加盟店のレジがこういった情報を参照して計算を行うというのはいろいろな点で難しい、新しいキャンペーンを始めるたびにレジのソフトウエアを修正しなければならない、あるいはユーザーの利用履歴など個人情報にアクセスする必要があるといった問題が発生します。
しかし、電子マネーのオブジェクトに支払に係る特典に関する処理を埋め込んでしまい、「支払」という操作をがいぶから実行すればそれらの処理が実行されるようしておけば、店舗のレジは何も考えずに「支払」操作を行うだけで値引やクーポンに応じた残高の引き落としはオブジェクトが勝手にやってくれるようにできます。
これがオブジェクトの利点となります。
今あげた電子マネーオブジェクトの「支払」操作はカプセル化というオブジェクトの基本的な性質を示しています。
電子マネーオブジェクトが「支払」という操作を提供することでユーザーがどのような特典を受けられるかはレジが知る必要がない、言い換えれば知らせないようにできるということ、これが「カプセル化」です。
継承
多くの電子マネーは先払い (プリペイド) 式ですが、最近は後払い (ポストペイド) 機能が追加されたものも出てきました。
しかし、後払いは貸し倒れになるリスクがあるため、誰にでも後払い機能を提供できるというわけではなく、先払い専用の電子マネーと後払い可の電子マネーの2種類が併存することになります。
後払い可の電子マネーであっても先払いで残高を用意することができるので、先払い専用の電子マネーと後払い可の電子マネーの処理の多くは同じものになります。
こういった場合、先払い式電子マネーのオブジェクトに差分だけを追加、変更して後払い可の電子マネーオブジェクトを作るとプログラムの重複部分がなくなり、作成も保守も楽になります。
このように差分を作りこむことで新しい機能を付加したオブジェクトを作り出すことを「継承」と呼び、オブジェクト指向プログラミングの一つの特徴となっています。
多態性 (ポリモーフィズム)
ところで、電子マネーとクレジットカードを比べてみると、形態はだいぶ異なっていますが、「支払」という機能面ではあまり違いがないことに気付きます。
しかし、実際の処理という面では先払い式の電子マネーは匿名で運用できるのに対して、クレジットカードはユーザーと必ず紐づいていないといけないといった点で違いがあり、「支払」操作が実施する内容もだいぶ異なったものになります。
とはいえ、レジから見た「支払」操作には違いがありませんので、電子マネーとクレジットカードといった異なる種類のオブジェクトに対して同一の「支払」操作を適用できるようにすると、たぶんかなり便利になります。
どのように実現するかは別として、異なる種類のオブジェクトに対して同一の操作を指示できるようにすること、これが「多態性 (ポリモーフィズム)」と呼ばれるものになります。
このようにプログラミングの視点からはオブジェクトはプログラミングを楽にする仕組みと考えてもいいと思います。
オブジェクトとクラスとインスタンス
オブジェクトは状態と操作の集まりとこれまで何回か説明してきました。
オブジェクトにどのような状態が保存されるか、どのような操作が提供されるかを記述したもの、言ってみればオブジェクトのひな型みたいなものがクラスです。
そしてクラスから作られた状態を持った実体をインスタンスと呼んでいます。
オブジェクトもインスタンスと同じくクラスから作られた状態を持った実体を指すことが多いですが、私の感覚では特定の実体を指す場合はインスタンスと呼び、より一般的に実体を呼ぶ場合はオブジェクトと呼んでいる感じですが、一般的な呼び分け方が定まってはいないようです。
それではクラスとインスタンスについて見ていきたいと思います。
クラスはフィールドとメソッド、コンストラクタで構成されます。
フィールド
フィールドはインスタンスの状態を記録するための変数で、インスタンス変数とも呼ばれます。
フィールドの値はクラスからインスタンスが生成されたときに初期化され、インスタンスが破棄されるまで有効です。
フィールドにはオブジェクトや配列など参照型の値をとることもでき、参照型のフィールドが参照しているオブジェクトや配列はインスタンスが破棄された時点で参照されなくなります。
フィールド名は慣例として英小文字で始まる名詞の名前を付け、複数の単語で構成される場合は2番目以降の単語の先頭を大文字とします。
同じフィールド名を持つフィールドを2個以上定義することはできません。
メソッド
メソッドはオブジェクトに対する操作を記述した関数で、「戻り値型 メソッド名(引数)」の形式をとります。
メソッド名は慣例として英小文字の動詞で始まる名前を付け、複数の単語で構成される場合は2番目以降の単語の先頭を大文字とします。
メソッドのオーバーロード
同じメソッド名を持つメソッドを複数定義することができます。
もちろん、引数の型と数が全く同じ場合にはどのメソッドを呼ぶべきかを判断できないのでコンパイルエラーとなりますが、引数の型と数が一致しないメソッドはいくつでも作ることができ、どのメソッドが呼ばれるかはメソッドを呼び出すコードからコンパイラが決定します。
このように同じメソッド名を持つメソッドを定義することをメソッドの「オーバーロード」と呼んでいます。
setter, getter
set、getのあとにフィールド名 (もちろん戦闘は大文字で) をつなげた名前を持つメソッドはsetter、getterと呼ばれ、該当するフィールドに値を設定する、該当するフィールドから値を取得するメソッドとする慣例があり、setterもしくは/およびgetterを持ったフィールドはプロパティとも呼ばれます。
なお、実体としてのフィールドを持たないsetterやgetter、例えば複数のフィールドから値を計算して返すgetterもプロパティと呼ばれることがあります。
setterやgetterが使用される理由はいくつかありますが、最初はフィールドで実装していたけれども単一のフィールドではプログラムに無理が出てしまい、複数のフィールドにわけざるを得ないといった場合や、設定される値の正当性を検査する必要があるといった場合に対応できるようにするというのが一つの理由です。
もう少し大きな理由があるのですが、それはまた後程触れると思います。
コンストラクタ
コンストラクタはクラス名と同じ名前の戻り値型を持たない関数で、フィールドに初期値を設定するなどの初期化処理を実行します。
オブジェクトが作られるとコンストラクタが呼ばれてインスタンスの初期化が行われ、インスタンスが使えるようになります。
インスタンスの生成
インスタンスの生成にはnewキーワードを使用して「new クラス名(パラメータ)」の形式で行います。
上記の式が実行されるとインスタンスに必要な領域が確保され、パラメータと一致する型と数の引数を持つコンストラクタが実行されます。
暗黙のコンストラクタ
コンストラクタが1個も定義されていない場合、暗黙のコンストラクタと呼ばれる引数のないコンストラクタが自動的に生成されます。
暗黙のコンストラクタはフィールドに初期化式がある場合には初期化式を実行し、初期化式のないフィールドはフィールド型によって決まる既定の値で初期化を行います。
コンストラクタからコンストラクタを呼ぶ
例えばa, bの2個のフィールドがあるクラスを考えます。
class Foo {
int a;
int b;}
これにそれぞれのフィールドの初期値を与える2個の引数を持ったコンストラクタを用意します。
class Foo {
int a;
int b;
Foo(int a, int b) {
this.a = a;
this.b = b;
}
}
なお、上のthis.aはフィールド「a」、aは引数「a」のことで、このように「this.」でフィールド名であることを明示できるので引数や変数にフィールド名と同じ名前を使うことができます。
このようにフィールド名と引数名を同じにしておくとコンストラクタを呼ぶ際にわかりやすいという利点があります。
さて、ここで引数なしのコンストラクタも必要になったとしましょう。引数なしのコンストラクタが呼ばれた場合には2個のフィールドの両方に「0」を設定するものと仮定します。
class Foo {
int a;
int b;
Foo(int a, int b) {
this.a = a;
this.b = b;
}
Foo() {
a = 0;
b = 0;
}
}
引数なしのコンストラクタでは迷うところがないのでaだけでフィールド「a」を指定できます。
このくらい簡単な場合はこれでも十分ですが、初期化手順が複雑な場合、同じコードを2か所で使うのはあまり賢くありません。
このため、以下のように別のコンストラクタを呼び出すという方法を使います。
class Foo {
int a;
int b;
Foo(int a, int b) {
this.a = a;
this.b = b;
}
Foo() {
this(0, 0);
}
}
コンストラクタでの「this(パラメータ)」は引数の型と数が一致するコンストラクタの呼び出しとなるので、ここで言えば「Foo(0, 0)」が呼び出されて期待通りの結果となります。
初期化ブロック
フィールドの初期化は条件によって初期値が変わる場合にはコンストラクタで実施し、初期値が一定である場合はフィールドの定義で初期値を代入することで行うのが一般的かと思います。
しかし、単なる代入では済まない初期化が必要な場合、例えばどこかから値をとってきて所定の計算を行ったうえで初期値を決定するといった場合にはフィールドの定義で初期値を代入するといったやり方は適用できません。
また、コンストラクタで計算を行って初期値を求めるというやり方をとってもいいのですが、コンストラクタを複数用意しなければならな場合、同じ計算を複数のコンストラクタに実装する必要が発生することもあります。
引数などを必要とはしないけれどもそれなりの計算が必要なフィールド初期化を行う手段として、初期化ブロックが用意されています。
初期化ブロックはクラス定義のトップレベルに置かれる無名のブロックで、インスタンス初期化時に自動的に実行されます。
初期化ブロックは特定の場合には非常に便利ですがフィールド初期化のコードが分散してしまい保守が面倒になる可能性があるので、まずはフィールドの初期化式、あるいはコンストラクタで処理できないかを検討したうえで、どうしてもという場合だけ使用するのが良いかと思います。
オブジェクトの生成と操作の実行
次はオブジェクトを使用する方法について考えていきます。
あるクラスのインスタンスを生成するにはnewキーワードに続けてコンストラクタ呼び出しを記述します。
AClass aClass = new AClass();
上のように記述するとAClassのインスタンスが生成され、引数のないコンストラクタで初期化されたのちにaClassという変数に参照が設定されます。
インスタンスに対する操作を行う場合はインスタンスの参照を保持する変数を指定してメソッド呼び出しを以下のように行います。
aClass.doSomething();
メソッドは特定のインスタンスに対してインスタンスの状態を変更するなどの操作を行うので、どのインスタンスに対して操作を行うのかを明示的に指定する必要があります。
それを指定するために「.」の前にインスタンスを保持する変数を指定する必要があるのです。
スタティック変数とスタティックメソッド
ここまではインスタンスの状態とインスタンスに対する操作について考えてきました。
プログラムによってはインスタンスに共通した値を保持したいといった場合や、インスタンスに関係ない機能を提供したいといった場合があります。
インスタンスに関連しない機能を持ったクラスの例として、Math (java.lang.Math) クラスがあります。
Mathクラスは指数関数や三角関数など数学的な関数と関連する定数値を集めたクラスで、すべてのメソッドとフィールドがスタティックで定義されています。
スタティックフィールド (クラス変数呼ぶ場合もある) を定義する場合は以下のように書きます。
class Math {
static double PI = 3.14; // 雑な値でごめんなさい
}
フィールド型の前にキーワード「static」を付けることでそのフィールドはスタティック変数として扱われ、その値を取得する際には以下のようにクラス名に「.」で続けてフィールド名を記述します。
double pi = Math.PI;
もちろん、スタティックフィールドに対する代入もできます。
Math.PI = 3.0;
こんなことをするといろいろ面倒が起きるので、代入できないようにすることもできますが、それについては後で説明します。
スタティックメソッド (クラスメソッドと呼ぶこともある) も同様に戻り値型の前にキーワードstaticを付けることで定義できます。
class Math {
static double abs(double a) {
if (a >= 0)return a;
else
return -a;
}
}
そしてスタティックフィールド同様にクラス名を指定してメソッドを呼び出します。
double a = Math.abs(-1.3);
スタティックメソッドとスタティックフィールドはクラス名の代わりにインスタンス名を指定しても使用することができます。
このため、スタティックフィールドとフィールド、スタティックメソッドとメソッドに同じ名前を付けることはできません。
メソッドの場合、正確にはメソッド名と引数の型、数が一致するスタティックメソッドとメソッドを定義できないということですが、間違いなく混乱のもとになるので、同じメソッド名を使わない方がいいと思います。
フィールドのfinalキーワード
フィールド型の前に「final」キーワードを指定するとそのフィールドは読み取り専用となります。
先程のPIであれば、以下のように書きます。
class Math {
static final double PI = 3.14; // 雑な値でごめんなさい
}
このようにfinalキーワードをつけることでフィールドは読み取り専用になり、代入しようとするとコンパイルエラーが発生します。
finalキーワードはスタティックフィールドだけでなくインスタンスフィールドにももちろん付けられます。
そして慣習として読み取り専用フィールドのフィールド名は大文字のみ、複数の単語で構成される場合は下線「_」で単語間を区切る書き方をします。
メソッドにもfinalキーワードを付けることができますが、それについては後程。
ということで、ここではクラスの定義、メソッド (インスタンスメソッド) とフィールド (インスタンス変数)、そしてスタティックメソッド (クラスメソッド) とスタティックフィールド (クラス変数) について概観し、コンストラクタとインスタンスの生成、スタティックフィールド・メソッドへのアクセスについて考えてきました。
0コメント