整数を 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 での処理時間を計測。
計測結果


intbigintbigbigint
maxavgmaxavgmaxavg
[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
英略称はそれぞれ次のような意味になります。
int0 〜 Integer/MAX_VALUE の中からランダムに 100 件選択
bigint10 進 30 桁程度の BigInteger をランダムに 100 件生成
bigbigint10 進 1800 桁程度の BigInteger をランダムに 100 件生成
max100 回のうちで最も長くかかった回の処理時間 (ミリ秒)
avg100 回の処理時間の平均値 (ミリ秒)
max はすべて「初回の処理時間」でした。
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)))

以上