カテゴリー「Java」の記事

Java:Reader/Writerにおけるclose()メソッド呼び出しの流儀

【課題】
今回の記事では,次の2点について確認します。

●確認1
テキストファイルを読み込むためのクラスインスタンスを以下の手順で作成してファイル読み込みを行ったとします

  File infile = new File("入力ファイルのパス");
 
  FileInputStream is = new FileInputStream(infile);
  InputStreamReader sr = new InputStreamReader(is,"utf-8");
  BufferedReader r = new BufferedReader(sr);
上記で作成したBufferedReaderでテキストを読み込み終わったあと,作成した3つのクラスインスタンス
・BufferedReader
・InputStreamReader
・FileInputStream
これ全部に対してcloseを実行する必要があるか,それとも最後に作成したBufferedReaderだけをcloseすれば大丈夫か。

ちなみにBufferedReaderのAPIリファレンスに使用例として以下のコードが掲載されています。

BufferedReader in = new BufferedReader(new FileReader("foo.in"));
この例の通りに記述すると,FileReaderへの参照がないためFileReaderをcloseできません。
それで問題ないのか確認します。
https://docs.oracle.com/javase/jp/6/api/java/io/BufferedReader.html

●確認2
Writerの派生クラスのインスタンスからデータを書き出した後,すべてのデータを書き出すためにflushが実行される必要がありますが,そのためにflushメソッドを明示的に呼び出すべきかどうか確認します。

●検証用テストコード
確認1と2の両方とも以下のコードで確認します。

public class FileIOTest
{
  public static void main(String[] args)
  {
    try {
      File infile = new File("入力ファイルのパス");
      File outfile = new File("出力ファイルのパス");
 
      FileInputStream is = new FileInputStream(infile);
      InputStreamReader sr = new InputStreamReader(is);
      BufferedReader r = new BufferedReader(sr);
 
      FileOutputStream os = new FileOutputStream(outfile);
      OutputStreamWriter sw = new OutputStreamWriter(os);
      BufferedWriter w = new BufferedWriter(sw);
 
      try {
        String line;
        while ((line = r.readLine()) != null) {
          w.write(line);
          w.newLine();
        }
      }
      finally {
        //try { r.read(); } catch(IOException ex) { ex.printStackTrace(); }
        try { r.close(); } catch(IOException ex) { ex.printStackTrace(); }
        //try { sr.read(); } catch(IOException ex) { ex.printStackTrace(); }
        try { sr.close(); } catch(IOException ex) { ex.printStackTrace(); }
        //try { is.read(); } catch(IOException ex) { ex.printStackTrace(); }
        try { is.close(); } catch(IOException ex) { ex.printStackTrace(); }
 
        //try { w.flush(); } catch(IOException ex) { ex.printStackTrace(); }
        try { w.close(); } catch(IOException ex) { ex.printStackTrace(); }
        //try { sw.flush(); } catch(IOException ex) { ex.printStackTrace(); }
        try { sw.close(); } catch(IOException ex) { ex.printStackTrace(); }
        //try { os.flush(); } catch(IOException ex) { ex.printStackTrace(); }
        try { os.close(); } catch(IOException ex) { ex.printStackTrace(); }
      }
    }
    catch (IOException ex) {
      ex.printStackTrace();
    }
  }
}
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
テストコードのうち,finallyブロックの前段6行がReaderクラスメソッドの検証用,後段6行がWriterクラスメソッドの検証用コードになります。
finallyブロックの「read」メソッドは,これを呼んでIOExceptionが起きるかどうかで各インスタンスがまだ読み込み可能か確認するために呼んでいます。

●確認1:検証
テストコードのfinallyブロックのうち,前段6行のコメントをはずして実行してみます。
コメントをはずしたfinallyブロック前段:

  try { r.read(); } catch(IOException ex) { ex.printStackTrace(); }
  try { r.close(); } catch(IOException ex) { ex.printStackTrace(); }
  try { sr.read(); } catch(IOException ex) { ex.printStackTrace(); }
  try { sr.close(); } catch(IOException ex) { ex.printStackTrace(); }
  try { is.read(); } catch(IOException ex) { ex.printStackTrace(); }
  try { is.close(); } catch(IOException ex) { ex.printStackTrace(); }

上記のように修正して実行すると,以下の2つの例外が起こります。
java.io.IOException: Stream closed
  at sun.nio.cs.StreamDecoder.ensureOpen(StreamDecoder.java:27)
  at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:128)
  at sun.nio.cs.StreamDecoder.read0(StreamDecoder.java:107)
  at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:93)
  at java.io.InputStreamReader.read(InputStreamReader.java:151)
  at ymnk.FileIOTest.main(FileIOTest.java:38)
java.io.IOException: Bad file descriptor
  at java.io.FileInputStream.read(Native Method)
  at ymnk.FileIOTest.main(FileIOTest.java:40)

