二进制是计算机中普遍采用的一种数制,由0和1组成,进位规则为“逢二进一”,如:0001表示十进制中的1,0010表示十进制中的2。二进制拥有大量非学有用的特性,详情参考:百度百科:二进制数。html
在有符号整数中,最高位表示符号位,0表示正数、1表示负数。java
在Java中整型(int)由4个字节(32位)表示,因此一个整型数字(以1024为例)用二进制表示为:程序员
# 下面是一个32位的二进制数,等价于十进制的1024 00000000 00000000 00000100 00000000 # 其换算过程为(0乘以任何数都为0),这里^表示次幂(非按位异或) 0 * 2 ^ 0 + 0 * 2 ^ 1 + 0 * 2 ^ 2 + ... + 1 * 2 ^ 10 == 2 ^ 10 == 1024
二进制运算涉及到原码、反码、补码,其中正数的原码、反码、补码都是同样的,而负数则不一样。负数的反码为符号位不变,其它位取反(0变成1,1变成0),补码则为反码加一。0与正数同样,反码、补码都是其原码0。 而在计算机中,正数由原码(也能够理解为补码,补码原码是同样的)而负数由补码来形式来表示。 计算机中负数使用补码表示的缘由多是因为有符号整数表示范围形成成的。一个无符号字节表示整数范围为:[0, 255]恰好256个整数,而有符号的整数范围则是:[-128, 127],若是是无符号整数能够用[00000000, 11111111]来表示这个范围,而在有符号的整数只能用[11111111, 01111111]但这只能表示到[-127, 127],而没法表示-128。 能够推导一下,若是是-127,它的反码为:10000000,而它的补码则为:10000001,而比这个补码小一位的数字恰好是:10000000,也就变相表示了-128,但其对应的原码在计算机中没法表示,因此计算机中采用补码表示负数。 上述推断过程来自:http://blog.chinaunix.net/uid-26495963-id-3074603.html编程
二进制实现加法运算,好比在十进制中1 + 2 == 3,而用二进制计算为:性能
00000000 00000000 00000000 00000001 + 00000000 00000000 00000000 00000010 = 00000000 00000000 00000000 00000011 (十进制为3)
二进制减法,实际是将减法转换为正数与负数相加,实际还是加法运算,如在十进制中1 - 2 == -1,用二进制计算为:测试
# 这里须要先找到1的补码,由于计算项中有负数(计算机中负数使用补码表示):1 + (-2) 00000000 00000000 00000000 00000001 + 11111111 11111111 11111111 11111110 (原码:10000000 00000000 00000000 00000010,反码:11111111 11111111 11111111 11111101) == 11111111 11111111 11111111 11111111 # 将计算结果(补码)转换为反码:11111111 11111111 11111111 11111110, # 再转换为原码:10000000 00000000 00000000 00000001, # 即十进制中的:-1
所谓进制就是人们用来计数的方法,好比十进制,逢十进一;二进制,逢二进一,经常使用的还有八进制、十六进制。优化
“数制”只是一套符号系统来表示指称“量”的多少,不管“数制”怎么变或者使用任意一种“数制”,其所表示“量”还是一致的,这一点是进制转换的基础。任意一种进制的基本表示方法能够以以下形式表示(负进制不予考虑):ui
n位的R进制中的数位排列是这样的:R^n-1 R^n-2 ... R^2 R^1 R^0
以十进制为例:1024 == 1 * 10^3 + 0 * 10^2 + 2 * 10^1 + 4 * 10^0 == 1000 + 0 + 20 + 4 而二进制则是一个道理:0000 1101 == 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 == 8 + 4 + 0 + 1 == 13.net
关于进制转换一种十分常见的方法是“短除法”,按进制整除取余,将每次余数倒排即为转换后的进制表示结果,如:十进制整数17转换为二进制unix
# 当整除结果为0时计算结束 17 / 2 == 8,余1 8 / 2 == 4,余0 4 / 2 == 2,余0 2 / 2 == 1, 余0 1 / 2 == 0,余1 # 余数倒排即为(前面补0):00010001 # 00010001 == 1 * 2^4 + 1 == 16 + 1 == 17
下面是一段使用Java实现的进制转换代码
// 以十进制数字 1024 为例,测试进制间转换 int number = 1024; // 使用短除法转换为二进制,短除法转换时除数为进制值,这里是二进制,因此是2 // 当整数被除尽后,余数倒排序即为转换后的二进制数字,由于要倒排,这里采用栈结构存储余数 Stack<Integer> stack = new Stack<>(); int m = number; while (m != 0) { // 整除后的余数 stack.push(m % 2); // 整除后的结果 m /= 2; } // 将数据取出,组成二进制序列 // 10000000000 while (!stack.isEmpty()) { System.out.print(stack.pop()); } System.out.println(); // 测试结果是否正确(后面的项全是0,因此省略了) assertEquals(number, 1 * (int) (Math.pow(2, 10)));
十进制转换为八进制、十六进制的方法是同样的,但须要注意的是十六进制中[10-16]由英文字母[a-f]表示,因此自定义实现进制转换时须要特殊处理。而其它进制间转换一般先转换为十进制,再转换为目标进制,因此转换过程是一致的。
在Java中,JDK默认提供了一系列进制转换方法
// Integer提供的进制转换方法 // 十进制转换二进制 assertEquals("10000000000", Integer.toBinaryString(1024)); // 十进制转换八进制 assertEquals("2000", Integer.toOctalString(1024)); // 十进制转换十六进制 assertEquals("400", Integer.toHexString(1024)); assertEquals("40c", Integer.toHexString(1036)); // 其它进制转换为十进制 assertEquals(1024, Integer.parseInt("10000000000", 2)); assertEquals(1024, Integer.parseInt("2000", 8)); assertEquals(1024, Integer.parseInt("400", 16)); // 字符串格式化使用占位符进行进制转换 // 1024, 2000, 400,没有二进制格式化方法 System.out.println(String.format("%d, %o, %x", 1024, 1024, 1024, 1024)); // 1024, 02000, 0x400 System.out.println(String.format("%d, %#o, %#x", 1024, 1024, 1024, 1024));
程序中全部数据最终都是以二进制存储的,而位运算则是针对二进制直接进行计算的一种方法,一般性能会很是高(目前CPU对加法作了优化,能够达到二进制运算的性能,但乘法仍差一些)。 Java中提供了一组位运算符,分别是:按位与&
、按位或|
、按位异或^
、按位取反~
、左移<<
、右移>>
以及无符号右移>>>
,下面分别演示其用法及效果。
&
仅当两个操做数都是1时,输出结果才为1,不然为0
// 比较数二进制长度不等时前面补0对齐 // 12 -> 1100 // 5 -> 0101 // 12 & 5 = 0100 0 4 assertEquals(4, 12 & 5); // 使用二进制写法测试 assertEquals(0b0100, 0b1100 & 0b0101);
|
仅当两个操做数都是0时,输出结果才为0,不然为1
assertEquals(13, 12 | 5); assertEquals(0b1101, 0b1100 | 0b0101);
^
仅当两个操做数不一样时,输出结果才为1,不然为0
assertEquals(9, 12 ^ 5); assertEquals(0b1001, 0b1100 ^ 0b0101);
~
这是一个单目运算符,用于将所有数字取反:0为1,1为0。将一个二进制数按位取反,等效于-1减去该数,如:~12 == -13
// 注意,上面都是简写的形式,前面的0未写出来,但取反时要写全,不然结果是错的,int类型是32位 // 下面目标数字实际是:0000000000000000000000000001100,按位取反:11111111111111111111111111110011 // 注意二进制左边第一位是0,表示无符号整数(为1表示负数,即有符号整数),要使用Integer.parseUnsignedInt()方法转换 assertEquals(0b11111111111111111111111111110011, ~0b1100); // ~12 == -1 - 12 == -13 assertEquals(-13, ~12); // ~-12 == -1 - (-12) == -1 + 12 == 11 assertEquals(11, ~-12);
<<
把一个数的所有位都向左移动指定位数。其实际效果为:左移n位就是乘以2的n次方
// 12 -> 1100:1100 << 0 == 1100 == 12 == 12 * 2 ^ 0 assertEquals(24, 12 << 1); // 12 -> 1100:1100 << 1 == 11000 == 24 == 12 * 2 ^ 1 assertEquals(24, 12 << 1); // 12 -> 1100:1100 << 2 == 110000 == 48 == 12 * 2 ^ 2 assertEquals(48, 12 << 2); // 12 -> 1100:1100 << 3 == 1100000 == 96 == 12 * 2 ^ 3 assertEquals(96, 12 << 3);
>>
把一个数的所有位都向右移动指定位。其实际效果为:右移n位就是除以2的n次方结果取整
// 1100 >> 1 == 0110 == 6 assertEquals(6, 12 >> 1); // 1100 >> 2 == 0011 == 3 assertEquals(3, 12 >> 2); // 1100 >> 3 == 0001 == 1 assertEquals(1, 12 >> 3); // 1100 >> 4 == 0000 == 0 assertEquals(0, 12 >> 4); // 100000 >> 5 == 000001 == 1 assertEquals(1, 32 >> 5); assertEquals(2, 32 >> 4); assertEquals(4, 32 >> 3); assertEquals(8, 32 >> 2); assertEquals(16, 32 >> 1); assertEquals(32, 32 >> 0);
>>>
与右移相似,可是符号位仍用原值填充
// 00000000000000000000000000001100 >>> 1 == 00000000000000000000000000000110 // => 110 == 6 assertEquals(12, 12 >>> 0); assertEquals(6, 12 >>> 1); assertEquals(3, 12 >>> 2); assertEquals(1, 12 >>> 3); assertEquals(0, 12 >>> 4); assertEquals(0, 12 >>> 5); // 11111111111111111111111111110100 >>> 1 == 01111111111111111111111111111010 assertEquals(-12, -12 >>> 0); assertEquals(2147483642, -12 >>> 1); assertEquals(1073741821, -12 >>> 2); assertEquals(536870910, -12 >>> 3); // 标志位使用原标志位填充,因此仍是1 // 11111111111111111111111111110100 >> 1 == 11111111111111111111111111111010 assertEquals(-6, -12 >> 1); // 10111111111111111111111111110100 >> 1 == 11011111111111111111111111111010 assertEquals(-536870918, -1073741836 >> 1); assertEquals(-1073741836, 0b10111111111111111111111111110100); assertEquals(-536870918, 0b11011111111111111111111111111010);
二进制及进制转换做为编程的基础知识,每一个技术人员都应该掌握并熟练应用,尤为是位运算能极大提高程序性能,高级程序员更应精通此道(笔者实际也不擅长,因此这里属于补课)。 本篇内容本来是放后面再写的,但在研究HashMap源码过程当中发现大量使用了位运算,因此不得不提早整理出来。