インタフェース

クラスの数が増えてくると、いくつかのクラスをまとめてグループとして扱いたくなることがあります。 本章では、同じグループに属するクラスのオブジェクトであれば、たとえ異なるクラスであっても、区別なく扱えるようにする方法を解説します。

リスト構造

前の章で長さが可変長の配列を表す ElasticIntArray オブジェクトを示しました。 ElasticIntArray の実装では、配列の長さが足りなくなるたびに、より長い配列を新しく作成し、古い配列の要素をすべて新しい配列にコピーしていました。 この実装では、多数の要素を配列に保存しようとしたときに、頻繁に配列の要素のコピーがおこり、効率が悪くなってしまいます。

しかし例えば、完全な配列は必要なく、int 型の要素の集合を表すデータ構造があればよい、与えられた値がその集合に含まれているか否かを検査できればよい、というのであれば、別な実装方法も考えられます。 ここではそのような実装のひとつとして、リンクド・リスト (linked list) 構造、あるいは短く、リスト構造、を利用した実装を紹介します。

リスト構造を使った配列の表現

リスト構造では、1 個の要素の値を保存するのに 1 個のオブジェクトを使い、このオブジェクトを鎖のように連結 (link) して集合を表現します。 まず、各要素が保存されるオブジェクトのことを IntNode オブジェクトと呼びましょう。 このオブジェクトは int 型の要素が保存されるノードを表すので IntNode とします。 Java 言語では通例、クラス名は必ず大文字からはじまるので intNode ではなく IntNode です。

IntNode クラスの定義は次のようになります。

IntNode オブジェクトは 2 つのフィールドを内蔵します。 ひとつは、このオブジェクトに保存されている要素の値を表す value フィールドです。 もうひとつは、このオブジェクトに連結されている次の IntNode オブジェクトを表す next フィールドです。 このフィールドには、次の IntNode オブジェクトの参照番号が代入されますから、このフィールドの値を読み取ることで、鎖状につながった IntNode オブジェクトを次々にたどることができます。

IntNode のコンストラクタは、value フィールドの初期値を引数にとり、value に代入します。 next フィールドの初期値は null (空) です。 null は参照型の定数で、任意の参照型の変数やフィールドに代入できます。 この定数は、変数やフィールドが、現在どのオブジェクトも表していないことを意味する、特別な参照番号 (しばしば 0 番) です。

リスト構造を使って集合を表現するオブジェクトのクラスを IntList としましょう。

集合の要素はそれぞれ IntNode オブジェクトに保存され、それらのオブジェクトは連結されて 1 本の鎖にされます。 この鎖の先頭のノードを表すのが head フィールドです。 最初、IntList オブジェクトが表す集合は空ですから、head フィールドの初期値は null です。

集合に要素を 1 つ追加するときには add メソッドを呼びます。 このメソッドは、要素を保存するために IntNode オブジェクトを 1 つ作り、鎖の先頭につなぎます。 まず

で新しい IntNode オブジェクトを作ります。 次に、それまで鎖の先頭だった IntNode オブジェクトを、新しく作った IntNode オブジェクトの次のノードとします。

鎖の先頭だった IntNode オブジェクトは head フィールドが表します。 今、変数 node は新しく作った IntNode オブジェクトを表しますから、このオブジェクトの next フィールドに IntList オブジェクトの head フィールドの値を代入します。 最初、鎖に 2 つの IntNode オブジェクトがつながっていたとすると、この代入の結果、オブジェクトの間の関連は下の図のようになります。 新しく作った IntNode オブジェクトは参照番号 212 のオブジェクトです。

最後に、

で IntList オブジェクトの head フィールドが新しく作った IntNode オブジェクトを表すように変更します。 以上で鎖の先頭に新しく作った IntNode が挿入されたことになります。

ある値が集合に含まれているか否かを調べるには find メソッドを呼びます。 このメソッドは、その値が含まれていれば true を、そうでなければ false を戻り値として返します。 find メソッドは、鎖の先頭のノードから順に調べてゆき、調べている値が途中で見つかれば true を返します。 また鎖の最後のノードの value フィールドの値は null なので、変数 node の値が null になったら探索を終了して false を返します。

なお find メソッドの中に

という式がありますが、左辺は node が表す IntNode オブジェクトの value フィールドの値を、右辺は仮引数 value の値です。

