« 2015年1月 | トップページ | 2015年6月 »

2015年3月

Java:例外が起こったときに実行されるコードとされないコード

【課題】
try〜catch〜finallyのうち,finallyブロックの中で例外が起こったときの挙動を改めて確認します。
finallyブロックについて,tryブロックの中で例外が発生してcatchされなかった場合でもfinallyブロックは必ず実行されることになっています。
この挙動について,いくつか疑問が生じます。今回はその疑問について,実際に検証コードを書いてどのような動作をするのか確認してみます。
(※本記事は2015年6月6日に全面改訂しました)

●問1) finallyブロックの中で例外が発生した場合どうなるのか。
「必ず実行される」finallyブロックですが,finallyブロックの中で例外が発生した場合,例外が発生した場所以降のコードは実行されるのでしょうか?
試してみましょう。テストコードは以下になります。

public class ThrowTest
{
  //------------------------------------------------------------------
  public static void main (String[] args)
  {
    try {
      ThrowTest.thrower();
    }
    catch (Throwable ex) {
      System.out.println("main:例外をキャッチしました");
      ex.printStackTrace();
    }
  }
  //------------------------------------------------------------------
  // 例外が発生するメソッド.
  private static void thrower ()
    throws Exception
  {
    try {
      String str = null;
      str.toString(); // tryの中で例外発生.
    }
    finally {
      String str = null;
      str.toString(); // catchの中で例外発生.
      System.out.println("finally:例外発生の後");
    }
  }
}
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
実行結果
main:例外をキャッチしました
java.lang.NullPointerException
  at test.ThrowTest.thrower(ThrowTest.java:30)
  at test.ThrowTest.main(ThrowTest.java:12)
結論
「finally:例外発生の後」が出力されなかったので,finallyの中で例外が発生した場合,それ以降のコードは実行されませんでした。
考察
finallyブロックの中ですべての処理を確実に行わせるには,メソッドの呼び出しで発生する可能性があるすべての例外をfinallyブロックの中で確実にキャッチする必要があります。
一例として,ファイルの入出力でfinallyブロック内でcloseメソッドを呼ぶ場合,すべての行をtry〜catchで囲む実装が考えられます。
  Reader r = null;
  Writer w = null;
  try {
    … // ReaderからWriterへのデータ転送処理
  }
  finally {
    try { if (r != null) { r.close(); }} catch(IOException ex){ ex.printStackTrace(); }
    try { if (w != null) { w.close(); }} catch(IOException ex){ ex.printStackTrace(); }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

●問2) tryブロックとfinallyブロックの両方で例外が発生したら,catchブロックはどちらの例外を受け取るか。
試してみましょう。問1のコードのうち,例外が発生するメソッドを以下のように書き換えます。

  //------------------------------------------------------------------
  // 例外が発生するメソッド.
  private static void thrower ()
    throws Exception
  {
    try {
      throw new Exception("tryブロックで例外発生");
    }
    finally {
      throw new Exception("finallyブロックで例外発生");
    }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
実行結果
main:例外をキャッチしました
java.lang.Exception: finallyブロックで例外発生
  at test.ThrowTest.thrower(ThrowTest.java:33)
  at test.ThrowTest.main(ThrowTest.java:12)
結論
finallyブロックで例外が発生した場合,呼び出し元はfinallyの例外をcatchしました。
そしてtryブロックの例外はcatchできませんでした。
後から発生したfinallyブロックの例外が,先に発生したtryブロックの例外を上書きした形になりました。
考察
tryブロックの例外がfinallyブロックの例外に上書きされないようにするには,以下の2つの対応が考えられます。
 1) try〜finallyではなくtry〜catch〜finallyとし,tryブロックで発生した例外はcatchで受け取るようにする。
 2) 問1の考察で述べたのと同じく,finallyブロックの例外はすべてfinallyブロックの中でcatchする。
どちらを採用すべきかは,例外が発生したときに,それ以降の処理をどこまでスキップさせるかで変わってくるかと思います。
tryブロック内の処理だけをスキップするのであれば,1)を採用することになるかと思います。
tryブロック以降の処理もスキップするのであれば,2)を採用することになるかと思います。

