Step 7: クラスとインターフェース
ううむ、ずいぶんと久しぶりだねえ〜 帰国してからもう2年以上になるよ....ああ、San Jose の青い空が...
それより早くはじめてよ。ひさしぶりなんだから(いつも通りこの色は美樹ちゃん)
はいはい、そうでした。しかし、ずいぶんと久しぶりなので今までの内容がかなり古くなっているところがあるなあ〜
書き換えが必要か?でも、参考にはなると思うので許してね(時間があれば書き直します)。
そういえば、結婚おめでとう。
あ、ありがとう。それより...
はいはい、なんか、ユニマガの記事みたいなはじまりだねえー
ユニマガの記事ってなんでみんな会話形式で始まるんだろう?
そういえば、ss のお兄さんは相変わらず忙しいのかな?「ワークステーションのおと」って「の音」「ノート」どっち?
毎月大変みたい。ねえねえ、横道それはじめてるよ!!
しつれいしました。それでははじめます。今回は、一番重要なクラスとインターフェースについてです。
Section 1: 構造
Java では、すべてがクラス単位で表現されます。
まず、どんな構造なのかお決まりの 「Hello World」を表示するプログラムで見てみよう。
class HelloWorld extends Object { public static void main(String args[]) { System.out.println("Hello, World!"); } }
となります。これを、HelloWorld.java
というファイルに保存してコンパイルし Java インタプリタ上で クラス HelloWorld
を実行することで Hello, World!
と表示されます。
この例では良くわからないね。では、もう少し詳しくクラスの記述方法を説明していきます。これ以降は、Step 3 で説明したオブジェクト指向を思い出しながら読んでいくとわかりやすいかな。また、前回のオブジェクト指向の復習も兼ねて前回と重複して説明するところもあります。
オブジェクトを生成するために、あるオブジェクトに共通する性質を抽出して抽象化したクラスを定義しなければなりません。その、クラスを定義するには、class というキーワードを使用します。
class MyClass { int a; /* フィールド*/ MyClass () { ... } /* コンストラクタ */ void method() { ... } /* メソッド */ }
クラスは、内部で使用されるデータであるフィールド、処理の手続き(ふるまい)であるメソッド、オブジェクトを生成するための初期化等を行うコンストラクタで構成されています。コンストラクタは、クラス名とおなじ名前のメソッドで、インスタンスを作るときに一度だけ呼ばれます。
続いて、オブジェクト指向プログラミングの利点の一つである継承の方法だけれど、extends というキーワードを使用します。
class MyClass extends MySuperClass { /* メンバーの記述 */ }
extends に続く文字列は、スーパークラスとなるクラス名を指定します。つまり、MyClass は MySuperClass のサブクラスというわけです。extends の後に指定できるスーパークラスは一つだけです。なぜなら Java では、多重継承を許可していないからです。また、class によって定義されたクラスは、すべて Object というクラスのサブクラスになります。よって、extends が存在していない class 定義は、extends Object が省略されているものとして扱われます。
継承することで得られる効果として
- スーパークラスで定義されたフィールドやメソッドは、サブクラスで改めて定義しなくても利用できる。
- サブクラスで新しく定義したフィールドやメソッドはそのまま追加される。
- スーパークラスで定義されたものと同じ名前のフィールドやメソッドをサブクラスで定義した場合、サブクラスで定義したものが優先される。
があります。このことで、既存の機能を再定義せずに利用することで開発効率をあげることができるのがオブジェクト指向プログラミングの良い点だね。
しかし、継承をすることでいくつか問題が発生します。例えば、メソッドを記述しているときに、ローカルで使用するローカル変数とフィールドで使われているオブジェクト名が同じ場合区別がつかなくなります(実際にはローカル変数が優先される)。このような時に、明示的にオブジェクトをあらわすために this というキーワードを使用します。例えば、Circleクラスのコンストラクタである Circle の引数に radius があり、その値を Circle クラスのオブジェクト radius に代入するような場合、以下のように this を使用します。
class Circle extends Shape { private int radius; public Circle(int x, int y, int radius) { super(x, y); this.radius = radius; } ... }
同じように、自分自身のコンストラクタをあらわすには this() を使用します。
また、this のときの問題と同じように、サブクラスのメソッドで同じ名前を持つスーパークラスのフィールドやメソッドを利用した場合があります。このような時に、明示的にスーパークラスのオブジェクトやメソッドを呼び出すのに super というキーワードを使用します。指定の方法は this と同じように super.drow() といった具合で使用します。スーパークラスのコンストラクタを呼び出すには super() を使用します。
Java では、名前と型の情報だけで実装を持たないメソッドを記述することができます。しかし、実装が無いままでは、そのメソッドを呼び出すことができません。では、どのように利用するのでしょう?それは、サブクラスで再定義(実装)することで利用可能になります。このような、クラスにあらかじめ予定されている機能と実際の実装を分離するテクニックはしばしば使用されます。例えば、実際に実行するシステムによって固有の動作をする場合でも、システムに依存しない記述が可能になります。このようなサブクラスで再定義される予定であるメソッドを(実装を持たないメソッド)、abstract なメソッドと呼びます。そして、abstract なメソッドを含むクラスを抽象クラスと呼びます。抽象クラスやメソッドを宣言する場合、abstract というキーワードをつけなくてはなりません。通常のクラスがオブジェクトを生成して活用することが目的だが、抽象クラスは他のクラスのスーパークラスとなることが目的です。
abstract class MyAbsClass { int a; /* フィールド */ MyAbsClass() { ... } /* コンストラクタ */ void method1() { ... } /* メソッド */ abstract void method2(); }
さらに、抽象クラスを徹底して機能のみを完全に分離して記述したインターフェースという特別なクラスがあります。インターフェースは、メンバーに static なフィールド(データ)と abstract なメソッドしか記述できません。インターフェースは、class ではなく interface というキーワードを利用します。
interface MyInterface { int a = 10; /* フィールド */ void method1(); /* メソッド */ void method2(); }
あれ?interface のメソッドの記述に abstract が書いてないよ。
良く気付いたね。実は、interface中のメソッドは全てabstractなので指定しなくていいんだ(指定しない方が良い)。また、インターフェース中のフィールド(データ)は必ず初期化しないといけないんだ。ただし、定数で初期化する必要はありません。
インターフェースもクラスの継承を利用できます(継承というより実装ですね)。しかし、extends の後に指定できるスーパークラスはインターフェースでなければなりません。また、interface によって定義されたものは Object クラスのサブクラスではないので 通常の class 定義されたもののスーパークラスにはなりません(つまり class 定義の extends としてインターフェースを指定することは出来ない)。
interface MyInterface extends Interface1, Interface2 { /* メンバーの記述 */ }インターフェースを定義する場合、extends の後には「カンマ( , )」で区切って複数のインターフェースを指定することが出来ます。
ねえねえ、そう言えばどうして多重継承を通常のクラスでは許可していないの?
そうだね。多重継承によって高機能なクラスが構築できたり、実在の継承関係に柔軟に対応できるので便利だよね。でも、プログラムで多重継承を実現すると、『複数のスーパークラスに同じ名称の情報が含まれていた場合、どのスーパークラスの情報を優先的に受け継げば良いのか?』という問題になんらかの法則で優先順位をきめる必要が出てくるね。これが、単純な多重継承ならルール作成もなんとかなるけど、あちこちで多重継承しているものを利用しようとするとルールを考えているだけで嫌になっちゃうよね。本来開発効率をあげるという目的に反して、かえって複雑化してデバッグがしにくくなるなど効率を下げてしまうよね。
しかし、いつでも何処でも任意のクラスを複数組み合わせるのではなく、クラスを複数のスーパークラスのグループに属することができます。それを実現するのがインターフェースです。考え方としては、「泳ぐために魚」「飛ぶために鳥」を継承して泳げて空も飛べるものを作るのではなく、泳ぐインターフェースと飛ぶインターフェースを実装することで泳げて空も飛べるものを作るということです。それには、あるていど多重継承を利用するような感じでインターフェースをデザインする必要はあります。その実装方法は、implements というキーワードを使用します。
implements に続く文字列は、組み込まれるインターフェース名を指定します。implements に指定できるのは、インターフェースのみで複数指定できます。extends で継承したクラスや implements で継承したインターフェースはスーパークラスとしてみなされます(instanceof 演算子で調べると true を返す)。
class MyClass extends MySuperClass implements Interface1, Interface2 { /* メンバーの記述 */ }
機能が多いことが利点とは限らないって事なのね。
そういうこと、例えば Window システムなどの GUI がある程度制限を設けてルールを作っているのも、あれもこれもできるとユーザーインターフェースの統一が出来なくなりかえって使いにくくなるよね。Java
の場合も、インターフェースという制限された機能を利用して複雑にならないように多重継承のような効果がえられるようにしたわけだね。Javaでは、このインターフェースをどれだけうまく利用できるかでプログラミングの効率がかわってきます。Java
のプログラミングでも、生物での遺伝と同じように生まれながらに持つ必要がある機能は継承し、後天的に身に付けることが可能なふるまいはインターフェースで実装すると良いかな。
Section 2: オブジェクトの生成
オブジェクトの生成、つまりインスタンスを作るには、コンストラクタを呼び出しオブジェクトを初期化することで生成されます。コンストラクタの定義は、通常のメソッドと同じように定義できます。しかし、名称がクラスと同じで、コンストラクタが返す値はそのクラスのオブジェクトと決まっています。コンストラクタも通常のメソッドと同様に複数定義することができます。これは、名称が同じでも引き数や戻り値が異なれば別の存在としてみなされるからです。
続いて、コンストラクタの実際の呼び出し方はどうでしょうか?通常のメソッドは、「インスタンス名.メソッド」という形で呼び出したり、「メソッド」をそのまま呼び出します。しかし、コンストラクタは、new というキーワードを付けて呼び出します。
オブジェクトは、必ずコンストラクタによって生成されます。しかし、クラスの中にはコンストラクタを定義していないものもあります。このようなクラスはどのようにオブジェクトを生成するのでしょうか?コンストラクタが存在しない場合、スーパークラスの引き数を持たないコンストラクタが呼び出されるというルールがあります。引き数の無いコンストラクタが定義されていなくても同じルールが適用されます。全てのクラスのスーパークラスである Object は、引き数のないコンストラクタを持っているので最終的には必ずコンストラクタが見つかります。
public class MyClassA { protected int count; MyClassA () { count=0; } MyClassA( int count ) { this.count = count; } } public class MyClassB extends MyClassA { public MyClassB ( int count ) { super(count); } } public class Test { public static void main(String argv[]) { MyClassB test1 = new MyClassB(); // MyClassA() が呼び出される MyClassB test2 = new MyClassB(10); // MyClassB(int) -> MyClassA(int) が呼び出される ... }
実体をもたないメソッド(abstract)を持つ抽象クラスは、コンストラクタを持っていても未定義のメソッドを持っているためインスタンスを作ることはできません。
Section 3: オブジェクトの消滅
あるオブジェクトが要らなくなった場合、つまりゴミになった場合、Java ではガーベージコレクタが働き自動的にオブジェクトを消してくれます。このおかげで、プログラマはオブジェクトが要るか要らないか注意しながらプログラミングする必要がなくなるので、開発が非常に楽になります。
Java のガーベージコレクション(GC)は、オブジェクトが何処からも参照されないことを確認してから、オブジェクトを消します。
Rectangle rect = new Rectangle(0, 5, 100, 200); rect = new Rectangle(0, 5, 200, 200); // はじめに生成した Rectangle オブジェクトは // ガーベージコレクションの対象となる Circle c1 = new Circle(20, 30, 100); Circle c2 = c1; c1 = null; c2 = null; // この時点でガーベージコレクションの対象になる
ところが、メモリーリークがなくなるわけではありません。ガベージコレクションの対象になるのは、オブジェクトが参照されなくなりもう使われていないと判断された時です。しかし、もし意図しないところからオブジェクトが参照されていた場合、それがメモリリークとなってしまいます。
ねえねえ、ほんとに楽になってるの?
ま、確かに注意しなければメモリーリークになる可能性があるわけだけど、C 言語でのプログラミングのように「ここで完全に使われなくなるから
free しなくちゃ」「ここでエラーで終了した場合、free は必要かな」「この条件の時は free が必要だ」などといった free
のしわすれに対して意識しなくてもよくなるだけでずいぶん楽になるよね。
Section 4: 多様性(ポリモーフィズム)とオーバーライド
継承という利点の他に、異なるオブジェクトに同一のメッセージを送った場合、ふるまいが変わる仕組みが必要になります。例えば、四角(Rectangle)、丸(Circle)、三角(Triangle)といったクラスは、形(Shape)クラスを継承しているとします。この時、描画という行為はすべてのクラスにあるので形(Shape)クラスで定義します。しかし、実際に描画される形はそれぞれ異なります。つまり、描画するというインターフェースは形(Shape)クラスに定義するが、実装は四角(Rectangle)、丸(Circle)、三角(Triangle)といったクラスに定義しなければなりません。インターフェイスが形(Shape)クラスで定義されると、そのサブクラスである四角(Rectangle)、丸(Circle)、三角(Triangle)クラスのインスタンスを扱うときに、それが実際にどのサブクラスに属するのかを知る必要がなくなります。それがどのクラスのインスタンスであろうと形(Shape)インスタンスの一種なので、形(Shape)クラスで定義されている「描画drow()」メソッドを実行できます。そして、インスタンス自身は自分がどのクラスに属しているか知っているので、自分専用の実装を使用して描画します。このように、インターフェイスがひとつでも、オブジェクトが属するクラスによってふるまいが変わることを多様性(ポリモーフィズム)といいます。そして、ふるまいによって実装を変える行いをオーバーライドといいます。注意:オーバーロードは、名前が同じで引き数が異なるメソッドが存在することをいいます。
上記の説明を Java であらわすと一休みの時のサンプルになります。drow() メソッドがオーバーライドされていることが確認できます。
さてさて、今回はここまで。次回は、クラスのアクセス制御とパッケージについて説明します。サンプルでも出てきているけど public, protected, private といったアクセス制御に関することと、static, final, native, synchronized といった特別な修飾子、import, package といったパッケージについてです。