目次へ戻る 下へ↓
15〜17、オブジェクト指向とインターフェース
作成者:Fumitaka Makino 更新日:2003-04-18 14:58

・インターフェースとは?

まず先にインターフェースとは何かを説明してしまいます。インターフェイスと言う概念は私たちの日常生活にも広く存在します。例えば「車」について考えてみましょう。「カローラ」や「マーチ」は「車」として扱うことができます。しかし「車」と呼ばれる固有の商品は現実には存在しません。にもかかわらず私たちは「ベンツ」でも「ビートル」でも「サイファ」でも「車」として認識しています。しかも「ベンツ」も「ビートル」も「サイファ」も全く別な会社の開発した乗用車です。それらは少なくとも簡単な「継承」の関係にはなさそうです。にもかかわらず共通の「車」という括り、つまり型があります。このとき「車」とはある取り決めに従った共通の概念です。そして、そのある取り決めを「満たしている」物体は全て「車」として扱えます。このとき「車」をインターフェースと呼ぶことができます。他にも「野菜」、「肉」、「食べ物」、「お菓子」などインターフェースは無数に存在します。そして私たちはインターフェースを利用してコミュニケーションをとったり表現をしたりしています。

「野菜スープ」 → 野菜に属する植物が入っていれば野菜スープ
「お菓子」 → 甘いもの、せんべいなどなんでも

Javaのコードにおいてのインターフェースとはメソッドの中身などが存在しないメソッドの宣言文とフィールドだけのクラス(広義)です。c++などではバーチャルクラスと呼ばれています。インターフェースはJavaにおいてはclassではなくinterfaceとして宣言され通常のclassからインプリメントされることにより利用されます。下記に車の定義を表す「Car」インターフェースを記述します。

例:インターフェースのサンプル

     
 

/**
  車をあらわすインターフェースクラスです。
*/
public interface Car {//classではなくinterface
  
  /**
    走るメソッドです。
  */
  public abstract void run();//別にabstractは省略しても構わない
  
  /**
    止まるメソッド
  */
  public abstract void stop();
  
}

 
     

上記のコードからわかるように、メソッドに具体的なロジックは一切記述せず。宣言のみとなっておりさらにabstract(抽象的な)という予約語があります。これらはメソッドがアブストラクトメソッドであり具体的な動作は定義されていないことを示しています。ちなみにこのabstract句はインターフェースにおいては付加しなくても構いません。なぜならインターフェースにおいては以下のような制限・約束があるからです。

  • メソッドは全てabstractで定義される。
  • フィールドは全てstaticで定義される。

次にこの「Car」インターフェースを取り込んだ目産の乗用車マーチを表す「March」クラスを作るとしましょう。その場合、取り込むインターフェースで定義された抽象メソッド全てを必ず実装しなければいけません(後に述べるアブストラクトクラスでない限り)。実装しない限りは「March」クラスのコンパイルは成功しません。これを逆説的に言うならば、

「あるインターフェースを取り込んだクラスは、そのインターフェースで定義されている機能を実装していることが保証されている」

ということができます。

例:インターフェースを取り込んだクラス

     
 

/**
  車インターフェースを取り込んだ目産マーチ
*/
public class March implements Car {
//imprementsというキーワードでインターフェースを取り込む。extendsとは違って何個でも取り込める。
  
  /**
    走ってるか走ってないかのフラグ
  */
  private boolean status = false;
  
  /**
    カーナビゲーションシステム
  */
  private CarNavigationSystem navi = new CarNavigationSystem();
  
  /**
    走るメソッド:インターフェースで定義
  */
  public void run(){
    flg = false;
  }
  
  /**
    止まるメソッド:インターフェースで定義
  */
  public void stop(){
    flg = true;
  }
  
  /**
    カーナビによる経路検索
    @param String key 経路選択用の文字列
  */
  public Root searchRoot( String key ){
    
    return navi.search(key);
    
  }
  
}

 
     

この時インターフェース「Car」で定義されていた2つのメソッドは「March」クラスで実装されています。これは、「Car」として扱うための条件がそろっているということです。もちろんその他にも「March」クラスで拡張されたメソッドやフィールドはありますが「車」としての定義に影響はありません。

わざわざコンパイルするときに制限まで加えて面倒なインターフェースを利用するのでしょうか?それにはまずインターフェースを利用した結果なにができるかを説明する必要があります。インターフェースを利用すると下記のようなことができます。

インターフェースを取り込んだクラスはインターフェースの型として扱うことが可能となる

これを「March」クラスで表現するなら

Car car1 = new March();

とできることです。もし同様の手法で「Benz」クラスを作ったとしたら

