第5章 對象封裝
5.1 何謂封裝
定義類并不等于做好了面向對象中封裝的概念,那么到底什么才有封裝的含義?
5.1.1 封裝對象初始流程
假設要寫個可以管理儲值卡的應用程序眉反,首先得定義儲值卡會記錄哪些數(shù)據(jù)扶认,像是儲值卡號碼慰于、余額酱讶、紅利點數(shù),在Java中可使用class關鍵字進行定義:
package cn.com.speakermore.ch05;
/**
* CashCard.java
* @author mouyo
*/
public class CashCard {
String number;//卡號
int balance;//余額
int bonus;//紅利點
}
假設將這個類定義在cn.com.speakermore.ch05包(默然說話:之所以說假設的意思裤园,主要是你可以把這個類文件放在你喜歡的任何包里撤师,甚至不放包里也行,不過我不建議你不放包里拧揽,因為這會帶來引用的問題剃盾。),使用CashCard.java儲存强法,編譯為CashCard.class万俗,然后將這個class文件給朋友使用,你的朋友要建立5張儲值卡的數(shù)據(jù)饮怯,象這樣:
CashCard card1=new CashCard();
card1.number="0520";
card1.balance=1000;
card1.bonus=1;//一次性存1000可得到1點紅利
CashCard card2=new CashCard();
card2.number="0521";
card2.balance=500;
card2.bonus=0;
CashCard card3=new CashCard();
card3.number="0522";
card3.balance=300;
card3.bonus=0;
應該看到了闰歪,在初始化每張卡片時有大量的重復代碼。只要程序中出現(xiàn)重復的代碼蓖墅,那就意味著有改進的空間库倘。在第4章談過,其實我們可以使用構造函數(shù)來改進這個問題论矾。
構造函數(shù)是與類名同名的方法教翩,不能聲明返回類型。在這個例子里贪壳,構造函數(shù)有3個參數(shù)饱亿,以完成類的三個屬性的初始化工作。這里的this關鍵字是用來引用類成員變量的闰靴,以區(qū)分構造函數(shù)中的同名參數(shù)彪笼。
package cn.com.speakermore.ch05;
/**
* CashCard.java
* @author mouyo
*/
public class CashCard {
String number;//卡號
int balance;//余額
int bonus;//紅利點
public CashCard(String number,int balance,int bonus){
this.number=number;
this.balance=balance;
this.bonus=bonus;
}
}
重新編譯過后,交給你的朋友蚂且,同樣是建立CashCard對象配猫,現(xiàn)在他只要這樣寫就可以了:
CashCard card1=new CashCard(“0521”,1000,1);
CashCard card2=new CashCard(“0522”,500,0);
CashCard card3=new CashCard(“0523”,400,0);
你使用了java的構造方法,實現(xiàn)了對象初始化流程的封裝杏死!有什么好處泵肄?讓使用這個類的用戶不用自己完成對象的初始化工作捆交,只需要簡單的傳遞參數(shù)就可以了,而且將來如果初始化有什么變化腐巢,用戶也不需要修改自己的代碼品追,只要你修改就可以了。
實際上系忙,如果你的朋友想建立5個或更多個CashCard對象诵盼,他可以使用數(shù)組惠豺,而無須一個一個的聲明對象名稱:
/**
* CashApp.java
* @author mouyo
*/
public class CashApp {
public static void main(String[] args) {
CashCard[] cards={
new CashCard("0520",1000,1),
new CashCard("0521",400,0),
new CashCard("0522",500,0),
new CashCard("0523",2000,2),
new CashCard("0524",4000,4),
};
for(CashCard card:cards){
System.out.println("("+card.number+","+card.balance+","+card.bonus+")");
}
}
}
執(zhí)行結果如下:
圖5.1 現(xiàn)金卡輸出
提示:后面的示例都是假設有兩個以上開發(fā)者共同合作開發(fā)银还。記住,如果面向對象或設計上的議題對你來說太抽象洁墙,請從兩人或多人共同開發(fā)的角度來想想看蛹疯,這樣的概念與設計對大家合作有沒有好處。
5.1.2 封裝對象操作流程
假設你的朋友現(xiàn)在使用CashCard創(chuàng)建了3個對象热监,然后他需要對這些儲值卡進行操作捺弦,比如讓客戶存錢。象這樣孝扛。
Scanner input=new Scanner(System.in);
CashCard card1=new CashCard("0521",500,0);
int money=input.nextInt();
if(money>0){//存錢必須是大于0的整數(shù)
card1.balance+=money;
if(money>=1000){
card1.bonus+=money/1000;//每1000元給紅利點數(shù)1點
}
}else{
System.out.println("儲值是負的列吼?你是猴子搬來的救兵嗎,親苦始?");
}
CashCard card2=new CashCard("0522",500,0);
money=input.nextInt();
if(money>0){//存錢必須是大于0的整數(shù)
card2.balance+=money;
if(money>=1000){
card2.bonus+=money/1000;//每1000元給紅利點數(shù)1點
}
}else{
System.out.println("儲值是負的寞钥?你是猴子搬來的救兵嗎,親陌选?");
}
CashCard card3=new CashCard("0523",500,0);
……
你的朋友拿到了儲值卡總要做點事理郑,所以自然就寫出上面的代碼:完成儲值驗證。首先儲值肯定應該是大于0的咨油,其次每1000元累加紅利一點您炉。很容易就可以發(fā)現(xiàn),驗證代碼是重復的役电。你朋友不覺得有啥赚爵,反正復制粘貼呀。
正在你朋友復制粘貼得正嗨時法瑟,老板來說冀膝,說紅利點要修改,把每1000元累加1點紅利瓢谢,要改為800元累加1點紅利畸写。你朋友傻眼了,因為他已經復制粘貼了300張卡氓扛,這一個一個改過來枯芬。论笔。。千所。狂魔。。淫痰。哎最楷,不知道你明白我的意思了沒有?我的意思是待错,在開發(fā)界有個潛規(guī)則:復制粘貼是萬惡首籽孙!
好吧,你的朋友正在哭鼻子的時候火俄,你想了想犯建,儲值這個動作應該就是CashCard這個對象自己的事情!我們可以定義一個方法來解決這個問題:
package cn.com.speakermore.ch05;
/**
* CashCard.java
* @author mouyong
*/
public class CashCard {
String number;//卡號
int balance;//余額
int bonus;//紅利點
public CashCard(){
}
/**
* 更簡單的初始化構造函數(shù)
* @param number 卡號
* @param balance 余額
* @param bonus 紅利點
*無返回值
*/
public CashCard(String number,int balance,int bonus){
this.number=number;
this.balance=balance;
this.bonus=bonus;
}
/**
* 儲值時將調用此方法完成儲值驗證<br />
* 1.儲值的錢應該大于0<br />
* 2.每1000元累加紅利1點
* @param money 存到儲值卡的錢數(shù)瓜客,單位:元
*/
public void store(int money){
if(money>0){
this.balance+=money;
if(money>=1000){
this.bonus+=money/1000;
}
}else{
System.out.println("儲值是負的适瓦?你是猴子搬來的救兵嗎,親谱仪?");
}
}
/**
* 取款時將調用此方法<br />
* 1.取款數(shù)額應該大于0
* 2.取款數(shù)額應該小于余額數(shù)
* @param money 取出的錢玻熙,單位:元
*/
public void charge(int money){
if(money>0){
if(money<=this.balance){
this.balance-=money;
}else{
System.out.println("錢不夠呀!");
}
}else{
System.out.println("取負數(shù)疯攒?你是猴子搬來的救兵嗎嗦随,親?");
}
}
/**
* 兌換紅利點
* 1.紅利點應該大于0
* 2.紅利點數(shù)應該小于等于已有的紅利點
* @param bonus 要兌換的紅利卸例,單位:點
* @return 紅利余額
*/
public int exchange(int bonus){
if(bonus>0){
if(bonus<=this.bonus){
this.bonus-=bonus;
}else{
System.out.println("紅利點數(shù)不夠呀");
}
}else{
System.out.println("取負數(shù)称杨?你是猴子搬來的救兵嗎,親筷转?");
}
return this.bonus;
}
}
既然存款要驗證姑原,那取款是不是也要驗證呢?兌換紅利點是不是也要驗證呢呜舒?所以锭汛,你直接寫了store()方法、charge()方法和exchange()方法袭蝗。在類中定義方法唤殴,如果不用返回值,方法名稱前應該聲明void到腥。
好了朵逝,現(xiàn)在你的朋友開心了,因為他要做的事情又簡單乡范,現(xiàn)在儲值的代碼變成了這樣了:
Scanner input=new Scanner(System.in);
CashCard card1=new CashCard("0521",500,0);
card1.store(input.nextInt());
CashCard card2=new CashCard("0522",500,0);
card2.store(input.nextInt());
CashCard card3=new CashCard("0523",500,0);
card3.store(input.nextInt());
好處是什么配名?如果現(xiàn)在老板要發(fā)瘋改業(yè)務啤咽,你只要修改封裝到CashCard中的業(yè)務方法就可以了,而不用去重復的修改大量的代碼渠脉。而你的朋友更輕松宇整,啥都不用做了(默然說話:話說,你是不是覺得有點鼻子酸酸的芋膘?眼睛濕濕的鳞青?難道你就是那個“誰入地獄”里的“誰”么?)
提示:在java命名規(guī)范中为朋,方法名稱首字母統(tǒng)統(tǒng)小寫臂拓。
5.1.3 封裝對象內部數(shù)據(jù)
在前面的例子中,你在CashCard類上定義了store()等方法潜腻,你是“希望”使用CashCard類的朋友這樣寫代碼:
CashCard card1=new CashCard("0521",500,0);
card1.store(input.nextInt());
因為只有這樣埃儿,你所做的判斷和限制才能起作用,不至于讓儲值卡上的錢出現(xiàn)負數(shù)等不正常的情況融涣。
但是,這個希望完全就是一廂情愿的精钮,因為你有可能沒時間向你的朋友說明CashCard應該怎么使用威鹿,你的朋友也許是個自作聰明的家伙,根本就不聽你說什么轨香。在這樣的情況一下忽你,你的朋友完全可能這樣在寫代碼:
CashCard card1=new CashCard("0521",500,0);
card1.balance+=input.nextInt();
card1.bouns+=100;
好吧,余額和紅利全亂套了臂容。問題在哪兒科雳?因為你沒有封裝CashCard中不想讓用戶直接存取的數(shù)據(jù)(余額、紅利)脓杉,如果有些數(shù)據(jù)是類私有的糟秘,那么你的朋友就無法操作。在java中可以使用private關鍵字來定義:
package cn.com.speakermore.ch05;
/**
* CashCard.java
* @author mouyong
*/
public class CashCard {
private String number;//卡號:使用private定義私有成員
private int balance;//余額:使用private定義私有成員
private int bonus;//紅利點:使用private定義私有成員
…略
/**
* 儲值時將調用此方法完成儲值驗證<br />
* 1.儲值的錢應該大于0<br />
* 2.每1000元累加紅利1點
* @param money 存到儲值卡的錢數(shù)球散,單位:元
*/
public void store(int money)< ------------------ 要修改余額尿赚,必須通過store()了
if(money>0){
this.balance+=money;
if(money>=1000){
this.bonus+=money/1000;
}
}else{
System.out.println("儲值是負的?你是猴子搬來的救兵嗎蕉堰,親凌净?");
}
}
/**
* 提供取值方法,獲得卡號
* @return the number
*/
public String getNumber() {
return number;
}
/**
* 提供取值方法屋讶,獲得余額
* @return the balance
*/
public int getBalance() {
return balance;
}
/**
* 提供取值方法冰寻,獲得紅利點
* @return the bonus
*/
public int getBonus() {
return bonus;
}
}
當我們這樣修改了CashCard之后,你朋友會發(fā)現(xiàn)他的代碼各種報錯了皿渗,這是因為你把number斩芭、balance和bounus全部私有化之后没卸,編譯程序再也不允許你的朋友直接訪問這些成員變量了。
圖5.2 坑爹的“私有屬性不能被訪問”報錯(圖中的中文報錯信息是netbeans顯示的秒旋,明顯錯了约计!不是可以訪問,而是不能被訪問)
如果沒有提供方法存取private成員迁筛,那用戶就不能存取煤蚌。在CashCard的例子中,如果想修改balance或bonus细卧,就一定得通過store()尉桩、charge、exchange()等方法贪庙,也就一定得經過你定義的流程蜘犁。
除非你愿意提供取值方法(getter),讓用戶可以取得number止邮、balance與bonus的值这橙,否則用戶一定無法取得〉寂基于你的愿意屈扎,CashCard類上定義了getNumber()、getBalance()與getBonus()等取值方法撩匕,所以可以這樣修改程序:
/**
* CashApp.java
* @author mouyong
*/
public class CashApp {
public static void main(String[] args) {
CashCard[] cards={
new CashCard("0520",1000,1),
new CashCard("0521",400,0),
new CashCard("0522",500,0),
new CashCard("0523",2000,2),
new CashCard("0524",4000,4),
};
for(CashCard card:cards){
System.out.println("("+card.getNumber()+","+card.getBalance()+","+card.getBonus()+")");
}
}
}
在java命名規(guī)范中(默然說話:這個規(guī)范有個屌炸天的名字——JavaBean鹰晨!)取值方法是專門做了規(guī)定的,也就是以get開頭止毕,之后接上首字母大寫的屬性單詞模蜡。因為有這個規(guī)范,所以一系列get方法完全不用你手工敲出來忍疾,iDE可以代勞,以NetBeans為例令漂,可以在源代碼中右擊膝昆,選擇“重構----封裝字段…”,在彈出的對話框中選擇你需要添加的方法叠必。
圖5.3 封裝字段
只要在那些復選框中打上勾荚孵,NetBeans就能生成對應的get(取值)或set(設值)方法。哦纬朝,當然了收叶,別忘記勾完了點下“重構”那個按鈕。
所以你封裝了什么共苛?封裝了類私有數(shù)據(jù)判没,讓使用者無法直接存取蜓萄,而必須通過你提供的操作方法,經過你定義的操作方法澄峰,經過你定義的流程才有可能存取私有數(shù)據(jù)嫉沽。事實上,使用者也無從得知你的類有哪些私有數(shù)據(jù)俏竞,使用者不會知道對象的內部細節(jié)绸硕。
在這里對封裝做個總結,封裝目的主要是隱藏對象細節(jié)魂毁,將對象當作黑箱進行操作玻佩。就如前面的例子,使用者會調用構造函數(shù)席楚,但不知道構造函數(shù)的細節(jié)咬崔,使用者會調用方法,但不知道方法的流程烦秩,使用者也不會知道有哪些私有數(shù)據(jù)垮斯,要操作對象,一律得通過你提供的方法調用闻镶。
private也可以用在方法或構造函數(shù)聲明上甚脉,私有方法或構造函數(shù)通常是類內部某個共享的流程,外界不用知道私有方法的存在铆农。private也可以用在內部類聲明,內部類會在稍后說明狡耻。
提示:私有構造函數(shù)的使用比較高級墩剖,有興趣的話可以參考“單例模式”:
http://openhome.cc/Gossip/DesignPattern/SingletonPattern.htm
5.2 類語法細節(jié)
面向對象概念是抽象的,不同程序語言會用不同語法來支持概念的實現(xiàn)夷狰。前一節(jié)討論過面向對象中封裝的通用概念岭皂,以及如何用java語法實現(xiàn),接下來則要討論java的特定語法細節(jié)沼头。
5.2.1 public權限修飾
前一節(jié)的CashCard類是定義在cn.com.speakermore.ch05包中爷绘,假設現(xiàn)在為了管理上的需求,要將CashCard類定義到另一個包中进倍,那么如果CashCard的相關方法(store土至、charge等方法)沒有加上public的聲明,你會發(fā)現(xiàn)在CardApp中均報錯了猾昆。即使你使用import語句導入了CashCard也一樣會報錯陶因。這就是“包私有權限”。如果不同包的類程序代碼想要直接使用垂蜗,必須聲明為public的楷扬。
首先類要成為public的解幽,這表示它是個公開類,可以在其他包的類中使用烘苹。接著是構造函數(shù)也要成為public的躲株,這表示其他包的方法中可以直接調用這個方法。最后是方法也應該成為public的镣衡,這表示它可以在其他包中被調用霜定。
總結一下,包管理其實還有權限管理上的概念捆探,沒有定義任何權限關鍵字時然爆,就是包權限。在Java中其實有private黍图、protected和public三個權限訪問修飾符曾雕,你已經認識了private與public的使用了,protected在下一章說明(默然說話:其實也就是private與public用得最多了助被。)
提示:如果類沒有聲明public剖张,那么類就不能在別的包中實例化,這樣即使你在類中聲明了public方法也無法調用揩环,所以類前的public聲明是很重要的搔弄。另外還要說明的是,一個類文件里只能聲明一個public的類丰滑,而且這個類的名字必須與文件名相同顾犹,大小寫敏感。這也是為何現(xiàn)在我們都是只在一個類文件中寫一 個類的原因:基本上絕大部分類應該為公有褒墨,不然怎么在別的包中使用呢炫刷?
5.2.2 關于構造函數(shù)
在定義類時,可以使用構造函數(shù)定義對象建立的初始流程郁妈。構造函數(shù)是與類名稱同名浑玛,無須聲明返回類型的方法(默然說話:構造函數(shù)一定不能聲明返回類型!構造函數(shù)一定不能聲明返回類型噩咪!構造函數(shù)一定不能聲明返回類型顾彰!重要的事說三遍!N改搿涨享!)。
/**
* Thing.java
* @author mouyong
*/
public class Thing {
private int value=0;//手工指定了初始化值0
private String str;//默認值為null
public Thing(String str,int value) {
this.str = str;
this.value=value;
}
}
如果象下面這樣創(chuàng)建Thing對象书在,則value和str會被初始化兩次:
Thing some=new Thing(“Hello”,10);
數(shù)據(jù)類型 | 初始值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0F |
double | 0.0D |
char | \u0000 |
boolean | false |
類 | null |
創(chuàng)建對象時灰伟,虛擬機會首先對數(shù)據(jù)成員進行初始化,如果你沒有指定初始值,則有默認值栏账。默認值如表5.1
表5.1 數(shù)據(jù)成員初始值
數(shù)據(jù)類型 | 初始值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0F |
double | 0.0D |
char | \u0000 |
boolean | false |
類 | null |
所以使用new創(chuàng)建Thing對象時帖族,value和str會被初始化為0和null,之后又被構造函數(shù)初始化挡爵。如果在定義類時沒有寫任何的構造函數(shù)竖般,編譯器會自動加入一個無參數(shù)的構造函數(shù),我們稱之為默認構造函數(shù)茶鹃。它什么代碼都沒有涣雕,也就是啥事也不做。
提示:只有編譯器自動加入無參構造函數(shù)才能稱為默認構造函數(shù)闭翩,自己寫的只能叫無參構造函數(shù)挣郭,這個在平時不是很嚴格區(qū)別,不過如果你要考試疗韵,可得注意了兑障。
如果你寫過一個構造函數(shù),編譯器就不會再幫你加上默認構造函數(shù)了蕉汪。這時的現(xiàn)象就是你不能再使用new Thing()這樣的形式創(chuàng)建對象了流译。所以,如果你還是希望使用默認的形式來創(chuàng)建對象者疤,一旦你寫了構造函數(shù)福澡,你就得手工添加上這個無參的構造函數(shù)。
5.2.3 構造函數(shù)與方法重載
因為使用者的環(huán)境或條件不同 驹马,創(chuàng)建對象時也許希望有對應的初始流程革砸。可以定義多個構造函數(shù)糯累,只要參數(shù)類型或個數(shù)不同业岁,這稱為重載(Overload)構造函數(shù)。例如:
/**
* OverloadTest.java
* @author mouyo
*/
public class OverloadTest {
private int a=10;
private String text="n.a.";
public OverloadTest(int a){
if(a>0){
this.a=a;
}
}
public OverloadTest(int a,String text){
if(a>0){
this.a=a;
}
if(text!=null){
this.text=text;
}
}
public static void main(String[] args) {
}
}
這個測試類在創(chuàng)建對象時可以有兩種選擇:一種是new OverLoadTest(100)寇蚊,另一種是new OverLoadTest(100,”O(jiān)K”)。
提示:通常我們定義了有參構造函數(shù)之后棍好,都應加上無參構造函數(shù)仗岸,即使內容為空也無所謂,這主要是為了日后使用上的彈性借笙。例如扒怖,運用反射機制生成對象,或者繼承時方便調用父類構造函數(shù)等业稼。其實主要還是因為jDK奇怪的設置:一旦我們寫過有參的構造函數(shù)之后盗痒,無參構造函數(shù)就不再默認加上了,所以就有點麻煩。
普通方法也可以進行重載俯邓,可為類似功能的方法提供統(tǒng)一的方法名稱骡楼,但參數(shù)類型或個數(shù)各不相同就可以了。比如前面大量使用的System.out.println()方法就提供了多個版本稽鞭。
System.out.println();
System.out.println(Object o);
System.out.println(boolean b);
System.out.println(char c);
System.out.println(String x);
雖然名稱都叫println()鸟整,但根據(jù)傳遞的來自變量類型不同,會調用對應的方法朦蕴。
方法重載最大的好處是讓程序設計人員不用苦惱為每個方法取不同的名字篮条,程序使用者也不需要為記住如此眾多極其相似的名字而崩潰。
注意:返回值類型不可作為方法重載的依據(jù)吩抓,這非常非常重要涉茧,切記切記!例如下面的代碼可是會劃紅波浪的疹娶!
public class Sample{
public int some(int i){
return 0;
}
public double some(int i){
return 0.0;
}
}
5.2.4 使用this
除了被聲明為static的地方外伴栓,this關鍵字可以出現(xiàn)在類中任何地方,它是一個代詞蚓胸,可以翻譯作“我”子巾,指代“當前對象”科贬。最常見的用法就是在構造函數(shù)中用于區(qū)別同名的構造參數(shù)與類屬性。
/**
* 5.2.4使用this
* @author mouyong
*/
public class Person {
private String name;
private Integer id;
private Date birthday;
public Person() {
}
public Person(String name, Integer id) {
this.name = name;
this.id = id;
}
public Person(String name,Integer id,Date birthday){
//this.name的意思,就是“我的屬性name”鸿捧,=name的意思就是"賦值為參數(shù)name"
this.name=name;
this.id=id;
this.birthday=birthday;
}
}
如果代碼出現(xiàn)重復,我的腦袋中的“警鐘”就要響起來瞧哟,重復的代碼會為后期的維護帶來麻煩并闲,所以能夠重用的代碼,絕對不寫兩遍叹哭,例如:
/**
* 5.2.4使用this
* @author mouyong
*/
public class Person {
private String name;
private Integer id;
private Date birthday;
public Person() {
}
public Person(String name, Integer id) {
this.name = name;
this.id = id;
}
public Person(String name,Integer id,Date birthday){
//使用this()調用其它構造函數(shù)來消除重復代碼忍宋,實現(xiàn)代碼重用
this(name,id);
this.birthday=birthday;
}
}
在Java中,this()代表調用另一個構造函數(shù)风罩,至于是哪一個糠排,JVM會根據(jù)你所傳入的參數(shù)數(shù)量與類型進行智能判斷。在上例中超升,this(a)會調用public Person(String name,Integer id)版本的構造函數(shù)入宦,再執(zhí)行后續(xù)代碼。
注意:this()調用只能出現(xiàn)在構造函數(shù)中室琢,且只能出現(xiàn)在構造函數(shù)的第一行乾闰。
在創(chuàng)建對象之后,調用構造函數(shù)之前盈滴,如果有想執(zhí)行的代碼(默然說話:真是一個奇怪的想法呀涯肩,但在實際中還真的存在這樣特殊的情況),可以使用一對大括號{}來定義“塊代碼”。我們有個教學例子:
/**
* 代碼塊(就是一對大括號{})測試
* @author mouyong
*/
public class CodeBlockTest {
{
//直接在類里開個大括號病苗,明顯很奇怪呢疗垛,呵呵~
System.out.println("這是一個代碼塊,注意看它執(zhí)行的時間");
}
public CodeBlockTest() {
System.out.println("默認構造函數(shù)");
}
public CodeBlockTest(int t){
this();
System.out.println("帶參構造函數(shù)");
}
public static void main(String[] args) {
new CodeBlockTest(1);
}
}
在這個例子中铅乡,調用了CodeBlockTest(int t)版本的構造函數(shù)继谚,第一行使用了this()來調用默認的構造函數(shù)。而我們看代碼輸出的順序會發(fā)現(xiàn)阵幸,代碼塊(就是那個大括號)里的輸出語句比兩個構造函數(shù)里的輸出語句都要先執(zhí)行花履。所以結果是:
這是一個代碼塊,注意看它執(zhí)行的時間
默認構造函數(shù)
帶參構造函數(shù)
在3.2.1節(jié)介紹過final關鍵字挚赊,如果局部變量聲明了final诡壁,表示設置后就不能再改動,對象的成員變量也可以聲明final荠割,如下:
class SpacialSample{
final int x=0;
}
這樣妹卿,x就不能再被賦值了,否則會編譯錯誤蔑鹦。但是夺克,有時候我們會不給它賦值,象下面這樣:
class SpacialSample{
final int x;
public SpacialSample(){
}
}
上面代碼的錯誤不會顯示在x聲明的地方嚎朽,而是顯示在構造函數(shù)的位置铺纽。
圖5.4 final成員變量的“延遲賦值”
編譯器會認為你準備進行“延遲賦值”,也就是說哟忍,編譯器會轉去檢查構造函數(shù)中是否有為這個final成員變量賦值的代碼狡门,如果沒有,則報錯锅很。
(默然說話:所以其馏,還是不要玩這么高級的語法了,這完全是在制造混亂呀爆安。咱們還是乖乖地在每個final聲明之后就立即賦值吧叛复。)
5.2.5 static類成員
在以前學過圓的面積計算,大家都知道:圓面積=半徑平方*π扔仓,而π就是一個常量致扯,它約等于3.14。我們來寫一個類Circle当辐,模擬這個計算過程。
/**
* 計算圓面積
* @author mouyong
*/
public class Circle {
private double radius;
final double PI=3.14;
public double getArea(){
return getRadius()*getRadius()*PI;
}
public Circle(){
radius=0;
}
public Circle(double radius){
this.radius=radius;
}
/**
* @return 半徑
*/
public double getRadius() {
return radius;
}
/**
* @param radius 設置半徑
*/
public void setRadius(double radius) {
this.radius = radius;
}
}
圖5.5 非static變量實例化后在每個對象中均占有空間
如果創(chuàng)建了多個Circle對象鲤看,那每個對象都會有自己的radius與PI成員缘揪,但是PI是個固定的常數(shù),并不需要在每個對象中都保存一次。我們可以給PI上聲明static找筝,表示它屬于類(類屬性):
public class Circle {
private double radius;
static final double PI=3.14;
……
}
圖5.6 static成員在對象中并不占有空間
聲明為static的成員變量蹈垢,可以使用類名進行引用,象這樣:
System.out.println(Circle.PI);
也就是通過“類名.static成員變量”的形式獲得static成員變量的值袖裕。除了成員變量曹抬,方法的前面也可以使用static,成為靜態(tài)方法急鳄,讓這個方法屬于類(類方法):
public class TeachSample {
public static void staticSimple(){
System.out.println("這是一個靜態(tài)方法谤民,可以使用類名.方法名()的方式調用");
}
}
聲明為靜態(tài)的方法也可以通過“類名.方法名()”的形式調用,象下面這樣:
TeachSample.staticSimple();
在Java中對靜態(tài)成員的引用疾宏,除了使用類张足,還允許使用對象名,但非常不贊成這樣的寫法坎藐。(默然說話:對的为牍,非常不贊成使用“對象名.靜態(tài)成員”的形式對靜態(tài)變量或靜態(tài)方法進行引用,因為這樣很容易造成誤解岩馍。而且碉咆,如果你真的會寫成“對象名.靜態(tài)成員”的形式,這本身就暗示著你的代碼上存在著代碼缺陷蛀恩。)
Java程序設計領域疫铜,有非常多好的命名習慣,比如:只有類名才會大寫首字母赦肋,static成員一定是通過類名來引用的块攒。所以,當我們看到一直以來在使用的“System.out”時佃乘,你要知道:“System”是一個Java類囱井,而后面的“out”是一個static成員變量。還有前面用過的Integer.parseInt()趣避,同樣庞呕,Integer是一個類,而parseInt()則是一個static方法程帕。
由于static成員是屬于類的住练,它不屬于任何個別對象,所以在static成員中使用this愁拭,會是一種錯誤(默然說話:this是一個對象讲逛,它表示“當前對象”,但是使用“類名.方法名()”引用的成員方法在沒有創(chuàng)建當前對象的時候就已經在執(zhí)行代碼了岭埠,所以編譯器會提示錯誤信息)盏混。
圖5.7 無法從靜態(tài)上下文中引用非靜態(tài)變量this
由于靜態(tài)方法是屬于類的蔚鸥,所以在執(zhí)行靜態(tài)方法時并不需要創(chuàng)建對象,而非靜態(tài)的成員變量均在創(chuàng)建對象之后才有了內存空間许赃,所以靜態(tài)方法中是不允許使用非靜態(tài)的成員變量的止喷,只能使用靜態(tài)的成員變量(默然說話:除了靜態(tài)成員變量可以在靜態(tài)方法中使用之外,在靜態(tài)方法中也可以聲明非靜態(tài)的局部變量混聊,并可以正常使用)弹谁。
同樣的道理,非靜態(tài)方法也是在創(chuàng)建對象之后才加載到內存的句喜,所以靜態(tài)方法中也只能調用到其他的靜態(tài)方法预愤,而不能調用非靜態(tài)的方法,只允許調用其他靜態(tài)方法(*默然說話:對了藤滥,似乎忘記陳述一個事實:所有的代碼必須加載進內存鳖粟,才能被計算機執(zhí)行,切記切記拙绊。這個沒有為什么向图,只是烏龜?shù)钠ü伞?guī)定(龜腚)!)标沪。
圖5.8 無法從靜態(tài)上下文中引用非靜態(tài)方法
如果有些代碼希望在程序加載之后就執(zhí)行榄攀,把這些代碼放在類的靜態(tài)塊之中是個好想法。靜態(tài)塊就是在前面所說的“塊代碼”(默然說話:就是在“使用this”小節(jié)講到的金句,那個寫在類中莫名其妙的大括號)前面加上static關鍵字檩赢。
public class CodeBlockTest {
{
//直接在類里開個大括號,顯示很奇怪呢违寞,呵呵~
System.out.println("這是一個代碼塊贞瞒,注意看它執(zhí)行的時間");
}
static{
//在塊的前面加static,就得到了static塊(靜態(tài)塊)
//靜態(tài)塊是在程序加載時就被執(zhí)行的趁曼,所以它早于塊
//靜態(tài)塊只在程序加載時被執(zhí)行军浆,所以,多次new該對象時并不會多次執(zhí)行靜態(tài)塊
System.out.println("靜態(tài)塊挡闰,它會什么時候執(zhí)行呢乒融?");
}
public CodeBlockTest() {
System.out.println("默認構造函數(shù)");
}
public CodeBlockTest(int t){
this();
System.out.println("帶參構造函數(shù)");
}
public static void main(String[] args) {
new CodeBlockTest(1);
new CodeBlockTest(1);
new CodeBlockTest(1);
}
}
它的執(zhí)行結果如下:
run:
靜態(tài)塊,它會什么時候執(zhí)行呢摄悯?
這是一個代碼塊赞季,注意看它執(zhí)行的時間
默認構造函數(shù)
帶參構造函數(shù)
這是一個代碼塊,注意看它執(zhí)行的時間
默認構造函數(shù)
帶參構造函數(shù)
這是一個代碼塊奢驯,注意看它執(zhí)行的時間
默認構造函數(shù)
帶參構造函數(shù)
成功構建 (總時間: 2 秒)
有些時候我們會發(fā)現(xiàn)申钩,如“System.out.println()”這樣的代碼,因為每次引用類的靜態(tài)成員時瘪阁,總是要先敲類名典蜕,再敲變量或方法名断盛,寫起來怪長的。在JDK5之后愉舔,又新增了一個import static語法,使用它可以讓我們在引用靜態(tài)成員時有效縮短書寫長度伙菜,提高效率轩缤。
import static java.lang.System.*;
public class TeachSample {
public void importStaticSample(){
out.println("這樣使用可以短一些,還是蠻方便的贩绕!");
}
}
5.2.6 不定長參數(shù)
在Java中火的,我們一直使用固定數(shù)量的方法參數(shù)來寫方法,如果同樣的方法名淑倾,不同的參數(shù)馏鹤,我們使用方法重載來完成,以減輕程序員的命名困擾娇哆。但有的時候我們會遇到方法傳入的參數(shù)不固定的問題湃累,例如治力,“通過一個方法來完成所有學生的總分計算”勃黍。在這里,學生的人數(shù)不是固定的马澈,而且它可能會很多弄息,也可能只有幾個人,所以疑枯,如果你通過方法重載的方式來完成這個問題,那會發(fā)現(xiàn)你可能需要寫幾十個方法废亭,并且這幾十個方法里的代碼都是一樣的(默然說話:叮!重復代碼敲警鐘>咴俊豆村!)。當然骂删,我們也可以只設置一個參數(shù)掌动,這個參數(shù)使用數(shù)組來完成參數(shù)的傳入(默然說話:這個解決方案其實挺不錯的四啰,我就比較喜歡)。但我們仍然會遇到一些問題粗恢,比如數(shù)組的構建需要額外的代碼(默然說話:你要實例化數(shù)組柑晒,設置數(shù)組長度,還要一個數(shù)一個數(shù)的裝到數(shù)組中 ,這些都有可能造成代碼上的麻煩眷射,使得代碼不夠優(yōu)雅和簡潔)匙赞。在JDK5之后,提供了不定長參數(shù)妖碉,可以讓我們輕松的解決這個問題(默然說話:再次聲明涌庭,用數(shù)組做參數(shù)的解決方案并無問題,不過欧宜,我們不應該介意多一種解決方案的坐榆,不是么?)冗茸。
public class TeachSample{
public static int caclSum(int…scores){
int sum=0;
for(int score:scores){
sum+=score;
}
}
}
實際上不定長參數(shù)只是一個優(yōu)化的寫法席镀,在實際編譯后,你會發(fā)現(xiàn)其實它還是使用了一個數(shù)組蚀狰。另外愉昆,要注意的問題是,一個方法只能聲明一個不定長度的參數(shù)跛溉,而且只能是最后一個參數(shù)能聲明為不定長度的參數(shù)芳室。
5.2.7 內部類
可以在類中再定義類,這就叫內部類(Inner Class)伍宦。不過這個特性似乎很少被用到(默然說話:的確很難想到必須使用內部類才能解決的問題次洼,所以現(xiàn)在內部類的概念在各種教材中幾乎都不提了,最主要的原因還是它在概念上的難以理解以及書寫上的復雜與啰嗦)亥啦。
內部類可以使用public翔脱、protected或private聲明粱坤,例如:
public Class TeachSample{
private class InnerClassSample{
}
}
內部類本身可以存取外部類的成員(使用成員變量瓷产,調用成員方法)
內部類可以使用static關鍵字(默然說話:這是比較令人驚訝的,因為Java的外部類是不能加static的尔邓,可是內部類可以加)梯嗽。內部靜態(tài)類的特點與類的static成員一致灯节,可以存取外部類的靜態(tài)成員,但不能使用非static成員形入。
總的來說亿遂,內部類可以直接訪問外部類的所有資源,包括public苞慢、protected挽放、private和默認權限辑畦,但外部類對內部類卻是一無所知蚯妇,不實例化內部類箩言,就無法使用任何非靜態(tài)資源。所以鸵赖,作為內部類這個特殊的存在饵骨,其主要目的,就是通過內部類去使用外部類的資源饼煞,而不是讓外部類更方便的使用內部類砖瞧。
更夸張的块促,Java還可以在一個方法中聲明類:
public class TeachSample{
public void methodClassTest(){
class InnerTest{
}
}
}
以上僅只是純教學實例。在現(xiàn)實中斋扰,方法內部類的寫法其實更多是以匿名類的形式出現(xiàn)屎鳍,但由于它的寫法實在是太啰嗦逮壁,所以JDK8中提出了Lambda,第9章與第12章會再討論忧饭。
5.2.8 傳值調用
在C眷昆、C++語句中作媚,都存在方法/函數(shù)的參數(shù)傳遞方式的不同漂问,一般都有按值傳遞(Call by value)與按引用(Call by references)傳遞,而Java中只有按值傳遞方式磷仰。但是Java中的按值傳遞在參數(shù)為一個實例時的情況看上去是比較復雜的》晗恚看例子:
/**
* 傳值調用的測試
* @author mouyong
*/
public class CallTest {
public static void main(String[] args){
Student stu1=new Student("默然說話");
//調用第一個測試,看姓名會不會被改變
test1(stu1);
System.out.println("測試1:"+stu1.name);
Student stu2=new Student("默然說話");
//調用第二個測試侧但,看姓名會不會被改變
test2(stu2);
System.out.println("測試2:"+stu2.name);
}
/**
* 將學生姓名修改為"新生1"妇多,看看能不能成功
* @param stu 學生對象
*/
public static void test1(Student stu){
stu.name="新生1";
}
/**
* 將學生對象用"新生1"的新對象替換了者祖,看看能不能成功
* @param stu 學生對象
*/
public static void test2(Student stu){
stu=new Student("新生1");
}
}
/**
* 用于傳值調用測試的學生類
* @author mouyong
*/
class Student{
//為了專注于主題茫舶,此處使用了包私有的訪問修飾
String name;
//用于在初始化時就可以給學生姓名的構造方法
public Student(String name){
this.name=name;
}
}
上面的例子執(zhí)行結果如下:
圖5.9 一個關于Java值傳遞的例子(代碼執(zhí)行結果)
上面的例子說明了什么?讓我們先從值傳遞和引用傳遞的概念說起吧古程。
一門語言挣磨,只要是允許定義函數(shù)或方法,那就避免不了參數(shù)的傳遞問題晤锥。因為一個函數(shù)或方法本身是被進行封裝的,也就是說,它是相對獨立的部分册烈,那么戈泼,它要如何獲得外部的信息婿禽,以完成自己的功能呢?答案就是通過參數(shù)大猛。在以前的語言中扭倾,傳遞參數(shù)的方式有兩種,一種是通過傳遞一個復制品給函數(shù)或方法挽绩,這樣膛壹,你對復制品的任何修改链方,都不會影響到原參數(shù)。而另一種則是通過傳遞原參數(shù)給函數(shù)或方法剿涮,在這種情況下,你對參數(shù)的任何修改萨赁,都是修改了原參數(shù),原參數(shù)就會被一個函數(shù)或方法改變了撒桨,甚至換成了另一個不同的對象。
Java語言在傳遞對象的時候企锌,是把對象的引用以值傳遞的形式進行的萍鲸,所以备徐,在test1中,stu1的姓名被修改了洪规,但是因為是值傳遞叉谜,所以在test2中對參數(shù)stu的重新賦值并不影響到stu2锭碳,所以我們看到輸出的姓名仍然是默然說話栈妆,而沒有變成新生1霞扬。這就是Java的“按值傳遞”:將對象的引用以值傳遞的方式傳遞給了Java方法。
5.3 重點復習
對象封裝的目的谴返,就是為了隱藏細節(jié),這樣在使用者使用對象時不會受到細節(jié)的困擾败去,這樣可以更大的發(fā)揮創(chuàng)造性吨铸。在Java中握巢,我們可以使用構造方法進行對象的初始化封裝,使用普通方法對操作過程進行封裝槐秧,我們還可以使用private關鍵字封裝對象的數(shù)據(jù)成員啄踊。
在使用private封裝成員變量之后,如果我們要對私有的成員變量進行存取刁标,記得使用命名規(guī)范規(guī)定的setter設置器與getter獲取器颠通。
Java有一個讓人迷惑的“包私有(package private)”權限,也稱為默認權限命雀。它是在你沒在類成員前面添加任何訪問修飾符時起效蒜哀,只有在相同包的類才能對它進行訪問斩箫。非常不推薦使用這個權限吏砂,這也意味著撵儿,你應該為每個類成員添加訪問修飾符(public、protected狐血、private)淀歇。
創(chuàng)建對象時,成員變量會進行初始化匈织,如果沒有指定初始值浪默,則會使用默認值初始化。
如果定義類時缀匕,沒有寫過任何構造方法纳决,編譯程序會自動加入默認無參的構造方法∠缧。可以對構造方法進行重載阔加。
this關鍵字代表當前對象,可以出現(xiàn)在類的任何位置满钟。this()代表調用本類的另一個構造方法胜榔,只能出現(xiàn)在構造方法的第一行代碼。調用哪個構造方法由傳入的參數(shù)類型與數(shù)量決定湃番。
聲明為static的成員為類成員夭织,盡管類成員也允許你使用對象名來調用,但我們應該不要使用這種形式吠撮,而應使用“類.靜態(tài)成員”的形式進行調用尊惰。
JDK5之后支持不定長參數(shù),但實際只是數(shù)組傳參的變態(tài)寫法纬向,而且限制一個方法只能有一個參數(shù)且只能是最后一個參數(shù)可以使用不定長的寫法择浊,所以不是很實用。
5.4 課后練習
5.4.1 選擇題
1.如果有以下的程序代碼:
public class Sample1{
private Sample1 sample;
private Sample1(){}
public static Sample create(){
if(sample==null){
sample=new Sample1();
}
return sample;
}
}
以下描述正確的是()
A.編譯失敗
B.必須new Sample1()產生Sample1實例
C.必須new Sample1().create()產生Sample1實例
D.必須Sample1.create()產生Sample1實例
2.如果有以下的程序片斷:
int[] scores1={88,81,74,86,77,65,85,93,99};
int[] scores2=Arrays.copyOf(scores1,scores1.length);
其中Arrays來自java.util.Arrays逾条,以下描述正確的是()
A.Arrays.copyOf()應該改為new Arrays().copyOf()
B.copyOf()是static成員
C.copyOf()是public成員
D.Arrays被聲明為public
3.如果有以下的程序代碼:
public class Sample1{
public int x;
public Sample1(int x){
this.x=x;
}
}
以下描述正確的是()
A. 創(chuàng)建Sample1時琢岩,可使用new Sample1()或new Sample1(10)形式
B.創(chuàng)建Sample1時,只能使用new Sample1()形式
C.創(chuàng)建Sample1時师脂,只能使用new Sample1(10)形式
D.無默認構造方法担孔,所以編譯失敗
4.如果有以下的程序片段:
public class Sample1{
public int x;
public Sample1(int x){
x=x;
}
}
以下描述正確的是()
A. new Sample1(10)創(chuàng)建對象后,對象成員x值為10
B.new Sample1(10)創(chuàng)建對象后吃警,對象成員x值為0
C.Sample1 sample=new Sample1(10)后糕篇,可使用sample.x取得10
D.編譯失敗
5.如果有以下的程序片段:
public class Sample1{
private int x;
public Sample1(int x){
this.x=x;
}
}
以下描述正確的是()
A. new Sample1(10)創(chuàng)建對象后,對象成員x值為10
B.new Sample1(10)創(chuàng)建對象后酌心,對象成員x值為0
C.Sample1 sample=new Sample1(10)后拌消,可使用sample.x取得10
D.編譯失敗
6.如果有以下的程序片段:
package com.speakermore.util
class Sample1{
public int x;
public Sample1(int x){
this.x=x;
}
}
以下描述正確的是()
A. com.speakermore.util包中其他類可以new Sample1(10)
B.com.speakermore.util包外其他類可以new Sample1(10)
C.可以在其他包import com.speakermore.util.Sample1
D.編譯失敗
7.如果有以下的程序片段:
public class Sample1{
private final int x;
public Sample1(){};
public Sample1(int x){
this.x=x;
}
}
以下描述正確的是()
A. new Sample1(10)創(chuàng)建對象后,對象成員x值為10
B.new Sample1(10)創(chuàng)建對象后安券,對象成員x值為0
C.Sample1 sample=new Sample1(10)后墩崩,可使用sample.x取得10
D.編譯失敗
8.如果有以下的程序片段:
public class Sample1{
public static int sum(int … nums){
int sum=0;
for(int i=0;i<nums.length;i++){
sum+=nums[i];
}
return sum;
}
}
以下描述正確的是()
A. 可使用Sample1.sum(1,2,3)加總1氓英,2,3
B.可使用new Sample1().Sample1.sum(1,2,3)加總1鹦筹,2铝阐,3
C.可使用Sample1.sum(new int[1,2,3])加總1,2铐拐,3
D.編譯失敗,因為不定長度參數(shù)只能用增強for語法
9.如果有以下的程序片段:
public class Sample1{
public static void someMethod(int i){
System.out.println(“int版本被調用”);
}
public static void someMethod(Integer integer){
System.out.println(“Integer版本被調用”);
}
}
以下描述正確的是()
A. Sample1.someMethod(1)顯示“int版本被調用”
B.Sample1.someMethod(1)顯示“Integer版本被調用”
C.Sample1.someMethod(new Integer(1))顯示“int版本被調用”
D.編譯失敗
10.如果有以下的程序片段:
public class Main{
public int some(int … nums){
int sum=0;
for(int num:nums){
sum+=num;
}
return sum;
}
public static void main(String[] args){
System.out.println(sum(1,2,3));
}
}
以下描述正確的是()
A. 顯示6
B.顯示1
C.無法執(zhí)行
D.編譯失敗
5.4.2 操作題
1.據(jù)說古代有座波羅教塔由3支鉆石棒支撐徘键,神在第一根棒上放置64個由小到大排列的金盤,命令僧侶將所有金盤從第一根棒移至第三根棒遍蟋,搬運過程遵守大盤在小盤下的原則若每日僅搬一盤吹害,在盤子全數(shù)搬至第三根棒,此截將毀損虚青。請寫一個程序赠制,可輸入任意盤數(shù),根據(jù)以上搬運原則顯示搬運過程挟憔。
2.如果有個二維數(shù)組代表迷宮如下钟些,0表示道路,2表示墻壁:
int[][] maze={
{2,2,2,2,2,2,2},
{0,0,0,0,0,0,2},
{2,0,2,0,2,0,2},
{2,0,0,2,0,2,2},
{2,2,0,2,0,2,2},
{2,0,0,0,0,0,2},
{2,2,2,2,2,0,2},
}
假設老鼠會從索引(1,0)開始绊谭,請使用程序找出老鼠如何跑至索引(6,5)位置政恍,并以■代表墻,◇代表老鼠达传,顯示走出迷宮路徑篙耗。
- 有個8乘8棋盤,騎士走法為西洋棋走法宪赶,請編寫程序宗弯,可指定騎士從棋盤任一位置出發(fā),以標號顯示走完所有位置搂妻。例如其中一個走法:
52 21 64 47 50 23 40 3
63 46 51 22 55 2 49 24
20 53 62 59 48 41 4 39
61 58 45 54 1 56 25 30
44 19 60 57 42 29 38 5
13 16 43 14 37 8 31 26
18 35 14 11 28 33 6 9
15 12 17 36 7 10 27 32
4.國際象棋中皇后可直線前進蒙保,吃掉遇到的棋子,如果棋盤上有8個皇后欲主,請編寫程序邓厕,顯示8個皇后相安無事地放置在棋盤上的所有方式。