程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 冒號和他的學生們(連載24)——對象封裝

冒號和他的學生們(連載24)——對象封裝

編輯:關於JAVA

24.對象封裝

陰陽地理兩分張,隱者為陰顯者陽               ——《玉髓經.曜星論》

“用廣東話說,真是有型有料又有性格啊!”歎號啧啧連聲,“這哪裡是在設計軟件,分明是在設計心儀的對象嘛。”

“我們可不就是在談對象設計嗎?”冒號笑著反問,“在OOP的世界裡,每位程序員都是造物主。保持熱情、專注力和審美情趣,說不定哪一天就像希臘神話裡的皮格瑪利翁一樣,雕塑的美女變活了。”

“哇,那可就美了!”逗號極盡誇張之調。

全班哄堂大笑。

“剛才提到抽象是OOP三大基本特性的基礎,下面我們逐個剖析。”冒號很快收攏了話題,“首當其沖的是封裝性。記得前面談對象范式時,引號曾試圖為我們解釋封裝性,可惜被我無情地打斷了。現在我們請他繼續講解吧。”

在眾人逗趣式的掌聲中,引號竟有些腼腆了:“所謂封裝性,就是將數據與相關行為包裝在一起以實現信息隱藏。”

“幾乎無懈可擊。”冒號贊揚得有些保守,“那麼封裝(encapsulation)與信息隱藏(information hiding)有區別嗎?”

“應該是一回事吧。”在冒號的逼視下,引號有些猶豫了,“嗯。。。信息隱藏是一種原則,而封裝是實現這種原則的一種方式。”

“言之有理!”冒號這回贊揚得很干脆,“盡管大多數參考書對二者不加區分,我還是要解析一番。其實廣義的封裝僅僅只是一種打包,即package或bundle,是密封的但可以是透明的。或者說,封裝就是把一些數據和方法裝在一個封閉的盒子裡——可能是黑盒子,也可能是白盒子。從語法上說,這是OOP與諸如C之類的過程式語言最大的不同。請問這帶來什麼效果?”

句號反應很快:“這等於引入了一種新的模塊機制,將相關的數據和作用其上的運算捆綁在一起形成被稱為類的模塊。”

“回答正確!”冒號很滿意,“剛才我們用C實現了隊列,但由於C不支持封裝,只能以文件形式來劃分模塊,顯然不如類劃分那麼方便和明晰。此外,封裝還有語法糖(Syntactic sugar)效果。”

問號好奇地問:“什麼是語法糖?是不是很甜?”

“所謂語法糖,就是一些語法上的甜頭。它不是核心語法,並沒有提供任何額外的功能,只是用起來更簡潔實用、更自然方便,看起來更酷、更炫而已。”冒號有意用時髦的詞匯來填補代溝,“我們知道,過程式函數采用謂語(主語,賓語)的形式,而OOP采用主語.謂語(賓語)的形式。”

“哦,就是那個狗吃屎和吃狗屎啊,那可不甜。”逗號又來插科打诨。

眾人笑得前仰後合。

冒號不為所動:“再拿隊列為例,如果增加一個隊列成員,用剛才的C實現,我們需要寫下:queue_add(queue, item)。假如用Java來實現,只需寫queue.add(item)。由於封裝使add綁定在queue上,一方面可以將對象queue前置,既更符合自然語言,又少敲一個字符;另一方面,這種綁定使add局限於Queue類中,因此不必加上‘queue_’的前綴以防與其他類的方法函數名相沖突。這同樣節省了打字,也使接口更簡單。”

句號提出:“如果C支持函數重載(overload),那麼‘queue_’的前綴就可省去。”

“你說的既對也不對。”冒號辯證地評判,“如果C支持重載,該前綴的確能省去;但從另一角度看,即使Java或C++不支持重載,前綴用樣能省去。因為函數add已經不再是全局函數,Queue類就是其上下文(context)。換句話說,分屬不同類的函數是不可能產生歧義(ambiguity)的,哪怕它們的簽名(signature)一模一樣。因此我們要把功勞記在封裝的名下。”

句號心悅誠服。

冒號繼續講解:“狹義的封裝是在打包的基礎上加上訪問控制(access control),以實現信息隱藏。相對於上述廣義的封裝,不妨認為多了一個將白盒子刷成黑盒子的過程。這一過程可以看作對抽象的一種補充:抽象意味著用戶可以從高層的接口來看待或使用一類對象,而不用關心它底層的實現,而黑盒封裝意味著用戶無權訪問底層的實現。”

逗號有點茫然:“那談起封裝,究竟指哪一個?”

“一般所說的封裝大多是狹義的。”冒號回復道,“考試中最無趣的一類試題就是名詞解釋,因為那只能印證記憶,不能印證理解。軟件編程中也有無數的名詞和概念,機械式的記憶沒有任何意義——除了面試時應付某些同樣無趣的考官。我們在這裡著意诠釋封裝的概念,不是出於學術理論的目的,而是為了讓大家深刻體會封裝的目的和意義,以便在實踐中靈活運用。”

問號詢問:“前面提到,代碼既要合法又要合理,那訪問控制還重要嗎?”

“合法合理是對程序員的要求。對於語言,我們還是希望它盡可能地提供更多的保障。這就好比社會和諧不能只靠法律,但法制當然越健全越好。”冒號解答道,“訪問控制不僅是一種語法限制,也是一種語義規范——標有public的公用接口對代碼閱讀者而言,顯然比注釋文檔更正式更直觀。因此,其重要性是不言而喻的。值得一提的是,訪問控制也不是滴水不漏的。C++用戶可以通過指針來間接訪問private成員,Java也可以通過反射機制來訪問。”

