今月も引き続きjava.utilパッケージの新機能を紹介していきます。

今週と来週は新たに追加されたマップとセット、NavigableMapインタフェースとNavigableSetインタフェースを紹介していきます。

マップを使っている時、どのようなキーが使われているか分からない場合がありませんか。

どのようなキーが使われているか分からないので、筆者はキーの一覧を取得して、そこからイテレーションで回して、目的のキーを選ぶということをやってしまいます。しかし、これはマップの性質をまったく活かしていません。

たとえば、英和辞書を保持しているマップがあったとします。キーが英単語で、バリューがその単語の意味だとします。

そこからsupercalifragilisticexpialidocious注1の意味を調べようとしたとしましょう。でも、supercalifragilisticexpialidociousのつづりがよく分かりません。とりあえずsuperからはじまる英単語を調べてみようとしても、super以外はsuperではじまる英単語を直接マップから取得することができません。

このようにキーが直接分からない場合などに使えるのが、java.util.NavigableMapインタフェースです。

さっそく、サンプルでその動作を調べていきましょう。

サンプルのソース NavigableMapSample.java

NavigableMapインタフェースを実装したクラスには、TreeMapクラスとスレッドセーフなConcurrentSkipListMapクラスがあります。サンプルではTreeMapクラスを使用していきます。

NavigableMapインタフェースは表1に示したような、指定されたキーに最も近いキーを返すメソッドが定義されています。

表1 NavigableMapインタフェースのナビゲーションメソッド
メソッド名 説明
lowerKey(key) 指定されたキーよりも小さいキーを返します
floorKey(key) 指定されたキーと等しいか小さいキーを返します
ceilingKey(key) 指定されたキーと等しいか大きいキーを返します
higherKey(key) 指定されたキーよりも大きいキーを返します
lowerEntry(key) 指定されたキーよりも小さいキーに対応するMap.Entryオブジェクトを返します
floorEntry(key) 指定されたキーと等しいか小さいキーに対応するMap.Entryオブジェクトを返します
ceilingEntry(key) 指定されたキーと等しいか大きいキーに対応するMap.Entryオブジェクトを返します
higherEntry(key) 指定されたキーよりも大きいキーに対応するMap.Entryオブジェクトを返します

これらのメソッドは指定したキーの小さい、もしくは大きいキーを選び出します。このためキーがソートされている必要があり、NavigableMapSampleインタフェースはSortedMapインタフェースの派生インタフェースとなっています。また、キーに相当するものがなかった場合はnullが返ります。

実際にこれらのメソッドを使ってみます。まずマップを用意します。

        NavigableMap<String, String> map = new TreeMap<String, String>();
        map.put("E", "Echo");
        map.put("M", "Mike");
        map.put("A", "Alpha");
        map.put("S", "Sierra");
        map.put("K", "Kilo");
        map.put("I", "India");
        map.put("Q", "Quebec");
        map.put("C", "Charlie");
        map.put("G", "Golf");
        map.put("O", "Oscar");

lowerKeyメソッド、floorKeyメソッドを使ってみましょう。

        // キーがマップに含まれている場合
        System.out.println("lower G: " + map.lowerKey("G"));
        System.out.println("floor G: " + map.floorKey("G"));
  
        // キーがマップに含まれていない場合
        System.out.println("lower F: " + map.lowerKey("F"));
        System.out.println("floor F: " + map.floorKey("F"));

この部分に対応する出力は次のようになりました。

lower G: E
floor G: G
lower F: E
floor F: E

キーがマップに含まれている場合、floorKeyメソッドは自分自身を帰すため、lowerKeyメソッドの戻り値とは異なります。逆にキーがマップに含まれていなければ、双方のメソッドとも、指定されたキーに最も近いキーを返すため、値は同じになっています。

ceilingKeyメソッド、higherKeyメソッドでもそれは同じです。

        // キーがマップに含まれている場合
        System.out.println("cieling I: " + map.ceilingKey("I"));
        System.out.println("heigher I: " + map.higherKey("I"));
 
        // キーがマップに含まれていない場合
        System.out.println("cieling J: " + map.ceilingKey("J"));
        System.out.println("heigher J: " + map.higherKey("J"));

この部分に対応する出力を次に示します。

cieling I: I
heigher I: K
cieling J: K
heigher J: K

ここで示したように指定されたキーの次を示すことができるため、これらのメソッド群はナビゲーションメソッドと呼ばれます。インタフェース名のNavigableもナビゲーションが可能なところから付けられたようです。

また、NavigableMapインタフェースではSortedMapインタフェースのheadMapメソッド、tailMapメソッド、subMapメソッドも強化されています。

これらのメソッドはすべてSortedMapオブジェクトが戻り値となっています。そこで、新たにNavigableMapオブジェクトを返すメソッドがオーバロードされています。

引数が1つ増加し(subMapメソッドは2つ)、指定されたキーを含むか含まないかを指定することができるようになりました。また、オーバロードされたメソッドは戻り値の型がNavigableMapインタフェースになっています。

        System.out.println("headMap: Eを含まない");
        System.out.println(map.headMap("E", false));
        System.out.println("headMap: Eを含む");
        System.out.println(map.headMap("E", true));
        System.out.println("tailMap: Oを含まない");
        System.out.println(map.tailMap("O", false));
        System.out.println("tailMap: Oを含む");
        System.out.println(map.tailMap("O", true));
        System.out.println("subMap: K, Oを含まない");
        System.out.println(map.subMap("K", false, "O", false));
        System.out.println("subMap: K, Oを含む");
        System.out.println(map.subMap("K", true, "O", true));