Car car2 = new Benz();

とするこができます。これならただの継承関係でも実現できました。では全く継承関係にありえないエアバス社製の飛行機ならどうでしょう?これも一応車輪で走ったりとまったりでき、その点で車と言えます。ではこのエアバス社製飛行機を車として扱うためにはどうしたらよいでしょうか?エアバス社製飛行機クラスを改変してどこかの車を継承して作り直すのは現実的ではありません。しかもすでにrunやstopメソッドを持っています。このような時インターフェースを利用します。下記を見て下さい。

例:インターフェースを取り込んだクラス

     
 

/**
  飛行機インターフェースと車インターフェースを取り込んだエアバス社の飛行機
*/
public class AirBus inplements Car,Airplane {
  
  /**
    走ってるか走ってないかのフラグ
  */
  private boolean status = false;
  
  /**
    速度
  */
  private int speed = 0;
  
  /**
    走るメソッド:インターフェースで定義
  */
  public void run(){
    flg = false;
  }
  
  /**
    止まるメソッド:インターフェースで定義
  */
  public void stop(){
    flg = true;
  }
  
  /**
    ジェット噴射
  */
  public void boost(){
    speed+=10;
  }
  
}

 
     

上記のように車インターフェースと飛行機インターフェースを取り込むことによりこのエアバス社製飛行機クラスは車としても、飛行機としても扱うことができるようになりました。これは下のように複数の型として扱うことができると言うことを意味します。

Car car3 = new AirBus();//車としての場合

Airplane air1 = new AirBus();//飛行機としての場合

このように、インターフェースを利用することによりクラスを複雑な仕様変更をせずに他の型として振舞わせることができます。具体的にどのように役に立つかは次の項で詳しく説明します。

 

・インターフェースの利用意義

これまで色々とオブジェクト指向プログラミングを学んできましたが、Javaでは一貫して一つのキーワードがありました。それは

「抽象化」(Abstraction)

と言う概念です。 この抽象化とは何でしょうか?別な表現をすると。

「機能と実装の分離」

ということができます。これはJavaをオブジェクト指向言語として用いていると頻繁に出くわす構造です。例えば

「サンドイッチを食べる。」

という動作を例に考えてみましょう。抽象化の概念に従うとこれは

「サンドイッチ」「食物を食べる」

というように分けられます。つまり最初の「サンドイッチを食べる」という具体的な動作から、より抽象的な「食物を食べる」という動作へと変更されています。これは定義が曖昧になった反面で、「サンドイッチ」を食べるという非常に限定的な動作が「食物」を食べるという汎用的な動作へと変化したことを意味します。この場合、機能は「食物を食べる」、実装は「サンドイッチを食べる」と解釈できます。そして抽象的、汎用的な動作として分離された「食物を食べる」という機能は、別な「パイナップルを食べる」という実装にも応用することができます。このように

目的の動作や目的を分析し汎用的な機能や動作を抽出するという行為

を抽象化といいます。

これをJavaにたとえるなら、抽象化されていない「サンドイッチを食べる」という動作は

eatSandwich( Sandwich sand )

というメソッドに相当するでしょう。しかしこれでは「パイナップルを食べる」という動作を実装するときに再び

eatPineapple( Pineapple pine )

と言うメソッドを追加しなければいけません。そのため「食物を食べる」という動作を抽出し

eatFood(Food food)

というメソッドを作ることにより食物であればなんでも食べられるようにすればあらゆるパターンに対応することができます。 ではこの時、どのようにして食べ物を統一的なFood型という型で扱えばよいのでしょうか?継承の概念を思い出した人は、SandwichPineappleも、また今後増える予定の食物は全てFoodクラスを継承しておけばよいと考えるかもしれません。そのため、それでは解決できないように、以下のような意地悪な条件を追加しておきます。

パイナップルは既に「植物クラス」 を継承している

これでは、継承を利用しダウンキャストによりPineappleのインスタンスをFood型として扱うことができません。なぜならJavaにおいては一般にクラスの多重継承は許可されておらず、常に単一継承だからです。しかし「継承関係ではない、自分以外の型」を実現する術が一つだけありましたね?それがインターフェースです。上の「インターフェースとは?」や第7回の「コラム・多重継承」を繰り返しよく読んでみてください。Javaにおいて「型の遺伝」の側面での多重継承は禁止されていませんでした。つまりPineappleはインターフェースを用いることによりFood型としても扱うことができるのです。これにより「食物を食べる」という汎用的な動作はeatFoodメソッドにより実現することが可能となりました。

図:食物インターフェースの概念図

