前言

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

BigInteger的基本使用

一般我们说的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进制输出这个整数即可。