IntList クラスにはあと 2 つメソッドが定義されています。 size メソッドは集合の要素の個数を返すメソッド、get メソッドは集合の要素を取り出すためのメソッドです。 get は仮引数 index を受け取り、index 番目の要素を返します。 もし仮引数 index の値が負であるか、要素の総数より大きい場合には、このメソッドは正しく動作しません。

このように定義した IntList クラスは次のようにして使います。

find メソッドの戻り値は boolean 型なので、println メソッドは 戻り値に応じて true または false と表示します。

配列を使った集合の表現

IntList オブジェクトはリスト構造を使って集合を表現します。 リスト構造を使うと新しい要素の追加は、集合に含まれている要素の個数にかからわず一定の時間でおこなえます。 比較のため、配列を使って集合を表現する IntArray クラスを以下に示します。 このクラスは前章の ElasticIntArray とほとんど同じですが、IntList クラスと同じメソッドをもつように修正してあります。

IntArray オブジェクトは集合の要素を配列に保存します。 array フィールドは要素を保存する配列を表し、num フィールドは現在までに保存されている要素の個数を表します。 はじめ、array フィールドが表す配列の長さは 8 で、要素は空ですから num フィールドの値は 0 です。

add メソッドは配列の末尾に新しい要素を追加します。 配列の末尾は num フィールドが表しますから、array[num] に追加する集合の要素を代入し、その後、num の値を 1 増やします。 ++ 演算子は num の右側についていることに注意してください。 また、配列のインデックスは 0 から始まるので、既に配列に保存されている集合の要素の数が num 個であるとき、新しく追加される要素が代入される場所のインデックスは num です。 num + 1 ではありません。

num フィールドの値が配列の長さ array.length 以上になったとき、add メソッドは、配列をより長いものに作り直します。 新しい要素を追加する前に、長さを 8 増やした配列を新たに作成し、古い配列の内容をコピーし、古い配列と交換します。 8 増やすことに特別な意味はありません。 8 は 2 の 3 乗で 2 進数では切りのよい数字であるため、8 増やすことにしました。 8 の代わりにもっと大きい数を選べば、配列が一杯になって配列を作り直す回数が減りますが、実際に保存されている要素の個数に対して配列の長さが長すぎ、無駄に長い配列になる可能性が高くなります。 長い配列を作成すると、その分多くのメモリを消費しますから、あまり無駄に長い配列を作成するのは好ましくありません。

find メソッドは、与えられた値が集合に含まれるか否かを調べます。 リスト構造と異なり、集合の要素が配列に保存されているので、インデックス 0 から num - 1 までの配列の各要素を順に調べるだけです。 インデックスは 0 から始まるので、num 個の要素の最後のインデックスは num - 1 となります。

size メソッドは集合の要素の個数を返すメソッド、get メソッドは集合の要素を取り出すためのメソッドです。 get は仮引数 index を受け取り、index 番目の要素を返します。 最後の set メソッドは仮引数 index と value を受け取り、index 番目の要素の値を value の値に変更するためのものです。 get や set メソッドの仮引数 index の値が負であるか、要素の総数より大きいと、これらのメソッドは正しく動作しません。

IntList と IntArray の比較

IntList と IntArray には同じ機能をもったメソッドが定義されていますが、それぞれのメソッドの実行時間まで同じではありません。 したがって状況に応じて 2 つのクラスを使い分けなければなりません。

まず新しい要素を集合に加える add メソッドですが、IntList では既に集合に含まれている要素の個数にかかわらず一定で、常に 3 ステップで実行を終了します。 一方、IntArray の add メソッドは、場合によっては配列の作り直しをするので、実行時間が一定ではありません。 配列を作り直す場合、要素のコピーをおこないますが、これは既に集合に含まれている要素の個数回の代入演算を必要とします。 したがって、最悪の場合、集合の要素の個数に比例する時間が実行にかかるといえます。

一方、find メソッドの実行時間はあまり変わりません。 どちらも if 文による比較を、最悪の場合、集合の要素の個数回繰り返します。 1 回の比較にかかる時間は IntList と IntArray とで異なりますが、おおよそ同じと考えることができます。

IntList の方が実行時間がかかるのは get メソッドです。 IntArray の場合、get メソッドの実行時間は集合の要素の個数にかかわらず一定で、1 ステップで実行を終了します。 しかし IntList の場合、仮引数 index で指定された要素にたどりつくまで、鎖状につながったノードを順にたどらなければなりません。

