Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

C 言語スタイルのフォーマット Scanner

 
 
Tiger scanf も忘れないで
 
 

scanf は printf に比べれば使用頻度は低いと思いますし、玄人ほど使わないとは思います。でも、それなりに便利なのは確かです。

Java で scanf のようなことをやろうとすれば DataInputStream クラスや StreamTokenizer を使用するのが今までのやり方でした。それはそれでもいいのですが、もうちょっと使いやすいといいですね。

特に正規表現といっしょに使えたりするともっといいのではないでしょうか。

そこで、Tiger で登場したのが java.util.Scanner クラスです。

大量の入力をさばくような用途には向かないかもしれませんが、ちょっとした入力には重宝します。

 

 
 
Tiger さっそく使おう
 
 

Scanner クラスはデフォルトでは StreamTokenizer クラスでデリミタを " " (スペースもしくはタブ) にしたときと同じような使い方になります。

サンプルのソース ScannerTest1.java

Scanner クラスの生成にはいろいろあるのですが、ここでは一番単純な文字列を引数にして生成します。

import java.util.Scanner;
 
public class ScannerTest1 {
    public static void main(String[] args) {
        String text = "Peter Piper picked a peck of pickled pepper";
 
        Scanner scanner = new Scanner(text);
        
        while (scanner.hasNext()) {
            System.out.println(scanner.next());
        }
    }
}

hasNext メソッドでトークンがあるか調べて、next メソッドで取りだすという手順は StreamTokenizer とメソッド名こそ違いますが、ほぼ同じです (Iterator インタフェースのメソッドですね)。

実行してみると

C:\examples>java ScannerTest1
Peter
Piper
picked
a
peck
of
pickled
pepper
 
C:\examples>

ここでは Scanner オブジェクトの生成に、引数を文字列にしました。その他には InputStream クラス、File クラスまた ReadableByteChannel も使えるのがうれしいところです。

また、Formatter クラスでは Appendable インタフェースでしたが、Scanner クラスは Readable インタフェースを引数にしようできます。Reader クラスが Readable インタフェースをインプリメントしているので、XXXReader であればすべて Scanner クラスのコンストラクタの引数に使用することが可能です。

next メソッドの戻り値は文字列なのですが、その他に nextInf メソッドなどプリミティブ型に対応したメソッドが用意されています。同様に hasNext メソッド以外に、hasNextInt メソッドなども用意されています。

これらのメソッドを使用すれば文字列から数値への変換が必要なくなるので、便利です。まずは試してみましょう。

サンプルのソース ScannerTest2.java

スキャンする文字列は One little, 2 little, 3 little Indian です。わざと数字と文字を混ぜて書いてみました。

import java.util.Scanner;
 
public class ScannerTest2 {
    public static void main(String[] args) {
        String text = "One little, 2 little, 3 little Indian";
        Scanner scanner = new Scanner(text);
 
        while (scanner.hasNextInt()) {
            System.out.println(scanner.nextInt());
        }
    }
}

これで 2 と 3 が出力されるはずです。やってみましょう。

C:\examples>java ScannerTest2
 
C:\examples>

あれっ、何も出力されていません。おかしいですね。

実をいうと、hasNextInt メソッドも nextInt メソッドも文字列のトークンを飛ばしていくわけではないのです。はじめのトークンが One なので hasNextInt メソッドは false になります。ですからそのままループを抜けてしまうのでした。

そうと分かればダミーの hasNext メソッドや next メソッドを使用してやればいいことになります。

サンプルのソース ScannerTest2_1.java

while ループの中に if 文で hasNextInt メソッド、hasNext メソッドを書いてみました。hasNext メソッドの後に next メソッドをコールしないと、次のトークンが読み込まれないので永久ループになってしまいます。この点を注意してください。

import java.util.Scanner;
 
public class ScannerTest2_1 {
    public static void main(String[] args) {
        String text = "One little, 2 little, 3 little Indian";
        Scanner scanner = new Scanner(text);
 
        while (true) {
            if (scanner.hasNextInt()) {
                System.out.println(scanner.nextInt());
            } else if (scanner.hasNext()) {
                scanner.next();
            } else {
                break;
            }
        }
    }
}

hasNextInt メソッドを hasNext メソッドより先に書いたのは hasNext メソッドはトークンがあれば数値であろうと文字であろうとお構いなしにあれば true を返してしまうからです。

これで期待通りに動くでしょう。早速実行です。

C:\examples>java ScannerTest2_1
2
3
 
C:\examples>

どうやら意図したとおりに実行できたようです。

ここでは hasNextInt メソッドを使用しましたが、hasNext メソッドでも同じことができます。それには hasNext メソッドの引数に正規表現を使う方法です。この場合、切り出されたトークンが指定された正規表現とマッチするかどうかが hasNext メソッドの戻り値になります。文字列全体とのマッチではないです。

サンプルのソース ScannerTest2_2.java

