在Java中,如何将字节数组转换为十六进制字符串并保留前导零?

171

我正在使用一些用于生成MD5哈希的Java示例代码。其中一部分将结果从字节转换为十六进制数字字符串:

byte messageDigest[] = algorithm.digest();     
StringBuffer hexString = new StringBuffer();
for (int i=0;i<messageDigest.length;i++) {
    hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
    }

然而,这种方法并不完全有效,因为 toHexString 显然会删除前导零。那么,从字节数组转换为十六进制字符串并保留前导零的最简单方法是什么?

28个回答

142

14
只要你使用Apache Commons Codec进行MD5加密,就可以看一下[DigestUtils.md5Hex()]这个方法。 - rescdsk
1
DigestUtils确实让事情变得更简单,但将其包含在项目中可能会带来麻烦。个人而言,想到要修改pom文件,我就感到很烦恼。 - Conor Pender

123

您可以使用下面这个。我测试过包含前导零字节和初始负字节的情况。

public static String toHex(byte[] bytes) {
    BigInteger bi = new BigInteger(1, bytes);
    return String.format("%0" + (bytes.length << 1) + "X", bi);
}
如果您想要小写的十六进制数字,请在格式字符串中使用"x"

3
没有外部依赖,简短易懂。此外,如果你知道你有16字节/32个十六进制数字,你的解决方案可以压缩为一个简单的一行代码。很酷! - Roboprog
非常好,谢谢。 - Lev
谢谢。我需要将一个16字节的IPv6字节数组转换为Scala中的零填充十六进制字符串:f"${BigInt(1, myIpv6ByteArray)}%032x" - Mark Rajcok
1
干得好先生,这个程序很好地将ECDSA密钥的原始字节字符串转换了。它甚至可以在前面转换压缩字节,而无需关于密钥的额外信息和错误!(举手高五) - Chadd Frasier

114

一个简单的方法是检查 Integer.toHexString() 输出了多少个数字,如果需要,为每个字节添加前导零。像这样:

一个简单的方法是检查 Integer.toHexString() 输出了多少个数字,如果需要,为每个字节添加前导零。像这样:

public static String toHexString(byte[] bytes) {
    StringBuilder hexString = new StringBuilder();

    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
            hexString.append('0');
        }
        hexString.append(hex);
    }

    return hexString.toString();
}

对于字节0x01,它不会产生“10”吗? - n0rd
4
不,十六进制值被添加到hexString之前先附加了一个0。 - Michael Myers
当我调用 Integer.toHexString((byte)0xff) 时,它返回了 "ffffffff",这是由于符号扩展造成的。因此,可能需要取返回字符串的最后两个字符。 - Marvo
这样会不会返回多余的零?例如,如果字节数组是 {0,1,2,3},它应该返回 0123,但实际上它会返回 00010203,这是哈希的期望结果吗? - Juzer Ali
1
@juzerali:这个问题要求“保留前导零”。如果您不想要前导零,那么使用该代码就没有必要了;请使用问题中的代码。 - Michael Myers
这个能处理大的负数而不会溢出吗? - piritocle

40

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

在Java 8及以前版本中,JAXB是Java标准库的一部分。随着将所有Java EE包移动到它们自己的库中的努力,它在Java 9中被deprecated,在Java 11中被removed这是一个漫长的故事。现在,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

对于反转,也可以使用DatatypeConverter.parseHexBinary(hexString) - Sanghyun Lee
2
请注意,从Java 11开始,java.xml包不再是JDK的一部分。 - AndreasB

35

我喜欢 Steve 的提交,但他本可以少些几个变量,在此过程中节省几行代码。

public static String toHexString(byte[] bytes) {
    char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
    char[] hexChars = new char[bytes.length * 2];
    int v;
    for ( int j = 0; j < bytes.length; j++ ) {
        v = bytes[j] & 0xFF;
        hexChars[j*2] = hexArray[v/16];
        hexChars[j*2 + 1] = hexArray[v%16];
    }
    return new String(hexChars);
}

我喜欢这个方法的原因是可以清楚地看到它在做什么(而不是依赖某种神奇的BigInteger黑盒转换),而且你无需担心像前导零之类的边界情况。该程序将每个4位半字节转换为一个十六进制字符,并使用表查找,因此它可能很快速。如果你使用按位移位和AND来替换v/16和v%16,它可能会更快,但是我现在太懒了,没有测试。


不错!它改进了Steve的“附加速度慢”的想法,使其适用于任意大小的字节数组。 - Ogre Psalm33
将v/16更改为v >>> 4,将v%16更改为v&0x0F以提高速度。此外,您可以使用j << 1来将其乘以2(尽管编译器可能会自动完成这个操作)。 - Scott Carey
或者更好的方法是将值添加到“0”以获取字符,这样就不需要查找表。例如:hexChars[j << 1] = (byte)(v >>> 4 + '0')。 - Scott Carey
(我犯了个错误!ASCII表格中没有将a-f或A-F跟随0-9,之前的方法行不通) - Scott Carey
6
可能有人需要逆函数。public static byte[] bytesFromHex(String hexString) { final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; char[] hexChars = hexString.toCharArray(); byte[] result = new byte[hexChars.length / 2]; for (int j = 0; j < hexChars.length; j += 2) { result[j / 2] = (byte) (Arrays.binarySearch(hexArray, hexChars[j]) * 16 + Arrays.binarySearch(hexArray, hexChars[j + 1])); } return result; } - cn1h
位移版本成为了https://dev59.com/NGkw5IYBdhLWcg3wx9eC#9855338上被接受的答案。 - simbo1905