実行結果を次に示します。

headMap: Eを含まない
{A=Alpha, C=Charlie}
headMap: Eを含む
{A=Alpha, C=Charlie, E=Echo}
tailMap: Oを含まない
{Q=Quebec, S=Sierra}
tailMap: Oを含む
{O=Oscar, Q=Quebec, S=Sierra}
subMap: K, Oを含まない
{M=Mike}
subMap: K, Oを含む
{K=Kilo, M=Mike, O=Oscar}

実行結果からも分かるように、キーを含むか含まないかを明確に指定できるようになりました。

NavigableMapインタフェースの応用例

NavigableMapインタフェースの使い方は分かったので、辞書的な使い方を試してみることにしましょう。

サンプルのソース ZIPNumberSearcher.java

ZIPといってもファイル圧縮ではなくて、郵便番号です。ここではキーを郵便番号、バリューを住所としたマップを用意しました。といっても、東京のごくごく一部の郵便番号しか登録していませんが。

    private NavigableMap<String, String> init() {
        NavigableMap<String, String> dic
            = new TreeMap<String, String>();

        // 東京の郵便番号
        dic.put("102-0083", "千代田区一番町");
        dic.put("100-0013", "千代田区霞が関");
        dic.put("104-0061", "中央区銀座");
                   ......

このマップで郵便番号を検索してみましょう。単にgetメソッドをコールした場合、そのキーがなければnullになってしまいます。

そこで、ceilingKeyメソッドやfloorKeyメソッドを組み合わせてみました。以下のコードでは150-0043を調べています。

        NavigableMap<String, String> dic = init();
         
        // 1500043 に最も近い番号を調べる
        String key = "150-0043";
 
        if (dic.containsKey(key)) {
            // キーが登録されていた場合
            System.out.printf("%s に対応する郵便番号は %d です%n",
                              key, dic.get(key));
        } else {
            // キーが登録されていなかった場合
            String lowerKey = dic.lowerKey(key);
            String higherKey = dic.higherKey(key);
            System.out.printf("%s に近い郵便番号%n", key);
            if (lowerKey != null) {
                System.out.printf("%s(%s)%n",
                                  dic.get(lowerKey), lowerKey);
            }
            if (higherKey != null) {
                System.out.printf("%s(%s)%n", 
                                  dic.get(higherKey), higherKey);
            }
        }

まずcontainsKeyメソッドでkeyがマップに登録されているかをチェックします。もし、登録されていればそのまま出力します。

登録されていなければ、keyの前後のキーを取り出し、それを出力します。この前後のキーを取得するのに、赤字で示したようにlowerKeyメソッドおよびhigherKeyメソッドを使用します。それらのキーに対応するバリューは青字で記述したようにgetメソッドで取得することができます。

keyより小さいキーがなかった場合、もしくはkeyより大きいキーがなかった場合、nullが返ります。そのため、nullかどうかのチェックを忘れないようにします。

出力結果を以下に示します。

150-0043 に近い郵便番号
渋谷区宇田川町(150-0042)
渋谷区松濤(150-0046)

実際には150-0043に相当するのは渋谷区道玄坂なのですが、このマップには登録されていなかったようです。そこで、その番号に近い150-0042と150-0046が表示されます。

ついでに登録されている150で始まる郵便番号も出力しておきましょう。

        // 150 で始まる郵便番号を抽出
        NavigableMap<String, String> subdic 
            = dic.subMap("150-0000", true, "150-9999", true);
 
        // これでも同じ
//        NavigableMap<String, String> subdic 
//            = dic.tailMap("150-0000", true);
//        subdic = subdic.headMap("150-9999", true);
 
        System.out.printf("%n150 番台の郵便番号%n%s",
                          subdic);

マップの部分マップを取得するには、前述したようにheadMapメソッド、tailMapメソッド、subMapメソッドを使用します。

ここではsubMapメソッドを使用しましたが、headMapメソッドとtailMapメソッドを組み合わせても同様の結果を得ることができます。

150 番台の郵便番号
{150-0042=渋谷区宇田川町, 150-0046=渋谷区松濤}

結局、2つの番号しか登録されていなかったようです。これじゃ、辞書として使い物にならないですね。

さて、来週はNavigableMapインタフェースと同様にナビゲーションが可能なセットについて紹介します。

 

注1 映画Mary Poppinsで言及される最も長い英単語。実際にはこれより長い英単語があるらしいです。

 

著者紹介 櫻庭祐一

横河電機 ネットワーク開発センタ所属。Java in the Box 主筆

今月の櫻庭

食欲の秋、読書の秋、スポーツの秋と秋は何をするにも最適な季節です。そして、秋はイベントの季節でもあります。

櫻庭が幹事をしている日本Javaユーザグループ(JJUG)でも11月6日にクロスコミュニティカンファレンスを開催します。場所は東京国際フォーラム、参加費は無料です。

また、11月6日から8日までSun Tech Days 2007 in Tokyoが同じく東京国際フォーラムで開催されます。基調講演はJavaの父 James Gosling氏。これは聞き逃せません。

みなさまお誘いあわせの上、ぜひご参加ください。