先ほどパイナップルは既に植物クラスを継承していると書きましたが、当然我々が食べている動物も動物クラスを継承しています。そして植物クラスを継承しているクラス、動物クラスを継承しているクラスの全てが食用になるわけではありません。例えば毒草クラスやドブ鼠クラスなどさまざまな理由で食用に適さないクラスが存在するわけです。そんな中で選別的に、しかも既存の継承関係に影響を及ぼさずに、食用のクラスは食物クラス型として扱うためにインターフェースを利用します。

これまでの要求と答えを整理すると以下のようになります。

要求:植物クラスを継承しているパイナップルクラスを食物(Food型)として扱いたい

答え:パイナップルの継承構造はそのままで、パイナップルは食物(Food型) をインターフェースとして取り込む

いかがでしょう?このように継承関係にないもの同士を一つのグループとして扱ってやる時(特に機能を汎用的にする時)にインターフェースは大きな威力を発揮します。ではインターフェースを用いて、人クラスが「食物を食べる」という動作をもち、色々な食物を食べるようなプログラムを作ってみてください。

サンプル用のソースファイル(JavaDoc)

Human.java インターフェースを利用して食物を食べるクラス
Food.java 食物インターフェース
Tomato.java トマトクラス implements Food
Beef.java 牛肉クラス implements Food

 

・実践1:インターフェースを取り込んだクラスを作る

今回はインターフェースを取り込んだクラスをより実践的に作成してみましょう。java.io.FileクラスのJavadocを参照してみてください。listメソッドというものが存在すると思います。そしてそのlistメソッドには、以下のように引数の違う2つのメソッドが存在します。

public String[] list()

この抽象パス名が示すディレクトリにあるファイルおよびディレクトリを示す文字列の配列を返します。

public String[] list(FilenameFilter filter)

この抽象パス名が示すディレクトリにあるファイルおよびディレクトリの中で、指定されたフィルタの基準を満たすものの文字列の配列を返します。このメソッドの動作は、list() メソッドと同じですが、返された配列の文字列はフィルタの基準を満たす必要があります。指定された filter が null の場合、すべての名前が受け入れられます。そうでない場合、名前がフィルタの基準を満たすのは、フィルタの FilenameFilter.accept(java.io.File, java.lang.String) メソッドが、この抽象パス名およびそれが示すディレクトリ内のファイルまたはディレクトリの名前で呼び出されたときに true が返される場合だけです。

つまり「ディレクトリを表すFileクラスのインスタンス」のlistメソッドを実行すると、下図のようにそのディレクトリの配下にある全てのディレクトリ、ファイルがString配列となってリターンされてきます。

図:listメソッドの対象

実際に利用して欲しいメソッドはlist(FilenameFilter filter)です。このメソッドは、javadocにもあるように通常のlistメソッドでリターンされるディレクトリやファイルの判定を行い任意のものだけを抽出することができます。当然ですがその判定基準は引数であるFilenameFilterに記述されています。javadocでFilenameFilterを調べてみてください。インターフェースであることが確認できると思います。つまりこのFilenameFileterと言う型は現実には実体が存在せず

FilenameFilter filter = new FilenameFilter();

のような構文の記述はできないと言うことです。つまりこのインターフェースを実装したクラスを作るのはlistメソッドでファイルのフィルタリングを行う人なのです。ではFilenameFilterのメソッドを見てみましょう以下の一つしかありません。

public boolean accept(File dir, String name)

指定されたファイルをファイルリストに含めるかどうかをテストします。

パラメータ:

dir - ファイルが見つかったディレクトリ

name - ファイルの名前

戻り値:

名前をファイルリストに含める場合は true、そうでない場合は false

このメソッドを説明すると、判定対象となるディレクトリ/ファイル群を一つ一つこのメソッドによって判定させています。判定対象のファイル情報(dirとname)を利用してこのファイルをリストに含めるかどうかをbooleanでリターンしてやればよいのです。trueならばlistメソッドを実行した際にリターンされるString配列に含まれることになり、falseであればString配列には含まれません。つまり、この判定ロジックを実装し、なおかつFilenameFilterインターフェースをインプリメントしたクラスのインスタンスをlistメソッドに引数として渡してやればよいのです。これだけでは非常にわかりにくいと思いますので下に図を示します。

図:listメソッドの判定メカニズム

利用者が作ったテストフィルタのインスタンスをlistメソッドの引数として渡します。 listメソッド内部では テストフィルタ型ではなくFilenameFilter型のインスタンスとして扱われ、判定対象のディレクトリ/ファイル を一つずつacceptメソッドにより判定しtrueかfalseかをリターンします。その判定を利用してlistメソッドはリターンするディレクトリ/ファイル名を決定します。

