【回炉重造】超详细的Java运算符修炼手册(优秀程序员不得不知道的运算技巧)

前言

这次重学java,才发现以前对运算符的运用只是冰山一角。就好似拥有者一把无比锋利的宝剑,却用来拍核桃...

目录

运算符分类

算数运算符

~(按位取反)

二进制存放形式、补码、反码

按位取反 "~" 运算符原理

位异或运算(^)

1.不用临时变量交换两个数 

2.在成对数中找单独数

3.在单独数中找成对数

位与运算符(&)

“与运算”的特殊用途:

按位或运算符(|)

“或运算”特殊作用:

++(自增)- -(自减)

+ - * / (加减乘除)

%(取余)

 取余常用于判断奇偶:

关于-10%-3=-1的问题

三目运算( 三元运算)

运用案例

三元运算符和if_else效率问题

关于用三元还是if_else问题

关系运算符

==,!=,>,<,>=,<=

逻辑运算符

与(&&)、(&)

或(||)、(|)

非(!)

位移运算符

<< 带符号左移

>>带符号右移

>>> 无符号右移

赋值运算符

=、+=、-=、*=、/=、%=、&=、^=、|=、<<=、>>=

 


运算符分类

运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。运算符按其功能来分,有算术运算符赋值运算符关系运算符逻辑运算符位运算符和其他运算符。

 

算数运算符

算术运算符即算术运算符号。是完成基本的算术运算 (arithmetic operators) 符号,就是用来处理四则运算的符号。

~(按位取反)

二进制存放形式、补码、反码

在说"~"运算符前,我们必须先要了解二进制的存放、补码、反码。

  • 二进制数在内存中是以补码的形式存放的;补码首位是符号位,0表示此数为正数,1表示此数为负数

如:

9的二进制是1001,它的补码就是01001,第一位0表示正数

同理,-9的补码表达形式肯定是1****,第一位1表示负数(因为负数的补码会有所变化,所以先用*代替)

  • 正数、负数的补码和反码是什么?

正数的补码、反码都是其二进制本身,只是需要在首位填加0,作为符号位。

如:9   反码:01001     补码:01001  (不变)

负数的反码:符号位1不变,后面有效位数全部取反(有效位是指该数的无符号二进制位,如9的有效位指1001,-1的有效位指1)

如:-9  反码:10110(符号位1不变,后面有效位数全部取反)

负数的补码:其反码再加1得到,即原码通过符号位不变,且有效位按位取反再加1也可得到;

所以负数,原码转补码(符号位不变    取反加1)

如: -9 补码:10111

所以我么可以得到,负数补码转原码(符号位不变   减1取反)

到这里,如果你看懂了上面的二进制在计算机中是以什么形式存放,以及补码与反码是什么的话,下面就可以开始讲解:

按位取反 "~" 运算符原理

按位取反运算符是将内存中的补码按位取反(包括符号位)

如:9在内存中以补码的形式存放 01001

那么 ~9 的计算原理是:

  • 按位取反(包括符号位)得到10110  

这里我们可以看到,10110已经是一个负数补码,而我们要转回10进制需要原码。那么上面已经说过,负数的补码转原码

负数补码转原码(符号位不变   减1取反)

  • 补码转原码:11010
  • 二进制转十进制:-10(记住头一位是表示正负)

所以 9 通过 "~" 运算符计算得到结果为:-10

我们再来演示一个负数的按位取反

如:-10在内存中以补码的形式存放  (10的二进制1010)取反+1+第一位符号位=10110

那么 ~-10 的计算原理是:

  • 按位取反(包括符号位)得到01001

这里我们可以看到,01001已经是一个正数补码,而我们要转回10进制需要原码。

因为正数员原码转补码、反码都是其本身。所以01001就是原码

  • 补码转原码:01001
  • 二进制转十进制:9(记住头一位是表示正负)

找规律

下面我们通过代码循环打印一下正数和负数通过按位取反后的结果规律

正数

    public static void main(String[] args) {
        for(int i=0;i<1000;i++){
            System.out.println("~"+i+"按位取反="+~i);
        }
    }

 得出结果:

~0按位取反= -1
~1按位取反= -2
~2按位取反= -3
~3按位取反= -4
~4按位取反= -5

