よんちゅBlog

― このブログは自分用のメモや日々の問題などを共有するためのものです ―

20121005191839 About | 記事一覧を見る | | 20121005185841 お知らせ: 2013/05/11 ブログデザインをリニューアルしました。

Java可変長引数メソッドの呼び出しいろいろ

Javaの可変長引数(valiableArityMethod)は、Java1.5より新たに追加された機能で、printfなどの実装に使われています。
あまり自分で実装することのない機能なので、いざ使おうとすると思わなくところではまったりします。
可変長引数を実装したメソッドが予想外の呼ばれ方をしてエラーが発生したり…

というわけで、以下可変長引数メソッドの呼ばれ方と結果を載せておく。

/**
 * 可変長引数テストメソッド
*/
public final void valiableArityMethod() {
    String[] params = { "a", "b", "c" };
    // 普通に呼び出し
    method(params);

    // 引数なし
    method();

    // 空配列
    method(new String[] {});

    // null  ※コンパイラ警告あり
    method(null);

    // 配列でキャストしたnull
    method((String[]) null);

    // 配列の型でキャストしたnull : 可変長引数に対して1個(値がnull)の引数が渡されたという意味になる
    method((String) null);
}

/**
 * 可変長引数実装メソッド
*/
private void method(final String... params) {
    // ※配列の中身を出力したい場合にparams.toString()としない <- ここ重要
    System.out.println(Arrays.toString(params));
}

実行結果は以下のようになります。

[a, b, c]
[]
[]
null
null
[null]

ここで注意しいたのは、メソッドにnullが渡される可能性があるということ。
うっかりnullチェックを忘れると実行時にエラーが発生するので注意が必要。

さらに、要素にnullが含まれる配列が渡される可能性もあるのでここも注意が必要。

※ method(null) について
可変長引数にnullを指定した場合、「呼び出しが曖昧である」という警告が発生します。

※これは「配列がnull(引数が無い)」なのか「nullが1つというデータ」なのか分からないためです。

JavaのList・Mapのfor文処理覚書

久々にJavaを使うと忘れてたりするのでメモっとく。

対象のJavaはJava6です。

Javaはバージョンによってコーティングの常識が違うので注意が必要、
特にfor文は色々な書き方が出来るので非常に紛らわしい。

Listのfor文

// 例:ArrayList
List<SomeClass> list = new ArrayList<SomeClass>();

// 拡張for文:推奨
for (SomeClass item : list) {
    // itemを処理
}

// イテレータ:拡張for文が使えない場合に使用(要素の追加や削除が必要な場合など)
for (Iterator<SomeClass> it = list.iterator(); it.hasNext(); ) {
    SomeClass item = it.next();
    // itemを処理
}

// インデックス:非推奨
for (int i = 0, size = list.size(); i < size; i++) {
    SomeClass item = list.get(i);
    // itemを処理
}

Mapのfor文

拡張for文を使用した場合

// 例:HashMap
Map<KeyClass, ValueClass> map = new HashMap<KeyClass, ValueClass>();

/*--- 拡張for文 ---*/

// 拡張for文:キーをループ
for (KeyClass key : map.keySet()) {
    // keyを処理
}

// 拡張for文:値をループ
for (ValueClass val : map.values()) {
    // valを処理
}

// 拡張for文:キーと値をループ
for (Map.Entry<KeyClass, ValueClass> entry : map.entrySet()) {
    // キーを取得
    KeyClass key = entry.getKey();
    // 値を取得
    ValueClass val = entry.getValue();
}

イテレータを使用した場合

