[雑談] 試験の役に立たないJava講座 (10)
ということで引き続きインプレス社の「徹底攻略Java SE 11 Silver問題集」の章立てに沿って、少し雑談していきたいと思います。
以前よりお話ししているとおり問題集の内容に沿ったものではなく抜けているところも多々ありますので、認定資格を取得しようというのであれば問題集を購入して、そちらをしっかり勉強してください。
こちらは章立てに沿って適当に書き散らかしているものですので、試験の役には立ちません。
今回は第7章、Javaを学ぶ上で一番面白い継承やらポリモフィズムやらというところに入ってきました。
普通この辺はクラスの継承から話が始まっていくのですが、ここでは少し趣向を変えてインタフェースから話を進めていきたいと思います。
インタフェース
インタフェースは以下のような形式で書きます。
public interface AInterface {
void doSomething(int param);
}
先頭のpublic interface AInterfaceでAInterfaceという名前のインタフェースを宣言しています。
中かっこ「{}」で囲まれた中がインタフェースの本体で、ここではメソッドを1個だけ宣言しています。
2行目のvoid doSomething(int param);がメソッドの宣言で、int型の引数paramを1個とり戻り値を持たないdoSomethingメソッドを宣言しています。
インタフェースで宣言するメソッドは基本的にはそのメソッドをどのように実行するかを示す本体を持たないので宣言の終わりはセミコロン「;」になっています。
インタフェースにおけるメソッド宣言はインタフェースを実装するオブジェクトで使用できるメソッドを知らせるものとなります。
インタフェースAInterfaceを実装するオブジェクトは必ずdoSomethingメソッドが使えなければならず、その動作はAIntefaceインタフェースで定められた仕様を満たしていることが期待されます。
つまりインタフェースを実装するということはインタフェースが決めている約束あるいは契約を守っているということを宣言するということになります。
それではインタフェースを実装したクラスを見てみます。
public class AClass implements AInterface {
// いろいろあるけどとりあえず省略
@Overridepublic void doSomething(int param) {
// doSomethingメソッドの本体
}
// ほかにもあるけどとりあえず省略
}
上の例にあるようにインタフェースを実装するクラスはpublic class AClass implements AInterfaceとクラス宣言にinplementsキーワードと実装するインタフェース名を記述します。
こう書くことでAClassクラスのオブジェクトはAInterfaceが規定する契約を守っていることを宣言し、AInterface型のオブジェクトとしても使用できるということを示しています。
そしてAInterfaceインタフェースで定義されたメソッドdoSomethingを上の例に示したようにpublicを指定する必要があります。
これはインタフェースで定義されたメソッドはどのクラスからも使えることが契約となっているからです。
インタフェースの例: java.util.List
インタフェースはどのように使われるか、その実例として標準ライブラリに含まれるjava.util.Listインタフェースを見ておこうと思います。
Listインタフェースを実装したオブジェクトは以下のような機能を持つことが期待されます。
- addメソッドを使用してリストの最後にオブジェクトを追加することができる。
- インデクス付きのaddメソッドを使用してリストのインデクスでしてされた位置にオブジェクトを挿入することができる。
- getメソッドを指定してインデクスで指定した位置のオブジェクトを読み出すことができる。
- setメソッドを使用してインデクスで指定した位置のオブジェクトを変更することができる。
- removeメソッドを使用してインデクスで指定した位置のオブジェクトを削除することができる。
ほかにも多くのメソッドが定義されていますが、詳しくはリンク先のドキュメントを参照してください。
Listインタフェースを実装したクラスの例として、標準ライブラリにはjava.util.ArrayListクラス、java.util.LinkedListクラスなどがあります。
詳細はリンク先を見ていただくとわかりますが、ArrayListクラスのオブジェクトはインデクスで指定された位置のオブジェクトを効率的に取得できるのに対して、LinkedListクラスのオブジェクトはリストの途中にオブジェクトを挿入・削除する操作を効率的に実行することができます。
これら2種類のクラスにはそれぞれ得意不得意があるので、プログラム開発の途中で性能が足りないことが判明して差し替えるといったことも行われますが、利用側が特定のクラスではなくListインタフェースのオブジェクトとして運用していれば、どちらのクラスのオブジェクトを使っているかに関係なく処理を行うことができます。
このように契約部分と実際の処理を分離して、オブジェクトの利用側は契約に基づいてオブジェクトが動作することを期待してプログラムを作成し、契約に基づいてはいるが異なる特性を持ったオブジェクトを必要に応じて使い分けること、あるいは契約の基づいてはいるが異なる特性を持ったオブジェクトを用意することを「ポリモフィズム」と呼んでいます。
定数定義
インタフェースは定数を定義することができます。
定数定義の例を以下に示します。
public interface SomeInterface {
int A_CONSTANT = 42;
}
インタフェースでは定数は宣言と同時に初期化されなければなりません。
また、定数はstaticの指定がなくともクラス定数 (スタティックフィールド) 扱いとなります。
デフォルトメソッド
インタフェースのメソッド定義は本体を持たないので、メソッド本体はインタフェースを実装するクラスでクラスごとに実装しなければなりません。
これは少し面倒なので、デフォルトメソッドという仕組みが用意されています。
デフォルトメソッドは以下のように記述します。
public interface SomeInteface {
default int doSomething(double param) {
// メソッドの本体}
}
インタフェースはクラス変数やメンバ変数を持つことができないので、デフォルト実装でできることはざっくりいうと脊髄反射で返せるレベルの操作のみとなります。
インタフェースの拡張
以下のようにextends キーワードを使用することでインタフェースを拡張することができます。
public interface AnotherInterface extends SomeInterface {
// インタフェース定義
}
インタフェースを拡張することで定数の追加、メソッド定義の追加、メソッド定義へのデフォルトメソッドの追加、変更が可能となります。
拡張されたインタフェースで定数が再定義された場合、同じ名前の定数が拡張前のインタフェースと拡張したインタフェースの両方で定義されている場合には以下のようになります。
- 拡張前のインタフェースを指定して定数を参照した場合 → 拡張前のインタフェースの定数が参照されます
- 拡張後のインタフェースを指定して定数を参照した場合 → 拡張後のインタフェースの定数定義が参照されます
- 拡張後のインタフェースを実装したクラスを指定して定数を参照した場合 → 拡張後のインタフェースで定義された定数が参照されます
インタフェースで定義された定数は定義したインタフェースに紐づけられているので、どの名前で呼ぶかに依存した結果となります。
拡張されたインタフェースでメソッドが再定義された場合、同じ名前のメソッドが拡張前のインタフェースと拡張した後のインタフェースの両方で定義されている場合には以下のようになります。
- 拡張前のインタフェースで定義されたメソッドと拡張したインタフェースで定義されたメソッドの引数の数、型、並びが一致していない場合 → 別のメソッドとして認識されますので、それぞれに対して実装が必要となります。
- 拡張前のインタフェースで定義されたメソッドと拡張したインタフェースで定義されたメソッドの引数の数、型、並びが一致していて、かつ両方ともメソッド本体を持たない場合 → 戻り値の型が一致していない場合はエラーとなり、戻り値の形が一致している場合は特に何も起きません。
- 拡張前のインタフェースで定義されたメソッドと拡張したインタフェースで定義されたメソッドの引数の数、型、並びが一致していて、拡張したインタフェースで定義したメソッドがデフォルトメソッドを持っている場合 → 拡張したインタフェースで定義されたデフォルトメソッドがインタフェースを実装したクラスで使用されます。
- 拡張前のインタフェースで定義されたメソッドと拡張したインタフェースで定義されたメソッドの引数の数、型、並びが一致していて、拡張前のインタフェースで定義したメソッドはデフォルトメソッドを持っているが拡張したインタフェースはデフォルトメソッドを持っていない場合 → 拡張前のインタフェースで定義されたデフォルトメソッドは無効となり、インタフェースを実装したクラスではデフォルトメソッドを使用できません。
もちろんインタフェースを実装したクラスがメソッドの動作としてデフォルトメソッドとは異なる処理を行いたい場合には、クラスでメソッドを実装することでインタフェースで実装されたデフォルトメソッドを上書きすることができます。
クラスの継承
インタフェースはデフォルトメソッドによりメソッド本体を定義することができますが、メンバ変数を持つことができないため、デフォルトメソッドでできることは限定されてしまいます。
このため、例えばそれまでの操作の履歴に応じて動作が変わるような状態を持った契約が必要な場合にはインタフェースを実装するのではなく、クラスを継承するという方法を使用します。
例えばあるクラス (AClass) が以下のように定義されているとします。
public class AClass {
public int doOneThing(int param) {
// 何らかの処理}
public int doSomething(int param) {
// 何らかの処理
}
}
このクラスを拡張して新しい機能を追加、変更したクラス (AnotherClass) を定義するには以下のように書きます。
public class AnotherClass extends AClass {
@Override
public int doSomething(int param) {// AClassとは別の処理
}
public int doAnotherThing(int param) {
// 何らかの処理
}
}
拡張したクラス (AnotherClass) のオブジェクトは以下のような特性を持ちます。
まず、拡張したクラスで定義されたメソッド (doAnotherthing) は拡張したクラスのオブジェクトでのみ使用することができ、拡張元のクラス (AClass) のオブジェクトでは使用することができません。
拡張元だけで定義されているメソッド (doOneThing) は拡張元のクラス (AClass) と拡張したクラス (AnotherClass) のどちらのオブジェクトでも同じものを使用することができます。
そしてdoSomethingのように拡張元のクラスと拡張したクラスで異なる実装を持ったメソッドは、拡張元のクラス (AClass) のオブジェクトでは常に拡張元のクラスのメソッドが実行され、拡張したクラス (AnotherClass) のオブジェクトでは常に拡張したクラスのメソッドが使用されます。
このようにクラスを拡張して機能の追加・変更を行うことをクラスの継承と呼んでいます。
アクセス修飾子
クラス、インターフェース、フィールド、メソッド、コンストラクタにはpublicやprivateなどのアクセス修飾子を付けることができます。
クラスとインターフェースに対するアクセス修飾子は少し面倒なのでわきに置くとして、フィールド、メソッド、コンストラクタに対するアクセス修飾子について少しふれておきたいと思います。
アクセス修飾子にはpublic、protected、private、そして何もつけないの4種類があります。
public アクセス修飾子
アクセス修飾子publicはフィールド、メソッド、コンストラクタを定義したクラス以外のクラスからそのフィールド、メソッド、コンストラクタにアクセス可能であることを示します。
publicを指定したフィールド、メソッド、コンストラクタはプログラム内のどこからでも使用できるので、クラスの外部仕様として機能します。
このため、publicを指定したフィールド、メソッド、コンストラクタを変更するということはプログラムの動作に大きな影響を与える可能性があります。
一般論として書き換え可能なフィールドあるいは変更可能なオブジェクトを保持するフィールドにpublicを指定することは推奨されません。
その理由はフィールドが一般に実装の主要な部分となっており、フィールドを公開することは実装の変更に対する制約となるということが一つ。
二点目にフィールドに対するアクセスがあったことをプログラムが知ることは非常に難しいため、アクセスの記録や不具合の調査が難しくなること。
そしてフィールドをサブクラスで再定義した場合には隠蔽が行われるだけでポリモフィズムの対象とはならないことが挙げられます。
このため、定数として扱われる値を保持する場合以外はフィールドをprivate指定し、フィールドに対するアクセスはアクセサメソッド (セッタ/ゲッタ) を使用すべきです。
protected アクセス修飾子
アクセス修飾子protectedはフィールド、メソッド、コンストラクタを定義したクラスと同じパッケージ内のクラスからそのフィールド、メソッド、コンストラクタにアクセス可能であるとともに、そのクラスを継承したサブクラスからもアクセス可能であることを示します。
これが実は少し面倒な仕組みになっています。
スタティックフィールド、スタティックメソッド、コンストラクタは継承したクラスからアクセスすることができます。
インスタンスフィールドとインスタンスメソッドは継承したクラスが継承と自クラスのオブジェクトを通してアクセスすることができます。
例えばパッケージ「a」にあるクラス「A」をパッケージ「b」にあるクラス「B」が継承していたとします。
package a;
public class A {
protected void sayHello() {System.out.println("Hello.");
}
}
package b;
import a.A;
public class B extends A {public void execute() {
sayHello();
B b = new B();
b.sayHello();
A a = new A();
a.sayHello(); // これはエラー
}
}
クラス「B」はクラス「A」を拡張しているので、クラス「A」のprotectedメソッドを継承して、protectedメソッドとして公開します。
上記メソッド「execute」を見ると、最初の「sayHello()」メソッド呼び出しは継承したメソッドなので問題なく実行できます。
2番目の「b.sayHello()」メソッド呼び出しは自クラスのオブジェクトを通したアクセスですので実行が可能です。
3番目の「a.sayHello()」メソッド呼び出しはスーパークラスのオブジェクトを通してアクセスしようとしているため、アクセス不可のエラーとなります。
ここまではそれほど難しくないのですが、同一パッケージ内からは継承関係にかかわらずアクセス可能という条件が絡んでくるととても面倒なことになります。
基本的には別パッケージで拡張したクラスを継承元クラスと同じパッケージ内のクラスから使用することは避けるべきと考えます。
実際問題として別パッケージで拡張したクラスを使用するという設計は信頼できない要素を導入する可能性があるのでよろしくないと思われますし。
パッケージプライベート (アクセス修飾子を指定しない場合)
アクセス修飾子が指定されていないメソッド、フィールド、コンストラクタはそれらが定義されているクラスの属するパッケージと同一のパッケージ内のクラスからはアクセス可能ですが、他のパッケージに属するクラスからはアクセスできなくなります。
publicアクセス修飾子がクラスが公開する外部仕様を規定し、protectedアクセス修飾子がクラスを拡張するクラスに対して公開する外部仕様を規定するように、パッケージはそれに属するクラスの集合としてpublicアクセス修飾子とprotected修飾子によりパッケージの外部仕様を規定し、パッケージプライベートのフィールド、メソッド、コンストラクタはパッケージ内部での実装を規定するものと考えていいと思います。
要はパッケージを一つの要素とみなして、パッケージに属するクラスの構成はpublicアクセス修飾子やprotectedアクセス修飾子が指定されたフィールド、メソッド、コンストラクタを有するクラスを界面として内部のクラス構成はパッケージ外からは見えないものという約束の下でプログラミングを行おうという考え方があり、パッケージ内部の実装を行う手段としてパッケージプライベートが用意されているということです。
そしてprotectedアクセス修飾子はパッケージプライベートとの一貫性を保つために同一パッケージ内では継承関係にかかわらずアクセス可能となっていると思われます。
パッケージプライベートは同一パッケージ内ではpublic同等のアクセス可能性を持っているので、パッケージごとに提供する機能、パッケージごとの役割をきちんと決めたうえでパッケージごとの役割・機能に従った実装を行わなければなりません。
private アクセス修飾子
privateアクセス修飾子が指定されたフィールド、メソッド、コンストラクタはそのクラス内からのみアクセス可能となります。
クラスの実装の詳細に関連するフィールドとメソッドはprivateアクセス修飾子を指定するべきです。
前にもふれたように特にフィールドは公開する定数的なものを定義する場合を除いてprivateアクセス修飾子を指定して、フィールドへのアクセスはアクセサ (セッタ/ゲッタ) を通して行うようにすべきです。
クラスの継承・拡張
クラスの持つ機能に何らかの機能を追加したい、あるいは特定の機能のふるまいを変えたいといった場合、例えばチェーン店のメニューに地域限定メニューを追加したい、地域限定で味を変えたいといった場合にはクラスを継承、拡張することで対応することができます。
例えば以下のような「SuperClass」クラスがあったとします。
public class SuperClass {
public void doSomething() {
System.out.println("Do Something");}
public void doAnotherThing() {
System.out.println("Do Another Thing");
}
}
「SuperClass」クラスに新しいメソッド「doOneMoreThing」を追加するとともにメソッド「doAnotherThing」の動作を変更したクラス「SubClass」を作りたい場合は以下のように書きます。
public class SubClass extends SuperClass {
@Override
public void doAnotherThing() {System.out.println("なんか別のことをやる");
}
public void doOneMoreThing() {
System.out.println("さらにもうひとつなんかやる");
}
}
クラス定義でクラス名の後にextends [継承元クラス名]を記述すると「SubClass」は「SuperClass」を継承したクラスとなります。
なお、継承元のクラスはスーパークラス、継承先のクラスはサブクラスと呼ばれるので、上の例ではそれぞれを「SuperClass」、「SubClass」としています。
以下のようにサブクラス「SubClass」のインスタンスを生成すると「SubClass」のインスタンスは「SubClass」で定義された「doAnotherThing」メソッドと「doOneThing」メソッドに加えてスーパークラスで定義されている「doSomething」メソッドも使えるようになります。
public class Main {
public static void main(String[] args) {
SubClass subClass = new SubClass();subCLass.doSomething();
subClass.doAnotherThing();
subClass.doOneMoreThing();
}
}
つまり、サブクラスはスーパークラスを継承することでスーパークラスで定義した機能に対してふるまいの変更や新しい機能の追加といった拡張を行うことができます。
また、サブクラスのインスタンスはスーパークラスのインスタンスと互換性があり、以下のようにスーパークラスのインスタンスのようにふるまうことができます。
public class Main {
public static void main(String[] args) {
SuperClass superClass = new SubClass();superClass.doSomething();
superClass.doAnotherThing();
}
}
この場合、「superClass」の実体は「SubClass」のインスタンスですので、「doAnotherThing」メソッドを実行すると「SubClass」で定義された「doAnotherThing」メソッドが実行されることに注意してください。
また、「superClass」の実体は「SubClass」のインスタンスですが、「SuperClass」型の変数として扱われているため、「SuperClass」で定義されていない「doOneMoreThing」メソッドを使用することはできません。
この例ではサブクラスのインスタンスをスーパークラスのインスタンスのようにふるまわせる理由がわからないと思いますが、例えばウインドウアプリケーションのボタンや文字入力などの要素を配置したり、再描画したりといった場合、それをすべてひっくるめて描画要素として扱い、それぞれの描画要素の配置や再描画の機能を呼び出すことでそれぞれの描画要素の詳細をウインドウレベルでは気にしないといった実装にしています。
共通の側面を持つ様々な種類のオブジェクトを多数扱う場合、クラスの継承やインターフェースの実装によって実現されるポリモフィズムが有効になります。
ということで、今回はインターフェースの実装、クラスの継承、そしてアクセス修飾子について考えてみました。
0コメント