javac final变量未赋值检测案例讲解
作者:一个努力的码农 发布时间:2023-09-29 04:25:17
前言
我们在前面介绍AssignAnalyzer时,对AssignAnalyzer.letInit(DiagnosticPosition, VarSymbol)方法进行了简单的介绍.本文就举一个案例,来深入理解一下.
案例
案例代码如下:
public class CheckInitError {
static final int b;
public CheckInitError(){
}
}
本代码在IDE环境(如Eclipse)中报如下错误:The blank final field b may not have been
initialized.
那么它是如何检测出错误的,本文就来揭秘.(Eclipse中内置了Java编译器)
解析
还是在javac中的Flow阶段,最终来到了AbstractAssignAnalyzer.analyzeTree(Env<?>, JCTree)方法,在该方法中,又调用了Flow.BaseAnalyzer.scan(JCTree)方法,进而调用AbstractAssignAnalyzer.visitClassDef(JCClassDecl)方法,同时,由于字段b是可追踪的,因此会在处理静态字段时调用AbstractAssignAnalyzer.newVar(JCVariableDecl)方法,将b所对应的符号保存在vardecls中,下标位置为0(下标为0的原因是它是第一个变量).这部分的内容是在上篇文章中有所介绍的,本文不再展开.
在AbstractAssignAnalyzer#visitClassDef方法中,处理完静态字段,静态初始块,实例字段,实例初始块之后,就会处理方法(包括构造器).
那么由于在CheckInitError中只定义了一个构造器,因此如下代码只会处理一次:
for (List<JCTree> l = tree.defs; l.nonEmpty(); l = l.tail) {
if (l.head.hasTag(METHODDEF)) {
scan(l.head);
}
}
由于CheckInitError构造器所对应的树节点是JCMethodDecl,因此最终会调用AbstractAssignAnalyzer.visitMethodDef(JCMethodDecl).
在AbstractAssignAnalyzer#visitMethodDef中,一开始,是进行判断:
// 如果该方法的语句体为null,则意味着该方法是一个抽象方法,不处理.
if (tree.body == null) {
return;
}
// 忽略处理合成方法,但是合成的lambda方法还是处理的
if ((tree.sym.flags() & (SYNTHETIC | LAMBDA_METHOD)) == SYNTHETIC) {
return;
}
然后是保存现场:
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());
接下来,判断当前方法是一个构造函数,其构造函数中的第一个语句不是this(…)的语句吗?
boolean isInitialConstructor =
TreeInfo.isInitialConstructor(tree);
思考: 为何会在这里做这样的判断? AssignAnalyzer的定位是检查final变量是否有多次赋值,那么假设我们在一个类中final 字段(未初始化的),那么不管你有多少个构造函数,那么就应该在一个最终调用的构造器中对这个变量进行初始化.举例:
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方法构造函数中的第一个语句不是this(…),而是super();因此isInitialConstructor为true.为啥呢? javac会在构造器的第一行插入super(),至于是在什么条件下插入,如何插入,我们后续介绍,本文不表.
由于isInitialConstructor等于true,因此,如下代码是不会执行的:
if (!isInitialConstructor) {
firstadr = nextadr;
}
接下来,是处理方法的参数,那么由于在案例中是没有参数的,因此如下代码是不会执行的:
for (List<JCVariableDecl> l = tree.params; l.nonEmpty(); l = l.tail) {
JCVariableDecl def = l.head;
scan(def);
// 参数应该有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);
}
这段代码的作用是依次处理参数,然后将参数加入到变量已经初始化的位图中,至于为啥? 原因很简单:参数是调用方传递的,当方法执行时,形参是肯定有值的(初始化的),否则就是一个错误
接下来处理方法体,由于javac默认添加了一个super()语句,那么就会进行实质的处理(副作用).但是这部分与本文关联不大,本文就不展开了.
方法体执行完之后,如果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,那么因此循环只会执行一次(firstadr = 0,nextadr = 1,这部分的内容在上篇文章有介绍)
在循环中,通过下标取得b对应的VarSymbol,调用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(初始化变量位图)中不存在对应的下标,因此会调用log#error方法,进行错误日志输出.然后将其加入到inits(这样做的目的,是为了一次编译能获得更多的错误信息)
对于当前,是报如下错误:
然后,是pendingExits 处理和恢复现场,这部分的内容,我们后续文章会举例子进行讲解.
来源:https://blog.csdn.net/qq_26000415/article/details/82776508
猜你喜欢
- 什么是OKHttp一般在Java平台上,我们会使用Apache HttpClient作为Http客户端,用于发送 HTTP 请求,并对响应进
- 题目要求思路一:DFS+序列化设计一种规则将所有子树序列化,保证不同子树的序列化字符串不同,相同子树的序列化串相同。用哈希表存所有的字符串,
- 实例如下:package com.bwsk.modules.weixin.util;import java.util.Random;/**
- 这篇文章介绍了Java+Nginx实现POP、IMAP、SMTP邮箱代理服务,我们本次使用的环境为Centos7下,java程序我们通过ec
- 本文基于SpringBoot 2.5.0-M2讲解Spring中Lifecycle和SmartLifecycle的作用和区别,以及如何控制S
- yield()介绍yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;
- 说道线程,肯定会想到使用 java.lang.Thread.java这个类那么创建线程也主要有2种方式第一种方式:public class
- java沙箱环境测试支付宝支付接口?准备工作,登陆支付宝开放平台,进入沙箱环境开放平台链接:https://developers.alipa
- 需求在配置类中,从application.properties中读取一个复杂list。如List<Person>或者初始化一个m
- 干java 开发这么多年, 之前一直没留意java 进程还区分守护进程和用户进程。守护进程这个概念最早还是在linux系统中接触的,直到近期
- 本文介绍了SpringCloud +Zookeeper完成配置中心,分享给大家,具有如下:使用场景项目配置更改不需要打包,重启提供配置文件的
- 本文将介绍使用Spring Boot集成Mybatis并实现主从库分离的实现(同样适用于多数据源)。延续之前的Spring Boot 集成M
- 1. 字段取别名,和属性名保持一致映射文件<mapper namespace="com.atguigu.mybatis.ma
- 在阿里开发手册的建表规约中有说明,数据库表中应该都要有create_time、update_time字段;那么在开发中,对于这些共有字段的处
- 自动注入和@Autowire@Autowire不属于自动注入!注入方式(重要)在Spring官网上(文档),定义了在Spring中的注入方式
- 引言容器是Java基础类库中使用频率最高的一部分,Java集合包中提供了大量的容器类来帮组我们简化开发,我前面的文章中对Java集合包中的关
- 一、创建项目1.File->new->project;2.选择“Spring Initializr”,点击next;(jdk1.
- 我们在java中处理字符串的时候,一般会选择String,在python中同样也是作用于字符串。那么我们今天延伸一下它的用法,只使用Stri
- package list;import java.util.ArrayList;/** * Java约瑟夫问题: n个人(不同id
- 我们都知道单精度浮点数(Single,float,Real)由32位0或1组成,它具体是如何来的。浮点数的32位N=1符号位(Sign)+8