jvm之前端編譯與優(yōu)化

在Java技術(shù)下談“編譯期”而沒有具體上下文語境的話肃廓,其實(shí)是一句很含糊的表述,因?yàn)樗赡苁侵敢粋€(gè)前端編譯器(叫“編譯器的前端”更準(zhǔn)確一些)把.java文件轉(zhuǎn)變成.class文件的過程陈哑;也可能是指Java虛擬機(jī)的即時(shí)編譯器(常稱JIT編譯器妻坝,Just In Time Compiler)運(yùn)行期把字節(jié)碼轉(zhuǎn)變成本地機(jī)器碼的過程;還可能是指使用靜態(tài)的提前編譯器(常稱AOT編譯器惊窖,Ahead Of Time Compiler)直接把程序編譯成與目標(biāo)機(jī)器指令集相關(guān)的二進(jìn)制代碼的過程刽宪。下面筆者列舉了這3類編譯過程里一些比較有代表性的編譯器產(chǎn)品:

  • 前端編譯器:JDK的Javac、Eclipse JDT中的增量式編譯器(ECJ)[1]界酒。
  • 即時(shí)編譯器:HotSpot虛擬機(jī)的C1圣拄、C2編譯器,Graal編譯器毁欣。
  • 提前編譯器:JDK的Jaotc庇谆、GNU Compiler for the Java(GCJ)[2]、Excelsior JET

這3類過程中最符合普通程序員對(duì)Java程序編譯認(rèn)知的應(yīng)該是第一類凭疮,本文中的“前端”指的也是這種由前端編譯器完成的編譯行為饭耳。限制了“編譯期”的范圍后,我們對(duì)于“優(yōu)化”二字的定義也需要放寬一些执解,因?yàn)镴avac這類前端編譯器對(duì)代碼的運(yùn)行效率幾乎沒有任何優(yōu)化措施可言(在JDK 1.3之后寞肖,Javac的-O優(yōu)化參數(shù)就不再有意義),哪怕是編譯器真的采取
了優(yōu)化措施也不會(huì)產(chǎn)生什么實(shí)質(zhì)的效果。因?yàn)镴ava虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)選擇把對(duì)性能的優(yōu)化全部集中到運(yùn)行期的即時(shí)編譯器中新蟆,這樣可以讓那些不是由Javac產(chǎn)生的Class文件(如JRuby觅赊、Groovy等語言的Class文件)也同樣能享受到編譯器優(yōu)化措施所帶來的性能紅利。但是琼稻,如果把“優(yōu)化”的定義放寬吮螺,把對(duì)開發(fā)階段的優(yōu)化也計(jì)算進(jìn)來的話,Javac確實(shí)是做了許多針對(duì)Java語言編碼過程的優(yōu)化措施來降低程序員的編碼復(fù)雜度欣簇、提高編碼效率规脸。相當(dāng)多新生的Java語法特性,都是靠編譯器的“語法糖”來實(shí)現(xiàn)熊咽,而不是依賴字節(jié)碼或者Java虛擬機(jī)的底層改進(jìn)來支持莫鸭。我們可以這樣認(rèn)為,Java中即時(shí)編譯器在運(yùn)行期的優(yōu)化過程横殴,支撐了程序執(zhí)行效率的不斷提升被因;而前端編譯器在編譯期的優(yōu)化過程,則是支撐著程序員的編碼效率和語言使用者的幸福感的提高衫仑。

Javac編譯器

分析源碼是了解一項(xiàng)技術(shù)的實(shí)現(xiàn)內(nèi)幕最徹底的手段梨与,Javac編譯器不像HotSpot虛擬機(jī)那樣使用C++語言(包含少量C語言)實(shí)現(xiàn),它本身就是一個(gè)由Java語言編寫的程序文狱,這為純Java的程序員了解它的編譯過程帶來了很大的便利粥鞋。

從Javac代碼的總體結(jié)構(gòu)來看,編譯過程大致可以分為1個(gè)準(zhǔn)備過程和3個(gè)處理過程瞄崇,它們分別如下所示呻粹。

1)準(zhǔn)備過程:初始化插入式注解處理器。
2)解析與填充符號(hào)表過程苏研,包括

  • 詞法等浊、語法分析。將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記集合摹蘑,構(gòu)造出抽象語法樹筹燕。
  • 填充符號(hào)表。產(chǎn)生符號(hào)地址和符號(hào)信息衅鹿。

3)插入式注解處理器的注解處理過程:
4)分析與字節(jié)碼生成過程撒踪,包括:

  • ·標(biāo)注檢查。對(duì)語法的靜態(tài)信息進(jìn)行檢查大渤。
  • 數(shù)據(jù)流及控制流分析制妄。對(duì)程序動(dòng)態(tài)運(yùn)行過程進(jìn)行檢查。
  • 解語法糖兼犯。將簡(jiǎn)化代碼編寫的語法糖還原為原有的形式。
  • 字節(jié)碼生成。將前面各個(gè)步驟所生成的信息轉(zhuǎn)化成字節(jié)碼切黔。

上述3個(gè)處理過程里砸脊,執(zhí)行插入式注解時(shí)又可能會(huì)產(chǎn)生新的符號(hào),如果有新的符號(hào)產(chǎn)生纬霞,就必須轉(zhuǎn)回到之前的解析凌埂、填充符號(hào)表的過程中重新處理這些新符號(hào),從總體來看诗芜,三者之間的關(guān)系與交互順序如圖10-4所示瞳抓。

image.png

我們可以把上述處理過程對(duì)應(yīng)到代碼中,Javac編譯動(dòng)作的入口是
com.sun.tools.javac.main.JavaCompiler類伏恐,上述3個(gè)過程的代碼邏輯集中在這個(gè)類的compile()和compile2()方法里孩哑,其中主體代碼如圖10-5所示,整個(gè)編譯過程主要的處理由圖中標(biāo)注的8個(gè)方法來完成翠桦。

image.png

接下來横蜒,我們將對(duì)照J(rèn)avac的源代碼,逐項(xiàng)講解上述過程销凑。

解析與填充符號(hào)表

解析過程由圖10-5中的parseFiles()方法(圖10-5中的過程1.1)來完成丛晌,解析過程包括了經(jīng)典程序編譯原理中的詞法分析和語法分析兩個(gè)步驟。

詞法斗幼、語法分析

詞法分析是將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合的過程澎蛛,單個(gè)字符是程序編寫時(shí)的最小元素,但標(biāo)記才是編譯時(shí)的最小元素蜕窿。關(guān)鍵字谋逻、變量名、字面量渠羞、運(yùn)算符都可以作為標(biāo)記斤贰,如“int a=b+2”這句代碼中就包含了6個(gè)標(biāo)記,分別是int次询、a荧恍、=、b屯吊、+送巡、2,雖然關(guān)鍵字int由3個(gè)字符構(gòu)成盒卸,但是它只是一個(gè)獨(dú)立的標(biāo)記骗爆,不可以再拆分。在Javac的源碼中蔽介,詞法分析過程由com.sun.tools.javac.parser.Scanner類來實(shí)現(xiàn)摘投。

語法分析是根據(jù)標(biāo)記序列構(gòu)造抽象語法樹的過程煮寡,抽象語法樹(Abstract Syntax Tree,AST)是一種用來描述程序代碼語法結(jié)構(gòu)的樹形表示方式犀呼,抽象語法樹的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語法結(jié)構(gòu)(SyntaxConstruct)幸撕,例如包、類型外臂、修飾符坐儿、運(yùn)算符、接口宋光、返回值甚至連代碼注釋等都可以是一種特定的語法結(jié)構(gòu)貌矿。語法分析過程由
com.sun.tools.javac.parser.Parser類實(shí)現(xiàn),這個(gè)階段產(chǎn)出的抽象語法樹是以com.sun.tools.javac.tree.JCTree類表示的


image.png

經(jīng)過詞法和語法分析生成語法樹以后罪佳,編譯器就不會(huì)再對(duì)源碼字符流進(jìn)行操作了逛漫,后續(xù)的操作都建立在抽象語法樹之上。

填充符號(hào)表

完成了語法分析和詞法分析之后菇民,下一個(gè)階段是對(duì)符號(hào)表進(jìn)行填充的過程尽楔,也就是圖10-5中enterTrees()方法(圖10-5中注釋的過程1.2)要做的事情。符號(hào)表(Symbol Table)是由一組符號(hào)地址和符號(hào)信息構(gòu)成的數(shù)據(jù)結(jié)構(gòu)第练,讀者可以把它類比想象成哈希表中鍵值對(duì)的存儲(chǔ)形式阔馋。符號(hào)表中所登記的信息在編譯的不同階段都要被用到。譬如在語義分析的過程中娇掏,符號(hào)表所登記的內(nèi)容將用于語義檢查(如檢查一個(gè)名字的使用和原先的聲明是否一致)和產(chǎn)生中間代碼呕寝,在目標(biāo)代碼生成階段,當(dāng)對(duì)符號(hào)
名進(jìn)行地址分配時(shí)婴梧,符號(hào)表是地址分配的直接依據(jù)下梢。