ScannerTest2_1 では hasNextInt メソッドを使用していた部分に hasNext("\\d+") を使用しています。引数の "\\d+" は 1 つ以上の数字列ということをあらわしています。切り出されたトークンが数字だけで構成されていた場合 (すなわち正の整数だったら)、hasNext メソッドの戻り値が true になります。

import java.util.Scanner;
 
public class ScannerTest2_2 {
    public static void main(String[] args) {
        String text = "One little, 2 little, 3 little Indian";
        Scanner scanner = new Scanner(text);
         
        while (true) {
            if (scanner.hasNext("\\d+")) {
                System.out.println(scanner.next());
            } else if (scanner.hasNextInt()) {
                scanner.next();
            } else {
                break;
            }
        }
    }
}

実行結果は一緒なので省略します。ここでは hasNext の引数に文字列を使用しましたが java.util.Pattern クラスも使用できます。というかここで使った例では Pattern クラスの方がふさわしいのですが、簡単化のために文字列のままにしています。

当たり前のことですが、この "\\d+" は正の整数だけとマッチするので負の整数や、浮動小数点数だとマッチしません。あしからず。

 

 
 
Tiger デリミタの設定
 
 

前のサンプはダミーの読み込みをしており、ちょっとかっこ悪いと思いませんか。

もうちょっとスマートに、ダミーの読み込みをしない方法を考えてみましょう。

まず考えられるのが、デリミタを変えてみようということです。StringTokenizer クラスや StreamTokenizer クラスは複数の文字をデリミタにすることはできますが、正規表現であらわされたデリミタは使えません。

ところが Scanner クラスでは正規表現のデリミタを使用することができるのです。

サンプルのソース ScannerTest2_3.java

デリミタを変更するには useDelimitar メソッドを使用します。引数は文字列もしくは Pattern クラスです。

import java.util.Scanner;
 
public class ScannerTest2_3 {
    public static void main(String[] args) {
        String text = "One little, 2 little, 3 little Indian";
        Scanner scanner = new Scanner(text);
        scanner.useDelimiter("\\D+");
 
        while (scanner.hasNext()) {
            System.out.println(scanner.nextInt());
        }
    }
}

このサンプルでデリミタとして使用した \D は数字以外の文字をあらわすので、数字以外の文字が 1 つ以上つながっているものをデリミタとしています。ようするに数字以外の文字のつながりがデリミタになります。そのため、小数点もデリミタになってしまうので、浮動小数点数は 2 つのトークンとみなされてしまいます。

複数の文字をデリミタに使用することも可能です。たとえばスペースとタブとカンマをデリミタに使いたいのであれば、"[ \t,]" と書くことができます。

 

 
 
Tiger scanf 的な使い方
 
 

今までの書き方は scanf とはちょっと違っていましたが、最後に scanf 的な書き方をやってみましょう。これも正規表現を使用する方法なのですが、行全体と正規表現をマッチさせる方法です。1 行のマッチが findInLine メソッド、複数行の場合が findWithinHorizon メソッドです。

サンプルのソース ScannerTest2_4.java

このサンプルでは findInLine メソッドを使用します。

import java.util.Scanner;
import java.util.regex.MatchResult;
 
public class ScannerTest2_4 {
    public static void main(String[] args) {
        String text = "One little, 2 little, 3 little Indian";
 
        Scanner scanner = new Scanner(text);
 
        scanner.findInLine("One little, (\\d+) little, (\\d+) little Indian");
        MatchResult result = scanner.match();
         
        for (int i = 1; i <= result.groupCount(); i++) {
            System.out.println(result.group(i));
        }
    }
}

findInLine メソッドの引数がマッチのために使用される正規表現です。マッチするとときにデリミタは無視されます。findInLine メソッドの戻り値はマッチした文字列なので、このままでは text とまったく同じものが得られます。

そこで、グループを取りだすために match メソッドを使用して正規表現のマッチの結果を取得します。この戻り値は java.util.regex.MatchResult インタフェースのオブジェクトなのですが、このインタフェースも Tiger で導入されたものです。

java.util.regex.MatchResult クラスの インデックスを引数とした group メソッドを使用すればマッチしたグループを取得することができます。しかし、あくまでも正規表現のグループなので戻り値が文字列になってしまうところが残念なところです。

また、group メソッドの引数を 0 にしてしまうと引数なしの group メソッドと同じになってしまいます。つまりここでは文全体が戻ってきてしまうので、引数は 1 からにする必要があります。

 

 
 
Tiger おわりに
 
 

Scanner クラスはいうなれば StreamTokenizer + DataInputStream + 正規表現 のようなクラスですね。正規表現が使えるのであれば、強力な武器になりそうです。

ただし、scanf と同じことがいえるのですが、数字でない入力を nextInt メソッドで読み込んでしまったら InputMissmatchException 例外が発生してしまいます。この例外は RuntimeException なので、コンパイル時には間違いを発見することができません。

したがって、使い方を注意しないとコードの品質を落としかねないのです。便利なクラスには落とし穴があるので、気をつけましょう。

 

今回使用したサンプルはここからダウンロードできます。

 

(Jun. 2004)

 
 
Go to Contents Go to Java Page