Java的运算符有赋值运算符、算术运算符、比较运算符、逻辑运算符、位运算符、复合赋值运算符。今天主要学习下位运算和位运算的常见使用场景,整数数值类型运算时类型溢出、浮点数值类型运算时精度丢失,导致运算结果异常的原因和解决办法。
位运算
位运算是按照整数的二进制位进行移位、与、或、取反、异或的运算。除按位与和按位或运算符外,其他只能用于处理整数的操作。整数数据在内存中以二进制的形式进行表示,如7的二进制表示为00000000 00000000 00000000 00000111共32位,左边最高位的是符号位,最高位是0则表示正数,若为1则表示负数,负数采用补码表示。
正数:原码=反码=补码
反码:原码中除最高位(符号位)以外每一位取反;
补码:反码 + 1;
按位与 &
按位与为双目运算符,符号为 &。运算规则:两个整型数据的二进制对应位都为1则结果对应位为1否则为0。若两个数精度不同则结果与精度高的数相同。
public class b01 { //有0为0,无0为1
public static void main(String[] args) {
int x = 35; //100011
int y = 43; //101011
int t = x & y; //100011
System.out.println(t);//35
}
}
按位或 |
按位或为双目运算符,符号为 |。运算规则:两个整型数据的二进制对应位都为0则结果对应位为0否则为1。若两个数精度不同则结果与精度高的数相同。
public class b01 { //有1为1,无1为0
public static void main(String[] args) {
int x = 35; //100011
int y = 43; //101011
int t = x | y; //101011
System.out.println(t);//43
}
}
按位异或 ^
按位异或为双目运算符,符号为 ^。运算规则:两个整型数据的二进制对应位都相同则结果对应位为0否则为1。若两个数精度不同则结果与精度高的数相同。
public class b01 { //相同为0,不同为1
public static void main(String[] args) {
int x = 35; //100011
int y = 43; //101011
int t = x ^ y; //001000
System.out.println(t);//8
}
}
按位取反(非) ~
按位非为单目运算符,符号为 ~。运算规则:当前操作整数的二进制表示0 —>1,1 —>0。若两个数精度不同则结果与精度高的数相同。
public class b01 { //0和1 互换
public static void main(String[] args) {
int x = 167776589; //00001010 00000000 00010001 01001101
int t = ~x; //11110101 11111111 11101110 10110010
System.out.println(t);//-167776590
}
}
左移 <<
运算规则:将运算符左边的操作数的二进制,按照运算符右边操作数指定的位数向左移动,右边移空的部分补0。在数值计算中相当于 。
public class b01 {
public static void main(String[] args) {
int x = 6; // 00000110 = 6
int y = x << 1;// 00001100 = 12
System.out.println(y);//12
int t = x << 3;// 00110000 = 48
System.out.println(t);//48
}
}
带符号右移 >>
运算规则:将运算符左边的操作数的二进制,按照运算符右边操作数指定的位数向右移动,如果最高位是0,右移空的位就填入0,如果最高位是1右移空的位置填入1(原码最高位为符号位不参与右移)。在数值计算中相当于。
public static void main(String[] args) {
//负数右移时
//原码->反码->补码->右移->反码->原码
//右移时:符号位不动,右移补1
int x = -24; // 10000000 00000000 00000000 00011000 = -24
int y = x >> 1;// 10000000 00000000 00000000 000001100 = -12
System.out.println(y);//-12
int t = x >> 3;// 10000000 00000000 00000000 000000011 = -3
System.out.println(t);//-3
}
}
无符号右移 >>>
运算规则:将运算符左边的操作数的二进制,按照运算符右边操作数指定的位数向右,(无论最高位是1还是0)左边被移空的部分都补0。在数值计算中相当于。
public class b01 { //不管最高位是1还是0
public static void main(String[] args) {
int x = 24; // 00000000 00000000 00000000 00011000 = 24
int n = x >>> 1;// 00000000 00000000 00000000 000001100 = 12
System.out.println(n);//14
int y = -24; // 10000000 00000000 00000000 00011000 = -24
int m = y >>> 1;// 01000000 00000000 00000000 00001100 = 2147483636
System.out.println(m);//2147483636
}
}
常见用途
与运算:判断奇偶(a&1 == 0 a为偶数,a&1 == 1 a为奇数)
异或运算:交换两个整数(a=a^b,b=a^b,a=a^b)
配合使用:求平均值,防止溢出( (x&y) +((x^y)>>1) )
移位运算: 右移:计算指定数值n的50%(n>>1)
左移:计算指定数值n的2倍(n<<1)
位运算的优劣
优势
运行效率更高: 直接对整数的二进制表示进行操作,在硬件层面直接执行
资源消耗更小: 需要资源较少,不需要像算术运算那样复杂的硬件支持
实现特定任务: 实现一些任务的最优选,如位掩码,权限控制,状态压缩,加密算法。
提供并行处理: 可以很容易的在多个位上同时进行,处理大型数据时提供并行处理的潜力。
缺点
可读性较差: 通常不如传统运算直观,代码的可读性较差
有易错倾向:容易出错,尤其在处理边界情况或进行不熟悉的操作时
适用范围小:只适用于整数类型(整形int,长整型long,短整型short,字节型byte)和字符型char
溢出结果异常:不检查溢出,故在一些情况下会导致结果异常
算法复杂: 对于复杂的数据结构和算法,位运算可能不是最佳选择,可能需要更复杂的逻辑
数值运算结果异常,原因及解决办法
整形类型溢出
整形:byte(8位,1字节,-128~127),short(16位,2字节,-32768~32767)
int(32位,4字节,-2147483648~2147483647),
float(64位,8字节,-2^63~2^(63-1) )
原因
由于整数存在范围限制,如果计算结果超出了范围限制,就会产生溢出,而溢出不会出错,会得到一个奇怪的结果(这是因为二进制最高位是符号位表示正负,若超过范围,最高位诶的符号位就会发生变化,由正数变为负数)
如:2147483640+15(int类型最大是2147483647)
public class b01 {
public static void main(String[] args) {
int x = 2147483640;
int y = 15;
int sum = x + y;
System.out.println(sum);//-2147483641
}
}
其运算过程如下:
解决方法
可以将int换成long,因为long可以表示的范围更大(-2^63——2^(63-1) ),所以类型不会溢出。(如果有更高的要求,long也不能满足,可以使用引用数据类型BigInteger)
public class b01 {
public static void main(String[] args) {
long x = 2147483640;
long y = 15;
long sum = x + y;
System.out.println(sum);//2147483655
}
}
浮点型精度丢失
两个基本的浮点数类型:float(单精度),double(双精度)。
float(32位,4字节,有效小数6~7位)
double(64位,8字节,有效小数15~17位)
原因
Java中浮点数的实现采用了IEEE 754标准,该标准使用二进制表示浮点数,由于二进制的局限性,通常无法精确表示所有小数(小数转二进制时对其进行乘二取整),这导致浮点数在某些情况下会出现舍入误差,也就是精度丢失问题。如:
0.9*2=1.8//取整数1
0.8*2=1.6//取整数1
0.6*2=1.2//取整数1
0.2*2=0.4//取整数0
0.4*2=0.8//取整数0
0.8*2=1.6//取整数1
......//开始循环,永远无法消除小数部分
二进制表示为:111001......
public class b01 {
public static void main(String[] args) {
double x = 10 - 9.9;
double y = 1.0 - 9.0 / 10;
//观察x,y是否相等
System.out.println(x);//0.09999999999999964
System.out.println(y);//0.00999999999999998
//正确答案应是0.1
}
}
解决方法
方法一:使用BigDecimal类
Java给我们提供了BigDecimal类,该类用于对超过16位有效位得数进行精确的计算,可以用于精确地表示和计算十进制数。通过使用BigDecimal类的String类型的构造函数来初始化BigDecimal对象,我们可以避免传统的浮点数表示方式带来的精度丢失。通过调用.subtract()方法,我们可以对这两个BigDecimal对象进行减法运算,而不会发生精度丢失。
public class b01 {
public static void main(String[] args) {
BigDecimal x = new BigDecimal("10");//使用String类型的构造参数
BigDecimal y = new BigDecimal("9.9");
BigDecimal sum = x.subtract(y);//计算 10 - 9.9
System.out.println(sum);//输出结果0.1
}
}
方法二:四舍五入
在Java中,我们可以使用BigDecimal类的setScale()方法来设置小数位数,并使用RoundingMode类的枚举常量来指定四舍五入的方式。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class b01 {
public static void main(String[] args) {
BigDecimal x = new BigDecimal(10);//这里可以不用String类型的构造参数
BigDecimal y = new BigDecimal(9.9);
//.setScale()设置小数位数为2
//RoundingMode.HALF_UP指定四舍五入的方式
BigDecimal n = x.subtract(y).setScale(2, RoundingMode.HALF_UP);//在计算结果上调用setScale()方法
System.out.println("Sum: " + n);//0.10
}
}
在我们对精度的要求不高时,计算量比较大时,double和float仍是我们的最佳选择,它的计算效率相较于BigDecimal要快得多,所以我们可以提升算法来规避进度丢失的问题。涉及精确计算时建议选择BigDecimal来提高计算结果的准确性。