.......省略
~995按位取反= -996
~996按位取反= -997
~997按位取反= -998
~998按位取反= -999
~999按位取反= -1000

 负数

    public static void main(String[] args) {
        for(int i=0;i>-1000;i--){
            System.out.println("~"+i+"按位取反="+~i);
        }
    }

  得出结果:

~0按位取反= -1
~-1按位取反= 0
~-2按位取反= 1
~-3按位取反= 2
~-4按位取反= 3

......省略

~-995按位取反= 994
~-996按位取反= 995
~-997按位取反= 996
~-998按位取反= 997
~-999按位取反= 998

 得出规律结论

通过正数和负数按位取反,我们可以看到,其实它的规律就是:

由此我们可以看出规律:“~x”的结果为“-(x+1)”

位异或运算(^)

异或位运算符,^是异或,计算方式是将两个数转换成二进制比较。相同为0,不同为1

比如:int x = 19^20

    public static void main(String[] args){
        //19二进制:0001 0011
        //20二进制:0001 0100
        //异或结果: 0000 0111
        System.out.println(19^20);//7
    }

另,负数按补码形式参加按位或运算。

可以看到二进制进行异或,答案是0000 0111,也就是x=7,那么在实际编写代码中有什么用呢?

我们来通过上面说的相同则为0,不同则为1,可以想到,如果自己和自己异或呢?

    public static void main(String[] args){
        //20二进制:0001 0100
        //20二进制:0001 0100
        //异或结果: 0000 0000
        System.out.println(20^20);//0
    }

显而易见,得到结果为0

那么我们就可以根据这个规律,可以用于以下几个场景:

1.不用临时变量交换两个数 

假如两个数交换,最大众的写法是
 

    public static void main(String[] args){
        int a=4,b=3;
        int temp = a;
        a = b;
        b = temp;
        System.out.println("a="+a+",b="+b);//a=3,b=4
    }

那么我们已知 a^a=0,而0^n=n

所以用异或交换我们可以这样:

    public static void main(String[] args){
        int a=4,b=3;
        a=a^b;
        b=a^b;
        a=a^b;
        System.out.println("a="+a+",b="+b);//a=3,b=4
    }

是不是很神奇!!!

那么我拆开解说:a^b^b等于a,因为b^b等于0,0^a等于a;

那么第一步我们已经将a^b赋值给了a

第二步的a^b实际上是执行了a^b^b的结果,也就是等于a,然后将a赋值给了b

第三步的a^b实际上是执行了a^b^a,第三个为什么是^a,因为第二步已经将a赋值给了b,所以结果等于b并赋值给了a

好吧再绕估计我自己都得说晕了,慢慢体会

2.在成对数中找单独数

题目:对于一个有多个数值的数组,只有一个是唯一的,其他都是成对的,怎样快速找到这个唯一值。

    public static void main(String[] args){
        int[] array = {2,3,4,4,3,5,6,6,5};
        int v = 0;
        for (int i = 0;i < array.length;i++) {
            v ^= array[i];
        }
        System.out.println("只出现一次的数是:" + v);//只出现一次的数是:2
    }

这个原理很简单,也就是其它的成对,所以异或之后为0,而单独的那个异或0还是等于其本身。

3.在单独数中找成对数

题目:1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间,能否设计一个算法实现? 

想一想:

假设:1^2^3......^n.....^1000=T 
而:   1^2^3......^n^n.....^1000 = T^n 
我们已经知道T^T^n = 0^n = n这样的过程。 
所以,我们对于上边的解题办法就有了: 

首先对1到1000,这1000个数进行异或运算,然后再把上边的1001个数进行疑惑运算,最后,再对这两个结果进行异或运算,就会得到唯一的那个n。  这里演示就没有用那么多数了,效果都一样 

    public static void main(String[] args){
        int[] array = {1,2,3,4,5,6,7,8,9,10,10};
        int v = 0,k = 0;
        for (int i = 0;i < array.length;i++) {
            v ^= array[i];
            k ^= i<array.length-1?array[i]:0;
        }
        System.out.println("出现两次的数是:" + (v^k));//出现两次的数是:10
    }

位与运算符(&)

