天道不一定酬所有勤
但是,天道只酬勤
Hollis出品的全套Java面试宝典不来了解一下吗?

Java 7 源码学习系列(三)——BigInteger

Hollis出品的全套Java面试宝典不来了解一下吗?

在java中,有很多基本数据类型我们可以直接使用,比如用于表示浮点型的float、double,用于表示字符型的char,用于表示整型的int、short、long等。但是,拿整数来说,如果我们想要表示一个非常大的整数,比如说超过64位,那么能表示数字最大的long也无法存取这样的数字时,我们怎么办。以前的做法是把数字存在字符串中,大数之间的四则运算及其它运算都是通过数组完成。JDK也有类似的实现,那就是BigInteger

什么是BigInteger(定义)

BigInteger类的基本结构如下所示:

java.lang.Object
 |_java.lang.Number
  |_java.math.BigInteger

BigInteger已实现的接口:Serializable, Comparable

类定义如下:

public class BigInteger extends Number implements Comparable<BigInteger>{}

BigInteger是不可变的任意精度的整数。所有操作中,都以二进制补码形式表示 BigInteger(如 Java 的基本整数类型)。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。

属性

下面看看BigInteger有哪些重点的属性,主要的有下面两个:

final int signum

signum属性是为了区分:正负数和0的标志位,整数用1表示,负数用-1表示,零用0表示。

final int[] mag

mag是magnitude的缩写形式,mag数组是存储BigInteger数值大小的,采用big-endian的顺序,也就是高位字节存入低地址,低位字节存入高地址,依次排列的方式。

我们来分析一下为什么BigInteger中要有这两个成员变量。 我们知道,BigInteger存储大数的方式就是将数字存储在一个整型的数组中(具体怎么存,后面有谈),这样就能解决可以存很多很多位数字的问题。那么,只用一个整型数组的话,如何表示一个整数的正负呢?那么就需要有一个单独的成员变量来标明该数的正负。

构造函数

public BigInteger(byte[] val) {
    if (val.length == 0)
        throw new NumberFormatException("Zero length BigInteger");
    if (val[0] < 0) {
        mag = makePositive(val); //这个函数的作用是将负数的byte字节数组转换为正值。
        signum = -1; //如果数组第一个值为负数,则将数组变正存入mag,signum赋-1
    } else {
        mag = stripLeadingZeroBytes(val);//如果非负,则可直接去掉前面无效零,再赋给mag
        signum = (mag.length == 0 ? 0 : 1);
    }
}

将包含 BigInteger 的二进制补码表示形式的 byte 数组转换为 BigInteger。输入数组假定为 big-endian 字节顺序:最高有效字节在第零个元素中。

再来看另外一种构造BigInteger的方式:public BigInteger(String val) 这个构造函数接收一个字符串,然后直接将字符串转换成BigInteger类型。

public static void main(String[] args) {
    BigInteger bigInteger = new BigInteger("123456789987654321123456789987654321123456789987654321");
    System.out.println(bigInteger);
}

这看起来很方便,只要我们明确的知道我们想要的数字的字符串形式,就可以直接用他构造一个BigInteger

接着,我们就分析一下这个函数是怎么实现的,难道只是把我们传入的字符串直接存到mag数组里面了么?以下是该构造函数的实现:

 public BigInteger(String val) {
    this(val, 10);
}

这个函数调用了另外一个构造方法,那么我们就直接分析这个构造方法: public BigInteger(String val, int radix)
该构造函数就是把一个字符串val所代表的的大整数转换并保存mag数组中,并且val所代表的字符串可以是不同的进制(radix决定),比如,我们这样构造一个BigInteger:BigInteger bigInteger = new BigInteger("101",2);,那么我们得到的结果就是5。
分析该构造函数源码之前,先想一个问题,构造一个大整数开始最主要的问题是如何把一个大数保存到mag数组中,通常我们自己实现的话很有可能是数组每块存一位数(假设大数为10进制),但这样的话想想也知道太浪费空间,因为一个int值可以保存远不止一位十进制数. Java语言里每个int值大小范围是-2^31至2^31-1-2147483648~2147483647,因此一个int值最多可保存一个10位十进制的整数,但是为了防止超出范围(2222222222这样的数int已经无法存储),保险的方式就是每个int保存9位的十进制整数.JDK里的mag数组即是这样的保存方式. 因此若一串数为:18927348347389543834934878. 划分之后就为:18927348 | 347389543 | 834934878. mag[0]保存18927348 ,mag[1]保存347389543 ,mag[2]保存834934878 这样划分可以最大利用每一个int值,使得mag数组占用更小的空间.当然这只是第一步.