見眾人頗有疑義,冒號便寫了一段Java代碼——

// 通過反射機制訪問私有變量

import java.lang.reflect.*;
class Private
{
   private String field = "這是私有變量";
   private void method()
   {
     System.out.println("調用私有方法");
   }
}
public class AccessTest
{
   public static void main(String[] args) throws Exception
   {
     Private privateObj = new Private();
     Field f = Private.class.getDeclaredField("field");
     f.setAccessible(true);
     System.out.println(f.get(privateObj));
     Method m = Private.class.getDeclaredMethod("method", new Class[0]);
     m.setAccessible(true);
     m.invoke(privateObj, new Object[0]);
   }
}

冒號講述道:“運行這段代碼,可以看到privateObj的域成員和方法成員都被訪問了。這是一種hack,僅限於特殊用途,不在我們關心之列。問題是,即使不考慮此類非常規做法,要實現信息隱藏也不是件容易的事。”

歎號不解:“信息隱藏困難在哪裡呢?加上private不就隱藏了成員嗎?”

“如果所有信息都隱藏了,這個對象還有什麼用嗎?”冒號一語破的。

逗號一愣:“可以用getter方法返回信息啊。”

冒號更不答話,投影出一段代碼——

import java.util.Date;
import java.util.Calendar;
class User
{
   private Date birthday; /** *//** 生日 */
   private boolean sex; /** *//** 性別。true代表男,false代表女 */
   public User(Date birthday, boolean sex)
   {
     this.birthday = birthday;
     this.sex = sex;
   }
   public Date getBirthday()
   {
     return birthday;
   }
   public void setBirthday(Date birthday)
   {
     this.birthday = birthday;
   }
   public boolean getSex()
   {
     return sex;
   }
   public void setSex(boolean sex)
   {
     this.sex = sex;
   }
   /** *//** 計算年齡,負數表示未知 */
   public int computeAge()
   {
     if (birthday == null) return -1;
     Calendar dob = Calendar.getInstance();
     dob.setTime(birthday);
     Calendar now = Calendar.getInstance();
     int age = now.get(Calendar.YEAR) - dob.get(Calendar.YEAR);
     if (now.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR))
       --age;
     return age;
   }
}

冒號提問:“這段代碼簡單得勿需多言,請問它的信息隱藏做得如何?”

眾人目不轉睛地盯了好一陣,無人應答。

冒號突發驚人之語:“如果我說User所有的方法都違背了信息隱藏原則,你們相信嗎?”

直直的眼睛全都變圓了。

引號忽然明白了:“記得書上曾說不能直接返回類的內部對象。GetBirthday返回Date類型的生日,用戶可以在調用此方法後直接對生日進行操作。”

“說得對極了!”冒號誇贊道,“如果一個方法返回了一個可變(mutable)域對象(field object)的引用,無異於前門緊閉而後門洞開。解決的方法是防御性復制(defensive copying),即返回一個clone的對象,以免授人以柄(handle)。”

逗號有些難以置信:“好像這類做法很普通啊。”

冒號耐心詳解:“首先,請注意可變和引用兩個條件,所有基本類型的域不是引用,因而是安全的,而Java中String之類非基本類由於是不可變的(immutable),也是安全的。同樣,在C++和C#中的非基本類的值類型(value type)也不在此列。此外C++中申明了const的指針或引用返回值也能防止客戶修改。其次,普通的做法不代表是正確的。事實上,恕我直言:普通的程序員是不合格的,合格的程序員是不普通的。最後,信息隱藏原則固然極其重要,但也不是金科玉律,在一定條件下也是允許的。比如僅作數據儲存之用的類甚至可以開放所有的域成員,又比如不同類的對象共享同一引用。此外在一定范圍之內為提高效率也可能采取變通之法,當然是在對用戶曉以利害之後。”

問號舉一反三:“同樣道理,setBirthday也會導致信息洩漏。考慮到Date類型如此常用,Java是不是該引入一個不可變的日期類型呢?”

歎號喃喃自語:“getSex和setSex會有什麼問題呢?boolean是基本類型啊。”

冒號提示:“考慮一下性別的可能性。”

歎號訝然道:“難不成還有不男不女型?”

眾皆大笑。

冒號淡淡一笑:“不排除這種可能。更實際的情況是,有時性別是未知的。”

句號建議:“可以將小boolean換成大Boolean,多一個null值。”

冒號進一步指出:“如果想處理三種以上的可能性,可以采用char類型或String類型。總之這是實現細節,最好不要暴露給客戶。因此不妨將getSex換成isMale和isFemale兩個接口。”

引號細細玩味:“如果isMale和isFemale均返回false,那麼性別不是保密就是中性了。至於性別用boolean、Boolean、char還是String來實現,用戶是懵然不知的,這樣比直接了當的getSex更隱蔽也更靈活。”

冒號揭開最後的答案:“方法computeAge的問題不在其實現,而在其命名。該名暗示年齡是計算出來的,這暴露了實現方式,應該改為getAge。請注意,信息隱藏中的信息不僅僅是數據結構,還包括實現方式和策略。試想,如果將來把年齡而不是生日作為User的輸入,用年齡倒推生日,getBirthday是不是要換成computeBirthday呢?”

歎號不禁喟曰:“不想如此簡單的get和set竟如此講究!”

“通,則大處圓融合一而小處各具其妙;不通,則大處千變萬化而小處無所分別。”冒號又打起了禅語,“領會OOP的精髓絕非一年半載之功,但若以抽象與封裝為鑰,必可早日開啟通達之門。封裝的故事遠未結束,下節課繼續。布置一下課後作業,請將示例中的User類按剛才的提示進行改進。”

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved