




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)
文檔簡介
1、第 12 章 傳遞和返回對象到目前為止,讀者應(yīng)對對象的“傳遞”有了一個較為深刻的認(rèn)識,記住實際 傳遞的只是一個句柄。在許多程序設(shè)計語言中,我們可用語言的“普通”方式到處傳遞對象,而且 大多數(shù)時候都不會遇到問題。 但有些時候卻不得不采取一些非常做法, 使得情況 突然變得稍微復(fù)雜起來(在C+中則是變得非常復(fù)雜)。Java亦不例外,我們十 分有必要準(zhǔn)確認(rèn)識在對象傳遞和賦值時所發(fā)生的一切。這正是本章的宗旨。若讀者是從某些特殊的程序設(shè)計環(huán)境中轉(zhuǎn)移過來的,那么一般都會問到:“Java有指針嗎?”有些人認(rèn)為指針的操作很困難,而且十分危險,所以一廂情 愿地認(rèn)為它沒有好處。同時由于 Java 有如此好的口碑,所
2、以應(yīng)該很輕易地免除 自己以前編程中的麻煩,其中不可能夾帶有指針這樣的“危險品” 。然而準(zhǔn)確地 說,Java是有指針的!事實上,Java中每個對象(除基本數(shù)據(jù)類型以外)的標(biāo)識 符都屬于指針的一種。 但它們的使用受到了嚴(yán)格的限制和防范, 不僅編譯器對它 們有“戒心”,運(yùn)行期系統(tǒng)也不例外?;蛘邠Q從另一個角度說,Java有指針,但沒有傳統(tǒng)指針的麻煩。我曾一度將這種指針叫做“句柄” ,但你可以把它想像成“安全指針”。和預(yù)備學(xué)校為學(xué)生提供的安全剪刀類似除非特別有意,否則 不會傷著自己,只不過有時要慢慢來,要習(xí)慣一些沉悶的工作。12.1 傳遞句柄 將句柄傳遞進(jìn)入一個方法時,指向的仍然是相同的對象。一個簡單的
3、實驗可 以證明這一點(若執(zhí)行這個程序時有麻煩,請參考第 3章 3.1.2小節(jié)“賦值”):542 頁程序toString方法會在打印語句里自動調(diào)用,而 PassHandles直接從Object繼承, 沒有toString的重新定義。因此,這里會采用toString的Object版本,打印出對 象的類,接著是那個對象所在的位置(不是句柄,而是對象的實際存儲位置) 。 輸出結(jié)果如下:p inside main(): PassHandles1653748h inside f() : PassHandles1653748可以看到,無論 p 還是 h 引用的都是同一個對象。這比復(fù)制一個新的 PassHa
4、ndles對象有效多了,使我們能將一個參數(shù)發(fā)給一個方法。但這樣做也帶 來了另一個重要的問題。12.1.1 別名問題 “別名”意味著多個句柄都試圖指向同一個對象,就象前面的例子展示的那 樣。若有人向那個對象里寫入一點什么東西, 就會產(chǎn)生別名問題。 若其他句柄的 所有者不希望那個對象改變, 恐怕就要失望了。 這可用下面這個簡單的例子說明:543 頁程序?qū)ο旅孢@行:Alias1 y = x; / Assign the handle它會新建一個 Alias1 句柄,但不是把它分配給由 new 創(chuàng)建的一個新鮮對象, 而是分配給一個現(xiàn)有的句柄。 所以句柄 x 的內(nèi)容即對象 x 指向的地址被 分配給y,所以
5、無論x還是y都與相同的對象連接起來。這樣一來,一旦x的i在下述語句中增值:x.i+;y的i值也必然受到影響。從最終的輸出就可以看出:544 頁上程序此時最直接的一個解決辦法就是干脆不這樣做: 不要有意將多個句柄指向同 一個作用域內(nèi)的同一個對象。 這樣做可使代碼更易理解和調(diào)試。 然而,一旦準(zhǔn)備 將句柄作為一個自變量或參數(shù)傳遞這是 Java 設(shè)想的正常方法別名問題 就會自動出現(xiàn),因為創(chuàng)建的本地句柄可能修改“外部對象” (在方法作用域之外 創(chuàng)建的對象)。下面是一個例子:544 頁程序輸出如下:x: 7Calling f(x)x: 8方法改變了自己的參數(shù)外部對象。一旦遇到這種情況,必須判斷它是否 合理
6、,用戶是否愿意這樣,以及是不是會造成問題。通常,我們調(diào)用一個方法是為了產(chǎn)生返回值,或者用它改變?yōu)槠湔{(diào)用方法的 那個對象的狀態(tài)(方法其實就是我們向那個對象“發(fā)一條消息”的方式) 。很少 需要調(diào)用一個方法來處理它的參數(shù); 這叫作利用方法的“副作用”(Side Effect)。 所以倘若創(chuàng)建一個會修改自己參數(shù)的方法, 必須向用戶明確地指出這一情況, 并 警告使用那個方法可能會有的后果以及它的潛在威脅。由于存在這些混淆和缺 陷,所以應(yīng)該盡量避免改變參數(shù)。若需在一個方法調(diào)用期間修改一個參數(shù),且不打算修改外部參數(shù),就應(yīng)在自 己的方法內(nèi)部制作一個副本, 從而保護(hù)那個參數(shù)。 本章的大多數(shù)內(nèi)容都是圍繞這 個問題
7、展開的。12.2 制作本地副本稍微總結(jié)一下:Java中的所有自變量或參數(shù)傳遞都是通過傳遞句柄進(jìn)行的。 也就是說,當(dāng)我們傳遞“一個對象”時,實際傳遞的只是指向位于方法外部的那 個對象的“一個句柄” 。所以一旦要對那個句柄進(jìn)行任何修改,便相當(dāng)于修改外 部對象。此外:參數(shù)傳遞過程中會自動產(chǎn)生別名問題不存在本地對象,只有本地句柄句柄有自己的作用域,而對象沒有對象的“存在時間”在Java里不是個冋題沒有語言上的支持(如常量)可防止對象被修改(以避免別名的副作用)若只是從對象中讀取信息,而不修改它,傳遞句柄便是自變量傳遞中最有效 的一種形式。這種做非常恰當(dāng);默認(rèn)的方法一般也是最有效的方法。然而,有時 仍需
8、將對象當(dāng)作“本地的”對待,使我們作出的改變只影響一個本地副本,不會 對外面的對象造成影響。 許多程序設(shè)計語言都支持在方法內(nèi)自動生成外部對象的 一個本地副本(注釋)。盡管Java不具備這種能力,但允許我們達(dá)到同樣的效 果。:在C語言中,通??刂频氖巧倭繑?shù)據(jù)位,默認(rèn)操作是按值傳遞。 C+也 必須遵照這一形式,但按值傳遞對象并非肯定是一種有效的方式。 此外,在C+ 中用于支持按值傳遞的代碼也較難編寫,是件讓人頭痛的事情。12.2.1 按值傳遞首先要解決術(shù)語的問題,最適合“按值傳遞”的看起來是自變量。 “按值傳 遞”以及它的含義取決于如何理解程序的運(yùn)行方式。 最常見的意思是獲得要傳遞 的任何東西的一個
9、本地副本,但這里真正的問題是如何看待自己準(zhǔn)備傳遞的東 西。對于“按值傳遞”的含義,目前存在兩種存在明顯區(qū)別的見解:(1) Java按值傳遞任何東西。若將基本數(shù)據(jù)類型傳遞進(jìn)入一個方法,會明確 得到基本數(shù)據(jù)類型的一個副本。 但若將一個句柄傳遞進(jìn)入方法, 得到的是句柄的 副本。所以人們認(rèn)為“一切”都按值傳遞。當(dāng)然,這種說法也有一個前提:句柄 肯定也會被傳遞。但 Java 的設(shè)計方案似乎有些超前,允許我們忽略(大多數(shù)時 候)自己處理的是一個句柄。也就是說,它允許我們將句柄假想成“對象” ,因 為在發(fā)出方法調(diào)用時,系統(tǒng)會自動照管兩者間的差異。(2) Java主要按值傳遞(無自變量),但對象卻是按引用傳遞
10、的。得到這個結(jié) 論的前提是句柄只是對象的一個“別名” ,所以不考慮傳遞句柄的問題,而是直 接指出“我準(zhǔn)備傳遞對象” 。由于將其傳遞進(jìn)入一個方法時沒有獲得對象的一個 本地副本,所以對象顯然不是按值傳遞的。Sun公司似乎在某種程度上支持這一 見解,因為它“保留但未實現(xiàn)”的關(guān)鍵字之一便是 byvalue (按值)。但沒人知道 那個關(guān)鍵字什么時候可以發(fā)揮作用。盡管存在兩種不同的見解,但其間的分歧歸根到底是由于對“句柄”的不同 解釋造成的。 我打算在本書剩下的部分里回避這個問題。 大家不久就會知道, 這 個問題爭論下去其實是沒有意義的最重要的是理解一個句柄的傳遞會使調(diào) 用者的對象發(fā)生意外的改變。12.2
11、.2 克隆對象 若需修改一個對象,同時不想改變調(diào)用者的對象,就要制作該對象的一個本 地副本。 這也是本地副本最常見的一種用途。 若決定制作一個本地副本, 只需簡 單地使用clone()方法即可。Clone是“克隆”的意思,即制作完全一模一樣的副 本。這個方法在基礎(chǔ)類 Object中定義成“ protected ”(受保護(hù))模式。但在希望 克隆的任何衍生類中,必須將其覆蓋為“ public ”模式。例如,標(biāo)準(zhǔn)庫類 Vector 覆蓋了 clone(),所以能為Vector調(diào)用clone(),如下所示:547 頁程序clone()方法產(chǎn)生了一個Object,后者必須立即重新造型為正確類型。這個例子
12、指出Vector的clone()方法不能自動嘗試克隆 Vector內(nèi)包含的每個對象由于別名問題,老的 Vector 和克隆的 Vector 都包含了相同的對象。我們通常把這 種情況叫作“簡單復(fù)制”或者“淺層復(fù)制” ,因為它只復(fù)制了一個對象的“表面” 部分。實際對象除包含這個“表面”以外,還包括句柄指向的所有對象,以及那 些對象又指向的其他所有對象,由此類推。這便是“對象網(wǎng)”或“對象關(guān)系網(wǎng)” 的由來。若能復(fù)制下所有這張網(wǎng),便叫作“全面復(fù)制”或者 “深層復(fù)制”。在輸出中可看到淺層復(fù)制的結(jié)果,注意對 v2 采取的行動也會影響到 v:548 頁上程序一般來說,由于不敢保證 Vector里包含的對象是“
13、可以克隆”(注釋)的, 所以最好不要試圖克隆那些對象。:“可以克隆”用英語講是cioneable,請留意Java庫中專門保留了這樣的 一個關(guān)鍵字。12.2.3 使類具有克隆能力盡管克隆方法是在所有類最基本的 Object中定義的,但克隆仍然不會在每個 類里自動進(jìn)行。這似乎有些不可思議, 因為基礎(chǔ)類方法在衍生類里是肯定能用的。 但 Java 確實有點兒反其道而行之;如果想在一個類里使用克隆方法,唯一的辦 法就是專門添加一些代碼,以便保證克隆的正常進(jìn)行。1.使用protected時的技巧為避免我們創(chuàng)建的每個類都默認(rèn)具有克隆能力,clone()方法在基礎(chǔ)類Object 里得到了“保留”(設(shè)為prot
14、ected) 0這樣造成的后果就是:對那些簡單地使用一 下這個類的客戶程序員來說, 他們不會默認(rèn)地?fù)碛羞@個方法; 其次,我們不能利 用指向基礎(chǔ)類的一個句柄來調(diào)用cione()(盡管那樣做在某些情況下特別有用,比如用多形性的方式克隆一系列對象) 。在編譯期的時候,這實際是通知我們對象 不可克隆的一種方式一一而且最奇怪的是,Java庫中的大多數(shù)類都不能克隆。因 此,假如我們執(zhí)行下述代碼:Integer x = new Integer(l);x = x.clone();那么在編譯期, 就有一條討厭的錯誤消息彈出, 告訴我們不可訪問 clone()因為Integer并沒有覆蓋它,而且它對 protec
15、ted版本來說是默認(rèn)的)。但是,假若我們是在一個從 Object 衍生出來的類中(所有類都是從 Object 衍生的),就有權(quán)調(diào)用Object.clone(),因為它是“ protected,”而且我們在一個繼 承器中?;A(chǔ)類clone()提供了一個有用的功能一一它進(jìn)行的是對衍生類對象的真 正“按位”復(fù)制,所以相當(dāng)于標(biāo)準(zhǔn)的克隆行動0然而,我們隨后需要將自己的克 隆操作設(shè)為public,否則無法訪問。總之,克隆時要注意的兩個關(guān)鍵問題是:幾 乎肯定要調(diào)用super.clone(,以及注意將克隆設(shè)為public。有時還想在更深層的衍生類中覆蓋cione(),否則就直接使用我們的clone()(現(xiàn)在已
16、成為 public) ,而那并不一定是我們所希望的 (然而,由于 Object.clone() 已制作了實際對象的一個副本,所以也有可能允許這種情況) 。 protected 的技巧 在這里只能用一次: 首次從一個不具備克隆能力的類繼承, 而且想使一個類變成“能夠克隆”。而在從我們的類繼承的任何場合,clone()方法都是可以使用的, 因為 Java 不可能在衍生之后反而縮小方法的訪問范圍。換言之,一旦對象變得 可以克隆, 從它衍生的任何東西都是能夠克隆的, 除非使用特殊的機(jī)制 (后面討 論)令其“關(guān)閉”克隆能力。2.實現(xiàn) Cloneable 接口為使一個對象的克隆能力功成圓滿,還需要做另一件
17、事情:實現(xiàn) Cloneable 接口。這個接口使人稍覺奇怪,因為它是空的!interface Cloneable 之所以要實現(xiàn)這個空接口, 顯然不是因為我們準(zhǔn)備上溯造型成一個 Cloneable,以及調(diào)用它的某個方法。有些人認(rèn)為在這里使用接口屬于一種“欺騙”行為,因為它使用的特性打的是別的主意,而非原來的意思。 Cloneable interface 的實現(xiàn)扮演了一個標(biāo)記的角色,封裝到類的類型中。兩方面的原因促成了 Clo neable in terface的存在。首先,可能有一個上溯造型 句柄指向一個基礎(chǔ)類型, 而且不知道它是否真的能克隆那個對象。 在這種情況下, 可用instanceof關(guān)
18、鍵字(第11章有介紹)調(diào)查句柄是否確實同一個能克隆的對象 連接:if(myHandle instanceof Cloneable) / .第二個原因 是考慮到我 們可能不愿所有對象 類型都能克隆。 所以 Object.clone()會驗證一個類是否真的是實現(xiàn)了Cloneable接口。若答案是否定的,則“擲”出一個 CloneNotSupportedException違例。所以在一般情況下,我們必 須將“ implement Cloneable 作”為對克隆能力提供支持的一部分。12.2.4 成功的克隆理解了實現(xiàn)clone()方法背后的所有細(xì)節(jié)后,便可創(chuàng)建出能方便復(fù)制的類,以 便提供了一個本地副
19、本:550-551 頁程序不管怎樣,clone()必須能夠訪問,所以必須將其設(shè)為public(公共的)。其次, 作為clone()的初期行動,應(yīng)調(diào)用 clone()的基礎(chǔ)類版本。這里調(diào)用的 clone()是 Object內(nèi)部預(yù)先定義好的。之所以能調(diào)用它,是由于它具有 protected (受到保護(hù) 的)屬性,所以能在衍生的類里訪問。Object.clo ne()會檢查原先的對象有多大,再為新對象騰出足夠多的內(nèi)存,將 所有二進(jìn)制位從原來的對象復(fù)制到新對象。這叫作“按位復(fù)制” ,而且按一般的 想法,這個工作應(yīng)該是由clone()方法來做的。但在Object.clone()正式開始操作前, 首先會檢
20、查一個類是否 Cloneable,即是否具有克隆能力一一換言之,它是否實 現(xiàn) 了 Cloneable 接 口 。 若 未 實 現(xiàn) , Object.clone() 就 擲 出 一 個 CloneNotSupportedException 違例,指出我們不能克隆它。因此,我們最好用一 個try-catch塊將對super.clone(的調(diào)用代碼包圍(或圭寸裝)起來,試圖捕獲一個 應(yīng)當(dāng)永不出現(xiàn)的違例(因為這里確實已實現(xiàn)了Clo neable接口)。在LocalCopy中,兩個方法g()和f()揭示出兩種參數(shù)傳遞方法間的差異。其 中,g()演示的是按引用傳遞,它會修改外部對象,并返回對那個外部對象的
21、一 個引用。而f()是對自變量進(jìn)行克隆,所以將其分離出來,并讓原來的對象保持 獨立。隨后,它繼續(xù)做它希望的事情。甚至能返回指向這個新對象的一個句柄, 而且不會對原來的對象產(chǎn)生任何副作用。注意下面這個多少有些古怪的語句:v = (MyObject)v.clone(); 它的作用正是創(chuàng)建一個本地副本。為避免被這樣的一個語句搞混淆,記住這 種相當(dāng)奇怪的編碼形式在 Java 中是完全允許的,因為有一個名字的所有東西實 際都是一個句柄。 所以句柄 v 用于克隆一個它所指向的副本, 而且最終返回指向 基礎(chǔ)類型Object的一個句柄(因為它在 Object.clone()中是那樣被定義的),隨后 必須將其造
22、型為正確的類型。在main()中,兩種不同參數(shù)傳遞方式的區(qū)別在于它們分別測試了一個不同的 方法。輸出結(jié)果如下:552 頁程序大家要記住這樣一個事實:Java對“是否等價”的測試并不對所比較對象的 內(nèi)部進(jìn)行檢查, 從而核實它們的值是否相同。 =和!=運(yùn)算符只是簡單地對比句柄 的內(nèi)容。若句柄內(nèi)的地址相同, 就認(rèn)為句柄指向同樣的對象, 所以認(rèn)為它們是“等 價”的。所以運(yùn)算符真正檢測的是 “由于別名問題, 句柄是否指向同一個對象?”12.2.5 Object.clo ne()的效果調(diào)用Object.clone()時,實際發(fā)生的是什么事情呢?當(dāng)我們在自己的類里覆蓋 clo ne()時,什么東西對于 su
23、per.clo ne(來說是最關(guān)鍵的呢?根類中的 clone()方法 負(fù)責(zé)建立正確的存儲容量, 并通過“按位復(fù)制” 將二進(jìn)制位從原始對象中復(fù)制到 新對象的存儲空間。 也就是說, 它并不只是預(yù)留存儲空間以及復(fù)制一個對象 實際需要調(diào)查出欲復(fù)制之對象的準(zhǔn)確大小, 然后復(fù)制那個對象。 由于所有這些工 作都是在由根類定義之clone()方法的內(nèi)部代碼中進(jìn)行的(根類并不知道要從自己 這里繼承出去什么),所以大家或許已經(jīng)猜到,這個過程需要用 RTTI 判斷欲克 隆的對象的實際大小。采取這種方式,clone()方法便可建立起正確數(shù)量的存儲空 間,并對那個類型進(jìn)行正確的按位復(fù)制。不管我們要做什么,克隆過程的第一
24、個部分通常都應(yīng)該是調(diào)用super.clone()。通過進(jìn)行一次準(zhǔn)確的復(fù)制,這樣做可為后續(xù)的克隆進(jìn)程建立起一個良好的基礎(chǔ)。 隨后,可采取另一些必要的操作,以完成最終的克隆。為確切了解其他操作是什么,首先要正確理解Object.clone()為我們帶來了什 么。特別地,它會自動克隆所有句柄指向的目標(biāo)嗎?下面這個例子可完成這種形 式的檢測:553-554頁程序一條Snake (蛇)由數(shù)段構(gòu)成,每一段的類型都是Snaka所以,這是一個一段段鏈接起來的列表。 所有段都是以循環(huán)方式創(chuàng)建的, 每做好一段, 都會使第 一個構(gòu)建器參數(shù)的值遞減, 直至最終為零。而為給每段賦予一個獨一無二的標(biāo)記, 第二個參數(shù)(一個
25、Char)的值在每次循環(huán)構(gòu)建器調(diào)用時都會遞增。in creme nt()方法的作用是循環(huán)遞增每個標(biāo)記,使我們能看到發(fā)生的變化;而 toString 則循環(huán)打印出每個標(biāo)記。輸出如下:554 頁中程序這意味著只有第一段才是由Object.clo ne()復(fù)制的,所以此時進(jìn)行的是一種“淺層復(fù)制”。若希望復(fù)制整條蛇即進(jìn)行“深層復(fù)制”必須在被覆蓋的 clo ne()里采取附加的操作。通常可在從一個能克隆的類里調(diào)用 super.clone()以確保所有基礎(chǔ)類行動(包 括Object.clone()能夠進(jìn)行。隨著是為對象內(nèi)每個句柄都明確調(diào)用一個clone();否則那些句柄會別名變成原始對象的句柄。 構(gòu)建器的
26、調(diào)用也大致相同首先構(gòu) 造基礎(chǔ)類,然后是下一個衍生的構(gòu)建器 , 以此類推, 直到位于最深層的衍生構(gòu) 建器。區(qū)別在于clone()并不是個構(gòu)建器,所以沒有辦法實現(xiàn)自動克隆。為了克隆, 必須由自己明確進(jìn)行。12.2.6 克隆合成對象試圖深層復(fù)制合成對象時會遇到一個問題。必須假定成員對象中的 clone() 方法也能依次對自己的句柄進(jìn)行深層復(fù)制, 以此類推。這使我們的操作變得復(fù)雜。 為了能正常實現(xiàn)深層復(fù)制, 必須對所有類中的代碼進(jìn)行控制, 或者至少全面掌握 深層復(fù)制中需要涉及的類,確保它們自己的深層復(fù)制能正確進(jìn)行。下面這個例子總結(jié)了面對一個合成對象進(jìn)行深層復(fù)制時需要做哪些事情:555-556頁程序De
27、pthReading和TemperatureReadin國E常相似;它們都只包含了基本數(shù)據(jù)類 型。所以clone()方法能夠非常簡單:調(diào)用super.clone(并返回結(jié)果即可。注意兩 個類使用的clone()代碼是完全一致的。OceanReading是由 DepthReading和 TemperatureReading對象合并而成的。為 了對其進(jìn)行深層復(fù)制,clone()必須同時克隆 OceanReading內(nèi)的句柄。為達(dá)到這 個目標(biāo),super.clone(的結(jié)果必須造型成一個 OceanReading對象(以便訪問depth 和 temperature句柄)。12.2.7用Vector進(jìn)
28、行深層復(fù)制下面讓我們復(fù)習(xí)一下本章早些時候提出的 Vector例子。這一次Int2類是可以 克隆的,所以能對Vector進(jìn)行深層復(fù)制:557-558頁程序Int3自Int2繼承而來,并添加了一個新的基本類型成員int j。大家也許認(rèn)為自己需要再次覆蓋clone(),以確保j得到復(fù)制,但實情并非如此。將Int2的clone() 當(dāng)作Int3的clone()調(diào)用時,它會調(diào)用 Object.clone(),判斷出當(dāng)前操作的是Int3, 并復(fù)制 Int3 內(nèi)的所有二進(jìn)制位。 只要沒有新增需要克隆的句柄, 對 Object.clone() 的一個調(diào)用就能完成所有必要的復(fù)制一一無論clo ne()是在層次結(jié)
29、構(gòu)多深的一級定義的。至此,大家可以總結(jié)出對 Vector進(jìn)行深層復(fù)制的先決條件:在克隆了 Vector 后,必須在其中遍歷,并克隆由 Vector指向的每個對象。為了對 Hashtable (散 列表)進(jìn)行深層復(fù)制,也必須采取類似的處理。這個例子剩余的部分顯示出克隆已實際進(jìn)行證據(jù)就是在克隆了對象以后,可以自由改變它,而原來那個對象不受任何影響。12.2.8 通過序列化進(jìn)行深層復(fù)制若研究一下第 10章介紹的那個 Java 1.1對象序列化示例,可能發(fā)現(xiàn)若在一個 對象序列化以后再撤消對它的序列化, 或者說進(jìn)行裝配, 那么實際經(jīng)歷的正是一 個“克隆”的過程。那么為什么不用序列化進(jìn)行深層復(fù)制呢?下面這
30、個例子通過計算執(zhí)行時間 對比了這兩種方法:559-560 頁程序其中, Thing2 和 Thing4 包含了成員對象,所以需要進(jìn)行一些深層復(fù)制。一 個有趣的地方是盡管 Serializable 類很容易設(shè)置,但在復(fù)制它們時卻要做多得多 的工作??寺∩婕暗酱罅康念愒O(shè)置工作, 但實際的對象復(fù)制是相當(dāng)簡單的。 結(jié)果 很好地說明了一切。下面是幾次運(yùn)行分別得到的結(jié)果:的確561 頁上程序除了序列化和克隆之間巨大的時間差異以外, 我們也注意到序列化技術(shù)的運(yùn) 行結(jié)果并不穩(wěn)定,而克隆每一次花費的時間都是相同的。12.2.9 使克隆具有更大的深度若新建一個類,它的基礎(chǔ)類會默認(rèn)為 Object,并默認(rèn)為不具備克
31、隆能力(就 象在下一節(jié)會看到的那樣) 。只要不明確地添加克隆能力,這種能力便不會自動 產(chǎn)生。但我們可以在任何層添加它,然后便可從那個層開始向下具有克隆能力。 如下所示:561-562頁程序添加克隆能力之前,編譯器會阻止我們的克隆嘗試。一旦在Scien tist里添加了克隆能力,那么Scientist以及它的所有“后裔”都可以克隆。12.2.10 為什么有這個奇怪的設(shè)計 之所以感覺這個方案的奇特,因為它事實上的確如此。也許大家會奇怪它為 什么要象這樣運(yùn)行, 而該方案背后的真正含義是什么呢?后面講述的是一個未獲 證實的故事大概是由于圍繞 Java 的許多買賣使其成為一種設(shè)計優(yōu)良的語言 但確實要花許
32、多口舌才能講清楚這背后發(fā)生的所有事情。最初,Java只是作為一種用于控制硬件的語言而設(shè)計,與因特網(wǎng)并沒有絲毫 聯(lián)系。象這樣一類面向大眾的語言一樣, 其意義在于程序員可以對任意一個對象 進(jìn)行克隆。這樣一來,clone()就放置在根類Object里面,但因為它是一種公用方 式,因而我們通常能夠?qū)θ我庖粋€對象進(jìn)行克隆。 看來這是最靈活的方式了, 畢 竟它不會帶來任何害處。正當(dāng) Java 看起來象一種終級因特網(wǎng)程序設(shè)計語言的時候,情況卻發(fā)生了變化。突然地,人們提出了安全問題,而且理所當(dāng)然,這些問題與使用對象有關(guān), 我們不愿望任何人克隆自己的保密對象。所以我們最后看到的是為原來那個簡 單、直觀的方案添加
33、的大量補(bǔ)?。篶lone()在Object里被設(shè)置成“ protected?!北仨殞⑵涓采w,并使用“ implement Cloneable ,同時解決違例的問題。只有在準(zhǔn)備調(diào)用Object的clone()方法時,才沒有必要使用 Cloneable接口, 因為那個方法會在運(yùn)行期間得到檢查,以確保我們的類實現(xiàn)了Cloneable。但為了保持連貫性(而且由于 Cloneable 無論如何都是空的),最好還是由自己實現(xiàn) Cloneable。12.3 克隆的控制為消除克隆能力,大家也許認(rèn)為只需將clone()方法簡單地設(shè)為private(私有) 即可,但這樣是行不通的, 因為不能采用一個基礎(chǔ)類方法, 并
34、使其在衍生類中更 “私有”。所以事情并沒有這么簡單。此外,我們有必要控制一個對象是否能夠 克隆。對于我們設(shè)計的一個類,實際有許多種方案都是可以采取的:(1) 保持中立,不為克隆做任何事情。也就是說,盡管不可對我們的類克隆, 但從它繼承的一個類卻可根據(jù)實際情況決定克隆。只有Object.clone()要對類中的 字段進(jìn)行某些合理的操作時,才可以作這方面的決定。支持clone(),采用實現(xiàn)Cloneable (可克隆)能力的標(biāo)準(zhǔn)操作,并覆蓋 clone()。在被覆蓋的clone()中,可調(diào)用super.clone(,并捕獲所有違例(這樣可 使clone()不“擲”出任何違例)。(3) 有條件地支持
35、克隆。若類容納了其他對象的句柄,而那些對象也許能夠 克隆(集合類便是這樣的一個例子) ,就可試著克隆擁有對方句柄的所有對象; 如果它們“擲”出了違例,只需讓這些違例通過即可。舉個例子來說,假設(shè)有一 個特殊的Vector,它試圖克隆自己容納的所有對象。編寫這樣的一個Vector時,并不知道客戶程序員會把什么形式的對象置入這個 Vector 中,所以并不知道它 們是否真的能夠克隆。不實現(xiàn)Cloneable(),但是將clone()覆蓋成protected,使任何字段都具有 正確的復(fù)制行為。這樣一來,從這個類繼承的所有東西都能覆蓋 cione(),并調(diào)用 super.clo ne(來產(chǎn)生正確的復(fù)制行
36、為。注意在我們實現(xiàn)方案里,可以而且應(yīng)該調(diào) 用super.clone(即使那個方法本來預(yù)期的是一個 Cloneable對象(否則會擲出一個違例),因為沒有人會在我們這種類型的對象上直接調(diào)用它。它只有通過一 個衍生類調(diào)用;對那個衍生類來說,如果要保證它正常工作,需實現(xiàn)Cloneable。(5)不實現(xiàn)Cloneable來試著防止克隆,并覆蓋clone(),以產(chǎn)生一個違例。為 使這一設(shè)想順利實現(xiàn), 只有令從它衍生出來的任何類都調(diào)用重新定義后的 clone() 里的 suepr.clone()。將類設(shè)為final,從而防止克隆。若clone()尚未被我們的任何一個上級類 覆蓋,這一設(shè)想便不會成功。若已被
37、覆蓋,那么再一次覆蓋它,并“擲”出一個 CloneNotSupportedException (克隆不支持)違例。為擔(dān)保克隆被禁止,將類設(shè)為 final 是唯一的辦法。 除此以外,一旦涉及保密對象或者遇到想對創(chuàng)建的對象數(shù)量 進(jìn)行控制的其他情況,應(yīng)該將所有構(gòu)建器都設(shè)為private,并提供一個或更多的特 殊方法來創(chuàng)建對象。 采用這種方式, 這些方法就可以限制創(chuàng)建的對象數(shù)量以及它 們的創(chuàng)建條件一一一種特殊情況是第16章要介紹的singleton (獨子)方案。面這個例子總結(jié)了克隆的各種實現(xiàn)方法,然后在層次結(jié)構(gòu)中將其“關(guān)閉”564-565頁程序第一個類 Ordinary 代表著大家在本書各處最常見到
38、的類: 不支持克隆, 但在Ordinary 對象的句便不能判斷它到底它正式應(yīng)用以后,卻也不禁止對其克隆。但假如有一個指向 柄,而且那個對象可能是從一個更深的衍生類上溯造型來的, 能不能克隆WrongClone 類 揭 示 了 實 現(xiàn) 克 隆 的 一 種 不 正 確 途 徑 。 它 確 實 覆 蓋 了 Object.clone(),并將那個方法設(shè)為public,但卻沒有實現(xiàn)Cloneableo所以一旦發(fā) 出對super.clone(的調(diào)用(由于對Object.clone()的一個調(diào)用造成的),便會無情地 擲出 CloneNotSupportedException違例。在IsCloneable中,
39、大家看到的才是進(jìn)行克隆的各種正確行動:先覆蓋clone(),并實現(xiàn)了 Cloneableo但是,這個clone()方法以及本例的另外幾個方法并不捕獲 CloneNotSupportedException 違例,而是任由它通過,并傳遞給調(diào)用者。隨后, 調(diào)用者必須用一個try-catch代碼塊把它包圍起來。在我們自己的clone()方法中,通常需要在cione()內(nèi)部捕獲CloneNotSupportedException違例,而不是任由它通 過。正如大家以后會理解的那樣,對這個例子來說,讓它通過是最正確的做法。類NoMore試圖按照J(rèn)ava設(shè)計者打算的那樣“關(guān)閉”克隆:在衍生類clone() 中
40、,我們擲出 CloneNotSupportedException違例。TryMore 類中的 clone()方法正 確地調(diào)用super.clone(),并解析成NoMore.clone(),后者擲出一個違例并禁止克 隆。但在已被覆蓋的clone()方法中,假若程序員不遵守調(diào)用super.clone(的“正確”方法,又會出現(xiàn)什么情況呢?在Back On中,大家可看到實際會發(fā)生什么。這個類用一個獨立的方法duplicate()制作當(dāng)前對象的一個副本,并在clo ne()內(nèi)部 調(diào)用這個方法,而不是調(diào)用 super.clone(違例永遠(yuǎn)不會產(chǎn)生,而且新類是可以 克隆的。因此,我們不能依賴“擲”出一個違
41、例的方法來防止產(chǎn)生一個可克隆的 類。唯一安全的方法在 ReallyNoMore中得到了演示,它設(shè)為final,所以不可繼 承。這意味著假如clone()在final類中擲出了一個違例,便不能通過繼承來進(jìn)行 修改,并可有效地禁止克隆(不能從一個擁有任意繼承級數(shù)的類中明確調(diào)用 Object.clone();只能調(diào)用super.clone()它只可訪問直接基礎(chǔ)類)。因此,只要制 作一些涉及安全問題的對象,就最好把那些類設(shè)為 final。在類CheckCloneable中,我們看到的第一個類是tryToClone(),它能接納任 何Ordinary對象,并用instanceof檢查它是否能夠克隆。若答
42、案是肯定的,就將 對象造型成為一個IsCloneable,調(diào)用clone(),并將結(jié)果造型回Ordinary,最后捕 獲有可能產(chǎn)生的任何違例。 請注意用運(yùn)行期類型鑒定(見第 11 章)打印出類名, 使自己看到發(fā)生的一切情況。在main()中,我們創(chuàng)建了不同類型的 Ordinary對象,并在數(shù)組定義中上溯造 型成為Ordinary。在這之后的頭兩行代碼創(chuàng)建了一個純粹的Ordinary對象,并試圖對其克隆。然而,這些代碼不會得到編譯,因為clone()是Object中的一個protected (受到保護(hù)的)方法。代碼剩余的部分將遍歷數(shù)組,并試著克隆每個對 象,分別報告它們的成功或失敗。輸出如下:5
43、67-568頁程序總之,如果希望一個類能夠克隆,那么:(1) 實現(xiàn) Cloneable接口(2) 覆蓋 clone() 在自己的clone()中調(diào)用super.clone() 在自己的clone()中捕獲違例 這一系列步驟能達(dá)到最理想的效果。12.3.1 副本構(gòu)建器 克隆看起來要求進(jìn)行非常復(fù)雜的設(shè)置,似乎還該有另一種替代方案。一個辦法是制作特殊的構(gòu)建器,令其負(fù)責(zé)復(fù)制一個對象。在 C+中,這叫作“副本構(gòu)建 器”剛開始的時候,這好象是一種非常顯然的解決方案 (如果你是C+程序員, 這個方法就更顯親切) 。下面是一個實際的例子:568-571 頁程序這個例子第一眼看上去顯得有點奇怪。不同水果的質(zhì)量肯
44、定有所區(qū)別,但為 什么只是把代表那些質(zhì)量的數(shù)據(jù)成員直接置入Fruit (水果)類?有兩方面可能的原因。第一個是我們可能想簡便地插入或修改質(zhì)量。 注意 Fruit 有一個 protected (受到保護(hù)的)addQualities()方法,它允許衍生類來進(jìn)行這些插入或修改操作(大 家或許會認(rèn)為最合乎邏輯的做法是在Fruit中使用一個protected構(gòu)建器,用它獲取 FruitQualities 參數(shù),但構(gòu)建器不能繼承, 所以不可在第二級或級數(shù)更深的類中 使用它)。通過將水果的質(zhì)量置入一個獨立的類,可以得到更大的靈活性,其中 包括可以在特定 Fruit 對象的存在期間中途更改質(zhì)量。之所以將 Fr
45、uitQualities 設(shè)為一個獨立的對象, 另一個原因是考慮到我們有時 希望添加新的質(zhì)量,或者通過繼承與多形性改變行為。注意對Gree nZebra來說(這實際是西紅柿的一類我已栽種成功,它們簡直令人難以置信) ,構(gòu)建器 會調(diào)用 addQualities(),并為其傳遞一個ZebraQualities對象。該對象是從 FruitQualities衍生出來的,所以能與基礎(chǔ)類中的FruitQualities句柄聯(lián)系在一起。當(dāng)然,一旦GreenZebra使用FruitQualities,就必須將其下溯造型成為正確的類型 (就象evaluate。中展示的那樣),但它肯定知道類型是ZebraQual
46、ities。大家也看到有一個Seed (種子)類,F(xiàn)ruit (大家都知道,水果含有自己的種 子)包含了一個Seed數(shù)組。最后,注意每個類都有一個副本構(gòu)建器,而且每個副本構(gòu)建器都必須關(guān)心為 基礎(chǔ)類和成員對象調(diào)用副本構(gòu)建器的問題,從而獲得“深層復(fù)制”的效果。對副 本構(gòu)建器的測試是在CopyConstructor類內(nèi)進(jìn)行的。方法 ripen()需要獲取一個Tomato參數(shù),并對其執(zhí)行副本構(gòu)建工作,以便復(fù)制對象:t = new Tomato(t);而slice()需要獲取一個更常規(guī)的Fruit對象,而且對它進(jìn)行復(fù)制:f = new Fruit(f);它們都在main()中伴隨不同種類的Fruit進(jìn)行
47、測試。下面是輸出結(jié)果:572 頁上程序從中可以看出一個問題。在slice()內(nèi)部對Tomato進(jìn)行了副本構(gòu)建工作以后,結(jié)果便不再是一個 Tomato 對象,而只是一個 Fruit 。它已丟失了作為一個 Tomato (西紅柿)的所有特征。此外,如果采用一個GreenZebra ripen()和slice()會把它分別轉(zhuǎn)換成一個Tomato和一個Fruit。所以非常不幸,假如想制作對象的一個 本地副本,Java中的副本構(gòu)建器便不是特別適合我們。1.為什么在C+的作用比在Java中大?副本構(gòu)建器是C+的一個基本構(gòu)成部分,因為它能自動產(chǎn)生對象的一個本地 副本。但前面的例子確實證明了它不適合在 Jav
48、a中使用,為什么呢?在Java中, 我們操控的一切東西都是句柄,而在C+中,卻可以使用類似于句柄的東西,也 能直接傳遞對象。這時便要用到C+的副本構(gòu)建器:只要想獲得一個對象,并按 值傳遞它,就可以復(fù)制對象。所以它在C+里能很好地工作,但應(yīng)注意這套機(jī)制 在Java里是很不通的,所以不要用它。12.4 只讀類盡管在一些特定的場合,由 clo ne()產(chǎn)生的本地副本能夠獲得我們希望的結(jié) 果,但程序員(方法的作者)不得不親自禁止別名處理的副作用。假如想制作一 個庫,令其具有常規(guī)用途, 但卻不能擔(dān)保它肯定能在正確的類中得以克隆, 這時 又該怎么辦呢?更有可能的一種情況是, 假如我們想讓別名發(fā)揮積極的作用
49、 禁止不必要的對象復(fù)制但卻不希望看到由此造成的副作用, 那么又該如何處 理呢?一個辦法是創(chuàng)建“不變對象” ,令其從屬于只讀類??啥x一個特殊的類, 使其中沒有任何方法能造成對象內(nèi)部狀態(tài)的改變。 在這樣的一個類中, 別名處理 是沒有問題的。 因為我們只能讀取內(nèi)部狀態(tài), 所以當(dāng)多處代碼都讀取相同的對象 時,不會出現(xiàn)任何副作用。作為“不變對象” 一個簡單例子,Java的標(biāo)準(zhǔn)庫包含了“封裝器” (wrapper) 類,可用于所有基本數(shù)據(jù)類型。 大家可能已發(fā)現(xiàn)了這一點, 如果想在一個象 Vector (只采用 Object 句柄)這樣的集合里保存一個 int 數(shù)值,可以將這個 int 封裝到 標(biāo)準(zhǔn)庫的I
50、nteger類內(nèi)部。如下所示:573頁中程序Integer 類(以及基本的“封裝器”類)用簡單的形式實現(xiàn)了“不變性” :它 們沒有提供可以修改對象的方法。若確實需要一個容納了基本數(shù)據(jù)類型的對象,并想對基本數(shù)據(jù)類型進(jìn)行修 改,就必須親自創(chuàng)建它們。幸運(yùn)的是,操作非常簡單:573-574頁程序注意 n 在這里簡化了我們的編碼。 若默認(rèn)的初始化為零已經(jīng)足夠(便不需要構(gòu)建器) ,而且不用考慮把它打印 出來(便不需要toString),那么IntValue甚至還能更加簡單。如下所示:class IntValue int n; 將元素取出來,再對其進(jìn)行造型,這多少顯得有些笨拙,但那是Vector的問題,不是
51、IntValue的錯。12.4.1 創(chuàng)建只讀類完全可以創(chuàng)建自己的只讀類,下面是個簡單的例子:574-575頁程序所有數(shù)據(jù)都設(shè)為private,可以看到?jīng)]有任何public方法對數(shù)據(jù)作出修改。事 實上,確實需要修改一個對象的方法是quadruple。,但它的作用是新建一個Immutable1 對象,初始對象則是原封未動的。方法f()需要取得一個Immutablel對象,并對其采取不同的操作,而 main() 的輸出顯示出沒有對x作任何修改。因此,x對象可別名處理許多次,不會造成 任何傷害,因為根據(jù) Immutable1 類的設(shè)計,它能保證對象不被改動。12.4.2 一“成不變”的弊端 從表面看,
52、不變類的建立似乎是一個好方案。但是,一旦真的需要那種新類 型的一個修改的對象, 就必須辛苦地進(jìn)行新對象的創(chuàng)建工作, 同時還有可能涉及 更頻繁的垃圾收集。對有些類來說,這個問題并不是很大。但對其他類來說(比 如 String 類),這一方案的代價顯得太高了。為解決這個問題,我們可以創(chuàng)建一個“同志”類,并使其能夠修改。以后只 要涉及大量的修改工作, 就可換為使用能修改的同志類。 完事以后, 再切換回不 可變的類。因此,上例可改成下面這個樣子:575-577頁程序和往常一樣, Immutable2 包含的方法保留了對象不可變的特征,只要涉及修 改,就創(chuàng)建新的對象。完成這些操作的是add()和 mul
53、tiply。方法。同志類叫作Mutable,它也含有add()和multiply()方法。但這些方法能夠修改 Mutable對象, 而不是新建一個。除此以外, Mutable 的一個方法可用它的數(shù)據(jù)產(chǎn)生一個 Immutable2 對象,反之亦然。兩個靜態(tài)方法modify1()和modify2()揭示出獲得同樣結(jié)果的兩種不同方法。 在modify1()中,所有工作都是在Immutable2類中完成的,我們可看到在進(jìn)程中 創(chuàng)建了四個新的Immutable2對象(而且每次重新分配了 val,前一個對象就成為 垃圾)。在方法modify2()中,可看到它的第一個行動是獲取Immutable2 y,然后
54、從中 生成一個Mutable (類似于前面對clone()的調(diào)用,但這一次創(chuàng)建了一個不同類型 的對象)。隨后,用Mutable對象進(jìn)行大量修改操作,同時用不著新建許多對象。 最后,它切換回Immutable2。在這里,我們只創(chuàng)建了兩個新對象( Mutable和 Immutable2 的結(jié)果),而不是四個。這一方法特別適合在下述場合應(yīng)用:(1) 需要不可變的對象,而且(2) 經(jīng)常需要進(jìn)行大量修改,或者(3) 創(chuàng)建新的不變對象代價太高12.4.3 不變字串 請觀察下述代碼:577-578頁程序q傳遞進(jìn)入upcase()時,它實際是q的句柄的一個副本。該句柄連接的對象 實際只在一個統(tǒng)一的物理位置處。
55、句柄四處傳遞的時候,它的句柄會得到復(fù)制。若觀察對upcase()的定義,會發(fā)現(xiàn)傳遞進(jìn)入的句柄有一個名字 s,而且該名字 只有在upcase(執(zhí)行期間才會存在。upcase(完成后,本地句柄s便會消失,而 upcase(返回結(jié)果一一還是原來那個字串,只是所有字符都變成了大寫。當(dāng)然, 它返回的實際是結(jié)果的一個句柄。 但它返回的句柄最終是為一個新對象的, 同時 原來的 q 并未發(fā)生變化。所有這些是如何發(fā)生的呢?1. 隱式常數(shù)若使用下述語句:String s = asdf;String x = Stringer.upcase(s);那么真的希望upcase()方法改變自變量或者參數(shù)嗎?我們通常是不愿意
56、的, 因為作為提供給方法的一種信息, 自變量一般是拿給代碼的讀者看的, 而不是讓 他們修改。這是一個相當(dāng)重要的保證,因為它使代碼更易編寫和理解。為了在C+中實現(xiàn)這一保證,需要一個特殊關(guān)鍵字的幫助:const。利用這個 關(guān)鍵字,程序員可以保證一個句柄(C+叫“指針”或者“引用”)不會被用來 修改原始的對象。但這樣一來,C+程序員需要用心記住在所有地方都使用 const。 這顯然易使人混淆,也不容易記住。2. 覆蓋+和 StringBuffer利用前面提到的技術(shù), String 類的對象被設(shè)計成“不可變” 。若查閱聯(lián)機(jī)文檔 中關(guān)于 String 類的內(nèi)容(本章稍后還要總結(jié)它) ,就會發(fā)現(xiàn)類中能夠修
57、改 String 的每個方法實際都創(chuàng)建和返回了一個嶄新的String對象,新對象里包含了修改過的信息原來的String是原圭寸未動的。因此,Java里沒有與C+的const對應(yīng)的特性可用來讓編譯器支持對象的不可變能力。 若想獲得這一能力, 可以自行設(shè) 置,就象 String 那樣。由于 String 對象是不可變的,所以能夠根據(jù)情況對一個特定的 String 進(jìn)行多 次別名處理。 因為它是只讀的, 所以一個句柄不可能會改變一些會影響其他句柄 的東西。因此,只讀對象可以很好地解決別名問題。通過修改產(chǎn)生對象的一個嶄新版本,似乎可以解決修改對象時的所有問題, 就象 String 那樣。但對某些操作來講,這種方法的效率并不高。一個典型的例子 便是為String對象覆蓋的運(yùn)算符“ +”?!案采w”意味著在與一個特定的類使用時, 它的含義已發(fā)生了變化(用于 String的“+”和“+二”是Java中能被覆蓋的唯一運(yùn) 算符,Java不允許程序員覆蓋其他任何運(yùn)算符一一注釋)。:C+允許程序員隨意覆蓋運(yùn)算符。由于這通常是一個復(fù)雜的過程(參見Thinking in C+,Prentice-Hall于1995年出版),所以Java的設(shè)計者認(rèn)定它是 一種“糟糕”的特性,決定不在 Java中采用。但具有諷剌意味的是,運(yùn)算符的覆蓋在Java中要比在C+中容易得多針對 Str
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 農(nóng)村衛(wèi)生安全生產(chǎn)培訓(xùn)
- 有關(guān)愛國的演講稿范文(28篇)
- 2025年貨幣經(jīng)紀(jì)公司服務(wù)市場分析現(xiàn)狀
- DBT29-272-2019 天津市快速軌道交通盾構(gòu)隧道設(shè)計規(guī)程
- 2025年中國鎳基變形高溫合金行業(yè)市場發(fā)展前景及發(fā)展趨勢與投資戰(zhàn)略研究報告
- 2025年尿試紙條項目可行性研究報告
- 2024-2030年中國激光牙齒漂白機(jī)行業(yè)市場深度分析及發(fā)展趨勢預(yù)測報告
- 古代詩歌鑒賞及其相關(guān)文史知識梳理提要(詩歌分類)
- 2025年木制餐盒項目投資可行性研究分析報告
- 2025年舞臺設(shè)備項目安全評估報告
- 【中考真題】2024年河南省普通高中招生考試歷史試卷(含答案)
- 2024版年度經(jīng)濟(jì)法基礎(chǔ)完整全套課件
- JT-T-445-2021汽車底盤測功機(jī)
- 體育科學(xué):田徑考試考試題(三)
- 2024年4月自考03200預(yù)防醫(yī)學(xué)(二)試題
- 《研學(xué)旅行市場營銷》課件-模塊八 研學(xué)旅行促銷策略
- 糖尿病孕婦護(hù)理:案例分析
- 《過華清宮絕句(其一)》-【中職專用】高一語文(高教版2023基礎(chǔ)模塊下冊)
- (2024年)新版藥品管理法培訓(xùn)課件
- 2022年4月自考00808商法試題及答案含解析
- JGJT280-2012 中小學(xué)校體育設(shè)施技術(shù)規(guī)程
評論
0/150
提交評論