概述

编译器是一种计算机程序, 它主要的目的是将便于人编写、阅读、维护的高级计算机语言所写的源代码程序, 翻译为计算机能解读、运行的低阶机器语言的程序, 即可执行文件。而 javac 就是java语言中的编译器, 它用于将 .java 文件转换成JVM能识别的 .class 字节码文件, 反编译则是将 .class 文件转换成 .java 文件。

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

java中的语法糖只存在于编译期, 在编译器将 .java 源文件编译成 .class 字节码时, 会进行解语法糖操作, 还原最原始的基础语法结构。这些语法糖包含条件编译、断言、Switch语句与枚举及字符串结合、可变参数、自动装箱/拆箱、枚举、内部类、泛型擦除、增强for循环、lambda表达式、try-with-resources语句、JDK10的局部变量类型推断等等。

关于反编译工具, 其实在JDK中自带了一个javap命令, 在以前的文章JDK的命令行工具系列 (二) javap、jinfo、jmap中也有提及到, 但是日常中很少会用到javap, 所以这次我们借助另一个反编译工具 CFR 来分析java中的语法糖, 这里我下载的是最新的cfr_0_132.jar。

字符串拼接

/*** 字符串拼接* option: --stringbuilder false*/
public void stringBuilderTest(int end) {char[] foo = new char[]{'@', 'a', '*'};char ch;int x = 0;while ((ch = foo[++x]) != '*') {System.out.println("" + x + ": " + ch);}
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --stringbuilder false

从反编译后的代码中能看出, 当我们使用+号进行字符串拼接操作时, 编译时会自动创建一个StringBuilder对象。所以当在循环中拼接字符串时, 应避免使用+号操作, 否则每次循环都会创建一个StringBuilder对象再回收, 造成较大的开销。

条件编译

/*** 条件编译* option: 不需要参数*/
public void ifCompilerTest() {if(false) {System.out.println("false if");}else {System.out.println("true else");}
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class

很明显, javac编译器在编译时期的解语法糖阶段, 会将条件分支不成立的代码进行消除。

断言

/*** 断言, JDK1.4开始支持* option: --sugarasserts false*/
public void assertTest(String s) {assert (!s.equals("Fred"));System.out.println(s);
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --sugarasserts false

如上, 当断言结果为true时, 程序继续正常执行, 当断言结果为false时, 则抛出AssertionError异常来打断程序的执行。

枚举与Switch语句

/*** 枚举与Switch语句* option: --decodeenumswitch false*/
public int switchEnumTest(EnumTest e) {switch (e) {case FOO:return 1;case BAP:return 2;}return 0;
}/*** 枚举, JDK1.5开始支持* option: --sugarenums false*/
public enum EnumTest {FOO,BAR,BAP
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --decodeenumswitch false

switch支持枚举是通过调用枚举类默认继承的父类Enum中的ordinal()方法来实现的, 这个方法会返回枚举常量的序数。由于笔者的经验尚浅, 具体的实现细节还不是很清楚(比如枚举常量FOO的序数是0, 而case FOO语句编译后的 case 1, 这个1是什么? 另外switchEnumTest()方法传入一个FOO, 调用ordinal()方法得到的序数为0, 那么他又是如何与case 1进行匹配的呢?), 欢迎读者在留言区一起讨论。

字符串与Switch语句

/** * 字符串与Switch语句* option: --decodestringswitch false*/
public int switchStringTest(String s) {switch (s) {default:System.out.println("Test");break;case "BB":  // BB and Aa have the same hashcode.return 12;case "Aa":case "FRED":return 13;}System.out.println("Here");return 0;
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --decodestringswitch false

switch支持字符串是通过hashCode()和equals()方法来实现的, 先通过hashCode()返回的哈希值进行switch, 然后通过equals()方法比较进行安全检查, 调用equals()是为了防止可能发生的哈希碰撞。

另外switch还支持byte、short、int、char这几种基本数据类型, 其中支持char类型是通过比较它们的ascii码(ascii码是整型)来实现的。所以switch其实只支持一种数据类型, 也就是整型, 其他诸如String、枚举类型都是转换成整型之后再使用switch的。

可变参数

/*** 可变参数* option: --arrayiter false*/
public void varargsTest(String ... arr) {for (String s : arr) {System.out.println(s);}
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --arrayiter false

可变参数其实就是一个不定长度的数组, 数组长度随传入方法的对应参数个数来决定。可变参数只能在参数列表的末位使用。

自动装箱/拆箱

/*** 自动装箱/拆箱* option: --sugarboxing false*/
public Double autoBoxingTest(Integer i, Double d) {return d + i;
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --sugarboxing false

首先我们知道, 基本类型与包装类型在某些操作符的作用下, 包装类型调用valueOf()方法的过程叫做装箱, 调用xxxValue()方法的过程叫做拆箱。所以上面的结果很容易看出, 先对两个包装类进行拆箱, 再对运算结果进行装箱。

枚举

/*** 枚举, JDK1.5开始支持* option: --sugarenums false*/
public enum EnumTest {FOO,BAR,BAP
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --sugarenums false

当我们自定义一个枚举类型时, 编译器会自动创建一个被final修饰的枚举类来继承Enum, 所以自定义枚举类型是无法继承和被继承的。当枚举类初始化时, 枚举字段引用该枚举类的一个静态常量对象, 并且所有的枚举字段都用常量数组$VALUES来存储。values()方法内则调用Object的clone()方法, 参照$VALUES数组对象复制一个新的数组, 新数组会有所有的枚举字段。

内部类

import java.util.*;
import java.io.*;public class CFRDecompilerDemo {int x = 3;/*** 内部类* option: --removeinnerclasssynthetics false*/public void innerClassTest() {new InnerClass().getSum(6);}public class InnerClass {public int getSum(int y) {x += y;return x;}}
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --removeinnerclasssynthetics false

首先我们要明确, 上述innerClassTest()方法中的this是外部类当前对象的引用, 而InnerClass类中的this则是内部类当前对象的引用。编译过程中, 编译器会自动在内部类定义一个外部类的常量引用this$0, 并且在内部类的构造器中初始化this$0, 当外部类访问内部类时, 会把当前外部类的对象引用this传给内部类的构造器用于初始化, 这样内部类就能通过所持有的外部类的对象引用, 来访问外部类的所有公有及私有成员。

泛型擦除

/*** 泛型擦除* option: */
public void genericEraseTest() {List<String> list =  new ArrayList<String>();
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class

在JVM中没有泛型这一概念,  只有普通方法和普通类, 所有泛型类的泛型参数都会在编译时期被擦除, 所以泛型类并没有自己独有的Class类对象比如List<Integer>.class, 而只有List.class对象。

增强for循环

/*** 增强for循环* option: --collectioniter false*/
public void forLoopTest() {String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};  List<String> list =  Arrays.asList(qingshanli);for (Object s : list) {System.out.println(s);}
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --collectioniter false

很明显, 增强for循环的底层其实还是通过迭代器来实现的, 这也就解释了为什么增强for循环中不能进行增删改操作。

lambda表达式

/*** lambda表达式* option: --decodelambdas false*/
public void lambdaTest() {String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};  List<String> list =  Arrays.asList(qingshanli);// 使用lambda表达式以及函数操作list.forEach((str) -> System.out.print(str + "; "));// 在JDK8中使用双冒号操作符list.forEach(System.out::println);
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --decodelambdas false

这里笔者经验尚浅, 关于lambda表达式的实现原理暂不做阐述, 以免误人子弟, 欢迎有兴趣的读者在留言区一起讨论。

try-with-resources语句

/*** try-with-resources语句* option: --tryresources false*/
public void tryWithResourcesTest() throws IOException {try (final StringWriter writer = new StringWriter();final StringWriter writer2 = new StringWriter()) {writer.write("This is qingshanli1");writer2.write("this is qingshanli2");}
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --tryresources false

在JDK7之前, 如IO流、数据库连接等资源用完后, 都是通过finally代码块来释放资源。而try-with-resources语法糖则帮我们省去了释放资源这一操作, 编译器在解语法糖阶段时会将它还原成原始的语法结构。

JDK10的局部变量类型推断

/*** 局部变量类型推断, JDK10开始支持* option: 不需要参数*/
public void varTest() {//初始化局部变量  var string = "qingshanli";//初始化局部变量  var stringList = new ArrayList<String>();stringList.add("九幽阴灵,诸天神魔,以我血躯,奉为牺牲。");stringList.add("三生七世,永堕阎罗,只为情故,虽死不悔!");stringList.add("blog:http://www.cnblogs.com/qingshanli/");//增强for循环的索引for (var s : stringList){System.out.println(s);}//传统for循环的局部变量定义for (var i = 0; i < stringList.size(); i++){System.out.println(stringList.get(i));}
}

JDK10环境下编译: /home/qingshanli/Downloads/jdk-10.0.2/bin/javac CFRDecompilerDemo.java

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --collectioniter false

可以看出, 局部变量类型推断其实也是一个语法糖。在编译过程的解语法糖阶段, 会使用变量真正的类型来替代var类型。所以java由始至终是一种强类型语言, java中的var和弱类型语言JavaScript中的var是完全不一样的, 例如下图 var i = "10" - 6 这样的语法运算在JavaScript中可以的, 而在Java语言中则不被允许。

另外目前已知的允许使用var声明变量的几个场景有初始化局部变量、增强for循环的索引、传统for循环的局部变量定义。而诸如方法的形参、构造器的形参、方法的返回值类型、对象的成员变量、只进行定义而不初始化的变量等则不支持这种用法。对于后面的几种不支持, 我的猜想是因为它们会被外部访问而导致充满了不确定性, 举个栗子, 比如对象的成员变量X, 被对象A访问并赋值ArrayList类型, 被对象B访问并赋值HashMap类型, 那么问题来了, 对象A和对象B都是同一个类的实例, 这就产生了冲突, 此时虚拟机又如何区分这个对象的成员变量X到底是什么类型呢?

源代码

import java.util.*;
import java.io.*;public class CFRDecompilerDemo {int x = 3;/*** 字符串拼接* option: --stringbuilder false*/public void stringBuilderTest(int end) {char[] foo = new char[]{'@', 'a', '*'};char ch;int x = 0;while ((ch = foo[++x]) != '*') {System.out.println("" + x + ": " + ch);}}/*** 条件编译* option: 不需要参数*/public void ifCompilerTest() {if(false) {System.out.println("false if");}else {System.out.println("true else");}}/*** 断言, JDK1.4开始支持* option: --sugarasserts false*/public void assertTest(String s) {assert (!s.equals("Fred"));System.out.println(s);}/*** 枚举与Switch语句* option: --decodeenumswitch false*/public int switchEnumTest(EnumTest e) {switch (e) {case FOO:return 1;case BAP:return 2;}return 0;}/** * 字符串与Switch语句* option: --decodestringswitch false*/public int switchStringTest(String s) {switch (s) {default:System.out.println("Test");break;case "BB":  // BB and Aa have the same hashcode.return 12;case "Aa":case "FRED":return 13;}System.out.println("Here");return 0;}/*** 可变参数* option: --arrayiter false*/public void varargsTest(String ... arr) {for (String s : arr) {System.out.println(s);}}/*** 自动装箱/拆箱* option: --sugarboxing false*/public Double autoBoxingTest(Integer i, Double d) {return d + i;}/*** 枚举, JDK1.5开始支持* option: --sugarenums false*/public enum EnumTest {FOO,BAR,BAP}/*** 内部类* option: --removeinnerclasssynthetics false*/public void innerClassTest() {new InnerClass().getSum(6);}public class InnerClass {public int getSum(int y) {x += y;return x;}}/*** 泛型擦除* option: */public void genericEraseTest() {List<String> list =  new ArrayList<String>();}/*** 增强for循环* option: --collectioniter false*/public void forLoopTest() {String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};  List<String> list =  Arrays.asList(qingshanli);for (Object s : list) {System.out.println(s);}}/*** lambda表达式* option: --decodelambdas false*/public void lambdaTest() {String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};  List<String> list =  Arrays.asList(qingshanli);// 使用lambda表达式以及函数操作list.forEach((str) -> System.out.print(str + "; "));// 在JDK8中使用双冒号操作符list.forEach(System.out::println);  }/*** try-with-resources语句* option: --tryresources false*/public void tryWithResourcesTest() throws IOException {try (final StringWriter writer = new StringWriter();final StringWriter writer2 = new StringWriter()) {writer.write("This is qingshanli1");writer2.write("this is qingshanli2");}}/*** 局部变量类型推断, JDK10开始支持* option: 不需要参数*/public void varTest() {//初始化局部变量  var string = "qingshanli";//初始化局部变量  var stringList = new ArrayList<String>();stringList.add("九幽阴灵,诸天神魔,以我血躯,奉为牺牲。");stringList.add("三生七世,永堕阎罗,只为情故,虽死不悔!");stringList.add("blog:http://www.cnblogs.com/qingshanli/");//增强for循环的索引for (var s : stringList){System.out.println(s);}//传统for循环的局部变量定义for (var i = 0; i < stringList.size(); i++){System.out.println(stringList.get(i));}}
}

参数资料

Java的编译原理

Java代码的编译与反编译那些事儿-HollisChuang's Blog

我反编译了Java 10的本地变量类型推断-HollisChuang's Blog

Java中的Switch对整型、字符型、字符串型的具体实现细节-HollisChuang's Blo...

一些防止java代码被反编译的方法

from: https://www.cnblogs.com/qingshanli/p/9375040.html

浅析java中的语法糖相关推荐

  1. Java 中的语法糖 (Syntactic Sugar)

    语法糖(Syntactic Sugar),也叫糖衣语法,是英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语.指的是,在计算机语言中添加某种语法,这种语法能使程序员更方便 ...

  2. Java 中的语法糖,真甜。

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 我们在日常开发中经常会使用到诸如泛型.自动拆箱和装箱.内部 ...

  3. Java中的语法糖及反编译工具

    雪压枝头低,虽低不着泥 壹·Java中的反编译工具 贰·常见的12"颗"语法糖 switch forEach lambda if条件编译 变长参数 enum assert断言 tr ...

  4. 阿里云面试:什么是语法糖?Java中有哪些语法糖?

    点击上方"芋道源码",选择"设为星标" 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 10:33 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | ...

  5. 什么是语法糖?Java中有哪些语法糖?

    本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,了解这些语法糖背后的原理 1 语法糖 语 ...

  6. 浅析Java中的final关键字

    浅析Java中的final关键字 谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来 ...

  7. 【转】浅析Java中的final关键字

    谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字.另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法. ...

  8. java里面的语法糖(糖衣语法)

    语法糖是一种用来方便程序员代码开发的手段,简化程序开发,但是不会提供实质性的功能改造,但可以提高开发效率或者语法的严谨性或者减少编码出错的机会. 总而言之,语法糖可以看作是编译器实现的一种小把戏. 语 ...

  9. 浅析Java中的Steam流

    Stream流 文章目录 Stream流 1. 集合遍历 2. 流式思想 3. Stream流 3.1 概念 3.2 流的获取 3.3 forEach 3.4 filter 3.5 map 3.6 c ...

最新文章

  1. 大前端开发者需要了解的基础编译原理和语言知识
  2. 金融系列4《基本指令》
  3. 《Java程序设计》第五周学习总结
  4. 在GridView中使用Cache
  5. CodeForces - 786BLegacy——线段树建图+最短路
  6. 介绍Unity中相机的投影矩阵与剪切图像、投影概念
  7. 移动端mintUI mt-datetime-picker 组件使用详解
  8. HTML draggable 属性
  9. 计算机保研夏令营预推免
  10. 试卷自动生成工具使用说明
  11. 转:C# 中 MSCHART 饼状图显示百分比
  12. steam使用技巧2
  13. docker | 基于 WSL2 在 Windows 下使用 docker
  14. win10+ubuntu双系统之三步彻底删除ubuntu系统
  15. 无人机无线Mesh自组网,CV5200远距离WiFi模组,实时通信传输技术
  16. 【UI】产品设计之什么是色彩情绪
  17. 硬件特征码已达到最大上限_监控录像机“资源不足”或“达到上限” 的原因及解决方法!...
  18. Simulink 水轮机初始化设置
  19. CAS1233850-90-2|2-(3-三氟甲基苯基)咪唑[4,5f][1,10]邻菲啰啉|分子式C20H11F3N4|分子量364.32
  20. Excel统一将多种段落标记号替换为黑方块的操作

热门文章

  1. multi-CPU, multi-core and hyper-thread--转
  2. 可重入锁ReentrantLock--转载
  3. 强监管下 协议支付会是互金平台救命稻草?(协议支付是代扣协议的升级版)
  4. RocketMQ控制台安装教程
  5. 不是你无法入门自然语言处理(NLP),而是你没找到正确的打开
  6. redis php 持久化,详解Redis RDB持久化、AOF持久化,
  7. Python数据结构——array
  8. jvm性能调优 - 03垃圾回收机制
  9. APM - 使用JavaAgent+Javassit 插桩C3P0
  10. JVM - 列出JVM默认参数及运行时生效参数