Javaの「メソッド」の挙動を変えるオーバーライドいまから始めるJava(8)

» 2003年08月16日 00時00分 公開
[平井玄@IT]

 第5回「メソッドとコンストラクタはなぜ必要?」では、プログラムの中の同じ処理をメソッドとして独立させることで、プログラムを書く手間を省略できることを説明しました。しかし、メソッドの効用は「手間の省略」だけではありません。Javaではあらゆる処理がクラスのメンバ変数やメソッドを使って記述されますから、メソッドは単なる処理の単位ではなく、「あるクラスに所属するメソッド」という関係の中で理解されるべきです。

メソッドの役割

 このことを、前回紹介したHTML文書からプレーンテキスト部分を取り出すプログラムで使ったString型のメソッドで説明しましょう。

if ( source.charAt(pos) != '<' ) {
  for ( pos++; pos < source.length(); pos++ ) {
    if ( source.charAt(pos) == '<' ) {
      System.out.println(source.substring( start, pos ));
      break;
    }
  }
}

 HTML文書のソースを格納するために使われたString型のメソッドは以下の3つです。

source.length() 変数sourceの長さを取得
source.charAt(<位置>) 変数sourceの指定した位置の文字を取得
source.substring(<開始位置>,<終了位置>) 変数sourceの指定した範囲の文字列を取得

 どのメソッドも、sourceというString型のインスタンスが保持する文字列について、長さを調べたり、文字や文字列を取り出したりするのに使っています。クラスのメソッドは、単に手間を省くのではなく、あるクラスの特定の機能を実現するために使われるのです。length()やcharAt()というメソッドがあることによって、String型の変数sourceは単に文字列を格納する「入れ物」としての役割だけでなく、文字列について調べたり、操作したりするための「道具」として利用できるわけです。このことは、Javaでプログラムを作るときにとても重要です。Javaではあらゆる処理をクラスとして記述していくことになりますので、どのようなクラスを定義し、そのクラスにどのようなメソッドを具体的に用意するかを考えるのがプログラマの仕事になるからです。

 このような意味で、前回紹介したHTMLパーサは、実は全然Java的ではありません。元になるHTML文書が固定されているのは説明の都合ですが、プログラムを見ても、扱うデータがHTML文書であり、HTML文書からプレーンテキストを取り出す処理を実現しているのだ、ということが簡単には読み取れないからです。前回のプログラムはString型の変数に格納された文字列の一部を一定のルールに従って取り出しているにすぎません。Java的なプログラムにするには、HTML文書からプレーンテキストを取り出すということを、クラスを使って記述するべきなのです。

 いったいどうしてわざわざクラスを使わなければならないのでしょうか? 何かの処理を実現するには、制御文を組み合わせて、つまりプログラムを「順番に実行する」「条件が合致すれば実行する」「繰り返して実行する」という制御構造の3つのパターンとして記述すれば十分のはずです。

 ところが、Javaをはじめとするオブジェクト指向のプログラム言語は、これでは不十分と考えます。HTML文書を扱うのであれば、文字列を扱うクラスとしてString型があるように、HTML文書を扱う「HTMLDocument型」のようなクラスを用意するべきだ、と考えるのです。クラスを用いることで、処理の対象となるデータと、そのデータに関する処理をひとそろいの道具として用意しよう、というのがオブジェクト指向的なプログラムの作り方です。

 そこで、前回紹介したHTMLパーサを、クラスを使って次のように書き換えてみました。

   HTML文書を扱うクラスを使ったプログラム
class HTMLDocument {
  String source;

  void showPlainText() {
    boolean processingTag = false;
    int pos;
    int start = 0;

    for ( pos = 0; pos < source.length(); pos++ ) {
      // タグ
      if (processingTag) {
        if ( source.charAt(pos) != '>' ) {
          for ( pos++; pos < source.length(); pos++ ) {
            if ( source.charAt(pos) == '>' ) {
              break;
            }
          }
        }
        start = pos + 1;
      }
      // テキスト
      else {
        if ( source.charAt(pos) != '<' ) {
          for ( pos++; pos < source.length(); pos++ ) {
            if ( source.charAt(pos) == '<' ) {
              System.out.println(source.substring( start, pos ));
              break;
           }
         }
        }
      }
      processingTag = !processingTag;
    }
  }
}

public class ExtractPlainTextWithClass {
  public static void main( String args[] ) {
    HTMLDocument doc = new HTMLDocument();

    doc.source = "<html><head><title>タイトル</title></head><body><p>段落</p></body></html>";
    doc.showPlainText();
  }
}

 前回紹介したプログラムに比べると、プログラムの本体部分が驚くほどすっきりしました。プログラマはもはやプレーンテキストをどのように取り出すのか、知っている必要はありません。HTML文書を扱いたくなったプログラマは、HTMLDocument型の変数を用意するだけで、HTML文書からプレーンテキストを取り出せます。ちょうど文字列を扱うときにString型のメソッドであるlength()がどのように文字数を数えているか知らなくてよいのと同じです。

HTMLのバージョンアップに対応する

 HTMLにはバージョンがあります。仮にHTMLDocument型がHTML 2.0に準拠したクラスだったとします。ところがクラスを使ってHTML文書を扱えるようになって喜んでいたのもつかの間、HTMLのバージョンが3.2に上がってしまったとしましょう。HTML 3.2には、HTML 2.0では定義されていなかった「<script>」というタグがあり、<script>〜</script>の間には「<」や「>」が「<」「>」にエンコードされずに現れることがあります。これでは、テキスト読み込み中に「<」を読み込んだらタグの始まり、という仕組みが破たんしてしまいます。試しに以下のプログラムを実行してみましょう。

   HTML文書を扱うクラスを使ったプログラム
class HTMLDocument {
(省略)
}

public class ExtractPlainTextWithClass2 {
  public static void main( String args[] ) {
    HTMLDocument doc = new HTMLDocument();

    doc.source = "<html><script>if ( a > b ) b = 
a;</script><head><title>タイトル</title></head><body><p>段落</p></body></html>";
    doc.showPlainText();
  }
}

   実行結果
C:\DOCUME~1 \MYDOCU~1\MYJAVA~1>java ExtractPlainTextWithClass2
if ( a
タイトル
段落

 <script>〜</script>に現れる「>」をHTMLパーサが誤認識してしまい、スクリプト部分を途中までしか読み取れませんでした。これでは困るので、パーサの仕組みを修正することにしましょう。ただし、現在のパーサはHTMLDocumentクラスのメソッドの一部として存在するので、メソッドを書き換えるとHTMLDocumentクラスの動作が変わってしまいます。このように処理が不適切になる場合は元のメソッドそのものを書き換えてしまっても構わないのですが、現在の処理内容を保ちつつ、新たな処理もできるようにしたい場合があります。HTML文書でいえば、HTML 2.0準拠の文書として扱う方法も残したいが、HTML 3.2準拠の文書も扱いたい、というような場合です。

 こんなとき、Javaではクラスの継承によってクラス間の関係を記述できるのでした。第4回「クラスの継承の本質を知る」では、平面上の場所を表すクラスを継承し、高さを格納するメンバ変数を導入することで、3次元座標を表すクラスを作りました。

 今回も同じように考えればよいのです。HTMLDocumentをスーパークラスとし、HTML 3.2に準拠する方法でプレーンテキストを取り出すサブクラスを作ればよいわけです。しかし、今回はメソッドの機能を変更することになります。座標を表すクラスの継承ではメンバ変数を追加するだけでしたが、メソッドの挙動を変えるにはどうしたらよいのでしょうか?

メソッドのオーバーライド

 Javaでは、スーパークラスとサブクラスで、概念としては同じことをするのだけれども処理する内容は異なる、という場合に「メソッドのオーバーライド」という方法を使います。HTML 3.2には、「<」の後に空白が続かないのがタグの始まり、というようにタグの始まりに関する定義を変更することで対応しました。HTMLを解釈する方法を切り替えて、メソッドの動作を変更したのが以下のプログラムです。

   HTMLDocument型のメソッドをオーバーライドするプログラム
class HTMLDocument {
(省略)
}

class HTML32Document extends HTMLDocument {
  void showPlainText() {
    boolean processingTag = false;
    int pos;
    int start = 0;

    for ( pos = 0; pos < source.length(); pos++ ) {
      // タグ
      if (processingTag) {
        if ( source.charAt(pos) != '>' ) {
          for ( pos++; pos < source.length(); pos++ ) {
            if ( source.charAt(pos) == '>' ) {
              break;
            }
          }
        }
        start = pos + 1;
      }
      // テキスト
      else {
        if ( source.charAt(pos) != '<' ) {
          for ( pos++; pos < source.length(); pos++ ) {
            if ( source.charAt(pos) == '<' ) {
              if ( source.charAt(pos+1) == ' ' )
                continue;
/*
注:変更個所である「source.charAt(pos+1)」は文字列の
長さを超える部分にアクセスする可能性があるが、
ここではエラーを回避する手だては取っていない。
*/
              System.out.println(source.substring( start, pos ));
              break;
           }
         }
        }
      }
      processingTag = !processingTag;
    }
  }
}

public class ExtractPlainTextWithClass3 {
  public static void main( String args[] ) {
    HTML32Document doc = new HTML32Document();

    doc.source = "<html><script>if ( a > b ) b = 
a;</script><head><title>タイトル</title></head><body><p>段落</p></body></html>";
    doc.showPlainText();
  }
}

   実行結果
C:\DOCUME~1\MYDOCU~1\MYJAVA~1>java ExtractPlainTextWithClass3
if ( a > b ) b = a;
タイトル
段落

 ここでは、HTML 3.2に対応したクラスとしてHTMLDocumentを継承するHTML32Documentというクラスを定義しました。変数docはHTML32Document型として宣言されているため、HTML文書はHTML 3.2対応のパーサで解析されて、適切にプレーンテキスト部分を取り出せました。HTML32Documentでは、スーパークラスであるHTMLDocumentのメソッドshowPlainText()と同名で引数の数も変わらないメソッドを再定義しています。

何がオーバーライドされるのか?

 「オーバーライド」とは英語で「決定されたことを覆す」という意味です。以下のプログラムを実行するとどのように表示されるか、考えてみてください。

   オーバーライドの意味を考えるプログラム
class HTMLDocument {
(省略)
}

class HTML32Document extends HTMLDocument  {
(省略)
}

public class ExtractPlainTextWithClass4 {
  public static void main( String args[] ) {
    HTML32Document doc32 = new HTML32Document();
    HTMLDocument doc20 = doc32;

    doc32.source = "<html><script>if ( a < b ) b =
 a;</script><head><title>タイトル</title></head><body><p>段落</p></body></html>";

    doc32.showPlainText();
    doc20.showPlainText();
  }
}

 上記のプログラムでは、HTML32Document型のインスタンスを、HTML32Document型の変数doc32と、HTMLDocument型の変数doc20に代入しています。「doc32.showPlainText();」という文はスクリプト部分を適切に解釈して表示できると予想できますが、「doc20.showPlainText();」という文はどうなるでしょうか? doc20に代入されているインスタンスはHTML32Document型ですが、変数doc20の型はHTMLDocumentです。

 このプログラムを実行すると、次のようになります。

C:\DOCUME~1\MYDOCU~1\MYJAVA~1>java ExtractPlainTextWithClass4
if ( a < b ) b = a;
タイトル
段落
if ( a < b ) b = a;
タイトル
段落

 実はメソッドをオーバーライドすると、オーバーライドされたスーパークラスのメソッドは隠されてしまいます。この例のようにスーパークラスの参照型を使っても、実行されるのはサブクラスのメソッドになります。元のメソッドの動作を上書きしてしまうことから、「オーバーライド」と呼ぶわけです。

 今回はクラスがデータを格納する単なる入れ物ではなく、「道具」であることを学びました。また、クラスのメソッドをオーバーライドすることで、スーパークラスとサブクラスで概念としては同じだが実際の処理内容が異なるメソッドに同じ名前を付けられることも学びました。次回はHTML文書を操作するクラスを通じて、クラスのメソッドを公開用と内部の作業用に分ける方法を説明します。


Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

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

メールマガジン登録

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