近三年多以來(lái),Valhalla項(xiàng)目一直是Java社區(qū)的“流行語(yǔ)”,但很少有關(guān)于這個(gè)重要項(xiàng)目的公開(kāi)文章。對(duì)于一部分人來(lái)說(shuō),Valhalla項(xiàng)目意味著創(chuàng)建值類型的能力,對(duì)于另外一部分人來(lái)說(shuō),Valhalla項(xiàng)目意味著具體化通用運(yùn)行時(shí)類型。
Valhalla項(xiàng)目有一個(gè)非常明確的目標(biāo):停止Java開(kāi)發(fā)人員在性能和抽象之間進(jìn)行選擇的要求。這篇文章將介紹什么是Valhalla項(xiàng)目,以及它帶來(lái)了什么問(wèn)題。
什么是Valhalla項(xiàng)目?
Valhalla項(xiàng)目是一個(gè)始于2014年的OpenJDK項(xiàng)目,由Brian Goetz領(lǐng)導(dǎo),目的是為Java Development Kit(JDK)10或未來(lái)的Java版本引入基于價(jià)值的優(yōu)化。該項(xiàng)目主要側(cè)重于允許開(kāi)發(fā)人員創(chuàng)建和使用值類型,或像原語(yǔ)一樣不引用值。用Goetz說(shuō):“Codes like a class, works like an int“。
相比于引用類型(對(duì)象),值類型的主要好處是無(wú)論在內(nèi)存還是計(jì)算中,都刪除了引用類型的開(kāi)銷(xiāo)。例如,盡管與對(duì)象關(guān)聯(lián)的實(shí)際大小和開(kāi)銷(xiāo)特定于Java虛擬機(jī)(JVM)實(shí)現(xiàn),但所有JVM都包含用于存儲(chǔ)有關(guān)對(duì)象信息的字節(jié),包括多態(tài)信息、標(biāo)識(shí)信息、同步信息和垃圾收集元數(shù)據(jù)(如引用計(jì)數(shù)器)。另外,當(dāng)必須訪問(wèn)與引用類型關(guān)聯(lián)的對(duì)象時(shí),引用必須是被間接引用的,賦予一個(gè)間接層。在某些情況下,這種開(kāi)銷(xiāo)是多余的、是不需要的。
盡管值類型在某些重要的特征中偏離了對(duì)象,但它們保持了與類相似的抽象層次。例如,值類型可能仍然有方法和字段,都帶有便于封裝的可見(jiàn)性修飾符。雖然,在當(dāng)前的Java版本(編寫(xiě)本文時(shí)的JDK 9)中,主要差異之一是值類型(如基本類型)不能被用作泛型類型參數(shù);例如,List
盡管像int這樣的原始類型可以被自動(dòng)結(jié)合到引用類型(比如Integer),但主要的缺點(diǎn)是:它會(huì)重新引入對(duì)象的開(kāi)銷(xiāo)。盡管自JDK 5引,這一直是Java泛型的一個(gè)障礙,但在Valhalla項(xiàng)目中,開(kāi)發(fā)人員可以創(chuàng)建新的值類型。為了解決這個(gè)問(wèn)題,Valhalla還負(fù)責(zé)提供一種機(jī)制,允許將值類型作為有效的泛型參數(shù)提供,同時(shí)仍然保持Java的當(dāng)前類型被刪除的通用語(yǔ)義。
什么是值類型?
值類型是一組直接存儲(chǔ)在內(nèi)存中的數(shù)據(jù)組,而不是對(duì)數(shù)據(jù)的引用(或指針)。因此,值類型可以被認(rèn)為是只消耗內(nèi)存來(lái)存儲(chǔ)包含在其字段中的數(shù)據(jù)聚合,而沒(méi)有額外的開(kāi)銷(xiāo)。從概念上講,原語(yǔ)是值類型的一個(gè)可重新定義的例子,其中int只消耗32位字節(jié)來(lái)存儲(chǔ)元數(shù)據(jù)。
將數(shù)據(jù)直接放在內(nèi)存中(而不是參考)被稱為扁平化,其優(yōu)點(diǎn)在數(shù)組中更為明顯。在對(duì)象數(shù)組的情況下,數(shù)組中的每個(gè)元素都存儲(chǔ)對(duì)與該元素關(guān)聯(lián)的對(duì)象的引用,要求在訪問(wèn)對(duì)象之前執(zhí)行取消引用。在值類型的數(shù)組中,值直接放置在數(shù)組中,并保證在連續(xù)的內(nèi)存中。這個(gè)想法如下圖所示:
使用值類型比參考類型有一些明顯的好處,包括:
* 減少內(nèi)存使用情況:沒(méi)有額外的內(nèi)存用于存儲(chǔ)對(duì)象元數(shù)據(jù),例如便于同步、標(biāo)識(shí)和垃圾回收的標(biāo)志。對(duì)于像Integer這樣的小對(duì)象,對(duì)象的開(kāi)銷(xiāo)可以匹配甚至超過(guò)數(shù)據(jù)本身的大小。
* 減少間接性:由于對(duì)象是以Java的引用類型存儲(chǔ)的,每次訪問(wèn)對(duì)象時(shí),都必須先解除引用,從而導(dǎo)致執(zhí)行額外的指令。與值類型相關(guān)聯(lián)的扁平化數(shù)據(jù)會(huì)立即出現(xiàn)在需要它們的位置,因此不需要取消引用。
* 增加局部性:扁平化值對(duì)象刪除了間接存儲(chǔ)在內(nèi)存中的可能性,增加值相鄰地存儲(chǔ)在內(nèi)存中的可能性,特別是對(duì)于數(shù)組或其他連續(xù)的內(nèi)存結(jié)構(gòu)。
引用類型和值類型之間的主要區(qū)別之一是標(biāo)識(shí)的對(duì)應(yīng)定義:引用類型的標(biāo)識(shí)內(nèi)在地綁定到對(duì)象,而值類型的標(biāo)識(shí)綁定到其當(dāng)前狀態(tài)。
例如,日期可以被認(rèn)為是一個(gè)值類型,其中一個(gè)值實(shí)例解析為2018年1月1日,等于另一個(gè)解析為2018年1月1日的值實(shí)例,不管這兩個(gè)實(shí)例是不是相同。只要一個(gè)實(shí)例的數(shù)據(jù)與另一個(gè)實(shí)例的數(shù)據(jù)匹配字段,這兩個(gè)實(shí)例是可互換的(即它們可以被識(shí)別為相同的值)。在實(shí)踐中,日期通常被存儲(chǔ)為自某個(gè)時(shí)期以來(lái)的時(shí)間單位(例如毫秒)。因此,如果兩個(gè)日期值類型從相同時(shí)期開(kāi)始具有相同的毫秒數(shù),則它們是相等的。
雖然值類型有許多的好處,但有好必然就有壞,特別是在現(xiàn)有的Java語(yǔ)言中。在JDK版本的每次迭代中,最重要的問(wèn)題之一是向后兼容性,主要是現(xiàn)有二進(jìn)制字節(jié)碼表示的兼容性。盡管可以將值類型與Java目前使用的原始值模型相匹配,但仍然存在一些主要缺陷,其中包括:
* 原語(yǔ)目前不支持限定的方法調(diào)用或字段訪問(wèn)(即使用點(diǎn)運(yùn)算符);
* 用戶定義的值類型不存在文字;
* 所有值類型都不支持內(nèi)置運(yùn)算符(如+);
* 很難為所有用戶定義類型的默認(rèn)值;
除了現(xiàn)有原始模型中的這些缺點(diǎn)之外,還有其他一些關(guān)于用戶定義的值類型必須解決的問(wèn)題,包括:
* 值類型可以實(shí)現(xiàn)接口嗎?
* 一個(gè)值可以是另一個(gè)值子類化?
* 值類型是否隱式地派生了一個(gè)基類?
* 值類型在必要時(shí)可以被當(dāng)作對(duì)象嗎?
什么是通用專業(yè)化?
自從JDK 5中包含了泛型以來(lái),Java泛型的一個(gè)主要缺陷就是缺少對(duì)原始類型參數(shù)的支持。例如,盡管List
在運(yùn)行時(shí)解決以下幾行問(wèn)題:
這允許現(xiàn)有代碼繼續(xù)使用泛型類的原始類型而不進(jìn)行更改,從而保留了現(xiàn)有Java代碼的向后兼容性。例如,現(xiàn)有代碼仍然可以使用原始類型列表(沒(méi)有任何泛型類型參數(shù)),因?yàn)樵碱愋秃头盒皖愋偷倪\(yùn)行時(shí)類型將通過(guò)刪除來(lái)解析相同類型:List。此外,由于泛型類型的參數(shù)不再存在于泛型類型,因此所有泛型參數(shù)都被引用類型對(duì)象替換。例如,給定以下類定義:
等效的運(yùn)行時(shí)類型將是:
然后,編譯器負(fù)責(zé)生成正確的強(qiáng)制轉(zhuǎn)換和橋接方法,以確保所有使用的均勻轉(zhuǎn)換的運(yùn)行類型所有用法都是類型安全的。
會(huì)有效地變成:
由于類型消除,運(yùn)行時(shí)泛型參數(shù)必須被解析為某種類型。之所以選擇對(duì)象,是因?yàn)樗丝梢源鎯?chǔ)任何用戶定義類型的最高類型。由于原始值(例如int,double,boolean等)沒(méi)有從Object類繼承,所以它們被禁止用作泛型類型參數(shù)。實(shí)際上,隨著泛型的引入,Java的運(yùn)行時(shí)性質(zhì)不能隨著泛型的引入而改變,導(dǎo)致原始類型不能用作泛型類型參數(shù)。
如何將值類型用作類型參數(shù)?
一般來(lái)說(shuō),在面向?qū)ο蟮木幊陶Z(yǔ)言中支持泛型的技術(shù)有兩種。第一種技術(shù),即同類轉(zhuǎn)換,將泛型類型解析為單一類型,而不考慮其類型參數(shù)。這是Java使用的技術(shù),List
隨著用戶定義的值類型的引入,非引用類型被用作泛型類型參數(shù)的需求變得更大。正如我們已經(jīng)看到的,同質(zhì)轉(zhuǎn)換不足以提供非引用類型的泛型參數(shù)方法。為了解決這種困境,在保持與現(xiàn)有向后兼容性的同時(shí),設(shè)計(jì)了一種混合解決方案,將同質(zhì)的作為參考類型,而異構(gòu)的用于值類型。
為了確保現(xiàn)有的泛型類仍然可以運(yùn)行,一個(gè)新的關(guān)鍵字any與泛型類型參數(shù)聲明一起使用,表示它將使用增強(qiáng)的泛型(允許將值類型和引用類型作為泛型類型參數(shù))。例如,增強(qiáng)的泛型Box類將如下所示:
盡管在需要增強(qiáng)泛型時(shí)包含any關(guān)鍵字似乎很麻煩,但是還有一些重要的情況是必須維護(hù)現(xiàn)有的泛型語(yǔ)義。例如,ArrayList類具有以下方法:
最初,remove(int)方法的設(shè)計(jì)是假定泛型參數(shù)是引用類型。如果這個(gè)限制被放寬,原始泛型類型參數(shù)被允許,則會(huì)出現(xiàn)方法沖突,如果通用參數(shù)T解析為類型int,則上述兩個(gè)方法將解析為相同的簽名。這可以通過(guò)創(chuàng)建兩個(gè)具有不同名稱的方法來(lái)解決,例如removeByPosition和removeElement,但是這仍然需要對(duì)ArrayList的接口進(jìn)行更改,因此不能假定所有的泛型類都可以自動(dòng)接受值類型的泛型參數(shù)。
在Valhalla項(xiàng)目的這一點(diǎn)上,關(guān)于泛型專業(yè)化的問(wèn)題仍然存在很多問(wèn)題,但是可以得出這樣的結(jié)論:如果將值類型添加到Java中,那么接受這些值類型的泛型很可能會(huì)與它們一起出現(xiàn)。雖然還有大量的工作做,包括具有值型的泛型語(yǔ)義,但是還是有一些重大的決定,即使在它的初期也是如此。
這是否意味著Java將會(huì)具體化?
作為Java使用同類轉(zhuǎn)換的必然結(jié)果,通用類型的泛型參數(shù)不通用,或者是在運(yùn)行時(shí)可用的。這對(duì)于無(wú)數(shù)的應(yīng)用程序來(lái)說(shuō)都是一個(gè)大問(wèn)題,但為了權(quán)衡向后兼容性,Java應(yīng)用程序引入了泛型。隨著Valhalla項(xiàng)目中泛型類型的重新審視,不禁要問(wèn):Java是否有具體化的類型?
這個(gè)問(wèn)題的答案很可能是,是的,但只有在價(jià)值型泛型的情況下。盡管對(duì)于值類型參數(shù)的通用特化的實(shí)現(xiàn)還沒(méi)有被固化,但實(shí)現(xiàn)可能包括泛化類型。然而,為了保持向后兼容性,引用類型泛型參數(shù)不太可能會(huì)被視為具體化。雖然普遍認(rèn)可的泛型有很多好處,但也有一些主要缺點(diǎn):
* 與現(xiàn)有的參考類型不兼容
* 假定泛型在運(yùn)行時(shí)被擦除的優(yōu)化失效
* 字節(jié)碼的復(fù)雜性來(lái)維護(hù)運(yùn)行時(shí)類型信息
盡管目前還沒(méi)有在stone中設(shè)置任何東西,但通用的通用具體化將會(huì)進(jìn)入項(xiàng)目Valhalla,即使有值類型的泛型會(huì)看到某種程度的部分具體化
結(jié)論
隨著Jigsaw項(xiàng)目和功能性編程語(yǔ)義的引入,對(duì)于Java語(yǔ)言來(lái)說(shuō),這是一個(gè)進(jìn)步,雖然Java不是完美的語(yǔ)言,但正在采取措施使其進(jìn)一步成熟,提供了新的功能和語(yǔ)義,以便在沒(méi)有適當(dāng)抽象的情況下獲得更好的性能和問(wèn)題映射。