●問3) try〜catch〜finallyの入れ子は可能なのか?
試してみましょう。問1のコードのうち,例外が発生するメソッドを以下のように書き換えます。

  //------------------------------------------------------------------
  // 例外が発生するメソッド.
  private static void thrower ()
    throws Exception
  {
    try {
      try {
        System.out.println("Start");
        throw new Exception("例外発生");
      }
      finally {
        System.out.println("内側のfinally:で実行");
      }
    }
    finally {
      System.out.println("外側のfinally:で実行");
    }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
実行結果
Start
内側のfinally:で実行
外側のfinally:で実行
main:例外をキャッチしました
java.lang.Exception: 例外発生
  at test.ThrowTest.thrower(ThrowTest.java:24)
  at test.ThrowTest.main(ThrowTest.java:9)
結論
try〜catch〜finallyの入れ子は可能でした。
考察
この書き方は,以下のケースで使えるかと思います。
 1) try〜catch〜finallyではJavaの文法としてfinallyを先に書くことはできないが,先に書いたほうがコードが理解しやすくなるケース。
 2) 問2の考察で述べた,例外のcatchを先送りにするケース。
 3) 複数のtry〜finallyを最後の1箇所でまとめてcatchするケース。

●問4) tryブロックの中でreturnしたら,finallyブロックは実行されるのか?
試してみましょう。問1のコードのうち,例外が発生するメソッドを以下のように書き換えます。今回はtry〜finallyの入れ子の中でreturnしてみました。

  //------------------------------------------------------------------
  // 今回は例外は発生しません.
  private static void thrower ()
    throws Exception
  {
    try {
      try {
        System.out.println("Start");
        return; // ここでreturn.
      }
      finally {
        System.out.println("内側のfinally:で実行");
      }
    }
    finally {
      System.out.println("外側のfinally:で実行");
    }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
実行結果
Start
内側のfinally:で実行
外側のfinally:で実行
結論
returnで終了した場合もfinallyは実行されました。
考察
処理の途中でreturnするケースとしてよくあるのが,入力値のチェックをして不合格だったら以降の処理を行わずにreturnで抜ける,というものがあるかと思います。
このケースではfinallyの処理は必要ない場合があります。
長大なメソッドを書いてしまった場合,メソッドの最後にfinallyがあるのを忘れる可能性が高いので注意が必要です。

【著作権表記】上記コードを含む本ブログのプログラムコードは,私的利用可,商用利用可,改変しての利用可です。利用の際に作者に許諾を得る必要はありません。

■関連書籍をAmazonで検索:[Java]
スッキリわかるJava入門 第2版(kindle版)

にほんブログ村 IT技術ブログへ にほんブログ村 IT技術ブログ プログラム・プログラマへ 人気ブログランキングへ ←この記事が役に立ったという方はクリックお願いします。


Wave music system IV

| | トラックバック (0)

シェルスクリプト:「.svn」ディレクトリを一括削除する

【概要】
Subversionでリビジョン管理した場合,管理下の各ディレクトリに「.svn」ディレクトリが作られます。
今回は,この「.svn」ディレクトリを一括削除するシェルスクリプトを書いてみます。
このスクリプトの用途としては,ソースコードがSubversionの管理下にあるサーブレットを本番環境にデプロイしたときに,一緒に本番環境にデプロイされてしまう「.svn」ディレクトリを後から削除するのに使えるかと思います。

【仕様】
このスクリプトの使用方法を以下のように定めます。
1)スクリプトの引数に指定したディレクトリ以下の「.svn」ディレクトリを探して,これを削除する。
2)スクリプトを引数なしで実行した場合,ディレクトリ指定はカレントディレクトリが指定されたものとする。
3)引数がディレクトリでなければ,エラーを出力して終了する。

【実装】
このスクリプトの実装は下記になります。下記スクリプトの後に,このスクリプトの詳細について解説しています。

#!/bin/bash

ARGUMENT=$1

if [ -z $ARGUMENT ] ; then
  ARGUMENT=$PWD
fi
echo "rmsvn Path: $ARGUMENT"

if [ ! -d $ARGUMENT ] ; then
  echo "rmsvn ERROR: not a directory."
  exit 1
fi

FILES=(`find $ARGUMENT -name .svn -print`)
COUNT=${#FILES[*]}

if [ $COUNT -gt 0 ]; then
  for i in `seq $COUNT`
  do
    SVNPATH=${FILES[$i-1]}

    sudo rm -rfv $SVNPATH
  done
fi

※上記コードでは,整形のため空白部分は全角スペースを使用しています。

【解説】
上記スクリプトの各部分について解説します。

ARGUMENT=$1
if [ -z $ARGUMENT ] ; then
  ARGUMENT=$PWD
fi
$1がスクリプトの引数で,これを変数ARGUMENTに代入して,以降はARGUMENTを引数として扱っています。
次のif文では,ARGUMENTが空かどうかを判定して,空であれば環境変数$PWDをARGUMENTとしています。
環境変数$PWDはカレントディレクトリのパスが設定されています。

if [ ! -d $ARGUMENT ] ; then
  echo "rmsvn ERROR: not a directory."
  exit 1
fi
このif文ではARGUMENTがディレクトリか判定して,そうでなければスクリプトを終了しています。
注)ここで条件を「-f」とすると,存在しない名前が指定されたときにエラーになりません。

