1. 概要

このチュートリアルでは、KotlinでByteArrayを16進文字列に変換するためのいくつかのアプローチについて説明します。

まず、この変換の一般的なアルゴリズムを説明します。 アルゴリズムがわかれば、KotlinまたはJava標準ライブラリを利用して変換を実装できます。 最後に、ボーナスとして、同じロジックに対して単純な古いループとビット単位の演算を使用する1つの追加のアプローチが表示されます。

2. アルゴリズム

バイトの配列を16進数に相当するものに変換するには、次の簡単な手順に従います。

  1. 配列の各バイトunsigned値を対応する16進値に変換します
  2. 計算されたすべての16進値を連結します

各16進値を表すには4ビットで十分なので、各バイト(8ビット)は2つの16進値に等しくなければなりません。 したがって、バイトに相当する16進数が1文字だけの場合は、デコードプロセスを可能にするために先行ゼロを追加する必要があります

この変換の一般的な考え方がわかったので、Kotlinに実装してみましょう。

3. Kotlin標準ライブラリ

3.1. フォーマッター

ByteArrayjoinToString()拡張関数は、バイトの配列をStringに変換します。 具体的には、この関数を使用すると、各バイトをCharSequenceに変換し、separatorを使用してそれらを連結できます。 これはまさに、上記のアルゴリズムを実装するために必要なものです。

fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }

上記のように、変換関数は“ %02x” 形式指定子を使用して、指定されたバイトを対応する16進値に変換します。 さらに、必要に応じて、16進値に先行ゼロを埋め込みます。 各バイトを変換した後、区切り文字として空の文字列を使用して、結果の配列を結合します。

この関数が期待どおりに動作することを確認しましょう。

val md5 = MessageDigest.getInstance("md5")
md5.update("baeldung".toByteArray())

val digest: ByteArray = md5.digest()
assertEquals("9d2ea3506b1121365e5eec24c92c528d", digest.toHex())

ここでは、エンコードプロセスが実際にMD516進ダイジェストで機能することを確認しています。

3.2. 符号なし整数

Kotlin 1.3以降、符号なしタイプを使用して同じロジックを実装することもできます。

@ExperimentalUnsignedTypes
fun ByteArray.toHex2(): String = asUByteArray().joinToString("") { it.toString(radix = 16).padStart(2, '0') }

現在、より冗長で読みやすい変換関数を使用しています。 つまり、フォーマット指定子の代わりにを使用して、バイトを16進数の文字列に変換し、結果をパディングします。 それに加えて、負の数の厄介さを避けるために、最初は配列を符号なしの同等のものに変換しています。

符号なし整数はKotlin1.5の時点で安定していますが、符号なし配列はまだベータ段階です。 そのため、この機能に明示的にオプトインするには、@ExperimentalUnsignedTypesアノテーションを追加する必要がありました。

実験的なAPIを使用せずに、同様の実装に java.lang.ByteAPIを使用することもできます。

fun ByteArray.toHex3(): String = joinToString("") {
    java.lang.Byte.toUnsignedInt(it).toString(radix = 16).padStart(2, '0')
}

ここで、 toUnsignedInt()メソッドは、各バイトの符号なしの値を見つける役割を果たします。

4. Java 17

Java 17(この記事の執筆時点で早期アクセス中)の時点で、java.util.HexFormatユーティリティクラスは、バイト配列を16進値に、またはその逆に変換する慣習的な方法です。 KotlinコードがJava17をターゲットにしている場合は、独自の実装の代わりにこのユーティリティを使用することをお勧めします。

val hex = HexFormat.of().formatHex(digest)
assertEquals("9d2ea3506b1121365e5eec24c92c528d", hex)

この抽象化は非常に使いやすく、バイト配列から16進への変換を処理するための豊富なAPIを提供します。

5. ループとビット演算

約束したように、今度はループとビット演算を使用して変換を実装します。

val hexChars = "0123456789abcdef".toCharArray()

fun ByteArray.toHex4(): String {
    val hex = CharArray(2 * this.size)
    this.forEachIndexed { i, byte ->
        val unsigned = 0xff and byte.toInt()
        hex[2 * i] = hexChars[unsigned / 16]
        hex[2 * i + 1] = hexChars[unsigned % 16]
    }

    return hex.joinToString("")
}

各バイトは2つの16進文字で表すことができます。 したがって、CharArrayを受信側のByteArrayの2倍のサイズで割り当てています。

その後、バイト配列を繰り返し処理します。 各反復で、最初に、現在のバイトの符号なし値を抽出します。 そのために、現在のバイト値を0xffおよびします。 16進値は常に正(符号ビットはゼロ)であるため、 0xff は、正の数をそのままにして、負の数の符号ビットを反転します。

上の図では、 「-1&0xff」 255であることが判明しました。

16で割ると、商と余りがそれぞれ1番目と2番目の16進文字を決定します。 そのため、CharArray要素に除算と剰余演算の結果を入力しました。

“ /” および“%” 演算子をビット単位の演算子に置き換えると、パフォーマンスが向上すると思われるかもしれません。 ただし、たとえばC2などの最新のJVMコンパイラは、実行時に内部でこのような最適化を適用することを期待しています。 だから、読みやすさをさらに損なうことはありません!

6. 結論

このチュートリアルでは、バイトの配列を16進表現に変換するためのさまざまなアプローチを見ました。 また、各実装の背後にあるロジックをよりよく理解するために、簡単なアルゴリズムの説明から始めました。

いつものように、すべての例はGitHubから入手できます。