javac final變量未賦值檢測案例講解
initialized. 是在Flow階段由AssignAnalyzer檢測出來的,需要的朋友可以參考下
前言
我們在前面介紹AssignAnalyzer時,對AssignAnalyzer.letInit(DiagnosticPosition, VarSymbol)方法進行了簡單的介紹.本文就舉一個案例,來深入理解一下.
案例
案例代碼如下:
public class CheckInitError { static final int b; public CheckInitError(){ } }
本代碼在IDE環(huán)境(如Eclipse)中報如下錯誤:The blank final field b may not have been
initialized.
那么它是如何檢測出錯誤的,本文就來揭秘.(Eclipse中內(nèi)置了Java編譯器)
解析
還是在javac中的Flow階段,最終來到了AbstractAssignAnalyzer.analyzeTree(Env<?>, JCTree)方法,在該方法中,又調(diào)用了Flow.BaseAnalyzer.scan(JCTree)方法,進而調(diào)用AbstractAssignAnalyzer.visitClassDef(JCClassDecl)方法,同時,由于字段b是可追蹤的,因此會在處理靜態(tài)字段時調(diào)用AbstractAssignAnalyzer.newVar(JCVariableDecl)方法,將b所對應的符號保存在vardecls中,下標位置為0(下標為0的原因是它是第一個變量).這部分的內(nèi)容是在上篇文章中有所介紹的,本文不再展開.
在AbstractAssignAnalyzer#visitClassDef方法中,處理完靜態(tài)字段,靜態(tài)初始塊,實例字段,實例初始塊之后,就會處理方法(包括構造器).
那么由于在CheckInitError中只定義了一個構造器,因此如下代碼只會處理一次:
for (List<JCTree> l = tree.defs; l.nonEmpty(); l = l.tail) { if (l.head.hasTag(METHODDEF)) { scan(l.head); } }
由于CheckInitError構造器所對應的樹節(jié)點是JCMethodDecl,因此最終會調(diào)用AbstractAssignAnalyzer.visitMethodDef(JCMethodDecl).
在AbstractAssignAnalyzer#visitMethodDef中,一開始,是進行判斷:
// 如果該方法的語句體為null,則意味著該方法是一個抽象方法,不處理. if (tree.body == null) { return; } // 忽略處理合成方法,但是合成的lambda方法還是處理的 if ((tree.sym.flags() & (SYNTHETIC | LAMBDA_METHOD)) == SYNTHETIC) { return; }
然后是保存現(xiàn)場:
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());
接下來,判斷當前方法是一個構造函數(shù),其構造函數(shù)中的第一個語句不是this(…)的語句嗎?
boolean isInitialConstructor = TreeInfo.isInitialConstructor(tree);
思考: 為何會在這里做這樣的判斷? AssignAnalyzer的定位是檢查final變量是否有多次賦值,那么假設我們在一個類中final 字段(未初始化的),那么不管你有多少個構造函數(shù),那么就應該在一個最終調(diào)用的構造器中對這個變量進行初始化.舉例:
public class CheckInitError { final int b; public CheckInitError(){ //this(3); // 第1種 b = 3; // 第2種 // 如果第1行,第2行注釋掉,則報錯,因為b沒有最終初始化 } public CheckInitError(int a ){ b = a; } }
那么對于CheckInitError來說, CheckInitError方法構造函數(shù)中的第一個語句不是this(…),而是super();因此isInitialConstructor為true.為啥呢? javac會在構造器的第一行插入super(),至于是在什么條件下插入,如何插入,我們后續(xù)介紹,本文不表.
由于isInitialConstructor等于true,因此,如下代碼是不會執(zhí)行的:
if (!isInitialConstructor) { firstadr = nextadr; }
接下來,是處理方法的參數(shù),那么由于在案例中是沒有參數(shù)的,因此如下代碼是不會執(zhí)行的:
for (List<JCVariableDecl> l = tree.params; l.nonEmpty(); l = l.tail) { JCVariableDecl def = l.head; scan(def); // 參數(shù)應該有PARAMETER的修飾符,否則就是一個錯誤 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)用方傳遞的,當方法執(zhí)行時,形參是肯定有值的(初始化的),否則就是一個錯誤
接下來處理方法體,由于javac默認添加了一個super()語句,那么就會進行實質的處理(副作用).但是這部分與本文關聯(lián)不大,本文就不展開了.
方法體執(zhí)行完之后,如果isInitialConstructor為true,則判斷構造器是否將類中的變量(final變量但是沒有初始賦值的)都初始化了.如下:
if (isInitialConstructor) { boolean isSynthesized = (tree.sym.flags() & GENERATEDCONSTR) != 0; // 判斷該構造器是否是合成的 // 這里判斷的是構造器是否將類中的變量(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); } } } }
那么對于當前,由于該構造器不是合成的,因此isSynthesized為false.同時,在該類中只定義了一個變量–> b,那么因此循環(huán)只會執(zhí)行一次(firstadr = 0,nextadr = 1,這部分的內(nèi)容在上篇文章有介紹)
在循環(huán)中,通過下標取得b對應的VarSymbol,調(diào)用AssignAnalyzer.checkInit(DiagnosticPosition, VarSymbol, String)方法進行判斷.如下:
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); } }
對于當前來說,符號是可跟蹤的,但是在inits(初始化變量位圖)中不存在對應的下標,因此會調(diào)用log#error方法,進行錯誤日志輸出.然后將其加入到inits(這樣做的目的,是為了一次編譯能獲得更多的錯誤信息)
對于當前,是報如下錯誤:
然后,是pendingExits 處理和恢復現(xiàn)場,這部分的內(nèi)容,我們后續(xù)文章會舉例子進行講解.
總結
通過本文,我們可以知道Eclipse中報如下錯誤:The blank final field b may not have been
initialized. 是在Flow階段由AssignAnalyzer檢測出來的.
到此這篇關于javac final變量未賦值檢測案例講解的文章就介紹到這了,更多相關javac final變量未賦值內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot實現(xiàn)多數(shù)據(jù)源的切換實踐
這篇主要介紹了SpringBoot實現(xiàn)多數(shù)據(jù)源的切換,本文基于AOP來實現(xiàn)數(shù)據(jù)源的切換,文中通過示例代碼介紹的非常詳細,感興趣的小伙伴們可以參考一下2022-03-03Spring+Vue整合UEditor富文本實現(xiàn)圖片附件上傳的方法
這篇文章主要介紹了Spring+Vue整合UEditor富文本實現(xiàn)圖片附件上傳的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07使用idea開發(fā)javaWeb應用程序的思路(實現(xiàn)用戶的增刪改查)
這篇文章主要介紹了使用idea開發(fā)javaWeb應用程序的思路(實現(xiàn)用戶的增刪改查),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01Java8中List轉Map(Collectors.toMap) 的技巧分享
在最近的工作開發(fā)之中,慢慢習慣了很多Java8中的Stream的用法,很方便而且也可以并行的去執(zhí)行這個流,這篇文章主要給大家介紹了關于Java8中List轉Map(Collectors.toMap) 的相關資料,需要的朋友可以參考下2021-07-07