语法糖(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. IDA Pro 权威指南学习笔记(十三) - 基本代码转换

    IDA提供的代码转换包括: 1.将数据转换为代码 2.将代码转换为数据 3.指定一个指令序列为函数 4.更改现有函数的起始或结束地址 5.更改指令操作数的显示格式 代码显示选项 通过 Options ...

  2. springcloud(二) eureka的使用

    上一节讲到order微服务是通过rest调用user微服务的地址.但是,user微服务的地址是写死的, 如果user微服务集群的话,那么order微服务该如何调用呢?这个时候注册中心该上场了 演示eu ...

  3. C# 设计模式-单例模式(Singleton)

    所谓单例模式即所谓的一个类只能有一个实例,说白了,也就是类只能在内部实例一次,然后提供这一实例,外部无法对此类实例化. 单例模式的特点: 1.只能有一个实例: 2.只能自己创建自己的唯一实例: 3.必 ...

  4. Python实践练习:strip()的正则表达式版本

    题目: 写一个函数,它接受一个字符串,做的事情和 strip()字符串方法一样.如果只传入了要去除的字符串,没有其他参数,那么就从该字符串首尾去除空白字符.否则,函数第二个参数指定的字符将从该字符串中 ...

  5. Shiro的Subject和Sessoin的创建

    之前要先了解Session的来源Shiro session和Spring session一样吗? 创建Subject的位置 AbstractShiroFilter . doFilterInternal ...

  6. ruby 功力修炼

    建表 ActiveRecord::Schema.define do drop_table :hosts if table_exists? :hosts create_table :hosts do | ...

  7. SignalR web实时同步 消息推送 广播

    源码:https://github.com/SignalR/SignalR demo:http://download.csdn.net/download/qq_21533697/9702791#com ...

  8. .net Core 2.0使用NLog

    最近研究了一下NLog的使用方式,简单的入了一下门. 实现的功能,对于不同的日志,进行不同的记录,分别有系统运行日志,和个人在程序中写的异常日志.发布之后放在了IIS上.进行查看日志的信息 参考了两篇 ...

  9. 使用Eclipse搭建JavaWeb开发环境的几个基本问题

    Eclipse搭建JavaWeb开发环境 eclipse是一个用于java程序开发的ide软件,tomcat是一个运行javaweb应用的服务器软件,使用eclipse开发javaweb应用的时,首要 ...

  10. windows系统mysql-5.7.19官方绿色版zip包安装教程

    环境: 系统环境 Windows 10 64位 mysql版本 5.7.19 一.万变不离的下载 下载页面:https://dev.mysql.com/downloads/mysql/ 点击 Down ...