Java高级——前端编译与优化

概述

前端编译器把java文件编译成class文件,如:JDK的Javac、Eclipse JDT中的增量式编译器(ECJ)

  • 前端编译器对代码的运行效率无任何优化措施
  • 性能优化集中到运行期的即时编译器中,这样可适用于非Javac生成的Class文件
  • 但有针对编码过程的优化,从而降低编码复杂度,如语法糖

《Java虚拟机规范》未规定如何将java文件编译为Class文件,给予了较大的灵活性

编译过程与具体编译器实现绑定,可能会导致某些代码在Javac中可以编译,而在ECJ则不可编译

Javac导入

Javac编译器由纯Java实现

  • 源码放在/langtools/src/share/classes/com/sun/tools/javac
  • JDK9后放在/src/jdk.compiler/share/classes/com/sun/tools/javac

下面导入JDK8中的Javac源码,新建项目,将源码langtools/src/share/classes中的com/sun复制到项目目录(根据提示将private改为package即可解决报错)

在这里插入图片描述

将Run-RunConfiguration-Main中的Mian Class指定为javac.Main

在这里插入图片描述

新建一个Test.java,在Argument-Program argument指定其路径

在这里插入图片描述

点击Run,即可编译出Test.class,与在命令行中运行javac Test.java效果是一样的

在这里插入图片描述

Javac编译过程

  • 初始化插入式注解处理器
  • 解析与填充符号表
  • 注解处理
  • 分析与字节码生成

在这里插入图片描述

Javac编译动作的入口是
com.sun.tools.javac.main.JavaCompiler类,关键代码如下

在这里插入图片描述

解析与填充符号表

解析

解析由parseFiles()方法完成,包括词法分析和语法分析

词法分析将源代码的字符流转变为标记(Token)集合

  • 字符是程序最小元素,但标记才是编译时的最小元素
  • 关键字、变量名、字面量、运算符都可以作为标记
  • 由com.sun.tools.javac.parser.Scanner类实现
  • 如int a=b+2包含了6个标记,分别是int、a、=、b、+、2

语法分析根据标记序列构造抽象语法树

  • 抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式
  • 其节点都代表着程序代码中的一个语法结构(Syntax Construct),如包、类型等
  • 由com.sun.tools.javac.parser.Parser类构造,com.sun.tools.javac.tree.JCTree类表示

如下是Eclipse AST View插件分析出来的某段代码的抽象语法树视图

在这里插入图片描述
生成语法树以后,编译器不会再对源码字符流进行操作了,后续操作都建立在抽象语法树之上

填充符号表

填充符号表由enterTrees()方法完成,符号表(Symbol Table)是由一组符号地址和符号信息构成的数据结构

  • 在语义分析阶段用于语义检查(如检查一个名字的使用和原先的声明是否一致)和产生中间代码
  • 在目标代码生成阶段根据符号表对符号名进行地址分配
  • 由com.sun.tools.javac.comp.Enter类实现
  • 生成一个待处理列表,包含了每一个编译单元的抽象语法树和package-info.java的顶级节点

注解处理过程

注解处理由processAnnotations()方法完成,JDK6之前注解只会在程序运行时发挥作用,JDK6设计了插入式注解处理器API将注解处理提前至编译期

可把插入式注解处理器看作编译器的插件,允许读取、修改、添加抽象语法树中的任意元素,如注解来实现自动产生getter/setter方法、生成受查异常表等

若在处理注解期间修改过语法树,编译器将回到解析及填充符号表的过程重新处理,直至不再修改语法树,循环过程称为一个轮次(Round)

若有新的注解处理器,会通过com.sun.tools.javac.processing.JavacProcessing-Environment类的doProcessing()方法生成一个新的JavaCompiler对象

分析和字节码生成

分析和字节码生成由Compile2()方法完成,包括标注检查、数据流及控制流分析、解语法糖、字节码生成

在这里插入图片描述
分析指语义分析,对结构上正确的源程序进行上下文相关性质的检查

抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的,如下

int a = 1;
boolean b = false;
char c = 2;

可能出现

int d = a + c;
int d = b + c;
char d = a + c;

上面3种赋值都能构成正确的抽象语法树,但只有第一种赋值在Java语义上是正确的(IDE的红线错误提示即来源于语义分析)

标注检查

