Powered by SmartDoc

イテレータの2つの形

2000年8月25日
浅海 智晴

目次

はじめに

世の中、表があれば裏がある。

横浜のラーメンと言えば、サンマーメンに代表される中華のチキンスープ系のスープです。

伊勢佐木町あたりをぶらぶら歩いていると、昔ながらの中華料理屋がいくつか並んでいますが、こういった店で食べるラーメンは、昔の面影が残っているノスタルジックなテーストで、涙が出てきそう。

こちらが、いわば横浜の"表"ラーメン。

そして今の横浜ラーメンを語るのであれば、「家系(いえけい)ラーメン」をおいて他にはありません。家系ラーメンは新杉田の吉村家(今は平沼橋に移転)を始祖とする、関東風トンコツ醤油ラーメンの一大勢力。濃厚なトンコツスープに鶏油(チーユ)を合わせた、脂の旨味をベースとしたラーメンは若人の味覚ととりこにして離さないのです。

これは、今の横浜ラーメンを語る上で欠かせない、"裏"横浜ラーメンと言えるでしょう。

さて、前回取り上げたイテレータは初心者にも簡単に使える、"表"イテレータ。でも、オブジェクトの世界をもっとディープに見るなら"裏"のイテレータも押さえておく必要があります。

ということで、今回はイテレータの表と裏、2つの種類のイテレータについて調べてみます。

イテレータの2つの形

前回はイテレータの観点から、オブジェクトの集まりに対する処理方法について調べてみました。

主にjava.util.Iteratorとして提供されているイテレータを中心に調べたわけですが、実は一般的にイテレータと呼ばれているものは、このjava.util.Iteratorによる方式だけではありません。

java.util.Iteratorは、正確にはエクスターナルイテレータ(external iterator;外部イテレータ)として呼ばれるものです。そう、エクスターナルがあればインターナルがあるのはものの道理で、インターナルイテレータ(internal iterator;内部イテレータ)ももちろん存在します。

つまり、「繰り返しの抽象化」というテクニックに関して、より深く理解するためにはエクスターナルイテレータとインターナルイテレータの2つのイテレータについての理解が必要になるわけです。

XMLアクセス

XMLドキュメントに格納された情報はエレメント、属性、テキストといった部品から構成されます。これらの部品をオブジェクトと見立てると、XMLドキュメントに対するアクセスはオブジェクトの集まりに対してアクセスしていることに他なりません。

このため、XMLドキュメントに対してアクセスするAPIを調べることによって、イテレータの実際の応用について検討することは有益でしょう。

ご存じのとおりXML操作のAPIにはDOM(Document Object Model)とSAX(Simple API for XML)の2種類があります。

DOMはW3Cの標準規格ですが、SAXはxml-devメーリングリストを中心に、ユーザの自発的な議論によって開発されたものです。

標準化団体の開発した公的な仕様と、ユーザグループが開発した私的な仕様。普通はどちらが生き残るのか、という話になる所ですが、SAXとDOMに関しては、そのどちらも必要ということに話は落ち着いているようです。

XMLドキュメント

SAXとDOMの比較のために、XMLドキュメント内の一定の条件を持つエレメントの数をカウントするプログラムを考えます。

データとしてリスト[XMLドキュメント]のXMLドキュメントを使います。

XMLドキュメント
<package name="org.w3c.dom">
  <class name="Element">
    <method name="getTagName"/>
    <method name="getAttribute"/>
    <method name="setAttribute"/>
    <method name="removeAttribute"/>
    <method name="getAttributeNS" since="DOM2"/>
    <method name="setAttributeNS" since="DOM2"/>
    <method name="remove" since="DOM2"/>
  </class>
  <class name="Attr">
    <method name="getName"/>
    <method name="getSpecified"/>
    <method name="getValue"/>
    <method name="setValue"/>
    <method name="getOwnerElement" since="DOM2"/>
  </class>
</package>

このXMLドキュメントはDOMのクラスとメソッドの一覧の一部です。個々のメソッドが定義されたAPIのバージョンはmethodエレメントのsince属性に格納されています。

このXMLドキュメントから"DOM2で追加されたメソッド"の数をカウントするプログラムを考えてみましょう。since属性に"DOM2"が設定されているmethodエレメントの数をカウントすれば目的を達成することができます。このプログラムにリスト[XMLドキュメント]を与えると、"5"を結果として出力することになるでしょう。