注解處理器

JDK 5之后,Java語言提供了對(duì)注解(Annotations)的支持塞蹭,注解在設(shè)計(jì)上原本是與普通的Java代碼一樣孽江,都只會(huì)在程序運(yùn)行期間發(fā)揮作用的。但在JDK 6中又提出并通過了JSR-269提案[1]番电,該提案設(shè)計(jì)了一組被稱為“插入式注解處理器”的標(biāo)準(zhǔn)API岗屏,可以提前至編譯期對(duì)代碼中的特定注解進(jìn)行處理,從而影響到前端編譯器的工作過程漱办。我們可以把插入式注解處理器看作是一組編譯器的插件这刷,當(dāng)這些插件工作時(shí),允許讀取娩井、修改暇屋、添加抽象語法樹中的任意元素。如果這些插件在處理注解期間對(duì)語法樹進(jìn)行過修改洞辣,編譯器將回到解析及填充符號(hào)表的過程重新處理咐刨,直到所有插入式注解處理器都沒有再對(duì)語法樹進(jìn)行修改為止昙衅,每一次循環(huán)過程稱為一個(gè)輪次(Round),這也就對(duì)應(yīng)著圖10-4的那個(gè)回環(huán)過程定鸟。

有了編譯器注解處理的標(biāo)準(zhǔn)API后绒尊,程序員的代碼才有可能干涉編譯器的行為,由于語法樹中的任意元素仔粥,甚至包括代碼注釋都可以在插件中被訪問到,所以通過插入式注解處理器實(shí)現(xiàn)的插件在功能上有很大的發(fā)揮空間蟹但。只要有足夠的創(chuàng)意躯泰,程序員能使用插入式注解處理器來實(shí)現(xiàn)許多原本只能在編碼中由人工完成的事情。譬如Java著名的編碼效率工具Lombok华糖,它可以通過注解來實(shí)現(xiàn)自動(dòng)產(chǎn)生getter/setter方法麦向、進(jìn)行空置檢查、生成受查異常表客叉、產(chǎn)生equals()和hashCode()方法诵竭,等等,幫助開發(fā)人員消除Java的冗長(zhǎng)代碼兼搏,這些都是依賴插入式注解處理器來實(shí)現(xiàn)的卵慰,
在Javac源碼中,插入式注解處理器的初始化過程是在initPorcessAnnotations()方法中完成的佛呻,而它的執(zhí)行過程則是在processAnnotations()方法中完成裳朋。這個(gè)方法會(huì)判斷是否還有新的注解處理器需要執(zhí)行,如果有的話吓著,通com.sun.tools.javac.processing.JavacProcessing-Environment類的doProcessing()方法來生成一個(gè)新的JavaCompiler對(duì)象鲤嫡,對(duì)編譯的后續(xù)步驟進(jìn)行處理。

語義分析與字節(jié)碼生成

經(jīng)過語法分析之后绑莺,編譯器獲得了程序代碼的抽象語法樹表示暖眼,抽象語法樹能夠表示一個(gè)結(jié)構(gòu)正確的源程序,但無法保證源程序的語義是符合邏輯的纺裁。而語義分析的主要任務(wù)則是對(duì)結(jié)構(gòu)上正確的源程序進(jìn)行上下文相關(guān)性質(zhì)的檢查诫肠,譬如進(jìn)行類型檢查、控制流檢查对扶、數(shù)據(jù)流檢查区赵,等等。舉個(gè)簡(jiǎn)單的例子浪南,假設(shè)有如下3個(gè)變量定義語句:

int a = 1;
boolean b = false;
char c = 2;

后續(xù)可能出現(xiàn)的賦值運(yùn)算:

int d = a + c;
int d = b + c;
char d = a + c;

后續(xù)代碼中如果出現(xiàn)了如上3種賦值運(yùn)算的話笼才,那它們都能構(gòu)成結(jié)構(gòu)正確的抽象語法樹,但是只有第一種的寫法在語義上是沒有錯(cuò)誤的络凿,能夠通過檢查和編譯骡送。其余兩種在Java語言中是不合邏輯的昂羡,無法編譯(是否合乎語義邏輯必須限定在具體的語言與具體的上下文環(huán)境之中才有意義。如在C語言中摔踱,a虐先、b、c的上下文定義不變派敷,第二蛹批、三種寫法都是可以被正確編譯的)。我們編碼時(shí)經(jīng)常能在IDE中看到由紅線標(biāo)注的錯(cuò)誤提示篮愉,其中絕大部分都是來源于語義分析階段的檢查結(jié)果腐芍。

標(biāo)注檢查
Javac在編譯過程中,語義分析過程可分為標(biāo)注檢查和數(shù)據(jù)及控制流分析兩個(gè)步驟试躏,分別由圖10-5的attribute()和flow()方法(分別對(duì)應(yīng)圖10-5中的過程3.1和過程3.2)完成猪勇。

標(biāo)注檢查步驟要檢查的內(nèi)容包括諸如變量使用前是否已被聲明、變量與賦值之間的數(shù)據(jù)類型是否能夠匹配颠蕴,等等泣刹,剛才3個(gè)變量定義的例子就屬于標(biāo)注檢查的處理范疇。在標(biāo)注檢查中犀被,還會(huì)順便進(jìn)行一個(gè)稱為常量折疊(Constant Folding)的代碼優(yōu)化椅您,這是Javac編譯器會(huì)對(duì)源代碼做的極少量?jī)?yōu)化措施之一(代碼優(yōu)化幾乎都在即時(shí)編譯器中進(jìn)行)。如果我們?cè)贘ava代碼中寫下如下所示的變量定義:

int a = 1+ 2;

則在抽象語法樹上仍然能看到字面量“1”“2”和操作符“+”號(hào)寡键,但是在經(jīng)過常量折疊優(yōu)化之后襟沮,它們將會(huì)被折疊為字面量“3”,如圖10-7所示昌腰,這個(gè)插入式表達(dá)式(Infix Expression)的值已經(jīng)在語法樹上標(biāo)注出來了(ConstantExpressionValue:3)开伏。由于編譯期間進(jìn)行了常量折疊,所以在代碼里面定義“a=1+2”比起直接定義“a=3”來遭商,并不會(huì)增加程序運(yùn)行期哪怕僅僅一個(gè)處理器時(shí)鐘周期的處理工作量固灵。


image.png

數(shù)據(jù)及控制流分析
數(shù)據(jù)流分析和控制流分析是對(duì)程序上下文邏輯更進(jìn)一步的驗(yàn)證,它可以檢查出諸如程序局部變量在使用前是否有賦值劫流、方法的每條路徑是否都有返回值巫玻、是否所有的受查異常都被正確處理了等問題。編譯時(shí)期的數(shù)據(jù)及控制流分析與類加載時(shí)的數(shù)據(jù)及控制流分析的目的基本上可以看作是一致的祠汇,但校驗(yàn)范圍會(huì)有所區(qū)別仍秤,有一些校驗(yàn)項(xiàng)只有在編譯期或運(yùn)行期才能進(jìn)行。下面舉一個(gè)關(guān)于final修飾符的數(shù)據(jù)及控制流分析的例子可很,見代碼清單10-1所示诗力。

// 方法一帶有final修飾
public void foo(final int arg) {
final int var = 0;
// do something
}
// 方法二沒有final修飾
public void foo(int arg) {
int var = 0;
// do something
}

在這兩個(gè)foo()方法中,一個(gè)方法的參數(shù)和局部變量定義使用了final修飾符我抠,另外一個(gè)則沒有苇本,在代碼編寫時(shí)程序肯定會(huì)受到final修飾符的影響袜茧,不能再改變arg和var變量的值,但是如果觀察這兩段代碼編譯出來的字節(jié)碼瓣窄,會(huì)發(fā)現(xiàn)它們是沒有任何一點(diǎn)區(qū)別的笛厦,每條指令,甚至每個(gè)字節(jié)都一模一樣俺夕。通過前面文字對(duì)Class文件結(jié)構(gòu)的講解我們已經(jīng)知道裳凸,局部變量與類的字段(實(shí)例變量、類變量)的存儲(chǔ)是有顯著差別的劝贸,局部變量在常量池中并沒有CONSTANT_Fieldref_info的符號(hào)引用登舞,自然就不可能存儲(chǔ)有訪問標(biāo)志(access_flags)的信息,自然在Class文件中就不可能知道一個(gè)局部變量是不是被聲明為final了悬荣。因此,可以肯定地推斷出把局部變量聲明為final疙剑,對(duì)運(yùn)行期是完全沒有影響的氯迂,變量的不變性僅僅由Javac編譯器在編譯期間來保障,這就是一個(gè)只能在編譯期而不能在運(yùn)行期中檢查的例子言缤。

