Java:将字节数组转换为十六进制字符串?

814
我有一个填满十六进制数字的字节数组,用简单的方式打印它是相当无意义的,因为有许多不可打印的元素。我需要的是以3a5f771c形式的确切十六进制代码。

17
何不先试一下,展示出你拥有的东西。你没有什么可以失去的,却可以获得一切。如果这是你所需要的,Integer类有一个“toHexString(...)”方法可能会有所帮助。此外,“String.format(...)”可以使用“%2x”代码字符串进行一些巧妙的格式化技巧。 - Hovercraft Full Of Eels
我需要的是形如3a5f771c...的确切十六进制代码 - 你要求一个确切的形式,但你没有提供一个确切的例子。根据你提供的内容,将前四个字节转换为字符串,然后将省略号连接到字符串中。 - jww
1
借助Java 8中的流,可以简单地实现如下: static String byteArrayToHex(byte[] a) { return IntStream.range(0, a.length) .mapToObj(i -> String.format("%02x", a[i])) .reduce((acc, v) -> acc + " " + v) .get(); } - tibetty
9
Java 17 来拯救:HexFormat.of().formatHex(bytes) - Ali Dehghani
@tibetty 你真的认为那很“简单”吗? - Christoph S.
34个回答

1065

这里的讨论以及尤其是这个答案中,这是我目前使用的函数:

private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
    char[] hexChars = new char[bytes.length * 2];
    for (int j = 0; j < bytes.length; j++) {
        int v = bytes[j] & 0xFF;
        hexChars[j * 2] = HEX_ARRAY[v >>> 4];
        hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
    }
    return new String(hexChars);
}

根据我的一些简单的基准测试(每个数组大小为一百万字节,执行一千次,每个数组大小为256字节,执行一千万次),它比任何其他选择快得多,在长数组上大约快了一半。与我参考的答案相比,在讨论中建议使用位运算符,可以使长数组的时间缩短约20%。(编辑:当我说它比其他选择更快时,我是指在讨论中提供的备选代码。性能与使用非常类似的Commons Codec相当。)

Java 9紧凑字符串版本(2020年):

private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
public static String bytesToHex(byte[] bytes) {
    byte[] hexChars = new byte[bytes.length * 2];
    for (int j = 0; j < bytes.length; j++) {
        int v = bytes[j] & 0xFF;
        hexChars[j * 2] = HEX_ARRAY[v >>> 4];
        hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
    }
    return new String(hexChars, StandardCharsets.UTF_8);
}

298
我刚刚发现了标准分发中的javax.xml.bind.DataTypeConverter。为什么谷歌这类问题时不会出现它呢?它包含了很多有用的工具,包括String printHexBinary(byte[]) byte[] parseHexBinary(String)。然而,printHexBinary比这个回答中的函数慢了许多(2倍),我检查了源代码,它使用了一个stringBuilderparseHexBinary使用了一个数组。不过,对于大多数情况来说,它已经足够快了,而且你可能已经有它了。 - maybeWeCouldStealAVan
84
+1 因为 Android 没有 DataTypeConverter。 - Vaiden
9
JDK 7现在是开源的了。我们应该提交一个补丁来改善printHexBinary方法的性能? - kevinarpe
45
javax.xml.bind.DataTypeConverter 将在 Java 11 中被移除。 - The Impaler
11
在这行代码int v = bytes[j] & 0xFF;中,为什么需要加上& 0xFF?我有什么遗漏吗?还是说这只是不必要的? - AlbAtNf
显示剩余15条评论

531

Apache Commons Codec库提供了一个Hex类,可以用来完成这种工作。

import org.apache.commons.codec.binary.Hex;

String foo = "I am a string";
byte[] bytes = foo.getBytes();
System.out.println( Hex.encodeHexString( bytes ) );

