JDK文档中对BigInteger的描述:
不可变的任意精度的整数。所有操作中,都以二进制补码形式表示 BigInteger(如 Java 的基本整数类型)。
按字面理解,BigInteger是一个大整数,有多大呢?可以无穷大!int类型是4个字节,所以int的范围是有限的,而BigInteger能表示的整数范围是无限的。
今天在看到同事发的一个获取文件md5的工具类,发现代码中使用BigInteger来把一个字节数组转换为16进制,看着很爽,所以这里也记录一下:
object FileUtil {
/** 获取文件md5(异步方式) */
fun getFileMd5(file: File, callback: (String) -> Unit) {
thread {
val fileMd5 = getFileMd5(file)
callback(fileMd5)
}
}
/** 获取文件md5(同步方式) */
fun getFileMd5(file: File): String {
val messageDigest = MessageDigest.getInstance("MD5")
FileInputStream(file).use { fis ->
val buf = ByteArray(8192)
var length: Int
while (fis.read(buf, 0, 8192).also { length = it } != -1) {
messageDigest.update(buf, 0, length)
}
}
// 获取md5签名(16个字节)
val md5Bytes = messageDigest.digest()
// 将byte数组的签名转换为16进制的字符串
val bigInteger = BigInteger(1, md5Bytes) // 把16个字节当成一个无符号的大整数
var md5String = bigInteger.toString(16) // 把大整数转换为16进制的字符串形式
repeat(32 - md5String.length) { // 预防字符串不够32位。1个byte需要两位16进制数,而md5Bytes的长度为16,所以需要32位的16进制数来表示。
md5String = "0$md5String"
}
return md5String
}
}
还有一个使用NIO读取文件的方式,但是在映射为MappedByteBuffer之后,即使流都关闭了,但它还是引用着文件,导致文件不能删除和重命名,虽然网上有方法解决,但是没有官方的函数可以解除文件的引用句柄,所以不推荐使用,如下:
fun getFileMd5(file: File): String {
// 获取md5签名
val md5Bytes = FileChannel.open(Paths.get(file.absolutePath), StandardOpenOption.READ).use { channel ->
val byteBuffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size())
MessageDigest.getInstance("MD5").run {
update(byteBuffer)
digest()
}
}
// 将签名转换为16进制的字符串
val bigInteger = BigInteger(1, md5Bytes) // 把16个字节当成一个无符号的大整数
var md5String = bigInteger.toString(16) // 把大整数转换为16进制的字符串形式
repeat(32 - md5String.length) { // 预防字符串不够32位。1个byte需要两位16进制数,而md5Bytes的长度为16,所以需要32位的16进制数来表示。
md5String = "0$md5String"
}
return md5String
}
在Android中,FileChannel.open()函数是在API 26版本才有的,可以使用下面的方式来代替:
FileInputStream(file).channel
一般我们说的short、int、long这些整数都是有大小限制的,分别是2字节、4字节、8字节,而BigInteger你要装多少个字节都可以,所以可以使用BigInteger表示一个16字节的整数,也可以是32字节的整数。。。多少字节的整数都是可以的,所以BigInteger可以表示比long类型还要大的整数。
最基本的就是使用接收String值的构造函数来创建对象,如下:
val bigInteger1 = BigInteger("255") // 以10进制解析字符串为整数
val bigInteger2 = BigInteger("FF", 16) // 以16进制解析字符串为整数
println(bigInteger1.toString(10)) // 输出:255
println(bigInteger2.toString(10)) // 输出:255
使用很简单,第二个构造函数中的16表示字符串中的FF是16进制的值。输出的时候指定的参数10表示以10进制输出。这个示例中写的数值都很小,有点大材小用了,因为这里是介绍BigInteger使用,所以就用小一点的值了,方便理解。
数值在计算机中是以字节的形式保存的,所以BigInteger也可以接收字节数组,它会自动转换为正确的整数,示例如下:
val number = -1
val baos = ByteArrayOutputStream()
val dos = DataOutputStream(baos)
dos.write(number) // 写出1个字节
dos.close()
val bytesOfNumber = baos.toByteArray()
val bigInteger = BigInteger(bytesOfNumber)
println(bigInteger.toString(10))
println(bigInteger.toString(16))
输出结果:
-1
-1
如上面的示例,写出的一个字节为8个1(即二进制:1111 1111),如果最高位用于表示符号位,则8个1的二进制对应的十进制值为-1,如果不需要符号位,则8个二进制位都表示数值,可以使用下面的构造函数:
val bigInteger = BigInteger(1, bytesOfNumber) // 参数1表示这是无符号的正整数
把前面的例子换成这个构造函数,再次运行,结果如下:
255
ff
所以,当我们想查看字节的内容,而不管它是正负数的时候,就可以以这种方式来使用。因为我们看正好数的值比较容易想出它对应的二进制值是什么,比如看到ff就能知道这个字节的8个二进制位都是1,如果是负数比较难想出它对应的二进制位是什么了,因为负数有反码、补码的相关知识。
前面的例子是写出1个字节,我们也可以使用writeInt来写出int的全部4个字节,如下:
val number = -1
val baos = ByteArrayOutputStream()
val dos = DataOutputStream(baos)
dos.writeInt(number) // 以大端的方式写出4个字节
dos.close()
val bytesOfNumber = baos.toByteArray()
val bigInteger = BigInteger(1, bytesOfNumber)
println(bigInteger.toString(10))
println(bigInteger.toString(16))
运行结果如下:
4294967295
ffffffff
从结果ffffffff我们就能轻松的知道,int值的4个字节的二进制位全是1。
最后,再次讲一下最前面的获取文件md5签名的例子,md5的签名是一个byte数组,如果我们把数组中的每一个byte值以10进制值的方式连接成一个字符串,这是很不方便的,因为byte数组的长度为16,也就是说共有16个字节,转换为10进制输出将会是一串非常长的数字,不是很方便。那我们就以16进制输出啊,我们把16个字节内容当成是一个整数,哎,这时BigInteger就派上用场了,如果你用int表示的话,int只能是4个字节啊,而long也只能是8个字节,所以用BigInteger,多少个字节都可以,把16个字节当成一个整数,而且当成无符号的整数,然后再以16进制输出这个整数即可。
关于BigInteger的其它函数功能这就里就不讲解了,看JDK文档就可以了。