FILES=(`find $ARGUMENT -name .svn -print`)
COUNT=${#FILES[*]}
コマンドをバッククオート「`」で囲むと,囲んだターミナルコマンドが実行されて,コマンドが出力した標準出力が結果になります。
これをカッコで括ることで,コマンドの結果を配列にすることができます。
ここではfindコマンドを実行して,結果を配列にして変数FILESにセットしています。
また,配列FILESの要素数をCOUNTにセットしています。

if [ $COUNT -gt 0 ]; then
 …
fi
このif文は,要素数が0以上か(削除する要素が存在するかどうか)チェックしています。

for i in `seq $COUNT`
do
  SVNPATH=${FILES[$i-1]}
 …
done
FILESから要素を1つづつ取り出して変数SVNPATHへ代入しています。
ここで,「seq」は1から引数の数値までカウントアップするコマンドですが,BSD系UNIX(Mac OS Xなど)にはこのコマンドがないため,代わりに「jot」コマンドを使います。
for i in `jot $COUNT`
do
  SVNPATH=${FILES[$i-1]}
 …
done
BSD(Mac OS X)では上記のようなコードになります。

sudo rm -rfv $SVNPATH
取り出した要素を削除します。

【著作権表記】上記コードを含む本ブログのプログラムコードは,私的利用可,商用利用可,改変しての利用可です。利用の際に作者に許諾を得る必要はありません。

■関連書籍をAmazonで検索:[シェルスクリプト]
プロフェショナル・シェルプログラミング (アスキー書籍) Kindle版

にほんブログ村 IT技術ブログへ にほんブログ村 IT技術ブログ プログラム・プログラマへ 人気ブログランキングへ ←この記事が役に立ったという方はクリックお願いします。


ボーズ・オンラインストア

| | トラックバック (0)

Java:前回作成したコードの処理速度を比較する

【課題】
前回の記事にて,自作メソッドのほかにStringクラスのformatメソッドを使って同等機能を実現する例を挙げましたが,String.formatは処理速度が遅いメソッドであるという話を聞いたので,String.formatによる実装と,前回作成したdecimalToStringメソッドとを速度比較してみました。
※両メソッドを10,000回繰り返し処理した所要時間で比較します。

比較した環境:
OS: Mac OS X 10.10.2
CPU: 2.3GHz Intel Core i5
Java: 1.6.0_65

●String.formatメソッドの処理速度

速度計測用コード:

Deque<String> que = new LinkedList<String>();
long start = System.currentTimeMillis();
for (long value = 0; value < 10000; value++) {
  que.add(String.format("%1$,3d",value));
}
System.out.println(String.valueOf(System.currentTimeMillis()-start)+" msec");
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

測定結果:
1回目:471 msec
2回目:491 msec
3回目:487 msec
4回目:497 msec
5回目:481 msec

●前回作成したdecimalToStringメソッドの処理速度
※ decimalToStringメソッドの中身は前回記事参照。

速度計測用コード:

Deque<String> que = new LinkedList<String>();
long start = System.currentTimeMillis();
for (long value = 0; value < 10000; value++) {
  BigDecimal dec = new BigDecimal(value);
  que.add(decimalToString(dec));
}
System.out.println(String.valueOf(System.currentTimeMillis()-start)+" msec");
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

測定結果:
1回目:168 msec
2回目:190 msec
3回目:189 msec
4回目:189 msec
5回目:197 msec

結果として,String.formatはdecimalToStringの倍以上の処理時間がかかる結果になりました。

【結論】
文字列を大量に処理する場所では,String.formatの使用はできれば避けたほうが良さそうです。

【著作権表記】上記コードを含む本ブログのプログラムコードは,私的利用可,商用利用可,改変しての利用可です。利用の際に作者に許諾を得る必要はありません。

■関連書籍をAmazonで検索:[Java]
増補改訂版 Java言語で学ぶデザインパターン入門(kindle版)



にほんブログ村 IT技術ブログへ にほんブログ村 IT技術ブログ プログラム・プログラマへ 人気ブログランキングへ ←この記事が役に立ったという方はクリックお願いします。


ノートンシリーズ新登場バナーソニーストアブックオフオンライン【PC・携帯共通】

| | トラックバック (0)

Java:数値を3桁ごとのカンマ区切りの文字列にする

【課題】
前回は数値文字列を数値に変換してBigDecimalを生成しましたが,今回はその逆を行います。
BigDecimalクラスの数値を3桁ごとにカンマで区切った数値文字列に変換します。
また,小数点を含む値についても対応します。
ただ,整数値の場合はStringクラスのformatメソッドを使って以下のように書くことで実現できてしまいます。
仕事でプログラミングを行っている人で,これで要求を満たすならこちらを使いましょう。

BigDecimal dec = new BigDecimal("12345678");
long value = dec.longValue();

String valueStr = String.format("%1$,3d", value);

System.out.println(valueStr);

次回記事にて上記コードと下記コードの速度比較を行っています。

【概要】
カンマの挿入は,少数点以上は下の桁から数えて3桁ごとに挿入する必要があります。
逆に小数点以下は,上の桁から3桁ごとにカンマを挿入します。
これを行うロジックはいくつか考えられますが,今回は以下の手順で行います。

(1)BigDecimalの値を文字列化して,数値文字列を小数点で区切って2つに分ける。
(2)少数点以上の数値文字列を文字配列に変換する。
(3)文字配列の最後(下の桁)から数字を1文字づつ取り出しStringBuilderに追加する。同時に3文字ごとにカンマを入れる。
※このときのStringBuilderの内容は,下の桁→上の桁の順に数字が並んでいます。
(4)負の値の場合は最後にマイナスを追加する。
(5)StringBuilderの内容をreverse()メソッドで反転させる。
※以降は小数点以下の桁があった場合の処理です。
(6)StringBuilderに小数点を挿入する。
(7)((1)で作成した)小数点以下の数値文字列を文字配列に変換する。
(8)文字配列の最初(上の桁)から数字を1文字づつ取り出しStringBuilderに追加する。同時に3文字ごとにカンマを入れる。
(9)StringBuilderの内容を文字列に変換する。

【実装】
上記手順をコードにすると以下のようになります。

import java.math.BigDecimal;

  //------------------------------------------------------------------
  /**
   * 動作確認用mainメソッド.
   */
  public static void main(String[] args)
  {
    BigDecimal dec = new BigDecimal("-12345678.9012345");
    String str = decimalToString(dec);
    System.out.println(str);
  }
  //------------------------------------------------------------------
  /**
   * BigDecimalを数値文字列に変換.
   * @param dec 変換するBigDecimal値.
   * @return 変換した文字列.
   */
  private static String decimalToString (
    BigDecimal dec)
  {
    String valueStr = String.valueOf(dec.toString());
    String[] valueStrs = valueStr.split("[.]");

    StringBuilder buff = new StringBuilder(valueStr.length());

    if (0 < valueStrs.length && !valueStrs[0].isEmpty()) {
      char[] chars = valueStrs[0].toCharArray();
      boolean isnegative = (0 < dec.scale());
      int lastindex = (isnegative)? 1: 0;
      int count = 1;
      for (int index = chars.length-1; lastindex <= index; index--) {
        buff.append(chars[index]);
        if (count++ % 3 == 0) {
          buff.append(',');
        }
      }
      if (',' == buff.charAt(buff.length()-1)) {
        buff.setLength(buff.length()-1);
      }
      if (isnegative) {
        buff.append("-");
      }
      buff.reverse();
    }

    if (1 < valueStrs.length && !valueStrs[1].isEmpty()) {
      buff.append(".");
      char[] chars = valueStrs[1].toCharArray();
      int count = 1;
      for (char c : chars) {
        buff.append(c);
        if (count++ % 3 == 0) {
          buff.append(',');
        }
      }
      if (',' == buff.charAt(buff.length()-1)) {
        buff.setLength(buff.length()-1);
      }
    }
    return buff.toString();
  }

※上記コードでは,整形のため空白部分は全角スペースを使用しています。
【著作権表記】上記コードを含む本ブログのプログラムコードは,私的利用可,商用利用可,改変しての利用可です。利用の際に作者に許諾を得る必要はありません。

■関連書籍をAmazonで検索:[Java]
増補改訂版 Java言語で学ぶデザインパターン入門(kindle版)



にほんブログ村 IT技術ブログへ にほんブログ村 IT技術ブログ プログラム・プログラマへ 人気ブログランキングへ ←この記事が役に立ったという方はクリックお願いします。


ソニーストア

| | トラックバック (0)

« 2015年1月 | トップページ | 2015年6月 »