语法糖(Syntactic Sugar),也叫糖衣语法,是英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语。指的是,在计算机语言中添加某种语法,这些语法糖虽然不会对语言的功能产生任何影响,却能使程序员更方便的使用语言开发程序,同时增强程序代码的可读性,避免出错的机会。但是如果只是大量添加和使用语法糖,却不去了解他,容易产生过度依赖,从而无法看清语法糖的糖衣背后,程序代码的真实面目。

总而言之,语法糖可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能会使得效率“大提升”,但我们也应该去了解这些“小把戏”背后的真是世界,这样才能更好地利用它们,而不是被它们迷惑。

下面我们就 泛型,自动拆箱/装箱、遍历循环和条件编译做简单的介绍和分析。了解他们背后的真相。

1、泛型与类型擦除

泛型是 JDK 1.5 的一项新增类型,它的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数可以用在类、接口和方法的创建中,分别成为泛型类、泛型接口和泛型方法。

泛型思想早在 C++ 语言的模板(Template) 就开始生根发芽。在 Java 语言还处于没有出现泛型的版本的时候,只能通过 Object 类是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。 例如,在 HashMap 的存取中,get() 方法返回的就是一个 Object 对象,由于 Java 语言所有的类型都继承于 java.lang.Object , 所以 Object 转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机知道这个Object 到底是什么类型的对象。在编译期间,编译器无法检查这个 Object 的强制转型是否成功,如果仅仅依赖于程序员去保障这项操作的正确性,许多 ClassCastException 的风险就会转嫁到程序运行期中。

但是,泛型技术在 C# 和 Java 之中的使用方式看似相同,在实现上却有着根本性的分歧,C# 的泛型无论是在源码中、编译后的 IL 中(Intermediate Language , 中间语言买这时候泛型符是一个占位符),或是运行期的 CLR 中,都是切实存在的, List<int> 和 List<String> 就是两个不同的类型,它们在运行期间,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。

Java 中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type , 也成为裸类型)了,并且在相应的地方插入了强制类型转换代码,因此,对于运行期的 Java 语言来说,ArrayList<Integer> 和 ArrayList<String> 就是同一个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。

下面我们来看看一段简单的泛型擦除的例子:

  1. public static void main(String[] args) {
  2. Map<String, String> map = new HashMap<String, String>();
  3. map.put("hello", "你好");
  4. map.put("how are you?", "聊天止于呵呵");
  5. System.out.println(map.get("hello"));
  6. System.out.println(map.get("how are you?"));
  7. }

上面这段代码经过编译后,我们可以借助反编译软件 XJad 来查看编译后的代码,如下:

  1. public static void main(String args[])
  2. {
  3. Map map = new HashMap();
  4. map.put("hello", "你好");
  5. map.put("how are you?", "聊天止于呵呵");
  6. System.out.println((String)map.get("hello"));
  7. System.out.println((String)map.get("how are you?"));
  8. }

可以看到,所有的泛型代码都不见了,程序又变回了 Java 泛型出现之前的写法,泛型类型都变回了原生类型。也因为这个原因,当使用泛型类型做方法参数的时候,无法根据泛型来实现重载。如下列代码:

  1. public static int method(List<String>list){
  2. return 1;
  3. }
  4. public static int method(List<Integer> list){
  5. return 2;
  6. }

编译器会拒绝进行编译。

2、自动装箱、拆箱与遍历循环

从技术上来讲,自动装箱、拆箱与遍历循环(Foreach循环) 这些语法糖,无论是从实现上还是从思想上都不能和放行相比,两者的难度和深度都有很大的差距。我们通过代码来看看这些语法糖的本质:

  1. public static void main(String[] args) {
  2. List<Integer> list = Arrays.asList(1,2,3,4);
  3. //如果是在 JDK 1.7 还有另外一颗语法糖
  4. // List<Integer> list = [1,2,3,4];
  5. int sum =0;
  6. for(int i : list){
  7. sum+=i;
  8. }
  9. System.out.print(sum);
  10. }

上面代码一共包含了 泛型、自动装箱、自动拆箱、遍历循环与变长参数 5 种语法糖。经过反编译后得到如下代码:

  1. public static void main(String args[]) {
  2. List list = Arrays.asList(new Integer[] {
  3. Integer.valueOf(1),
  4. Integer.valueOf(2),
  5. Integer.valueOf(3),
  6. Integer.valueOf(4) });
  7. int sum = 0;
  8. for (Iterator iterator = list.iterator(); iterator.hasNext();) {
  9. int i = ((Integer) iterator.next()).intValue();
  10. sum += i;
  11. }
  12. System.out.print(sum);
  13. }