1つ目の例外は,finallyブロック3行目の「sr.read();」を実行して「すでにcloseされている」という例外が起きています。
「r.close();」を実行することでsrもcloseされることがわかります。
2つ目の例外は,finallyブロック5行目の「is.read();」を実行して「間違ったファイル記述子」という例外が起きています。
エラーの意味がイマイチよくわかりませんが,「r.close();」「sr.close();」をコメントにすると例外が起きなくなるので,原因は先に実行された「r.close();」メソッドの実行かと思われます。
「sr.read();」と「sr.close();」をコメントにしても例外が起きるので,「r.close();」を実行すると,以降のreadメソッド呼び出しはすべて例外になるようです。
ちなみにcloseメソッドでは例外は起きません。これについて各APIリファレンスによると「InputStreamReader」と「BufferedReader」のcloseメソッドの説明では「すでに閉じられているストリームを閉じても、何の影響もありません」となっています。
※「FileInputStream」のcloseメソッドの説明には多重closeについての記述はありません。
●確認1:結論
ファイル入力について,
BufferedReader r = new BufferedReader(
            new InputStreamReader(
            new FileInputStream(new File("入力ファイルパス"))));
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
このコードでBufferedReaderインスタンスを生成して読み込みを行い「r.close()」で閉じた場合,BufferedReaderだけでなくInputStreamReaderもFileInputStreamも閉じられます。BufferedReaderだけをcloseすれば大丈夫です。

●確認2:検証
テストコードのfinallyブロックのうち,前段6行を元に戻して後段6行のコメントをはずして実行してみます。
コメントをはずしたfinallyブロック後段:

  try { w.flush(); } catch(IOException ex) { ex.printStackTrace(); }
  try { w.close(); } catch(IOException ex) { ex.printStackTrace(); }
  try { sw.flush(); } catch(IOException ex) { ex.printStackTrace(); }
  try { sw.close(); } catch(IOException ex) { ex.printStackTrace(); }
  try { os.flush(); } catch(IOException ex) { ex.printStackTrace(); }
  try { os.close(); } catch(IOException ex) { ex.printStackTrace(); }

これで実行すると,以下の例外が起こります。
java.io.IOException: Stream closed
  at sun.nio.cs.StreamEncoder.ensureOpen(StreamEncoder.java:26)
  at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:121)
  at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:212)
  at ymnk.FileIOTest.main(FileIOTest.java:45)

finallyブロック後段3行目の「sw.flush();」を実行して「すでにcloseされている」という例外が起きています。closeは2行目の「w.close();」で行われており,closeしたストリームをflushすると例外になることがわかります。
これについてOutputStreamWriterのAPIリファレンスを調べてみると,closeメソッドを実行した後は,write及びflushメソッドの呼び出しでIOException例外が発生する旨の記述があります。
同時に,closeが呼ばれたら,まずflushが行われること,closeしたストリームをcloseしても問題ない旨の記述があります。
https://docs.oracle.com/javase/jp/6/api/java/io/Writer.html
●確認2:結論
Writerの派生クラスは出力が完了したあとで明示的にflushを呼ばなくてもcloseでflushが行われます。
また,closeを実行してからflushを呼ぶと例外が起きてしまいます。
flushを呼んで例外が起きた場合,その例外を確実にcatchしないと以降のclose処理が行われないため,closeを呼ぶべき場所でflushも呼ぶのは逆に危険です。

【まとめ】
どちらの確認でも結論は同じでした。
「入出力の終了時はcloseを1回呼べばOK」

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

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



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


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

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)

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)

Java:全角の数値文字列を数値として受け付ける

【課題】
ユーザが入力を行う場合,数値は文字列として入力フィールドに入力され,この文字列は内部で処理するためにどこかで数値に変換する処理が必要になるかと思います。
今回は,数値文字列をBigDecimalに変換する処理をJavaで作成してみます。
この変換には以下の2つの処理も含めることにします。
1)全角の数値文字列もBigDecimalに変換できるようにしてみます。
2)ユーザが数値入力するとき,慣習的に3桁ごとにカンマを入れて入力する場合がありますが,数値に変換する前にこれを取り除くことでカンマの入力を許容します。


【概要】
●入力文字列に対する事前処理
BigDecimalのコンストラクタは,文字列を受け取ると,これを数値として解釈しようとします。
コンストラクタに渡される数値文字列について,数字以外は以下のように解釈します。そのためユーザプログラムは以下の事前処理を行う必要はありません。
・文字列の先頭が「-」の場合は負の値として読み込む。「+」の場合は「+」を読み飛ばす。
・文字列の先頭(先頭が「+」「-」のときは次の文字)がピリオドの場合は前にゼロを補完する。
・文字列の最後がピリオド(小数点)で終わっていたら,小数点を無視する。

以上より,BigDecimalのコンストラクタに渡す入力文字列には,上記で行われない以下の事前処理が必要になります。
・半角及び全角の数字を半角に揃える。
・半角及び全角のマイナス,プラス,ピリオドを半角に揃える。
・半角及び全角のカンマは読み飛ばす。
・それ以外の文字が出現したら,NumberFormatException例外にする。

●半角及び全角の数字の扱い
文字が数字かどうかの判定は,CharacterクラスのisDigit()メソッドを使用します。
このメソッドは半角及び全角の数字を与えるとtrueを返します。
数字の数値への変換は,Characterクラスのdigit()メソッドを使用します。
このメソッドは半角及び全角の数字を与えると,これを数値に変換します。
この2つのメソッドにより,全角半角の区別なく数字を数値に置き換えることができます。