DOM

まずDOMを使ったプログラムの例です。

UseDOM(リスト[UseDOM.java])は、DOMを利用して処理を行うプログラムです。

このプログラムのmainメソッドでは、JAXPを利用して外部のXMLドキュメントファイルからDOMツリーの生成を行っています。そして、このDOMツリーをcountメソッドに渡しています。

ここでポイントとなるのはorg.w3c.dom.NodeListです。もちろん、NodeListはDOMがNodeアクセス用に用意しているイテレータです。

org.w3c.dom.DocumentgetElementsByTagNameメソッドにより、XMLドキュメント内に含まれる「タグ名が"method"の全エレメント」をアクセスするためのイテレータとしてNodeListが返されてくるわけです。

countメソッドは、このNodeListを使って、典型的なシーケンシャルアクセスを行っています。

UseDOM.java
import java.net.URL;
import java.net.MalformedURLException;
import java.io.File;
import java.io.IOException;
import javax.xml.parsers.*;
import org.xml.sax.SAXException;
import org.w3c.dom.*;

public class UseDOM {
    public static void main(String[] args) {
	URL url = null;
	try {
	    url = new File(args[0]).toURL();
	} catch (MalformedURLException e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	try {
	    DocumentBuilderFactory factory
		= DocumentBuilderFactory.newInstance();
	    DocumentBuilder builder = factory.newDocumentBuilder();
	    Document doc = builder.parse(url.toExternalForm());
	    System.out.println(count(doc));
	} catch (ParserConfigurationException e) {
	    e.printStackTrace();
	} catch (SAXException e) {
	    e.printStackTrace();
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }

    public static int count(Document doc) {
	NodeList nodes = doc.getElementsByTagName("method");
	int sum = 0;
	int size = nodes.getLength();
	for (int i = 0;i < size;i++) {
	    Element element = (Element)nodes.item(i);
	    String since = element.getAttribute("since");
	    if ("DOM2".equals(since)) {
		sum++;
	    }
	}
	return (sum);
    }
}

SAX

今度はSAXを使うことを考えてみましょう。

まず、SAXパーサから呼ばれるハンドラのクラスが必要です。このハンドラクラスとしてCountHandler用意します。(リスト[CountHandler.java])

CountHandler.java
import org.xml.sax.*;

public class CountHandler extends HandlerBase {
    private int count_ = 0;

    public void startElement(String name, AttributeList attrs) {
	if (!"method".equals(name)) {
	    return;
	}
	String since = attrs.getValue("since");
	if ("DOM2".equals(since)) {
	    count_++;
	}
    }

    public int getCount() {
	return (count_);
    }
}

このハンドラクラスをSAXパーサに登録すればOKです。リスト[UseSAX.java]はJAXPを使用してSAXパーサを生成しています。このSAXパーサに先程のCountHandlerを登録すればプログラムは完成です。

UseSAX.java
import java.net.URL;
import java.net.MalformedURLException;
import java.io.File;
import java.io.IOException;
import javax.xml.parsers.*;
import org.xml.sax.*;

public class UseSAX {
    public static void main(String[] args) {
	URL url = null;
	try {
	    url = new File(args[0]).toURL();
	} catch (MalformedURLException e) {
	    e.printStackTrace();
	    System.exit(1);
	}
	try {
	    SAXParserFactory spf = SAXParserFactory.newInstance ();
	    SAXParser sp = spf.newSAXParser();
	    Parser parser = sp.getParser();
	    CountHandler handler = new CountHandler();
	    parser.setDocumentHandler(handler);
	    parser.parse (url.toExternalForm());
	    System.out.println(handler.getCount());
	} catch (ParserConfigurationException e) {
	    e.printStackTrace();
	} catch (SAXParseException e) {
	    e.printStackTrace();
	} catch (SAXException e) {
	    e.printStackTrace();
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }
}

SAXとDOM

SAXとDOMの存在は、ノードの集合に対してシーケンシャルアクセスを行うAPIとして、2つのメカニズムが共存できることを表しています。

DOMとSAXの操作イメージ

その理由はもちろんDOMとSAXが全く異なったコンセプトに基づくAPIだからです。具体的には以下のような分類が行われています。

DOM版とSAX版の違いは、「誰が処理の主導権を持っているのか」という点にあります。

DOM版ではアプリケーション主導でDOMツリーのトラバースが行われているのに対して、SAX版ではSAXパーサが処理の主導権を持っています。

DOM版であるリスト[UseDOM.java]ではcountメソッド内で(1)DOMツリーからシーケンシャルアクセスのためのイテレータであるNodeListの取得、(2)NodeListを使ったシーケンシャルアクセスのよって取得したElementに対しての判定処理を行っています。

SAX版であるリスト[UseSAX.java]では、SAXパーサから呼ばれるCountHandler(リスト[CountHandler.java])で、(1)イベントによって通知されたElementに対しての判定処理を行っています。

ここで挙げているような単純な例では、使い勝手や機能に優劣をつけるのはちょっと難しい気がします。SAXとDOM、どちらを選ぶのかはまさに好みの問題と言えるでしょう。

NodeListは、まさに前回ご紹介したイテレータですが、「繰り返しを抽象化する」という点に関しては、SAXのイベント駆動型メカニズムもイテレータ的な処理に他なりません。実はSAXの手法もイテレータという技術の範疇に入っています。

全く異なったメカニズムを持つ2つの手法を同じ"イテレータ"という用語で表現するのはかなり紛らわしいので、この2つの形態のイテレータを区別するためにそれぞれ以下のような名前がついています。

プログラムで用いたDOMのNodeListは典型的なエクスターナルイテレータで、SAXのようにコールバックのハンドラを登録する形態のイテレータがインターナルイテレータということになります。

Lispのイテレータ

先程の例はXMLに特化した話でしたが、もっと汎用性のある形で、エクスターナルイテレータとインターナルイテレータの違いについて考えてみましょう。

ここでは、インターナルイテレータの優位性が極端な形で現れるLispを題材にします。

なお本節のLispの話題は、全体の雰囲気が伝える目的ですのでLispの細かい文法は気にせず、ご覧ください。このプログラムはEmacs上でも動作するので、興味のある方は「*scratch*バッファ」などで試してみるのも面白いでしょう。

setqはLispで代入を行うための関数です。以下のプログラムは変数numsに、リスト"(1 2 3 4 5)"を代入するものです。リスト"(1 2 3 4 5)"は数字の1, 2, 3, 4, 5が格納された要素数5のリストです。(1)

(setq nums '(1 2 3 4 5))

この各要素の値を1増やすプログラムcountUpを考えてみます。このcountUpは以下のように実行されることになります。

> (countUp num)
(2 3 4 5 6)

普通の方法

リスト[Lisp - while]は、リストの全要素の値を1増やすLispの関数countUpの定義です。

Lisp - while
(defun countUp (list)
  (let (result)
    (while list
      (setq result (append result (list (+ (car list) 1))))
      (setq list (cdr list)))
    result))

引数で渡されたlistwhileを使ってシーケンシャルアクセスしながら、結果となるリストを構築しています。

Lispを知らない方でもなんとなくイメージはつかんでいただけるのではないかと思います。

なお、Lispではデータ構造のトラバース処理ではエクスターナルイテレータを使わないのが普通です。Lispの名前の由来であるList Processorからも分かる通り、Lispはリスト構造というシンプルでかつ強力なデータ構造の操作を行うことを主眼としたプログラミング言語で、すべての情報をリスト構造にしてしまうことで、結果的にポリモーフィックイテレーションを実現しているからです。

mapcar

Lispでは先程のcountUpのように、whileを使った一般的なシーケンシャルアクセスができることが分かりました。

しかし、Lisp的にはもっと良い方法があります。

この良い方法とはラムダ式とmapcar関数を使う方法。この方法を使えば以下のようなプログラムで同様の処理が実現できます。

Lisp - mapcar
(defun countUp (list)
  (mapcar (function (lambda (element)
	       (+ element 1)))
          list))

なんともあっさりと、しかもシンプルに実現できました。このシンプルさはプログラマを虜にしてしまいそうです。

もちろん、このプログラムは典型的なインターナルイテレータの実装例となっています。

ここでlambdaという見慣れない名前が出てきますが、これはLispの理論的な基盤でもあるラムダ式(λ式;lambda expression)です。ラムダ式というとちょっと難しそうですが、"名前のついていない関数"と考えてもらえれば分かりやすいと思います。

"名前のついていない"というと、Javaでは無名クラス(匿名クラス;anonymous class)が思い浮かびますね。そう、Javaの無名クラスは、Lispのラムダ式の機能(の一部)をJavaで実現するためにサポートされたものなのです。

Lispプログラミングにおいては、このようなインターナルイテレータの活用がとても重要でした。これは「ラムダ式」という関数をデータとして扱うための仕組みが、第一級の言語仕様として用意されており、このメカニズムを活用するプログラミングスタイルがとても自然なことだからです。

もちろん統一的なデータ構造であるリストがこのプログラミングスタイルの基盤となっています。

Lispのイテレータの比較

Lispのイテレータ

Lispのイテレータの比較を図[Lispのイテレータ]に示します。

(1)はリスト[Lisp - while]の動きを図にしたものです。正確にはエクスターナルイテレータではありませんが、リストのみをデータ構造とするというLispの言語の成り立ち上、事実上エクスターナルイテレータ的な意味合いを持っています。

(2)はリスト[Lisp - mapcar]の動きを図にしたものです。先程のSAXの例と同じく、典型的なインターナルイテレータのメカニズムとなっています。SAXの場合にはSAXパーサが、ノードトラバースを主導していましたが、この例ではmapcar関数がノードトラバースを主導しています。

  1. 正確には代入(assignment)ではなくバインド(binding)ですが、これも この際気にしなくてかまいません。

Javaで書いてみる

今度は同じプログラムをJavaで書いてみます。

使用するデータ構造はjava.lang.Integerが格納されたjava.util.Listです。

エクスターナルイテレータバージョン

エクスターナルイテレータを使ったJavaプログラムはリスト[Javaによるエクスターナルイテレータ]となります。

このプログラムは特に難しいところはないでしょう。

Javaによるエクスターナルイテレータ
    public static List useExternalIterator(List list) {
	List result = new ArrayList();
	Iterator iter = list.iterator();
	while (iter.hasNext()) {
	    Integer value = (Integer)iter.next();
	    int intValue = value.intValue();
	    result.add(new Integer(intValue + 1));
	}
	return (result);
    }

もちろん、Javaではコンテナ直接アクセス(リスト[コンテナ直接サクセス])と配列(リスト[配列アクセス])によるアクセスを行う方法もあります。どの方法を選ぶのが適切なのかという点は、もちろんケースバイケースで、これは前回、前々回に検討しました。

コンテナ直接サクセス
    public static List useContainer(List list) {
	List result = new ArrayList();
	int size = list.size();
	for (int i = 0;i < size;i++) {
	    Integer value = (Integer)list.get(i);
	    int intValue = value.intValue();
	    result.add(new Integer(intValue + 1));
	}
	return (result);
    }
配列アクセス
    public static List useArray(List list) {
	List result = new ArrayList();
	Integer[] integers = new Integer[list.size()];
	integers = (Integer[])list.toArray(integers);
	for (int i = 0;i < integers.length;i++) {
	    int intValue = integers[i].intValue();
	    result.add(new Integer(intValue + 1));
	}
	return (result);
    }

インターナルイテレータバージョン

それではインターナルイテレータを使ったJavaプログラムを考えてみましょう。

インターナルイテレータの場合はちょっと大変で、実際のアプリケーションを書く前に2点ほど準備が必要です。

まず必要なのがinterfaceです。インターナルイテレータを主導するフレームワークと、そこから呼び出されるハンドラを接続するために必要となります。

IAdder.java
public interface IAdder {
    Integer add(Integer num);
}

次に必要となるのは、フレームワーク部分です。

Lispでは標準で用意されているmapcar関数を使うことができましたが、Javaの場合には自前で用意することになります。(リスト[ListOperations])

ListOperations
import java.util.*;

public final class ListOperations {
    public static List mapcar(IAdder adder, List list) {
	List result = new ArrayList();
	int size = list.size();
	for (int i = 0;i < size;i++) {
	    Integer value = (Integer)list.get(i);
	    value = adder.add(value);
	    result.add(i, value);
	}
	return (result);
    }
}

インタフェース(リスト[IAdder.java])とフレームワーク(リスト[ListOperations])を使ったクライアントプログラムはリスト[Util.java]となります。

Util.java
    public static List useInternalIterator(List list) {
	List result = ListOperations.mapcar(
	    new IAdder() {
	        public Integer add(Integer value) {
		    int intValue = value.intValue();
		    return (new Integer(intValue + 1));
		}
	    },
	    list
	);
	return (result);
    }

インターナルイテレーションバージョンはリスト[IAdder.java], リスト[ListOperations], リスト[Util.java]の3つのプログラムから構成されることになり、明らかにエクスターナルイテレーションバージョン(リスト[Javaによるエクスターナルイテレータ])よりも複雑です。

もちろん、IAdder(リスト[IAdder.java])とListOperations(リスト[ListOperations])は他の用途に再利用できそうな感触はあって、そうなればかなりスリムになるでしょう。

とはいえ、アプリケーション本体であるuseInternalIterator(リスト[Util.java])単体でも、useExternalIterator(リスト[Javaによるエクスターナルイテレータ])よりプログラムが複雑になっているのは問題ですね。このあたりの複雑さが、Javaでインターナルイテレータがあまり使われていない要因となっています。

選択肢

Javaでインターナルイテレータを作る場合、問題になるのが「処理」を表すinterfaceの設計方法です。

先程の例ではIntegerを入力と出力とするinterfaceを定義しました。

そうなるとオブジェクトIntegerに対応する基本型intも要りそうな気がしてきます。プログラム上に登場するのは圧倒的にintの方が多いですから。

public interface IAdder {
    int add(int num);
}

また、足し算できるのはIntegerだけではない、という観点もあります。この方向を追求すると、以下のようにjava.lang.Numberを使いたくなります。

public interface IAdder {
    Number add(Number num);
}

さらに、汎用性を求めるのならObjectを入出力にすることも考えられます。たとえば文字列のように数値ではなく、かつ加算に準じる処理(たとえば文字列の連結)が可能なオブジェクトはいくらでもあります。

public interface IAdder {
    Object add(Object element);
}

Javaは静的型の言語なので、インタフェース設計の際には、このような型に対する考慮がどうしても必要になります。このあたりが、インターナルイテレータを複雑にしてしまう要因となっています。

より汎用的にする

より汎用的にすることを考えていくと、入出力の型だけでなく、メソッド名もより汎用的な形にすることが考えられます。

この目的のためのインタフェースとしてリスト[IUnaryFunction.java]が考えられます。汎用性を追求する以上、メソッド名はfunctionという無味乾燥なものになりますし、入出力はObjectを使うことになります。

IUnaryFunction.java
public interface IUnaryFunction {
    Object function(Object arg);
}

このIUnaryFunctionを使ったmapcar関数はリスト[ListOperations.java]のようになります。

ListOperations.java
import java.util.*;

public final class ListOperations {
    public static List mapcar(IUnaryFunction func, List list) {
	List result = new ArrayList();
	int size = list.size();
	for (int i = 0;i < size;i++) {
	    Object value = list.get(i);
	    value = func.function(value);
	    result.add(value);
	}
	return (result);
    }
}

IUnaryFunctionと新mapcarを使った、クライアントプログラムはリスト[useInternalIterator]というようになります。

プログラム内で自らキャストをしなければならないこともあり、先程のIAdder版のプログラムと比較すると、若干可読性は下がっています。

useInternalIterator
    public static List useInternalIterator(List list) {
	List result = ListOperations.mapcar(
	    new IUnaryFunction() {
	        public Object function(Object value) {
		    Integer integer = (Integer)value;
		    int intValue = integer.intValue();
		    return (new Integer(intValue + 1));
		}
	    },
	    list
	);
	return (result);
    }

Integer版のIAdderのように、コンテナに格納されているオブジェクトの型を決め打ちしてしまうと、アプリケーションは書きやすくなりますが、フレームワーク側の再利用の可能性が低くなってしまいます。しかし、IUnaryFunctionのように汎用的にしてしまうと、今度はアプリケーションプログラムから使いにくくなってしまいますし、また出来上がったプログラムの可読性も下がります。

このあたりは、Javaの静的型による厳しい言語仕様が"あだ"となってしまうようです。

ビジタ

Javaではあまり使い勝手のよくないインターナルイテレータですが、アプリケーションの処理がもう少し複雑になってくると俄然事情が変わってきます。

前節で作ったプログラムは、「Listに格納されているIntegerの集まりに対して操作を加える」というものでした。

ここで、問題を「Listに格納されているByte, Short, Integer, Long, Float, Doubleの集まりに対して操作を加える」というように再定義してみます。このような、複数の種類のオブジェクトから構成されるオブジェクトの集まりに対する処理を考える場合、ビジタ(visitor)のテクニックが有効です。

ビジタは『Design Patterns』でもVisitorパターンとしてパターン化されている、とても利用頻度の高いテクニックです。

Visitorパターンは以下の5つの参加者から構成されています。

これらの参加者と今回のプログラムの関係を図[UseVisitorの構造]に示します。

ここでは、エレメントはjava.lang.Objectで、コンクリートエレメントがjava.lang.Byte, java.lang.Short, java.lang.Integer, java.lang.Long, java.lang.Float, java.lang.Doubleの6つの数値関連のクラスです。

一点純粋なVisitorパターンと異なっているのは、各クラスがacceptメソッドを持っていないことです。これは既存クラスに対してVisitorパターンを適用するためのテクニックで、トラバース処理側でacceptメソッド相当の処理を行っています。

IVisitor.java
public interface IVisitor {
    void visit(Byte value);
    void visit(Short value);
    void visit(Integer value);
    void visit(Long value);
    void visit(Float value);
    void visit(Double value);
}
CountUp.java
import java.util.*;

public class CountUp implements IVisitor {
    private List list_ = new ArrayList();

    public void visit(Byte value) {
	list_.add(new Byte((byte)(value.byteValue() + 1)));
    }

    public void visit(Short value) {
	list_.add(new Short((short)(value.shortValue() + 1)));
    }

    public void visit(Integer value) {
	list_.add(new Integer(value.intValue() + 1));
    }

    public void visit(Long value) {
	list_.add(new Long(value.longValue() + 1));
    }

    public void visit(Float value) {
	list_.add(new Float(value.floatValue() + 1));
    }

    public void visit(Double value) {
	list_.add(new Double(value.doubleValue() + 1));
    }

    public List getList() {
	return (list_);
    }
}
Util.java
import java.util.*;

public final class Util {
    public static void traverse(List nums, IVisitor visitor) {
	Iterator iter = nums.iterator();
	while (iter.hasNext()) {
	    Object object = iter.next();
	    if (object instanceof Byte) {
		visitor.visit((Byte)object);
	    } else if (object instanceof Short) {
		visitor.visit((Short)object);
	    } else if (object instanceof Integer) {
		visitor.visit((Integer)object);
	    } else if (object instanceof Long) {
		visitor.visit((Long)object);
	    } else if (object instanceof Float) {
		visitor.visit((Float)object);
	    } else if (object instanceof Double) {
		visitor.visit((Double)object);
	    } else {
		throw (new InternalError());
	    }
	}
    }
}
UseVisitor.java
import java.util.*;

public class UseVisitor {
    public static List countUp(List list) {
	CountUp visitor = new CountUp();
	Util.traverse(list, visitor);
	return (visitor.getList());
    }
}
UseVisitorの構造

このプログラムの良い所は、Utiltraverseメソッド(リスト[Util.java])の再利用が可能になっていることです。

リストをシーケンシャルアクセスする処理と各オブジェクトのタイプに応じて適切なvisitメソッドを呼び分ける処理は、決まり切ったルーチンワーク的な処理だけに、アプリケーション開発のたびに新規にコーディングすることになれば、あまり楽しいものではありません。

ビジタを使わなかった場合

ビジタの効果を見るためには、ビジタを使わない場合の処理を見てみるのが早道です。

DontUseVisitor(リスト[DontUseVisitor.java])は、ビジタを利用せず、エクスターナルイテレータを使って、同様の処理を行ったプログラムです。

DontUseVisitor.java
import java.util.*;

public class DontUseVisitor {
    public static List countUp(List list) {
	List result = new ArrayList();
	Iterator iter = list.iterator();
	while (iter.hasNext()) {
	    Object object = iter.next();
	    if (object instanceof Byte) {
		Byte value = (Byte)object;
		result.add(new Byte((byte)(value.byteValue() + 1)));
	    } else if (object instanceof Short) {
		Short value = (Short)object;
		result.add(new Short((short)(value.shortValue() + 1)));
	    } else if (object instanceof Integer) {
		Integer value = (Integer)object;
		result.add(new Integer(value.intValue() + 1));
	    } else if (object instanceof Long) {
		Long value = (Long)object;
		result.add(new Long(value.longValue() + 1));
	    } else if (object instanceof Float) {
		Float value = (Float)object;
		result.add(new Float(value.floatValue() + 1));
	    } else if (object instanceof Double) {
		Double value = (Double)object;
		result.add(new Double(value.doubleValue() + 1));
	    } else {
		throw (new InternalError());
	    }
	}
	return (result);
    }
}

トラバース処理とカウント処理が渾然一体となっていることが、ひと目で分かります。

このプログラムの問題は、アプリケーションの本質的な処理であるカウント処理と、汎用処理であるトラバース処理がきれいに分離されていないことにあります。

本来プログラムの開発では、アプリケーションロジックだけを記述することが理想的なのですが、アプリケーションロジックを取り囲む汎用性の高いロジックも同時に開発しなければならなくなっているのです。

ビジタを使えば、トラバースロジックとアプリケーションロジックを奇麗に分離することができ、さらに汎用的なロジックであるトラバースロジックは再利用可能になるというわけです。

フレームワークとしてのインターナルイテレータ

オブジェクト指向技術の一つの到達点はフレームワークと呼ばれる技術です。オブジェクト指向のメリットの一つである再利用は、フレームワーク技術の本格的な適用により、より大きな成果をあげることができるでしょう。

インターナルイテレータやその発展形であるビジタは、このフレームワーク技術の原始的な形態とも言えるものです。一見構造が複雑で使いづらそうに見えるインターナルイテレータがなぜ便利なのか。これはフレームワーク技術の特徴が、分かりやすい形で現れていると考えることができます。

エクスターナルイテレータとインターナルイテレータの比較

このあたりの事情を図[エクスターナルイテレータとインターナルイテレータの比較]に視覚化してみました。ただし、グラフの傾斜や形などはボクの直感的な印象をそのまま表しているものなので、注意して下さいね。

Lispにおいては、最初からインターナルイテレータの方が簡単である、という結果になりました。

それに対して、Javaの場合には、コンテナのシーケンシャルアクセスといったような、ごく簡単な応用ではエクスターナルイテレータの方が簡単ですが、処理の内容が複雑になるに従って、インターナルイテレータの方が簡単になってきます。

LispとJavaの違いをもう一つ図にしてみましょう。

図[LispとJavaの比較]は簡単なフレームワークと複雑なフレームワークのLispとJavaでの実装の違いを視覚化したものです。上側にある箱が再利用可能なフレームワークで、下側にある箱がアプリケーションが実装しなければならない処理を表しています。ハッチングされている部分がアプリケーション側で開発が必要な処理です。

Lispの場合には、静的型の制約がないことと、リストという単一のデータ構造をベースにしていることによって、フレームワークとアプリケーションの接続はとてもスムーズです。しかし、Javaの場合、ユーザ定義型と静的型の制約があるために、この接続部分がかなり複雑になってしまうわけです。

簡単なフレームワークの場合、Lispでは接続部分がシンプルなので、フレームワークを使うことの意義はそれなりにあります。しかし、Javaでは接続部分が複雑になってしまうため、フレームワークを使う意義がどうしても薄れてしまうわけです。それに対して、複雑なフレームワークの場合には、インタフェースの部分が多少複雑であっても、フレームワークを再利用する価値は大いにあります。

LispとJavaの比較

この問題をJavaにおけるインターナルイテレータとエクスターナルイテレータという観点で対比させたのが図[簡単なフレームワークと複雑なフレームワーク]です。図[LispとJavaの比較]と同様にハッチングされている部分がアプリケーション側で開発が必要な処理です。

フレームワークが簡単なものであるなら、フレームワークの提供する複雑なインタフェースに合せたコーディングをするより、自分ですべてやってしまった方が楽です。(図[簡単なフレームワークと複雑なフレームワーク]の上)しかし、フレームワークの処理が複雑になればなるほど、自分でコーディングすることは全くナンセンスになってしまいます。(図[簡単なフレームワークと複雑なフレームワーク]の下)

簡単なフレームワークと複雑なフレームワーク

単純なシーケンシャルアクセスでは、それほど便利ではなかったインターナルイテレータが、ビジタという複雑なメカニズムが必要になった途端、とても便利なものになる。このからくりは再利用できるフレームワークの複雑度がポイントというわけです。

まとめ

裏イテレータのディープな味はいかがだったでしょうか。

クライアント主導ではない、フレームワーク主導の制御構造は、本質的に難易度の高い技術ですが、うまく使いこなすことができれば、非常に大きな生産性の向上が期待できます。

こういった筋の良さが、上級プログラマがインターナルイテレータを好む理由となっています。しかしながら、フレームワーク技術そのものはまだまだ発展途上であり、"誰でも気軽に使える"というような成熟した技術にはなっていません。

これから発展が期待される、また発展してもらわなくては困るフレームワーク技術へ取り組むきっかけとして、まずインターナルイテレータをとば口にしてみるのもよいかもしれません。

では最後に、エクスターナルイテレータとインターナルイテレータに関する今回の結論です。

今月の情報源

家系ラーメン,http://www.asahi-net.or.jp/~et7t-tum/iekei0.html

とうまさんの家系ラーメンのページ。家系ラーメンのブレイクはこのページによるところが大きいでしょう。

Design Patterns - Elements of Reusable Object-Oriented Software, E. Gamma他, Addison Wesley, 1995

今回はIteratorパターンとVisitorパターンを取り上げてみました。

Java Collections API Design FAQ,

http://java.sun.com/products/jdk/1.2/docs/guide/collections/designfaq.html#6

JavaのCollectionを使うなら必読のページです。Java Collection設計上の取捨選択のポイントが網羅されていて、とても参考になります。

JGL 3.1, ObjectSpace, http://www.objectspace.com/products/prodJGL.asp

JGLにあってJava Collectionにないもの。第2回に謎かけしていた、その答えはインターナルイテレータです。

ボク自身もJGLのインターナルイテレータを使ってずいぶんコーディングしてみましたが、Javaにおいて単純なシーケンシャルアクセスはエクスターナルイテレータの方が絶対便利で、さらに言うなら配列やコンテナ直接アクセスを使うのがより好ましいというのが結論でした。

いかにも玄人受けしそうなインターナルイテレータを仕様から落としたJava Collection設計者の勇気には脱帽です。

Java API for XML Parsing Specification Version 1.0.1, http://java.sun.com/xml/

SAX1, http://www.megginson.com/SAX/SAX1/index.html

DOM1, http://www.w3.org/TR/REC-DOM-Level-1/

XMLアクセスのためにSAXとDOMのどちらを使うべきか。もちろん、最終的な答えは適材適所ということになりますが、とりあえずどちらか一方だけ、ということならDOMを覚えた方が得というのが、ボクの意見です。その答えは...次回に分かるかもしれません。

なお今回はJAXPを中心にSAX1, DOM1を使ってみましたが、そろそろSAX2, DOM2に移行する潮時のような気がします。ポイントはネームスペースの使用の有無。とはいえ現状のネームスペースはDTDと連携が取れていないという致命的な欠陥があるので、今ひとつ使いづらいのが難点なんですよね。

Building Application Frameworks : Object-Oriented Foundations of Framework Design, Mohamed Fayad Douglas C. Schmidt他編, Willy, 1999

こういったとてもよくまとまった本が出てくるということは、そろそろフレームワーク技術も本格的な実用フェーズに入ってきたということかもしれません。

とはいっても、この本は???ページにも渡る大著。同じWillyから出ている姉妹書である『Implementing Application Frameworks : Object-Oriented Frameworks at Work』や『Domain-Specific Application Frameworks : Frameworks Experience by Industry』と合せると総計???ページにもなってしまいます。

ページ数は後程お知らせします

フレームワーク技術がいかに難易度の高い技術なのかがよく分かります。

とはいえ、フレームワーク技術はこれからのソフトウェアエンジニアの必須技術になることは間違いありません。この技術をマスターできているか否かが、デザイナとプログラマのスキルを分ける分水嶺となるでしょう。