可以看到,

自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法。如本例中的 Integer.valueof() 与 Integer.intValue() 方法,而遍历循环则把代码还原成了 迭代器的实现,这也是为何遍历循环需要被遍历的类实现 Iterable 接口的原因。

变长参数则在调用的时候变成了一个数组类型的参数,在变长类型出现之前,程序员就是使用数组来完成类似功能的。

有的时候,不节制的或不正确的使用装箱和拆箱会给我们带了极大的困扰,如下列代码:

  1. public static void main(String[] args) {
  2. Integer a = 1;
  3. Integer b = 2;
  4. Integer c = 3;
  5. Integer d = 3;
  6. Integer e = 321;
  7. Integer f = 321;
  8. Long g = 3L;
  9. System.out.println(c == d);
  10. System.out.println(e == f);
  11. System.out.println(c == (a + b));
  12. System.out.println(c.equals(a + b));
  13. System.out.println(g == (a + b));
  14. System.out.println(g.equals(a + b));
  15. }

读完上面代码,不妨自己思考一下,这六条输出语句的输出结果是什么? 这些语法糖解除之后的参数会是什么样子的呢?

下面来揭晓答案吧!

首先第一个:c ==d,因为“ ==” 号是用来判断两个引用指向的是否是同一个对象,因为在Integer 在构造对象的时候,128内的整数做了一个缓存优化,构造相同数值的代码会指向同一个对象,所以这个输出是true。

第二个, e == f ,类似于第一条,只是因为 e 与 f 不在优化范围之内,所以输出是 false。

第三个,c == ( a + b) , Java 中 == 号只有遇见运算符的情况下才会发生拆箱操作,这行代码被编译成 c.intValue() == a.intValue() + b.intValue(),比较的是值是否相等,所以输出结果为 true。

第四个,c.equals( a + b ),类似于上一条,会发生拆箱操作,编译为 c.equals(Integer.valueOf(a.intValue() + b.intValue())) ,输出结果为 true。

第五个,g == (a + b),类似于第三条,发生拆箱,同时因为有"=="号的存在,会发生强制类型转换, 被编译为  g.longValue() == (long)(a.intValue() + b.intValue())  ,输出结果为 true。

第六个,g.equals( a+b) ,类似于第四条,会发生拆箱操作,但是没有”==“号,不会放生强制类型转换。而第四条比较的都是Integer类型,而这里却是 Long 和 Integer 类型,equals 只能比较同种类型的对象,所以返回自然就是 false 了。

你做对了几道?

所以程序猿们做好少用,用者需谨慎!

3、条件编译

