【回爐重造】超詳細的Java運算子修煉手冊(優秀程式設計師不得不知道的運算技巧)

辰小白發表於2020-02-01

前言

這次重學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個位元組的,二進位制是有64位+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
}

 

總算寫完了,有緣江湖再見!

 

相關文章