语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某种语法,这种语法对语言本身功能来说没有什么影响,只是为了方便程序员的开发,提高开发效率。说白了,语法糖就是对现有语法的一个封装。
Java作为一种与平台无关的高级语言,当然也含有语法糖,这些语法糖并不被虚拟机所支持,在编译成字节码阶段就自动转换成简单常用语法。一般来说Java中的语法糖主要有以下几种:
1. 泛型与类型擦除
2. 自动装箱与拆箱,变长参数、
3. 增强for循环
4. 内部类与枚举类
泛型与类型擦除
Java语言并不是一开始就支持泛型的。在早期的JDK中,只能通过Object类是所有类型的父类和强制类型转换来实现泛型的功能。强制类型转换的缺点就是把编译期间的问题延迟到运行时,JVM并不能为我们提供编译期间的检查。
Java 泛型的参数只可以代表类,不能代表个别对象。由于 Java 泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型。Java 编译器在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快。Java 允许对个别泛型的类型参数进行约束,包括以下两种形式(假设 T 是泛型的类型参数,C 是一般类、泛类,或是泛型的类型参数):T 实现接口 I 。T 是 C ,或继承自 C 。一个泛型类不能实现Throwable接口。
泛型在Java中要求,必须是Object类型的。
在JDK1.5中,Java语言引入了泛型机制。但是这种泛型机制是通过类型擦除来实现的,即Java中的泛型只在程序源代码中有效(源代码阶段提供类型检查),在编译后的字节码中自动用强制类型转换进行替代。也就是说,Java语言中的泛型机制其实就是一颗语法糖,相较与C++、C#相比,其泛型实现实在是不那么优雅。
在编译后的字节码文件中,就已经被替换为原来的原生类型(裸类型),并且在相应的地方插入了强制类型转换,因此对于运行期的java语言来说,ArrayList 与ArrayList就是同一个类。所以说泛型技术实际上就是java语言的一颗语法糖,java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。
1 /**
2 * 在源代码中存在泛型3 */
4 public static voidmain(String[] args) {5 Map map = new HashMap();6 map.put("hello","你好");7 String hello = map.get("hello");8 System.out.println(hello);9 }
当上述源代码被编译为class文件后,泛型被擦除且引入强制类型转换
1 public static voidmain(String[] args) {2 HashMap map = new HashMap(); //类型擦除
3 map.put("hello", "你好");4 String hello = (String)map.get("hello");//强制转换
5 System.out.println(hello);6 }
1 public classGenericTypes {2
3 public static void fun1(Listlist) {4 System.out.println("方法一");5
6 }7
8 public static voidfun1(List list) {9 System.out.println("方法二");10
11 }12
13 }
上面使用了泛型的重载能否编译执行呢?
这是不能被执行的,Erasure of method fun1(List) is the same as another method in type GenericTypes 会报错误。这是因为参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这俩个方法的特征签名变得一模一样。
但如果给上述俩个方法加不同的方法返回值, 居然重载成功。(方法重载的要求就是两同一不同:同一类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有任何关系。)
原因:下面实例中的重载当然不是根据返回值来确定的,之所以这次能编译和执行成功,是因为俩个方法加入了不同的返回值后才能共存在一个class文件中。因为方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在class文件格式中,只要描述符不是完全一致的俩个方法就可以共存。也就是说俩个方法如果有相同的名称和特征签名,但返回值不同,那他们也是可以合法的共存在同一个Class文件中的。
public static String fun1(Listlist) {
System.out.println("方法一");
public static int fun1(List list) {
System.out.println("方法二");}
自动装箱与拆箱
Java中的自动装箱与拆箱指的是基本数据类型与他们的包装类型之间的相互转换。
我们知道Java是一门面向对象的语言,在Java世界中有一句话是这么说的:“万物皆对象”。但是Java中的基本数据类型却不是对象,他们不需要进行new操作,也不能调用任何方法,这在使用的时候有诸多不便。因此Java为这些基本类型提供了包装类,并且为了使用方便,提供了自动装箱与拆箱功能。自动装箱与拆箱在使用的过程中,其实是一个语法糖,内部还是调用了相应的函数进行转换。
下面代码演示了自动装箱和拆箱功能
1 public static voidmain(String[] args) {2 Integer a = 1;3 int b = 2;4 int c = a +b;5 System.out.println(c);6 }
经过编译后,代码如下
1 public static voidmain(String[] args) {2 Integer a = Integer.valueOf(1); //自动装箱
3 byte b = 2;4 int c = a.intValue() + b;//自动拆箱
5 System.out.println(c);6 }
变长参数
所谓变长参数,就是方法可以接受长度不定确定的参数
变长参数特性是在JDK1.5中引入的,使用变长参数有两个条件,一是变长的那一部分参数具有相同的类型,二是变长参数必须位于方法参数列表的最后面。变长参数同样是Java中的语法糖,其内部实现是Java数组。
1 public classVarargs {2 public static voidprint(String... args) {3 for(String str : args){4 System.out.println(str);5 }6 }7
8 public static voidmain(String[] args) {9 print("hello", "world");10 }11 }
编译为class文件后如下,从中可以很明显的看出变长参数内部是通过数组实现的
1 public classVarargs {2 publicVarargs() {3 }4
5 public static voidprint(String... args) {6 String[] var1 =args;7 int var2 =args.length;8 //增强for循环的数组实现方式
9 for(int var3 = 0; var3 < var2; ++var3) {10 String str =var1[var3];11 System.out.println(str);12 }13
14 }15
16 public static voidmain(String[] args) {17 //变长参数转换为数组
18 print(new String[]{"hello", "world"});19 }20 }
增强for循环
增强for循环与普通for循环相比,功能更强并且代码更简洁
增强for循环的对象要么是一个数组,要么实现了Iterable接口。这个语法糖主要用来对数组或者集合进行遍历,其在循环过程中不能改变集合的大小。
1 public static voidmain(String[] args) {2 String[] params = new String[]{"hello","world"};3 //增强for循环对象为数组
4 for(String str : params){5 System.out.println(str);6 }7
8 List lists = Arrays.asList("hello","world");9 //增强for循环对象实现Iterable接口
10 for(String str : lists){11 System.out.println(str);12 }13 }
编译后的class文件为
1 public static voidmain(String[] args) {2 String[] params = new String[]{"hello", "world"};3 String[] lists =params;4 int var3 =params.length;5 //数组形式的增强for退化为普通for
6 for(int str = 0; str < var3; ++str) {7 String str1 =lists[str];8 System.out.println(str1);9 }10
11 List var6 = Arrays.asList(new String[]{"hello", "world"});12 Iterator var7 =var6.iterator();13 //实现Iterable接口的增强for使用iterator接口进行遍历
14 while(var7.hasNext()) {15 String var8 =(String)var7.next();16 System.out.println(var8);17 }18
19 }
内部类
内部类就是定义在一个类内部的类
Java语言中之所以引入内部类,是因为有些时候一个类只在另一个类中有用,我们不想让其在另外一个地方被使用。内部类之所以是语法糖,是因为其只是一个编译时的概念,一旦编译完成,编译器就会为内部类生成一个单独的class文件,名为outer$innter.class。
1 public classOuter {2
3 classInner{4
5 }6 }
使用javac编译后,生成两个class文件Outer.class和Outer$Inner.class,其中Outer$Inner.class的内容如下:
1 classOuter$Inner {2 Outer$Inner(Outer var1) {3 this.this$0 =var1;4 }5 }
内部类分为四种:成员内部类、局部内部类、匿名内部类、静态内部类,每一种都有其用法,这里就不介绍了
枚举类型
枚举类型就是一些具有相同特性的类常量
java中类的定义使用class,枚举类的定义使用enum。在Java的字节码结构中,其实并没有枚举类型,枚举类型只是一个语法糖,在编译完成后被编译成一个普通的类。这个类继承java.lang.Enum,并被final关键字修饰。
public enum Fruit {
APPLE,ORINGE
}
使用jad对编译后的class文件进行反编译后得到:
1 //继承java.lang.Enum并声明为final
2 public final class Fruit extendsEnum3 {4
5 public staticFruit[] values()6 {7 return(Fruit[])$VALUES.clone();8 }9
10 public staticFruit valueOf(String s)11 {12 return(Fruit)Enum.valueOf(Fruit, s);13 }14
15 private Fruit(String s, inti)16 {17 super(s, i);18 }19 //枚举类型常量
20 public static finalFruit APPLE;21 public static finalFruit ORANGE;22 private static final Fruit $VALUES[];//使用数组进行维护
23
24 static
25 {26 APPLE = new Fruit("APPLE", 0);27 ORANGE = new Fruit("ORANGE", 1);28 $VALUES = (newFruit[] {29 APPLE, ORANGE30 });31 }32 }