Java応用の補足【オブジェクト指向】
Java応用の範囲で、
講義では直接出てこないけれど、
知っておいてほしい話などを書いていきます。
staticと非staticの違い
クラスにフィールドやメソッドを定義するとき、
staticを付ける場合とつけない場合があります。
staticを付けた場合は、それぞれクラスフィールド、クラスメソッドと呼びます。
合わせてクラスメンバと呼ぶこともあります。
staticをつけない場合は、インスタンスフィールド、インスタンスメソッドと呼びます。
合わせてインスタンスメンバと呼ぶこともあります。
違いはというと、クラスメンバはクラスで一つしか持たないのに対し、
インスタンスメンバはインスタンス毎に保持するフィールドやメソッドになります。
特徴としては
- 同一クラス内で、クラスメソッドから非static(インスタンスメンバ)にはアクセスできない
- インスタンスメソッドからstatic(クラスメンバ)にはアクセスできる
- クラスメンバはクラス名.フィールド(メソッド)でアクセスする
- インスタンスメンバはインスタンス名.フィールド(メソッド)でアクセスする
といったものがあげられます。
ルールとして覚えてしまうのは簡単ですが、
なぜそうなるのかイメージができていないと理解が深まりません。
理解を深めるために具体例で説明します。
まずはフィールドから考えてみます。
public class 生徒 { public static int 最高得点; // 全体で一つ public int 得点; // 生徒によっと異なる }
このようなクラスがあったとします。
得点はテストの点数だと思ってください。
一つの教室の中を全体と考えると、
最高得点という点数は教室の中で1つしかありません。
このような全体で一つの情報はstaticになります。
一方で、生徒の得点は生徒によって異なるため、
インスタンスごとに持つことになります。
外部からこのクラスのフィールドにアクセスするとき
生徒.最高得点
は、教室の中の最高点を知りたいだけなので、違和感はないでしょう。
しかし
生徒.得点
としても、誰の点数のことを指しているのか特定できません。
なので、
生徒 Aさん = new 生徒();
Aさん.得点;
という風に、インスタンスを使ってアクセスする必要があります。
次はメソッドです。
public class 会社員 { public static String 会社名紹介() { } public String 自己紹介() { } }
このようなクラスがあったとします。
一つの会社を全体とした場合のクラスと想定します。
会社が同じであれば、どの会社員に会社名を聞いても同じになるはずです。
そのため、staticが付くクラスメソッドになります。
一方で、自己紹介というのは、会社員一人一人みんな異なります。
このようにインスタンスに依存するメソッドは、
staticを付けないインスタンスメソッドになります。
この例で考えると、
自己紹介をするときに会社名も紹介するのは自然なことなので、
インスタンスメソッドからクラスメソッドを呼ぶことに違和感はありません。
しかし、会社名紹介のメソッドの中で自己紹介メソッドを呼ぼうとしても、
誰の自己紹介なのかを特定することができないので、おかしなことになります。
このような具体例で考えると、
というルールもしっくりくるのではないでしょうか。
@Overrideアノテーション
Javaではアノテーションと呼ばれる機能があります。
アットマーク(@)から始まる文字で、色々な種類があります。
アノテーションはコンパイル時に情報をコンパイラにお知らせしてくれたり、
サーバー起動時に情報をサーバーに読み込ませたりしてくれます。
ここではメソッドをオーバーライドする際に使用される
@Overrideについて説明します。
これはメソッド宣言部に付けることができます。
@Override public int methodA() { // }
という感じです。
このアノテーションをつけておくと、コンパイル時に、
「このメソッドはオーバーライドしますよ」とコンパイラにお知らせします。
すると何が起きるかというと、メソッドがオーバーライドになっていない場合、
コンパイルエラーにしてくれます。
それの何が良いかというと、例えば、
オーバーライドするつもりでメソッドを定義したけれど、
メソッド名でスペルミスがあったとします。
その時、アノテーションをつけていない場合は、
単に異なるメソッドが定義されたものとして、
コンパイルエラーになりません。
実際に動作させたときに、
思った通りの動きにならないことに気づきます。
しかし、アノテーションをつけておけば、
スペルミスがあった場合はコンパイルエラーになるので、
オーバーライドされていなかった!というミスをあらかじめ防ぐことができます。
メソッドをオーバーライドする際には一緒に使うような癖をつけておくと良いでしょう。
is-a関係
オブジェクト指向でも重要な概念として、is-a関係というものがあります。
ここではis-a関係について説明します。
英語で「A is a B」と書くと、AはBである。
という意味になります。
継承では、継承元と継承先の間にis-a関係が成り立っているかどうかが重要になります。
例えば、「人間 is a 生き物」とすると、
「人間は生き物である」という意味になります。
これは普通に考えて成り立ちますね。
なので、「人間」というクラスが「生き物」というクラスを継承するのは自然です。
public class 人間 extends 生き物
では次に、「パソコン」と「テレビ」という2つのクラスがあった場合を考えてみましょう。
パソコンとテレビは、両方とも画面があり、画面に情報を映し出す機能を持っています。
もし、両方のクラスで「画面に表示する」というメソッドを作成したいとすると、
片方を先に作成して、継承してしまえば、
わざわざメソッドを両方に定義する必要はなくなります。
しかし、これをis-a関係に当てはめてみると、
「パソコン is a テレビ」(またはその逆)となります。
public class パソコン extends テレビ
これは、パソコンはテレビである。とうい意味になり、不自然です。
このような場面では継承は使用するべきではありません。
継承を使用すると、特徴を受け継ぐことができ、
既に定義されているメソッドを使用することができますが、
継承をするときにはis-a関係が成り立っているかどうかを考慮するようにしましょう。
継承はしないが、同じ処理を一つにまとめたい場合は、
一般的にhas-a関係(委譲)を使用します。
また、継承関係はないけれど同一視してポリモーフィズムを活用したいという場合は、
インターフェースを使用します。
has-a関係
is-a関係と合わせてよく出てくる言葉なので、合わせて説明しておきます。
is-a関係は継承を表す言葉ですが、has-a関係は継承とは関係ありません。
「A has a B」書くと、AはBを持っている。という意味になります。
具体例で考えていきましょう。
「パソコン」というクラスがあったとします。
パソコンではメモリの容量をチェックしたり、
メモリに異常がないかチェックしたり、
メモリをクリアする処理ができるとします。
ソースコードで見ると
public class パソコン { public int メモリ容量; // その他、パソコンに関する情報 public void メモリチェック() { // メモリチェックの処理 } public void メモリクリア() { // メモリクリアの処理 } // その他、パソコンに関する処理 }
こんな感じだったとしましょう。
これでも特に違和感は感じません。
しかし、メモリに関する情報や処理は、
もっと増える可能性もあります。
また、パソコン以外の機器でも、
メモリを保持している機器は存在します。
そう考えると、メモリというのは別のクラスとして切り離して役割分担したほうが色々と都合がよさそうです。
(カプセル化の概念も絡んでますね)
実際にクラスを分けるとこんな感じ。
※実際はコンストラクタとかアクセッサとかちゃんと書くべきですが、
面倒なので省略してます。
また、ファイルも分ける必要があります。
public class パソコン { public メモリ メモリ1; public void メモリチェック() { メモリ1.メモリチェック(); } public void メモリクリア() { メモリ1.メモリクリア(); } } public class メモリ { private int 容量; public void メモリチェック() { // メモリチェックの処理 } public void メモリクリア() { // メモリクリアの処理 } }
このソースコードではパソコンクラスはメモリをフィールドとして保持している状態です。
この状態が、「パソコン has a メモリ」という状態になっています。
また、パソコンクラスの持つメモリチェックやメモリクリアの処理内容は、
メモリクラスのメソッドに処理を任せています。
このような状態を「委譲」と呼んだりします。
合わせて知っておくと良いでしょう。
継承に関する問題は具体例に置き換える
継承に関連するテストで、
コンパイルエラーになるかならないかを判断する問題を苦戦する人が多いです。
ここでは問題を解くためのテクニックをお伝えします。
例えば、以下のような問題を考えてみます
// 以下はコンパイルエラーになるか public class Main { A a = new B(); a.methodB(); } class A { public void methodA() { } } class B extends A { public void methodB() { } }
答えはコンパイルエラーになります。
しかし、変数の型はAだけど、インスタンスはBなので使えるのでは?
と思う人が多いようです。
このような問題は、クラス名がAとかBといった曖昧なものなので、
分かりにくくなっています。
もっと具体的なものに置き換えてみましょう。
// 以下はコンパイルエラーになるか public class Main { 生き物 生物 = new 人間(); 生物.働く(); } class 生き物 { public void 呼吸する() { } } class 人間 extends 生き物 { public void 働く() { } }
こう考えるとどうでしょうか。
人間は働くことができるけれど、
それは他の生き物に当てはまるでしょうか?
明らかに違和感があります。
このように、意味のないソースコードだけを読んで違和感があるかないかを考えるのは難しいですが、
身近なものに置き換えて考えることで、イメージがしやすくなります。
そうすると違和感があるかどうかも考えやすくなります。
抽象的に書かれたソースコードは、
一度具体例に落とし込んで考え、
それを抽象的に考え直すことで、
他のソースコードにも応用が利きます。
継承が難しいと感じる人は、
具体例に落とし込むというテクニックを試してみてください。
UML(クラス図)
講義の中では直接出てきませんが、
オブジェクト指向の理解を深めたり、
イメージするためにはUMLというツールが非常に役に立つので軽く紹介しておきます。
UMLは、Unified Modeling Language の略です。
主にオブジェクト指向を用いた分析や設計を図を描く場合に使われるものです。
UMLは一つの図のことを表しているのではなく、
複数の図の集合体です。
かなりたくさんの種類がありますが、
よく使用されるものは限られています。
- クラス図
- シーケンス図
- ユースケース図
あたりが使用頻度が高いです。
特に最も使用頻度が高いのはクラス図です。
UMLと言えば主にクラス図のことを指すと思ってもらって構いません。
オブジェクト指向を学習するのであれば、
クラス図の最低限の知識は押さえておきましょう。
クラス図を使いこなせるようになれば、
オブジェクト指向の言語で書かれたソースコードを読んで、
複数のクラスの関係性を整理するのにとても役に立ちます。
また、人に説明するときにも視覚的に分かりやすく、役に立ちます。
ソースコードを読んでよく混乱するという人は、
一度クラス図を学習して、図を描く癖をつけると、
理解が深まるかもしれません。
具体的な描き方などはここでは割愛します。
興味を持ったら他のサイトを参照に学習してみてください。
多重継承
Javaでは、多重継承(複数のクラスを同時に継承すること)は禁止されています。
実際にやってみるとコンパイルエラーになります。
その理由を考えてみましょう。
例えば、次のようなソースがあったとします。
public class Main { public static void main(String[] args) { C c = new C(); c.method(); } } class A { public void method() { // 処理1 } } class B { public void method() { // 処理2 } } class C extends A, B { }
CはAとBという2つのクラスを継承しています。
この場合どうなるでしょう。
Cクラスのインスタンスを作成し、
methodというメソッドを実行してみました。
この場合、AとB両方のクラスに名前と引数が同じメソッドがあるせいで、
どの処理を呼べばよいかを判断できません。
このような事態を防ぐために、
多重継承が禁止されているのです。
また、同じ多重継承でも、
インターフェース同士での継承の場合は多重継承が許されています。
それは、インターフェースは処理を実装しないため、
たとえメソッド名、引数が同じであっても不都合が起こらないからです。
多重継承に限らずですが、
禁止されているのには何かしら理由があります。
これを許してしまうとどのような不都合が起きてしまうのか、
という背景を考察する癖がつくと、
プログラミングの理解が深まり記憶にも残りやすくなります。