javac final变量未赋值检测案例讲解

目录
  • 前言
  • 案例
  • 解析
  • 总结

前言

我们在前面介绍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 处理和恢复现场,这部分的内容,我们后续文章会举例子进行讲解.

总结

通过本文,我们可以知道Eclipse中报如下错误:The blank final field b may not have been
initialized. 是在Flow阶段由AssignAnalyzer检测出来的.

到此这篇关于javac final变量未赋值检测案例讲解的文章就介绍到这了,更多相关javac final变量未赋值内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaCV实现照片马赛克效果

    目录 前言 准备工作 代码实现 完整代码 前言 青空最近在逛一些社区的时候发现了有很多图片是像素图,感觉挺好玩的.正巧最近自己在学习JavaCV,所以在这里给大家演示一下如何使用JavaCV来处理像素图. 像素图其实有点类似于类似于打马赛克的功能.通过像素的变化,演示一个像素画的功能.像素画在 NFT 中特别的流行. 准备工作 我们先引入 JavaCV 的依赖库 <dependency> <groupId>org.bytedeco</groupId> <artif

  • java冷知识:javac AbstractProcessor详解

    目录 它可以做什么? Processor AbstractProcessor 源码 google的 auto-service javapoet 你喜欢的lombok实现原理是怎样的呢? 它可以做什么? 它做的事情当然是生成新类或修改原始的类,比如你遇到这样的情况下就可以使用: 反射好慢,曾见过一个大厂大量是Gson,由于Gson序列化时大量使用了反射,每一个field,每一个get.set都需要用反射,由此带来了性能问题.解决方法就是使用它尽量减少反射(替换成JSONObject) 生成代码,只

  • 解决javac不是内部或外部命令,也不是可运行程序的报错问题

    目录 简述 下面就讲讲解决方法 解决过程 可能还有的问题 解决之后的效果 简述 在学着使用Java的命令行来编译java文件的时候,遇到了这个问题 Windows操作系统报错 “‘javac’不是内部或外部命令,也不是可运行的程序或批处理文件” 下面就讲讲解决方法 回复评论区的找不到环境变量和用户变量的哥们 瞎发帖 解决过程 根据探究的情况来看,这里其实是没有安装好(或者是安装了之后但是在环境变量上却没有设置好) 第一步: 在http://www.oracle.com/technetwork/j

  • JavaCV实战之调用摄像头基础详解

    目录 关于<JavaCV的摄像头实战>系列 本篇概览 环境和版本信息 源码下载 基本套路分析 基本框架编码 部署媒体服务器 关于<JavaCV的摄像头实战>系列 <JavaCV的摄像头实战>顾名思义,是使用JavaCV框架对摄像头进行各种处理的实战集合,这是欣宸作为一名Java程序员,在计算机视觉(computer vision)领域的一个原创系列,通过连续的编码实战,与您一同学习掌握视频.音频.图片等资源的各种操作 另外要说明的是,整个系列使用的摄像头是USB摄像图或

  • JavaCV实现读取视频信息及自动截取封面图详解

    目录 概述 javacv 介绍 引入 javacv 读取视频信息 创建 VideoInfo 类 使用 FFmpegFrameGrabber 读取视频信息 截图 概述 最近在对之前写的一个 Spring Boot 的视频网站项目做功能完善,需要利用 FFmpeg 实现读取视频信息和自动截图的功能,查阅资料后发现网上这部分的内容非常少,于是就有了这篇文章. 视频网站项目地址 GitHub 码云 本文将介绍如何利用Javacv实现在视频网站中常见的读取视频信息和自动获取封面图的功能. javacv 介

  • javac final变量未赋值检测案例讲解

    目录 前言 案例 解析 总结 前言 我们在前面介绍AssignAnalyzer时,对AssignAnalyzer.letInit(DiagnosticPosition, VarSymbol)方法进行了简单的介绍.本文就举一个案例,来深入理解一下. 案例 案例代码如下: public class CheckInitError { static final int b; public CheckInitError(){ } } 本代码在IDE环境(如Eclipse)中报如下错误:The blank

  • 利用try-catch判断变量是已声明未声明还是未赋值

    目的是如果一个变量是已声明未赋值,就可以直接赋值:并且不能改变变量的作用域 如果未声明的话,就重新声明, 在网上搜了下,常见的方法是if(typeof(a)=='undefined'){var a='ss';}, 但是这种方法对未声明或已声明未赋值的变量都会返回true.而且如果是这样: 复制代码 代码如下: var a; function f(){ if(typeof(a)=='undefined') {var a=1;} } f(); console.log(a); 会显示undefined

  • Java未赋值变量的初始值解析(默认值)

    目录 Java未赋值变量的初始值(默认值) 初始值(默认值) 实例变量(非静态字段) 类变量(静态字段) 本地变量 参数 解决java未赋值变量的默认值问题 如下所示 Java未赋值变量的初始值(默认值) 初始值(默认值) 参考:官方文档 Java程序中,任何变量必须初始化后才能使用. 以下为不同数据类型的默认值. 数据类型 初始值 byte 0 short 0 int 0 long 0L char 'u0000' float 0.0f double 0 boolean false 所有引用类型

  • 案例讲解WEB 漏洞-文件操作之文件下载读取

    目录 原理 漏洞危害 利用方式 系统文件 window Linux 常见脚本敏感文件参考 任意文件读取 任意文件下载 Google search 漏洞利用代码 漏洞挖掘 漏洞验证 漏洞防范 案例 pikuchu靶场-文件下载测试 小米路由器-文件读取真实测试-漏洞 RoarCTF2019-文件读取真题复现 百度杯2017二月-Zone真题复现 原理 产生:任意语言代码下载函数 文件下载(一些网站由于业务需求,往往需要提供文件查看或文件下载功能,但若对用户查看或下载的文件不做限制,则恶意用户就能够

  • JavaWeb案例讲解Servlet常用对象

    概述 本次文章基于第三章的ServletConfig,ServletContext,HttpServletRequest,HttpServletResponse对象完成一个图书订阅系统的购买图书和查看图书购买记录功能. 搭建项目主页面 创建一个动态网站项目,在src中新建包com.book.servlet. 在包中,新建HomeServlet作为主页.效果图如下: 为了让一访问项目根路径地址就默认进入HomeServlet,这里需要将 HomeServlet的虚拟地址写入web.xml文件中作为

  • Lua中的变量与赋值方法

    看以下案例: test.lua -- 第一个lua脚本 --注释使用"--"符 --变量未定义时,默认初始化的值为nil --这样的定义为全局 num1 = 1 ; --加了关键字local表示这个变量是局部变量 local num2 = 2 ; --定义变量的末尾不加分号;也是可以的,个人建议,因为Lua是C写的,写分号还是规范点 num3 = 3 --定义一个函数,目的是实现两数相加并返回 function add() --a = 1 也可以在函数内部定义 --b = 2 retu

  • c语言中enum类型的用法案例讲解

    11.10 枚举类型 在实际问题中,有些变量的取值被限定在一个有限的范围内.例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等等.如果把这些量说明为整型,字符型或其它类型显然是不妥当的.为此,C语言提供了一种称为"枚举"的类型.在"枚举"类型的定义中列举出所有可能的取值,被说明为该"枚举"类型的变量取值不能超过定义的范围. 应该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型. 11.10.

  • Go语言运算符案例讲解

    算数运算符 算数运算符和C语言几乎一样 运算符 描述 实例 + 相加 A + B - 相减 A - B * 相乘 A * B / 相除 B / A % 求余 B % A ++ 自增 A++ – 自减 A– 注意点: 只有相同类型的数据才能进行运算 package main import "fmt" int main(){ var num1 int32 = 10 //var num2 int64 = num1 // 类型不同不能进行赋值运算 var num2 int64 = int64(

  • Java之类加载机制案例讲解

    1.类加载 <1>.父子类执行的顺序 1.父类的静态变量和静态代码块(书写顺序) 2.子类的静态变量和静态代码块(书写顺序) 3.父类的实例代码块(书写顺序) 4.父类的成员变量和构造方法 5.子类的实例代码块 6.子类的成员变量和构造方法 <2>类加载的时机 如果类没有进行初始化,则需要先进行初始化,虚拟机规范则是严格规定有且只有5种情况必须先对类进行初始化(而加载,验证,准备要在这个之前开始) 1.创建类的实例(new的方式),访问某个类的静态变量,或者对该静态变量赋值,调用类

  • java volatile案例讲解

    本篇来自java并发编程实战关于volatile的总结. 要说volatile,先得明白内存可见性.那我们就从内存可见性说起. 一.内存可见性 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值.这看起来很自然.然而,当读操作和写操作在不同的线程中执行时,情况却并非如此,这听起来或许有些难以接受.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能

随机推荐