13
我的投票反对发生在4个月前,所以我不太确定当时的想法,但我可能是反对库的大小。这是程序中的一个小函数;没有必要将如此庞大的库添加到项目中以执行它。 - ArtOfWarfare
7
我同意,所以你可以用import org.apache.commons.codec.binary.Hex;代替import org.apache.commons.codec.*; - cytinus
18
@ArtOfWarfare 我必须反对你的观点。唯一糟糕的事情是,JRE和JDK默认没有包含Apache Commons库。有些库非常有用,它们真的应该默认被加入到你的类路径中,而这个库就是其中之一。 - corsiKa
49
我强烈建议将此答案置为首位。始终投票使用经过充分测试、高性能、开源的库,而不是无法改进它的自定义代码。 - Dmitriy Likhten
8
如果你使用BouncyCastle(_org.bouncycastle:bcprov-jdk15on_),你可以使用这个类:org.bouncycastle.util.encoders.Hex,其中包含这个方法:String toHexString(byte[] data) - Guillaume Husta
显示剩余7条评论

348

Java Architecture for XML Binding (JAXB) 中的方法 javax.xml.bind.DatatypeConverter.printHexBinary() 是将 byte[] 转换为十六进制字符串的便捷方式。DatatypeConverter 类还包括许多其他有用的数据操作方法。

在Java 8及之前版本中,JAXB是Java标准库的一部分。随着Java 9的发布,它被废弃,并在Java 11中移除, 这是将所有Java EE包移动到自己的库中的努力的一部分。 这是一个漫长的故事。现在,javax.xml.bind不存在了,如果你想使用包含DatatypeConverter的JAXB,你需要从Maven安装JAXB APIJAXB Runtime

使用示例:

byte bytes[] = {(byte)0, (byte)0, (byte)134, (byte)0, (byte)61};
String hex = javax.xml.bind.DatatypeConverter.printHexBinary(bytes);

将导致:

000086003D

这个答案与这个相同。


13
可惜的是,在安卓系统中并没有一个可行的好方案。 - Kazriko
16
自JDK 9起,DatatypeConverter不再可访问。 - pmcollins
@pmcollins 你为什么这么说?至少在API文档中,它还在:https://docs.oracle.com/javase/9/docs/api/javax/xml/bind/DatatypeConverter.html - PhoneixS
3
它仍然存在,但不是默认运行时的一部分(由于Java 9模块化)。 - Spotlight
3
不要依赖于javax.xml.bind,虽然它可以编译通过,但在运行时可能找不到。如果你这样做了,请准备好处理java.lang.NoClassDefFoundError。 - Dmytro
显示剩余3条评论

288

最简单的解决方案,没有外部库,没有数字常量:

public static String byteArrayToHex(byte[] a) {
   StringBuilder sb = new StringBuilder(a.length * 2);
   for(byte b: a)
      sb.append(String.format("%02x", b));
   return sb.toString();
}

27
平均而言,这种方法速度非常慢,对于长度为162字节的情况下,平均比前面所述的方法慢1000倍。如果性能很重要,请避免使用String.Format。 - pt123
12
可能会慢一些。对于偶尔发生的事情,比如登录或类似情况,这是很好的选择。 - Pointer Null
50
如果速度慢,那又怎样?在我的使用情境中,它只是一个调试语句,所以感谢这段代码片段。 - vikingsteve
12
如果您只需要一个函数,那么通过包含几十KB的额外JAR文件来重用库可能并不高效(在某些平台上,比如Android,整个JAR文件将被包含在最终应用程序中)。有时候,在不需要性能的情况下,更短、更清晰的代码更好。 - personne3000
3
@personne3000 或许是这样,但如果是那样的话,你需要流支持而不是单个调用功能。这个功能容易理解和记忆,因此易于维护。 - Maarten Bodewes
显示剩余3条评论

183

以下是从简单(一行代码)到复杂(大型库)排序的一些常见选项。如果您对性能感兴趣,请参见下面的微基准测试。

选项1:代码片段-简单(仅使用JDK / Android)

选项1a:BigInteger

一个非常简单的解决方案是使用BigInteger的十六进制表示:

new BigInteger(1, someByteArray).toString(16);

注意,由于它处理的是数字而不是任意字节串,因此它会省略前导零 - 这可能符合您的要求(例如对于3字节输入:000AE3 vs 0AE3)。与选项2相比,这也非常慢,大约慢100倍。

选项1b:String.format()

