對於這個系列裡的問題,每個學Java的人都應該搞懂。當然,如果只是學Java玩玩就無所謂了。如果你認為自己已經超越初學者了,卻不很懂這些問題,請將你自己重歸初學者行列。內容均來自於CSDN的經典老貼。
問題一:我聲明了什麼!
Strings="Helloworld!";
許多人都做過這樣的事情,但是,我們到底聲明了什麼?回答通常是:一個String,內容是“Helloworld!”。這樣模糊的回答通常是概念不清的根源。如果要准確的回答,一半的人大概會回答錯誤。
這個語句聲明的是一個指向對象的引用,名為“s”,可以指向類型為String的任何對象,目前指向"Helloworld!"這個String類型的對象。這就是真正發生的事情。我們並沒有聲明一個String對象,我們只是聲明了一個只能指向String對象的引用變量。所以,如果在剛才那句語句後面,如果再運行一句:
Stringstring=s;
我們是聲明了另外一個只能指向String對象的引用,名為string,並沒有第二個對象產生,string還是指向原來那個對象,也就是,和s指向同一個對象。
問題二:"=="和equals方法究竟有什麼區別?
==操作符專門用來比較變量的值是否相等。比較好理解的一點是:
inta=10;
intb=10;
則a==b將是true。
但不好理解的地方是:
Stringa=newString("foo");
Stringb=newString("foo");
則a==b將返回false。
根據前一帖說過,對象變量其實是一個引用,它們的值是指向對象所在的內存地址,而不是對象本身。a和b都使用了new操作符,意味著將在內存中產生兩個內容為"foo"的字符串,既然是“兩個”,它們自然位於不同的內存地址。a和b的值其實是兩個不同的內存地址的值,所以使用"=="操作符,結果會是false。誠然,a和b所指的對象,它們的內容都是"foo",應該是“相等”,但是==操作符並不涉及到對象內容的比較。
對象內容的比較,正是equals方法做的事。
看一下Object對象的equals方法是如何實現的:
booleanequals(Objecto){
returnthis==o;
}
Object對象默認使用了==操作符。所以如果你自創的類沒有覆蓋equals方法,那你的類使用equals和使用==會得到同樣的結果。同樣也可以看出,Object的equals方法沒有達到equals方法應該達到的目標:比較兩個對象內容是否相等。因為答案應該由類的創建者決定,所以Object把這個任務留給了類的創建者。
看一下一個極端的類:
ClassMonster{
privateStringcontent;
...
booleanequals(Objectanother){returntrue;}
}
我覆蓋了equals方法。這個實現會導致無論Monster實例內容如何,它們之間的比較永遠返回true。
所以當你是用equals方法判斷對象的內容是否相等,請不要想當然。因為可能你認為相等,而這個類的作者不這樣認為,而類的equals方法的實現是由他掌握的。如果你需要使用equals方法,或者使用任何基於散列碼的集合(HashSet,HashMap,HashTable),請察看一下Javadoc以確認這個類的equals邏輯是如何實現的。
問題三:String到底變了沒有?
沒有。因為String被設計成不可變(immutable)類,所以它的所有對象都是不可變對象。請看下列代碼:
Strings="Hello";
s=s+"world!";
s所指向的對象是否改變了呢?從本系列第一篇的結論很容易導出這個結論。我們來看看發生了什麼事情。在這段代碼中,s原先指向一個String對象,內容是"Hello",然後我們對s進行了+操作,那麼s所指向的那個對象是否發生了改變呢?答案是沒有。這時,s不指向原來那個對象了,而指向了另一個String對象,內容為"Helloworld!",原來那個對象還存在於內存之中,只是s這個引用變量不再指向它了。
通過上面的說明,我們很容易導出另一個結論,如果經常對字符串進行各種各樣的修改,或者說,不可預見的修改,那麼使用String來代表字符串的話會引起很大的內存開銷。因為String對象建立之後不能再改變,所以對於每一個不同的字符串,都需要一個String對象來表示。這時,應該考慮使用StringBuffer類,它允許修改,而不是每個不同的字符串都要生成一個新的對象。並且,這兩種類的對象轉換十分容易。
同時,我們還可以知道,如果要使用內容相同的字符串,不必每次都new一個String。例如我們要在構造器中對一個名叫s的String引用變量進行初始化,把它設置為初始值,應當這樣做:
publicclassDemo{
privateStrings;
...
publicDemo{
s="Initialvalue";
}
...
}
而非
s=newString("Initialvalue");
後者每次都會調用構造器,生成新對象,性能低下且內存開銷大,並且沒有意義,因為String對象不可改變,所以對於內容相同的字符串,只要一個String對象來表示就可以了。也就說,多次調用上面的構造器創建多個對象,他們的String類型屬性s都指向同一個對象。
上面的結論還基於這樣一個事實:對於字符串常量,如果內容相同,Java認為它們代表同一個String對象。而用關鍵字new調用構造器,總是會創建一個新的對象,無論內容是否相同。
至於為什麼要把String類設計成不可變類,是它的用途決定的。其實不只String,很多Java標准類庫中的類都是不可變的。在開發一個系統的時候,我們有時候也需要設計不可變類,來傳遞一組相關的值,這也是面向對象思想的體現。不可變類有一些優點,比如因為它的對象是只讀的,所以多線程並發訪問也不會有任何問題。當然也有一些缺點,比如每個不同的狀態都要一個對象來代表,可能會造成性能上的問題。所以Java標准類庫還提供了一個可變版本,即StringBuffer。
問題四:final關鍵字到底修飾了什麼?
final使得被修飾的變量"不變",但是由於對象型變量的本質是“引用”,使得“不變”也有了兩種含義:引用本身的不變,和引用指向的對象不變。
引用本身的不變:
finalStringBuffera=newStringBuffer("immutable");
finalStringBufferb=newStringBuffer("notimmutable");
a=b;//編譯期錯誤
引用指向的對象不變:
finalStringBuffera=newStringBuffer("immutable");
a.append("broken!");file://編譯通過
可見,final只對引用的“值”(也即它所指向的那個對象的內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象的變化,final是不負責的。這很類似==操作符:==操作符只負責引用的“值”相等,至於這個地址所指向的對象內容是否相等,==操作符是不管的。
理解final問題有很重要的含義。許多程序漏洞都基於此----final只能保證引用永遠指向固定對象,不能保證那個對象的狀態不變。在多線程的操作中,一個對象會被多個線程共享或修改,一個線程對對象無意識的修改可能會導致另一個使用此對象的線程崩潰。一個錯誤的解決方法就是在此對象新建的時候把它聲明為final,意圖使得它“永遠不變”。其實那是徒勞的。
問題五:到底要怎麼樣初始化!
本問題討論變量的初始化,所以先來看一下Java中有哪些種類的變量。
1.類的屬性,或者叫值域
2.方法裡的局部變量
3.方法的參數
對於第一種變量,Java虛擬機會自動進行初始化。如果給出了初始值,則初始化為該初始值。如果沒有給出,則把它初始化為該類型變量的默認初始值。
int類型變量默認初始值為0
float類型變量默認初始值為0.0f
double類型變量默認初始值為0.0
boolean類型變量默認初始值為false
char類型變量默認初始值為0(ASCII碼)
long類型變量默認初始值為0
所有對象引用類型變量默認初始值為null,即不指向任何對象。注意數組本身也是對象,所以沒有初始化的數組引用在自動初始化後其值也是null。
對於兩種不同的類屬性,static屬性與instance屬性,初始化的時機是不同的。instance屬性在創建實例的時候初始化,static屬性在類加載,也就是第一次用到這個類的時候初始化,對於後來的實例的創建,不再次進行初始化。這個問題會在以後的系列中進行詳細討論。
對於第二種變量,必須明確地進行初始化。如果再沒有初始化之前就試圖使用它,編譯器會抗議。如果初始化的語句在try塊中或if塊中,也必須要讓它在第一次使用前一定能夠得到賦值。也就是說,把初始化語句放在只有if塊的條件判斷語句中編譯器也會抗議,因為執行的時候可能不符合if後面的判斷條件,如此一來初始化語句就不會被執行了,這就違反了局部變量使用前必須初始化的規定。但如果在else塊中也有初始化語句,就可以通過編譯,因為無論如何,總有至少一條初始化語句會被執行,不會發生使用前未被初始化的事情。對於try-catch也是一樣,如果只有在try塊裡才有初始化語句,編譯部通過。如果在catch或finally裡也有,則可以通過編譯。總之,要保證局部變量在使用之前一定被初始化了。所以,一個好的做法是在聲明他們的時候就初始化他們,如果不知道要出事化成什麼值好,就用上面的默認值吧!
其實第三種變量和第二種本質上是一樣的,都是方法中的局部變量。只不過作為參數,肯定是被初始化過的,傳入的值就是初始值,所以不需要初始化。
問題六:instanceof是什麼東東?
instanceof是Java的一個二元操作符,和==,>,<是同一類東東。由於它是由字母組成的,所以也是Java的保留關鍵字。它的作用是測試它左邊的對象是否是它右邊的類的實例,返回boolean類型的數據。舉個例子:
Strings="IAManObject!";
booleanisObject=sinstanceofObject;
我們聲明了一個String對象引用,指向一個String對象,然後用instancof來測試它所指向的對象是否是Object類的一個實例,顯然,這是真的,所以返回true,也就是isObject的值為True。
instanceof有一些用處。比如我們寫了一個處理賬單的系統,其中有這樣三個類:
publicclassBill{//省略細節}
publicclassPhoneBillextendsBill{//省略細節}
publicclassGasBillextendsBill{//省略細節}
在處理程序裡有一個方法,接受一個Bill類型的對象,計算金額。假設兩種賬單計算方法不同,而傳入的Bill對象可能是兩種中的任何一種,所以要用instanceof來判斷:
publicdoublecalculate(Billbill){
if(billinstanceofPhoneBill){
file://計算電話賬單
}
if(billinstanceofGasBill){
file://計算燃氣賬單
}
...
}
這樣就可以用一個方法處理兩種子類。
然而,這種做法通常被認為是沒有好好利用面向對象中的多態性。其實上面的功能要求用方法重載完全可以實現,這是面向對象變成應有的做法,避免回到結構化編程模式。只要提供兩個名字和返回值都相同,接受參數類型不同的方法就可以了:
publicdoublecalculate(PhoneBillbill){
file://計算電話賬單
}
publicdoublecalculate(GasBillbill){
file://計算燃氣賬單
}
所以,使用instanceof在絕大多數情況下並不是推薦的做法,應當好好利用多態。
Java編程規范
命名規范
定義這個規范的目的是讓項目中所有的文檔都看起來像一個人寫的,增加可讀性,減少項目組中因為換人而帶來的損失。(這些規范並不是一定要絕對遵守,但是一定要讓程序有良好的可讀性)
Package的命名
Package的名字應該都是由一個小寫單詞組成。
Class的命名
Class的名字必須由大寫字母開頭而其他字母都小寫的單詞組成
Class變量的命名
變量的名字必須用一個小寫字母開頭。後面的單詞用大寫字母開頭。
StaticFinal變量的命名
StaticFinal變量的名字應該都大寫,並且指出完整含義。
參數的命名
參數的名字必須和變量的命名規范一致。
數組的命名
數組應該總是用下面的方式來命名:
byte[]buffer;
而不是:
bytebuffer[];
方法的參數
使用有意義的參數命名,如果可能的話,使用和要賦值的字段一樣的名字:
SetCounter(intsize){
this.size=size;
}
Java文件樣式
所有的Java(*.Java)文件都必須遵守如下的樣式規則
版權信息
版權信息必須在Java文件的開頭,比如:
/**
*Copyright®2000ShanghaiXXXCo.Ltd.
*Allrightreserved.
*/
其他不需要出現在Javadoc的信息也可以包含在這裡。
Package/Imports
package行要在import行之前,import中標准的包名要在本地的包名之前,而且按照字母順序排列。如果import行中包含了同一個包中的不同子目錄,則應該用*來處理。
packagehotlava.Net.stats;
importJava.io.*;
importJava.util.Observable;
importhotlava.util.Application;
這裡Java.io.*使用來代替InputStreamandOutputStream的。
Class
接下來的是類的注釋,一般是用來解釋類的。
/**
*Aclassrepresentingasetofpacketandbytecounters
*Itisobservabletoallowittobewatched,butonly
*reportschangeswhenthecurrentsetiscomplete
*/
接下來是類定義,包含了在不同的行的extends和implements
publicclassCounterSet
extendsObservable
implementsCloneable
ClassFIElds
接下來是類的成員變量:
/**
*Packetcounters
*/
protectedint[]packets;
public的成員變量必須生成文檔(JavaDoc)。proceted、private和package定義的成員變量如果名字含義明確的話,可以沒有注釋。
存取方法
接下來是類變量的存取的方法。它只是簡單的用來將類的變量賦值獲取值的話,可以簡單的寫在一行上。
/**
*Getthecounters
*@returnanarraycontainingthestatisticaldata.Thisarrayhasbeen
*freshlyallocatedandcanbemodifIEdbythecaller.
*/
publicint[]getPackets(){returncopyArray(packets,offset);}
publicint[]getBytes(){returncopyArray(bytes,offset);}
publicint[]getPackets(){returnpackets;}
publicvoidsetPackets(int[]packets){this.packets=packets;}
其它的方法不要寫在一行上
構造函數
接下來是構造函數,它應該用遞增的方式寫(比如:參數多的寫在後面)。
訪問類型("public","private"等.)和任何"static","final"或"synchronized"應該在一行中,並且方法和參數另寫一行,這樣可以使方法和參數更易讀。
public
CounterSet(intsize){
this.size=size;
}
克隆方法
如果這個類是可以被克隆的,那麼下一步就是clone方法:
public
Objectclone(){
try{
CounterSetobj=(CounterSet)super.clone();
obj.packets=(int[])packets.clone();
obj.size=size;
returnobj;
}catch(CloneNotSupportedExceptione){
thrownewInternalError("UnexpectedCloneNotSUpportedException:"+e.getMessage());
}
}
類方法
下面開始寫類的方法:
/**
*Setthepacketcounters
*(suchaswhenrestoringfromadatabase)
*/
protectedfinal
voidsetArray(int[]r1,int[]r2,int[]r3,int[]r4)
throwsIllegalArgumentException
{
//
//Ensurethearraysareofequalsize
//
if(r1.length!=r2.length||r1.length!=r3.length||r1.length!=r4.length)
thrownewIllegalArgumentException("Arraysmustbeofthesamesize");
System.arraycopy(r1,0,r3,0,r1.length);
System.arraycopy(r2,0,r4,0,r1.length);
}
toString方法
無論如何,每一個類都應該定義toString方法:
public
StringtoString(){
Stringretval="CounterSet:";
for(inti=0;iretval+=data.bytes.toString();
retval+=data.packets.toString();
}
returnretval;
}
}
main方法
如果main(String[])方法已經定義了,那麼它應該寫在類的底部.
代碼編寫格式
代碼樣式
代碼應該用unix的格式,而不是Windows的(比如:回車變成回車+換行)
文檔化
必須用javadoc來為類生成文檔。不僅因為它是標准,這也是被各種Java編譯器都認可的方法。使用@author標記是不被推薦的,因為代碼不應該是被個人擁有的。
縮進
縮進應該是每行2個空格.不要在源文件中保存Tab字符.在使用不同的源代碼管理工具時Tab字符將因為用戶設置的不同而擴展為不同的寬度.
如果你使用UltrEdit作為你的Java源代碼編輯器的話,你可以通過如下操作來禁止保存Tab字符,方法是通過UltrEdit中先設定Tab使用的長度室2個空格,然後用Format|TabstoSpaces菜單將Tab轉換為空格。
頁寬
頁寬應該設置為80字符.源代碼一般不會超過這個寬度,並導致無法完整顯示,但這一設置也可以靈活調整.在任何情況下,超長的語句應該在一個逗號或者一個操作符後折行.一條語句折行後,應該比原來的語句再縮進2個字符.
{}對
{}中的語句應該單獨作為一行.例如,下面的第1行是錯誤的,第2行是正確的:
if(i>0){i++};//錯誤,{和}在同一行
if(i>0){
i++
};//正確,{單獨作為一行
}語句永遠單獨作為一行.
如果}語句應該縮進到與其相對應的{那一行相對齊的位置。
括號
左括號和後一個字符之間不應該出現空格,同樣,右括號和前一個字符之間也不應該出現空格.下面的例子說明括號和空格的錯誤及正確使用:
CallProc(AParameter);//錯誤
CallProc(AParameter);//正確
不要在語句中使用無意義的括號.括號只應該為達到某種目的而出現在源代碼中。下面的例子說明錯誤和正確的用法:
if((I)=42){//錯誤-括號毫無意義
if(I==42)or(J==42)then//正確-的確需要括號
程序編寫規范
exit()
exit除了在main中可以被調用外,其他的地方不應該調用。因為這樣做不給任何代碼代碼機會來截獲退出。一個類似後台服務地程序不應該因為某一個庫模塊決定了要退出就退出。
異常
申明的錯誤應該拋出一個RuntimeException或者派生的異常。
頂層的main()函數應該截獲所有的異常,並且打印(或者記錄在日志中)在屏幕上。
垃圾收集
Java使用成熟的後台垃圾收集技術來代替引用計數。但是這樣會導致一個問題:你必須在使用完對象的實例以後進行清場工作。比如一個prel的程序員可能這麼寫:
...
{
FileOutputStreamfos=newFileOutputStream(projectFile);
project.save(fos,"IDEProjectFile");
}
...
除非輸出流一出作用域就關閉,非引用計數的程序語言,比如Java,是不能自動完成變量的清場工作的。必須象下面一樣寫:
FileOutputStreamfos=newFileOutputStream(projectFile);
project.save(fos,"IDEProjectFile");
fos.close();
Clone
下面是一種有用的方法:
implementsCloneable
public
Objectclone()
{
try{
ThisClassobj=(ThisClass)super.clone();
obj.field1=(int[])fIEld1.clone();
obj.field2=fIEld2;
returnobj;
}catch(CloneNotSupportedExceptione){
thrownewInternalError("UnexpectedCloneNotSUpportedException:"+e.getMessage());
}
}
final類
絕對不要因為性能的原因將類定義為final的(除非程序的框架要求)
如果一個類還沒有准備好被繼承,最好在類文檔中注明,而不要將她定義為final的。這是因為沒有人可以保證會不會由於什麼原因需要繼承她。
訪問類的成員變量
大部分的類成員變量應該定義為protected的來防止繼承類使用他們。
注意,要用"int[]packets",而不是"intpackets[]",後一種永遠也不要用。
publicvoidsetPackets(int[]packets){this.packets=packets;}
CounterSet(intsize)
{
this.size=size;
}
編程技巧
byte數組轉換到characters
為了將byte數組轉換到characters,你可以這麼做:
"Helloworld!".getBytes();
Utility類
Utility類(僅僅提供方法的類)應該被申明為抽象的來防止被繼承或被初始化。
初始化
下面的代碼是一種很好的初始化數組的方法:
objectArguments=newObject[]{arguments};
枚舉類型
Java對枚舉的支持不好,但是下面的代碼是一種很有用的模板:
classColour{
publicstaticfinalColourBLACK=newColour(0,0,0);
publicstaticfinalColourRED=newColour(0xFF,0,0);
publicstaticfinalColourGREEN=newColour(0,0xFF,0);
publicstaticfinalColourBLUE=newColour(0,0,0xFF);
publicstaticfinalColourWHITE=newColour(0xFF,0xFF,0xFF);
}
這種技術實現了RED,GREEN,BLUE等可以象其他語言的枚舉類型一樣使用的常量。他們可以用==操作符來比較。
但是這樣使用有一個缺陷:如果一個用戶用這樣的方法來創建顏色BLACK
newColour(0,0,0)
那麼這就是另外一個對象,==操作符就會產生錯誤。她的equal()方法仍然有效。由於這個原因,這個技術的缺陷最好注明在文檔中,或者只在自己的包中使用。
Swing
避免使用AWT組件
混合使用AWT和Swing組件
如果要將AWT組件和Swing組件混合起來使用的話,請小心使用。實際上,盡量不要將他們混合起來使用。
滾動的AWT組件
AWT組件絕對不要用JScrollPane類來實現滾動。滾動AWT組件的時候一定要用AWTScrollPane組件來實現。
避免在InternalFrame組件中使用AWT組件
盡量不要這麼做,要不然會出現不可預料的後果。
Z-Order問題
AWT組件總是顯示在Swing組件之上。當使用包含AWT組件的POP-UP菜單的時候要小心,盡量不要這樣使用。
調試
調試在軟件開發中是一個很重要的部分,存在軟件生命周期的各個部分中。調試能夠用配置開、關是最基本的。
很常用的一種調試方法就是用一個PrintStream類成員,在沒有定義調試流的時候就為null,類要定義一個debug方法來設置調試用的流。
性能
在寫代碼的時候,從頭至尾都應該考慮性能問題。這不是說時間都應該浪費在優化代碼上,而是我們時刻應該提醒自己要注意代碼的效率。比如:如果沒有時間來實現一個高效的算法,那麼我們應該在文檔中記錄下來,以便在以後有空的時候再來實現她。
不是所有的人都同意在寫代碼的時候應該優化性能這個觀點的,他們認為性能優化的問題應該在項目的後期再去考慮,也就是在程序的輪廓已經實現了以後。
不必要的對象構造
不要在循環中構造和釋放對象
使用StringBuffer對象
在處理String的時候要盡量使用StringBuffer類,StringBuffer類是構成String類的基礎。String類將StringBuffer類封裝了起來,(以花費更多時間為代價)為開發人員提供了一個安全的接口。當我們在構造字符串的時候,我們應該用StringBuffer來實現大部分的工作,當工作完成後將StringBuffer對象再轉換為需要的String對象。比如:如果有一個字符串必須不斷地在其後添加許多字符來完成構造,那麼我們應該使用StringBuffer對象和她的append()方法。如果我們用String對象代替StringBuffer對象的話,會花費許多不必要的創建和釋放對象的CPU時間。
避免太多的使用synchronized關鍵字
避免不必要的使用關鍵字synchronized,應該在必要的時候再使用她,這是一個避免死鎖的好方法。
可移植性
BorlandJbulider不喜歡synchronized這個關鍵字,如果你的斷點設在這些關鍵字的作用域內的話,調試的時候你會發現的斷點會到處亂跳,讓你不知所措。除非必須,盡量不要使用。
換行
如果需要換行的話,盡量用println來代替在字符串中使用"
"。
你不要這樣:
System.out.print("Hello,world!
");
要這樣:
System.out.println("Hello,world!");
或者你構造一個帶換行符的字符串,至少要象這樣:
Stringnewline=System.getProperty("line.separator");
System.out.println("Helloworld"+newline);
PrintStream
PrintStream已經被不贊成(deprecated)使用,用PrintWrite來代替她。