解語法糖

語法糖(Syntactic Sugar)嚼蚀,也稱糖衣語法,是由英國(guó)計(jì)算機(jī)科學(xué)家Peter J.Landin發(fā)明的一種編程術(shù)語管挟,指的是在計(jì)算機(jī)語言中添加的某種語法轿曙,這種語法對(duì)語言的編譯結(jié)果和功能并沒有實(shí)際影響,但是卻能更方便程序員使用該語言僻孝。通常來說使用語法糖能夠減少代碼量导帝、增加程序的可讀性,從而減少程序代碼出錯(cuò)的機(jī)會(huì)穿铆。

Java在現(xiàn)代編程語言之中已經(jīng)屬于“低糖語言”(相對(duì)于C#及許多其他Java虛擬機(jī)語言來說)您单,尤其是JDK 5之前的Java≤癯“低糖”的語法讓Java程序?qū)崿F(xiàn)相同功能的代碼量往往高于其他語言虐秦,通俗地說就是會(huì)顯得比較“啰嗦”,這也是Java語言一直被質(zhì)疑是否已經(jīng)“落后”了的一個(gè)浮于表面的理由凤优。

Java中最常見的語法糖包括了前面提到過的泛型悦陋、變長(zhǎng)參數(shù)、自動(dòng)裝箱拆箱筑辨,等等俺驶,Java虛擬機(jī)運(yùn)行時(shí)并不直接支持這些語法,它們?cè)诰幾g階段被還原回原始的基礎(chǔ)語法結(jié)構(gòu)棍辕,這個(gè)過程就稱為解語法糖痒钝。Java的這些語法糖是如何實(shí)現(xiàn)的秉颗、被分解后會(huì)是什么樣子, 后面會(huì)詳細(xì)介紹。

字節(jié)碼生成

字節(jié)碼生成是Javac編譯過程的最后一個(gè)階段送矩,在Javac源碼里面由com.sun.tools.javac.jvm.Gen類來完成蚕甥。字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息(語法樹、符號(hào)表)轉(zhuǎn)化成字節(jié)碼指令寫到磁盤中栋荸,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作菇怀。

例如前文多次登場(chǎng)的實(shí)例構(gòu)造器<init>()方法和類構(gòu)造器<clinit>()方法就是在這個(gè)階段被添加到語法樹之中的。請(qǐng)注意這里的實(shí)例構(gòu)造器并不等同于默認(rèn)構(gòu)造函數(shù)晌块,如果用戶代碼中沒有提供任何構(gòu)造函數(shù)爱沟,那編譯器將會(huì)添加一個(gè)沒有參數(shù)的、可訪問性與當(dāng)前類型一致的默認(rèn)構(gòu)造函數(shù)匆背,這個(gè)工作在填充符號(hào)表階段中就已經(jīng)完成呼伸。

<init>()和<clinit>()這兩個(gè)構(gòu)造器的產(chǎn)生實(shí)際上是一種代碼收斂的過程,編譯器會(huì)把語句塊(對(duì)于實(shí)例構(gòu)造器而言是“{}”塊钝尸,對(duì)于類構(gòu)造器而言是“static{}”塊)括享、變量初始化(實(shí)例變量和類變量)、調(diào)用父類的實(shí)例構(gòu)造器等操作收斂到<init>()和<clinit>()方法之中珍促,并且保證無論源碼中出現(xiàn)的順序如何铃辖,都一定是按先執(zhí)行父類的實(shí)例構(gòu)造器,然后初始化變量猪叙,最后執(zhí)行語句塊的順序進(jìn)行娇斩,上面所述的動(dòng)作由Gen::normalizeDefs()方法來實(shí)現(xiàn)。除了生成構(gòu)造器以外穴翩,還有其他的一些代碼替換工作用于優(yōu)化程序某些邏輯的實(shí)現(xiàn)方式犬第,如把字符串的加操作替換為StringBuffer或StringBuilder(取決于目標(biāo)代碼的版本是否大于或等于JDK 5)的append()操作,等等芒帕。

完成了對(duì)語法樹的遍歷和調(diào)整之后瓶殃,就會(huì)把填充了所有所需信息的符號(hào)表交到com.sun.tools.javac.jvm.ClassWriter類手上,由這個(gè)類的writeClass()方法輸出字節(jié)碼副签,生成最終的Class文件遥椿,到此,整個(gè)編譯過程宣告結(jié)束淆储。

Java語法糖的味道

幾乎所有的編程語言都或多或少提供過一些語法糖來方便程序員的代碼開發(fā)冠场,這些語法糖雖然不會(huì)提供實(shí)質(zhì)性的功能改進(jìn),但是它們或能提高效率本砰,或能提升語法的嚴(yán)謹(jǐn)性碴裙,或能減少編碼出錯(cuò)的機(jī)會(huì)。現(xiàn)在也有一種觀點(diǎn)認(rèn)為語法糖并不一定都是有益的,大量添加和使用含糖的語法舔株,容易讓程序員產(chǎn)生依賴莺琳,無法看清語法糖的糖衣背后挫望,程序代碼的真實(shí)面目膘婶。

總而言之箫老,語法糖可以看作是前端編譯器實(shí)現(xiàn)的一些“小把戲”蹦浦,這些“小把戲”可能會(huì)使效率得到“大提升”,但我們也應(yīng)該去了解這些“小把戲”背后的真實(shí)面貌脐雪,那樣才能利用好它們莺掠,而不是被它們所迷惑闲礼。

泛型

泛型的本質(zhì)是參數(shù)化類型(Parameterized Type)或者參數(shù)化多態(tài)(Parametric Polymorphism)的應(yīng)用寡具,即可以將操作的數(shù)據(jù)類型指定為方法簽名中的一種特殊參數(shù)秤茅,這種參數(shù)類型能夠用在類、接口和方法的創(chuàng)建中童叠,分別構(gòu)成泛型類框喳、泛型接口和泛型方法。泛型讓程序員能夠針對(duì)泛化的數(shù)據(jù)類型編寫相同的算法厦坛,這極大地增強(qiáng)了編程語言的類型系統(tǒng)及抽象能力五垮。

在2004年,Java和C#兩門語言于同一年更新了一個(gè)重要的大版本粪般,即Java 5.0和C#2.0,在這個(gè)大版本中污桦,兩門語言又不約而同地各自添加了泛型的語法特性亩歹。不過,兩門語言對(duì)泛型的實(shí)現(xiàn)方式卻選擇了截然不同的路徑凡橱。本來Java和C#天生就存在著比較和競(jìng)爭(zhēng)小作,泛型這個(gè)兩門語言在同一年、同一個(gè)功能上做出的不同選擇稼钩,自然免不了被大家對(duì)比審視一番顾稀,其結(jié)論是Java的泛型直到今天依然作為Java語言不如C#語言好用的“鐵證”被眾人嘲諷。筆者在本節(jié)介紹Java泛型時(shí)坝撑,并不會(huì)去嘗試推翻這個(gè)結(jié)論静秆,相反甚至還會(huì)去舉例來揭示Java泛型的缺陷所在,但同時(shí)也必須向不了解Java泛型機(jī)制和歷史的讀者說清楚巡李,Java選擇這樣的泛型實(shí)現(xiàn)抚笔,是出于當(dāng)時(shí)語言現(xiàn)狀的權(quán)衡,而不是語言先進(jìn)性或者設(shè)計(jì)者水平不如C#之類的原因侨拦。

Java與C#的泛型

Java選擇的泛型實(shí)現(xiàn)方式叫作“類型擦除式泛型”(Type Erasure Generics)殊橙,而C#選擇的泛型實(shí)現(xiàn)方式是“具現(xiàn)化式泛型”(Reified Generics)C#里面泛型無論在程序源碼里面、編譯后的中間語言表示(Intermediate Language,這時(shí)候泛型是一個(gè)占位符)里面膨蛮,抑或是運(yùn)行期的CLR里面都是切實(shí)存在的叠纹,List<int>與List<string>就是兩個(gè)不同的類型,它們由系統(tǒng)在運(yùn)行期生成敞葛,有著自己獨(dú)立的虛方法表和類型數(shù)據(jù)誉察。而Java語言中的泛型則不同,它只在程序源碼中存在制肮,在編譯后的字節(jié)碼文件中冒窍,全部泛型都被替換為原來的裸類型了,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼豺鼻,因此對(duì)于運(yùn)行期的Java語言來說ArrayList<int>與ArrayList<String>其實(shí)是同一個(gè)類型

如果你是一名C#開發(fā)人員综液,可能很難想象代碼清單10-2中的Java
代碼都是不合法的。
代碼清單10-2 Java中不支持的泛型用法