使用%X占位符,String.format()可以将大多数基本类型(shortintlong)编码为十六进制:

String.format("%X", ByteBuffer.wrap(eightByteArray).getLong());

选项1c:整数/长整数(仅限4/8字节数组)

如果您只有4字节数组,您可以使用Integer类的toHexString方法:

Integer.toHexString(ByteBuffer.wrap(fourByteArray).getInt());

对于8字节数组Long同样适用。

Long.toHexString(ByteBuffer.wrap(eightByteArray).getLong());

选项1d:JDK17+ HexFormat

JDK 17最终提供了直观的十六进制编码的一级支持,使用HexFormat

HexFormat hex = HexFormat.of();
hex.formatHex(someByteArray)

选项二:高级代码片段

以下是一个完整且可复制粘贴的代码片段,支持大写/小写字节序。它被优化以最小化内存复杂度并最大化性能,并应该与所有现代Java版本(5+)兼容。

private static final char[] LOOKUP_TABLE_LOWER = new char[]{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66};
private static final char[] LOOKUP_TABLE_UPPER = new char[]{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46};
        
public static String encode(byte[] byteArray, boolean upperCase, ByteOrder byteOrder) {

    // our output size will be exactly 2x byte-array length
    final char[] buffer = new char[byteArray.length * 2];

    // choose lower or uppercase lookup table
    final char[] lookup = upperCase ? LOOKUP_TABLE_UPPER : LOOKUP_TABLE_LOWER;

    int index;
    for (int i = 0; i < byteArray.length; i++) {
        // for little endian we count from last to first
        index = (byteOrder == ByteOrder.BIG_ENDIAN) ? i : byteArray.length - i - 1;
        
        // extract the upper 4 bit and look up char (0-A)
        buffer[i << 1] = lookup[(byteArray[index] >> 4) & 0xF];
        // extract the lower 4 bit and look up char (0-A)
        buffer[(i << 1) + 1] = lookup[(byteArray[index] & 0xF)];
    }
    return new String(buffer);
}

public static String encode(byte[] byteArray) {
    return encode(byteArray, false, ByteOrder.BIG_ENDIAN);
}

完整的源代码和解码器可以在此处找到,使用Apache v2许可证。

选项3:使用小型优化库:bytes-java

在之前的项目中,我创建了这个用于Java字节操作的小工具包。它不需要外部依赖,与Java 7+兼容。其中包括一个非常快速和经过充分测试的HEX编解码器:

import at.favre.lib.bytes.Bytes;
...
Bytes.wrap(someByteArray).encodeHex()

你可以在 GitHub: bytes-java 上查看它。

选项4:Apache Commons Codec

当然,还有那些优秀的“老派”共用编解码器(commons codecs)。(警告:以下是个人观点)在上面概述的项目中,我分析了这段代码并感到相当失望。有很多重复的、无组织的代码,过时和奇特的编解码器可能只适用于少数人,以及非常过度工程化和缓慢的流行编解码器实现(尤其是Base64)。因此,如果你想使用它或其他替代品,我会做出明智的决定。无论如何,如果你仍然想使用它,这里是一段代码片段:

import org.apache.commons.codec.binary.Hex;
...
Hex.encodeHexString(someByteArray));

选项5:Google Guava

很多时候,您已经将Guava作为依赖项。如果是这样,只需使用以下内容:

import com.google.common.io.BaseEncoding;
...
BaseEncoding.base16().lowerCase().encode(someByteArray);

选项六:Spring Security

如果你使用Spring框架Spring Security,你可以使用以下方法:

import org.springframework.security.crypto.codec.Hex
...
new String(Hex.encode(someByteArray));

选项7:Bouncy Castle

如果您已经使用安全框架Bouncy Castle,您可以使用其Hex工具:

import org.bouncycastle.util.encoders.Hex;
...
Hex.toHexString(someByteArray);

不是真正的选项8: Java 9+兼容性或“不要使用JAXBs javax / xml / bind / DatatypeConverter”

在之前的Java(8及以下)版本中,JAXB的Java代码作为运行时依赖项包含在其中。自Java 9和Jigsaw模块化以来,您的代码无法在未经明确声明的情况下访问其模块之外的其他代码。因此,如果您收到类似于以下异常:

java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException

如果在Java 9+的JVM上运行,请切换到上述任何一种替代实现。另请参见此问题


微基准测试

以下是一个简单JMH微基准测试编码不同尺寸的字节数组的结果。这些值是每秒操作次数,因此较高的值更好。请注意,微基准测试往往不能代表真实世界的行为,因此请对这些结果持怀疑态度。

| Name (ops/s)         |    16 byte |    32 byte |  128 byte | 0.95 MB |
|----------------------|-----------:|-----------:|----------:|--------:|
| Opt1: BigInteger     |  2,088,514 |  1,008,357 |   133,665 |       4 |
| Opt2/3: Bytes Lib    | 20,423,170 | 16,049,841 | 6,685,522 |     825 |
| Opt4: Apache Commons | 17,503,857 | 12,382,018 | 4,319,898 |     529 |
| Opt5: Guava          | 10,177,925 |  6,937,833 | 2,094,658 |     257 |
| Opt6: Spring         | 18,704,986 | 13,643,374 | 4,904,805 |     601 |
| Opt7: BC             |  7,501,666 |  3,674,422 | 1,077,236 |     152 |
| Opt8: JAX-B          | 13,497,736 |  8,312,834 | 2,590,940 |     346 |

规格:JDK 8u202,i7-7700K,Win10,24 GB Ram。查看完整基准测试此处

2022年基准测试更新

以下是使用当前的JMH 1.36、Java 17和更高端的计算机的结果。

| Name (ops/s)         |    16 byte |    32 byte |  128 byte | 0.95 MB |
|----------------------|-----------:|-----------:|----------:|--------:|
| Opt1a: BigInteger    |  2,941,403 |  1,389,448 |   242,096 |       5 |
| Opt1d: HexFormat     | 32,635,184 | 20,262,332 | 7,388,135 |     922 |
| Opt2/3: Bytes Lib    | 31,724,981 | 22,786,906 | 6,197,028 |     930 |

规格: JDK Temurin 17.0.6,Ryzen 5900X,Win11,24GB DDR4内存


2
非常全面!由于您正在使用Java 17,您可能希望将HexFormat.of添加到使用JDK17+的选项中。 - automorphic
不知道这一点,谢谢提示 - 我会更新答案的! - Patrick
@Patrick,能否让“HexFormat”进入基准测试? - Jin Kwon

65

一种完整的番石榴解决方案:

import com.google.common.io.BaseEncoding;
...
byte[] bytes = "Hello world".getBytes(StandardCharsets.UTF_8);
final String hex = BaseEncoding.base16().lowerCase().encode(bytes);

现在 hex"48656c6c6f20776f726c64"


在Guava中,您还可以使用new HashCode(bytes).toString() - mfulton26
2
从Guava 22.0开始,它是 HashCode.fromBytes(checksum).toString() - Devstr

47

这个简单的一行代码对我很有用:
String result = new BigInteger(1, inputBytes).toString(16);
编辑-使用此代码将删除前导零,但它适用于我的用例。感谢@Voicu指出


61
这个_One-liner_会删除前导的零字节。 - Voicu
@Voicu ... 这将有50%的概率添加一个前导零。 - Maarten Bodewes

42

Java 17最终包含了HexFormat类,因此您可以轻松地执行以下操作:

HexFormat.of().formatHex(bytes);

它支持将配置设置为小写/大写、分隔符、前缀、后缀等。


8
最终,有一些东西不需要外部库或是一个不完整的解决方案。 - Diamond

29

如果是固定长度的内容,比如哈希值,我会使用类似这样的方式:

md5sum = String.format("%032x", new BigInteger(1, md.digest()));

2
谢谢,这非常简洁明了,而且十分恰当。 - Deepan Prabhu Babu

24

使用 DataTypeConverter 类javax.xml.bind.DataTypeConverter

String hexString = DataTypeConverter.printHexBinary(bytes[] raw);


4
Java 11中删除了一些类。详情请见:JEP 320: Remove the Java EE and CORBA Modules - Basil Bourque

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,