许多程序设计语言都提供了条件编译的途径,如C、C++ 中使用预处理器指示符(#ifdef)完成条件编译。C、C++ 的预处理器最初的任务是解决编译时的代码依赖关系(如#include),而在 Java 语言之中并没有使用预处理器,因为 Java 语言天然的编译方式(不一个个编译 Java 文件,而是将所有编译单元的语法树顶级结点输入到待处理列表后再进行编译,因此各个文件之间能够互相提供符号信息) 无需使用预处理器。

Java 语言也可以进行条件编译,方法就是使用条件为常量的 if 语句。如下面代码:

  1. public static void main(String[] args) {
  2. if(true){
  3. System.out.println("True");
  4. }else{
  5. System.out.println("False");
  6. }
  7. }

经过编译后会变成如下代码:

  1. public static void main(String args[])
  2. {
  3. System.out.println("True");
  4. }

而有的时候使用常量的判断语句会提示错误,被拒绝编译:

  1. while(false){
  2. System.out.println("True");
  3. }

Java 语言中条件编译的实现,也是 Java 语言的一颗语法糖,根据布尔常量的真假,编译器将会把分支中不成立的代码块消除掉,这一工作将在编译器接触语法糖阶段完成。由于这种条件编译的实现方式使用了 if 语句,所以它必须遵循最基本的Java 语法,只能写在方法体内部,因此它只能实现语句基本块级别的条件编译,而无法根据条件调整 Java 类的结构。

Java语言中还有其它的一些语法糖,如内部类、枚举类、断言语句、对枚举和字符串的switch 支持、try语句中定义和关闭资源等,读者可以跟踪 Javac源码、反编译 Class 文件等方式了解他们的本质实现。

深入理解java虚拟机(十二) Java 语法糖背后的真相的更多相关文章

  1. java虚拟机(十二)--可视化工具分析GC日志

    在上篇博客中,我们学习了Parallel.CMS.G1三种垃圾收集器的日志格式,本次我们通过工具去分析日志,会更加的直观 日志格式博客地址:java虚拟机(十一)--GC日志分析 GCeasy: 这是 ...

  2. java基础(十二 )-----Java泛型详解

    本文对java的泛型的概念和使用做了详尽的介绍. 概述 泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用. 什么是泛型?为什么要使用泛型? 泛型,即“参数化类型”.一提到 ...

  3. Java基础十二--多态是成员的特点

    Java基础十二--多态是成员的特点 一.特点 1,成员变量. 编译和运行都参考等号的左边. 覆盖只发生在函数上,和变量没关系. Fu f = new Zi();System.out.println( ...

  4. 实战Java虚拟机之二“虚拟机的工作模式”

    今天开始实战Java虚拟机之二:“虚拟机的工作模式”. 总计有5个系列 实战Java虚拟机之一“堆溢出处理” 实战Java虚拟机之二“虚拟机的工作模式” 实战Java虚拟机之三“G1的新生代GC” 实 ...

  5. Java虚拟机(二):JVM内存模型

    所有的Java开发人员可能会遇到这样的困惑?我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?该怎么解决呢?其实如果你经常解决服务器性能问题,那么这些问 ...

  6. “全栈2019”Java第九十二章:外部类与内部类成员覆盖详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  7. “全栈2019”Java第十二章:变量

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  8. “全栈2019”Java第二十二章:控制流程语句中的决策语句if-else

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  9. 《深入理解 Java 虚拟机》学习 -- Java 内存模型

    <深入理解 Java 虚拟机>学习 -- Java 内存模型 1. 区别 这里要和 JVM 内存模型区分开来: JVM 内存模型是指 JVM 内存分区 Java 内存模型(JMM)是指一种 ...

随机推荐

  1. Clustering Factor——索引的成本指标

    使用索引是我们面对海量数据搜索是一种常用的手段.通过有效的索引访问,可以使我们更快的访问到需要的数据,减少物理.逻辑IO,从而提高系统性能.在CBO时代,Oracle对于提交SQL的执行路径是有所选择 ...

  2. form中的input的redonly和disable区别

    Readonly和Disabled是用在表单中的两个属性,它们都能够做到使用户不能够更改表单域中的内容.但是它们之间有着微小的差别,总结如下: Readonly只针对input(text / pass ...

  3. Tkinter tkMessageBox

            Tkinter tkMessageBox: tkMessageBox模块用于显示在您的应用程序的消息框.此模块提供了一个功能,您可以用它来显示适当的消息  tkMessageBox模块 ...

  4. **类的起源--type

    通过type类的实例化,创建新的类. #!/usr/bin/env python # Version = 3.5.2 def func(self): print('Hello,{}'.format(s ...

  5. LVM 逻辑卷管理

    简介: LVM ( Logical Volume Manager ) 逻辑卷管理 一.创建 LV 1.首先在你的虚拟机上添加一块新的硬盘用来做实验. 2.安装 lvm : yum -y install ...

  6. Python基础语法习题一

    Part 1 习题 1.简述编译型与解释型语言的区别,且分别列出你知道的哪些语言属于编译型,哪些属于解释型 2.执行 Python 脚本的两种方式是什么 3.Pyhton 单行注释和多行注释分别用什么 ...

  7. Makefile 自动搜索 c 和 cpp 文件, 并生成 .a 静态库文件

    最近 又弄linux 下的 .a 静态库编译, 于是想 做个 一劳永逸的Makefile, 经过一番折腾, 最后成功了 只需要 改两个 参数 就可以执行了(MYLIB 和 VPATH), 代码 如下: ...

  8. NormalMapping

    [NormalMapping] 法线贴图内的数据是法线,高度贴图内的数据是高度,不是一个东西.在ShaderLab中,UnpackNormal()分析的是法线贴图(注意不是高度贴图). 可以看到,在G ...

  9. Java网络编程小结 URLConnection协议处理器

    URL和URLConnection类 网络中的URL(Uniform Resource Locator)是统一资源定位符的简称.它表示Internet上某一资源的地址.通过URL我们可以访问Inter ...

  10. 我的MBTI性格测试

    写在前面: 很多人争论MBTI靠谱不靠谱.一个人的性格肯定不能只用这么几个维度就能描述的,一个人的性格也肯定不是通过这么几个问题就能测出来的,一个人的性格也肯定不是一成不变的,所以MBTI的准确度肯定 ...