この方法によりFileクラスのlistメソッドはさまざまなフィルタを、たった一つのメソッドにより判定することができるのです。もしフィルタのタイプごとにメソッドを作っていたら毎回毎回Fileクラスのソースコードを変更しなければいけません。これは非常に非合理的なのでファイルの判定はFilenameFilterインターフェースをインプリメントしたクラスに任せてしまい、listメソッドは判定の結果のみ受け取ってtrueの物だけを配列に加えるという動作をするようになっています。

今回のインターフェースの利用形態は、利用対象のクラスまたはメソッドが望む型(内部で利用する型)を実現するためのものであり、利用する側がインターフェースをインプリメントしたクラスを作るというタイプです。

サンプル用のソースファイル(JavaDoc)

TestFilter.java 上記のサンプルで利用したディレクトリのみtrueを返すテストフィルタ、mainメソッドも付属しているので利用法はjavadocを参照すること

 

・実践2:インターフェース型の戻り値

今回は、VectorクラスからiteratorメソッドによりIteratorインターフェースをインプリメントしたクラスのインスタンスを取得してみましょう。まずIterator(イテレーター)インターフェースとは何かを説明しなければなりません。Iteratorインターフェース(以降Iteratorと表現)とはGoFの23パターンの1つであるIteratorパターンをJava上で利用するための統一的なインターフェースです。このIteratorというのは日本語で反復子とも呼ばれ、繰り返し処理を行うときの代名詞ともなっています。下図を見て下さい。Iteratorのモデル図(1〜4は時系列でそれぞれ独立)です。Iteratorというのは内部に配列を持っており、その配列を先頭から1つずつリターンします。それを実現するために、hasNextメソッドとnextメソッドを持っています。hasNextメソッドは次の配列が存在するかどうかをbooleanで返し、nextメソッドは次の配列の要素を返します。

図:Iteratorパターンのメカニズム

  1. hasNext()メソッドの戻り値はtrueで、next()メソッドはスポットを一つ移動しObj1をリターンする。
  2. hasNext()メソッドの戻り値はtrueで、next()メソッドはスポットを一つ移動しObj2をリターンする。
  3. hasNext()メソッドの戻り値はtrueで、next()メソッドはスポットを一つ移動しObj3をリターンする。
  4. 上記の動作が内部配列の末尾まで達したとき。hasNext()メソッドの戻り値はfalseで、next()メソッドはObj3以降に要素がないので、NoSuchElementExceptionを発生する。

それらの機能を利用するとIteratorの内部の要素に対して、以下のような統一的なコードによりアクセスできます。

例:Iteratorへのアクセス

     
 

iterator it = vec.iterator();//Iteratorオブジェクトの取得
Object tmpObj = null;
while( it.hasNext() ) {//次の要素があるかどうかを判定、trueならnextで次の要素を取得
  tmpObj = it.next();
}

 
     

この手法を用いることにより、内部配列がいくつでも関係なく、シンプルに繰り返し処理を行うことが可能です。もちろん実装はIterator(をインプリメントしたクラスのインスタンス)を生成するクラスによって変わってきます。しかし、少なくともIteratorを利用してコードを記述している場合には、Iterator(をインプリメントしたクラス)の内部仕様が変更になっても、Iteratorを利用しているクラスのコードに変更は必要ありません。

また主にjava.utilパッケージに含まれるJava Collection Frameworkにおいては、反復アクセスはIteratorを利用してください。HashtableやHashMapの要素やキーを取得する際にも、Setを取得した後にIteratorを取得することができます。また一部でEnumerationインターフェースを利用しているものがありますが、必ず何らかの方法でIteratorの取得方法が提供されています。特に古いコードとの互換性問題などがない限りはIteratorを利用してください。

サンプル用のソースファイル(JavaDoc)

IterateTest.java Vectorの内部要素をIteratorを利用して取得するプログラム

より高度なインターフェースの理解を進めたい人は自分でIteratorインターフェースをインプリメントした ConcreteIterator(*)を作ってみるのも良いと思います。
*通常インターフェースをインプリメント、アブストラクトクラスを継承した具象クラスを コンクリートクラスといいます。

 

・付録:用語説明
GoFの23パターン GoF ( Gang of Four と呼ばれる、Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) が書いた「オブジェクト指向における再利用のためのデザインパターン(改定版) ISBN4-7973-1112-6」と言う本に掲載されたオブジェクトプログラミングのクラス設計パターンのことを指す。googleなどで「デザインパターン」をキーワードに検索を書ければ非常に多くのサイトがヒットします。

 

↑上へ 目次へ戻る