整数を Java のバイト配列に変換する。
以下、試行錯誤した内容を順を追って書く。
急いで結論が見たい人は、ここへジャンプしてください。
■整数をバイト列(シーケンス)へ変換
これはそれ程難しくない。方法はいろいろあると思いますがここでは末尾再帰で。
(defn byte-split [n] (loop [n n, coll []] (if (zero? n) coll (recur (bit-shift-right n 8) (concat [(bit-and n 0xff)] coll))))) (->> (byte-split 0x0123456789abcdef) (map (partial format "%02x"))) ;;=> ("01" "23" "45" "67" "89" "ab" "cd" "ef")
ビッグエンディアンで生成しています。
byte-split の評価結果のシーケンスの各要素は1バイトの値ですが、型は引数 n と同じになります。
(type (first (byte-split 100))) ;=> java.lang.Integer (type (first (byte-split (long 100)))) ;=> java.lang.Long (type (first (byte-split (bigint 100)))) ;=> java.math.BigInteger
■整数をバイト配列(Java配列)に変換する。
clojure.core/byte-array でシーケンスを配列に変換できる。上で作った byte-split を使えば簡単。
ただしシーケンスの要素の型はあらかじめ byte に変換しておく必要がある。
(def arr1 (->> (byte-split 0x123456) (map byte) byte-array)) (class arr1) ;=> [B arr1 ;=> [18, 52, 86] (map (partial format "%02x") arr1) ;=> ("12" "34" "56")
[B は Java の バイト型配列という意味。
これで終りなら簡単な話だが実は問題がある。
(def arr2 (->> (byte-split 0x6789ab) (map byte) byte-array)) ;;=> java.lang.IllegalArgumentException: Value out of range for byte: 137 ;; [Thrown class java.lang.RuntimeException]
エラーが出ているのはこの部分。なお 137 は 16進表記で 0x89 である。
(byte 0x89) ;=> Value out of range for byte: 137
Integer, Long, Byte といった Java の整数型には、unsigned という概念がない。そのため、byte 型の値の範囲は「-128 ~ 127」で固定。
このケースでは byte-split の評価値は実は int のシーケンスなので、int である限り 0x89 (137) という値は問題を起こさない。しかし、byte に変換しようとすると、有効範囲に収まっていないので例外が投げられるというわけ。
Clojure 1.1 では範囲外の値を byte 関数にあたえても例外はおきなかった。元のビットパターンのまま byte 型に変換してくれていたのだが Clojure 1.2 では範囲チェックが入ったらしく、例外になってしまう。
この問題、Clojure 1.1 で作った socket クライアントを Clojure 1.2 で実行したところ発覚した。送信データ列作成で引っかかった。
解決策は幾つかある
■解決策1:範囲を超えたら収まるように変換
2の補数表現にあわせて、範囲オーバーした数は負数に変換してやればいい。
(ns example.code1) (defn int-to-byte-array [n] (->> (byte-split n) (map #(byte (if (> 0x80 %) % (- % 0x100)))) byte-array)) (->> (int-to-byte-array 0x6789ab) (map (partial format "%02x"))) ;=> ("67" "89" "ab")
■解決策2:byteValue メソッドを使う
Integer, Long, BigInteger クラスには byte 型への変換メソッドが用意されている。
(ns example.code2) (defn int-to-byte-array [n] (->> (byte-split n) (map #(.byteValue %)) byte-array))
残念ながら、数値をまるごと渡すと。
(.byteValue 0x6789ab) ;=> 0xab
となってしまう バイト配列に分割してくれるわけではない。最下位の1バイトだけの処理になる。
■解決策3:java.io.ByteArrayOutputStream を使う。
それっぽいクラスが JavaAPI にありました。
(ns example.code3) (defn int-to-byte-array [n] (with-open [baos (java.io.ByteArrayOutputStream.)] (dorun (->> (byte-split n) (map #(.write baos (int %))))) (.toByteArray baos)))
これも、数値をそのまま渡すと1バイト分しか処理してくれない。
(.write baos 0x6789ab) (.toByteArray baos) ;=> [0xab]
■解決策4:java.math.BigInteger/toByteArray を使う。
この方法なら byte-split 不要。
(ns example.code4) (defn int-to-byte-array [n] (if (instance? BigInteger n) (.toByteArray n) (.. (bigint n) toByteArray)))
Clojure の整数は値の大きさによって、int, long, bigint と型が変換される。toByteArray はあくまでも BigInteger のメソッドなので、int や long は、bigint に変換する必要がある。
■パフォーマンス
どれが速いのか。簡単にはかってみた。
ランダムな数値を100個生成して、各 int-to-byte-array での処理時間を計測。
計測結果
int | bigint | bigbigint | ||||
---|---|---|---|---|---|---|
max | avg | max | avg | max | avg | |
[1] 負の数(2の補数)へ変換 | 1.409 | 0.103 | 1.954 | 0.408 | 42.836 | 4.548 |
[2] byteValue メソッドで変換 | 1.250 | 0.274 | 2.413 | 1.017 | 63.859 | 8.889 |
[3] ByteArrayOutputStream クラスで変換 | 0.852 | 0.085 | 1.415 | 0.314 | 37.175 | 3.942 |
[4] BigInteger/toByteArray で変換 | 0.091 | 0.008 | 0.557 | 0.064 | 0.561 | 0.098 |
int | 0 〜 Integer/MAX_VALUE の中からランダムに 100 件選択 |
---|---|
bigint | 10 進 30 桁程度の BigInteger をランダムに 100 件生成 |
bigbigint | 10 進 1800 桁程度の BigInteger をランダムに 100 件生成 |
max | 100 回のうちで最も長くかかった回の処理時間 (ミリ秒) |
avg | 100 回の処理時間の平均値 (ミリ秒) |
BigInteger/toByteArray が圧倒的。他と2桁近く違いました。
int や long も同じだが...
byte 型同様に int や long でも同じ現象/同じ問題がある。が、byte列を作るケースと比べると需要は格段に低いと思う。頭の隅にでも、こう言うことがあるんだと覚えておけばいいかな。
■結論
BigInteger/toByteArray を使うのがよさそうだ。
もう一度コードを書いておきます。
(defn int-to-byte-array [n] (if (instance? BigInteger n) (.toByteArray n) (.. (bigint n) toByteArray)))
■ちなみに文字列は...
文字列は String クラスのメソッドで簡単にバイト配列に変換出来る。
(.getBytes "水底") ;=> [-26, -80, -76, -27, -70, -107] (.getBytes "水底" "utf-8") ;=> [-26, -80, -76, -27, -70, -107] (.getBytes "水底" "sjis") ;=> [-112, -123, -110, -22] (.getBytes "水底" "euc-jp") ;=> [-65, -27, -60, -20]
■duck-streams/to-byte-array
APIを眺めていて気になる関数を見つけた。
clojure.contrib.duck-streams/to-byte-array
ただし今回作った int-to-byte-array とは用途がちょっと違う。文字列やファイルをダンプするのに使う。
次のようにすると、バイナリファイル /bin/ls をダンプした バイト配列が得られる。
※実際に実行すると処理にかなり時間がかかります。
(use '[clojure.contrib.duck-streams :only (to-byte-array file-str)]) (let [filename "/bin/ls"] (to-byte-array (file-str filename)))
以上