public class TypeErasureGenerics<E> {
public void doSomething(Object item) {
if (item instanceof E) { // 不合法儒飒,無法對(duì)泛型進(jìn)行實(shí)例判斷
...
}
E newItem = new E(); // 不合法谬莹,無法使用泛型創(chuàng)建對(duì)象
E[] itemArray = new E[10]; // 不合法,無法使用泛型創(chuàng)建數(shù)組
}
}

上面這些是Java泛型在編碼階段產(chǎn)生的不良影響桩了,如果說這種使用層次上的差別還可以通過多寫幾行代碼附帽、方法中多加一兩個(gè)類型參數(shù)來解決的話,性能上的差距則是難以用編碼彌補(bǔ)的井誉。C#2.0引入了泛型之后蕉扮,帶來的顯著優(yōu)勢(shì)之一便是對(duì)比起Java在執(zhí)行性能上的提高,因?yàn)樵谑褂闷脚_(tái)提供的容器類型(如List<T>颗圣,Dictionary<TKey喳钟,TValue>)時(shí),無須像Java里那樣不厭其煩地拆箱和裝箱在岂,如果在Java中要避免這種損失奔则,就必須構(gòu)造一個(gè)與數(shù)據(jù)類型相關(guān)的容器類(譬如IntFloatHashMap這樣的
容器)。顯然蔽午,這除了引入更多代碼造成復(fù)雜度提高易茬、復(fù)用性降低之外,更是喪失了泛型本身的存在價(jià)值及老。

Java的類型擦除式泛型無論在使用效果上還是運(yùn)行效率上抽莱,幾乎是全面落后于C#的具現(xiàn)化式泛型,而它的唯一優(yōu)勢(shì)是在于實(shí)現(xiàn)這種泛型的影響范圍上:擦除式泛型的實(shí)現(xiàn)幾乎只需要在Javac編譯器上做出改進(jìn)即可骄恶,不需要改動(dòng)字節(jié)碼岸蜗、不需要改動(dòng)Java虛擬機(jī),也保證了以前沒有使用泛型的庫可以直接運(yùn)行在Java 5.0之上叠蝇。但這種聽起來節(jié)省工作量甚至可以說是有偷工減料嫌疑的優(yōu)勢(shì)就顯得非常短視璃岳,真的能在當(dāng)年Java實(shí)現(xiàn)泛型的利弊權(quán)衡中勝出嗎年缎?答案的確是它勝出了,但我們必須在那時(shí)的泛
型歷史背景中去考慮不同實(shí)現(xiàn)方式帶來的代價(jià)铃慷。

泛型的歷史背景

泛型思想早在C++語言的模板(Template)功能中就開始生根發(fā)芽单芜,而在Java語言中加入泛型的首次嘗試是出現(xiàn)在1996年。Martin Odersky(后來Scala語言的締造者)當(dāng)時(shí)是德國(guó)卡爾斯魯厄大學(xué)編程
理論的教授犁柜,他想設(shè)計(jì)一門能夠支持函數(shù)式編程的程序語言洲鸠,又不想從頭把編程語言的所有功能都再做一遍,所以就注意到了剛剛發(fā)布一年的Java馋缅,并在它上面實(shí)現(xiàn)了函數(shù)式編程的3大特性:泛型扒腕、高階函數(shù)和模式匹配,形成了Scala語言的前身Pizza語言萤悴。后來瘾腰,Java的開發(fā)團(tuán)隊(duì)找到了Martin Odersky,表示對(duì)Pizza語言的泛型功能很感興趣覆履,他們就一起建立了一個(gè)叫作“Generic Java”的新項(xiàng)目蹋盆,目標(biāo)是把Pizza語言的泛型單獨(dú)拎出來移植到Java語言上,其最終成果就是Java 5.0中的那個(gè)泛型實(shí)現(xiàn)硝全,但是移植的過程并不是一開始就朝著類型擦除式泛型去的栖雾,事實(shí)上Pizza語言中的泛型更接近于現(xiàn)在C#的泛型。Martin Odersky自己在采訪自述中提到伟众,進(jìn)行Generic Java項(xiàng)目的過程中他受到了重重約束析藕,甚至多次讓他感到沮喪,最緊凳厢、最難的約束來源于被迫要完全向后兼容無泛型Java账胧,即保證“二進(jìn)制向后兼容性”(Binary Backwards Compatibility)。二進(jìn)制向后兼容性是明確寫入《Java語言規(guī)范》中的對(duì)Java使用者的嚴(yán)肅承諾数初,譬如一個(gè)在JDK 1.2中編譯出來的Class文件找爱,必須保證能夠在JDK 12乃至以后的版本中也能夠正常運(yùn)行[5]梗顺。這樣泡孩,既然Java到1.4.2版之前都沒有支持過泛型,而到Java 5.0突然要支持泛型了寺谤,還要讓以前編譯的程序在新版本的虛擬機(jī)還能正常運(yùn)行仑鸥,就意味著以前沒有的限制不能突然間冒出來。

舉個(gè)例子变屁,在沒有泛型的時(shí)代眼俊,由于Java中的數(shù)組是支持協(xié)變(Covariant)的,對(duì)應(yīng)的集合類也可以存入不同類型的元素粟关,類似于代碼清單10-3這樣的代碼盡管不提倡疮胖,但是完全可以正常編譯成Class文件。
代碼清單10-3 以下代碼可正常編譯為Class

Object[] array = new String[10];
array[0] = 10; // 編譯期不會(huì)有問題,運(yùn)行時(shí)會(huì)報(bào)錯(cuò)
ArrayList things = new ArrayList();
things.add(Integer.valueOf(10)); //編譯澎灸、運(yùn)行時(shí)都不會(huì)報(bào)錯(cuò)
things.add("hello world");

為了保證這些編譯出來的Class文件可以在Java 5.0引入泛型之后繼續(xù)運(yùn)行院塞,設(shè)計(jì)者面前大體上有兩條路可以選擇:
1)需要泛型化的類型(主要是容器類型),以前有的就保持不變性昭,然后平行地加一套泛型化版本的新類型拦止。
2)直接把已有的類型泛型化,即讓所有需要泛型化的已有類型都原地泛型化糜颠,不添加任何平行于已有類型的泛型版汹族。

在這個(gè)分叉路口,C#走了第一條路其兴,添加了一組System.Collections.Generic的新容器顶瞒,以前的System.Collections以及System.Collections.Specialized容器類型繼續(xù)存在。C#的開發(fā)人員很快就接受了新的容器忌警,倒也沒出現(xiàn)過什么不適應(yīng)的問題搁拙,唯一的不適大概是許多.NET自身的標(biāo)準(zhǔn)庫已經(jīng)把老容器類型當(dāng)作方法的返回值或者參數(shù)使用,這些方法至今還保持著原來的老樣子法绵。

但如果相同的選擇出現(xiàn)在Java中就很可能不會(huì)是相同的結(jié)果了箕速,要知道當(dāng)時(shí).NET才問世兩年,而Java已經(jīng)有快十年的歷史了朋譬,再加上各自流行程度的不同盐茎,兩者遺留代碼的規(guī)模根本不在同一個(gè)數(shù)量級(jí)上。而且更大的問題是Java并不是沒有做過第一條路那樣的技術(shù)決策徙赢,在JDK 1.2時(shí)字柠,遺留代碼規(guī)模尚小,Java就引入過新的集合類狡赐,并且保留了舊集合類不動(dòng)窑业。這導(dǎo)致了直到現(xiàn)在標(biāo)準(zhǔn)類庫中還有Vector(老)和ArrayList(新)、有Hashtable(老)和HashMap(新)等兩套容器代碼并存枕屉,如果當(dāng)
時(shí)再擺弄出像Vector(老)常柄、ArrayList(新)、Vector<T>(老但有泛型)搀擂、ArrayList<T>(新且有泛型)這樣的容器集合西潘,可能叫罵聲會(huì)比今天聽到的更響更大。

到了這里哨颂,相信讀者已經(jīng)能稍微理解為什么當(dāng)時(shí)Java只能選擇第二條路了喷市。但第二條路也并不意味著一定只能使用類型擦除來實(shí)現(xiàn),如果當(dāng)時(shí)有足夠的時(shí)間好好設(shè)計(jì)和實(shí)現(xiàn)威恼,是完全有可能做出更好的泛型系統(tǒng)的品姓,否則也不會(huì)有今天的Valhalla項(xiàng)目來還以前泛型偷懶留下的技術(shù)債了寝并。下面我們就來看看當(dāng)時(shí)做的類型擦除式泛型的實(shí)現(xiàn)時(shí)到底哪里偷懶了,又帶來了怎樣的缺陷腹备。

.類型擦除

