數值表達式
1. 奇偶判斷
不要使用 i % 2 == 1 來判斷是否是奇數,因為i為負奇數時不成立,請使用 i % 2 != 0 來判斷是否是奇數,或使用高效式 (i & 1) != 0來判斷。
2. 小數精確計算
System.out.println(2.00 -1.10);//0.8999999999999999
上面的計算出的結果不是 0.9,而是一連串的小數。問題在於1.1這個數字不能被精確表示為一個double,因此它被表示為最接近它的double值,該程序從2中減去的就是這個值,但這個計算的結果並不是最接近0.9的double值。
一般地說,問題在於並不是所有的小數都可以用二進制浮點數精確表示。
二進制浮點對於貨幣計算是非常不適合的,因為它不可能將1.0表示成10的其他任何負次冪。
解決問題的第一種方式是使用貨幣的最小單位(分)來表示:System.out.println(200-110);//90
第二種方式是使用BigDecimal,但一定要用BigDecimal(String)構造器,而千萬不要用BigDecimal(double)來構造(也不能將float或double型轉換成String再來使用BigDecimal(String)來構造,因為在將float或double轉換成String時精度已丟失)。例如new BigDecimal(0.1),它將返回一個BigDecimal,也即0.1000000000000000055511151231257827021181583404541015625,正確使用BigDecimal,程序就可以打印出我們所期望的結果0.9:
System.out.println(newBigDecimal("2.0").subtract(newBigDecimal("1.10")));// 0.9
另外,如果要比較兩個浮點數的大小,要使用BigDecimal的compareTo方法。
3. int整數相乘溢出
我們計算一天中的微秒數:
long microsPerDay = 24 * 60 * 60 * 1000 * 1000;// 正確結果應為:86400000000
System.out.println(microsPerDay);// 實際上為:500654080
問題在於計算過程中溢出了。這個計算式完全是以int運算來執行的,並且只有在運算完成之后,其結果才被提升為long,而此時已經太遲:計算已經溢出。
解決方法使計算表達式的第一個因子明確為long型,這樣可以強制表達式中所有的后續計算都用long運算來完成,這樣結果就不會溢出:
long microsPerDay = 24L * 60 * 60 * 1000 * 1000;
4. 負的十六進制與八進制字面常量
“數字字面常量”的類型都是int型,而不管他們是幾進制,所以“2147483648”、“0x180000000(十六進制,共33位,所以超過了整數的取值范圍)”字面常量是錯誤的,編譯時會報超過int的取值范圍了,所以要確定以long來表示“2147483648L”、“0x180000000L”。
十進制字面常量只有一個特性,即所有的十進制字面常量都是正數,如果想寫一個負的十進制,則需要在正的十進制字面常量前加上“-”即可。
十六進制或八進制字面常量可就不一定是正數或負數,是正還是負,則要根據當前情況看:如果十六進制和八進制字面常量的最高位被設置成了1,那么它們就是負數:
System.out.println(0x80);//128
//0x81看作是int型,最高位(第32位)為0,所以是正數
System.out.println(0x81);//129
System.out.println(0x8001);//32769
System.out.println(0x70000001);//1879048193
//字面量0x80000001為int型,最高位(第32位)為1,所以是負數
System.out.println(0x80000001);//-2147483647
//字面量0x80000001L強制轉為long型,最高位(第64位)為0,所以是正數
System.out.println(0x80000001L);//2147483649
//最小int型
System.out.println(0x80000000);//-2147483648
//只要超過32位,就需要在字面常量后加L強轉long,否則編譯時出錯
System.out.println(0x8000000000000000L);//-9223372036854775808
從上面可以看出,十六進制的字面常量表示的是int型,如果超過32位,則需要在后面加“L”,否則編譯過不過。如果為32,則為負int正數,超過32位,則為long型,但需明確指定為long。
System.out.println(Long.toHexString(0x100000000L + 0xcafebabe));// cafebabe
結果為什么不是0x1cafebabe?該程序執行的加法是一個混合類型的計算:左操作數是long型,而右操作數是int類型。為了執行該計算,Java將int類型的數值用拓寬原生類型轉換提升為long類型,然后對兩個long類型數值相加。因為int是有符號的整數類型,所以這個轉換執行的是符號擴展。
這個加法的右操作數0xcafebabe為32位,將被提升為long類型的數值0xffffffffcafebabeL,之后這個數值加上了左操作0x100000000L。當視為int類型時,經過符號擴展之后的右操作數的高32位是1,而左操作數的第32位是1,兩個數值相加得到了0:
0x ffffffffcafebabeL
+0x 0000000100000000L
-----------------------------
0x 00000000cafebabeL
如果要得到正確的結果0x1cafebabe,則需在第二個操作數組后加上“L”明確看作是正的long型即可,此時相加時拓展符號位就為0:
System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL));// 1cafebabe
5. 窄數字類型提升至寬類型時使用符號位擴展還是零擴展
System.out.println((int)(char)(byte)-1);// 65535
結果為什么是65535而不是-1?
窄的整型轉換成較寬的整型時符號擴展規則:如果最初的數值類型是有符號的,那么就執行符號擴展(即如果符號位為1,則擴展為1,如果為零,則擴展為0);如果它是char,那么不管它將要被提升成什么類型,都執行零擴展。
了解上面的規則后,我們再來看看迷題:因為byte是有符號的類型,所以在將byte數值-1(二進制為:11111111)提升到char時,會發生符號位擴展,又符號位為1,所以就補8個1,最后為16個1;然后從char到int的提升時,由於是char型提升到其他類型,所以采用零擴展而不是符號擴展,結果int數值就成了65535。
如果將一個char數值c轉型為一個寬度更寬的類型時,只是以零來擴展,但如果清晰表達以零擴展的意圖,則可以考慮使用一個位掩碼:
int i = c & 0xffff;//實質上等同於:int i = c ;
如果將一個char數值c轉型為一個寬度更寬的整型,並且希望有符號擴展,那么就先將char轉型為一個short,它與char上個具有同樣的寬度,但是它是有符號的:
int i = (short)c;
如果將一個byte數值b轉型為一個char,並且不希望有符號擴展,那么必須使用一個位掩碼來限制它:
char c = (char)(b & 0xff);// char c = (char) b;為有符號擴展
6. ((byte)0x90 == 0x90)?
答案是不等的,盡管外表看起來是成立的,但是它卻等於false。為了比較byte數值(byte)0x90和int數值0x90,Java通過拓寬原生類型將byte提升為int,然后比較這兩個int數值。因為byte是一個有符號類型,所以這個轉換執行的是符號擴展,將負的byte數值提升為了在數字上相等的int值(10010000à111111111111111111111111 10010000)。在本例中,該轉換將(byte)0x90提升為int數值-112,它不等於int數值的0x90,即+144。
解決辦法:使用一個屏蔽碼來消除符號擴展的影響,從而將byte轉型為int。
((byte)0x90 & 0xff)== 0x90
7. 三元表達式(?:)
char x = 'X';
int i = 0;
System.out.println(true ? x : 0);// X
System.out.println(false ? i : x);// 88
條件表達式結果類型的規則:
(1) 如果第二個和第三個操作數具有相同的類型,那么它就是條件表達式的類型。
(2) 如果一個操作的類型是T,T表示byte、short或char,而另一個操作數是一個int類型的“字面常量”,並且它的值可以用類型T表示,那條件表達式的類型就是T。
(3) 否則,將對操作數類型進行提升,而條件表達式的類型就是第二個和第三個操作被提升之后的類型。
現來使用以上規則解上面的迷題,第一個表達式符合第二條規則:一個操作數的類型是char,另一個的類型是字面常量為0的int型,但0可以表示成char,所以最終返回類型以char類型為准;第二個表達式符合第三條規則:因為i為int型變量,而x又為char型變量,所以會先將x提升至int型,所以最后的結果類型為int型,但如果將i定義成final時,則返回結果類型為char,則此時符合第二條規則,因為final類型的變量在編譯時就使用“字面常量0”來替換三元表達式了:
final int i = 0;
System.out.println(false ? i : x);// X
在JDK1.4版本或之前,條件操作符 ?: 中,當第二個和延續三個操作數是引用類型時,條件操作符要求它們其中一個必須是另一個的子類型,那怕它們有同一個父類也不行:
public class T {
public static void main(String[] args) {
System.out.println(f());
}
public static T f() {
// !!1.4不能編譯,但1.5可以
// !!return true?new T1():new T2();
return true ? (T) new T1() : new T2();// T1
}
}
class T1 extends T {
public String toString() {
return "T1";
}
}
class T2 extends T {
public String toString() {
return "T2";
}
}
在5.0或以上版本中,條件操作符在第二個和第三個操作數是引用類型時總是合法的。其結果類型是這兩種類型的最小公共超類。公共超類總是存在的,因為Object是每一個對象類型的超類型,上面的最小公共超類是T,所以能編譯。
8. +=復合賦值問題
x+=i與x=x+i等效嗎,許多程序員都會認為第一個表達式x+=i只是第二個表達式x=x+i的簡寫方式,但這並不准確。
Java語言規范中提到:復合賦值 E1 op= E2等價於簡單賦值 E1 = (T)((E1) op (E2)),其中T是E1的類型。
復合賦值表達式自動地將所執行計算的結果轉型為其左側變量的類型。如果結果的類型與該變量的類型相同,那么這個轉型不會造成任何影響,然而,如果結果的類型比該變量的類型要寬,那么復合賦值操作符將悄悄地執行一個窄化原生類型轉換,這樣就會導致結果不正確:
short x=0;
int i = 123456;
x +=i;
System.out.println(x);//-7616
使用簡單的賦值方式就不會有這樣的問題了,因為寬類型不能自動轉換成窄的類型,編譯器會報錯,這時我們就會注意到錯誤:x = x + i;//編譯通不過
請不要將復合賦值操作符作用於byte、short或char類型的變量;在將復合賦值操作符作用於int類型的變量時,要確保表達式右側不是long、float或double類型;在將復合賦值操作符作用於float類型的變量時,要確保表達式右側不是double類型。其實一句:不要將讓左側的類型窄於右側的數字類型。
總之,不要在short、byte或char類型的變量之上使用復合賦值操作符,因為這一過程會伴隨着計算前類型的提升與計算后結果的截斷,導致最后的計算結果不正確。
9. i =++i;與i=i++;的區別
int i = 0;
i = i++;
System.out.println(i);
上面的程序會輸出什么?大部分會說是 1,是也,非也。運行時正確結果為0。
i=++i;相當於以下二個語句(編譯時出現警告,與i=i;警告相同):
i=i+1;
i=i;
i = i++;相當於以下三個語句:
int tmp = i;
i = i + 1;
i = tmp;
下面看看下面程序片段:
int i = 0, j = 0, y = 0;
i++;//相當於:i=i+1;
System.out.println("i=" + i);// i=1
++i;//相當於:i=i+1;
System.out.println("i=" + i);// i=2
i = i++;//相當於:int tmp=i;i=i+1;i=tmp;
System.out.println("i=" + i);// i=2
i = ++i;//編譯時出現警告,與i=i;警告相同。相當於:i=i+1;i=i;
System.out.println("i=" + i);// i=3
j = i++;//相當於:int tmp=i;i=i+1;j=tmp;
System.out.println("j=" + j);// j=3
System.out.println("i=" + i);// i=4
y = ++i;//相當於:i=i+1;y=i;
System.out.println("y=" + y);// y=5
System.out.println("i=" + i);// i=5
10.Integer.MAX_VALUE + 1=?
System.out.println(Integer.MAX_VALUE + 1);
上面的程序輸出多少?2147483647+1=2147483648?答案為-2147483648。
查看源碼Integer.MAX_VALUE 為MAX_VALUE = 0x7fffffff;所以加1后為0x80000000,又0x80000000為整型字面常量,滿了32位,且最位為1,所以字面上等於 -0,但又由於 -0就是等於0,所以-0這個編碼就規定為最小的負數,32位的最小負數就是-2147483648。
11.-1<<32=?、-1<<65=?
如果左操作數是int(如果是byte、short、char型時會提升至int型再進行位操作)型,移位操作符只使用其右操作數的低5位作為移位長度(也就是將右操作數除以32取余);如果左操作數是long型,移位操作符只使用其右操作數的低6位作為移位長度(也就是將右操作數除以64取余);
再看看下面程序片段就會知道結果:
System.out.println(-1 << 31);// -2147483648 向左移31%32=31位
System.out.println(-1 << 32);// -1 向左移32%32=0位
System.out.println(-1 << 33);// -2 向左移33%32=1位
System.out.println(-1 << 1);// -2 向左移1%32=1位
System.out.println(-1L << 63);// -9223372036854775808 向左移63%64=63位
System.out.println(-1L << 64);// -1 向左移64%64=0位
System.out.println(-1L << 65);// -2 向左移65%64=1位
System.out.println(-1L << 1);// -2 向左移1%64=1位
byte b = -1;// byte型在位操作前類型提升至int
System.out.println(b << 31);// -2147483648 向左移31%32=31位
System.out.println(b << 63);// -2147483648 向左移63%32=31位
short s = -1;// short型在位操作前類型提升至int
System.out.println(s << 31);// -2147483648 向左移31%32=31位
System.out.println(s << 63);// -2147483648 向左移63%32=31位
char c = 1;// char型在位操作前類型提升至int
System.out.println(c << 31);// -2147483648 向左移31%32=31位
System.out.println(c << 63);// -2147483648 向左移63%32=31位
12.一個數永遠不會等於它自己加1嗎?i==i+1
一個數永遠不會等於它自己加1,對嗎?如果數字是整型,則對;如果這個數字是無窮大或都是浮點型足夠大(如1.0e40),等式就可能成立了。
Java強制要求使用IEEE 754浮點數算術運算,它可以讓你用一個double或float來表示無窮大。
浮點型分為double型、float型。
無窮分為正無窮與負無窮。
無窮大加1還是無窮大。
一個浮點數值越大,它和其后繼數值之間的間隔就越大。
對一個足夠大的浮點數加1不會改變它的值,因為1不足以“填補它與其后者之間的空隙”。
浮點數操作返回的是最接近其精確數學結果的浮點數值。
一旦毗鄰的浮點數值之間的距離大於2,那么對其中的一個浮點數值加1將不會產生任何效果,因為其結果沒有達到兩個數值之間的一半。對於float類型,加1不會產生任何效果的最小數是2^25,即33554432;而對於double類型,最小數是2^54,大約是1.8*10^16。
33554432F轉二進制過程:
33554432的二進制為:10000000000000000000000000,將該二進制化成規范的小數二進制,即小數從右向左移25位1.0000000000000000000000000,化成浮點數二進制0,25+127, 00000000000000000000000 00(丟棄最后兩位),即0, 10011000, 00000000000000000000000,最后的結果為1.00000000000000000000000*2^25
毗鄰的浮點數值之間的距離被稱為一個ulp,它是最小單位(unit in the last place)的首字母縮寫。在5.0版本中,引入了Math.ulp方法來計算float或double數值的ulp。
二進制浮點算術只是對實際算術的一種近似。
// 注,整型數不能被 0 除,即(int)XX/0運行時拋異常
double i = 1.0 / 0.0;// 正無窮大
double j = -1.0 / 0.0;// 負無窮大
// Double.POSITIVE_INFINITY定義為:POSITIVE_INFINITY = 1.0 / 0.0;
System.out.println(i + " " + (i == Double.POSITIVE_INFINITY));//Infinity true
// Double.NEGATIVE_INFINITY定義為:NEGATIVE_INFINITY = -1.0 / 0.0;
System.out.println(j + " " + (j == Double.NEGATIVE_INFINITY));//-Infinity true
System.out.println(i == (i + 1));// true
System.out.println(0.1f == 0.1);// false
float f = 33554432;
System.out.println(f + " " + (f==(f+1)));//3.3554432E7 true
13.自己不等於自己嗎?i!=i
NaN(Not a Number)不等於任何數,包括它自身在內。
double i = 0.0/0.0;可表示NaN。
float和double類型都有一個特殊的NaN值,Double.NaN、Float.NaN表示NaN。
如果一個表達式中產生了NaN,則結果為NaN。
System.out.println(0.0 / 0.0);// NaN
System.out.println(Double.NaN + " " + (Double.NaN == (0.0 / 0.0)));//NaN false
14.自動拆箱
// 為了兼容以前版本,1.5不會自動拆箱
System.out.println(new Integer(0) == new Integer(0));// false
// 1.4編譯非法,1.5會自動拆箱
System.out.println(new Integer(0) == 0);// true
15.為什么-0x00000000==0x00000000、-0x80000000== 0x80000000
十六進制(0X開頭)、八進制(0開頭)、以及二進制(0B開頭,JDK1.7已開始支持)表示了內存中直接存儲形式,這與十進制是有區別的。
規則:為了取一個整數類型(十六進制、八進制或二進制)的負值,對每一位(即內存所存儲的二進制內容)取反,然后再加1;但如果是對一個十進制數求負操作,實質上直接求這個十進制整數的補碼即可,但也可以按照前面對十六進制數那樣取負操作,只不過先要將這個十進制數轉換為補碼形式再按前面規則取反后加1。
運用上面的規則,-0x00000000的運算過程:對0x00000000先取反得到0xffffffff,再加1,-0x00000000的最后結果就為 0xffffffff+1,其最后的結果還是0x00000000,所以-0x00000000 == 0x00000000。前面是對0x00000000求負的過程,如果是對0求負呢?按上面的規則,對0取負就是求-0的補碼即可,根據后面的規則,-0的補碼為0x00000000。不過也可采用上面對一個十六進制取負的規來求,先求0的十六進制形式0x00000000,再按前面的取負過程來即可得到對0取負的結果也為0x00000000。
運用前面的規則,-0x80000000的運算過程:對0x80000000先取反得到0x7fffffff,再加1,-0x80000000的最后結果就為 0x7fffffff+1,其最后的結果還是0x80000000,即-0x80000000 == 0x80000000。前面是對0x80000000求負的過程,如果是對2147483648求負呢?按上面的規則,對2147483648取負就是求-2147483648的補碼,並要所后面的規則,-2147483648的補碼就是0x80000000。不過也可采用上面對一個十六進制取負的規來求,先求2147483648的十六進制形式0x80000000,再按前面的取負過程來即可得到對2147483648取負的結果也為0x80000000。
所有的整數在內存中都是以補碼形式存儲的,只不過正數或零的補碼為本身而已;如果是負數,則對其絕對值取反后加1即是內存中存儲形式。求一個負數的補碼有個比較簡單的規則:先求這個數絕對值的原碼,然后從該二進制最末向前開始找第一個為1的位置,最后將這個1之前的各位取反(包括最高位符號位0),其他位不變,最終所得的二進制就為這個負數的補碼,也就是最終在內存中存儲的形式。不過在找這個第一個為1時可能找不到或在最高位,比如-0,其絕對值為0(0x00000000),也有可能這個1在最高位,比如-2147483648,其絕對值為2147483648(0x80000000),如果遇到絕對值的原碼為0x00000000或0x80000000的情況下補碼直接就是0x00000000或0x80000000。
上面是講了原碼到補碼的轉換規則,那怎么由補碼到原碼就簡單了:如果補碼是正的,則說明原碼是正的,所以這時補碼就是原碼;如果補碼是負的,則原碼一定是負的,此時轉換過程正好與從負原碼到補碼的轉換過程相反,即先將補碼減1,再取反,但符號位不要變,即永遠為1。
// 因為Integer.MIN_VALUE定義成了0xffffffff,所以根據上面的規則得到相等的結論
System.out.println(Integer.MIN_VALUE == -Integer.MIN_VALUE);// true
/*
* 0x80000000取反得0x7fffffff,再加1得0x80000000,因為負數是
* 以補碼形式存儲於內存中的,所以推導出結果原碼為:0x80000000,
* 即為-0,又因為-0是等於0的,所以不需要-0這個編碼位,否則就多了
* 一個0x80000000編碼位了,所以最后就規定0x80000000為最小負數
*/
System.out.println(-0x80000000);// -2147483648
/*
* 0x7fffffff取反得0x80000000,再加1得0x80000001,因為負數是
* 以補碼形式存儲於內存中的,所以推導出0x80000001的原碼為:0xffffffff(取反時
*,第一位為符號位不要變,還是1),所以最后的結果就為 -0x7fffffff = -2147483647
*/
System.out.println(-0x7fffffff);// -2147483647
另外,還發現有趣現象:最大整數加1后會等於最小整數:
// MAX_VALUE = 0x7fffffff; MIN_VALUE = 0x80000000;
System.out.println((Integer.MAX_VALUE + 1) == Integer.MIN_VALUE);// true
// MIN_VALUE = 0x8000000000000000L; MIN_VALUE = 0x8000000000000000L;
System.out.println((Long.MAX_VALUE + 1) == Long.MIN_VALUE);// true
當然,-Byte. MIN_VALUE==Byte.MIN_VALUE、-Short.MIN_VALUE== Short.MIN_VALUE、-Long.MIN_VALUE== Long.MIN_VALUE,也是成立的。
16.Math.abs結果一定為非負數嗎?
System.out.println(Math.abs(Integer.MIN_VALUE));// -2147483648
上面的程序不會輸出2147483648,而是-2147483648,為什么?
其實我們看一下Math.abs源碼就知道為什么了,源碼:(a < 0) ? -a : a;,結合上面那個迷題,我們就發現-Integer.MIN_VALUE= Integer.MIN_VALUE,所以上面的答案就是最小整數自己。
另外我們也可以從API文檔看到對Math.abs()方法的解釋:如果參數等於 Integer.MIN_VALUE的值(即能夠表示的最小負 int值),則結果與該值相同且為負。
所以Math.abs不能保證一定會返回非負結果。
當然,Long.MIN_VALUE也是這樣的。
17.不要使用基於減法的比較器
Comparator<Integer> c = new Comparator<Integer>() {
public int compare(Integer i1, Integer i2) {
return i1 - i2;// 升序
}
};
List<Integer> l = new ArrayList<Integer>();
l.add(new Integer(-2000000000));
l.add(new Integer(2000000000));
Collections.sort(l, c);
System.out.println(l);// [2000000000, -2000000000]
上面程序的比較器是升序,結果卻不是這樣,比較時出現了什么問題?
先看看下面程序片斷:
int x = -2000000000;
int y = 2000000000;
/*
* -2000000000 即 -(01110111001101011001010000000000)
* 的補碼為: 10001000110010100110110000000000
*
* 計算過程使用豎式表示:
* 10001000110010100110110000000000
* 10001000110010100110110000000000
* --------------------------------
* 00010001100101001101100000000000
*
* 計算結果溢出,結果為294967296
*/
System.out.println(x - y);// 294967296
所以不要使用減法的比較器,除非能確保要比較的數值之間的距離永遠不會大於Intger.MAX_VALUE。
基於整型的比較器的實現一般使用如下的方式來比較:
public int compare(Integer i1, Integer i2) {
return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1));
}
18. int i=-2147483648與int i=-(2147483648)?
int i=-(2147483648);
編譯通不過!為什么
int字面常量2147483638只能作為一元負操作符的操作數來使用。
類似的還有最大long:
long i=–(9223372036854775808L);