标注检查由attribute()完成,实现类是com.sun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check

标注检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配

在标注检查中,会进行一个称为常量折叠(Constant Folding)的代码优化,如对于

int a = 1 + 2;

在抽象语法树上仍然能看到字面量“1”“2”和操作符“+”号,但是在经过常量折叠优化之后,会变成字面量“3”,

数据流及控制流分析

数据流及控制流分析由flow()方法完成,实现类为com.sun.tools.javac.comp.Flow

对程序上下文逻辑更进一步的验证,如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值等

编译时和类加载时的数据及控制流分析基本一样,但校验范围有所区别,有一些校验项只能在编译期或运行期进行

public void foo(final int arg) {
	final int var = 0;
}
public void foo(int arg) {
	int var = 0;
}

如上,一个方法有final,一个没有,但它们编译出来的指令都是一模一样

  • 局部变量在常量池中并没有CONSTANT_Fieldref_info
  • 也无存储有访问标志(access_flags)的信息
  • 故Class文件不会知道局部变量是否为final
  • final的不可变性仅由Javac在编译期间保证

解语法糖

解语法糖由desugar()方法完成,实现类为com.sun.tools.javac.comp.TransTypes和com.sun.tools.javac.comp.Lower

语法糖指在计算机语言中添加的某种语法,其对语言的编译结果和功能并没有实际影响,但是却能更方便使用该语言

Java中的语法糖如泛型、变长参数、自动装箱拆箱等,JVM运行时并不直接支持这些语法,需在编译阶段将其还原回原始的基础语法结构,称为解语法糖

字节码生成

字节码生成由generate()方法完成,实现类为com.sun.tools.javac.jvm.Gen

把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,还进行了代码添加(<init>)和转换(将+转为StringBuffer的append)

完成对语法树的遍历和调整之后,就会把的符号表交到com.sun.tools.javac.jvm.ClassWriter的writeClass()方法输出字节码,生成Class文件

前端编译优化

泛型

泛型将操作的数据类型指定为方法签名中的一种特殊参数,用于类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法

Java和C#的泛型

C#是具现化式泛型(Reified Generics)

  • 在源码、编译后的中间语言或是运行期都是实际存在的
  • 运行期的List<int>与List<string>是两个不同的类型,有独立的虚方法表和类型数据

Java是类型擦除式泛型(Type Erasure Generics)

  • 只在程序源码中存在,在Class中泛型被替换为原来的裸类型(Raw Type),并且加入强制转型代码
  • 运行期的ArrayList<int>与ArrayList<String>是同一个类型

Java泛型使用效果(无法instanceof、创建对象和数组)和运行效率(需不断拆箱装箱)都不如C#,唯一优势在于实现只需要修改Javac,不需要改动字节码和JVM,也保证了以前没有使用泛型的库也可运行在JDK5之上

泛型擦除

为了保证兼容性,定义裸类型为所有该类型泛型化实例的共同父类型(Super Type),如下

ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list; // 裸类型
list = ilist;
list = slist;

在编译时把ArrayList<Integer>还原回ArrayList,在元素访问、修改时自动插入一些强制类型转换和检查指令

public static void main(String[] args) {
	Map<String, String> map = new HashMap<String, String>();
	map.put("hello", "你好");
	map.put("how are you?", "吃了没?");
	System.out.println(map.get("hello"));
	System.out.println(map.get("how are you?"));
}

对于如上代码,编译成Class,再反编译,程序变回泛型出现之前的写法,泛型都变回了裸类型,在元素访问时插入强制转型代码

public static void main(String[] args) {
	Map map = new HashMap();
	map.put("hello", "你好");
	map.put("how are you?", "吃了没?");
	System.out.println((String) map.get("hello"));
	System.out.println((String) map.get("how are you?"));
}

泛型缺点

泛型擦除无法支持原始类型,如下

ArrayList<int> list = new ArrayList<int>();

导致只能使用包装类型,运行速度慢

ArrayList<Integer> list = new ArrayList<Integer>();

运行时无法获取泛型类型信息

if (item instanceof E) { // 不合法,无法对泛型进行实例判断

}
E newItem = new E(); // 不合法,无法使用泛型创建对象
E[] itemArray = new E[10]; // 不合法,无法使用泛型创建数组