我們繼續(xù)以ArrayList為例來介紹Java泛型的類型擦除具體是如何實(shí)現(xiàn)的食茎。由于Java選擇了第二條路,直接把已有的類型泛型化馏谨。要讓所有需要泛型化的已有類型别渔,譬如ArrayList,原地泛型化后變成了ArrayList<T>惧互,而且保證以前直接用ArrayList的代碼在泛型新版本里必須還能繼續(xù)用這同一個(gè)容器哎媚,這就必須讓所有泛型化的實(shí)例類型,譬如ArrayList<Integer>喊儡、ArrayList<String>這些全部自動(dòng)成為ArrayList的子類型才能可以拨与,否則類型轉(zhuǎn)換就是不安全的。由此就引出了“裸類型”(Raw Type)的概念艾猜,裸類型應(yīng)被視為所有該類型泛型化實(shí)例的共同父類型(Super Type)买喧,只有這樣,像代碼清單10-4中的賦值才是被系統(tǒng)允許的從子類到父類的安全轉(zhuǎn)型匆赃。
代碼清單10-4 裸類型賦值

ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list; // 裸類型
list = ilist;
list = slist;

接下來的問題是該如何實(shí)現(xiàn)裸類型淤毛。這里又有了兩種選擇:一種是在運(yùn)行期由Java虛擬機(jī)來自動(dòng)地、真實(shí)地構(gòu)造出ArrayList<Integer>這樣的類型算柳,并且自動(dòng)實(shí)現(xiàn)從ArrayList<Integer>派生自ArrayList的繼承關(guān)系來滿足裸類型的定義低淡;另外一種是索性簡(jiǎn)單粗暴地直接在編譯時(shí)把ArrayList<Integer>還原回ArrayList,只在元素訪問瞬项、修改時(shí)自動(dòng)插入一些強(qiáng)制類型轉(zhuǎn)換和檢查指令蔗蹋,這樣看起來也是能滿足需要,這兩個(gè)選擇的最終結(jié)果大家已經(jīng)都知道了囱淋。代碼清單10-5是一段簡(jiǎn)單的Java泛型例子猪杭,我們可以看一下它編譯后的實(shí)際樣子是怎樣的。
代碼清單10-5 泛型擦除前的例子

public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("hello", "你好");
map.put("how are you?", "吃了沒妥衣?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you?"));
}

把這段Java代碼編譯成Class文件皂吮,然后再用字節(jié)碼反編譯工具進(jìn)行反編譯后,將會(huì)發(fā)現(xiàn)泛型都不見了称鳞,程序又變回了Java泛型出現(xiàn)之前的寫法涮较,泛型類型都變回了裸類型稠鼻,只在元素訪問時(shí)插入了從Object到String的強(qiáng)制轉(zhuǎn)型代碼冈止,如代碼清單10-6所示。
代碼清單10-6 泛型擦除后的例子

public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "你好");
map.put("how are you?", "吃了沒候齿?");
System.out.println((String) map.get("hello"));
System.out.println((String) map.get("how are you?"));
}

類型擦除帶來的缺陷在此舉3個(gè)例子熙暴,把前面與C#對(duì)比時(shí)簡(jiǎn)要提及的擦除式泛型的缺陷做更具體的說明闺属。
代碼清單10-7 原始類型的泛型(目前的Java不支持)

ArrayList<int> ilist = new ArrayList<int>();
ArrayList<long> llist = new ArrayList<long>();
ArrayList list;
list = ilist;
list = llist;

這種情況下,一旦把泛型信息擦除后周霉,到要插入強(qiáng)制轉(zhuǎn)型代碼的地方就沒辦法往下做了掂器,因?yàn)椴恢С謎nt、long與Object之間的強(qiáng)制轉(zhuǎn)型俱箱。當(dāng)時(shí)Java給出的解決方案一如既往的簡(jiǎn)單粗暴:既然沒法轉(zhuǎn)換,那就索性別支持原生類型的泛型了吧国瓮,你們都用ArrayList<Integer>,ArrayList<Long>,反正都做了自動(dòng)的強(qiáng)制類型轉(zhuǎn)換狞谱,遇到原生類型時(shí)把裝箱乃摹、拆箱也自動(dòng)做了得了。這個(gè)決定后面導(dǎo)致了無數(shù)構(gòu)造包裝類和裝箱跟衅、拆箱的開銷孵睬,成為Java泛型慢的重要原因。

第二伶跷,運(yùn)行期無法取到泛型類型信息掰读,會(huì)讓一些代碼變得相當(dāng)啰嗦,譬如代碼清單10-2中羅列的幾種Java不支持的泛型用法叭莫,都是由于運(yùn)行期Java虛擬機(jī)無法取得泛型類型而導(dǎo)致的蹈集。像代碼清單10-8這樣,我們?nèi)懸粋€(gè)泛型版本的從List到數(shù)組的轉(zhuǎn)換方法雇初,由于不能從List中取得參數(shù)化類型T雾狈,所以不得不從一個(gè)額外參數(shù)中再傳入一個(gè)數(shù)組的組件類型進(jìn)去,實(shí)屬無奈抵皱。
代碼清單10-8 不得不加入的類型參數(shù)

public static <T> T[] convert(List<T> list, Class<T> componentType) {
T[] array = (T[])Array.newInstance(componentType, list.size());
...
}

最后善榛,通過擦除法來實(shí)現(xiàn)泛型,還喪失了一些面向?qū)ο笏枷霊?yīng)有的優(yōu)雅呻畸,帶來了一些模棱兩可的模糊狀況移盆,例如代碼清單10-9的例子。

代碼清單10-9 當(dāng)泛型遇見重載1

public class GenericTypes {
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}

請(qǐng)讀者思考一下伤为,上面這段代碼是否正確咒循,能否編譯執(zhí)行?也許你已經(jīng)有了答案绞愚,這段代碼是不能被編譯的叙甸,因?yàn)閰?shù)List<Integer>和List<String>編譯之后都被擦除了,變成了同一種的裸類型List位衩,類型擦除導(dǎo)致這兩個(gè)方法的特征簽名變得一模一樣裆蒸。初步看來,無法重載的原因已經(jīng)找到了糖驴,但是真的就是如此嗎僚祷?其實(shí)這個(gè)例子中泛型擦除成相同的裸類型只是無法重載的其中一部分原因佛致,請(qǐng)?jiān)俳又匆豢创a清單10-10中的內(nèi)容。
代碼清單10-10 當(dāng)泛型遇見重載2

public class GenericTypes {
public static String method(List<String> list) {
System.out.println("invoke method(List<String> list)");
return "";
}
public static int method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
return 1;
}
public static void main(String[] args) {
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
}

執(zhí)行結(jié)果:

invoke method(List<String> list)
invoke method(List<Integer> list)

代碼清單10-9與代碼清單10-10的差別辙谜,是兩個(gè)method()方法添加了不同的返回值俺榆,由于這兩個(gè)返回值的加入,方法重載居然成功了装哆,即這段代碼可以被編譯和執(zhí)行了(java8編譯已經(jīng)不通過了)罐脊。這是我們對(duì)Java語言中返回值不參與重載選擇的基本認(rèn)知的挑戰(zhàn)嗎?

由于Java泛型的引入蜕琴,各種場(chǎng)景(虛擬機(jī)解析爹殊、反射等)下的方法調(diào)用都可能對(duì)原有的基礎(chǔ)產(chǎn)生影響并帶來新的需求,如在泛型類中如何獲取傳入的參數(shù)化類型等奸绷。所以JCP組織對(duì)《Java虛擬機(jī)規(guī)范》做出了相應(yīng)的修改梗夸,引入了諸如Signature、LocalVariableTypeTable等新的屬性用于解決伴隨泛型而來的參數(shù)類型的識(shí)別問題号醉,Signature是其中最重要的一項(xiàng)屬性反症,它的作用就是存儲(chǔ)一個(gè)方法在字節(jié)碼層面的特征簽名,這個(gè)屬性中保存的參數(shù)類型并不是原生類型畔派,而是包括了參數(shù)化類型的信息铅碍。修改后的虛擬機(jī)規(guī)范要求所有能識(shí)別49.0以上版本的Class文件的虛擬機(jī)都要能正確地識(shí)別Signature參數(shù)。

從Signature屬性的出現(xiàn)我們還可以得出結(jié)論线椰,擦除法所謂的擦除胞谈,僅僅是對(duì)方法的Code屬性中的字節(jié)碼進(jìn)行擦除,實(shí)際上元數(shù)據(jù)中還是保留了泛型信息憨愉,這也是我們?cè)诰幋a時(shí)能通過反射手段取得參數(shù)化類型的根本依據(jù)烦绳。

自動(dòng)裝箱、拆箱與遍歷循環(huán)

就純技術(shù)的角度而論配紫,自動(dòng)裝箱径密、自動(dòng)拆箱與遍歷循環(huán)(for-each循環(huán))這些語法糖,無論是實(shí)現(xiàn)復(fù)雜度上還是其中蘊(yùn)含的思想上都不能和泛型相提并論躺孝,兩者涉及的難度和深度都有很大差距享扔。專門拿來講解它們只是因?yàn)檫@些是Java語言里面被使用最多的語法糖。我們通過代碼清單10-11和代碼清單10-12中所示的代碼來看看這些語法糖在編譯后會(huì)發(fā)生什么樣的變化植袍。

代碼清單10-11 自動(dòng)裝箱惧眠、拆箱與遍歷循環(huán)

public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}

代碼清單10-12 自動(dòng)裝箱、拆箱與遍歷循環(huán)編譯之后

public static void main(String[] args) {
List list = Arrays.asList( new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4) });
int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
int i = ((Integer)localIterator.next()).intValue();
sum += i;
}
System.out.println(sum);
}

代碼清單10-11中一共包含了泛型于个、自動(dòng)裝箱氛魁、自動(dòng)拆箱、遍歷循環(huán)與變長(zhǎng)參數(shù)5種語法糖,代碼清單10-12則展示了它們?cè)诰幾g前后發(fā)生的變化呆盖。泛型就不必說了,自動(dòng)裝箱贷笛、拆箱在編譯之后被轉(zhuǎn)化成了對(duì)應(yīng)的包裝和還原方法应又,如本例中的Integer.valueOf()與Integer.intValue()方法,而遍歷循環(huán)則是把代碼還原成了迭代器的實(shí)現(xiàn)乏苦,這也是為何遍歷循環(huán)需要被遍歷的類實(shí)現(xiàn)Iterable接口的原因株扛。最后再看看變長(zhǎng)參數(shù),它在調(diào)用的時(shí)候變成了一個(gè)數(shù)組類型的參數(shù)汇荐,在變長(zhǎng)參數(shù)出現(xiàn)之前洞就,程序員的確也就是使用數(shù)組來完成類似功能的。

條件編譯

許多程序設(shè)計(jì)語言都提供了條件編譯的途徑掀淘,如C旬蟋、C++中使用預(yù)處理器指示符(#ifdef)來完成條件編譯。C革娄、C++的預(yù)處理器最初的任務(wù)是解決編譯時(shí)的代碼依賴關(guān)系(如極為常用的#include預(yù)處理命令)倾贰,而在Java語言之中并沒有使用預(yù)處理器,因?yàn)镴ava語言天然的編譯方式(編譯器并非一個(gè)個(gè)地編譯Java文件拦惋,而是將所有編譯單元的語法樹頂級(jí)節(jié)點(diǎn)輸入到待處理列表后再進(jìn)行編譯匆浙,因此各個(gè)文件之間能夠互相提供符號(hào)信息)就無須使用到預(yù)處理器。那Java語言是否有辦法實(shí)現(xiàn)條件編譯呢厕妖?
Java語言當(dāng)然也可以進(jìn)行條件編譯首尼,方法就是使用條件為常量的if語句。如代碼清單10-14所示言秸,該代碼中的if語句不同于其他Java代碼软能,它在編譯階段就會(huì)被“運(yùn)行”,生成的字節(jié)碼之中只包括“System.out.println("block 1")举畸;”一條語句埋嵌,并不會(huì)包含if語句及另外一個(gè)分子中的“System.out.println("block 2");”

代碼清單10-14 Java語言的條件編譯

public static void main(String[] args) {
if (true) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}

該代碼編譯后Class文件的反編譯結(jié)果:

public static void main(String[] args) {
System.out.println("block 1");
}

Java語言中條件編譯的實(shí)現(xiàn)俱恶,也是Java語言的一顆語法糖雹嗦,根據(jù)布爾常量值的真假,編譯器將會(huì)把分支中不成立的代碼塊消除掉合是,這一工作將在編譯器解除語法糖階段(com.sun.tools.javac.comp.Lower
類中)完成了罪。

除了上面介紹的泛型、自動(dòng)裝箱聪全、自動(dòng)拆箱泊藕、遍歷循環(huán)、變長(zhǎng)參數(shù)和條件編譯之外难礼,Java語言還有不少其他的語法糖娃圆,如內(nèi)部類玫锋、枚舉類、斷言語句讼呢、數(shù)值字面量撩鹿、對(duì)枚舉和字符串的switch支持、try語句中定義和關(guān)閉資源(這3個(gè)從JDK 7開始支持)悦屏、Lambda表達(dá)式(從JDK 8開始支持节沦,Lambda不能算是單純的語法糖,但在前端編譯器中做了大量的轉(zhuǎn)換工作)础爬,等等甫贯,大家可以通過跟蹤Javac源碼、反編譯Class文件等方式了解它們的本質(zhì)實(shí)現(xiàn)看蚜。

實(shí)戰(zhàn):插入式注解處理器

通過閱讀Javac編譯器的源碼叫搁,我們知道前端編譯器在把Java程序源碼編譯為字節(jié)碼的時(shí)候,會(huì)對(duì)Java程序源碼做各方面的檢查校驗(yàn)供炎。這些校驗(yàn)主要是以程序“寫得對(duì)不對(duì)”為出發(fā)點(diǎn)常熙,雖然也會(huì)產(chǎn)生一些警告和提示類的信息,但總體來講還是較少去校驗(yàn)程序“寫得好不好”碱茁。有鑒于此裸卫,業(yè)界出現(xiàn)了許多針對(duì)程序“寫得好不好”的輔助校驗(yàn)工具,如CheckStyle纽竣、FindBug墓贿、Klocwork等。這些代碼校驗(yàn)工具有一些是基于Java的源碼進(jìn)行校驗(yàn)蜓氨,有一些是通過掃描字節(jié)碼來完成聋袋,在本節(jié)的實(shí)戰(zhàn)中,我們將會(huì)使用注解處理器API來編寫一款擁有自己編碼風(fēng)格的校驗(yàn)工具:NameCheckProcessor穴吹。

當(dāng)然幽勒,由于我們的實(shí)戰(zhàn)都是為了學(xué)習(xí)和演示技術(shù)原理,而且篇幅所限港令,不可能做出一款能媲美CheckStyle等工具的產(chǎn)品來啥容,所以NameCheckProcessor的目標(biāo)也僅定為對(duì)Java程序命名進(jìn)行檢查。根據(jù)
《Java語言規(guī)范》中6.8節(jié)的要求顷霹,Java程序命名推薦(而不是強(qiáng)制)應(yīng)當(dāng)符合下列格式的書寫規(guī)范咪惠。

  • ·類(或接口):符合駝式命名法,首字母大寫淋淀。
  • 方法:符合駝式命名法遥昧,首字母小寫。
  • 類或?qū)嵗兞俊7像勈矫ㄌ砍簦鬃帜感?/li>
  • 常量永脓。要求全部由大寫字母或下劃線構(gòu)成,并且第一個(gè)字符不能是下劃線鞋仍。

上文提到的駝式命名法(Camel Case Name)常摧,正如它的名稱所表示的那樣,是指混合使用大小寫字母來分割構(gòu)成變量或函數(shù)的名字凿试,猶如駝峰一般排宰,這是當(dāng)前Java語言中主流的命名規(guī)范似芝,我們的實(shí)戰(zhàn)目標(biāo)就是為Javac編譯器添加一個(gè)額外的功能那婉,在編譯程序時(shí)檢查程序名是否符合上述對(duì)類(或接口)、方法党瓮、字段的命名要求详炬。

代碼實(shí)現(xiàn)

要通過注解處理器API實(shí)現(xiàn)一個(gè)編譯器插件,首先需要了解這組API的一些基本知識(shí)寞奸。我們實(shí)現(xiàn)注解處理器的代碼需要繼承抽象類javax.annotation.processing.AbstractProcessor呛谜,這個(gè)抽象類中只有一個(gè)
子類必須實(shí)現(xiàn)的抽象方法:“process()”,它是Javac編譯器在執(zhí)行注解處理器代碼時(shí)要調(diào)用的過程枪萄,我們可以從這個(gè)方法的第一個(gè)參數(shù)“annotations”中獲取到此注解處理器所要處理的注解集合隐岛,從第二個(gè)
參數(shù)“roundEnv”中訪問到當(dāng)前這個(gè)輪次(Round)中的抽象語法樹節(jié)點(diǎn),每個(gè)語法樹節(jié)點(diǎn)在這里都表示為一個(gè)Element瓷翻。在javax.lang.model.ElementKind中定義了18類Element聚凹,已經(jīng)包括了Java代碼中可能出現(xiàn)的全部元素,如:“包(PACKAGE)齐帚、枚舉(ENUM)妒牙、類(CLASS)、注解(ANNOTATION_TYPE)对妄、接口(INTERFACE)湘今、枚舉值(ENUM_CONSTANT)瀑焦、字段(FIELD)睬辐、參數(shù)(PARAMETER)、本地變量(LOCAL_VARIABLE)纹份、異常
(EXCEPTION_PARAMETER)孝常、方法(METHOD)愉豺、構(gòu)造函數(shù)(CONSTRUCTOR)、靜態(tài)語句塊(STATIC_INIT茫因,即static{}塊)蚪拦、實(shí)例語句塊(INSTANCE_INIT,即{}塊)、參數(shù)化類型(TYPE_PARAMETER驰贷,泛型尖括號(hào)內(nèi)的類型)盛嘿、資源變量(RESOURCE_VARIABLE,try-resource中定義的變量)括袒、模塊(MODULE)和未定義的其他語法樹節(jié)點(diǎn)(OTHER)”次兆。除了process()方法的傳入?yún)?shù)之外,還有一個(gè)很重要的實(shí)例變量“processingEnv”锹锰,它是AbstractProcessor中的一個(gè)protected變量芥炭,在注解處理器初始化的時(shí)候(init()方法執(zhí)行的時(shí)候)創(chuàng)建,繼承了AbstractProcessor的注解處理
器代碼可以直接訪問它恃慧。它代表了注解處理器框架提供的一個(gè)上下文環(huán)境园蝠,要?jiǎng)?chuàng)建新的代碼、向編譯器輸出信息痢士、獲取其他工具類等都需要用到這個(gè)實(shí)例變量彪薛。