while 文で index 回の繰り返しが発生しますが、繰り返し回数は最大で集合の要素の個数回です。

上に示した定義では IntList の size メソッドも、集合の要素の個数回の繰り返しが発生するので、IntArray の size メソッドに比べて長い実行時間がかかります。 しかし IntList の size メソッドは、定義を工夫することで、IntArray の size メソッドと同様に 1 ステップで実行できるようになります。 IntArray では、現在までに集合に保存された要素の個数を num フィールドに保存していますが、同様のフィールドを IntList にも用意すればよいのです。 例えば次のように IntList の定義を修正します。

add メソッドが呼ばれて新しい要素が追加されるたびに num フィールドの値を 1 増やせば、num フィールドには常に集合の要素の個数が保存されることになります。 そうすると size メソッドは、num フィールドの値を戻り値として返すだけの短いものになります。

しかしながら、このように修正すると、add メソッドの実行時間が若干長くなり、またオブジェクト内に本来余分なフィールドが 1 つ増えることになります。 余分なフィールドが増えるということは、そのオブジェクトを作成するたびに、余分にメモリを消費するということです。 size メソッドが頻繁に呼ばれ、size メソッドの実行時間がプログラム全体の実行時間に大きな影響をおよぼすなら、上のような修正をほどこすべきです。 一方、size メソッドがほとんど呼ばれないのなら、わざわざ上のような修正をおこなう必要はありません。

インタフェース

int 型の値の集合を表すオブジェクトひとつとっても、IntList と IntArray の 2 つの実装方法があり、状況によって使い分けなければなりません。 ところがそのように使い分けることにすると、同じようなプログラムを 2 つのクラス用に別々に用意しなければならなくなります。

和集合の計算

たとえば今、2 つの集合の和集合を計算するメソッドが必要になったとします。 まず IntList オブジェクトで表された集合 a と b を引数にとり、b の全ての要素を a に追加するメソッド sum を示します。

sum メソッドは static メソッドになっていますが、これは sum メソッドは this も Example オブジェクトのフィールドも利用する必要がないからです (そもそも Example オブジェクトはフィールドを内蔵しません)。 sum を通常の static でないメソッドとして定義してもよいのですが、static メソッドにすれば Example オブジェクトを作らずに、簡単に呼び出すことができます。

sum メソッドは引数 b が表す集合の要素をひとつずつ取り出し、その要素が引数 a が表す集合に既に含まれていないようなら追加します。 途中に

という if 文がありますが、if 文の条件を表す式は、a.find(v) の戻り値が false であれば、という意味です。 ! は NOT 演算子ですから、!a.find(v) の計算結果は a.find(v) の戻り値の true または false を反転させたものになります。

さて上で示した sum メソッドは、IntList オブジェクトで表された集合の和集合を計算するメソッドです。 IntArray オブジェクトで表された集合の和集合を計算するには別のメソッドが必要です。

メソッドの名前は同じ sum ですが、仮引数の型が異なるので、最初に示した sum とは区別されます。 したがって両方の sum メソッドを Example クラスに定義することができます。 実引数を IntList オブジェクトを実引数にして呼び出せば最初に示した sum メソッドが、IntArray オブジェクトを実引数にして呼び出せば後に示した sum メソッドが呼ばれます。

ところが、もし片方の実引数を IntList オブジェクト、もう一方の実引数を IntArray オブジェクトにして呼び出そうとすると、それは誤ったプログラムとなります。 そのような組み合わせの実引数を受け取る sum メソッドは定義されていないからです。 必要ならば次のように別途定義して Example クラスに追加しなければなりません。

これは引数 a の型が IntList、b の型が IntArray の場合のための sum メソッドです。 引数 a の型が IntArray、b の型が IntList の場合の sum メソッドも同様に定義します。

結局、実引数の組み合わせにより 4 通りの sum メソッドを定義しました。 しかし各 sum メソッドの中身は完全に同じであることがわかります。 中身が完全に同じなら 1 つの sum メソッドに統合できるとよいのですが、仮引数 a と b の型が異なるので、そのままでは統合できません。 Java 言語は全ての変数や引数に型をつけ、その型に当てはまらない値が代入されたときはプログラムの誤りと見なします。 これは非常に有用な機能なのですが、sum メソッドの例に限っては、ある意味で邪魔な機能だといえます。 本来、sum の定義では IntList オブジェクトも IntArray オブジェクトも引数にとれるはずなのですが、引数に型をつけた瞬間、どちらか一方の種類のオブジェクトしか引数にとれなくなってしまうのです。 これを回避するためには、型の取り扱いをもう少し工夫しなければなりません。