// 例:HashMap
Map<KeyClass, ValueClass> map = new HashMap(KeyClass, ValueClass>();

/*--- イテレータ ---*/

// イテレータ:キーをループ
for (Iterator<KeyClass> it : map.keySet().iterator(); it.hasNext(); ) {
    KeyClass key = it.next();
    // keyを処理
}

// イテレータ:値をループ
for (Iterator<ValueClass val : map.values().iterator(); it.hasNext(); ) {
    ValueClass val = it.next();
    // valを処理
}

// イテレータ:キーと値をループ
for (Iterator<Map.Entry<KeyClass, ValueClass>> it = map.entrySet().iterator(); it.hasNext(); ) {
    // entryを取得
    Map.Entry<KeyClass, ValueClass> entry = it.next();
    // キーを取得
    KeyClass key = entry.getKey();
    // 値を取得
    ValueClass val = entry.getValue();
}

配列のfor文(おまけ)

// 初期化
SomeClass array[] = { new SomeClass(), new SomeClass() };

// 拡張for文:推奨
for (SomeClass item : array) {
    // itemを処理
}

// インデックス:非推奨
for (int i = 0, len = array.length(); i < size; i++) {
    SomeClass item = array[i];
    // itemを処理
}

List・Mapに関して覚えておくべき項目について

とりあえずよく遭遇する状況を選抜

  1. List・Mapの初期化方法
  2. ループ中に要素の追加・削除を行う方法
  3. List(コレクション)と配列の相互変換

Java〜サブネットマスクの表記変換(CIDR<->IPv4)〜

サブネットマスクを書くとき、「255.255.255.0」のようにIPアドレスのように書く方式と、
「/24」のようにCIDR(サイダー)と呼ばれる表記方法を用いる場合がありますよね。

今回はこのサブネットの、IPアドレス表記とCIDR表記の相互変換のコードを紹介します。

自分で書かなくても、どこかにありそうな気がするんだけど、あまり良さげなのが見つからなかったんですよね。

ここにCIDRからIP形式に変換するコードはあったけど、ちょっと力技かなと思いました。

CIDR表記 → IPアドレス表記へ

まずはCIDR表記をIPアドレス表記へ変換する場合です。

ご存知の通り、実際に使用されるサブネットマスクの多くは「/8」「/16」「/24」のいずれかだったりします。
というわけで、この3つに関しては計算せずにif文で判定しています。

そして、計算で導出する場合は、以下の手順で計算します。
まず、「2^(CIDR表記のサブネットマスク)-1」によってマスク値の10進表記を求めます。
次にこれを2進表記に変換します。
するとCIDR表記の値の数だけ”1”が並んだものになります。
(例えば「/24」→111111111111111111111111)

そしてこの値が32ビットになるように末尾に"0"を足します。
(例えば「/24」→11111111111111111111111100000000)

あとは、8ビットづつ区切って10進数に変換してドット(.)で連結すれば完成です。
(11111111.11111111.11111111.00000000 → 255.255.255.0)

/**
 * サブネット(CIDER表記)をIPv4形式に変換する。
 * 
 * @param subnet
 *            サブネット(CIDR表記) : 0〜32
 * @return 結果文字列(IPv4形式のサブネットマスク)
 */
public static String convertSubnetCIDR2IPv4(final int subnet) {
    if (subnet < 0 || subnet > 32) {
        throw new IllegalArgumentException(
            "引数(subnet)は 0〜32 までの数値でなければなりません。(" + subnet + ")");
    }
    // 典型的なサブネット形式は計算せずに導出
    if (subnet % 8 == 0) {
        if (subnet == 8) {
            return "255.0.0.0";
        } else if (subnet == 16) {
            return "255.255.0.0";
        } else if (subnet == 24) {
            return "255.255.255.0";
        }
    }

    // 汎用的なサブネット変換
    final StringBuilder subnetBinary = new StringBuilder(
        Long.toBinaryString((long) (Math.pow(2, subnet) - 1)));
    for (int i = subnetBinary.length(); i <= 32; i++) {
        subnetBinary.append("0");
    }

    final StringBuilder result = new StringBuilder();
    for (int i = 8, length = subnetBinary.length(); i <= length; i += 8) {
        result.append(Integer.parseInt(subnetBinary.substring(i - 8, i), 2))
            .append(".");
    }
    result.setLength(result.length() - 1);
    return result.toString();
}

IPアドレス表記 → CIDR表記

続いてIPアドレス表記をCIDR表記に変換する場合です。

計算方法は、CIDR表記→IPアドレス表記の逆ですね。
ただし、サブネットマスクの2進表記を求めた後は、先頭から"1"の数を数えることでCIDR表記の値を求めています。

また、ここを計算で導出する場合は、2進表記が32ビットになるように"0"を足して、
それを10進数(long)に変換し、+1します。
そして、そうやって求めた値の対数(低=2)をとると、計算によってCIDR表記の値を求めることができます。

低=2の対数を求める場合は「対数の変換公式」を用いましょう。
(忘れている方は思い出してね。難しくないので)

Javaで対数(低=10)を計算する場合は、java.lang.Math#log10(double) を使用します。

/**
 * サブネット(IPv4形式)をCIDR表記に変換する。
 * 
 * @param subnet
 *            サブネット(IPv4形式)
 * @return 結果(CIDR表記の値)
 */
public static int convertSubnetIP2CIDRv4(final String subnet) {
    if (subnet == null || subnet.isEmpty()) {
        throw new IllegalArgumentException("引数(subnet)は nullまたは空文字です。");
    }
    final String[] split = subnet.split("\\.");
    if (split.length != 4) {
        throw new IllegalArgumentException("引数(subnet)は IPv4形式ではありません。("
            + subnet + ")");
    }
    final StringBuilder sb = new StringBuilder();
    for (String numStr : split) {
        final int num = Integer.parseInt(numStr);
        if (num < 0 || num > 255) {
            throw new IllegalArgumentException(
                "引数(subnet)は IPv4形式ではありません。(" + subnet + ")");
        }
        sb.append(Integer.toBinaryString(num));
    }
    final String subnetBinary = sb.toString();
    int i = 0;
    while (subnetBinary.indexOf("1", i) != -1) {
        i++;
    }
    return i;
}
補足

コード内のメソッド名の「To」を「2」と表記していますが、これは誤記ではありませんよ。

これは慣例のようなものですね。
私はあまり好きではないので、今回のように変換前の対象(CIDRやIP)が大文字の場合にしか使いませんけど。

私の好き嫌いはともかくとして、こういう命名の仕方もあることは、覚えておいてもいいのではないでしょうか。

JavaでZipファイルを解凍(コピペ用)

Zip解凍処理などは一度作ると次に作ることはなかなかないもの。
初めて使う人や久しぶりにコードを書く人などはググってコピペすることが多いだろう。

というわけで、コピペ用のコードを残しておこうと思う。

Apache Antを使ってZipを解凍

Zipを扱う方法としては、Java標準にZipを扱うクラス群(java.util.zip)が存在するが、解凍・圧縮時共に文字コードUTF-8として扱ってしまうため、環境によっては文字化けが起きてしまう。

例えば、Windows上で圧縮したファイルまたはディレクトリの名前に日本語が含まれていた場合には正常に解凍できなかったりします。
この当たりの話は昔から有名ですね。

対策としては、Apache Ant(ant.jar) を使用する方法が一般的でしょう。
今回は2010/07/27時点で最新の1.8.1(apache-ant-1.8.1-bin.zip)を使用して確認しています。

というわけで、以下コピペ用サンプル

/**
 * zipファイルを解凍します。<br>
 * Ant1.8.1にて確認
 * 
 * @param zipFile
 *            解凍するZIPファイル
 * @param outputDir
 *            解凍先ディレクトリ
 * @param charset
 *            文字コード(ファイル名またはディレクトリ名に使用される文字コード)
 * 
 * @return 出力ディレクトリ直下に解凍されたファイルまたディレクトリのリスト
 * 
 * @throws FileNotFoundException
 * @throws ZipException
 * @throws IOException
 */
public static List<File> unZip(final File zipFile, final File outputDir,
	final String charset) throws FileNotFoundException, ZipException,
	IOException {
	if (zipFile == null) {
		throw new IllegalArgumentException("引数(zipFile)がnullです。");
	}
	if (outputDir == null) {
		throw new IllegalArgumentException("引数(outputDir)がnullです。");
	}
	if (charset == null || charset.isEmpty()) {
		throw new IllegalArgumentException("引数(charset)がnullまたは空文字です。");
	}
	if (outputDir.exists() && !outputDir.isDirectory()) {
		throw new IllegalArgumentException(
			"引数(outputDir)はディレクトリではありません。outputDir=" + outputDir);
	}

	// 出力ディレクトリ直下に解凍されたファイルまたディレクトリのセット
	final Set<File> fileSet = new HashSet<File>();

	// 解答したファイルの親ディレクトリのセット
	final Set<File> parentDirSet = new HashSet<File>();

	ZipFile zip = null;
	try {
		try {
			// 文字コードを指定することで文字化けを回避
			zip = new ZipFile(zipFile, charset);
		} catch (IOException e) {
			throw e;
		}

		final Enumeration<?> zipEnum = zip.getEntries();
		while (zipEnum.hasMoreElements()) {
			// 解凍するアイテムを取得
			final ZipEntry entry = (ZipEntry) zipEnum.nextElement();

			if (entry.isDirectory()) {
				// 解凍対象がディレクトリの場合
				final File dir = new File(outputDir, entry.getName());
				if (dir.getParentFile()
					.equals(outputDir)) {
					// 親ディレクトリが出力ディレクトリなのでfileSetに格納
					fileSet.add(dir);
				}
				// ディレクトリは自分で生成
				if (!dir.exists() && !dir.mkdirs()) {
					logger.error("ディレクトリの生成に失敗しました。dir=" + dir);
				}
			} else {
				// 解凍対象がファイルの場合
				final File file = new File(outputDir, entry.getName());
				final File parent = file.getParentFile();
				assert parent != null;

				if (parent.equals(outputDir)) {
					// 解凍ファイルの親ディレクトリが出力ディレクトリの場合
					fileSet.add(file);
				}

				if (!parentDirSet.contains(parent)) {
					// 親ディレクトリが初見の場合
					parentDirSet.add(parent);

					// 解凍ファイルの上位にある出力ディレクトリ直下のディレクトリを取得
					final File rootDir = getRootDir(outputDir, file);
					assert rootDir != null;
					fileSet.add(rootDir);

					// 親ディレクトリを生成
					if (!parent.exists() && !parent.mkdirs()) {
						logger.error("親ディレクトリの生成に失敗しました。parent=" + parent);
					}
				}

				// 解凍対象のファイルを書き出し
				FileOutputStream fos = null;
				InputStream is = null;
				try {
					fos = new FileOutputStream(file);
					is = zip.getInputStream(entry);

					byte[] buf = new byte[1024];
					int size = 0;
					while ((size = is.read(buf)) != -1) {
						fos.write(buf, 0, size);
					}
					fos.flush();
				} catch (FileNotFoundException e) {
					throw e;
				} catch (ZipException e) {
					throw e;
				} catch (IOException e) {
					throw e;
				} finally {
					if (fos != null) {
						try {
							fos.close();
						} catch (IOException e1) {
							logger.error("IOリソース開放失敗(FileOutputStream)", e1);
						}
					}
					if (is != null) {
						try {
							is.close();
						} catch (IOException e1) {
							logger.error("IOリソース開放失敗(InputStream)", e1);
						}
					}
				}
			}
		}
	} catch (FileNotFoundException e) {
		throw e;
	} catch (ZipException e) {
		throw e;
	} catch (IOException e) {
		throw e;
	} finally {
		if (zip != null) {
			try {
				zip.close();
			} catch (IOException e) {
				logger.error("IOリソース開放失敗(ZipFile)", e);
			}
		}
	}

	// Setだと何かと不便なのでListに変換
	List<File> retList = new ArrayList<File>(fileSet);
	// ソート:特に意味はなし
	Collections.sort(retList);

	return retList;
}

/**
 * zipファイルを解凍します。
 * 
 * @param zipFilePath
 *            解凍するZIPファイルのフルパス
 * @param outputDirPath
 *            解凍先ディレクトリのフルパス
 * @param charset
 *            文字コード(ファイル名またはディレクトリ名に使用される文字コード)
 * @return 出力ディレクトリ直下に解凍されたファイルまたディレクトリのリスト
 * 
 * @throws IOException
 */
public static List<File> unZip(final String zipFilePath,
	final String outputDirPath, final String charset) throws IOException {
	if (zipFilePath == null || zipFilePath.isEmpty()) {
		throw new IllegalArgumentException("引数(zipFilePath)がnullまたは空文字です。");
	}
	if (outputDirPath == null || outputDirPath.isEmpty()) {
		throw new IllegalArgumentException(
			"引数(outputDirPath)がnullまたは空文字です。");
	}
	return unZip(new File(zipFilePath), new File(outputDirPath), charset);
}


/**
 * 指定ディレクトリ直下にある、指定ファイルの親ディレクトリを再帰的に検索する。
 * 
 * 
 * @param dir
 * @param file
 * @return
 */
private static File getRootDir(final File dir, final File file) {
	assert dir != null;
	assert !dir.exists() || dir.exists() && dir.isDirectory();
	assert file != null;

	final File parent = file.getParentFile();
	if (parent == null) {
		return null;
	}
	if (parent.equals(dir)) {
		return file;
	}
	return getRootDir(dir, parent);
}
/** インポート文
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipException;

import org.apache.commons.logging.Log;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
*/

補足

少し変わった点として、戻り値として「出力ディレクトリに指定したディレクトリ"直下"に解凍されたファイルまたはディレクトリのFileオブジェクトリスト」を返すようになっています。

コード上で、何もしないにも関わらず例外を補足(catch)して再スローしている箇所がありますが、これはどこで何の例外が発生するのかソースを見て分かるようにするために、わざと書いています。

実際に使用する場合は、「FileNotFoundException」や「ZipException」、「IOException」は補足(catch)せずに、throws宣言するだけという方法でも良いでしょう。

また、throws宣言する場合も今回のケースだと別々にthrowsしてもあまり意味がないので「throws IOException」だけでも良いと思います。

今回のサンプルでは、引数のチェックやclose()時の例外情報の記録などを行っていますが、プログラミングの書籍その他Web上のサンプルでは、紙面上の都合(スペースなど)や、それが本質ではないからなどの理由により省略されていることがほんとんどです。

経験者などから見ればそれで良いのですが、初学者にとってはあまり良いことではないでしょう。

ということで、今回はその辺を考慮してサンプルとしてはかなり長めのコードとなっていますが、ご了承下さい。

Javaでテキストファイルを読み込む方法(コピペ用)

Javaでテキストファイルを読み込む方法をよく忘れるのでコピペ用にメモ

コピペ用コード:テキストファイル読み込み

/**
 * 指定ファイルをテキストファイルとして全データを読み込む。
 * 
 * @param file
 *            読み込むファイル
 * @param charset
 *            読み込むファイルの文字コード
 * @return 読み込んだ結果
 * @throws RuntimeException
 */
public final List<String> readTextFile(final File file, final String charset) {
    if (file == null) {
        throw new IllegalArgumentException("引数(file)がnullです。");
    }
    if (charset == null || charset.isEmpty()) {
        throw new IllegalArgumentException("引数(charset)がnullまたは空文字です。");
    }

    // 終了処理が必要な非メモリリソースは必ずtry/catch文の外で宣言すること
    FileInputStream is = null;
    InputStreamReader isr = null;
    BufferedReader reader = null;

    final List<String> textList = new ArrayList<String>();
    try {
        is = new FileInputStream(file);
        isr = new InputStreamReader(is, charset);
        reader = new BufferedReader(isr);

        while (reader.ready()) {
            String text = reader.readLine();
            textList.add(text);
        }
    } catch (FileNotFoundException e) {
        throw new RuntimeException("ファイルが見つかりませんでした。file=" + file, e);
    } catch (UnsupportedEncodingException e) {
        throw new RuntimeException("指定した文字コードはサポートされていません。charset=" + charset, e);
    } catch (IOException e) {
        throw new RuntimeException("ファイル読み込み中にIOエラーが発生しました。file=" + file, e);
    } finally {
        if (reader != null) {
            try {
                reader.close();
            }  catch (IOException e) {
                logger.error("IOリソース開放失敗(BufferedReader)", e);
            }
        }
        if (isr != null) {
            try {
                isr.close();
            } catch (IOException e) {
                logger.error("IOリソース開放失敗(InputStreamReader)", e);
            }
        }
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
                logger.error("IOリソース開放失敗(FileInputStream)", e);
            }
        }
    }
    return textList;
}
補足
  1. 指定する文字コードがデフォルト文字コードで良い場合は、「FileInputStream + InputStreamReader」を「FileReader」に変更しても良い。
  2. 例外をRuntimeExceptionに変更している(例外翻訳している)が、実際に使用する場合は共通の例外などに書き換える。
  3. 各ストリームをcloseした場合の例外処理にて例外ログを記録しているが、必須ではないがあった方が良い