如写一个泛型版本的从List到数组的转换方法,需显式传递数组的组件类型componentType

public static <T> T[] convert(List<T> list, Class<T> componentType) {
	T[] array = (T[])Array.newInstance(componentType, list.size());
}

为了在反射中获取参数化类型,新增Signature属性用于存储一个方法在字节码层面的特征签名,其包含了参数化类型的信息

其他

如下,包含泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖

public static void main(String[] args) {
	List<Integer> list = Arrays.asList(1, 2, 3, 4);
	int sum = 0;
	for (int i : list) {
		sum += i;
	}
	System.out.println(sum);
}

反编译后

  • 自动装箱、拆箱转化为对应的包装和还原方法
  • 遍历循环把代码还原成了迭代器实现
  • 变长参数变成了一个数组类型的参数
public static void main(String[] args) {
	List list = Arrays.asList( new Integer[] {
	Integer.valueOf(1),
	Integer.valueOf(2),
	Integer.valueOf(3),
	Integer.valueOf(4) });
	int sum = 0;
	for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
		int i = ((Integer)localIterator.next()).intValue();
		sum += i;
	}
	System.out.println(sum);
}

条件编译

使用条件为常量的if语句,会触发条件编译,如下

public static void main(String[] args) {
	if (true) {
		System.out.println("block 1");
	} else {
		System.out.println("block 2");
	}
}

在编译时,就会优化

public static void main(String[] args) {
	System.out.println("block 1");
}

若非if语句,则可能在控制流分析中提示错误且拒绝编译,如下

public static void main(String[] args) {
	// 编译器将会提示“Unreachable code”
	while (false) {
		System.out.println("");
	}
}

实战:插入式注解处理器

在前端编译这部分,可通过插入式注解处理器API影响Java编译子系统的行为

接下来基于插入式注解处理器API编写一个校验工具NameCheckProcessor规范书写格式:

  • 类(或接口):驼式命名法,首字母大写
  • 方法:驼式命名法,首字母小写
  • 类或实例变量:符合驼式命名法,首字母小写。
  • 常量:由大写字母或下划线构成,首字符不能是下划线

代码实现

继承抽象类javax.annotation.processing.AbstractProcessor,实现抽象方法process(),其是Javac在执行注解处理器代码时的回调,如下,把当前轮次中的每一个RootElement传递到NameChecker执行名称检查逻辑

import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

@SupportedAnnotationTypes("*") //设置要处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_6) //设置处理的Java版本
public class NameCheckProcessor extends AbstractProcessor {
    private NameChecker nameChecker;
    @Override
    public void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        nameChecker = new NameChecker(processingEnv);
    }
  
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (!roundEnv.processingOver()) {
            for (Element element : roundEnv.getRootElements())
                nameChecker.checkNames(element);
        }
        return false;	//不需要改变或添加抽象语法树,返回false,编译器不构造新的JavaCompiler实例
    }
}
  • 域processingEnv是父类中的一个protected变量,用于创建新的代码、向编译器输出信息、获取其他工具类等
  • visitType()、visitVariable()和visitExecutable()访问类、字段和方法
  • checkCamelCase()与checkAllCaps()检查驼式命名法和全大写命名
import java.util.EnumSet;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementScanner6;
import javax.tools.Diagnostic;


public class NameChecker {
    private final Messager messager;
    NameCheckScanner nameCheckScanner = new NameCheckScanner();

    NameChecker(ProcessingEnvironment processsingEnv) {
        this.messager = processsingEnv.getMessager();
    }

    public void checkNames(Element element) {
        nameCheckScanner.scan(element);
    }

    /**
     * 名称检查器实现类,继承了JDK 6中的ElementScanner6
     * 将会以Visitor模式访问抽象语法树中的元素
     */
    private class NameCheckScanner extends ElementScanner6<Void, Void> {
        /**
         * 此方法用于检查Java类
         */
        @Override
        public Void visitType(TypeElement e, Void p) {
            scan(e.getTypeParameters(), p);
            checkCamelCase(e, true);
            super.visitType(e, p);
            return null;
        }

        /**
         * 检查方法命名是否合法
         */
        @Override
        public Void visitExecutable(ExecutableElement e, Void p) {
            if (e.getKind() == ElementKind.METHOD) {
                Name name = e.getSimpleName();
                if (name.contentEquals(e.getEnclosingElement().getSimpleName()))
                	messager.printMessage(Diagnostic.Kind.WARNING, "一个普通方法 “" + name + "”不应当与类名重复,避免与构造函数产生混淆", e);
                	checkCamelCase(e, false);
            }
            super.visitExecutable(e, p);
            return null;
        }

        /**
         * 检查变量命名是否合法
         */
        @Override
        public Void visitVariable(VariableElement e, Void p) {
            // 如果这个Variable是枚举或常量,则按大写命名检查,否则按照驼式命名法规则检查
            if (e.getKind() == ElementKind.ENUM_CONSTANT || e.getConstantValue() != null || heuristicallyConstant(e))
                checkAllCaps(e);
            else
                checkCamelCase(e, false);
            return null;
        }

        /**
         * 判断一个变量是否是常量
         */
        private boolean heuristicallyConstant(VariableElement e) {
            if (e.getEnclosingElement().getKind() == ElementKind.INTERFACE)
                return true;
            else if (e.getKind() == ElementKind.FIELD && e.getModifiers().containsAll(EnumSet.of(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)))
                return true;
            else {
                return false;
            }
        }

        /**
         * 检查传入的Element是否符合驼式命名法,如果不符合,则输出警告信息
         */
        private void checkCamelCase(Element e, boolean initialCaps) {
            String name = e.getSimpleName().toString();
            boolean previousUpper = false;
            boolean conventional = true;
            int firstCodePoint = name.codePointAt(0);
            if (Character.isUpperCase(firstCodePoint)) {
                previousUpper = true;
                if (!initialCaps) {
                    messager.printMessage(Diagnostic.Kind.WARNING, "名称“" + name + "”应当以小写字母开头", e);
                    return;
                }
            } else if (Character.isLowerCase(firstCodePoint)) {
                if (initialCaps) {
                    messager.printMessage(Diagnostic.Kind.WARNING, "名称“" + name + "”应当以大写字母开头", e);
                    return;
                }
            } else
                conventional = false;
            if (conventional) {
                int cp = firstCodePoint;
                for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
                    cp = name.codePointAt(i);
                    if (Character.isUpperCase(cp)) {
                        if (previousUpper) {
                            conventional = false;
                            break;
                        }
                        previousUpper = true;
                    } else
                        previousUpper = false;
                }
            }
            if (!conventional)
                messager.printMessage(Diagnostic.Kind.WARNING, "名称“" + name + "”应当符合驼式命名法(Camel Case Names)", e);
        }

        /**
         * 大写命名检查,要求第一个字母必须是大写的英文字母,其余部分可以是下划线或大写字母
         */
        private void checkAllCaps(Element e) {
            String name = e.getSimpleName().toString();
            boolean conventional = true;
            int firstCodePoint = name.codePointAt(0);
            if (!Character.isUpperCase(firstCodePoint))
                conventional = false;
            else {
                boolean previousUnderscore = false;
                int cp = firstCodePoint;
                for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) {
                    cp = name.codePointAt(i);
                    if (cp == (int) '_') {
                        if (previousUnderscore) {
                            conventional = false;
                            break;
                        }
                        previousUnderscore = true;
                    } else {
                        previousUnderscore = false;
                        if (!Character.isUpperCase(cp) && !Character.isDigit(cp)) {
                            conventional = false;
                            break;
                        }
                    }
                }
            }
            if (!conventional)
                messager.printMessage(Diagnostic.Kind.WARNING, "常量“" + name + "”应当全部以大写字母或下划线命名,并且以字母开头", e);
        }
    }
}

验证

使用如下代码验证

public class BADLY_NAMED_CODE {
    enum colors {
        red, blue, green;
    }

    static final int _FORTY_TWO = 42;
    public static int NOT_A_CONSTANT = _FORTY_TWO;

    protected void BADLY_NAMED_CODE() {
        return;
    }

    public void NOTcamelCASEmethodNAME() {
        return;
    }
}

利用上面导入的Javac,参数-processor指定执行编译时需要附带的注解处理器(有多个用逗号分隔)

javac src/com/sun/NameChecker.java
javac src/com/sun/NameCheckProcessor.java
javac -processor com.sun.NameCheckProcessor src/com/sun/BADLY_NAMED_CODE.java

输出如下

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值