注解處理器除了process()方法及其參數(shù)之外,還有兩個(gè)經(jīng)常配合著使用的注解怠蹂,分別是:@SupportedAnnotationTypes和@SupportedSourceVersion善延,前者代表了這個(gè)注解處理器對(duì)哪些注解感興趣,可以使用星號(hào)“*”作為通配符代表對(duì)所有的注解都感興趣城侧,后者指出這個(gè)注解處理器可以處理哪些版本的Java代碼易遣。

每一個(gè)注解處理器在運(yùn)行時(shí)都是單例的,如果不需要改變或添加抽象語法樹中的內(nèi)容嫌佑,process()方法就可以返回一個(gè)值為false的布爾值豆茫,通知編譯器這個(gè)輪次中的代碼未發(fā)生變化,無須構(gòu)造新的JavaCompiler實(shí)例歧强,在這次實(shí)戰(zhàn)的注解處理器中只對(duì)程序命名進(jìn)行檢查澜薄,不需要改變語法樹的內(nèi)容,因此process()方法的返回值一律都是false摊册。

代碼清單10-16 注解處理器NameCheckProcessor

package jvm.annotation;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

// 可以用"*"表示支持所有Annotations
@SupportedAnnotationTypes("*")
// 只支持JDK 1.8的Java代碼
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class NameCheckProcessor extends AbstractProcessor {

    private NameChecker nameChecker;

    /**
     * 初始化名稱檢查插件
     */
    @Override
    public void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        nameChecker = new NameChecker(processingEnv);
    }

    /**
     * 對(duì)輸入的語法樹的各個(gè)節(jié)點(diǎn)進(jìn)行進(jìn)行名稱檢查
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (!roundEnv.processingOver()) {
            for (Element element : roundEnv.getRootElements())
                nameChecker.checkNames(element);
        }
        return false;
    }
}

從代碼清單10-16中可以看到NameCheckProcessor能處理基于JDK 8的源碼肤京,它不限于特定的注解,對(duì)任何代碼都“感興趣”茅特,而在process()方法中是把當(dāng)前輪次中的每一個(gè)RootElement傳遞到一個(gè)名為NameChecker的檢查器中執(zhí)行名稱檢查邏輯忘分,NameChecker的代碼如代碼清單10-17所示。

代碼清單10-17 命名檢查器NameChecker

package jvm.annotation;

import java.util.EnumSet;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementScanner8;

import static javax.lang.model.element.ElementKind.ENUM_CONSTANT;
import static javax.lang.model.element.ElementKind.FIELD;
import static javax.lang.model.element.ElementKind.INTERFACE;
import static javax.lang.model.element.ElementKind.METHOD;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
import static javax.tools.Diagnostic.Kind.WARNING;

/**
 * 程序名稱規(guī)范的編譯器插件:<br>
 * 如果程序命名不合規(guī)范白修,將會(huì)輸出一個(gè)編譯器的WARNING信息
 */
public class NameChecker {

    private final Messager messager;

    NameCheckScanner nameCheckScanner = new NameCheckScanner();

    NameChecker(ProcessingEnvironment processsingEnv) {
        this.messager = processsingEnv.getMessager();
    }

    /**
     * 對(duì)Java程序命名進(jìn)行檢查妒峦,根據(jù)《Java語言規(guī)范》第三版第6.8節(jié)的要求,Java程序命名應(yīng)當(dāng)符合下列格式:
     *
     * <ul>
     * <li>類或接口:符合駝式命名法兵睛,首字母大寫肯骇。
     * <li>方法:符合駝式命名法窥浪,首字母小寫。
     * <li>字段:
     * <ul>
     * <li>類笛丙、實(shí)例變量: 符合駝式命名法漾脂,首字母小寫。
     * <li>常量: 要求全部大寫胚鸯。
     * </ul>
     * </ul>
     */
    public void checkNames(Element element) {
        nameCheckScanner.scan(element);
    }

    /**
     * 名稱檢查器實(shí)現(xiàn)類骨稿,繼承了JDK 1.6中新提供的ElementScanner6<br>
     * 將會(huì)以Visitor模式訪問抽象語法樹中的元素
     */
    private class NameCheckScanner extends ElementScanner8<Void, Void> {

        /**
         * 此方法用于檢查Java類
         */
        @Override
        public Void visitType(TypeElement e, Void p) {
            scan(e.getTypeParameters(), p);
            checkCamelCase(e, true);
            super.visitType(e, p);
            return null;
        }

        /**
         * 檢查方法命名是否合法
         */
        @Override
        public Void visitExecutable(ExecutableElement e, Void p) {
            if (e.getKind() == METHOD) {
                Name name = e.getSimpleName();
                if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
                    messager.printMessage(WARNING, "一個(gè)普通方法 “" + name + "”不應(yīng)當(dāng)與類名重復(fù),避免與構(gòu)造函數(shù)產(chǎn)生混淆", e);
                checkCamelCase(e, false);
            }
            super.visitExecutable(e, p);
            return null;
        }

        /**
         * 檢查變量命名是否合法
         */
        @Override
        public Void visitVariable(VariableElement e, Void p) {
            // 如果這個(gè)Variable是枚舉或常量姜钳,則按大寫命名檢查坦冠,否則按照駝式命名法規(guī)則檢查
            if (e.getKind() == ENUM_CONSTANT || e.getConstantValue() != null
                    || heuristicallyConstant(e))
                checkAllCaps(e);
            else checkCamelCase(e, false);
            return null;
        }

        /**
         * 判斷一個(gè)變量是否是常量
         */
        private boolean heuristicallyConstant(VariableElement e) {
            if (e.getEnclosingElement().getKind() == INTERFACE) return true;
            else if (e.getKind() == FIELD
                    && e.getModifiers().containsAll(EnumSet.of(PUBLIC, STATIC, FINAL)))
                return true;
            else {
                return false;
            }
        }

        /**
         * 檢查傳入的Element是否符合駝式命名法,如果不符合哥桥,則輸出警告信息
         */
        private void checkCamelCase(Element e, boolean initialCaps) {
            String name = e.getSimpleName().toString();
            boolean previousUpper = false;
            boolean conventional = true;
            int firstCodePoint = name.codePointAt(0);

            if (Character.isUpperCase(firstCodePoint)) {
                previousUpper = true;
                if (!initialCaps) {
                    messager.printMessage(WARNING, "名稱“" + name + "”應(yīng)當(dāng)以小寫字母開頭", e);
                    return;
                }
            } else if (Character.isLowerCase(firstCodePoint)) {
                if (initialCaps) {
                    messager.printMessage(WARNING, "名稱“" + name + "”應(yīng)當(dāng)以大寫字母開頭", e);
                    return;
                }
            } else conventional = false;

            if (conventional) {
                int cp = firstCodePoint;
                for (int i = Character.charCount(cp); i < name.length(); i += Character
                        .charCount(cp)) {
                    cp = name.codePointAt(i);
                    if (Character.isUpperCase(cp)) {
                        if (previousUpper) {
                            conventional = false;
                            break;
                        }
                        previousUpper = true;
                    } else previousUpper = false;
                }
            }

            if (!conventional)
                messager.printMessage(WARNING, "名稱“" + name + "”應(yīng)當(dāng)符合駝式命名法(Camel Case Names)", e);
        }

        /**
         * 大寫命名檢查辙浑,要求第一個(gè)字母必須是大寫的英文字母,其余部分可以是下劃線或大寫字母
         */
        private void checkAllCaps(Element e) {
            String name = e.getSimpleName().toString();

            boolean conventional = true;
            int firstCodePoint = name.codePointAt(0);

            if (!Character.isUpperCase(firstCodePoint)) conventional = false;
            else {
                boolean previousUnderscore = false;
                int cp = firstCodePoint;
                for (int i = Character.charCount(cp); i < name.length(); i += Character
                        .charCount(cp)) {
                    cp = name.codePointAt(i);
                    if (cp == (int) '_') {
                        if (previousUnderscore) {
                            conventional = false;
                            break;
                        }
                        previousUnderscore = true;
                    } else {
                        previousUnderscore = false;
                        if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
                            conventional = false;
                            break;
                        }
                    }
                }
            }