运算规则:两个数都转为二进制,然后从高位开始比较,如果两个数都为1则为1,否则为0。

比如:129&128.

129转换成二进制就是10000001,128转换成二进制就是10000000。从高位开始比较得到,得到10000000,即128。

另,负数按补码形式参加按位或运算。

“与运算”的特殊用途:

(1)清零。如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。

(2)取一个数中指定位

方法:找一个数X,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指定位。

例:设X=10101110,

    取X的低4位,用 X & 0000 1111 = 0000 1110 即可得到;

    还可用来取X的2、4、6位。

按位或运算符(|)

参加运算的两个对象,按二进制位进行“或”运算。

运算规则:0|0=0;   0|1=1;   1|0=1;    1|1=1;

      即 :参加运算的两个对象只要有一个为1,其值为1。

例如:3|5 即 0000 0011 | 0000 0101 = 0000 0111   因此,3|5的值得7。 

另,负数按补码形式参加按位或运算。

“或运算”特殊作用:

(1)常用来对一个数据的某些位置换为1。

方法:找到一个数X,对应X要置换为1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置换为1。

例:将X=10100000的低4位置换为1 ,用 X | 0000 1111 = 1010 1111即可得到。

++(自增)- -(自减)

++ --运算符的使用:
作用:就是对变量进行自增或者自减(注意是变量,常量是不可以这样做的

而++ -- 所在变量前后,所表达的意思也不一样

关于自增自减运算的问题《java编程思想》中是这样说的:

  • (i++,i--)运算符在操作数后面,先赋值,再运算
  • (++i, --i)运算符在操作数前面,先运算,再赋值

这里说的自增或自减是指的自身加1或减1

再来谈谈i=i++的问题

// 示例一
int i = 1;
i = i++;
System.out.println(i);
// 输出结果 1
------分割-----------------------
// 示例二
int i = 1;
i = ++i;
System.out.println(i);
// 输出结果 2

解析:示例一,i++,++在右边,所以是先赋值再自增。所以i++是先将 "i" 的值1赋值给 "i",再加1所以打印为 1

   示例二,++i,++在左边,所以是先自增再赋值。所以++i是先将 "i" 加1这时 "i" == 2,再赋值给 "i"所以打印为 2

案例1:请分别计算出x,y的值?

int x = 3;

int y = x++ + ++x + x * 10;

计算过程:

在进行混合运算时我们看式子,从左往右看

首先x++,++在变量x的后面,要先把变量x的值拿出来放在这个位置上(即 int y = 3 + ++x + x * 10),然后自身+1;这里变量x = 3+1 = 4,(如果不好理解,可以令int a = x++,则a = 3);

接着往右看遇到++x,++在变量x的前面,要先自身+1(即x = 4+1 = 5),然后值再放到这个位置即(int y = 3 + 5 + x * 10)

最后x * 10,此时x = 5,即 int y = 3 + 5 + 5 * 10;

最终的结果是x = 5;y = 58;

案例2:问哪句会报错,为什么?

byte b = 5;

b++;

b = b +1;

计算过程:

我们先看b = b + 1;

b是byte类型的数,在参与运算时会自动类型提升为int数,因此b + 1的结果是个int数,int数要赋值给byte数,会报错:损失精度。

b++;

在混合运算时,byte类型数会自动类型提升为int类型数,然而确没有报错

因为在这里++隐含了强制类型转换,即b = (byte)(b + 1);使得自加后的结果还是 byte数所以这里不会报错.

+ - * / (加减乘除)

java中的加减乘除与平时生活中运用的数学加减乘除误差。这里不做过多解释,看代码;

    public static void main(String[] args) {
        System.out.println(3+3); //结果:6
        System.out.println(3-3); //结果:0
        System.out.println(3*3); //结果:9
        System.out.println(3/3); //结果:1
    }

%(取余)

如果对于整数的除法,希望得到余数而不是商,那么可以使用取余运算(%)。

注意,只有对整数使用取余运算,才有余数的数学意义。

注意:进行除法运算时,若两个操作数是整型的,结果也会是整型的,舍弃掉小数部分;如果有一个数是浮点数,结果将自动转型为浮点型。进行取余运算时,若两个操作数是整型的,结果也会是整型的,如果有一个数是浮点数,结果将自动转型为浮点型

 取余常用于判断奇偶:

    public static void main(String[] args) {
        int n = 123;
        if(n%2==0){
            System.out.println("偶数");
        }else{
            System.out.println("基数");
        }
    }

关于-10%-3=-1的问题

一下代码运行后输出结果是?大家可以大胆猜一下;

    public static void main(String[] args) {
        int a=-10,b=-3;
        System.out.print(a%b);
    }

 答案出乎意料的是 -1,什么鬼?

那这样呢?

    public static void main(String[] args) {
        int a=-10,b=3;
        System.out.print(a%b);
    }

输出为 -1

最后再试一次

    public static void main(String[] args) {
        int a=10,b=-3;
        System.out.print(a%b);
    }

输出为 1 

好的,这到底是怎么一回事,某度上是这样回答的!

  • 余数是指整数除法中被除数未被除尽部分。
  • 余数和除数的差的绝对值要小于除数的绝对值(适用于实数域);
  • 所以从定义上来说,负数除以负数,余数可以是负数。
  • 在java中的定义就是遵循上面定义。

不过,好像并没有什么帮助

课本上倒是给出了答案:

在取余操作中,余数的正负符号完全取决于左操作数,和操作数的正负号一致。
也就是说,谁被取余,符号就看谁的

三目运算( 三元运算)

三元运算符格式:

数据类型 变量名 = 布尔类型表达式?结果1:结果2

三元运算符计算方式:

  • 布尔类型表达式结果是 true,三元运算符整体结果为结果1,赋值给变量。
  • 布尔类型表达式结果是 false,三元运算符整体结果为结果2,赋值给变量。 

运用案例

例如:

public static void main(String[] args) {
    int i = (1==2 ? 100 : 200);
    System.out.println(i);//200
    int j = (3<=4 ? 500 : 600);
    System.out.println(j);//500
}

下面看下实际应用:

    public static void main(String[] args) {
        int a=1,b=2;
        //谁大输出谁
        if(a>b){
            System.out.print(a);
        }else{
            System.out.print(b);
        }
    }

上面这串语句我们完全可以用三元运算符代替

如:

    public static void main(String[] args) {
        int a=1,b=2;
        //谁大输出谁
        System.out.println(a>b?a:b); 
    }

优点是这样可以大大缩短代码量。缺点就是必须有一个参数接收程序的结果。

三元运算符和if_else效率问题

既然三目运算符相比与if-else来说,比较简洁,那么他们在性能上又有没差异呢?
结论是:三目运算符的运算速度比if-else的效率高出1~0.5倍左右,当然机器可能也会导致误差和结果波动,百度上有测出2倍多的 ,对着数据表示怀疑- -!!

那么三目运算符比之快在哪里呢?

  • 程序运行时,处理器会通过并行运算而加速运行,当遇到选择支时则会停下判断 (例如高速行驶的大卡车遇到了分岔路)if-else 是先赋值再运算,为了节省时间,分支预测会先猜测运行 if 还是 else 并继续运行 (默认是if),若猜对则因并行运算而节省时间,若猜错则因消除运算而耗费时间。 (卡车直接冲向一边康康可不可以走通,如果可以则继续走,如果不可以则回头走另一条路)(成本是回到分岔处的时间)
  • 三目 是先运算再赋值,遇到选择支时停止并行并判断条件。(在分岔处停下康地图) (成本是重新加速需要的时间),在多数情况下,运算结果为0与为1的可能相近,分支预测&并行运算 会比三目耗费更多的时间,所以应更多的使用三目。在一些情况下,运算结果大多为0或大多为1(80+%),这时 分支预测&并行运算 的损耗远小于三目,所以应选择 if-else
     

关于用三元还是if_else问题

追求代码简练用三元
追求清晰用if
在ireport一些动态执行的情况下,中只能用三元、
也就是说三元适用范围更广。

但是这里复杂的业务逻辑尽量使用if,简单的判断用三元

关系运算符

 

==,!=,>,<,>=,<=

==:比较符号两边数据是否相等,相等结果是true。

!=:不等于符号 ,如果符号两边的数据不相等,结果是true。

>:比较符号左边的数据是否大于右边的数据,如果大于结果是true。

<:比较符号左边的数据是否小于右边的数据,如果小于结果是true。

>=:比较符号左边的数据是否大于或者等于右边的数据,如果小于结果是true。

<=:比较符号左边的数据是否小于或者等于右边的数据,如果小于结果是true。

  • 比较运算符,是两个数据之间进行比较的运算,运算结果都是布尔值 true 或者 false 。
public static void main(String[] args) {
    System.out.println(1==1);//true
    System.out.println(1<2);//true
    System.out.println(3>4);//false
    System.out.println(3<=4);//true
    System.out.println(3>=4);//false
    System.out.println(3!=4);//true
}

逻辑运算符

逻辑运算符,是用来连接两个布尔类型结果的运算符,运算结果都是布尔值 true 或者 false

与(&&)、(&)

&&使用

  • 两边都是true,结果是true
  • 一边是false,结果是false
  • 短路特点:符号左边是false,右边不再运算

&则是没有短路特性

如果符号左边是false,还是会执行右边的表达式

或(||)、(|)

  • 两边都是false,结果是false
  • 一边是true,结果是true
  • 短路特点: 符号左边是true,右边不再运算

|则是没有短路特性

如果符号左边是true,还是会执行右边的表达式

非(!)

逻辑非 "!" 运算是把相应的变量数据转换为相应的真/假值。若原先为假,则逻辑非以后为真,若 原先为真,则逻辑非以后为假。

如:

 ! true 结果是false
 ! false结果是true

位移运算符

<< 带符号左移

将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。

另,负数按补码形式参加按位或运算。

列如:

    public static void main(String[] args){
        int a=2;
        System.out.print("a 移位的结果是"+(a<<2));//a 移位的结果是8
    }

分析上面代码,2 的二进制是00000010,它向左移动2 位,就变成了00001000,即8。如果从另一个角度来分析,它向左移动2 位,其实就是乘上2 的2 次方,结果还是8 

所以a<<2等效于a*2的2次方

>>带符号右移

将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。

另,负数按补码形式参加按位或运算。

例如:

    public static void main(String[] args){
        int a=8;
        System.out.print("a 移位的结果是"+(a>>2));//a 移位的结果是2
    }

分析上面代码,8 的二进制是00001000,它向右移动2 位,就变成了00000010,即2。如果从另一个角度来分析,它向右移动2 位,其实就是除上2 的2 次方,结果还是2

所以a>>2等效于a/2的2次方

>>> 无符号右移

将一个数的各二进制位全部右移若干位,空位都以0补齐(无论正负数)

这个运算其实正数的话是和>>运算符一样,没什么区别。

问题就在负数。

下面看代码:

    public static void main(String[] args){
        int a=-8;
        System.out.print("a 移位的结果是"+ (a>>>2));//a 移位的结果是1073741822
    }

神马??  1073741822!!!,博主看到这个答案的时候当场懵逼!

最终经过一番研究,恍然大悟!

其实,位移运算符提到过是最高位补0,而我运算的时候选择性左边补0,比如2的二次方10,习惯性010这样补,正数倒是没有提。但是一到负数,运算结果就会出错!!!!

其实int类型是占4个字节的,二进制是有32位+1位符号位的。如果上面 -8 要进行 >>> 运算,运算过程应该如下:

(不理解补码向最上看,或者去看目录,上面有提到)

1 00000000 00000000 00000000 00001000 (-8的二进制,第一位符号位)
1 11111111 11111111 11111111 11111000 (内部补码存放形式)
0 00111111 11111111 11111111 11111110 (进行位移运算后,此时符号位为0是正数,正数的补码转原码就是其本身)

所以,最后的结果为

00111111 11111111 11111111 11111110 也就是十进制的 1073741822

赋值运算符

=、+=、-=、*=、/=、%=、&=、^=、|=、<<=、>>=

  • 赋值运算符,就是将符号右边的值,赋给左边的变量。

例如:

public static void main(String[] args){
    int i = 5;
    i+=5;//计算方式 i=i+5 变量i先加5,再赋值变量i
    System.out.println(i); //输出结果是10
}

总算写完了,有缘江湖再见!

 

发布了106 篇原创文章 · 获赞 47 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq493820798/article/details/102962327