對(duì)象與基本類型
幾乎所有Java 初學(xué)者都被告知,在Java里一切都被視為對(duì)象(Object),操縱對(duì)象的表示符實(shí)際上時(shí)對(duì)象的一個(gè)引用(Reference)。例如
String str; // 注意!此處創(chuàng)建了一個(gè)引用,而非對(duì)象 |
str=new String(“Hello”);// 這里創(chuàng)建了一個(gè)String 對(duì)象并與str相關(guān)聯(lián) |
通常用new 操作符來(lái)創(chuàng)建一個(gè)新對(duì)象,并存儲(chǔ)在堆里面。 【注】具體內(nèi)容可以參看 Java 堆與棧
程序設(shè)計(jì)中有一系列小的、簡(jiǎn)單的變量(筆者是這樣認(rèn)為的),將它們存儲(chǔ)在堆里往往并不是很高效,因此對(duì)于這些基本類型,Java 創(chuàng)建一個(gè)并非引用的“自動(dòng)”的變量,并將其值存儲(chǔ)在棧中。 【注】具體內(nèi)容可以參看 Java 堆與棧
Java 確定了每種基本類型所占存儲(chǔ)空間的大小,并且不隨計(jì)算機(jī)硬件架構(gòu)的變化而變化,因此他們更具可移植性。Java 基本類型及其包裝器的具體信息參見下表:
基本類型 |
大小 |
最小值 |
最大值 |
包裝器類型 |
boolean |
— |
— |
— |
Boolean |
char |
16-bit |
Unicode o |
Unicode 264-1 |
Character |
byte |
8 bits |
-128 |
+127 |
Byte |
short |
16 bits |
-2 15 |
+2 15 -1 |
Short |
int |
32 bits |
-2 31 |
+2 31 -1 |
Integer |
long |
64 bits |
-2 63 |
+2 63 -1 |
Long |
float |
32 bits |
IEEE754 |
IEEE754 |
Float |
double |
64 bits |
IEEE754 |
IEEE754 |
Double |
void |
— |
— |
— |
Void |
基本類型具有的包裝器類型使得可以在堆中創(chuàng)建一個(gè)非基本對(duì)象用來(lái)表示對(duì)應(yīng)的基本類型,例如
Character ch=new Character( ‘c ’); |
Java SE5 的自動(dòng)包裝功能將自動(dòng)的將基本類型轉(zhuǎn)換為包裝器類型:
Character ch= ‘c ’;//Autoboxing |
可以看到Java SE5 的自動(dòng)包裝功能為我們提供了很大的方便(泛型中你會(huì)看到它的優(yōu)勢(shì)),然而如果你不了解它的工作原理則會(huì)讓你陷入意想不到的困惑和麻煩之中。請(qǐng)看下面的例子:
public static void main(String args[]) { Integer i1 = 127 ; Integer i2 = 127 ; System.out.println(i1 == i2); i1 = 128 ; i2 = 128 ; System.out.println(i1 == i2); } /* Output: * true * false */
有些令人驚訝,兩個(gè)范例語(yǔ)法完全一樣,只不過(guò)改個(gè)數(shù)值而已,結(jié)果卻相反。這是因?yàn)樵谧詣?dòng)包裝時(shí)對(duì)于從–128 到127之間的值,它們被包裝為Integer對(duì)象后,會(huì)存在內(nèi)存中被重用,而其它的值,被包裝后的Integer對(duì)象并不會(huì)被重用,即相當(dāng)于每次包裝時(shí)都新建一個(gè)Integer對(duì)象。
然而下面的輸出卻是ture ,這是因?yàn)槟阌昧薾1=n2,n1只是n2的別名而已。
在看一個(gè)例子:
public static void main(String args[]) { Integer i1 = 12 ; Integer i2 = 12 ; System.out.println(i1 == i2); i1 = new Integer( 13 ); i2 = new Integer( 13 ); System.out.println(i1 == i2); i1 = i2 = new Integer( 14 ); System.out.println(i1 == i2); } /* Output: * true * false * true */
天吶!數(shù)字13 仿佛被詛咒了一般,==對(duì)它的判斷似乎失去了效用!事實(shí)上可憐的Java一無(wú)所知,如果你對(duì)輸出的結(jié)果感到疑惑,說(shuō)明你對(duì)==和Java的存儲(chǔ)還不是很了解。
如果知道下面的規(guī)則,你對(duì)上面例子的輸出結(jié)果或許就不會(huì)那么疑惑了:
1. 對(duì)于基本類型,== 將比較兩邊的值是否相等;
2. 對(duì)于對(duì)象,== 則比較對(duì)象的是否指向同一個(gè)對(duì)象。
如果你仍然對(duì)輸出結(jié)果不甚明白,那么別急,關(guān)于Java 堆與棧的介紹將會(huì)讓你明了,但是在這里我還要添加關(guān)于equals()的介紹。
或許你已經(jīng)被告知,要想比較對(duì)象的實(shí)際內(nèi)容是否相同必須使用所有對(duì)象都適用的特殊方法equals() ,例如:
Integer n1=new Integer(13); |
Integer n2=new Integer(13); |
System.out.println(n1.equals(n2)); //true |
這看上去的確很簡(jiǎn)單,但是下面的例子中equals() 又要讓你失望和疑惑了:
public class Value { public int i;} |
Value v1=new Value(); |
Value v2=new Value(); v1.i=v2.i=13; |
System.out.println(v1.equals(v2)); //false |
哦,它輸出了false !好吧,讓我們看看equals()方法的原型:
public boolean equals(Object obj) { return ( this == obj); }
再讓我們看看Integer 中的equals()方法:
public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false ; }
OK! 一切都清楚了,繼承自O(shè)bject 中的equals()方法與==沒什么兩樣,只是Integer類對(duì)equals()方法進(jìn)行了重寫而已,這樣我們很容易能寫出令上面例子中System.out.println(v1.equals(v2));語(yǔ)句輸出true的方法
Java 的堆是一個(gè)位于隨機(jī)訪問(wèn)存儲(chǔ)器(RAM )的運(yùn)行時(shí)數(shù)據(jù)區(qū)。通常使用new操作符在堆中創(chuàng)建對(duì)象,它們不需要程序代碼來(lái)顯式的釋放。堆是由垃圾回收來(lái)負(fù)責(zé)的,堆的優(yōu)勢(shì)是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器,因?yàn)樗窃谶\(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)。但缺點(diǎn)是,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,存取速度較慢。
Java 的棧也位于RAM ,它的存取速度比堆要快,僅次于寄存器且據(jù)可以共享,主要存放一些基本類型的變量和對(duì)象的引用;但存在于棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。
棧有一個(gè)很重要的特殊性,就是存在棧中的數(shù)據(jù)可以共享。假設(shè)我們同時(shí)定義:
int a = 3; |
int b = 3 ; |
編譯器先處理int a = 3 ;首先它會(huì)在棧中創(chuàng)建一個(gè)變量為a的引用,然后查找棧中是否有3這個(gè)值,如果沒找到,就將3存放進(jìn)來(lái),然后將a指向3。接著處理int b = 3;在創(chuàng)建完b的引用變量后,由于在棧中查找到3這個(gè)值,便將b直接指向3。這樣,就出現(xiàn)了a與b同時(shí)均指向3的情況。
這時(shí),如果再令a=4 ;那么編譯器會(huì)重新搜索棧中是否有4值,如果沒有,則將4存放進(jìn)來(lái),并令a指向4;如果已經(jīng)有了,則直接將a指向這個(gè)地址。因此a值的改變不會(huì)影響到b的值。
好了,我們?cè)賮?lái)看上面的例子:
Integer i1 = 12; |
Integer i2 = 12; |
System.out.println(i1==i2); //true |
由于是自動(dòng)包裝,對(duì)于從–128 到127之間的值,它們被包裝為Integer對(duì)象后,會(huì)存在內(nèi)存中被重用,因此輸出的是true;
i1=new Integer(13); |
i2=new Integer(13); |
System.out.println(i1==i2); //flase |
由于使用的是new 操作符,而不是自動(dòng)包裝功能,Java在堆里面創(chuàng)建了兩個(gè)Integer對(duì)象,分別與i1和i2關(guān)聯(lián),由于==對(duì)于對(duì)象比較的是引用,所以輸出是false;
然而下面的語(yǔ)句中實(shí)際上只創(chuàng)建了一個(gè)對(duì)象,這里又出現(xiàn)的別名的現(xiàn)象
i1=i2=new Integer(14); |
System.out.println(i1==i2); |
因此用第一種方式創(chuàng)建多個(gè)int, 在內(nèi)存中其實(shí)只存在一個(gè)對(duì)象而已.這種寫法有利與節(jié)省內(nèi)存空間.同時(shí)它可以在一定程度上提高程序的運(yùn)行速度,因?yàn)镴VM會(huì)自動(dòng)根據(jù)棧中數(shù)據(jù)的實(shí)際情況來(lái)決定是否有必要?jiǎng)?chuàng)建新對(duì)象。而對(duì)于Integer i = new Integer (int);的代碼,則一概在堆中創(chuàng)建新對(duì)象,而不管其字符串值是否相等,是否有必要?jiǎng)?chuàng)建新對(duì)象,從而加重了程序的負(fù)擔(dān)。
好了,這樣以來(lái)相信下面的程序也不會(huì)為我們帶來(lái)太多的疑惑了:
public static void main(String args[]) { Integer i1 = new Integer( 13 ); Integer i2 = new Integer( 13 ); int i3 = 13 ; System.out.println(i1 == i2); System.out.println(i2 == i3); System.out.println(i3 == i1); } /* Output * false * true * true */
賦值操作符“= ”對(duì)我們來(lái)說(shuō)是再熟悉不過(guò)的了。它取右邊的值(右值)復(fù)制給左邊的值(左值),左值必須是一個(gè)明確的已命名的變量。例如:
Integer n1=new Integer(13); |
Integer n2=new Integer(14); |
n2=n1; // 別名現(xiàn)象 |
n2=11; |
System.out.println(n1); //11 |
了解了Java 的存儲(chǔ)分配就不難知道,對(duì)于基本類型復(fù)制的是值,對(duì)于對(duì)象復(fù)制的則是對(duì)象的引用。
進(jìn)行賦值的時(shí)候Java 編譯器會(huì)自動(dòng)檢查左值和右值的類型是否匹配,必要的時(shí)候會(huì)洗的進(jìn)行類型轉(zhuǎn)換。
一方面Java 中允許我們顯式的進(jìn)行類型轉(zhuǎn)換,例如:
int i=(int)1.7; //i 的值1 ,如例子所示將浮點(diǎn)類型轉(zhuǎn)化為整型是Java總是對(duì)數(shù)字執(zhí)行截尾而非舍入 |
另一方面,編譯器會(huì)自動(dòng)進(jìn)行類型的提升,例如:
int i=13; |
long l=i; |
Java 的自動(dòng)提升確實(shí)提供了一定的便利,然而更多的是讓我們陷入一種迷茫之中,例如:
short s=1; |
s=s+1; |
運(yùn)行的時(shí)候編譯器會(huì)提示“可能損失精度”。首先在s=s+1; 語(yǔ)句中由于Java將整型常量1默認(rèn)為int類型,編譯器會(huì)自動(dòng)將s提升為int與1相加之后返回int類型,而s為short類型,則需要進(jìn)行窄化轉(zhuǎn)換,并造成“可能損失精度“。
然而下面的語(yǔ)句卻能順利的通過(guò):
short s=1; |
s+=1; |
許多程序員都會(huì)認(rèn)為E1 op = E2 只是E1 = E1 op E2)的簡(jiǎn)寫方式,而事實(shí)上中講到,復(fù)合賦值E1 op= E2等價(jià)于簡(jiǎn)單賦值E1 = (T)((E1)op(E2)),其中T是E1的類型,除非E1只被計(jì)算一次(參見《Java語(yǔ)言規(guī)范)。可以看到復(fù)合賦值幫我們進(jìn)行了顯式的類型轉(zhuǎn)換。
再來(lái)看下面的例子,
public static void main(String args[]) { int a = 3 ; int b = 7 ; a ^= b ^= a ^= b; System.out.println( " a= " + a + " b= " + b); } /* Output: * a=0 b=3 */
如果你了解C 語(yǔ)言,那么上述表達(dá)式是交換兩個(gè)int類型數(shù)據(jù)的方法之一,然而這在Java中卻不管用,得到的結(jié)果卻是a=0 b=3;
《Java 語(yǔ)言規(guī)范》描述到:操作符的操作數(shù)是從左向右求值的。為了求表達(dá)式x ^= expr的值,x的值是在計(jì)算expr之前被提取的,并且這兩個(gè)值的異或結(jié)果被賦給變量x。因此Java中a^=b^=a^=b;的實(shí)際行為:
int tmp1 = a ; // a在表達(dá)式中第一次出現(xiàn) int tmp2 = b ; // b的第一次出現(xiàn) int tmp3 = a ^ b ; // 計(jì)算a ^ b a = tmp3 ; // 最后一個(gè)賦值:存儲(chǔ)a ^ b 到 a b = tmp2 ^ tmp3 ; // 第二個(gè)賦值:存儲(chǔ)最初的a值到b中 a = tmp1 ^ b ; // 第一個(gè)賦值:存儲(chǔ)0到a中
參考資料
1. 《Thinking in Java, Second Edition 》,Bruce Eckel,Prentice Hall
2. 《Java Puzzlers : Traps, Pitfalls, and Corner Cases 》,Joshua Bloch,Neal Gafter,Addison Wesley/Pearson
3. 《The Java Language Specification 》,James Gosling,bill Joy,Guy Steele,Addison-Wesley Professional
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
