语法糖(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. START WITH...CONNECT BY PRIOR详解

    START WITH...CONNECT BY PRIOR详解 START WITH...CONNECT BY PRIOR详解 ORACLE中的SELECT语句可以用START WITH...CONN ...

  2. OCR (Optical Character Recognition,光学字符识别)

    OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗.亮的模式确定其形状,然后用字符识别方法将形状翻译 ...

  3. 第六篇 Flask 中内置的 Session

    Flask中的Session非常的奇怪,他会将你的SessionID存放在客户端的Cookie中,使用起来也非常的奇怪 1. Flask 中 session 是需要 secret_key 的 from ...

  4. linux 远程连接服务器ftp命令整理

    Ftp命令的功能是在本地机和远程机之间传送文件.该命令的一般格式如下: $ ftp 主机名/IP ftp将给出提示符,等待用户输入命令: $ ftp ftp > 最常用的命令有: ls 列出远程 ...

  5. C语言调用汇编

    程序的入口是main,在main里调用汇编的函数. 首先要解决怎么定义函数的问题 在C语言中,要extern 一个函数声明即可,然后这个函数在汇编里面实现. 在汇编里面,用EXPORT 把C语言定义的 ...

  6. PDO execute 执行错误

    PHP   PDO execute 执行错误,原因分析 $dbcon->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);或者 $db ...

  7. oracle常用函数总结(二)

    之前也有写过“oracle常用函数总结(一)”,为了尽量找全常见oracle函数,笔者特意查找了相关资料来作为参考,下边给大家罗列出来,部分和之前有重复的,希望能帮到大家! 列举了31个函数和1个分组 ...

  8. 服务器报警邮件发送到QQ邮箱,但是被系统拦截

    # 为啥发送到QQ邮箱呢?因为QQ邮箱可以和微信关联,第一时间收到消息 if 没有设置白名单,然后被拦截当做垃圾邮件了: 设置白名单就可以了,这样的状态特征是: 邮件在垃圾箱里面能找到 elif 还是 ...

  9. 团队合作的Ground Rules

    在每个Sprint中,我们会为Sprint的确定DOD(Definition of Done,完成的定义).在团队成员合作的过程中,我们也需要定义合作规则,这就是Ground rules,就像小学生守 ...

  10. 自动补齐flexselect+级联下拉框案例

    在开发web应用时,经常遇到类似省市区级联下拉框操作,即选中省份自动级联加载该省份所有的市,选中市自动级联加载该市所有的区:假设省市区的数据量很大,此时用户想选中某市,因而要从上往下查找,可能半天都找 ...