22

我发现 Integer.toHexString 有点慢。如果你要转换很多字节,你可以考虑构建一个包含“00”..“FF”的字符串数组,并使用整数作为索引。

hexString.append(hexArray[0xFF & messageDigest[i]]);

这样做更快,而且可以确保正确的长度。只需要字符串数组:

String[] hexArray = {
"00","01","02","03","04","05","06","07","08","09","0A","0B","0C","0D","0E","0F",
"10","11","12","13","14","15","16","17","18","19","1A","1B","1C","1D","1E","1F",
"20","21","22","23","24","25","26","27","28","29","2A","2B","2C","2D","2E","2F",
"30","31","32","33","34","35","36","37","38","39","3A","3B","3C","3D","3E","3F",
"40","41","42","43","44","45","46","47","48","49","4A","4B","4C","4D","4E","4F",
"50","51","52","53","54","55","56","57","58","59","5A","5B","5C","5D","5E","5F",
"60","61","62","63","64","65","66","67","68","69","6A","6B","6C","6D","6E","6F",
"70","71","72","73","74","75","76","77","78","79","7A","7B","7C","7D","7E","7F",
"80","81","82","83","84","85","86","87","88","89","8A","8B","8C","8D","8E","8F",
"90","91","92","93","94","95","96","97","98","99","9A","9B","9C","9D","9E","9F",
"A0","A1","A2","A3","A4","A5","A6","A7","A8","A9","AA","AB","AC","AD","AE","AF",
"B0","B1","B2","B3","B4","B5","B6","B7","B8","B9","BA","BB","BC","BD","BE","BF",
"C0","C1","C2","C3","C4","C5","C6","C7","C8","C9","CA","CB","CC","CD","CE","CF",
"D0","D1","D2","D3","D4","D5","D6","D7","D8","D9","DA","DB","DC","DD","DE","DF",
"E0","E1","E2","E3","E4","E5","E6","E7","E8","E9","EA","EB","EC","ED","EE","EF",
"F0","F1","F2","F3","F4","F5","F6","F7","F8","F9","FA","FB","FC","FD","FE","FF"};

1
@Marvo 0x000000FF == 0xFF,所以你提出的更改没有任何作用。掩码只是像任何其他数字一样的整数。0xFF!= -1 - ComputerDruid

13

对于固定长度的内容,比如哈希值,我会使用类似这样的方法:

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

掩码中的0表示填充...


只需使用标准的Java即可实现一行解决方案! - skomisa

12

我一直在寻找同样的东西...这里有一些好的想法,但我进行了一些微型基准测试。我发现以下修改后的代码是最快的(从Ayman的代码修改而来,速度大约是原来的两倍,并且比Steve的代码快了约50%):

public static String hash(String text, String algorithm)
        throws NoSuchAlgorithmException {
    byte[] hash = MessageDigest.getInstance(algorithm).digest(text.getBytes());
    return new BigInteger(1, hash).toString(16);
}

编辑:糟糕 - 没有注意到这与kgiannakakis的基本相同,因此可能会去掉前导0。不过,将其修改为以下内容后,它仍然是最快的:

public static String hash(String text, String algorithm)
        throws NoSuchAlgorithmException {
    byte[] hash = MessageDigest.getInstance(algorithm).digest(text.getBytes());
    BigInteger bi = new BigInteger(1, hash);
    String result = bi.toString(16);
    if (result.length() % 2 != 0) {
        return "0" + result;
    }
    return result;
}

4
这还不对。比如哈希值为 {0, 0, 0, 0},使用 BigIntegertoString 方法会返回 "0"。这段代码在其前面添加了另一个 "0" 并返回了 "00",但结果应该是 "00000000" - Daniel Lubarov
BigInteger.toString() 是我在 Java 中发现的最慢的实现方式,大约比高性能实现慢100倍,参见 https://dev59.com/NGkw5IYBdhLWcg3wx9eC#58118078。此外,在您的答案中计算了哈希值,但这不是问题的一部分。 - Patrick
虽然这并不是原始帖子所问的内容,但如果您正在寻找散列字符串并将其更改为中间某个值“0”,那么这是OK的。同时,在长度小于期望长度时,请在前面添加零(大多数哈希都有期望长度,通常是2的幂,如128)。 - comodoro

11
static String toHex(byte[] digest) {
    StringBuilder sb = new StringBuilder();
    for (byte b : digest) {
        sb.append(String.format("%1$02X", b));
    }

    return sb.toString();
}

3
StringBuilder 的默认初始容量为16个字符。MD5哈希由32个字符组成。在添加了前16个字符后,内部数组将被复制到一个长度为34的新数组中。此外,每个摘要字节都会创建一个新的 Formatter 实例。默认情况下,每个 Formatter 都会实例化一个新的 StringBuilder 来缓冲其输出。我认为更容易的方法是创建一个只有32个字符初始容量的 StringBuffer (new Formatter(new StringBuilder(32))) ,然后使用它的 formattoString 方法。 - Robert
当然,对于可变长度的摘要,您将使用“digest.length * 2”的初始容量。 - Robert

6
Guava使得这也相当简单:

Guava

,它可以帮助您轻松实现。
BaseEncoding.base16().encode( bytes );

当Apache Commons不可用时,它是一个不错的替代品。它还有一些很好的输出控制,比如:

byte[] bytes = new byte[] { 0xa, 0xb, 0xc, 0xd, 0xe, 0xf };
BaseEncoding.base16().lowerCase().withSeparator( ":", 2 ).encode( bytes );
// "0a:0b:0c:0d:0e:0f"

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