インタフェースの定義

4 通りの実引数の組み合わせに 1 つで対応できる sum メソッドを定義するためには、IntList と IntArray の両方を包含する新しい型を定義します。 この新しい型を IntSet としましょう。 そうすると、4 通りの実引数の組み合わせに対応した sum メソッドを次のように定義できます。

仮引数 a と b の型は IntSet ですから、IntList オブジェクトでも IntArray オブジェクトでも、実引数として sum メソッドに渡せます。

IntSet のような型は Java 言語ではインタフェース (interface) として定義します。 インタフェースが表す型は、そのインタフェースによって指定されたメソッドをもつ、複数のクラスを包含します。 sum メソッドは、size と get と find と add メソッドが定義されているクラスのオブジェクトが引数であれば正しく動作します。 そこで IntSet をそのような 4 つのメソッドからなるインタフェースとして定義します。

以下に IntSet インタフェースの定義を示します。

このインタフェースは add と find、size、get を定義した複数のクラスを包含する型を表します。 ただし、例えば add メソッドであれば何でもよいわけではなく、引数の個数や型、戻り値の型が上で示した通りでなければなりません。 メソッドのオーバーロード機能により、同じ名前でも引数の個数や型 (シグネチャ) が異なれば、異なるメソッドと見なされます。 したがってインタフェースの場合も、メソッドの名前だけでなくシグネチャまで含めて考えなければなりません。

インタフェースの定義はクラスの定義と似ていますが、名前の前は class ではなく interface です。 また、それぞれのメソッドの定義には波括弧 { } で囲まれたメソッドの動作の定義がありません。 代わりにセミコロン ; が末尾につきます。 各メソッドの定義の前に public がありませんが、書いてもかまいません。 厳密にはインタフェースの場合、メソッドの前の public は省略してよいことになっています。 つまり public と書いても書かなくても、常に public と書いてあると解釈されます。

インタフェースの利用

インタフェースを定義したら、インタフェースが表す型に包含されるクラスを定義しなければなりません。 IntList クラスが IntSet インタフェースが表す型に包含されるようにするには、IntList クラスの定義を次のように変えます。

クラス名に続けて implements (実装する) というキーワードを書き、インタフェース名を書きます。 キーワードの末尾には s がつきます。 implement ではありません。 この定義は、IntList クラスが IntSet インタフェースで定義されているメソッドを全て実装している、という意味です。 これを省略して Java 言語では、IntList クラスは IntSet インタフェースを実装する、といいます。 IntList クラスは、IntSet インタフェースで定義されていないメソッドを定義することもできますが、少なくとも、IntSet インタフェースに定義されているメソッドは全て定義していなければなりません。

implements の後には、複数のインタフェースをカンマ , で区切って並べて書くこともできます。 その場合、そのクラスは、それら複数のインタフェースを実装することになります。 複数のインタフェースを実装するクラスは、それらのインタフェースで定義されているメソッドを全て定義していなければなりません。

IntList クラスと同様に IntArray クラスも IntSet インタフェースを実装し、IntSet インタフェースが表す型に IntArray クラスが包含されるようにしましょう。

IntArray には set メソッドが定義されていましたが、これは IntSet インタフェースには定義されていないことに注意してください。 インタフェースを実装するクラスは、そのインタフェースに定義されているメソッドを全て定義していれば、それ以外のメソッドをいくつ定義してもかまいません。

さて、これで IntList クラスと IntArray クラスは共に IntSet インタフェースが表す型に包含されました。 IntList クラスや IntArray クラスの定義中に implements キーワード以下を書くことは必須です。 IntSet インタフェースで定義されているメソッドを全て定義するだけでは、それらのクラスが IntSet インタフェースを実装していることにはならず、そのインタフェースが表す型に包含されるとは見なされません。

IntSet 型の変数には IntList オブジェクトや IntArray オブジェクトへの参照番号を代入できます。 また、IntSet 型の実引数としてメソッドに渡すことができます。 例えば

main メソッドの中では、IntSet 型の変数に変数 list の値を代入しています。 変数 list の型は IntList ですが、IntList クラスは IntSet インタフェースを実装しているので、この代入は許されます。 また、sum メソッドの 2 つの引数の型は IntSet ですが、変数 list や array を実引数として呼ぶことができます。

