一个人如何学习Java?(将字节数组转换为十六进制字符串)

5

我知道这听起来像是一个广泛的问题,但我可以通过一个例子来缩小范围。我在Java方面非常新手。在我的“学习”项目中,我想创建一个内部MD5文件哈希器供我们使用。我开始非常简单,尝试对字符串进行哈希,然后再转到文件。我创建了一个名为MD5Hasher.java的文件,并编写了以下内容:

import java.security.*;
import java.io.*;
public class MD5Hasher{
    public static void main(String[] args){
        String myString = "Hello, World!";
        byte[] myBA = myString.getBytes();
        MessageDigest myMD;
        try{
            myMD = MessageDigest.getInstance("MD5");
            myMD.update(myBA);
            byte[] newBA = myMD.digest();
            String output = newBA.toString();
            System.out.println("The Answer Is: " + output);
        } catch(NoSuchAlgorithmException nsae){
            // print error here
        }
    }
}

我访问了java.sun.com,查看了java.security的javadoc,以了解如何使用MessageDigest类。阅读后,我知道我需要使用“getInstance”方法来获取可用的MessageDigest对象。Javadoc接着说:“使用update方法处理数据。”因此,我查看了update方法,并确定我需要使用其中一个,将我的字符串转换为字节数组并传递给它,因此我添加了这一部分。Javadoc继续说:“在更新所有要更新的数据之后,应调用digest方法之一来完成哈希计算。”我再次查看了方法,并发现digest返回一个字节数组,因此我添加了这一部分。然后,我对新的字节数组使用“toString”方法,以获得可以打印的字符串。但是,当我编译和运行代码时,打印出来的只有这个:

答案是:[B@4cb162d5

我在StackOverflow上找到了一些信息,发现了以下示例:(如何生成MD5哈希?)

String plaintext = 'your text here';
MessageDigest m = MessageDigest.getInstance("MD5");
m.reset();
m.update(plaintext.getBytes());
byte[] digest = m.digest();
BigInteger bigInt = new BigInteger(1,digest);
String hashtext = bigInt.toString(16);
// Now we need to zero pad it if you actually want the full 32 chars.
while(hashtext.length() < 32 ){
    hashtext = "0"+hashtext;
}

看起来我唯一会遗漏的部分可能是"BigInteger"部分,但我不确定。

因此,经过这一切,我想问的是,你怎么知道要使用“BigInteger”部分?我错误地认为我的newBA对象上的“toString”方法会将其转换为可读的输出,但显然我错了。一个人怎么才能知道在Java中该走哪条路?我有C语言背景,所以这个Java东西看起来很奇怪。有什么建议可以让我不必总是通过谷歌搜索如何做某些事情而变得更好吗?

感谢大家抽出时间阅读。 :-)


2
成为一名优秀的程序员,学会了解你的资源以及如何在什么时候和如何使用它们是非常重要的。谷歌是一个很好的资源,在为自己提升开发技能方面,充分利用谷歌绝不令人感到羞耻。其他资源包括你的同行开发者和自学教材。 - Jagd
11个回答

6
在这种情况下,关键是要意识到字节不是“人类可读”的,但字符是。因此,您需要以某种格式将字节转换为字符。对于任意的字节(如哈希值),通常使用十六进制作为“人类可读”格式。然后,每个字节都要转换为一个由2个字符组成的十六进制字符串,然后将它们连接在一起。
这与您使用的语言无关。您只需要了解/意识到它以一种与语言无关的方式工作。您必须了解您所拥有的(一个字节数组)和您想要的(一个十六进制字符串)。编程语言只是实现所需结果的“工具”。您只需在所需的功能要求和想要使用的编程语言之间进行谷歌搜索,例如“在Java中将字节数组转换为十六进制字符串”。
那么,你找到的代码示例是错误的。实际上,你应该在循环中确定每个字节,并测试它是否小于0x10,然后用零填充它,而不仅仅是根据结果字符串的长度来填充零(这可能不一定是由第一个字节小于0x10引起的!)。
StringBuilder hex = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
    if ((b & 0xff) < 0x10) hex.append("0");
    hex.append(Integer.toHexString(b & 0xff));
}
String hexString = hex.toString();

根据@extraneon的答案评论更新,使用new BigInteger(byte[])也是错误的解决方案。这不会取消字节的符号。在Java中,字节(作为所有原始数)是有符号的。它们具有负范围。在Java中,byte范围从-128127,而您需要将范围设置为0255以获得适当的十六进制字符串。您基本上只需要删除符号以使它们无符号。上面示例中的& 0xff正是如此。
new BigInteger(bytes).toString(16)获取的十六进制字符串与全世界已知的所有其他十六进制字符串生成MD5生成器的结果不兼容。每当MD5摘要中有负字节时,它们将不同。

2

2

MessageDigests用于计算某些东西的字节数组,通常看到的字符串(例如1f3870be274f6c49b3e31a0c6728957f)实际上只是将字节数组转换为十六进制字符串。

当您调用 MessageDigest.toString()时,它会调用 MessageDigest.digest().toString(),在Java中,toString方法用于byte[](由MessageDigest.digest()返回)返回对字节的引用,而不是实际的字节。

在您发布的代码中,字节数组被更改为整数(在这种情况下为BigInteger,因为它非常大),然后转换为十六进制以打印到字符串中。

