javac 中的if-else变量初始化问题

前言

在前几篇文章中我们介绍过了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方法所对应的树如下:

1

那么在该方法的处理过程就是对这个树的先序遍历.

那么在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 = 00000000000000000000000000000000

    split方法执行完之后,执行如下代码:

    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.

猜你喜欢

转载自blog.csdn.net/qq_26000415/article/details/82870282