前言
在前几篇文章中我们介绍过了AbstractAssignAnalyzer.visitIf(JCIf),该方法是用来判断在if语句中变量的初始化情况的.例如如下代码:
public void assgin(){
int c;
int a = 0;
if(c != 0){
}
}
在IDE 中会提示 The local variable c may not have been initialized,那么它是如何检测的呢?
又如如下代码:
public void assgin(){
int c;
int a = 0;
if((c = 1) != c){
}
}
我们可以通过看代码的方式知道,变量a,c都已初始化了,变量值分别是0,1,那么AbstractAssignAnalyzer.visitIf方法是判断在if语句中变量的初始化情况的.那么该方法又是如何检测的呢?接下来我们就案例来进行讲解
解析
变量未初始化问题
本案例的测试代码如下:
public class IfAssign {
public void assgin1(){
int c;
int a = 0;
if(c != 0 ){
}
}
}
还是在javac中的Flow阶段,最终来到了AbstractAssignAnalyzer.analyzeTree(Env<?>, JCTree)方法,在该方法中,又调用了Flow.BaseAnalyzer.scan(JCTree)方法,此时由于IfAssign中没有静态变量,实例变量,静态初始化块,实例初始化块,只有两个方法(其中一个是编辑器自动插入的<init> 方法).因此,最终会执行如下代码:
for (List<JCTree> l = tree.defs; l.nonEmpty(); l = l.tail) {
if (l.head.hasTag(METHODDEF)) {
scan(l.head);
}
}
此时IfAssign所对应的代码如下:
public class IfAssign {
public IfAssign() {
super();
}
public void assgin1() {
int c;
int a = 0;
if (c != 0) {
}
}
}
至于为何<init>方法声明在assgin1方法之前,这点我们之后进行讲解,这个不是本文重点,故不进行讲解
因此,该循环第一遍处理的是<init>方法,这个不是本文重点,不进行讲解.
第二遍的时候,处理assgin1方法,由于方法对应的tree节点是JCMethodDecl.因此会最终调用AssignAnalyzer.visitMethodDef(JCMethodDecl)方法.
对于assgin1方法所对应的树如下:
那么在该方法的处理过程就是对这个树的先序遍历.
那么在AssignAnalyzer#visitMethodDef方法中,是进行判断,保存现场,参数处理,这部分的内容在
BaseAnalyzer,AbstractAssignAnalyzer,AssignAnalyzer解析 中介绍,本文就不在介绍了.
我们重点看对方法体的处理:
scan(tree.body);
由于JCMethodDecl的body对应的是JCBlock,因此会调用AbstractAssignAnalyzer.visitBlock(JCBlock).如下:
public void visitBlock(JCBlock tree) {
int nextadrPrev = nextadr;
scan(tree.stats);
nextadr = nextadrPrev;
}
在该方法中首先保存了nextadr,然后处理完JCBlock持有的stats后,再恢复nextadr.这是为啥呢?设想如下情况:
public void test(int b){
int a = 0;
if(a != b){
int c;
}else{
int d;
}
}
对于这个代码,c,d应该都是属于同一个本地变量表中的位置.那么在visitBlock方法中的保存,恢复的操作就是保证这个情况下变量位置的。
接下来,调用的是TreeScanner.scan(List<? extends JCTree>),代码如下:
public void scan(List<? extends JCTree> trees) {
if (trees != null)
for (List<? extends JCTree> l = trees; l.nonEmpty(); l = l.tail)
scan(l.head);
}
由于当前处理的是assgin1方法中的代码块,在该块中包含3条语句:
-
2条变量声明语句
-
if 语句
因此,这里的循环会执行3次.
-
第一次执行(int c)如下:
由于变量语句对应的节点为JCVariableDecl,因此,会执行AbstractAssignAnalyzer.visitVarDef(JCVariableDecl),如下:
public void visitVarDef(JCVariableDecl tree) { // 判断当前符号是否是可追踪的,关于判断标准,在上篇博客有介绍 boolean track = trackable(tree.sym); if (track && tree.sym.owner.kind == MTH) {// 如果当前变量是方法中的并且是可追踪的 newVar(tree); } if (tree.init != null) { scanExpr(tree.init); if (track) { letInit(tree.pos(), tree.sym); } } }
判断变量是否可追踪的标志为: 给定符号的位置>= startPos && (sym 属于方法 || (当前的符号是 final && 当前符号是classDef的成员))
由于当前变量是方法中声明的,因此该符号c是可追踪的,因此会执行cAbstractAssignAnalyzer.newVar(JCVariableDecl).如下:
void newVar(JCVariableDecl varDecl) { VarSymbol sym = varDecl.sym; vardecls = ArrayUtils.ensureCapacity(vardecls, nextadr);// 对vardecls 进行扩容 if ((sym.flags() & FINAL) == 0) {// 如果当前符号不是final的 sym.flags_field |= EFFECTIVELY_FINAL; // 那么就修改该变量的标识符为有效final } sym.adr = nextadr; vardecls[nextadr] = varDecl; exclVarFromInits(varDecl, nextadr); // 从初始化对应的bitmap中去除 uninits.incl(nextadr);// 加入到未初始化的bitmap中 nextadr++; }
这个方法,在之前的博客中有介绍,这里不在介绍,方法执行后,符号c的adr为0,符号c成为了有效final,加入到了uninits中, vardecls中保存了符号c(下标为0),nextadr 为1.
-
第二次执行(int a = 0)如下:
和int c同样,也会先执行AbstractAssignAnalyzer#newVar方法,此时符号a的adr为1,成为了有效final,加入到了uninits中, vardecls中保存了符号a(下标为1),nextadr 为2.
不同的是,由于符号有初始化部分(即0),因此会执行AbstractAssignAnalyzer.scanExpr(JCTree)方法,如下:void scanExpr(JCTree tree) { if (tree != null) { scan(tree); if (inits.isReset()) { // 如果在扫描指定树时, inits 发生了变化,则进行合并 merge(tree); } } }
对于当前,0对应的是JCLiteral,因此会执行TreeScanner#visitLiteral方法,不过此方法是空实现…
接下来,会执行AssignAnalyzer#letInit方法(AssignAnalyzer#visitVarDef中调用),代码如下:
void letInit(DiagnosticPosition pos, VarSymbol sym) { if (sym.adr >= firstadr && trackable(sym)) {// 如果该变量是可以追踪的 if ((sym.flags() & EFFECTIVELY_FINAL) != 0) {// 如果该变量是有效final的 if (!uninits.isMember(sym.adr)) {// 如果不是uninits的成员,则修改变量不是有效final的 //assignment targeting an effectively final variable //makes the variable lose its status of effectively final //if the variable is _not_ definitively unassigned sym.flags_field &= ~EFFECTIVELY_FINAL; } else { // 从uninits中去除 uninit(sym); } } else if ((sym.flags() & FINAL) != 0) {// 如果该变量是final的 if ((sym.flags() & PARAMETER) != 0) {// 是参数 if ((sym.flags() & UNION) != 0) { //multi-catch parameter log.error(pos, "multicatch.parameter.may.not.be.assigned", sym); } else { log.error(pos, "final.parameter.may.not.be.assigned", sym); } } else if (!uninits.isMember(sym.adr)) { log.error(pos, flowKind.errKey, sym); } else { uninit(sym); } } inits.incl(sym.adr); } else if ((sym.flags() & FINAL) != 0) { log.error(pos, "var.might.already.be.assigned", sym); } }
这个方法,同样在之前的博客有介绍,这里不再赘述.
方法执行后,uninits中去除了符号a,inits中加入了符号a.此时uninits为:10000000000000000000000000000000, inits为: 01000000000000000000000000000000
uninits 中的1代表的是符号c,inits中的1代表的是符号a
-
第三次执行(if语句)如下:
由于这个处理的是JCIf,因此会调用AbstractAssignAnalyzer.visitIf(JCIf),这里首先处理的是if语句中的cond部分(当前就是(c = 1) != c).因此会调用AbstractAssignAnalyzer.scanCond(JCTree)方法,如下:
void scanCond(JCTree tree) { ..... 省略无关代码 } else { /* * 这里分析的是if(a > 1){ int a } */ scan(tree); if (!inits.isReset()) split(tree.type != syms.unknownType); } if (tree.type != syms.unknownType) { // 重置inits, uninits resetBits(inits, uninits); } }
这个方法同样在之前的博客中有介绍,这里不在讲解.
由于((c = 1) != c) 是括号表达式,因此在scanCond方法中执行的是else部分,这里会最终调用TreeScanner.visitParens(JCParens),代码如下:
public void visitParens(JCParens tree) { scan(tree.expr); // 这里处理的是括号表达式中的内容即(c = 1) != c }
最终调用的是AbstractAssignAnalyzer.visitBinary(JCBinary),在该方法中,最终会执行如下代码:
void visitBinary(JCBinary tree){ .... 省略其他代码 scanExpr(tree.lhs); scanExpr(tree.rhs); }
因此这里会最先处理(c = 1),然后再处理 c.
因此,会首先调用TreeScanner#visitParens方法处理括号表达式–> c = 1,由于是赋值语句,因此最终执行的是AbstractAssignAnalyzer.visitAssign(JCAssign),代码如下:
public void visitAssign(JCAssign tree) { JCTree lhs = TreeInfo.skipParens(tree.lhs); if (!(lhs instanceof JCIdent)) { scanExpr(lhs); } scanExpr(tree.rhs); letInit(lhs); }
在该方法首先是去除=号左边的括号,这里最终获得的是c,由于c是标识符,因此不会调用scanExpr方,然后处理=号右边的1,由于是字面量,因此会调用TreeScanner#visitLiteral方法,不过此方法是空实现.然后执行AssignAnalyzer#letInit方法,执行之后,inits为11000000000000000000000000000000,uninits为00000000000000000000000000000000.意味着下标为0,1的符号(即 c ,a)都已初始化.
然后处理的是(c = 1) != c 中 != 号的右值c,由于这里是符号,因此会调用AbstractAssignAnalyzer.visitIdent(JCIdent),代码如下:
public void visitIdent(JCIdent tree) { if (tree.sym.kind == VAR) { checkInit(tree.pos(), (VarSymbol)tree.sym); referenced(tree.sym); } }
首先调用AbstractAssignAnalyzer.checkInit(DiagnosticPosition, VarSymbol, String)方法,检查变量已经初始化,如下:
void checkInit(DiagnosticPosition pos, VarSymbol sym) { checkInit(pos, sym, "var.might.not.have.been.initialized"); }
然后调用:
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中不包含该变量,则输出错误信息,这里的错误信息为 var.might.not.have.been.initialized 所对应的本地化信息,即 可能尚未初始化变量xxx.这也就说明了我们开篇所要讲解的问题
由于符号c 已经加入到inits中,因此是不会输出错误信息的.
然后调用AbstractAssignAnalyzer.referenced(Symbol),如下:
void referenced(Symbol sym) { unrefdResources.remove(sym); }
这也就意味着符号c 是被引用过的了.
这里视线回到AbstractAssignAnalyzer#scanCond,处理完((c = 1) != c) 之后,执行如下代码:
if (!inits.isReset()) split(tree.type != syms.unknownType);
由于inits的状态不是BitsState.UNKNOWN,因此会执行AbstractAssignAnalyzer.split(boolean),注意,由于((c = 1) != c)的type不是syms.unknownType,因此在split方法中的参数为true.代码如下:
void split(boolean setToNull) { initsWhenFalse.assign(inits); uninitsWhenFalse.assign(uninits); initsWhenTrue.assign(inits); uninitsWhenTrue.assign(uninits); if (setToNull) { resetBits(inits, uninits); } }
执行完之后:
initsWhenFalse = initsWhenTrue = 11000000000000000000000000000000
uninitsWhenFalse = uninitsWhenTrue = 00000000000000000000000000000000
inits = uninits = 00000000000000000000000000000000split方法执行完之后,执行如下代码:
if (tree.type != syms.unknownType) { // 重置inits, uninits resetBits(inits, uninits); }
执行完之后, inits = uninits = 00000000000000000000000000000000
视线回到AbstractAssignAnalyzer#visitIf方法中,处理完if语句conditon部分之后,执行如下代码:
final Bits initsBeforeElse = new Bits(initsWhenFalse); final Bits uninitsBeforeElse = new Bits(uninitsWhenFalse); assignToInits(tree.cond, initsWhenTrue); uninits.assign(uninitsWhenTrue);
initsBeforeElse, uninitsBeforeElse 对应的是if语句的else部分的,由于接下来处理if语句中的then,else(如果有的话)部分,那么就需要对这两种情况做初始化.
准备工作做完之后,处理then部分.如下:
scan(tree.thenpart);
同样,由于then对应的是JCBlock,因此会调用AbstractAssignAnalyzer.visitBlock(JCBlock),由于当前对应的是空block,因此是不会处理的.
接下来,回到AbstractAssignAnalyzer#visitIf方法,接下来会执行如下代码:
if (tree.elsepart != null) { final Bits initsAfterThen = new Bits(inits); final Bits uninitsAfterThen = new Bits(uninits); assignToInits(tree.thenpart, initsBeforeElse); uninits.assign(uninitsBeforeElse); scan(tree.elsepart); andSetInits(tree.elsepart, initsAfterThen); uninits.andSet(uninitsAfterThen); } else { // 最终inits 的集合是 在if 之前初始化和在condition部分初始化和在then部分初始化的交集 andSetInits(tree.thenpart, initsBeforeElse); uninits.andSet(uninitsBeforeElse); }
由于当前,if语句中没有else部分,因此会执行如下代码:
andSetInits(tree.thenpart, initsBeforeElse); uninits.andSet(uninitsBeforeElse);
该步骤处理之后,inits就是与initsBeforeElse交集(与运算)的部分.uninits就是与uninitsBeforeElse交集(与运算)的部分.
andSetInits方法如下:
protected void andSetInits(JCTree tree, Bits bits) { inits.andSet(bits); } // com.sun.tools.javac.util.Bits.andSet(Bits) public Bits andSet(Bits xs) { Assert.check(currentState != BitsState.UNKNOWN); internalAndSet(xs); currentState = BitsState.NORMAL; return this; } // com.sun.tools.javac.util.Bits.internalAndSet(Bits) protected void internalAndSet(Bits xs) { Assert.check(currentState != BitsState.UNKNOWN); sizeTo(xs.bits.length); for (int i = 0; i < xs.bits.length; i++) { bits[i] = bits[i] & xs.bits[i]; } }
这部分的代码还是很好看的,后面会有一遍文章专门讲解Bits这个类的.
因此,当我们处理完之后,inits = 11000000000000000000000000000000,uninits = 00000000000000000000000000000000
看到这里,我们可以总结一下,对于下面的情况:
public void test(){ int a = 0; int b ; if( (b = 1) != 0){ int c = 1; } }
则最终inits = 11000000000000000000000000000000,uninits = 00000000000000000000000000000000
而对于下面这种情况:
public void test5(){ int a = 0; int b ; if( (b = 1) != 0){ int c = 1; }else{ int d = 4; } }
则最终inits = 11100000000000000000000000000000,uninits = 00000000000000000000000000000000
为啥呢,inits也好,uninits也好,都是记录变量初始化的位图,那么对于该方法来说,
inits,uninits下标为0的就是符号a,下标为1的就是符号b,接下来,由于有then,else部分,
则在then 中, inits = 11100000000000000000000000000000(下标为2的是符号c),
而在else部分中,inits = 11100000000000000000000000000000(下标为2的是符号d),
那么最终进行与运算, inits就是11100000000000000000000000000000,表示在变量表中,下标0,1,2的都是初始化的,而不管是符号c,还是符号d.