摘要计算的字节数组表示一个数字(根据http://en.wikipedia.org/wiki/MD5,是128位数字),该数字可以转换为任何其他基数,因此MD5的结果可以表示为十进制数字、二进制数字(如字节数组)或最常见的十六进制数字。


2
您已经成功地获取了消息的内容,但您不知道如何适当地呈现找到的摘要值。您手头有一个字节数组,这有点难以阅读,字节数组的toString方法返回的是[B@somewhere,这一点并没有什么用处。
BigInteger作为将字节数组格式化为单个数字的工具出现了。
您需要执行以下步骤:
  • 构建一个BigInteger对象,传入正确的值(在此情况下,该值恰好被编码为一个字节数组——您的摘要)
  • 指示BigInteger对象返回该数字(即16进制)的字符串表示形式(例如普通的可读文本)base 16
while循环会在该值前面添加0字符以获得32位宽度。我可能会使用String.format方法来实现,但无论您采取什么方式都可以。

非常酷...谢谢。我添加了以下几行代码,它输出了我想要的结果:BigInteger newBI = new BigInteger(newBA); String outupt = newBI.toString(16); - Brian
@Brian:这也是错误的解决方案。当前导字节为负数时,它将返回一个负的十六进制字符串。请参阅我的答案以获取正确的十六进制字符串转换方法。您还会发现,一些用户可能建议使用new BigInteger(bytes).abs().toString(16),但这也是根本错误的。对于带有负前导字节的情况,这会导致错误的十六进制字符串,无法将其转换回相同的字节。因此,不能与世界上已知的另一个生成MD5哈希值的程序共享/使用。 - BalusC
我不确定哪里出了错。我使用 "BigInteger newBI = new BigInteger(newBA);" 构造了一个 BigInteger,然后使用 toString(16) 返回了一个可读的十六进制字符串。我的问题在哪里? - Brian
Java中的字节是有符号的(范围为-128至127),而您希望它们是无符号的(范围为0至255)以用于十六进制字符串。我答案中的& 0xFF去除了符号,但BigInteger不会这样做。在某些情况下(当字节为负数时),您将获取错误的十六进制字符串。 - BalusC
我指向的构造函数有一个符号参数,它是BigInteger(int sign, byte[] value)。如果你像这样调用它new BigInteger(1, value),我认为你应该没问题。 - extraneon
实际上,没有'1'符号,"-123"的哈希值是"-35fbb7c73bebac9b46e0afbe17b90b7d",但有了它,哈希值变成了"ca044838c4145364b91f5041e846f483"。再次感谢您的帮助和贡献。 - Brian

1

BigInteger被使用的原因是字节数组非常长,太大了不能适应一个int或者long。然而,如果你确实想要查看字节数组中的所有内容,有一种替代方法。你可以将这行代码替换为:

String output = newBA.toString();

使用:

String output = Arrays.toString(newBA);

这将打印出数组的内容,而不是引用地址。


"String output = Arrays.toString(newBA)" -- 这并不是很有用。它会将数组内容显示为一系列字节值,例如“[-33, 1, 93, -104,...]”,而不是DF015D98...。 - Jason S
@Jason:你说得对,在这个特定的应用程序中,Arrays.toString()并不提供最佳表示。我会保留我的答案,因为它对于大多数打印数组的情况都是有用的。 - Justin Ardini

1

虽然我完全没有使用Java来处理MD5哈希的经验,但我可以推荐Sun's Java Tutorials作为学习Java的绝佳资源。他们涵盖了大部分语言,并在我学习Java时给了我很多帮助。

此外,还可以查看其他帖子中提出相同问题的建议。


0
使用一个IDE,它可以显示“toString()”方法来自哪里。在大多数情况下,它只是来自Object类,并且不会非常有用。通常建议重写toString方法以提供一些干净的输出,但许多类并没有这样做。

哦,是的...我使用NetBeans。它在让我取得目前的进展方面帮助非常巨大。 - Brian

0
我错误地认为我的newBA对象上的“toString”方法会将其转换为可读输出,但显然我错了。在Java中,一个人应该如何知道该走哪条路呢?
你可以将这里的“Java”替换为你不懂/未掌握的语言。即使你在某种特定语言上工作了10年,你仍然会得到那些“啊哈!原来是这样!”的效果,尽管不像刚开始那么频繁。
你需要学习的重点是,toString()并不返回你想要/期望的表示形式,而是实现者选择的任何表示形式。toString()的默认实现如下(javadoc):
返回对象的字符串表示形式。通常,toString方法返回一个“文本表示”此对象的字符串。结果应该是简洁但信息丰富的表示,易于人们阅读。建议所有子类都覆盖此方法。
Object类的toString方法返回一个字符串,由对象所属类的名称、@字符和对象哈希码的无符号十六进制表示组成。换句话说,此方法返回等于以下值的字符串:
getClass().getName() + '@' + Integer.toHexString(hashCode())

0

我也是一个开发新手。对于当前的问题,我建议阅读David Bishop所著的书籍《Java Applets密码学入门。它展示了你需要的内容等等...


0
有什么建议可以让我不用一直Google如何做某事而变得更好吗?
首先,不要从MD5哈希器开始!认真地逐步完成一些程序,这些程序可以在不担心像MD5这样的特定领域问题的情况下完成。
如果你把所有东西都倒入主函数中,那么你就不是在编写Java程序了。
在这种规模的程序中,你的main()应该只做一件事:创建一个MD5Hasher对象,然后调用一些方法。你应该有一个构造函数,它接受一个初始字符串,一个“执行工作”的方法(更新、摘要),以及一个打印结果的方法。
获取一些教程,并花时间进行简单的传统练习(斐波那契生成器、解决一些逻辑谜题的程序),这样你就可以在烦恼库的问题之前理解语言基础。然后你就可以开始做有用的事情了。

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