划分的问题还没有说完,上述构造函数能够支持不同进制的数,最终转换到mag数组里面的数都是十进制,那么不同进制的大数,每次选择划分的位数就不相同,若是2进制,每次就可以选择30位来存储到一个int数中(int值大小范围是-2^31至2^31-1),若是3进制3^19<2147483647<3^20,因此每次就可以选择19位来存储到一个int数中,对于不同进制每次选择的位数不同,因此需要有一个数组来保存不同进制应当选择的位数,于是就有:

private static int digitsPerInt[] = {0, 0, 30, 19, 15, 13, 11,  
       11, 10, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6,  
       6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5}; 

该数组保存了java支持的最大至最小进制所对应的每次划分的位数

该构造方法里还包含了一个相关的数组bitsPerDigit,该数组用于计算初始化mag数组的大小.

  private static long bitsPerDigit[] = { 0, 0,  
       1024, 1624, 2048, 2378, 2648, 2875, 3072, 3247, 3402, 3543, 3672,  
       3790, 3899, 4001, 4096, 4186, 4271, 4350, 4426, 4498, 4567, 4633,  
       4696, 4756, 4814, 4870, 4923, 4975, 5025, 5074, 5120, 5166, 5210,  
                                          5253, 5295};  

“bitsPerDigit是用于计算radix进制m个有效数字 转换成2进制所需bit位[假设所需x位],我们来看一个计算式:radix^m - 1 = 2^x - 1, 解这个方程得 x = m * log2(radix) , 现在m是几位有效数字,常量就只有 log2(radix),这是一个小数,这不是我们喜欢的,所以我们希望用一个整数来表示,于是我们把他扩大1024倍然后取整,例如3进制 bitsPerDigit[3][3] = 1624(我用计算器算了一下 x = log2(3) * 1024 ~= 1623.xxx) ,我们队这个数取整,为什么取1624呢,其实只要不超过太多都可以的,你可以设置为1620,1600,1610...;”

也就是说对于一串数(N进制),其转换成二进制的位数再乘以1024就是bitsPerDigit数组里面对应的数据,乘以1024再取整可能让人看着舒服吧.

有了以上的介绍之后,我们现在可以贴上该方法的源代码仔细看看.