IntSet 型の変数が表すオブジェクトのメソッドを呼び出すことも可能です。 呼び出せるメソッドは IntSet インタフェースで定義されているメソッドだけです。 例えば上の例の場合、変数 set1 や set2 が表すオブジェクトの add メソッドを呼んでいます。

一方、変数 set2 が表すオブジェクトの set メソッドを呼び出すことはできません。 変数 set2 は IntArray オブジェクトを表し、IntArray クラスには set メソッドが定義されていますが、変数 set2 の型は IntSet なので set メソッドは呼び出せません。 IntSet インタフェースには set メソッドが定義されていないからです。 もちろん変数 array が表すオブジェクトの set メソッドは呼び出せます。 上の main メソッドの中では、変数 array と変数 set2 が同じオブジェクトを表すにもかかわらず、呼び出せるメソッドの種類に違いがあることに注意してください。 どのメソッドを呼び出せるかは、変数の型によって決まります。 その変数が表しているオブジェクトのクラスは無関係です。

メソッド呼び出しは一般的には次のような形をとります。

呼び出せるメソッドは、「式」の部分の型によって決まります。 型がクラスであれば、そのクラスで定義されているメソッド、インタフェースであれば、そのインタフェースで定義されているメソッドです。 「式」の部分が this で省略されている場合は、this の型によって決まります。

キャスト

IntList クラスや IntArray クラスは IntSet インタフェースを実装しているので、IntList や IntArray 型の式は、IntSet 型の式を書けるところなら、どこにでも書くことができます。 しかし逆は成り立ちません。 IntSet 型の式や変数を IntList 型の式や変数の代わりに書くことはできません。 例えば上の Example クラスの main メソッドを修正して

としてはいけません。 例えば変数 set1 の型は IntList ですから、初期値を表す式も IntList 型の式でなければなりません。 ところが変数 list の型は IntSet ですから、これは誤りです。 上の main メソッドを実際に実行すると、変数 list は IntList オブジェクトを表しますが、変数の型が IntSet であるので誤りです。 その変数が実際にはどのクラスのオブジェクトを表しているかは無関係です。

上で誤りとした文を、どうしても実行したい場合には、キャスト演算をおこないます。

式の前方に括弧で囲った型名を書くと、その式の型は括弧内の型であると見なされます。 例えば変数 list の型は IntSet ですが、キャスト演算により IntList 型となりますから、IntList 型の変数 set1 の初期値にできます。 ただしキャスト演算を使うと、プログラムの実行中に、式の計算結果の値の型が本当に括弧内の型であるか検査されます。 変数 list の型は IntSet ですから、この変数は IntList オブジェクトだけでなく、IntArray オブジェクトを表している可能性もあります。 もし変数 list が IntArray オブジェクトを表していた場合は、後で説明する例外が発生します。

インタフェースのフィールド

インタフェースにはメソッドだけでなく、フィールドを定義することもできます。 しかしインタフェースのフィールドの値は定数でなければならず、後で別な値を代入することはできません。 例えば IntSet インタフェースに EMPTY というフィールドを追加してみましょう。

インタフェースのフィールドは定数なので、定数であることが一目瞭然になるように、フィールド名を全て大文字にするのが普通です。 必ず大文字にしなければならないわけではありませんが、一般的な習慣ではそのようにします。 定義の仕方はクラスにフィールドを定義する場合と同じですが、インタフェースのフィールドの場合は定数ですから、必ず初期値を与えなければなりません。 また先頭の public は省略できるので、普通は書きません。

インタフェースのフィールドは、厳密には、クラスの static フィールドと同様に扱われます。 static フィールドの値を式の中で使いたいときは、フィールド名の前に「クラス名 .」と書きます。 インタフェースのフィールドの場合も同様です。 EMPTY の例であれば、

と書けば、その値を得ることができます。

フィールドの前の「クラス名 .」の部分は省略できるときがあります。 例えば IntSet インタフェースの EMPTY の場合、IntSet インタフェースを実装しているクラスのメソッドやコンストラクタの中では、IntSet. を省略し、EMPTY とだけ書くことができます。 次の例は EMPTY フィールドを使って書き直した IntList クラスの get メソッドです。

IntList クラスは IntSet インタフェースを実装しているので、EMPTY の前の IntSet. は省略できる。


Copyright (C) 2003 by Shigeru Chiba, All rights reserved.