Javaの「抽象クラス」と「インターフェイス」を理解するいまから始めるJava(14)

» 2004年04月24日 00時00分 公開
[平井玄@IT]

すべてのクラスのスーパークラスとは?

 本連載では、これまで主にString型を使ってJavaのさまざまな言語仕様について説明してきました。最もお世話になるString型がそうであるように、クラスとはある形式のデータを扱うための「道具」です。また、HTML 2.0とHTML 3.2のように、扱うデータの形式が変更されたときは、全く新しくクラスを作るのではなく、変更に関係するメソッドをオーバーライド(上書き)すればよいことも学びました。オブジェクト指向の目的は既存のコードを流用することによって開発の効率を高めることです。メンバ変数やメソッド、継承といったオブジェクト指向特有の概念も、その目的のために用意された機能だと考えれば、すんなり受け入れられたのではないでしょうか。

 最終回は、おさらいをかねて抽象クラスとインターフェイスについて説明することにします。抽象クラスとインターフェイスもまた、既存のコードを流用することによって開発の効率を高めるためにあります。まず、String型のクラス定義を見てみましょう。

public final class String
  implements java.io.Serializable, Comparable, CharSequence
{


 Stringクラスは変更しない文字列を扱うときに使うクラスです。では、Stringクラスはスーパークラスのない、クラス階層の頂点にあるクラスなのでしょうか? 実はそんなことはありません。Javaでは、クラス階層の頂点(ルート)としてObjectというクラスがあり、すべてのクラスはObjectクラスのサブクラスです。初めからそう決まっているので、クラスの定義にわざわざ書かなくてよいだけです。従って、ユーザーが自分で作るクラスは次のようにも書けます。

明示的にObjectのサブクラスであることを宣言する
public class IAmSonOfObject extends Object{
  public static void main( String args[] ) {
    System.out.println("Hello, World!");
  }
}

 すべてのクラスがObjectクラスのサブクラスであるということは、すべてのクラスはObjectクラスで用意されているメソッドが利用可能ということです。例えば、Objectクラスにはオブジェクトを文字列として表すtoString()というメソッドが用意されています。このメソッドは、次のようにInteger型のオブジェクトでも利用可能です。

明示的にObjectのサブクラスであることを宣言する
public class ShowByToString {
  public static void main( String args[] ) {
    Integer i = new Integer(123);

    System.out.println(i.toString());
  }
}


 Objectクラスはすべてのクラスのスーパークラスですので、Object型の変数にはどんなオブジェクトへの参照も持たせることができます。例えば、前回紹介したインターネット上のHTML文書からプレーンテキスト部分のみを取り出すプログラムには以下のような部分がありました。

URL url = new URL(sourceURL);
Object content = url.getContent();
if ( content instanceof InputStream ) {

 URLクラスのgetContent()メソッドは、あるURLのコンテンツを取得し、Object型の参照を返してきます。しかし、これだけではその参照がどのクラスなのか分かりません。そこで、instanceofを使ってObjectのクラスが実際には何かを確かめています。この場合は、「if ( content instanceof InputStream ) {…」によって、content変数がInputStreamクラスかどうかを確認していることになります。このif文によってcontent変数がInputStreamクラスだと確認できれば、「(InputStream)content」のようにしてObject型の変数contentを、InputStream型として扱ってもよいことになります。

抽象クラスはインスタンス化できない

 いくつかのクラスに共通する機能をスーパークラスでまとめて定義するのがJava的なプログラムの作り方です。例えば、Javaでは画面に表示されるボタンやスクロールバーといったGUI部品に共通するスーパークラスとしてComponentクラスがあります。Componentクラスには、ボタンやスクロールバーなどに共通するメソッドとして、表示のオン/オフを設定するsetVisible()やサイズを取得するためのgetSize()などが定義されています。ボタンを扱うためのButtonクラスや、スクロールバーを扱うためのScrollbarクラスでは、Componentクラスのメソッドを利用することで表示のオン/オフを切り替えるなどができるわけです。

 しかし、ComponentクラスはButtonクラスやScrollbarクラスとは違って、インスタンス化できません。Component(部品)はあくまでもクラス階層を設計するうえでの概念として存在するだけで、画面上に存在するわけではないからです。Javaでは、このようにクラス階層を設計するうえで、概念としてのみ存在するクラスを定義できます。

 このことをComponentクラスの定義で確認してみましょう。

public abstract class Component implements ImageObserver, MenuContainer,
Serializable
{

 クラス定義にある「abstract」というキーワードが、このクラスが概念としてのみ存在することを示します。abstractは日本語で「抽象的な」という意味で、言葉どおりこのクラスは直接インスタンス化できません。abstractキーワードを付けて抽象クラスと定義されたクラスは、ButtonやScrollbarのように、そのサブクラスとしてインスタンス化します。

 クラスを抽象クラスとして定義すると、abstractキーワードを付けた抽象メソッドを定義できるようになります。抽象メソッドは、メソッドの名前だけをスーパークラスで定義し、実際の動作はサブクラスでオーバーライドすることになります。例えば、抽象クラスであるGraphicsにはdrawLine()という抽象メソッドが定義されています。drawLine()は線を引くためのメソッドですが、実際に線を引くための処理はOSなどによって異なります。そこで抽象メソッドとして名前だけを決めておき、アーキテクチャに依存する部分はサブクラスで個々にオーバーライドするわけです。というわけで、Graphicsクラスにはたくさんのabstractなメソッドが存在します。

 別のいい方をすると、スーパークラスでabstractキーワードを付けたメソッドは、必ずサブクラス側でオーバーライドしなければなりません。あるクラスをインスタンス化し、オブジェクトとしてメモリ内で実体を持つには、メソッドが抽象的な、定義のみの存在であってはならないのです。

インターフェイスの意味は?

 ここまでで、共通して利用可能なメソッドが定義されている場所として2つを紹介しました。1つはスーパークラスのメソッドです。すべてのクラスのスーパークラスであるObjectクラスのメソッドは、すべてのサブクラスで利用できます。また、いくつかのクラスに共有のメソッドをまとめる概念上のスーパークラスで定義するために、インスタンス化できない抽象クラスという仕組みも用意されています。

 そしてもう1つ、これから説明する「インターフェイス」という仕組みもあります。ただし、Javaのインターフェイスについて、「Java入門」と銘打っている書籍などでは、

Javaでは多重継承がない代わりにインターフェイスという技術が使われます


なんていうことがチャラっと書いてあります。しかし、読者は多重継承もインターフェイスも分からないから入門書を読んでいるわけで「多重継承がないのなら多重継承なんて言葉は使わなくてもいいからインターフェイスをきちんと説明してくれ」と思ってしまいます。

 確かに、オブジェクト指向を学ぶという意味では多重継承とインターフェイスがどのように関係があって、どのように異なるのかについて学ぶことはすごく重要です。しかし、Java初学者にインターフェイスとは何かという大所高所に立った説明は不要でしょう。そこで、「インターフェイス」という言葉からJavaのインターフェイスが何をするものなのか説明します。

 「インターフェイス」はコンピュータ業界ではよく使われる言葉です。コンピュータの背面に付いている「シリアルインターフェイス」や「IEEE 1394インターフェイス」という接続端子的な意味から、キーボードやマウスといった「ヒューマンインターフェイス」、さらには疑似的なボタンなどを画面に表示する「グラフィカルユーザーインターフェイス(GUI)」まで、さまざまな場面で使われます。インターフェイス(interface)は本来「界面」という意味で、性質の異なるもの同士の接し合っている面のことをいいます。

 実は、Javaのインターフェイスもこの意味で使われています。どういうことでしょうか? 冒頭で紹介したStringクラスの定義をもう一度見てみましょう。

public final class String
    implements java.io.Serializable, Comparable, CharSequence
{

 implements以下の下線部がインターフェイスについての定義です。String型は、java.io.Serializable、Comparable、CharSequenceという3つのインターフェイスを実装(implements)していることになります。

 例えば、CharSequence(文字シーケンス)というインターフェイスには、文字シーケンスの長さを返すlength()というメソッドが含まれます。また、StringBufferクラスもCharSequenceインターフェイスを実装していますので、Stringクラス同様、length()メソッドが利用できます。

 何となく分かってきませんか? あるクラスがどんなインターフェイスを実装しているかを見れば、そのクラスでどんなメソッドが利用可能なのか分かるのです。例えば、Windows用のパソコンとプレイステーション2は別の機械ですが、どちらにもUSBインターフェイスが付いていますので、USBキーボードを接続して操作できます。同様に、StringクラスとStringBufferクラスは継承関係のない別のクラスですが、どちらもCharSequenceというインターフェイスを実装しています。従ってどちらのクラスでもlength()やcharAt()など、CharSequenceインターフェイスのメソッドを利用できるというわけです。

 では、インターフェイスはどのように定義されているのでしょうか? CharSequenceインターフェイスの定義を見てみましょう。

public interface CharSequence {
    int length();
    char charAt(int index);
    CharSequence subSequence(int start, int end);
    public String toString();
}

 コメントなどは削除してありますが、極めて単純です。クラスの定義ではclassというキーワードを使いますが、それがinterfaceに変わっているだけです。

 インターフェイスの定義では、メソッドとfinalに指定されたフィールドだけが書けます。メソッドは定義だけですので、インターフェイスを実装するクラスの側でメソッドをオーバーライドしなければなりません。従って、インターフェイスのメソッドはabstractキーワードの付いていない抽象メソッドと同じです。

 一方、StringクラスではCharSequenceインターフェイスのlength()をオーバーライドして、

public int length() {
    return count;
}

というコードが書かれています。従って、インターフェイスの場合、継承関係のない異なるクラスで同じ名前のメソッドが使えるといっても、別々のクラスでメソッドの実装まで同じということではありません。あくまでも、メソッドの呼び出し方が共通しているだけです。

 では、実際にインターフェイスを実装するクラスを定義してみましょう。

CharSequenceインターフェイスを実装するクラス(2)
class MyClassWithCharSequenceIF implements CharSequence {
  // 空のクラス
}

public class IFSample {
  public static void main( String args[] ) {
    MyClassWithCharSequenceIF mcwcs = new MyClassWithCharSequenceIF ();

    System.out.println(mcwcs.toString());
  }
}

C:\>javac IFSample.java
IFSample.java:1: MyClassWithCharSequenceIF は abstract でなく、java.lang.CharSequence 内の abstract メソッド subSequence(int,int) をオーバーライドしません。
class MyClassWithCharSequenceIF implements CharSequence {
^
エラー 1 個

C:\>


 この場合は、CharSequenceインターフェイスで定義されているメソッドをMyClassWithCharSequenceIFクラスでオーバーライドしていないのでエラーになります。abstractを付けて定義した抽象クラスの抽象メソッドと同じく、CharSequenceインターフェイスを実装するクラスは、必ずCharSequenceインターフェイスのメソッドをオーバーライドしなければならないからです。CharSequenceインターフェイスには4つのメソッドがありますので、それぞれをオーバーライドしてみましょう。

CharSequenceインターフェイスを実装するクラス(2)
class MyClassWithCharSequenceIF implements CharSequence {
  public int length() {
    return 0;
  }

  public char charAt(int index) {
    return 'A';
  }

  public CharSequence subSequence(int start, int end) {
    return "ABC";
  }

  public String toString() {
    return "ABCDEFG";
  }
}

public class IFSample2 {
  public static void main( String args[] ) {
    MyClassWithCharSequenceIF mcwcs = new MyClassWithCharSequenceIF();

    System.out.println(mcwcs.toString());
  }
}

C:\>javac IFSample2.java
C:\>java IFSample2
ABCDEFG

C:\>


 今度は無事にコンパイルできました。mainの中身だけ見ると、MyClassWithCharSequenceIFというクラスは何なのかえたいが知れません。しかし、クラスの定義を見ると「implements CharSequence」とありますから、このクラスがCharSequenceインターフェイスを実装していることが分かります。

 プログラマは、クラス独自のメソッドが定義されているかどうかまでは分かりません。しかし、少なくともCharSequenceインターフェイスのメソッドは利用可能だと分かります。また、CharSequence(文字シーケンス)インターフェイスを持つということは、このクラスが文字列に関係していそうだ、という予測もつきます。クラスとプログラマ(が作ったクラス)という異なる性質を持つもの同士のインターフェイス(界面)として、両者を取り持っているのがJavaのインターフェイスというわけです。

インターフェイスの継承

 インターフェイスもクラスと同じように継承できます。

interface CharSequence2 extends CharSequence {
    int CharCodeAt(int index);
}

 インターフェイスの継承はクラスの継承と同じく、extendsキーワードを使います。USB 1.0がUSB 2.0になるようなイメージです。クラスの場合、基になる親クラスのことをスーパークラス、子になるクラスをサブクラスといいますが、インターフェイスの場合はそれぞれスーパーインターフェイス、サブインターフェイスといいます。

 クラスの場合、1つのスーパークラスからは1つのサブクラスしかを継承できません。しかし、インターフェイスの場合は複数のインターフェイスを基に継承できます。

interface IntegratedPort extends Keyboard, Mouse {
    bool getConnected();
}

 この場合は、KeyboardインターフェイスとMouseインターフェイスの両方を継承させて、IntegratedPortインターフェイスというインターフェイスを定義しています。IntegratedPortインターフェイスは、KeyboardとMouseインターフェイスのどちらかで定義されたメソッドとメンバ変数を併せ持ちます。また、IntegratedPortインターフェイス独自のメソッドとして、getConnected()を追加しています。従って、IntegratedPortインターフェイスを実装したクラスは、getConnected()と、KeyboardとMouseインターフェイスに由来するメソッドが利用可能になるわけです。

Column

クラスは仕様と実装を記述するのに対して、インターフェイスは仕様のみ定義して実装までは記述できません。したがって、スーパークラスをextendsしてサブクラスを作ると、クラスの仕様だけではなく実装も継承されます。

一方、本文では、クラスを主、インターフェイスを従と捉えて、implementsを「インターフェイスの実装」と述べました。ですが、クラスがインターフェイスをimplementsすることは「インターフェイスの継承」とも呼ばれます。インターフェイスを仕様を定義するという点でクラスと同格と捉えれば、クラスがインターフェイスをimplementsすることは、クラスがインターフェイスの仕様を継承することになるからです。

つまり、あるクラスがインターフェイスをimplementsすることは、そのクラスがインターフェイスで定義された仕様を継承していると考えられるので、extendsとimplementsのどちらも「継承」と呼べるわけです。なお、implementsを単に「インターフェイスの実装」と捉える場合は、インターフェイスで定義されたメソッドを実装するという意味になります。


 以上で連載を終わります。この連載では、これからJavaを学ぶ読者として「コンピュータがどのように動いているかの専門教育を受けていないがJavaの学習に興味のある方」を想定しました。そのため、コンピュータの専門家から見ると「そんなことは分かっている」「それは技術的におかしい」という個所があったことでしょう。しかし、コンピュータはどういうふうに動いているかの基礎知識は、Javaのように「メモリ管理を意識しないで済む」などといわれる言語でも、暗黙のうちに必要とされています。この連載で扱っていないJavaの言語仕様はたくさんありますが、なぜその機能があるのか、実際コンピュータの中では何が起きているのか、という視点を持てば、今後の学習がずいぶん楽になると思います。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。