public BigInteger(String val, int radix) {  

        int cursor = 0, numDigits;  
        int len = val.length();//获取字符串的长度  

        //不符合条件的情况  
        if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)  
            throw new NumberFormatException("Radix out of range");  
        if (val.length() == 0)  
            throw new NumberFormatException("Zero length BigInteger");  
        //判断正负,处理掉字符串里面的"-"  
        signum = 1;  
        int index = val.lastIndexOf("-");  
        if (index != -1) {  
            if (index == 0) {  
                if (val.length() == 1)  
                    throw new NumberFormatException("Zero length BigInteger");  
                signum = -1;  
                cursor = 1;  
            } else {  
                throw new NumberFormatException("Illegal embedded minus sign");  
            }  
        }  
        //跳过前面的0  
        while (cursor < len &&  
                Character.digit(val.charAt(cursor),radix) == 0)  
        cursor++;  
        if (cursor == len) {//若字符串里全是0,则存储为ZERO.mag  
            signum = 0;  
            mag = ZERO.mag;  
            return;  
        } else {//numDigits为实际的有效数字  
            numDigits = len - cursor;  
        }  
        //numDigits位的radix进制数转换为2进制需要多少位  
        //bitsPerDigit数组里面的元素乘了1024这里就需要右移10位(相当于除以1024),做除法的时候会有  
        //小数的丢失,因此加1确保位数一定够  
        //一个int有32bit,因此除以32即是我们开始估算的mag数组的大小  
        int numBits = (int)(((numDigits * bitsPerDigit[radix]) >>> 10) + 1);  
        int numWords = (numBits + 31) /32;  
        mag = new int[numWords];  
        //开始按照digitsPerInt截取字符串里的数  
        //将不够digitsPerInt[radix]的先取出来转换  
        int firstGroupLen = numDigits % digitsPerInt[radix];  
        if (firstGroupLen == 0)  
            firstGroupLen = digitsPerInt[radix];  
        //把第一段的数字放入mag数组的最后一位  
        String group = val.substring(cursor, cursor += firstGroupLen);  
            mag[mag.length - 1] = Integer.parseInt(group, radix);  
        if (mag[mag.length - 1] < 0)  
            throw new NumberFormatException("Illegal digit");  
        //剩下的一段段转换  
        int superRadix = intRadix[radix];  
        int groupVal = 0;  
        while (cursor < val.length()) {  
            group = val.substring(cursor, cursor += digitsPerInt[radix]);  
            groupVal = Integer.parseInt(group, radix);  
            if (groupVal < 0)  
            throw new NumberFormatException("Illegal digit");  
                destructiveMulAdd(mag, superRadix, groupVal);  
        }  
        mag = trustedStripLeadingZeroInts(mag);  

    }  

现在我对最后的几行还没有分析,是因为有一个intRadix数组我们还没有解释.intRadix数组其实就是一个保存了对应各种radix的最佳进制的表, 上面我们说过了对于十进制我们选择一次性截取9位数,这样能充分利用一个int变量同时还可保证不超出int的范围,因此intRadix[10]=10^9=1000000000. intRadix[3]=3^19=1162261467. 也就是每次截取的数都不会超过其radix对应的最佳进制.举例 十进制数18927348347389543834934878 其最终转换为:

    18927348*(10^9)^2 +347389543*(10^9)+834934878,最终从整体上来看mag数组保存的是一个10^9进制的数.

intRadix如下:

 private static int intRadix[] = {0, 0,  
        0x40000000, 0x4546b3db, 0x40000000, 0x48c27395, 0x159fd800,  
        0x75db9c97, 0x40000000, 0x17179149, 0x3b9aca00, 0xcc6db61,  
        0x19a10000, 0x309f1021, 0x57f6c100, 0xa2f1b6f,  0x10000000,  
        0x18754571, 0x247dbc80, 0x3547667b, 0x4c4b4000, 0x6b5a6e1d,  
        0x6c20a40,  0x8d2d931,  0xb640000,  0xe8d4a51,  0x1269ae40,  
        0x17179149, 0x1cb91000, 0x23744899, 0x2b73a840, 0x34e63b41,  
        0x40000000, 0x4cfa3cc1, 0x5c13d840, 0x6d91b519, 0x39aa400  
    };  

intRadix[10]=0x3b9aca00 = 1000000000; intRadix[3]=0x4546b3db=1162261467;

我们注意到 numWords = (numBits + 31) /32. 初始数组的大小并不是大整数划分的数目而是将计算大整数对应的二进制位数(加上31确保numWords大于0)然后除以32得到,因此mag数组中每一个int数的32位是被完全利用的,也就是把每个int数当成无符号数来看待.若不完全利用int的32位的话,我们完全可以根据划分的结果来确定mag数组的初始大小,之前的例子:18927348 | 347389543 | 834934878,我们知道10进制数每次选择9位不会越界,我们可以直观的得到mag数组的大小为3,但是这样的话每个int元素仍然有些空闲的位没有利用.

因此我们之前的划分方法只是整个数组初始化的想象中第一步. 这个例子按照numWords = (numBits + 31) /32这样计算最后得到的应当仍是3.但是若是再大一些的数串结果就不一定一样,积少成多,很大的数串时节省的空间就能体现出来啦.

