javac final變量未賦值檢測(cè)案例講解
initialized. 是在Flow階段由AssignAnalyzer檢測(cè)出來的,需要的朋友可以參考下
前言
我們?cè)谇懊娼榻BAssignAnalyzer時(shí),對(duì)AssignAnalyzer.letInit(DiagnosticPosition, VarSymbol)方法進(jìn)行了簡單的介紹.本文就舉一個(gè)案例,來深入理解一下.
案例
案例代碼如下:
public class CheckInitError { static final int b; public CheckInitError(){ } }
本代碼在IDE環(huán)境(如Eclipse)中報(bào)如下錯(cuò)誤:The blank final field b may not have been
initialized.
那么它是如何檢測(cè)出錯(cuò)誤的,本文就來揭秘.(Eclipse中內(nèi)置了Java編譯器)
解析
還是在javac中的Flow階段,最終來到了AbstractAssignAnalyzer.analyzeTree(Env<?>, JCTree)方法,在該方法中,又調(diào)用了Flow.BaseAnalyzer.scan(JCTree)方法,進(jìn)而調(diào)用AbstractAssignAnalyzer.visitClassDef(JCClassDecl)方法,同時(shí),由于字段b是可追蹤的,因此會(huì)在處理靜態(tài)字段時(shí)調(diào)用AbstractAssignAnalyzer.newVar(JCVariableDecl)方法,將b所對(duì)應(yīng)的符號(hào)保存在vardecls中,下標(biāo)位置為0(下標(biāo)為0的原因是它是第一個(gè)變量).這部分的內(nèi)容是在上篇文章中有所介紹的,本文不再展開.
在AbstractAssignAnalyzer#visitClassDef方法中,處理完靜態(tài)字段,靜態(tài)初始?jí)K,實(shí)例字段,實(shí)例初始?jí)K之后,就會(huì)處理方法(包括構(gòu)造器).
那么由于在CheckInitError中只定義了一個(gè)構(gòu)造器,因此如下代碼只會(huì)處理一次:
for (List<JCTree> l = tree.defs; l.nonEmpty(); l = l.tail) { if (l.head.hasTag(METHODDEF)) { scan(l.head); } }
由于CheckInitError構(gòu)造器所對(duì)應(yīng)的樹節(jié)點(diǎn)是JCMethodDecl,因此最終會(huì)調(diào)用AbstractAssignAnalyzer.visitMethodDef(JCMethodDecl).
在AbstractAssignAnalyzer#visitMethodDef中,一開始,是進(jìn)行判斷:
// 如果該方法的語句體為null,則意味著該方法是一個(gè)抽象方法,不處理. if (tree.body == null) { return; } // 忽略處理合成方法,但是合成的lambda方法還是處理的 if ((tree.sym.flags() & (SYNTHETIC | LAMBDA_METHOD)) == SYNTHETIC) { return; }
然后是保存現(xiàn)場(chǎng):
final Bits initsPrev = new Bits(inits); final Bits uninitsPrev = new Bits(uninits); int nextadrPrev = nextadr; int firstadrPrev = firstadr; int returnadrPrev = returnadr; Assert.check(pendingExits.isEmpty());
接下來,判斷當(dāng)前方法是一個(gè)構(gòu)造函數(shù),其構(gòu)造函數(shù)中的第一個(gè)語句不是this(…)的語句嗎?
boolean isInitialConstructor = TreeInfo.isInitialConstructor(tree);
思考: 為何會(huì)在這里做這樣的判斷? AssignAnalyzer的定位是檢查final變量是否有多次賦值,那么假設(shè)我們?cè)谝粋€(gè)類中final 字段(未初始化的),那么不管你有多少個(gè)構(gòu)造函數(shù),那么就應(yīng)該在一個(gè)最終調(diào)用的構(gòu)造器中對(duì)這個(gè)變量進(jìn)行初始化.舉例:
public class CheckInitError { final int b; public CheckInitError(){ //this(3); // 第1種 b = 3; // 第2種 // 如果第1行,第2行注釋掉,則報(bào)錯(cuò),因?yàn)閎沒有最終初始化 } public CheckInitError(int a ){ b = a; } }
那么對(duì)于CheckInitError來說, CheckInitError方法構(gòu)造函數(shù)中的第一個(gè)語句不是this(…),而是super();因此isInitialConstructor為true.為啥呢? javac會(huì)在構(gòu)造器的第一行插入super(),至于是在什么條件下插入,如何插入,我們后續(xù)介紹,本文不表.
由于isInitialConstructor等于true,因此,如下代碼是不會(huì)執(zhí)行的:
if (!isInitialConstructor) { firstadr = nextadr; }
接下來,是處理方法的參數(shù),那么由于在案例中是沒有參數(shù)的,因此如下代碼是不會(huì)執(zhí)行的:
for (List<JCVariableDecl> l = tree.params; l.nonEmpty(); l = l.tail) { JCVariableDecl def = l.head; scan(def); // 參數(shù)應(yīng)該有PARAMETER的修飾符,否則就是一個(gè)錯(cuò)誤 Assert.check((def.sym.flags() & PARAMETER) != 0, "Method parameter without PARAMETER flag"); initParam(def); } protected void initParam(JCVariableDecl def) { inits.incl(def.sym.adr); uninits.excl(def.sym.adr); }
這段代碼的作用是依次處理參數(shù),然后將參數(shù)加入到變量已經(jīng)初始化的位圖中,至于為啥? 原因很簡單:參數(shù)是調(diào)用方傳遞的,當(dāng)方法執(zhí)行時(shí),形參是肯定有值的(初始化的),否則就是一個(gè)錯(cuò)誤
接下來處理方法體,由于javac默認(rèn)添加了一個(gè)super()語句,那么就會(huì)進(jìn)行實(shí)質(zhì)的處理(副作用).但是這部分與本文關(guān)聯(lián)不大,本文就不展開了.
方法體執(zhí)行完之后,如果isInitialConstructor為true,則判斷構(gòu)造器是否將類中的變量(final變量但是沒有初始賦值的)都初始化了.如下:
if (isInitialConstructor) { boolean isSynthesized = (tree.sym.flags() & GENERATEDCONSTR) != 0; // 判斷該構(gòu)造器是否是合成的 // 這里判斷的是構(gòu)造器是否將類中的變量(final變量但是沒有初始賦值的)都初始化了. for (int i = firstadr; i < nextadr; i++) { JCVariableDecl vardecl = vardecls[i]; VarSymbol var = vardecl.sym; if (var.owner == classDef.sym) { // choose the diagnostic position based on whether // the ctor is default(synthesized) or not if (isSynthesized) { checkInit(TreeInfo.diagnosticPositionFor(var, vardecl), var, "var.not.initialized.in.default.constructor"); } else { checkInit(TreeInfo.diagEndPos(tree.body), var); } } } }
那么對(duì)于當(dāng)前,由于該構(gòu)造器不是合成的,因此isSynthesized為false.同時(shí),在該類中只定義了一個(gè)變量–> b,那么因此循環(huán)只會(huì)執(zhí)行一次(firstadr = 0,nextadr = 1,這部分的內(nèi)容在上篇文章有介紹)
在循環(huán)中,通過下標(biāo)取得b對(duì)應(yīng)的VarSymbol,調(diào)用AssignAnalyzer.checkInit(DiagnosticPosition, VarSymbol, String)方法進(jìn)行判斷.如下:
void checkInit(DiagnosticPosition pos, VarSymbol sym, String errkey) { if ((sym.adr >= firstadr || sym.owner.kind != TYP) && trackable(sym) && !inits.isMember(sym.adr)) { log.error(pos, errkey, sym); inits.incl(sym.adr); } }
對(duì)于當(dāng)前來說,符號(hào)是可跟蹤的,但是在inits(初始化變量位圖)中不存在對(duì)應(yīng)的下標(biāo),因此會(huì)調(diào)用log#error方法,進(jìn)行錯(cuò)誤日志輸出.然后將其加入到inits(這樣做的目的,是為了一次編譯能獲得更多的錯(cuò)誤信息)
對(duì)于當(dāng)前,是報(bào)如下錯(cuò)誤:
然后,是pendingExits 處理和恢復(fù)現(xiàn)場(chǎng),這部分的內(nèi)容,我們后續(xù)文章會(huì)舉例子進(jìn)行講解.
總結(jié)
通過本文,我們可以知道Eclipse中報(bào)如下錯(cuò)誤:The blank final field b may not have been
initialized. 是在Flow階段由AssignAnalyzer檢測(cè)出來的.
到此這篇關(guān)于javac final變量未賦值檢測(cè)案例講解的文章就介紹到這了,更多相關(guān)javac final變量未賦值內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的切換實(shí)踐
這篇主要介紹了SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的切換,本文基于AOP來實(shí)現(xiàn)數(shù)據(jù)源的切換,文中通過示例代碼介紹的非常詳細(xì),感興趣的小伙伴們可以參考一下2022-03-03靈活控制任務(wù)執(zhí)行時(shí)間的Cron表達(dá)式范例
這篇文章主要為大家介紹了靈活控制任務(wù)執(zhí)行時(shí)間的Cron表達(dá)式范例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10Spring+Vue整合UEditor富文本實(shí)現(xiàn)圖片附件上傳的方法
這篇文章主要介紹了Spring+Vue整合UEditor富文本實(shí)現(xiàn)圖片附件上傳的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07使用idea開發(fā)javaWeb應(yīng)用程序的思路(實(shí)現(xiàn)用戶的增刪改查)
這篇文章主要介紹了使用idea開發(fā)javaWeb應(yīng)用程序的思路(實(shí)現(xiàn)用戶的增刪改查),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01Java8中List轉(zhuǎn)Map(Collectors.toMap) 的技巧分享
在最近的工作開發(fā)之中,慢慢習(xí)慣了很多Java8中的Stream的用法,很方便而且也可以并行的去執(zhí)行這個(gè)流,這篇文章主要給大家介紹了關(guān)于Java8中List轉(zhuǎn)Map(Collectors.toMap) 的相關(guān)資料,需要的朋友可以參考下2021-07-07