Java BigInteger源码分析

Java BigInteger源码分析

题外话

最近一直碰到RSA加密,不可避免的涉及到大数运算,这在Python中不是问题,但是在Java和JS中,可能需要了解一下其底层实现。
原因还是在于我使用Python加密的结果整了一会才在Java中复现。
Java中RSA加密

String pubkeyStr = "010001111111111111111";
String modulusStr = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
Cipher cipher = Cipher.getInstance("RSA/ECB/NoPadding");
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(new BigInteger(modulusStr, 16),
                new BigInteger(pubkeyStr, 16));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey key = keyFactory.generatePublic(publicKeySpec);
String msg = "aabc";
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] bytes = cipher.doFinal(msg.getBytes(StandardCharsets.UTF_8));
System.out.println(new BigInteger(1, bytes));

关键还是在于BigInter实现原理。

底层数据结构

简单来说,BigInteger的底层是利用一个int的数组(称为mag)存储大整数,也就是说,当要存放的整数大于32位时,就会被分割成32位为一组的形式,每一组就作为底层数组的一个元素。并且,BigInteger的底层数组mag时大端存放的,也就是说mag[0]、mag[1]…mag[mag.length - 1]分别代表整数的最高32位、次高32位…最低32位。

并且, BigInteger的mag数组仅仅用来存放绝对值的二进制位,其符号被signum存放,signum为0即代表0;signum为1即代表正数、signum为-1即代表负数
为什么mag数组中符号位要单独拎出来?就是说为什么大数不用补码表示,每个元素的符号位其实不参与表示大数。

  • 不用补码存储可以按照我们传统的计算思路完成运算
  • 如果采用补码,乘除等操作将会变得很复杂,并且,获取相反数、绝对值等算法的复杂度也会由常数变为线性

几个常量数组

只列出了int型的,long类型原理相同。

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};
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};

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
};

bitsPerDigit

这个数组保存的是相应的进制对应的二进制位数乘以1024
bitsPerDigit[2] = 1024,表示 ‘ ` \log_2 21024 ‘ `
bitsPerDigit[3] = 1624,表示 ‘ ` \log_2 3
1024 ‘ `
依此类推,都是向上取整,毕竟可以忍受多分配空间,但绝不能少分配。

digitsPerInt

Java中int类型8个字节,也就是32位,以补码存放,数据位31位,表示的最大整数为 2 31 − 1 2^{31}-1 2311
digitsPerInt表示mag数组中每个int元素可以表示对应的进制位多少位。
digitsPerInt[2] = 30表示,mag数组中每个int元素最多表示30位二进制数。
digitsPerInt[3] = 19,因为$$3^{19} \lt 2^{31-1} \lt 3^{20}$$
依此类推。

intRadix

该数组表示了大数每个位之间的进制,$$2^{30}=0x40000000$ , , $3^{19}=0x4546b3db$$,....
假如大数存储的是三进制数,那么一位int元素存储19位三进制数,满0x4546b3db就向前一位进1

从字符串构造

以"010001"为例

// new BigInteger("010001", 16)
/* 省略除去前导0,正负号判断的代码 */
...
/* 除去前导0后,"10001",numDigits=5位数,16进制,需要bit数为20,+1保证无法整除情况下向上取整 */
long numBits = ((numDigits * bitsPerDigit[radix]) >>> 10) + 1;
/* new int[Integer.MAX_VALUE]也无法存储该大数,报错 */
if (numBits + 31 >= (1L << 32)) {
	reportOverflow();
}
/* mag数组长度 */
int numWords = (int) (numBits + 31) >>> 5;
int[] magnitude = new int[numWords];

/* 处理mag第一位,可能比其他位短 */
/* digitsPerInt[16]=7 */
int firstGroupLen = numDigits % digitsPerInt[radix];
if (firstGroupLen == 0)
	firstGroupLen = digitsPerInt[radix];
String group = val.substring(cursor, cursor += firstGroupLen);
magnitude[numWords - 1] = Integer.parseInt(group, radix);
if (magnitude[numWords - 1] < 0)
	throw new NumberFormatException("Illegal digit");

/* 处理剩下的位,spuerRaidx为16的7次方 */
int superRadix = intRadix[radix];
int groupVal = 0;
/* 每7位截取,按对应进制转化成int */
while (cursor < len) {
	group = val.substring(cursor, cursor += digitsPerInt[radix]);
	groupVal = Integer.parseInt(group, radix);
	if (groupVal < 0)
		throw new NumberFormatException("Illegal digit");
	/* 处理进位 */
	destructiveMulAdd(magnitude, superRadix, groupVal);
...

这里说一下destructiveMulAdd这个函数,类似我们做字符串转正整数时的进位操作

String s = "123";
int ret = 0;
int len = s.length();
for (int i = 0; i < len; i++){
    ret = ret * 10 + (s.charAt(i) - 48);
}

二者都是从正序处理字符串。
我们对每一位乘进制10再加上新增的一位,destructiveMulAdd对数组的每一位乘进制$$16^7$$再加groupVal。

从字节数组构造

首先考虑正数的情况
这个比上面就简单了,字节存储的就是补码,int存储的也是补码。事实上只需要把字节拼起来就行了。

/* keep为去除前导0后的数组索引位置 */
/* 一个字节8为,int32位,mag数组长度即为字节数/4,即>>>2 */
int intLength = ((indexBound - keep) + 3) >>> 2;
int[] result = new int[intLength];
int b = indexBound - 1;

/* 逆序处理 */
for (int i = intLength-1; i >= 0; i--) {
	/* result[i]只有8位,可存储32位,需要继续填充 */
	result[i] = a[b--] & 0xff;
        /* 剩下可填充字节数 */
	int bytesRemaining = b - keep + 1;
	/* 已经8位,最多可再右移3*8=24位 */
	int bytesToTransfer = Math.min(3, bytesRemaining);
	for (int j=8; j <= (bytesToTransfer << 3); j += 8)
		result[i] |= ((a[b--] & 0xff) << j);
}

因为在Java中用int数组存储,所以看起来这部分代码才多一点,说白了,字节数组转整数就下面几行py代码

s = '010001'
t = [ord(x) for x in s]
r, p = 0, 0
for x in t[::-1]:
    r += x << (p * 8)
    p += 1
print(r)

当然,上面这是没有考虑符号的情况下。
倘若未指定符号,字节数组第一位的符号为大数的符号。
类比计算机存储的方式,补码是符号位和数值一起存放的,而现在大数存储符号和数值是分开的,那么我们需要的应该是拼接字节后的二进制数的真值
这么说太抽象,举个例子就好了

byte[] b = {(byte) 0xff, (byte) 0x00, (byte) (0xf1), (byte) 0x44};
/* -16715452 */
System.out.println(new BigInteger(b));

b数组拼接字节后为:
1111_1111_0000_0000_1111_1111_0001_0100_0100
我们并不能粗暴的把大数的值当成 这个数前面加个符号负号,应该是它去掉符号后再加负号,这是补码,它的真值是:
000_0000_1111_1111_0000_0000_1110_1011_1100
那么实际大数的结果就是-1111_1111_0000_0000_1110_1011_1100,正是16715452的二进制

关于大数存储的总结:

符号位单独存放,mag数组的二进制表示的是大数的真值,存储形式为int。
使用字符串构造函数时,默认为正数,只需将字符串表示的二进制数拼接即为表示的大数
使用字节数组时,若指定signum,符号为正,将所有字节拼接即为大数;
若不指定signum,当字节数组的第一个元素的二进制数符号位为1时,符号为负,同时拼接字节的真值的相反数为大数的值。
假如拼接后的字节为
1111_1111_0000_0000_1111_1111_0001_0100_0100
实际mag存储的为
0000_0000_1111_1111_0000_0000_1110_1011_1100(前导0只是为了方便对比)

BigInteger加减法

这篇知乎文章已经说得很清楚了BigInteger的加减法
算法思想和leetocde这道题相似

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值