概述

编译器是一种计算机程序, 它主要的目的是将便于人编写、阅读、维护的高级计算机语言所写的源代码程序, 翻译为计算机能解读、运行的低阶机器语言的程序, 即可执行文件。而 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代码被反编译的方法

作者:张小凡
出处:https://www.cnblogs.com/qingshanli/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】。

浅析java中的语法糖的更多相关文章

  1. Java 中的语法糖

    百度百科对语法糖的定义 语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这 ...

  2. [转]谈谈Java中的语法糖

    *该博客转自 http://blog.csdn.net/danchu/article/details/54986442 语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某 ...

  3. JVM(二):Java中的语法糖

    JVM(二):Java中的语法糖 上文讲到在语义分析中会对Java中的语法糖进行解糖操作,因此本文就主要讲述一下Java中有哪些语法糖,每个语法糖在解糖过后的原始代码,以及这些语法糖背后的逻辑. 语法 ...

  4. 【Java基础】Java中的语法糖

    目录 Java中的语法糖 switch对String和枚举类的支持 对泛型的支持 包装类型的自动装箱和拆箱 变长方法参数 枚举 内部类 条件编译 断言 数值字面量 for-each try-with- ...

  5. Java中的语法糖

    一.范型 1. C#和Java范型的区别 在C#中范型是切实存在的,List<int>和List<String>就是两种不同的类型,它们在系统运行期间生成,有自己的虚方法表和类 ...

  6. Java 中的语法糖(7/15整个周六上午总结)

    语法糖定义指的是,在计算机语言中添加某种语法,这种语法能使程序员更方便的使用语言开发程序,同时增强程序代码的可读性,避免出错的机会:但是这种语法对语言的功能并没有影响.Java中的泛型,变长参数,自动 ...

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

    我把自己以往的文章汇总成为了 Github ,欢迎各位大佬 star https://github.com/crisxuan/bestJavaer 我们在日常开发中经常会使用到诸如泛型.自动拆箱和装箱 ...

  8. 浅析Java中的final关键字

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

  9. [转载]浅析Java中的final关键字

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

随机推荐

  1. 【Netty4】深入学习Netty

    Netty is an asynchronous event-driven network application framework  for rapid development of mainta ...

  2. mac下mysql的卸载和安装

    1. mysql的卸载 1 sudo rm /usr/local/mysql 2 sudo rm -rf /usr/local/mysql* 3 sudo rm -rf /Library/Startu ...

  3. 网络学习笔记(三):HTTP缓存

      HTTP缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,合理的使用缓存可以有效的提升web性能.   浏览器将js文件.css文件.图片等资源缓存,当下次请求这些资源时,可以不发送网络请 ...

  4. node.js的异步I/O、事件驱动、单线程

    nodejs的特点总共有以下几点 异步I/O(非阻塞I/O) 事件驱动 单线程 擅长I/O密集型,不擅长CPU密集型 高并发 下面是一道很经典的面试题,描述了node的整体运行机制,相信很多人都碰到了 ...

  5. Java多线程(2)线程锁

    多线程访问同一个资源进行读写操作,就很容易出一些问题(比如我们常见的读者写者,生产者消费者模型)所以我们会选择对他们设置信号量或者加锁,来限制同一个时刻只有一个线程对某个对象进行操作. 多线程是一个蛮 ...

  6. C++ luogu1352没有上司的舞会 from_树形DP

    luogu1352没有上司的舞会 分析(树形DP模板题): 没学树形DP的,看一下. 把该题抽象到一颗树中,设i的下属就是他的儿子,则有两种情况: 如果i参加,他的儿子就不能参加. 如果i不参加,他的 ...

  7. POJ 2449:Remmarguts' Date(A* + SPFA)

    题目链接 题意 给出n个点m条有向边,源点s,汇点t,k.问s到t的第k短路的路径长度是多少,不存在输出-1. 思路 A*算法是启发式搜索,通过一个估价函数 f(p) = g(p) + h(p) ,其 ...

  8. HDU 3183:A Magic Lamp(RMQ)

    http://acm.hdu.edu.cn/showproblem.php?pid=3183 题意:给出一个数,可以删除掉其中m个字符,要使得最后的数字最小,输出最后的数字(忽略前导零). 思路:设数 ...

  9. 50行Python代码,教你获取公众号全部文章

    > 本文首发自公众号:python3xxx 爬取公众号的方式常见的有两种 - 通过搜狗搜索去获取,缺点是只能获取最新的十条推送文章 - 通过微信公众号的素材管理,获取公众号文章.缺点是需要申请自 ...

  10. Altium Designer设计PCB--如何增大电源地的线宽

    笑话: 看见楼下老大爷在下棋,我看了一会儿,跟大爷说:大爷,你che没了. 大爷一脸不屑:小朋友,那叫ju. 然后我静静地在那看了两个小时. 对完棋,大爷起身要走. 我说:大爷,我刚才说的不是你的棋, ...