Java没有无符号int数,因此mag数组中常常会符号为负的元素. 而最终把原大整数转换为mag数组保存的radix对应的最佳进制数的过程由destructiveMulAdd完成.现在把构造函数的最后一部分的和方法destructiveMulAdd的解析附上:

int superRadix = intRadix[radix];  
        int groupVal = 0;  
        while (cursor < val.length()) {  
            //选取新的一串数  
            group = val.substring(cursor, cursor += digitsPerInt[radix]);  
            groupVal = Integer.parseInt(group, radix);//转换为十进制整数  
            if (groupVal < 0)  
            throw new NumberFormatException("Illegal digit");  
            //mag*superRadix+groupVal.类似于:18927348*10^9+347389543  
                destructiveMulAdd(mag, superRadix, groupVal);  
        }  
        //去掉mag数组前面的0,使得数组元素以非0开始.  
        mag = trustedStripLeadingZeroInts(mag);  

   private final static long LONG_MASK = 0xffffffffL;  
    // Multiply x array times word y in place, and add word z  
    private static void destructiveMulAdd(int[] x, int y, int z) {  
        // Perform the multiplication word by word  
        //将y与z转换为long类型  
        long ylong = y & LONG_MASK;  
        long zlong = z & LONG_MASK;  
        int len = x.length;  

        long product = 0;  
        long carry = 0;  
        //从低位到高位分别与y相乘,每次都加上之前的进位,和传统乘法一模一样.  
        for (int i = len-1; i >= 0; i--) {  
            //每次相乘时将x[i]转换为long,这样其32位数就可转变为其真正代表的数  
            product = ylong * (x[i] & LONG_MASK) + carry;  
            //x[i]取乘积的低32位.  
            x[i] = (int)product;  
            //高32位为进位数,留到下次循环相加  
            carry = product >>> 32;  
        }  

        // Perform the addition  
        //执行加z  
        //mag最低位转换为long后与z相加  
        long sum = (x[len-1] & LONG_MASK) + zlong;  
        //mag最低位保留相加结果的低32位.  
        x[len-1] = (int)sum;  
        //高32位当成进位数  
        carry = sum >>> 32;  
        //和传统加法一样进位数不断向高位加  
        for (int i = len-2; i >= 0; i--) {  
            sum = (x[i] & LONG_MASK) + carry;  
            x[i] = (int)sum;  
            carry = sum >>> 32;  
        }  
    }  

整个过程下来,因为保存的方法和我们脑海中那简单的存储方法会有不同,最终mag数组里的元素跟原先的字符串就会有很大的不同,但实质上还是表示着相同的数,现把18927348347389543834934878例子的构造过程展示出:

初始化之后计算得numBits=87,这样数组初始化大小numWords=3. 进入最终的循环前mag数组:[0] [0] [18927348] 第一次循环后: [0] [4406866] [-1295432089] (1892734810^9+347389543) 第二次循环后: [1026053] [-1675546271] [440884830]. ((1892734810^9+347389543)*10^9+834934878) 最终我们就把18927348347389543834934878 转换成10^9进制的数保存到了mag数组中.虽然最终的结果我们让我们不太熟悉,但是其中数串划分的方法和数组节省空间的思想都是值得学习的

现在有最后一个问题,如何mag数组转换为原来的数串呢?JDK里面是通过不断做除法取余实现的,BigInteger类的实例在调用toString方法的时候会返回原先的数串.代码如下:

 public String toString(int radix) {
        if (signum == 0)
            return "0";
        if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
            radix = 10;

        // Compute upper bound on number of digit groups and allocate space
        int maxNumDigitGroups = (4*mag.length + 6)/7;
        String digitGroup[] = new String[maxNumDigitGroups];

        // Translate number to string, a digit group at a time
        BigInteger tmp = this.abs();
        int numGroups = 0;
        while (tmp.signum != 0) {
            BigInteger d = longRadix[radix];

            MutableBigInteger q = new MutableBigInteger(),
                              a = new MutableBigInteger(tmp.mag),
                              b = new MutableBigInteger(d.mag);
            MutableBigInteger r = a.divide(b, q);
            BigInteger q2 = q.toBigInteger(tmp.signum * d.signum);
            BigInteger r2 = r.toBigInteger(tmp.signum * d.signum);

            digitGroup[numGroups++] = Long.toString(r2.longValue(), radix);
            tmp = q2;
        }

        // Put sign (if any) and first digit group into result buffer
        StringBuilder buf = new StringBuilder(numGroups*digitsPerLong[radix]+1);
        if (signum<0)
            buf.append('-');
        buf.append(digitGroup[numGroups-1]);

        // Append remaining digit groups padded with leading zeros
        for (int i=numGroups-2; i>=0; i--) {
            // Prepend (any) leading zeros for this digit group
            int numLeadingZeros = digitsPerLong[radix]-digitGroup[i].length();
            if (numLeadingZeros != 0)
                buf.append(zeros[numLeadingZeros]);
            buf.append(digitGroup[i]);
        }
        return buf.toString();
    }

private static String zeros[] = new String[64];
    static {
        zeros[63] =
            "000000000000000000000000000000000000000000000000000000000000000";
        for (int i=0; i<63; i++)
            zeros[i] = zeros[63].substring(0, i);
    }

上述方法核心的地方就是 a.divide(b, q, r). longRadix数组和intRadix数组有着相似的涵义.

intRadix[10]=10^9.因此longRadix[10]=10^18,相当于对intRadix进行了平方,也就是对long类型来说的最佳进制数.

简单的想一下可以明白:mag数组若是不断除以10^9可以得到834934878,347389543,18927348最终可获得原先字符串.若是除以10^18(Java支持该数量级的运算),两次分别得到:34738954318927348,834934878,因此使用longRadix数组运算的效率更高. 对于上述方法出现的类MutableBigInteger,借用网上的一段话解释可能比我说的更好些:

"MutableBigInteger是BigInteger类的另一个版本,它的特点是不创建临时对象的前提上使调用程序得到象BigInteger类型的返回值(称为可变对象技术)。因为大整数的除法是由大量的其他算术操作组成的,所以需要大量的临时对象,而完成大量的操作而不创建新的对象可以极大地改善程序的性能,(因为创建对象的代价是很高的)所以在Java的大整数类中使用MutableBigInteger类中的方法来执行大整数除法。"

而最为关键的divide方法不好意思啊我看了好久仍然是没有弄懂代码的思路,希望大家能够指点迷津!

JDK的BigInteger类中还实现了好多方法都值得我们一看,除了基本的四则元素外,里面还提供了判断素数的方法,求幂,求模,求逆元,求最大公约数,用到了Miller-Rabin算法,滑动窗口算法快速求幂(我看了看好像是),欧几里得算法,中国剩余定理等,3000多行的代码....若有兴趣的话仔细看看其中某个方法对我们可能会有启发.

参考:

http://blog.csdn.net/zhaoyunfullmetal/article/details/19325415

赞(1)
如未加特殊说明,此网站文章均为原创,转载必须注明出处。HollisChuang's Blog » Java 7 源码学习系列(三)——BigInteger
Hollis出品的全套Java面试宝典不来了解一下吗?

评论 7

  1. #1

    不错,不错,[undefined]

    lighters8年前 (2016-10-19)回复
  2. #2

    太 6了。。

    lighters8年前 (2016-10-20)回复
  3. #3

    分享图片

    云深处7年前 (2017-10-09)回复
  4. #4

    kokk点xyz 狠友福利

    窒息梦6年前 (2018-04-29)回复
  5. #5

    加油更

    yameimei6年前 (2018-05-01)回复
  6. #6

    kekk点xyz 狠ff友ff福ff利

    熟悉的脸庞6年前 (2018-06-05)回复
  7. #7

    写的很通俗易懂,感谢分享

    tlanyan3年前 (2021-06-11)回复

HollisChuang's Blog

联系我关于我