先ほど登場したテレビクラスに機能を追加しましょう。最近のデジタルテレビは録画機能を備えているものもありますので、これをクラスを通じて学んでみましょう。
以下は「テレビ」クラスに「録画」メソッドを追加したイメージになります。
これでは次回から「テレビ」クラスをインスタンス化すると、全てのテレビクラスに、録画機能がついていることになります。つまりこのクラスは「テレビ」の設計書ではなく「録画機能付テレビ」の設計書ということになってしまいます。
このような場合Java言語では継承を利用しこの問題を解決します。
「あるクラスが他のクラスの特性を引き継ぐことを継承」と呼びます。先の例でいうなら「あるクラス(録画機能付テレビ)が他のクラス(テレビ)の特性を引き継ぐ」と表現します。具体的に見てみましょう。
このように「録画機能付テレビがテレビの特性を引き継ぐ」場合「class 録画機能付テレビ extends テレビ」と表現します。記述順は先のクラスの記述順とほぼ同じで「class 継承後のクラス名 extends 継承元のクラス名」と記述します。「他のクラスの特性を引き継ぐ」という以上、継承後のクラスで継承元のクラスのメソッドや定数など(一部を除くほぼ)全ての機能を利用することができます。
これで録画機能付テレビのインスタンスを生成したいときは「録画機能付テレビ」クラスをインスタンス化、通常のテレビのインスタンスを生成したいときは「テレビ」クラスをインスタンス化、することできちんとインスタンス化を区別することができるようになりました。
先ほどのオーバーロードでの「名前」と「年齢」の出力順を逆にするプログラム変更を思い出してみましょう。ひとつのメソッドの内容を変更すると他のメソッドに影響があるように、こちらの継承の例でも「テレビ」クラスの「映像を写す」メソッドの内容を変更すると、特性を引き継いでいる「録画機能付テレビ」クラスでも内容を変更したことになります。
継承元のクラスを「親クラス」「スーパークラス」と呼び、継承後のクラスを「子クラス」「サブクラス」と呼びます。また2つのクラスを親子関係と表現したり、子クラスの子クラスのことを孫クラスと表現したりすることもあります。
実際に2つのクラスを作り、その2つを親子関係にしないさい。その親子関係のクラスのうち子クラスのインスタンスを生成し、親クラスにしかないメソッドを実行してみたり、親クラスのメソッドの内容を変更すると子クラスに影響があることを、実際に動作確認しなさい。
Java言語では特に気にする必要はないのですが、気になるキーワード「多重継承」を紹介しておきます。C++言語等いくつかの多言語では、Java言語と異なり、親クラスを2つ持つことができます。このことを「多重継承」と呼びます。
それに対しJava言語のように親クラスを1つしかもてないことを「単一継承」と呼びます。現時点では「単一継承」が特に問題に感じないと思うのですが、キーワードだけ紹介しておくことにしました。
Javaでは多重継承できないのですが、多重継承できたとしたら、どのような問題があるでしょうか。考えてみてください。
いままでのサンプルや課題ではインスタンスを生成した際、その生成した場所をメモしておく変数はインスタンス化したクラスの型でした。以下はいままで通りの表記で記述した例になります。
「あるクラスが他のクラスの特性を引き継ぐことを継承」と呼ぶはずです。以下のようなコードはエラーとならないのでしょうか。このプログラムはエラーになりません。クラスの特性を引き継いでいるのですから当たり前といえば当たり前です。
では以下のような逆のプログラムはどうでしょうか。このプログラムはエラーになります。
先ほどはエラーにならなかったのに今回はエラー。なぜでしょうか。「大は小を兼ねる」ではないですが「録画機能付テレビ」は「テレビ」の特性を全て持っていますが、「テレビ」は「録画機能付テレビ」の「録画」という特性を持っていません。なのでエラーとなってしまいました。
では以下の例はどうでしょうか。こちらもエラーになります。なぜでしょうか。
実際のインスタンスは「録画機能付テレビ」ですが、生成した場所をメモする変数は「テレビ」になっています。これを文章でたとえるなら以下のようになります。
- テレビはインスタンス化すると、メモリを10マス使用します。内訳は以下のようになります。
- 先頭の1マス目から2マス分は「電波を受信」のために使用します。
- 先頭の3マス目から4マス分は「映像を写す」のために使用します。
- 先頭の7マス目から4マス分は「音を鳴らす」のために使用します。
- 録画機能付テレビはインスタンス化すると、メモリを13マス使用します。内訳は以下のようになります。
- 先頭の1マス目から2マス分は「電波を受信」のために使用します。
- 先頭の3マス目から4マス分は「映像を写す」のために使用します。
- 先頭の7マス目から4マス分は「音を鳴らす」のために使用します。
- 先頭の10マス目から3マス分は「録画」のために使用します。
- 録画機能付テレビをインスタンス化します。そのときの生成した場所はアドレス0010番地でした。つまり0010(10進数)番地から13マス文使用しているので0022(10進数)番地まで使用しています。
- インスタンスの先頭は0010番地であるとnew演算子が知らせてくれたのでメモします。メモする変数は、今回テレビ型としました。その中に0010番地と記録します。
- メモする変数は、今回テレビ型なので、以下を呼び出せることをJava言語は認識しました。
- 0010番地から「電波を受信」のために使用していることを認識。
- 0012番地から「映像を写す」のために使用していることを認識。
- 0016番地から「音を鳴らす」のために使用していることを認識。
- 0020番地から「録画」のために使用しているが、受け取った変数がテレビ型なため「録画」について認識できず。
実際には録画機能付テレビをインスタンス化していても、変数がテレビ型なのでJava言語が認識できない機能もあるということです。
先ほどの例で、いったんテレビ型変数で録画機能付テレビのインスタンスを認識すると録画機能が認識できなくなると説明しました。ではいったん認識できなくなった録画機能を再認識するにはどうすればよいでしょうか。
このようなときJava言語ではキャスト変換を用いて、クラスの型を認識しなおします。記述順は「(再認識したいクラスの名称)」と単純に括弧でクラス名をくくるのみとなります。以下がキャスト変換のサンプルになります。この再認識を行うことによって「録画」機能を再認識することができました。
ではこの様子を行番号とともに追ってみましょう。
- まずは使用済みのメモリと使用していないメモリの状況を把握する。
- テレビ型変数を用意する。指定されたアドレスのうち、先頭の2マスは受信用プログラム、その後ろ4マスは映像用プログラム、その後ろ4マスは音声用プログラムと認識する。ただしどのメモリが先頭かは指定されていない。
- 録画機能付テレビをインスタンス化する。13マス使用するので空いているメモリは0010番地以降なら空いている。0010番地から13マス使用しインスタンス化する。その際にtv変数を0010番地にあわせる。
- 録画機能付テレビ型変数を用意する。指定されたアドレスのうち、先頭の2マスは受信用プログラム、その後ろ4マスは映像用プログラム、その後ろ4マスは音声用プログラム、その後ろ3マスは録画用プログラムと認識する。ただしどのメモリが先頭かは指定されていない。
- tv変数に入っているアドレス0010番地を、recTv変数に複写。つまりrecTv変数を0010番地にあわせる。
- recTv変数の指し示すインスタンスの録画プログラムを呼び出す。0010番地+10マスの0020番地のプログラムを呼び出す。
先ほどまでのサンプルですが親クラスと子クラスに同じ名称のメソッドがありませんでした。オーバーロードで学びましたが、パラメータが異なるのであればよいですが、同じ名称の同じパラメータのメソッドが双方に存在する場合どちらのメソッドが呼ばれるのでしょうか。
class テレビ {
電波を受信 {
}
映像を写す {
System.out.println("テレビのメソッド");
}
音を鳴らす {
}
}
class 録画機能付テレビ extends テレビ {
録画 {
}
映像を写す {
System.out.println("録画機能付テレビのメソッド");
}
}
「録画機能付テレビ」のインスタンスでは「映像を写す」メソッドは双方に存在するのですが、このような場合、子クラスのメソッドが優先して呼び出されます。もちろん「親」「子」「孫」とメソッドが存在する場合、「孫」から優先的に呼ばれます。この機能のことをオーバーライドと呼びます。
実際に3つのクラスを作り、「親」「子」「孫」の関係にしなさい。その際に3つのクラスに同一のメソッドを設置したりしなかったりいろいろなパターンを試し、動作確認を行いなさい。
現在学んだオーバーライドでは、まるまる既存機能を破棄してしまいます。先の例なら「録画機能付テレビのメソッド」と表示されても親クラスに記述されている「テレビのメソッド」は破棄します。「テレビのメソッド」「録画機能付テレビのメソッド」と連続で標準出力に出力したい、というような既存機能も使いたい場合は「super」を利用します。このように「super.メソッド名」と記述することによって、継承しなかった場合のクラスのメソッドが呼び出せるようになります。
class テレビ {
電波を受信 {
}
映像を写す {
System.out.println("テレビのメソッド");
}
音を鳴らす {
}
}
class 録画機能付テレビ extends テレビ {
録画 {
}
映像を写す {
super.映像を写す();
System.out.println("録画機能付テレビのメソッド");
}
}
では以下のサンプルではどうでしょう。ちらは「録画機能付」と標準出力に出力後、改行をせず「super.メソッド名」メソッドを実行しました。「super.メソッド名」を呼び出す順番を工夫した、良いサンプルになりました。
class テレビ {
電波を受信 {
}
映像を写す {
System.out.println("テレビのメソッド");
}
音を鳴らす {
}
}
class 録画機能付テレビ extends テレビ {
録画 {
}
映像を写す {
System.out.print("録画機能付");
super.映像を写す();
}
}
このようにオーバーライドで既存の機能を全て削除するもよし、superを用いて呼び出すもよし、工夫次第で簡単に機能追加ができるということになります。
実際に自分でも上記のようにsuperを用いて、既存機能+新規機能のように実行できるプログラムを作成し、自分でも確認しなさい。
先の解説で「final修飾子」が登場しましたが思い出してみましょう。変更できないと解説しました。この「final修飾子」ですがメソッドに付加することができます。さてメソッドがfinalとはどういうことでしょうか。メソッドを変更できない。つまりオーバーライドできないという意になります。では実際のクラスで説明しましょう。
class テレビ {
電波を受信 {
}
final 映像を写す {
System.out.println("テレビのメソッド");
}
音を鳴らす {
}
}
class 録画機能付テレビ extends テレビ {
録画 {
}
映像を写す { // 「映像を写す」メソッドはオーバーライドできない。という理由でコンパイルエラーとなる。
System.out.println("録画機能付テレビのメソッド");
}
}
メソッドに「final修飾子」を付加したようにクラスにもつけることができるでしょうか。
final class テレビ {
電波を受信 {
}
映像を写す {
System.out.println("テレビのメソッド");
}
音を鳴らす {
}
}
class 録画機能付テレビ extends テレビ { // 「テレビ」クラスは継承できない。という理由でコンパイルエラーとなる。
録画 {
}
映像を写す {
System.out.println("録画機能付テレビのメソッド");
}
}
子クラスに変更させたくない場合、もしくはクラス自体を継承させたくない等、変更させたくない場合には「final修飾子」を利用しましょう。
クラスの継承禁止、メソッドのオーバーライド禁止を実際に行い、結果がどうなるか具体的に確認しなさい。
equalsと==ですが、違いをきちんと把握しているでしょうか。まずはObject#equalsのJavadocの解説文を見てみましょう。
このオブジェクトと他のオブジェクトが等しいかどうかを示します。
equals メソッドは同値関係を実装します。
- 反射性
- 対称性
- 推移性
- 整合性
- null でない任意の参照値 x について、x.equals(null) は false を返す
Object クラスの equals メソッドは、もっとも比較しやすいオブジェクトの同値関係を実装します。つまり、すべての参照値 x と y について、このメソッドは x と y が同じオブジェクトを参照する (x==y が true) 場合にだけ true を返します。
通常、このメソッドをオーバーライドする場合は、hashCode メソッドを常にオーバーライドして、「等価なオブジェクトは等価なハッシュコードを保持する必要がある」という hashCode メソッドの汎用規約に従う必要があることに留意してください。
いろいろと書いてありますが、ここではnullことは考慮せずに話をすすめましょう。
注目すべき点は「x と y が同じオブジェクトを参照する (x==y が true) 場合にだけ true」という箇所です。皆さんが頻繁に利用してきた「==」ですがこれは比較演算子の中の「等価演算子(イコールイコールと筆者は呼んでいる)」になります。
実はこの等価演算子ですが正確に言うと使い方が2つあります。
1つめはプリミティブ型変数(intやlongのような小文字で始まる型のこと。IntegerやLongはプリミティブ型ではない)の値の比較です。左辺と右辺が同じ値であればtrueとなります。
2つめはインスタンス同士の比較です。プリミティブ型変数でないインスタンス(IntegerやLong、自作したクラスもこれに該当する)が同じインスタンスを指し示すかどうかを判定し、同じインスタンスであればtrueとなります。インスタンスの場所(アドレス)を覚える変数。この変数が2つ存在した場合にその2つが同じアドレスであるかどうか、といった判定を行うことになります。
この2つをふまえJavadocの解説文を読むと「x==y が true」の場合、equalsもtrueということなので下記が成り立つと言えましょう。
テレビ tv1 = new テレビ();
テレビ tv2 = new テレビ();
テレビ tv3 = tv2;
System.out.println(tv1 == tv2); // false
System.out.println(tv1 == tv3); // false
System.out.println(tv2 == tv3); // true
System.out.println(tv1.equals(tv2)); // false
System.out.println(tv1.equals(tv3)); // false
System.out.println(tv2.equals(tv3)); // true
では次にInteger#equalsのJavadocの解説文を見てみましょう。
このオブジェクトを指定されたオブジェクトと比較します。結果が true になるのは、引数が null ではなく、このオブジェクトと同じ int 値を含む Integer オブジェクトである場合だけです。
この文章を解説するとIntegerクラスにはintValue()というメソッドで値を取得できるのですが、このintValueの戻り値同士を==し同じintの値かという判定を行うという意味になります。
Integer tv1 = new Integer(1);
Integer tv2 = new Integer(1);
Integer tv3 = tv2;
System.out.println(tv1 == tv2); // false
System.out.println(tv1 == tv3); // false
System.out.println(tv2 == tv3); // true
System.out.println(tv1.equals(tv2)); // true
System.out.println(tv1.equals(tv3)); // true
System.out.println(tv2.equals(tv3)); // true
両者の違いはequalsの2,3番目になります。これはInteger#equalsの解説文にもあるのですがObject#equalsをオーバーライドしておりObject#equalsの動作を変更しているということを意味します。
つまり大抵の場合、自作クラスはequalsメソッドをオーバーライドしておりませんし、Object#equalsを直接呼び出しているので自作クラスのequalsは==と同様であることがほとんどということになります。
ではequalsをオーバーライドする際のルールはあるのでしょうか。それが「反射性」「対称性」「推移性」「整合性」「hashCode メソッドの汎用規約」になります。
ここで登場する任意の参照値とは「インスタンスの場所を覚えている変数のこと」である。
No. | 名称 | 解説 |
---|---|---|
1 | 反射性 | x.equals(x) == true 自分自身だからイコールだ。 |
2 | 対称性 | x.equals(y) == true なら y.equals(x) == true どちらを主眼においてもイコールだ。 |
3 | 推移性 | x.equals(y) == true かつ y.equals(z) == true なら x.equals(z) == true 3つになってもイコールだ。 |
4 | 整合性 | x.equals(y) == true もしくは false の場合、何回呼び出しても同じ結果 |
では次にObject#hashCodeのJavadocの解説文を見てみましょう。
hashCode メソッドの一般的な規則を次に示します。
- Java アプリケーションの実行中に同じオブジェクト上で複数回呼び出される場合は必ず、このオブジェクトに対する equals による比較で使われた情報が変更されていなければ、hashCode メソッドは同じ整数を一貫して返さなければならない。アプリケーションを一度終了してからもう一度同じアプリケーションを実行した場合、このメソッドから返される整数は前回の実行時に返された整数と同じであるとは限らない
- equals(Object) メソッドで 2 つのオブジェクトが等価とされた場合、どちらのオブジェクトで hashCode メソッドを呼び出しても結果は同じ整数値にならなければならない
- equals(java.lang.Object) メソッドで 2 つのオブジェクトが等価でないとされた場合は、これらのオブジェクトに対して hashCode メソッドを呼び出したときに、結果が異なる整数値にならなくてもかまわない。しかし、等しくないオブジェクトについては異なる整数値が生成されるようにすれば、ハッシュテーブルのパフォーマンスを上げることができる
できる限り、Object クラスで定義される hashCode メソッドは、異なるオブジェクトについては異なる整数値を返します。通常、これはオブジェクトの内部アドレスを整数値に変換する形で実装されますが、そのような実装テクニックは JavaTM プログラミング言語では不要です。
これを要約すると「クラスの状態として同じ値と思われるなら同じハッシュコードを返却しないさい」という意味になります。さらに同じフィールドを持つ異なるクラス(Abc1クラスとAbc2 extends Abc1)が複数あったとし、そのクラスが同じ値を保持していたとしても異なるハッシュコードを返しなさい。という意味になります。
みなさんはあまりequalsメソッドを実装することがないと思います。であれば通常はequalsは==と同じです。ですが必要に応じてequalsメソッドをオーバーライドし実装を少なくできるといいですね。
equalsメソッドをオーバーライドしたほうがよいもの。オーバーライドして意味の薄いもの。それぞれを検討しなさい。