14: 創建窗口與Applet
設計的宗旨是"能輕松完成簡單的任務,有辦法完成復雜的任務"。
本章只介紹Java 2的Swing類庫,並且合理假定Swing是Java GUI類庫的發展方向。
本章的開頭部分會講,用Swing創建applet與創建應用程序有什麼不同,以及怎樣創建一個既能當applet在浏覽器裡運行,又能當普通的應用程序,在命令行下運行程序。
Swing類庫的體系龐大,而本章的目的也只是想讓你從基礎開始理解並且熟悉這些概念。如果你有更高的要求,只要肯花精力研究,Swing大概都能做到。
你越了解Swing,你就越能體會到:
與其它語言或開發環境相比,Swing是一個好得多的編程模型。而JavaBeans (本章的臨近結尾的地方會作介紹)則是專為這個構架服務的類庫。 就整個Java開發環境來講,"GUI builders" (可視化編程環境)只是一個"社交"的層面。當你用圖形工具把組件放到窗體上的時候,實際上是GUI builder在調用JavaBeans和Swing為你編寫代碼。這樣不僅能可以加快GUI的開發速度,而且能讓你做更多實驗。這樣你就可以嘗試更多的方案,得到更好的效果了。 Swing的簡單易用與設計合理,使你即便用GUI builder也能得到可讀性頗佳的代碼。這一點解決了GUI builder的一個老問題,那就是代碼的可讀性。
Swing囊括了所有比較時髦的用戶界面元素:從帶圖像的按鈕到樹型控件和表格控件,應有盡有。考慮到類庫的規模,其復雜性還是比較理想的。如果要做的東西比較簡單,代碼就不會很多,如果項目很復雜,那麼代碼就會相應地變得復雜。也就是說入門很容易,但是如果有必要,它也可以變得很強大。
對Swing的好感,很大程度上源於其"使用的正交性"。也就是說,一旦領會了這個類庫的精神,你就可以把這種概念應用到任何地方。這一點首先就緣於其標准的命名規范。通常情況下,如果想把組件嵌套到其它組件裡,直接插就行了。
為了照顧運行速度,組件都是"輕量級"的。為了能跨平台,Swing是完全用Java寫的。
鍵盤支持是內置的;運行Swing應用的時候可以完全不用鼠標,這一點,不需要額外的編程。加滾動條,很容易,只要把控件直接嵌到JScrollPane裡就行了。加Tooltip也只要一行代碼。
Swing還提供一種更前衛的,被稱為"pluggable look andfeel(可插接式外觀)"的功能,也就是說用戶界面的外觀可以根據操作系統或用戶的習慣動態地改變。你甚至可以自己發明一套外觀(當然是很難的)。
基本的applet
Java能創建applet,也就是一種能在Web浏覽器裡運行的小程序。Applet必須安全,所以它的功能很有限。但是applet是一種很強大的客戶端編程工具,而後者是web開發的一個大課題。
Applet的限制
applet編程的限制是很多的,所以也經常被稱作關在"沙箱"裡。因為時時刻刻都有一個人——也就是Java的運行時安全系統——在監視著你。
不過你也可以走出沙箱,編寫普通的應用程序而不是applet,這樣就可以訪問操作系統的其它功能了。迄今為止,我們寫的都是這種應用程序,只不過它們都是些沒有圖形界面的控制台程序罷了。Swing也可以寫GUI界面的應用程序。
大體上說,要想知道applet能做什麼,最好先去了解一下,為什麼要有applet:用它來擴展浏覽器裡的Web頁面的功能。因為上網的人是不可能真正知道這個頁面是不是來自於一個無惡意的網站的,但是你又必須確保所有運行的代碼都是安全的。所以你會發現,它的最大的限制是:
Applet不能訪問本地磁盤。Java為applet提供了數字簽名。你可以選擇讓有數字簽名(由可信的開發商簽署)的applet訪問你的硬盤,這樣applet的很多限制就被解除了。本章的後面在講Java Web Start的時候,會介紹一個這樣的例子的。Web Start是一種通過Internet將應用程序安全地送到客戶端的方案。 Applet的啟動時間很長,因為每次都得下載所有東西,而且每下載一個類都要向服務器發一個請求。或許浏覽器會作緩存,但這並不是一定的。所以一定要把applet的全部組件打成一個JAR的(Java ARchive)卷宗(除了.class文件之外,還包括圖像,聲音),這樣只要發一個請求就可以下載整個applet了。JAR卷宗裡的東西可以逐項地"數字簽名"。
Applet的優勢
如果你不在意這些限制,applet還是有明顯優勢的,特別是在構建clIEnt/server 或是其他網絡應用的時候:
沒有安裝的問題。Applet是真正平台無關的(包括播放音頻文件) ,所以你用不著去為不同的平台修改程序,用戶也用不著安裝完了之後再作調整。實際上每次載入有applet的Web頁面時,安裝就自動完成了。因此軟件的更新可以不驚動客戶自動地完成。為傳統的clIEnt/server系統構建和安裝一個新版的軟件,通常都是一場惡夢。 由於Java語言和applet內置了安全機制,因此你不用擔心錯誤代碼會破壞別人的機器。有了這兩個優勢,Java就能在intranet的client/server應用裡大展身手了。所謂intranet的clIEnt/server應用,是指僅存在於公司內部的,或者可以限定和控制用戶環境的(Web浏覽器和插件)特殊場合的clIEnt/server應用。
由於applet是自動集成到HTML裡面的,因此你就有了一種與平台無關的,能支持applet的文檔系統了(譯者注:指Html)。這真是太有趣了,因為我們通常都認為文檔是程序的一部分,而不是相反。
應用框架
類庫通常按功能進行分類。有些類庫是拿來直接用的,比如Java標准類庫裡面的String和ArrayList。有些類庫則是用來創建其它類的。此外還有一種被稱為應用框架(application framework)的類庫。它的目的是,提供一個或一組具備某些基本功能的類,幫助程序員創建應用程序。而這些基本功能,是這類應用程序所必備的。於是你寫應用程序的時候,只要繼承這個類,然後再根據需要,覆寫幾個你感興趣的方法,定制一下它的行為就可以了。應用框架的默認控制機制會在適當的時機,調用那些你寫的方法。應用框架是一種"將會變和不會變的東西分開來"的絕好的例子。它的設計思想是,通過覆寫方法把程序的個性化部分留在本地。[76]
Applet是用應用框架創建的。你只要繼承JApplet類,再覆寫幾個方法就可以了。下面幾個方法可以控制Web頁面上的applet的創建和執行:
方法
操作
init( )
applet初始化的時候會自動調用,其任務包括裝載組件的布局。必須覆寫。
start( )
在Web浏覽器上顯示applet的時候調用。顯示完畢之後,applet才開始正常工作,(特別是那些用stop( )關閉的applet)。(此外,應用框架)調用完init( )之後也會調用這個方法。
stop( )
讓applet從Web浏覽器上消失的時候調用,這樣它就能關閉一些很耗資源的操作了。此外(應用框架調用)destroy( )之前也會先調用這個方法。
destroy( )
當(浏覽器)不再需要這個applet了,要把它從頁面裡卸載下來的時候,就會調用這個方法以釋放資源了。
注意applet不需要main()。它已經包括在應用框架裡了;你只要把啟動代碼放到init( )裡面就行了。
JLabel的構造函數需要一個String作參數。
init( )方法負責將組件add( )到表單上。或許你會覺得,應該能直接調用它自己(JApplet)的add( )方法。實際上AWT就是這麼做的。Swing要求你將所有的組件都加到表單的"內容面板(content pane)"上,所以add( )的時候,必須先調用getContentPane( )。
在Web浏覽器裡運行applet
要想運行程序,先得把applet放到Web頁面裡,然後用一個能運行Java的Web浏覽器打開頁面。你得用一種特殊的標記把applet放到Web頁面裡,然後讓它來告訴頁面該怎樣裝載並運行這個applet。
這個步驟曾經非常簡單。那時Java自己就很簡單,所有人都用同一個Java,浏覽器裡的Java也都一樣。所以你只要稍微改一下Html就行了,就像這樣:
但是隨後而來的浏覽器和語言大戰使我們(不僅是程序員,還包括最終用戶)都成了輸家。不久,Sun發現再也不能指望靠浏覽器來支持風味醇正的Java了,唯一的解決方案是利用浏覽器的擴展機制來提供"插件(add-on)"。通過這個辦法(要想禁掉Java,除非把所有第三方的插件全都禁掉,但是為了獲取競爭優勢,沒有一個浏覽器的提供商會這麼做的),Sun確保了Java不會被敵對的浏覽器提供商給禁掉。
對於Internet Explorer,這種擴展機制就是ActiveX的控件,而Netscape的則是plugin。你做頁面時必須為這兩個浏覽器各寫一套標記,不過JDK自帶了一個能自動生成標記的HTMLconverter工具。下面就是用HTMLconverter處理過的Applet1的Html頁面:
classid = "clsid:CAFEEFAC-0014-0001-0000-ABCDEFFEDCBA"
codebase = "http://Java.sun.com/products/plugin/autodl/jinstall-1_4_1-Windows-i586.cab#Version=1,4,1,0"
WIDTH = 100HEIGHT = 50 >
type = "application/x-Java-applet;jpi-version=1.4.1"
CODE =Applet1
WIDTH =100
HEIGHT =50
scriptable = false
pluginspage = "http://Java.sun.com/products/plugin/index.Html#download">
code的值是applet所處的.class文件的名字,width和height則表示applet的初始尺寸(和前面一樣,以象素為單位)。此外applet標記裡面還可以放一些東西:到哪裡去找.class文件(codebase),怎樣對齊(align),applet相互通訊的時候要用的標識符(name),以及提供給applet的參數。參數的格式是這樣的:
你可以根據需要,加任意多個參數。
AppletvIEwer的用法
Sun的JDK包含一個名為AppletvIEwer的工具,它可以根據
然後用HtmlConverter過一遍,它會自動幫你生成正確的applet標記。
現在當用戶下載applet時,浏覽器就會提醒他們現在正在裝載的是一個帶簽名的applet,並且問他是不是信任這個簽發者。正如我們前面所講的,測試用的證書並不具備很高的可信度,因此它會給一個警告。如果客戶信任了,applet就能訪問整個客戶系統了,於是它就和普通的程序沒什麼兩樣了。
JNLP和JavaWeb Start
雖然經過簽名的applet功能強大,甚至能在有效地取代應用程序,但它還是得在Web浏覽器上運行。這不僅使客戶端增加了額外的運行浏覽器的開銷,而且常常使用戶界面變得非常的單調和混亂。浏覽器有它自己的菜單和工具條,而他們正好壓在applet的上面。
Java的網絡啟動協議(Java Network LaunchProtocol簡稱JNLP)能在不犧牲applet優點的前提下解決這個問題。你可以在客戶端上下載並安裝單獨的JNLP應用程序。它可以用命令行,桌面圖標,或隨JNLP一同分發的應用程序管理器啟動。程序甚至可以從最初下載的那個網站上啟動。
JNLP程序運行的時候會動態地從Internet上下載資源,並且自動檢查其版本(如果用戶連著Internet的話)。也就是說它同時具備applet和application的優點。
和applet一樣,客戶機在對待JNLP應用程序的時候也必須注意安全問題。JNLP應用程序是一種易於下載的,基於Web的應用程序,因此有可能會被惡意利用。有鑒於此,JNLP應用程序應該和applet一樣被放在沙箱裡。同applet一樣,它可以用帶簽名的JAR文件部署,這時用戶可以選擇是不是信任簽發者。和applet的不同之處在於,即便沒有簽名,它仍然可以通過JNLP API去訪問客戶系統的某些資源(這就需要用戶在程序運行時認可這些請求了)。
JNLP是一個協議而非產品,因而得先把它實現了才能用。Java Web Start有稱JAWS就是Sun提供的,能免費下載的,JNLP的官方樣板實現。你只要下載安裝就行了,如果要做開發,不要忘了把JAR文件放到classpath裡面。要想在網站上部署JNLP應用程序,只要確保服務器能認得application/x-Java-jnlp-file的MIME類型就行了。如果是用最新版的Tomcat服務器(http://jakarta.apache.org/tomcat),那它應該已經幫你配置好了。否則就去查查服務器的用戶手冊。
創建JNLP應用程序並不難。先創建一個標准的應用程序,然後用JAR打包,最後再准備一個啟動文件就行了。啟動文件是一個很簡單的XML文件,它負責向客戶端傳遞下載和安裝應用程序的信息。如果你決定用不帶簽名的JAR文件來部署軟件,那還得用JNLP API來訪問客戶端系統上的資源。
注意,FileOpenService和FileCloseService是Javax.jnlp裡的類,要使用這兩個服務,不但要用ServiceManager.lookup()提出請求,而且要用這個方法所返回的對象來訪問客戶端資源。如果你不想受JNLP束縛,要直接使用這些類的話,那就必須使用簽名的JAR文件。
這個啟動文件的後綴名必須是.jnlp,此外它還必須和JAR文件呆在一個目錄裡。
這是一個根節點為
jnlp元素的spec屬性告訴客戶端系統,這個應用程序需要哪個版本的JNLP。codebase屬性告訴客戶端到哪個目錄去找啟動文件和資源。通常它應該是一個指向Web服務器的HTTP URL,但這裡為了測試需要,我們把它指到本機的目錄了。href屬性表示文件的名字。
information標記裡有多個提供與程序相關的信息的子元素。它們是供Java WebStart的管理控制台或其它類似程序使用的。這些程序會把JNLP應用安裝到客戶端上,讓後讓用戶通過命令行,快捷方式或者其它什麼方法啟動。
resource標記的功能Html文件裡的applet標記相似。j2se子元素指明程序運行所需的J2SE的版本,jar子元素告訴客戶端class文件被打在哪個JAR文件裡。此外jar元素還有一個download屬性,其值可以是"eager"或"lazy",它的作用是告訴JNLP是不是應該下載完這個jar再開始運行程序。
application-desc屬性告訴客戶端系統,可執行的class,也就是JAR文件的入口是哪個類。
jnlp標記還有一個很有用的子元素,那就是這裡沒用到的security標記。下面我們來看看security標記長什麼樣子:
只有在部署帶簽名的JAR文件時才能使用security標記。上面那段程序不需要這個標記,因為所有的本地資源都是通過JNLP服務來訪問的。
此外還有一些其它標記,具體細節可以參考http://java.sun.com/products/Javawebstart/download-spec.htm
現在.jnlp文件也寫好了,接下來就是在網頁裡加超鏈接了。這個頁面應該是個下載頁面。頁面上除了有復雜的格式和詳細介之外,千萬別忘了把這條加上:
clickhere
這樣你就可以點擊鏈接啟動JNLP應用程序的安裝進程了。你只要下載一次,以後就可以通過管理控制台來進行配置了。如果你用的是Windows的Java Web Start的話,那麼第二次啟動程序的時候,它會提示你,是不是創建一個快捷方式。這種東西是可以配置的。
我們這裡只介紹了兩個JNLP服務,而當前版本裡有七種。它們都是為特定的任務所設計的,比如像打印,剪貼板操作等。
編程技巧
由於Java的GUI編程是一個還在不斷改進的技術,Java 1.0/1.1與Java 2的Swing類庫之間就有著非常重大的區別,與舊模式相比,Swing能讓你用一種更好的方式編程。這裡,我們會就其中一些問題做個介紹,同時檢驗一下這些編程技巧。
動態綁定事件
Swing的事件模型的優點就在於它的靈活性。你可以調用方法給組件添加或刪除事件。
Button可以連不止一個listener。通常組件是以多播(multicast)方式處理事件的,也就是說你可以為一個事件注冊多個listener。但是對於一些特殊的,以單播(unicast)方式處理事件的組件,這麼做就會引發TooManyListenersException了。 程序運行的時候能動態地往Button b2上面添加或刪除listener。你應該已經知道加listener的方法了,此外每個組件還有一個能用來刪listener的removeXXXListener( )方法。
這種靈活性為你帶來更大的便利
值得注意的是,listener的添加順序並不一定就是它們的調用順序(雖然絕大多數JVM確實是這麼實現的)。
將業務邏輯(business logic)與用戶界面分離開來
一般情況下,設計類的時候總是強調一個類"只作一件事情"。涉及用戶界面的時候更是如此,因為你很可能會把"要作什麼"同"要怎樣顯示"給混在一起了。這種耦合嚴重妨礙了代碼的復用。比較好的做法是將"業務邏輯(business login)"同GUI分離開來。這樣不僅方便了業務邏輯代碼的復用,也簡化了GUI的復用。
還有一種情況,就是多層系統(multitIEred systems),也就是說”業務對象(business object)"完全貯存在另一台機器上。業務規則的集中管理能使規則的更新立即對新交易生效,因此這是這類系統所追求的目標。但是很多應用程序都會用到這些業務對象,所以它們絕不能同特定的顯示模式連在一起。它們應該只做業務處理,別的什麼都不管。
樹立了將UI同業務邏輯相分離的觀點之後,當你再碰到用Java去維護遺留下來的老代碼時,也能稍微輕松一點。
范式
內部類,Swing事件模型,還能繼續用下去的AWT事件模型,以及那些要我們用老辦法用的新類庫的功能,所有這些都使程序設計變得更混亂了。現在就連大家寫亂七八糟的代碼的方式也變得五花八門了。
這些情況都是事實,但是你應該總是使用最簡單也最有條理的解決方案:用Listener(通常要寫成內部類)來處理事件。
用了這個模型,你可以少寫很多"讓我想想這個事件是誰發出的"這種代碼。所有代碼都在解決問題,而不是在做類型檢查。這是最佳的編程風格,寫出來的代碼不僅便於總結,可讀性和可維護性也高。
並發與Swing
寫Swing程序的時候,你很可能會忘了它還正用著線程。雖然你並沒有明確地創建過Thread對象,但它所引發的問題卻會乘你不備嚇你一跳。絕大多數情況下,你寫的Swing或其他帶窗口顯示的GUI程序都是事件驅動的,而且除非用戶用鼠標或鍵盤點擊GUI組件,否則什麼事都不會發生。
只要記住Swing有一個事件分派線程就行了,它會一直運行下去,並且按順序處理Swing的事件。如果你想確保程序不會發生死鎖或者競爭的情形,那麼倒是要考慮一下這個問題。
重訪Runnable
在第13章,我曾建議大家在實現Runnable接口時一定要慎重。 當然如果你設計的類必須繼承另一個類而這個類又得有線程的行為,那麼選擇Runnable還是對的。
不同的JVM,在如何實現線程方面,存在著巨大的性能和行為差異。
管理並發
當你用main方法或另一個線程修改Swing組件的屬性時,一定要記住,有可能事件分派線程正在和你競爭同一個資源。
看來線程遇到Swing的時候,麻煩也跟著來了。要解決這個問題,你必須確保Swing組件的屬性只能由事件分派線程來修改。
這要比聽上去的容易一些。Swing提供了兩個方法,SwingUtilities.invokeLater( )和SwingUtilitIEs.invokeandWait( ),你可以從中選一個。它們負責絕大多數的工作,也就是說你不用去操心那些很復雜的線程同步的事了。
這兩個方法都需要runnable對象作參數。當Swing的事件處理線程處理完隊列裡的所有待處理事件之後,就會啟動它的run( )方法了。
能用這兩個方法來設置Swing組件的屬性。
可視化編程與JavaBeans
看到現在你已經知道Java在代碼復用方面的價值了。復用程度最高的代碼是類,因為它是由一組緊密相關的特征(字段fIEld)和行為(方法)組成的,它既能以合成(composition),也能以繼承的方式被復用。
繼承和多態是面向對象編程的基礎,但是在構建應用程序的時候,絕大多數情況下,你真正需要的是能幫你完成特定任務的組件。你希望能把這些組件用到設計裡面,就像電子工程師把芯片插到電路板上一樣。同樣,也應該有一些能加速這種"模塊化安裝"的編程方法。
Microsoft的Visual Basic為"可視化編程(Visual programming)"贏得了初次成功——非常巨大的成功,緊接著是第二代的Borland Delphi(直接啟發了JavaBean的設計) 。有了這些工具,組件就變得看得見摸的著了,而組件通常都表示像按鈕,文本框之類的可視組件,因此這樣一來組件編程也變得有意義了。實際上組件的外觀,通常是設計時是什麼樣子運行時也就這樣,所以從控件框(palette)裡往表單上拖放組件也就成了可視化編程的步驟了。而當你在這麼做的時候,應用程序構造工具在幫你寫代碼,所以當程序運行時,它就會創建那些組件了。
通常簡單地把組件拖到表單上還不足以創建程序。你還得修改一些特征,比如它的顏色,上面的文字,所連接的數據庫等等。這些在設計時可以修改的特征被稱為屬性(propertIEs)。你可以在應用程序的構建工具裡控制組件的屬性。當程序創建完畢,這些配置信息也被保存下來,這樣程序運行時就能激活這些配置信息了。
看到現在你或許已經習慣這樣來理解對象了,也就是對象不僅是一組特征,還是一組行為。設計的時候,可視組件的行為部分的表現為事件,也就是說"是些能發生在這個組件上的事情"。一般來說你會用把代碼連到事件的方法來決定事件發生時該做些什麼。
下面就是關鍵部分了:應用程序的構建工具用reflection動態地查詢組件,找出這個組件支持哪些屬性和事件。一旦知道它是誰,構建工具就能把這些屬性顯示出來,然後讓你作修改了(創建程序的時候會把這些狀態保存下來),當然還有事件。總之,只要你在事件上雙擊鼠標或者其他什麼操作,編程工具就會幫你准備好代碼的框架,然後連上事件。現在,你只要編寫事件發生時該執行的代碼就可以了。
編程工具幫你做了這麼多事,這樣你就能集中精力去解決程序的外觀和功能問題了,至於把各部分銜接起來的細節問題,就交給構建工具吧。可視化編程工具之所以能獲得如此巨大的成功,是因為它能極大的提高編程的效率,當然這一點首先體現在用戶界面,但是其它方面往往也受益頗豐。
JavaBean是干什麼用的?
言歸正傳,組件實際上是一段封裝成類的代碼。關鍵在於,它能讓應用程序的構建工具把自己的屬性和事件提取出來。創建VB組件的時候,程序員必須按照特定的約定,用相當復雜的代碼把屬性和事件發掘出來。Delphi是第二代的可視化編程工具,而且整個語言是圍繞著可視化編程設計的,所以用它創建可視化組件要簡單得多。但是Java憑借其JavaBean在可視化組件的創建技術領域領先群雄。Bean只是一個類,所以你不用為創建一個Bean而去編寫任何額外的代碼,也不用去使用特殊的語言擴展。事實上你所要做的只是稍稍改變一下方法命名的習慣。是方法的名字告訴應用程序構建工具,這是一個屬性,事件還是一個普通的方法。
JDK文檔把命名規范(naming convention)錯誤地表述成"設計模式(design pattern)”。這真是不幸,設計模式(請參閱www.BruceEckel.com上的Thinking in Patterns (with Java))本身已經夠讓人傷腦筋的了,居然還有人來混淆視聽。重申一遍,這算不上是什麼設計模式,只是命名規范而已,而且還相當簡單。
對於名為xxx的屬性,你通常都得創建兩個方法:getXxx( )和setXxx( )。注意構建工具會自動地將"get"和"set"後面的第一個字母轉換成小寫,以獲取屬性的名字。"get"所返回的類型與”set"所使用的參數的類型相同。屬性的名字同"get"和”set"方法返回的類型無關。 對於boolean型的屬性,你既可以使用上述的"get"和"set"方法,也可以用"is"來代替"get"。 Bean的常規方法無需遵循上述命名規范,但它們都必須是public的。 用Swing的listener來處理事件。就是我們講到現在一直在用的這個方案:用addBounceListener(BounceListener)和removeBounceListener(BounceListener)來處理BounceListener。絕大多數情況下,內置的事件和監聽器已經可以滿足你的需要了,但是你也可以創建你自己的事件和監聽器接口。
第一點回答了你在比較新舊代碼時或許會注意的一個問題:很多方法的名字都有了一些很小的,但明顯沒什麼意義的變化。現在你應該知道了,為了把組件做成JavaBean,絕大多數修改是在同"get"和"set"的命名規范接軌。
用Introspector提取BeanInfo
當你把Bean從控件框(palette)裡拖到表單上的時候,JavaBean架構中最重要的一環就開始工作了。應用程序構建工具必須能創建這個Bean(有默認構造函數的話就可以了),然後在不看Bean源代碼的情況下提取所有必須的信息,然後創建屬性表和事件句柄。
從第十章看,我們已經能部分地解決這個問題了:Java的reflection機制可以幫我們找出類的所有方法。我們不希望像別的可視化編程語言那樣用特殊的關鍵字來解決JavaBean的問題,因此這是個完美的解決方案。實際上給Java加上reflection的主要原因,就是為了支持JavaBean(雖然也是為了支持"對象的序列化(object serializaiton)"和"遠程方法調用(remote method invocation)"。所以也許你會想設計應用程序構建工具的人會逐個地reflect Bean,找出所有的方法,再在裡面挑出Bean的屬性和事件。
這麼做當然也可以,但是Java為我們提供了一個標准的工具。這不僅使Bean的使用變得更方便了,而且也為我們創建更復雜的Bean指出了一條標准通道。這個工具就是Introspector,其中最重要的方法就是static getBeanInfo( )。當你給這個方法傳一個Class對象時,它會徹底盤查這個類,然後返回一個BeanInfo對象,這樣你就可以通過這個對象找出Bean的屬性,方法和事件了。
通常你根本不用為此操心;絕大多數Bean都是從供應商那裡直接買過來的,更何況你也不必知道它在底層都玩了什麼花樣。你只要直接把Bean放到表單上,然後配置一下屬性,再寫個程序處理一下事件就可以了。
一個更復雜的Bean
所有的字段都是private的這是Bean的通常做法——也就是說做成"屬性"之後,通常只能用方法來訪問了。
JavaBeans和同步
只要你創建了Bean,你就得保證它能在多線程環境下正常工作,這就是說:
只要允許,所有Bean的public方法都必須是synchronized。當然這會影響性能(不過在最新版本的JDK裡,這種影響已經明顯下降了)。如果性能下降確實是個問題,那麼你可以把那些不致於引起問題的方法的synchronized給去掉,但是要記住,會不會引發問題不是一眼就能看出來的。這種方法首先是要小(就像上面那段程序裡的getCircleSize( )),而且/或是"原子操作",就是說這個方法所調用的代碼如此之少,以至於執行期間對象不會被修改了。所以把這種方法做成非synchronized的,也不會對性能產生什麼重大影響。所以你應該把Bean的所有public方法都做成synchronized,只有在有絕對必要,而且確實對性能提高有效的時候,才能把synchronized移掉。 當你將多播事件發送給一隊對此感興趣的listener時,必須做好准備,listener會隨時加入或從隊列中刪除。
第一個問題很好解決,但第二個問題就要好好想想了。
paintComponent( )也沒有synchronized。決定覆寫方法的時候是不是該加synchronized不像決定自己寫的方法那樣清楚。。這裡,好像paintComponent()加不加synchronized一樣都能工作。但必須考慮的問題有:
這個方法是否會修改對象的"關鍵"變量?變量是否”關鍵"的判斷標准是,它們是否會被其它線程所讀寫。(這裡,讀寫實際上都是由synchronized方法來完成的,所以你只要看這一點就可以了)在這段程序裡,paintComponent( )沒有修改任何東西。 這個方法是否與這種"關鍵"變量的狀態有關?如果有一個synchronized方法修改了這個方法要用到的變量,那麼最好是把這個方法也作成synchronized的。基於這點,你或許會發現cSize是由synchronized方法修改的,因此paintComponent( )也應該是synchronized。但是這裡你應該問問"如果在執行paintComponent( )的時候,cSize被修改了,最糟糕的情況是什麼呢?"如果問題並不嚴重,而且轉瞬即逝的,那麼為了防止synchronized所造成的性能下降,你完全可以把paintComponent( )做成非synchronized的。 第三個思路是看基類的paintComponent( )是不是synchronized,答案是"否"。這不是一個萬無一失的判斷標准,只是一個思路。就拿上面那段程序說吧,paintComponent( )裡面混了一個通過synchronized方法修改的cSize字段,所以情況也改變了。但是請注意,synchronized不會繼承;也就是說派生類覆寫的基類synchronized方法不會自動成為synchronized方法。 paint( )和paintComponent( )是那種執行得越快越好的方法。任何能夠提升性能的做法都是值得大力推薦的,所以如果你發覺不得不對這些方法用synchronized,那麼很有可能是一個設計失敗的信號。
main( )的測試代碼是根據BangBeanTest修改而得的。為了演示BangBean2的多播功能,它多加了幾個監聽器。
封裝Bean
要想在可視化編程工具裡面用JavaBean,必須先把它放入標准的Bean容器裡。也就是把所有Bean的class文件以及一份申明"這是一個Bean"的"manifest"文件打成一個JAR的包。manifest文件是一種有一定格式要求的文本文件。對於BangBean,它的manifest文件是這樣的:
Manifest-Version: 1.0
Name: bangbean/BangBean.class
Java-Bean: True
第一行表明manifest的版本,除非Sun今後發通知,否則就是1.0。第二行(空行忽略不計)特別提到了BangBean.class文件,而第三行的意思是"這是y一個Bean"。沒有第三行,應用程序構建工具不會把它看成Bean。
唯一能玩點花樣的地方是,你必須在"Name:"裡指明正確的路徑。如果你翻到前面去看BangBean.Java,就會發覺它屬於bangbean package(因此必須放到classpath的某個目錄的"bangbean"的子目錄裡),而manifest的name也必須包含這個package的信息。此外還必須將manifest文件放到package路徑的根目錄的上一層目錄裡,這裡就是將manifest放到"bangbean"子目錄的上一層目錄裡。然後在存放manifest文件的目錄裡打入下面這條jar命令:
jar cfm BangBean.jar BangBean.mf bangbean
這裡假定JAR文件的名字是BangBean.jar,而manifest文件的名字是BangBean.mf。
或許你會覺得有些奇怪,"我編譯BangBean.java的時候還生成了一些別的class文件,它們都放到哪裡去了?"是的,它們最後都放在bangbean子目錄裡,而上面那條jar命令的最後一個參數就是bangbean。當你給jar一個子目錄做參數時,它會將整個子目錄都打進JAR文件裡(這裡還包括BangBean.Java的源代碼——你自己寫Bean的時候大概不會想把源代碼打進包吧)。此外如果你把剛做好的JAR文件解開,就會發現你剛寫的那個manifest已經不在裡面了,取而代之的是jar自己生成的(大致根據你寫的),名為MANIFEST.MF的manifest文件,而且它把它放在META-INF子目錄裡 (意思是“meta-information”)。如果你打開這個manifest文件,就會發現jar給每個文件加了條簽名的信息,就像這樣:
Digest-Algorithms: SHA MD5
SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0=
MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==
總之,你不必為這些事擔心。你作修改的時候可以只改你自己寫的manifest文件,然後重新運行一遍jar,讓它來創建新的JAR文件。你也可以往JAR文件裡加新的Bean,只是要把它們的信息加到manifest裡面就行了。
值得注意的是,你應該為每個Bean創建一個子目錄。這是因為當你創建JAR文件的時候,你會把子目錄的名字交給jar,而jar又會把子目錄裡的所有東西都放進JAR。所以Frog和BangBean都有它們自己的子目錄。
等你把Bean封裝成JAR文件之後,你就能把它們用到支持Bean的IDE裡了。這個步驟會隨開發工具的不同有一些差別,不過Sun在他們的"Bean Builder"裡提供了一個免費的JavaBean的測試床(可以到Java.sun.com/beans去下載)。要把Bean加入BeanBuiler,只要把JAR文件拷貝到正確的目錄裡就行了。
Bean的高級功能
你已經知道做一個Bean有多簡單了,但是它的功能並不僅限於此。JavaBean的架構能讓你很快上手,但是經過擴展,它也可以適應更復雜的情況。這些用途已經超出了本書的范圍,但是我會做一個簡單的介紹。你可以在Java.sun.com/beans上找到更多的細節。
屬性是一個能加強的地方。在我們舉的例子裡,屬性都是單個的,但是你也可以用一個數組來表示多個屬性。這被稱為索引化的屬性(indexed property)。你只要給出正確的方法(還是要遵循方法的命名規范),Introspector就能找到索引化的屬性,這樣應用程序構建工具就能作出正確的反映了。
屬性可以被綁定,也就是說它們能通過PropertyChangeEvent通知其它對象。而其它對象能根據Bean的變化,修改自己的狀態。
屬性是可以被限制的,也就是說如果其他對象認為屬性的這個變化是不可接受的,那麼它們可以否決這個變化。Bean用PropertyChangeEvent通知其他對象,而其他對象則用PropertyVetoException來表示反對,並且命令它將屬性的值恢復到原來的狀態。
你也可以修改Bean在設計時的表示方式:
你可以為Bean提供自定義的屬性清單。當用戶選擇其它Bean的時候,構建工具會提供普通屬性清單,但是當他們選用你的Bean時,它會提供你定義的清單。 你可以為屬性創建一個自定義的編輯器,這樣雖然構建工具用的是普通的屬性清單,但當用戶要編輯這個特殊屬性時,編輯器就會自動啟動了。 你可以為Bean提供一個自定義的BeanInfo類,它返回的信息,可以同Introspector默認提供的BeanInfo不同。 還可以把所有FeatureDescriptor的"專家(expert)"模式打開,看看基本功能和高級功能有什麼區別。
總結
這一章只是想跟你介紹一下Swing的強大功能然後領你入門,這樣當你知道相對而言Swing有多簡單之後,你就能自己去探路了。你看到的這些已經能大致滿足UI設計之需了。但是Swing不止於此;它的目標是要成為一種功能齊全的UI設計工具。只要你能想到,它都有辦法能作到。
如果你在這裡找不到你想要的,那麼研究一下Sun的JDK文檔吧,或者去搜Web,如果還不行,就去找一本Swing的專著。
PS:TIJ的學習暫時就告與段落了,因為3th目前網上翻譯只到第14章,第三次看這本書,算是比較明白了。但是從第8章開始,很多地方還是有一些不太懂的地方。特別是I/O部分,需要稍後再盡快加強一下。總之,再第三次看了才發現這本書的好,真的實在太經典了,我覺得自己最少應該再看個2~3遍才差不多。
最後,感謝本書的作者Bruce Eckel的無私奉獻,寫了這麼好的一本書而且還免費放到網上。當然還要感謝shhgs,這位熱心的網友的翻譯,對於我這種英盲來說,這種幫助實在是太大了。作者和譯者的無私奉獻的精神值得我們大家學習,和那些惟利是圖,見錢眼開的人來說。這兩位的風格,人品何止高出一兩倍,實在是小輩的榜樣和偶像,真的是萬分的敬仰之情難於言表。