CUBは子供の白熊

Java SE 8 実践プログラミングの練習問題を解く

第9章 Java 7 の機能を復習する : 問題 4 : 複数例外と共通のスーパークラス

問題

Java ライブラリを使用しているとき、複数例外をキャッチすることで恩恵を得られる状況に遭遇したライブラリは何か?

解答

JAXP ライブラリ(javax.xml のサブパッケージ)でありがたいと思った。

例えば、XML ファイルを読んで DOM オブジェクトを生成するケースである。

I/O エラーはともかく、XML構文解析エラー(SAXException)と滅多に起きない JAXP 環境の不備(ParserConfigurationException)の各々にコードを書くのは嫌だ。

File xmlFile = ~;
try {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder = factory.newDocumentBuilder();
    Document document = builder.parse(xmlFile);
    ...
} catch (ParserConfigurationException | SAXException ex) {
    ...
}

さらに問題

また、共通の例外となるスーパークラスにより恩恵を得られる状況に遭遇したライブラリは何か?

解答

何と言っても、Reflection を使ってメソッドを実行する際の ReflectiveOperationExceptionである。

例えば、クラス名とメソッド名が文字列で与えられたときに

  1. クラス名からデフォルト・コンストラクタでオブジェクトを生成し
  2. メソッド名から引数なしのメソッドを実行

するコードを書いてみる。

String className = ~;
String methodName = ~;

Class<?> clazz = Class.forName(className);   // ClassNotFoundException
Method method = clazz.getMethod(methodName); // NoSuchMethodException
Object object = clazz.newInstance();         // InstantiationException
                                             // IllegalAccessException
method.invoke(object);                       // InvocationTargetException
                                             // IllegalAccessException

4行のコードで何と5種類のチェック例外が起こる。
ところがReflectiveOperationExceptionは、これら5種類のチェック例外の親である。

ReflectiveOperationExceptionを使用

String className = ~;
String methodName = ~;
try {
    Class<?> clazz = Class.forName(className);
    Method method = clazz.getMethod(methodName);
    Object object = clazz.newInstance();
    method.invoke(object);
} catch (ReflectiveOperationException ex) {
    ...
}

第9章 Java 7 の機能を復習する : 問題 3 : 複数例外のキャッチ

問題

複数の例外をキャッチするcatch節の中でその例外を再度スローする場合に、その処理が書かれているメソッドでのthrows宣言はどのように宣言すればよいか?
次の例で答えよ。

public void process() throws ??? {
    try {
        ...
    } catch (FileNotFoundException | UnknownHostException ex) {
        logger.log(Level.SEVERE, "...", ex);
        throw ex;
    }
}

解答

普通に、2つの例外をthrows宣言に書けばよい。

■ 普通の解答

public void process() throws FileNotFoundException, UnknownHostException {

あと、FileNotFoundExceptionUnknownHostExceptionIOExceptionの派生クラスなので、IOExceptionでまとめることも可。

■ ひとつにまとめる

public void process() throws IOException {

第9章 Java 7 の機能を復習する : 問題 2 : 抑制された例外

問題

以下のコードを try-with-resources 文を使わず、かつcloseメソッドでスローされる例外を抑制された例外として元の例外に追加するように実装せよ。

■ try-with-resources 文を使った例

try (BufferedReader in = Files.newBufferedReader(Paths.get("/usr/share/dict/words"));
     BufferedWriter out = Files.newBufferedWriter(Paths.get("/tmp/out.txt"))
    ) {
    String line;
    while ((line = in.readLine()) != null) {
        out.write(line.toLowerCase());
        out.newLine();
    }
}

解答

IOExceptionがスローされるのは、大きく分けて以下の4つのケースである。

  • inないしoutのオープン
  • ファイルを読み書きするメインパート
  • outのクローズ
  • inのクローズ

そこで、これら4つのケースで発生した例外を覚えておくことにする。

IOException openException = null;     // リソースのオープンで発生した例外
IOException mainException = null;     // 中心となる例外
IOException inCloseException = null;  // in.close() で発生した例外
IOException outCloseException = null; // out.close() で発生した例外

try {
    BufferedReader in;
    try {
        in = Files.newBufferedReader(Paths.get("/usr/share/dict/words"));
    } catch (IOException ex) {
        openException = ex;
        throw ex;
    }
    try {
        BufferedWriter out;
        try {
            out = Files.newBufferedWriter(Paths.get("/tmp/out.txt"));
        } catch (IOException ex) {
            openException = ex;
            throw ex;
        }
        try {
            String line;
            while ((line = in.readLine()) != null) {
                out.write(line.toLowerCase());
                out.newLine();
            }
        } catch (IOException ex) {
            mainException = ex;
            throw ex;
        } finally {
            try {
                out.close();
            } catch (IOException ex) {
                outCloseException = ex;
                throw ex;
            }
        }
    } finally {
        try {
            in.close();
        } catch (IOException ex) {
            inCloseException = ex;
            throw ex;
        }
    }
} catch (IOException ex) {
    // ex は openException, mainException, inCloseException, outCloseException のいずれか
    if (openException != null) {
        // mainException, outCloseException は発生していない
        if (inCloseException != null)
            openException.addSuppressed(inCloseException);
        throw openException;
    }
    if (mainException != null) {
        if (inCloseException != null)
            mainException.addSuppressed(inCloseException);
        if (outCloseException != null)
            mainException.addSuppressed(outCloseException);
        throw mainException;
    }
    if (inCloseException != null && outCloseException != null) {
        inCloseException.addSuppressed(outCloseException);
        throw inCloseException;
    }
    throw ex;
}

なんというステップ数、可読性は最悪。
try-with-resources 文は偉大だ!

第9章 Java 7 の機能を復習する : 問題 1 : try-with-resources 文

問題

以下のコードを try-with-resources 文を使わないで実装せよ。

try (Scanner in = new Scanner(Paths.get("/usr/share/dict/words"));
     PrintWriter out = new PrintWriter("/tmp/out.txt")
    ) {
    while (in.hasNext())
        out.println(in.next().toLowerCase());
}

実装に当たっては、ScannerPrintWriter両方がオープンされた場合に両方のリソースをきちんとクローズすること。

この中には、Exceptionがスローされる個所が7つある。

  • Scannerのコンストラクタ
  • PrintWriterのコンストラクタ
  • ScannerhasNextメソッド
  • Scannernextメソッド
  • PrintWriterprintlnメソッド
  • Scannercloseメソッド
  • PrintWritercloseメソッド

ちょっと待った!

この問題は間違っている。

ScannerPrintWriterクラスは、ほとんどのメソッドでチェック例外をスローしない。
Scannerクラスは、内部に持っているReadableオブジェクトがスローする例外を、PrintWriterクラスは、内部に持っているWriterオブジェクトがスローする例外を、隠蔽している。
そこで、各クラスは隠蔽した例外を公開するためにScanner#ioExceptionPrintWriter#checkErrorメソッドを用意しているぐらいである。

■ 実際に発生する例外(イタリックは非チェック例外)

クラス メソッド 例外
Scanner コンストラクタ IOException
PrintWriter コンストラクタ FileNotFoundException
SecurityException
Scanner hasNext IllegalStateException
Scanner next NoSuchElementException
IllegalStateException
PrintWriter println なし
Scanner close なし
PrintWriter close なし

何のことはない、例外をスローするのはコンストラクタだけである。
closeメソッドが例外をスローしないんじゃ、try-with-resources 文のありがたみが薄れる。
Scanner, PrintWriterの替わりにBufferedReader, BufferedWriterを使うことにする。

■ try-with-resources 文を使った例

try (BufferedReader in = Files.newBufferedReader(Paths.get("/usr/share/dict/words"));
     BufferedWriter out = Files.newBufferedWriter(Paths.get("/tmp/out.txt"))
    ) {
    String line;
    while ((line = in.readLine()) != null) {
        out.write(line.toLowerCase());
        out.newLine();
    }
}

解答

try-with-resources 文を使わないとしたら、try-finally 句をネストすればよい。

■ try-with-resources 文を使わない

BufferedReader in = Files.newBufferedReader(Paths.get("/usr/share/dict/words"));
try {
    BufferedWriter out = Files.newBufferedWriter(Paths.get("/tmp/out.txt"));
    try {
        String line;
        while ((line = in.readLine()) != null) {
            out.write(line.toLowerCase());
            out.newLine();
        }
    } finally {
        out.close();
    }
} finally {
    in.close();
}

ただし、次の練習問題のために、抑制された例外には対応していない。

第9章 Java 7 の機能を復習する

Java 7 の変更点は、Java 5, Java 8 に比べればそんなに大きくない(InvokeDynamic は大きな変更だけど、いかんせん地味だ)。

私的には、Java 7 の最大の変更点は try-with-resources 文である。

この章は、第8章の “その他の Java 8 機能” と同じぐらいのボリュームだが、Java 7 の新機能を網羅的に解説している。

第8章 その他の Java 8 機能を理解する : 問題 16 : 正規表現の名前付きキャプチャグループ

問題

市(city)、州(state)、郵便番号(zip code)を含む行を解析するために、名前付きキャプチャグループを用いた正規表現を使用せよ。
郵便番号は、5桁と9桁(5桁ハイフン4桁)の両方を受け付けるようにせよ。

解答

本文には、市(city)と州(state)の2つをピックアップする名前付きキャプチャグループの正規表現を使っていた。

■ 市(city)と州(state)の2つだけの正規表現

(?<city>[\p{L} ]+),\s*(?<state>[A-Z]{2})

つまり、" で囲まない CSV フォーマットを想定しているんですね。

じゃ、郵便番号(zip code)を追加すると、正規表現は次のようになる。

■ 市(city)と州(state)と郵便番号(zip code)の正規表現

(?<city>[\p{L} ]+),\s*(?<state>[A-Z]{2})\s*,\s*(?<zipCode>[0-9]{5}(-[0-9]{4})?)

■ 市、州、郵便番号を含む行の解析

Path path = Paths.get("cities.txt");
Pattern pattern = Pattern.compile(
    "(?<city>[\\p{L} ]+),\\s*(?<state>[A-Z]{2})\\s*,\\s*(?<zipCode>[0-9]{5}(-[0-9]{4})?)"
);
try {
    for (String line : Files.readAllLines(path)) {
        Matcher matcher = pattern.matcher(line);
        if (matcher.matches())
            continue;
        System.out.print("City is "         + matcher.group("city"));
        System.out.print(", State is "      + matcher.group("state"));
        System.out.println(", Zip Code is " + matcher.group("zipCode"));
    }
} catch (IOException ex) {
    ex.printStackTrace();
}

第8章 その他の Java 8 機能を理解する : 問題 15 : 簡易 grep

問題

Files.linesPattern#asPredicateを使用して、与えられた正規表現に一致するすべての行を表示する grep のようなプログラムを書け。

解答

機能をいろいろ盛り込みたいところだけど、ここは簡潔に以下のような仕様とする。

  • 第1引数は正規表現の式、第2引数はファイルのパス
  • CASE_INSENSITIVEなどのオプションは用意しない
    埋め込みフラグ表現(?i)を使うこと
  • ファイルのパスにディレクトリは指定できない
  • ファイルのパスにワイルドカードは使えない
  • 単にマッチした行を標準出力に出力するだけ

■ main メソッド

public static void main(String[] args) {
    // コマンドライン引数のチェック
    if (args.length < 2) {
        System.out.println("Usage : grep <regular expression> <file path>");
        return;
    }
    // コマンドライン引数から正規表現とファイルのパスを取得
    Pattern pattern = Pattern.compile(args[0]);
    Path path = Paths.get(args[1]);
    // 検索
    try (Stream<String> stream = Files.lines(path)) {
        stream.filter(pattern.asPredicate())
            .forEach(System.out::println);
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}

実質、3ステップで出来ちゃうんですね。

不思議の国のアリスから "Wonderland" を検索してみると…

■ 実行結果

> grep  (?i)wonderland  alice.txt
Project Gutenberg's Alice's Adventures in Wonderland, by Lewis Carroll
Title: Alice's Adventures in Wonderland
*** START OF THIS PROJECT GUTENBERG EBOOK ALICE'S ADVENTURES IN WONDERLAND ***
ALICE'S ADVENTURES IN WONDERLAND
Wonderland, though she knew she had but to open them again, and all
with the dream of Wonderland of long ago: and how she would feel with
End of Project Gutenberg's Alice's Adventures in Wonderland, by Lewis Carroll
*** END OF THIS PROJECT GUTENBERG EBOOK ALICE'S ADVENTURES IN WONDERLAND ***