【実装】
概要から,具体的なコードは以下のようになります。

●事前処理

  //-----------------------------------------------------------------------
  private static final char CHAR_ZEN_MINUS = 0xFF0D;
  private static final char CHAR_ZEN_PLUS = 0xFF0B;
  private static final char CHAR_ZEN_PERIOD = 0xFF0E;
  private static final char CHAR_ZEN_COMMA = 0xFF0C;
  //-----------------------------------------------------------------------
  /**
   * 事前処理:数値を表現する全角文字を半角に置き換える。カンマは読み飛ばす.
   * @param numStr 入力文字列.
   * @return 置き換えを行った入力文字列.
   * @throws NumberFormatException 数値を表現する文字以外の文字が含まれていた.
   */
  public static String buildNumString (
    String numStr)
    throws NumberFormatException
  {
    String result = "0";
    if (numStr != null && !numStr.isEmpty()) {
      StringBuilder buff = new StringBuilder();
      char[] chars = numStr.toCharArray();
      for (char numChar : chars) {
        if (Character.isDigit(numChar)) {
          buff.append(Character.digit(numChar, 10));
        }
        else if (numChar == '-' || numChar == CHAR_ZEN_MINUS) {
          buff.append('-');
        }
        else if (numChar == '+' || numChar == CHAR_ZEN_PLUS) {
          buff.append('+');
        }
        else if (numChar == '.' || numChar == CHAR_ZEN_PERIOD) {
          buff.append('.');
        }
        else if (numChar != ',' && numChar != CHAR_ZEN_COMMA) {
          throw new NumberFormatException("数値表現に誤りがあります:"+numStr);
        }
      }
      result = buff.toString();
    }
    return result;
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

●BigDecimalへの変換
事前処理の結果をコンストラクタに渡してBigDecimalのインスタンスを作ります。

import java.math.BigDecimal;
  //-----------------------------------------------------------------------
  /**
   * 入力文字列を事前処理して,その結果からBigDecimalを作成.
   * @param args 入力文字列.
   */
  public static void main(String[] args)
  {
    try {
      String numStr = args[0];
      String str = buildNumString(numStr);
      BigDecimal dec = new BigDecimal(str);
      System.out.println(dec.toString());
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
【著作権表記】上記コードを含む本ブログのプログラムコードは,私的利用可,商用利用可,改変しての利用可です。利用の際に作者に許諾を得る必要はありません。

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



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


ioPLAZA【DVDミレル】

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

Java:JPEGファイルから画像サイズのみ読み込む

【課題】
今回は,JPEGファイルから画像サイズ(縦×横ピクセル値)の情報だけを取得してみます。

【概要】
Javaのクラスを使って画像サイズを取得する方法として,ImageBufferクラスインスタンスにJPEGデータを読み込むと,サイズ取得メソッドで値を取り出すことができます。
この方法は,画像データを全部読み込んでのサイズ取得になるので,サイズ取得のみが目的の場合はメモリと処理時間を無駄に費やしてしまいます。
今回はローレベルクラスを使ってJPEGファイルから必要な情報のみ読み取ります。

●JPEGファイルの画像サイズの格納場所
まずはJPEGファイルの中身について。JPEGファイルはJFIFというフォーマットに従って作られています。JPEGのデータは複数のセグメントに分かれており,1つのセグメントは基本的に次のような構成になっています。

 サイズ
(1)マーカー2byte
(2)セグメントサイズ値2byte
(3)データ(2)の値-2byte
マーカーはそのセグメントを識別する値で,各マーカーの値には対応するマーカー名が存在します。
セグメントサイズに格納される値は,自分自身とデータサイズの合計です。マーカー値のサイズは含みません。
そのため(2)セグメントサイズの値は(3)データの実サイズより+2byte大きい値になります。
この構成の例外として,JPEGデータの最初のセグメント「SOI(0xFFD8)」はマーカーのみです。マーカーのみなので,SOIセグメントのサイズは2バイトです。
画像サイズが入っているセグメントのマーカー名はSOFです。SOFはSOF0(0xFFC0)からSOF15(0xFFCF)まであるようですが,サイズ値の格納場所は変わらないようです。
SOFで画像サイズが格納されている場所は以下になります。下記の「縦ピクセル数」と「横ピクセル数」が目的の値です。

 サイズ
(1)マーカー:SOF (SOF0〜SOF15のいずれか)2byte
(2)セグメントサイズ値2byte
(3)データ(1)(詳細不明)1byte
(2)縦ピクセル数2byte
(3)横ピクセル数2byte
(以下略)

●処理の概要
以上から,サイズ取得の処理は以下のようになります。
(1)SOIをスキップ
(2)マーカがSOF0〜SOF15のいずれかのセグメントを探す。
(2-1)マーカを参照してSOFではないセグメントだったら,そのセグメントをスキップして次のセグメントに移る。
(2-2)セグメントをスキップするときのサイズは,そのセグメントのセグメントサイズ値から決定する。
(3)マーカSOF0〜SOF15が見つかったら,セグメントサイズの2バイトとデータ部の先頭1バイトをスキップして,2バイトづつデータを取得し整数化する。

●使用するクラス
JPEGファイルを読み込むために今回使用するJavaのクラスは「FileImageInputStream」です。
ファイルのアクセスは,このクラスの以下のメソッドを使用します。
(1)readUnsignedShort() …… 2バイト読み込んで値をintで返す。ストリーム位置は2進む。
(2)skipBytes() ………… データを読み込まずにストリーム位置のみ移動する。
(3)readShort() ………… 2バイト読み込んで,値をshortで返す。ストリーム位置は2進む。

●実装
以上より,実際のコードは以下のようになります。
(2015/06/28改訂)

import java.awt.Dimension;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
 
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
 
public class JPEGImagesize
{
  //----------------------------------------------------------------------------------
  private static final int SOI  = 0xFFD8;
  private static final int SOF0 = 0xFFC0;
  private static final int SOF15 = 0xFFCF;
  //----------------------------------------------------------------------------------
  /**
   * JPEG画像データの縦横サイズ取得.
   * @param file JPEGファイル.
   * @return ファイルサイズ. 幅,高さの順のintの配列.
   * @throws IOException
   */
  private static Dimension getSize (
    File file)
    throws IOException
  {
    int width = 0, height = 0;
    ImageInputStream stream = null;
    try {
      stream = new FileImageInputStream(file);
 
      if (stream.readUnsignedShort() == SOI) {
        int size, kind;
        while ((kind = stream.readUnsignedShort()) < SOF0 || SOF15 < kind) {
          size = stream.readUnsignedShort() - 2;
          if (stream.skipBytes(size) < size) { break; }
        }
        if (SOF0 <= kind && kind <= SOF15) {
          size = stream.readUnsignedShort();
          stream.skipBytes(1);
          height = stream.readShort();
          width = stream.readShort();
        }
      }
    }
    catch (EOFException ex) {
      width = 0; height = 0;
    }
    finally {
      try {
        if (stream != null) { stream.close(); }
      } catch (IOException ex) { ex.printStackTrace(); }
    }
    return new Dimension(width, height);
  }
  //----------------------------------------------------------------------------------
  /**
   * (検証用)JPEGの縦横サイズを取得して表示する.
   * @param args args[0]に画像ファイルのパスが指定されている想定.
   */
  public static void main(String[] args)
  {
    try {
      if (0 < args.length) {
        File file = new File(args[0]);
        if (file.exists()) {
          Dimension size = JPEGImagesize.getSize(file);
          System.out.println("width :"+String.valueOf(size.width));
          System.out.println("height:"+String.valueOf(size.height));
        }
      }
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }
  }
}
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
【著作権表記】上記コードを含む本ブログのプログラムコードは,私的利用可,商用利用可,改変しての利用可です。利用の際に作者に許諾を得る必要はありません。

■関連書籍をAmazonで検索:[Java]
現場で使えるJavaライブラリ(Kindle版)



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


ノートンシリーズ新登場バナー

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

Java:文字コード変換ツールを作る(3)コマンドラインツール その2

今回は,前回の記事で作成したコマンドラインツールについて,文字コード変換の機能はそのままで入出力の仕様が異なるツールを作成してみます。
文字コード変換機能は前々回の記事のものをそのまま使います。
今回作成するツールの仕様は以下になります。

【仕様】
1. コマンドライン引数にファイルまたはディレクトリのパスを指定することができる。
1.1. 引数にファイルが指定されたときは,そのファイルを変換処理する。
1.2. 引数にディレクトリが指定されたときは,そのディレクトリに含まれるファイルを一括変換する。
1.3. 引数が指定されていない場合は,カレントディレクトリに含まれるファイルを一括変換する。
2. 出力ファイルを保存する場所として,入力ファイルと同じディレクトリに「utf8」というディレクトリを新規作成する。
2.1 すでに「utf8」というファイルまたはディレクトリが存在した場合はなにもせず終了。
3. 出力ファイルのファイル名は入力ファイル名と同じ。

※前回のツールとは2.以降が異なります。

【実装】
以上の仕様を実現するために,必要な機能を実装していきます。

■1入力ファイルに対して1出力ファイルを生成する。
入力が1ファイルの場合とディレクトリの場合に対応するために,入力ファイル1つに対する変換処理をまとめたメソッドを作ります。このメソッドは,前回作成した「convertOneFile」から出力ファイル名を生成するコードを省いたもので,前回同様入力ファイル1つに対して出力ファイル1つを生成します。

※下記コードの「ConvtoUTF8.convertFile(infile, outfile);」は前々回作成したコード変換プログラムです。

import java.io.File;
import java.io.IOException;

public class ConvtoUTF8
{
  //----------------------------------------------------------------------
  /**
   * 1入力ファイルのコード変換処理を行う。
   * @param outdir 出力ファイルを保存するディレクトリ。
   * @param infile 入力ファイル。
   */
  private static void convertOneFile (
    File  outdir,
    File  infile)
    throws IOException
  {
    if (infile.canRead()) {
      File outfile = new File(outdir, infile.getName());
      if (!outfile.exists()) {
        ConvtoUTF8.convertFile(infile, outfile);
        System.out.println(infile.getName());
      }
    }
  }
}

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

■上記コードを起動するmain関数。

main関数の引数(args)からコマンドライン引数を取得するルールは,前回と同じくargs[0]を処理対象のファイルまたはディレクトリパスとして取得します。
また,引数が指定されておらずargsが空の場合はカレントディレクトリが処理対象です。
ここでは処理する対象を特定してconvertOneFileメソッドを実行するのは前回と同じですが,更に,事前に出力先ディレクトリを作成して出力先をconvertOneFileに渡します。
例外のcatchは前回同様main関数でのみ行います。これにより処理中に例外が発生した場合は,それ以降の処理をすべてキャンセルしてmain関数のcatchに飛んできます。

  //----------------------------------------------------------------------
  private static final String EXTENSION_NAME = "utf8";
  //----------------------------------------------------------------------
  /**
   * @param args コマンドライン引数。ファイルまたはディレクトリパス指定として解釈する。
   */
  public static void main(String[] args)
  {
    try {
      String pathStr = (0 < args.length)? args[0]: ".";
      File path = new File(pathStr);
      if (path.isDirectory()) {
        File outdir = new File(path, EXTENSION_NAME);
        if (!outdir.exists() && outdir.mkdir()) {
          File[] infiles = path.listFiles();
          for (File file : infiles) {
            if (!file.canRead()) { continue; }
            ConvtoUTF8.convertOneFile(outdir, file);
          }
        }
      }
      else if (path.canRead()) {
        File parent = path.getParentFile();
        File outdir = new File(parent, EXTENSION_NAME);
        if (!outdir.exists() && outdir.mkdir()) {
          ConvtoUTF8.convertOneFile(outdir, path);
        }
      }
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

以上の処理と前々回のコード変換を1つのクラス(「ConvtoUTF8」クラス)にまとめると,変換ツールが完成します。


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

■関連書籍をAmazonで検索:[Java]
Java言語プログラミングレッスン 第3版(上) Java言語を始めよう(kindle版)



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


高速・夜行バス予約サイト WILLER TRAVEL高速・夜行バス予約サイト WILLER TRAVEL

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

Java:文字コード変換ツールを作る(2)コマンドラインツール その1

今回は,前回の記事で作成した文字コード変換プログラムに機能追加してコマンドラインツールに仕立てます。今回作成するツールの仕様は以下になります。

【仕様】
1. コマンドライン引数にファイルまたはディレクトリのパスを指定することができる。
1.1. 引数にファイルが指定されたときは,そのファイルを変換処理する。
1.2. 引数にディレクトリが指定されたときは,そのディレクトリに含まれるファイルを一括変換する。
1.3. 引数が指定されていない場合は,カレントディレクトリに含まれるファイルを一括変換する。
2. 出力ファイルは入力ファイルと同じディレクトリに作成する。
3. 出力ファイルのファイル名は,入力ファイル名の拡張子の前に拡張子「utf8」を追加する。
  例:「XXX.txt」→「XXX.utf8.txt」
3.1. ファイル名に拡張子がない場合は,拡張子を「utf8」にする。

【実装】
以上の仕様を実現するために,必要な機能を実装していきます。

■出力ファイルのファイル名を生成する。

入力ファイルのファイル名から出力ファイルのファイル名を生成します。
処理内容は,入力ファイルのファイル名をピリオドで分割してからピリオドで連結し直します。このとき最後の連結の前に「utf8」を挿入します。

import java.io.File;
import java.util.regex.Pattern;

public class ConvtoUTF8
{
  //----------------------------------------------------------------------
  private static final String EXTENSION_NAME = "utf8";
  private static final Pattern PAT_SEPA_PERIOD = Pattern.compile("[.]");
  //----------------------------------------------------------------------
  /**
   * 拡張子を「.XX」から「.utf8.XX」に変更したファイル名を作る.
   * 拡張子がない場合は拡張子を「.utf8」にする.
   * @param file 入力ファイル.
   * @return 生成した出力ファイル名.
   */
   private static String buildOutputFileName (
     File  file)
   {
     StringBuilder buff = new StringBuilder();
     String fileName = file.getName();
     String[] sepas = PAT_SEPA_PERIOD.split(fileName);
     if (1 < sepas.length) {
       int maxlen = sepas.length - 1;
       int index = 0;
       for (; index < maxlen; index++) {
         buff.append(sepas[index]).append(".");
       }
       buff.append(EXTENSION_NAME).append(".").append(sepas[index]);
     }
     else {
       buff.append(fileName).append(".").append(EXTENSION_NAME);
     }
     return buff.toString();
   }
}

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

■1入力ファイルに対して1出力ファイルを生成する。

処理対象が,1ファイルの場合とディレクトリの場合があるので,まず入力ファイル1つに対する変換処理をまとめたメソッドを作ります。二種類の処理はこのメソッドの呼び出し方の違いで実現します。

ここでは最初に出力ファイル名を生成しますが,生成した出力ファイル名と同名の入力ファイルが存在していた場合の対応としては,単に当該ファイルの変換処理をスキップしているだけです。

※下記コードの「ConvtoUTF8.convertFile(infile, outfile);」は前回作成したコード変換プログラムです。

import java.io.IOException;

public class ConvtoUTF8
{
  //----------------------------------------------------------------------
  /**
   * 1入力ファイルのコード変換処理を行う。
   * @param parent 入力ファイルが保存されているディレクトリ。
   * @param infile 入力ファイル。
   */
   private static void convertOneFile (
     File  parent,
     File  infile)
     throws IOException
   {
     if (infile.canRead()) {
       String outFileName = ConvtoUTF8.buildOutputFileName(infile);
       File outfile = new File(parent, outFileName);
       if (!outfile.exists()) {
         ConvtoUTF8.convertFile(infile, outfile);
         System.out.println(infile.getName());
       }
     }
   }
}

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

■上記コードを起動するmain関数。

main関数の引数(args)にはコマンドライン引数が文字列の配列として格納されています。argsの最初の要素を処理対象のファイルまたはディレクトリパスとして取得します。
引数に何も指定されていない場合はargsは空の配列です。この場合はカレントディレクトリを処理対象にします。カレントディレクトリは「File dir = new File(".");」で取得することができます。

処理対象のファイルまたはディレクトリが確定したら,ファイルの場合はそのファイルに対して,ディレクトリの場合はディレクトリに含まれる各ファイルに対して,さきに作成したconvertOneFile()メソッドを実行して変換処理を行います。

例外のcatchはツール全体のうちmain関数でのみ行います。これにより処理中に例外が発生した場合は,それ以降の処理をすべてキャンセルしてmain関数のcatchを実行して処理を終了させます。

 //----------------------------------------------------------------------
 /**
  * @param args コマンドライン引数。ファイルまたはディレクトリパス指定として解釈する。
  */
 public static void main(String[] args)
 {
   try {
     String pathStr = (0 < args.length)? args[0]: ".";
     File path = new File(pathStr);
     if (path.isDirectory()) {
       File[] files = path.listFiles();
       for (File file : files) {
         if (file.canRead()) {
           ConvtoUTF8.convertOneFile(path, file);
         }
       }
     }
     else if (path.canRead()) {
       ConvtoUTF8.convertOneFile(path.getParentFile(), path);
     }
   }
   catch (Exception ex) {
     ex.printStackTrace();
   }
 }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

以上の処理と前回のコード変換を1つのクラス(「ConvtoUTF8」クラス)にまとめると,変換ツールが完成します。


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

■関連書籍をAmazonで検索:[Java]
やさしいJava 第5版 (「やさしい」シリーズ)



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


ノートンスタンダードバナー上新電機 パソコン買取サービス

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

Java:文字コード変換ツールを作る(1)文字コード変換

JavaのStringクラスは文字コード変換機能を備えています。今回はこの機能を使って,ファイルから読み込んだテキストを文字コード変換して新規テキストファイルに出力するプログラムを作ってみます。
変換する文字コードは,とりあえず「MS932」(Shift_JIS)から「UTF-8」への変換にしました。
加えて「テキストを行単位で読み込んで変換し,出力時に各行の終わりに改行を出力する」ことで,改行コードを揃える機能をもたせます。改行コードはCRLFを出力します。

■Stringクラスの文字コード変換

Stringクラスの文字列を,文字コードを指定して取り出すのは以下のようなコードになります。

String str = "文字列";
byte[] utf8bytes = str.getBytes("utf-8");
取り出した結果はbyteの配列になります。このバイト列をファイルに書き出します。

改行について,JavaにおけるCRLFの文字列表現は「"\r\n"」です。(「\」はバックスラッシュ)
これより,CRLFのバイト列は以下のように取得します。

byte[] CRLF = "\r\n".getBytes("utf-8");

改行コードは固定なので,クラス変数で定義しても良いかと思います。しかしながら,クラス変数として以下のように書くと,コンパイルエラーになってしまいます。
private static final byte[] CRLF = "\r\n".getBytes("utf-8");
エラーの内容は,「UnsupportedEncodingException」例外が起きる可能性があるのでその対応が必要,というものです。クラス変数の初期化で例外対応が必要な場合は少々工夫が必要になります。この場合の初期化コードは以下のようにして対応できます。
※例外に対する処理は,文字コード指定が固定なので特に行いません。
private static final byte[] CRLF;
static {
  try {
    CRLF = "\r\n".getBytes("utf-8");
  } catch (UnsupportedEncodingException ex) {}
}
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

■ファイルからテキスト入力

テキストファイルから文字コードを指定してテキストを読み込む処理は以下のようになります。
Javaでのテキスト入力処理としてはポピュラーなものかと思います。
ここで入力ファイルの文字コードは「MS932」を指定しています。

File infile = new File(入力テキストファイルのパス表現);
 
FileInputStream   finst = null;
InputStreamReader reader = null;
BufferedReader   in = null;
 
try {
  finst = new FileInputStream(infile);
  reader = new InputStreamReader(finst, "MS932");
  in = new BufferedReader(reader);
 
  String line;
  while ((line = in.readLine()) != null) {
    // 1行ごとの入力テキスト(=line)に対する処理をここで行う.
  }
}
finally {
  try {
    if (in != null) { in.close(); }
  } catch (IOException ex) {}
}
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
注:ここでは例外をcatchしません。例外が発生したときは,finallyで後始末をしたあとで,呼び出し元で例外をcatchすることとします。

■バイト列データをファイルに出力

変換したbyte配列をファイルに書き出す処理は以下のようになります。

File outfile = new File(出力テキストファイルのパス表現);
 
FileOutputStream   foutst = null;
BufferedOutputStream out = null;
try {
  foutst = new FileOutputStream(outfile);
  out = new BufferedOutputStream(foutst);
 
  // 出力するデータの例.
  byte[] utf8bytes = "文字列".getBytes("utf-8");
  byte[] CRLF = "\r\n".getBytes("utf-8");
 
  // データを出力する.
  out.write(utf8bytes);
  out.write(CRLF);
  out.flush();
}
finally {
  try {
    if (out != null) { out.close(); }
  } catch (IOException ex) {}
}
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
注:ここでは例外をcatchしません。例外が発生したときは,finallyで後始末をしたあとで,呼び出し元で例外をcatchすることとします。

■以上からファイル変換メソッドを作る

これまでのコードをまとめて変換プログラムを作ると,以下のようになります。
※出力バッファサイズの指定を追加しています。
こちらの記事の内容を反映しています(2015/07/05)。

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
 
public class ConvtoUTF8
{
  //----------------------------------------------------------------------
  private static final String INPUT_ENCODING = "MS932";
  private static final String OUTPUT_ENCODING = "utf-8"; 
  private static final int   OUTPUT_BUFFER_SIZE = 1024*64;
  private static final byte[] CRLF;
  static {
    try {
      CRLF = "\r\n".getBytes(OUTPUT_ENCODING);
    } catch (UnsupportedEncodingException ex) {}
  }
  //----------------------------------------------------------------------
  /**
   * 入力ファイルを読み込んで変換処理を行い,出力ファイルに書き出す.
   * 変換内容は,1)文字コードをMS932からUTF-8へ. 2)改行コードをCRLFに統一.
   * @param infile  入力ファイル.
   * @param outfile 出力ファイル.
   * @throws IOException
   */
  private static void convertFile (
    File  infile,
    File  outfile)
    throws IOException
  {
    FileInputStream finst = null;
    InputStreamReader reader = null;
    BufferedReader in = null;
 
    FileOutputStream    foutst = null;
    BufferedOutputStream out = null;
    try {
      finst = new FileInputStream(infile);
      reader = new InputStreamReader(finst, INPUT_ENCODING);
      in = new BufferedReader(reader);
 
      foutst = new FileOutputStream(outfile);
      out = new BufferedOutputStream(foutst, OUTPUT_BUFFER_SIZE);
 
      String line;
      while ((line = in.readLine()) != null) {
        out.write(line.getBytes(OUTPUT_ENCODING));
        out.write(CRLF);
      }
      out.flush();
    }
    finally {
      try {
        if (out != null) { out.close(); }
      } catch (IOException ex) {}
      try {
        if (in != null) { in.close(); }
      } catch (IOException ex) {}
    }
  }
}
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

次回の記事及び次々回の記事にて,上記コードを使って文字コード変換コマンドラインツールを作ります。


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

■関連書籍をAmazonで検索:[Java]
EFFECTIVE JAVA 第2版 (The Java Series)



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


ドミノ・ピザ【PC向けサイト】

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

Java:CSVパーサを作る(その3) - RFC4180対応 後編

前回の記事で未実装だった「レコードの確定」と「レコードのフィールドへの分割」を行うメソッドについて実装を行い,CSVパーサを完成させます。

【実装の考察】
●レコードの確定
レコード確定では,入力テキストデータに対して,ダブルクォーテーション(二重引用符)のペアをヒントに各レコードの末尾を確定して,レコードの切り分けを行います。処理手順は以下のようになります。

  1. BufferedReaderのreadLineメソッドを使ってテキストを1行分(現在の位置から改行が現れるまで,またはファイルの終了まで)取り出して,行の先頭からダブルクォーテーションを探す。見つからなければその1行を1レコードとして確定する。(readLineメソッドは「CR」「LF」「CRLF」を改行と認識するので,「CRLF以外の改行も考慮する」仕様の要求を満たしています。)
  2. ダブルクォーテーションが見つかった場合,ペアになる後ろのダブルクォーテーションを探す。後ろのダブルクォーテーションが見つかったらその位置から後続のダブルクォーテーションのペアを探す。この手順を行の終わりまで繰り返す。ダブルクォーテーションペアの外側で行が終了していれば,その行を1レコードとして確定する。
  3. ペアの後ろのダブルクォーテーションが見つからずにダブルクォーテーションペアの内側で改行に達したら,その改行を文字列フィールドに含まれる改行と見なしてBufferedReaderのreadLineメソッドより次の行を取り出して前の行と連結し,ペアの後ろのダブルクォーテーションを探すところから処理から継続する。これをダブルクォーテーションペアの外側で改行が見つかるまで繰り返す。後ろのダブルクォーテーションが見つからずにファイルの末尾に達したときは,ファイルの末尾にダブルクォーテーションを付加して行の末尾とする。この連結した行を1レコードとして確定する。
以上の処理を行うコードは次のようになります。(2009年6月22日改訂)
//------------------------------------------------------------------
/**
 * BufferedReaderから1レコード分のテキストを取り出す。
 * @param reader 行データを取り出すBufferedReader。
 * @return 1レコード分のテキスト。
 * @throws IOException 入出力エラー
 */
private String buildRecord (
  BufferedReader reader)
  throws IOException
{
  String  result = reader.readLine();
  int    pos;
  if (result != null && 0 < result.length() &&
    0 <= (pos = result.indexOf("\"")))
  {
    boolean  inString = true;
    String   rawline = result;
    String   newline = null;
    StringBuffer buff = new StringBuffer(1024);
    while (true) {
      while (0 <= (pos = rawline.indexOf("\"", ++pos))) {
        inString = !inString;
      }
      if (inString && (newline = reader.readLine()) != null) {
        buff.append(rawline);
        buff.append("\n");
        pos = -1;
        rawline = newline;
        continue;
      }
      else {
        if (inString || 0 < buff.length()) {
          buff.append(rawline);
          if (inString) {
            buff.append("\"");
          }
          result = buff.toString();
        }
        break;
      }
    }
  }
  return result;
}
注意)上記コードでは,フィールド内の改行をLF("\n")に決め打ちしていますが,実際にはCSVパーサの出力結果を受け取るプログラム(データベースなど)が要求する改行コードを挿入する必要があります。
使用すべき改行コードの選択は,プログラムが稼動するプラットフォームの改行コードを取得する
「String returnStr = System.getProperty("line.separator");」
が知られていますが,実行環境によってはCSVパーサの出力結果を受け取るプログラムがCSVパーサとは異なるOSで稼動する場合もありうるので,上記コードで取得できる改行コードを常に適用できるとは限りません。
そのため,実コードでは使用する改行コードをプロパティファイルで設定できるようにするなど動作環境に合わせて変更できる仕組みを作る必要があります。

●レコードのフィールドへの分割
フィールド分割では,レコードに切り分けたテキストに対して,最初にレコード全体をカンマで分割し,分割した個々の文字列にダブルクォーテーションをヒントに必要な連結やエスケープ処理を行って,個々のフィールドを確定します。処理手順は以下のようになります。
  1. レコード全体をStringクラスのsplitメソッドを使ってカンマで分割し,分割した個々の文字列データを順に先頭からダブルクォーテーションを探す。見つからなければその文字列は1フィールドとして確定する。
  2. ダブルクォーテーションが見つかったら,次のダブルクォーテーションを探す。次のダブルクォーテーションの直後にダブルクォーテーションがあれば,エスケープされたダブルクォーテーションとして処理し,そうでなければフィールドの終わりと見なす。
  3. フィールドで後ろのダブルクォーテーションが見つからない場合,フィールドに含まれるカンマでsplitメソッドが分割したものと見なして,フィールドの後ろに(splitメソッドが削除した)カンマと次のフィールドを連結する。
  4. フィールドの開始と終了のダブルクォーテーションは削除する。
以上の処理を行うコードは次のようになります。(2009年6月22日改訂)
//------------------------------------------------------------------
/**
 * 1レコード分のテキストを分割してフィールドの配列にする。
 * @param src 1レコード分のテキストデータ。
 * @param dest フィールドの配列の出力先。
 */
private void splitRecord (
  String    src,
  LinkedList dest)
{
  String[]  columns = src.split(",");
  int     maxlen = columns.length;
  int     startPos, endPos, columnlen;
  StringBuffer buff = new StringBuffer(1024);
  String   column;
  boolean  isInString, isEscaped;

  for (int index = 0; index < maxlen; index++) {
    column = columns[index];
    if ((endPos = column.indexOf("\"")) < 0) {
      dest.addLast(column);
    }
    else {
      isInString = (endPos == 0);
      isEscaped = false;
      columnlen = column.length();
      buff.setLength(0);
      startPos = (isInString)? 1: 0;
      while (startPos < columnlen) {
        if (0 <= (endPos = column.indexOf("\"", startPos))) {
          buff.append((startPos < endPos)?
                column.substring(startPos, endPos): isEscaped? "\"": "");
          isEscaped = !isEscaped;
          isInString = !isInString;
          startPos = ++endPos;
        }
        else {
          buff.append(column.substring(startPos));
          if (isInString && index < maxlen - 1) {
            column = columns[++index];
            columnlen = column.length();
            buff.append(",");
            startPos = 0;
          }
          else {
            break;
          }
        }
      }
      dest.addLast(buff.toString());
    }
  }
}


以上で「RFC4180対応のCSVパーサ」はひととおりできあがったかと思います。

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

■関連書籍をAmazonで検索:[Java]
ソースコードリーディングから学ぶ Javaの設計と実装



にほんブログ村 IT技術ブログへ にほんブログ村 IT技術ブログ プログラム・プログラマへ 人気ブログランキングへ ←この記事が役に立ったという方はクリックお願いします。
▼CSVパーサを作る[][][その3]
Wave SoundTouch music system IV

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