            if (!conventional)
                messager.printMessage(WARNING, "常量“" + name + "”應(yīng)當(dāng)全部以大寫字母或下劃線命名泰讽,并且以字母開頭", e);
        }
    }
}

NameChecker的代碼看起來有點(diǎn)長(zhǎng)例衍,但實(shí)際上注釋占了很大一部分昔期,而且即使算上注釋也不到190行已卸。它通過一個(gè)繼承于javax.lang.model.util.ElementScanner8的NameCheckScanner類,以Visitor模式來完成對(duì)語法樹的遍歷硼一,分別執(zhí)行visitType()累澡、visitVariable()和visitExecutable()方法來訪問類、字段和方法般贼,這3個(gè)visit*()方法對(duì)各自的命名規(guī)則做相應(yīng)的檢查愧哟,checkCamelCase()與checkAllCaps()方法則用于實(shí)現(xiàn)駝式命名法和全大寫命名規(guī)則的檢查。

整個(gè)注解處理器只需NameCheckProcessor和NameChecker兩個(gè)類就可以全部完成哼蛆,為了驗(yàn)證我們的實(shí)戰(zhàn)成果蕊梧,代碼清單10-18中提供了一段命名規(guī)范的“反面教材”代碼,其中的每一個(gè)類腮介、方法及字段的命名都存在問題肥矢,但是使用普通的Javac編譯這段代碼時(shí)不會(huì)提示任意一條警告信息。

代碼清單10-18 包含了多處不規(guī)范命名的代碼樣例

package jvm.annotation;

public class BADLY_NAMED_CODE {

    enum colors {
        red, blue, green;
    }

    static final int _FORTY_TWO = 66;

    public static int NOT_A_CONSTANT = _FORTY_TWO;

    protected void BADLY_NAMED_CODE() {
        return;
    }

    public void NOTcamelCASEmethodNAME() {
        return;
    }
}

運(yùn)行與測(cè)試
我們可以通過Javac命令的“-processor”參數(shù)來執(zhí)行編譯時(shí)需要附帶的注解處理器叠洗,在相應(yīng)的工程下src/java/mian目錄下執(zhí)行以下命令編譯

javac jvm/annotation/BADLY_NAMED_CODE.java
javac -processor jvm.annotation.NameCheckProcessor jvm/annotation/BADLY_NAMED_CODE.java

警告: 來自注釋處理程序 'jvm.annotation.NameCheckProcessor' 的受支持 source 版本 'RELEASE_8' 低于 -source '11'
jvm/annotation/BADLY_NAMED_CODE.java:3: 警告: 名稱“BADLY_NAMED_CODE”應(yīng)當(dāng)符合駝式命名法(Camel Case Names)
public class BADLY_NAMED_CODE {
       ^
jvm/annotation/BADLY_NAMED_CODE.java:5: 警告: 名稱“colors”應(yīng)當(dāng)以大寫字母開頭
    enum colors {
    ^
jvm/annotation/BADLY_NAMED_CODE.java:6: 警告: 常量“red”應(yīng)當(dāng)全部以大寫字母或下劃線命名甘改,并且以字母開頭
        red, blue, green;
        ^
jvm/annotation/BADLY_NAMED_CODE.java:6: 警告: 常量“blue”應(yīng)當(dāng)全部以大寫字母或下劃線命名,并且以字母開頭
        red, blue, green;
             ^
jvm/annotation/BADLY_NAMED_CODE.java:6: 警告: 常量“green”應(yīng)當(dāng)全部以大寫字母或下劃線命名灭抑,并且以字母開頭
        red, blue, green;
                   ^
jvm/annotation/BADLY_NAMED_CODE.java:9: 警告: 常量“_FORTY_TWO”應(yīng)當(dāng)全部以大寫字母或下劃線命名十艾,并且以字母開頭
    static final int _FORTY_TWO = 66;
                     ^
jvm/annotation/BADLY_NAMED_CODE.java:11: 警告: 名稱“NOT_A_CONSTANT”應(yīng)當(dāng)以小寫字母開頭
    public static int NOT_A_CONSTANT = _FORTY_TWO;
                      ^
jvm/annotation/BADLY_NAMED_CODE.java:13: 警告: 一個(gè)普通方法 “BADLY_NAMED_CODE”不應(yīng)當(dāng)與類名重復(fù),避免與構(gòu)造函數(shù)產(chǎn)生混淆
    protected void BADLY_NAMED_CODE() {
                   ^
jvm/annotation/BADLY_NAMED_CODE.java:13: 警告: 名稱“BADLY_NAMED_CODE”應(yīng)當(dāng)以小寫字母開頭
    protected void BADLY_NAMED_CODE() {
                   ^
jvm/annotation/BADLY_NAMED_CODE.java:17: 警告: 名稱“NOTcamelCASEmethodNAME”應(yīng)當(dāng)以小寫字母開頭
    public void NOTcamelCASEmethodNAME() {
                ^
11 個(gè)警告

NameCheckProcessor的實(shí)戰(zhàn)例子只演示了JSR-269嵌入式注解處理API其中的一部分功能腾节,基于這組API支持的比較有名的項(xiàng)目還有用于校驗(yàn)Hibernate標(biāo)簽使用正確性的Hibernate Validator Annotation
Processor(本質(zhì)上與NameCheckProcessor所做的事情差不多)忘嫉、自動(dòng)為字段生成getter和setter方法等輔助內(nèi)容的Lombok(根據(jù)已有元素生成新的語法樹元素)等.

小結(jié)

我們從Javac編譯器源碼實(shí)現(xiàn)的層次上學(xué)習(xí)了Java源代碼編譯為字節(jié)碼的過程荤牍,分析了Java語言中泛型、主動(dòng)裝箱拆箱庆冕、條件編譯等多種語法糖的前因后果参淫,并實(shí)戰(zhàn)練習(xí)了如何使用插入式注解處理器來完成一個(gè)檢查程序命名規(guī)范的編譯器插件。在前端編譯器中愧杯,“優(yōu)化”手段主要用于提升程序的編碼效率涎才,之所以把Javac這類將Java代碼轉(zhuǎn)變?yōu)樽止?jié)碼的編譯器稱作“前端編譯器”,是因?yàn)樗煌瓿闪藦某绦虻匠橄笳Z法樹或中間字節(jié)碼的生成力九,而在此之后耍铜,還有一組內(nèi)置于Java虛擬機(jī)內(nèi)部的“后端編譯器”來完成代碼優(yōu)化以及從字節(jié)碼生成本地機(jī)器碼的過程,即前面多次提到的即時(shí)編譯器或提前編譯器跌前,這個(gè)后端編譯器的編譯速度及編譯結(jié)果質(zhì)量高低棕兼,是衡量Java虛擬機(jī)性能最重要的一個(gè)指標(biāo)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抵乓,一起剝皮案震驚了整個(gè)濱河市伴挚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌灾炭,老刑警劉巖茎芋,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蜈出,居然都是意外死亡田弥,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門铡原,熙熙樓的掌柜王于貴愁眉苦臉地迎上來偷厦,“玉大人,你說我怎么就攤上這事燕刻≈黄茫” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵卵洗,是天一觀的道長(zhǎng)请唱。 經(jīng)常有香客問我,道長(zhǎng)忌怎,這世上最難降的妖魔是什么籍滴? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮榴啸,結(jié)果婚禮上孽惰,老公的妹妹穿的比我還像新娘。我一直安慰自己鸥印,他們只是感情好勋功,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布坦报。 她就那樣靜靜地躺著,像睡著了一般狂鞋。 火紅的嫁衣襯著肌膚如雪片择。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天骚揍,我揣著相機(jī)與錄音字管,去河邊找鬼。 笑死信不,一個(gè)胖子當(dāng)著我的面吹牛嘲叔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播抽活,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼硫戈,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了下硕?” 一聲冷哼從身側(cè)響起丁逝,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梭姓,沒想到半個(gè)月后霜幼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡糊昙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年辛掠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谢谦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片释牺。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖回挽,靈堂內(nèi)的尸體忽然破棺而出没咙,到底是詐尸還是另有隱情,我是刑警寧澤千劈,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布祭刚,位于F島的核電站,受9級(jí)特大地震影響墙牌,放射性物質(zhì)發(fā)生泄漏涡驮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一喜滨、第九天 我趴在偏房一處隱蔽的房頂上張望捉捅。 院中可真熱鬧,春花似錦虽风、人聲如沸棒口。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽无牵。三九已至漾肮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間茎毁,已是汗流浹背克懊。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留七蜘,地道東北人保檐。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像崔梗,于是被迫代替她去往敵國(guó)和親夜只。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354