语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
Java语法糖的种类
java7(含)之前的语法糖主要包括以下语法:
- 泛型
- 自动装箱和自动拆箱
- 遍历循环
- 变长参数
- 内部类
- 枚举变量
- 断言语句
- 枚举和字符串的switch支持
- try语句的定义和关闭资源
为了了解这些语法糖背后的事情,我们来分别举例子,通过反编译每个例子生成的字节码文件,看看语法糖的真正实现。
泛型
泛型是Java5之后才添加的一种新特性,它的本质是参数化类型的应用,也就是类、接口、方法所操作的类型被指定一个参数,分别被称为泛型类、泛型接口、泛型方法。
但是Java的泛型和C++/C#的泛型本质上是不一样的,C++/C#的泛型是一个占位符,在运行期都是存在的,比如List<Integer>
和List<String>
是不同的类型,在运行期都有自己的虚方法表和类型数据,这种实现才是真正的泛型,但是在Java里,泛型就是一个语法糖,只是在源码内存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Object)了,也就是说在运行期中,List<Integer>
和List<String>
在运行期就变成了List<Object>
和List<Object>
,就是相同的类型,这种泛型就是伪泛型了,这种也是泛型擦除。
因为Java中的所有类的父类都是Object,而Object可以强制转换为所有类,基于Java的这个特性,泛型才能实现。
我们举个例子,反编译一下以下代码:
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("hi", "你好");
map.put("how are you?", "最近好吗");
System.out.println(map.get("hi"));
System.out.println(map.get("how are you?"));
}
反编译之后的结果:
public static void main(String[] var0) {
HashMap var1 = new HashMap();
var1.put("hi", "你好");
var1.put("how are you?", "最近好吗");
System.out.println((String)var1.get("hi"));
System.out.println((String)var1.get("how are you?"));
}
通过泛型擦除的方式有些缺点,导致一些人对Java语言提供的伪泛型颇有微词,比如运行效率,因为强制转换会有一定的性能损失,暂且不说运行消息影响是否很大,但是有一点对我们开发者来说是很难接受的,就是就遇到重载的问题。
比如下面的代码:
public class GenericTypes{
public void method(List<String> list) {
System.out.println("invoke method List<String>list");
}
public void method(List<Integer> list) {
System.out.println("invoke method List<Integer>list");
}
}
对我们开发人员来说,希望这是个重载,毕竟很可能有这种需求,但是这段代码是编译不通过的,所以很尴尬。
还有更尴尬的事情,我们知道方法的重载不能根据返回值来确定的,但是下面的代码在JDK 1.6的环境确实能编译通过,并且能运行,得到我们要的效果,1.7之后已经优化了,无法编译通过。
public class GenericTypes{
public String method(List<String> list) {
System.out.println("invoke method List<String> list");
return "";
}
public Integer method(List<Integer> list) {
System.out.println("invoke method List<Integer> list");
return 1;
}
public static void main(String[] args) {
GenericTypes types = new GenericTypes();
types.method(new ArrayList<String>());
types.method(new ArrayList<Integer>());
}
}
虽然重载要求方法具备不同的特征签名,而返回值不在这个签名里,所以返回值不会参与重载的选择,但是对在Class文件格式中,只要描述符不是完全一致,就可以共存。
自动装箱和自动拆箱
编译之前的源码:
public static void main(String[] args) {
Integer i = 10;
i = i + 5;
System.out.println(i);
}
经过反编译之后的代码:
public static void main(String[] var0) {
Integer var1 = Integer.valueOf(10);
var1 = Integer.valueOf(var1.intValue() + 5);
System.out.println(var1);
}
可以看出,包装类型只要涉及到计算的操作,都要经过拆箱Integer.intValue()
和装箱操作Integer.valueOf()
,所以,如果数字经常变化时,最好使用非包装类型,减少拆箱和装箱操作,提高效率。
另外要说下自动装箱和拆箱是有一定的坑的,使用的时候要注意,举个例子:
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == a + b);
System.out.println(c.equals(a + b));
System.out.println(g == a + b);
System.out.println(g.equals(a + b));
}
读者可以先猜测一下答案,然后再上机进行验证一些下。
结果是不是有些出乎意料。
我们反编译一下这段代码,看看最终拆箱装箱之后的结果:
public static void main(String[] var0) {
Integer var1 = Integer.valueOf(1);//1
Integer var2 = Integer.valueOf(2);//2
Integer var3 = Integer.valueOf(3);//3
Integer var4 = Integer.valueOf(3);//4
Integer var5 = Integer.valueOf(321);//5
Integer var6 = Integer.valueOf(321);//6
Long var7 = Long.valueOf(3L);
System.out.println(var3 == var4);//a
System.out.println(var5 == var6);//b
System.out.println(var3.intValue() == var1.intValue() + var2.intValue());//c
System.out.println(var3.equals(Integer.valueOf(var1.intValue() + var2.intValue())));//d
System.out.println(var7.longValue() == (long)(var1.intValue() + var2.intValue()));//e
System.out.println(var7.equals(Integer.valueOf(var1.intValue() + var2.intValue())));//f
}
我们先把结果贴出来,一个一个的分析:
true
false
true
true
true
false
结果b为false很容易理解,==是对引用的比较,var5 == var6不一样很正常的,为什么a会是true呢?答案就是在装箱的方法valueOf()中,我们看下源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
原来和String一样,Integer也有一个cache,如果装箱结果命中了这个cache,直接从缓存池中拿一个对象,而默认情况下,这个缓存池的大小是-128到127,当然这个缓存池的大小可以进行调整,具体请看源码,这里就不具体介绍。
通过c的结果我们可以知道在数字的==前后如果有数字的计算,会进行拆箱操作,使用原始值进行比较,所以c为true。
d就不用说了,类型一样,值一样,肯定为true。
e和c类似,Long和Integer无法进行==,编译器就会做拆箱操作,进行数学层面的比较。
f是不同类型的equals的比较,肯定为true,这个也是equals的设计原则,想了解深的可以看下我的另外一篇文章。
好了,装箱和拆箱操作就先讲到这,原则是尽量避免使用自动装箱和拆箱。
条件编译
我们知道C/C++的条件编译是通过#ifdef来实现的,Java的编译器在编译阶段就会处理条件编译,举个例子:
public static void main(String[] args) {
if(true){
System.out.println("true");
}else {
System.out.println("false");
}
}
反编译之后的代码:
public static void main(String[] var0) {
System.out.println("true");
}
由于条件编译的实现方式使用了if语句,所以它必须遵循最基本的Java语法,只能写在方法体内部,隐藏它只能实现语句的基本块的选择性编译,没办法根据条件调整整个Java类的结构,不管怎样,条件编译能提高运行效率就行。
变长参数
变长参数就是参数个数不确定,但是变长参数有条件的:
1. 变长的那一部分参数一定要具有相同的类型。
2. 变长参数必须位于方法参数列表的最后面。
变长参数同样是Java中的语法糖,其内部实现是Java数组。
static void printStrs(String... strs){
for (String str:strs){
System.out.println(str);
}
}
public static void main(String[] args) {
printStrs("我", "是", "中", "国", "人");
}
class文件反编译只有的代码:
static void printStrs(String ... var0) {
String[] var1 = var0;
int var2 = var0.length;
for(int var3 = 0; var3 < var2; ++var3) {
String var4 = var1[var3];
System.out.println(var4);
}
}
public static void main(String[] var0) {
printStrs(new String[]{"我", "是", "中", "国", "人"});
}
变长参数让我们少写了一些代码,很方便。
增强的遍历循环
我们上一个例子就使用了普通数组的增强遍历循环,这让我们开发人员可以少些很多代码。
上面的例子,可以看出普通数组的遍历循环最终是普通的for循环。
那么Collection框架下的list或者set是怎么样个语法糖呢?我们举个例子:
List<String> list = Arrays.asList("我", "是", "中", "国", "人");
for(String str:list){
System.out.println(str);
}
class文件反编译之后的结果:
public static void main(String[] var0) {
List var1 = Arrays.asList(new String[]{"我", "是", "中", "国", "人"});
Iterator var2 = var1.iterator();
while(var2.hasNext()) {
String var3 = (String)var2.next();
System.out.println(var3);
}
}
可以看出Collection框架下的遍历循环最终还是使用了遍历器进行遍历。
值得提一下的是,如果我们使用了这个语法糖的遍历循环,就不能在循环体内删除元素,会报异常,这里就不做试验了。
内部类
内部类就是定义在一个类内部的类。
有些时候我们希望一个类只在另一个类中有用,不想让这类在其他地方被使用,这个时候就要用到内部类了,内部类之所以是语法糖,是因为其只是一个编译时的概念,一旦编译完成,编译器就会为内部类生成一个单独的class文件,名为outer$innter.class,比如Mybatis生成器生成的Example文件就使用了静态内部类。
public class Out {
class Inner{
}
}
我们看下编译之后的class文件会发现生成了两个class文件,一个是Out.class,一个是Out$Inner.class,内容如下:
public class Out {
}
class Out$Inner {
// $FF: synthetic field
final Out this$0;
Out$Inner(Out var1) {
this.this$0 = var1;
}
}
通过注释可以看出是编译器生成的成员,感兴趣的可以看下我的关于反射的系列文章。
内部类有四种:成员内部类、局部内部类、匿名内部类、静态内部类,每一种都有各自的用法,这里就不介绍了。
枚举类型
枚举类型可以看做是一组类型一样的常量,当一个变量有几种可能的取值时,可以将它定义为枚举类型。
枚举类型在编译之后会生成一个类,最终还是变为类。
public enum Vehicle{
CAR, BUS;
}
这个语法糖通过我们上面提到的网址已经做不到反编译了,使用jad 来进行反编译:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)+
// Source File Name: Vehicle.java
public final class Vehicle extends Enum
{
public static Vehicle[] values()
{
return (Vehicle[])$VALUES.clone();
}
public static Vehicle valueOf(String s)
{
return (Vehicle)Enum.valueOf(Vehicle, s);
}
private Vehicle(String s, int i)
{
super(s, i);
}
public static final Vehicle CAR;
public static final Vehicle BUS;
private static final Vehicle $VALUES[];
static+
{
CAR = new Vehicle("CAR", 0);
BUS = new Vehicle("BUS", 1);
$VALUES = (new Vehicle[] {
CAR, BUS
});
}
}
可以看出在Java的字节码结构中,其实并没有枚举类型,枚举类型只是一个语法糖,在编译完成后被编译成一个普通的类。这个类继承了java.lang.Enum,并被final关键字修饰,是不可被继承的类。
断言
声明一个简单的断言:
public static void main(String[] args) {
assert false : "执行错误";
}
运行时加上参数-ea,执行结果:
Exception in thread "main" java.lang.AssertionError: 执行错误
at Main.main(Main.java:6)
那么断言是怎么实现的呢?通过jad看下反编译后的结果:
public class Main
{
public Main()
{
}
public static void main(String args[])
{
if(!$assertionsDisabled)
throw new AssertionError("\u6267\u884C\u9519\u8BEF");
else
return;
}
static final boolean $assertionsDisabled = !Main.desiredAssertionStatus();
}
最终还是语法糖,有异常时抛出AssertionError。
枚举的switch支持
我们知道switch只支持int型以及编译器能自己转换为int的类型,在老版本中,枚举变量和下面要介绍的字符串是不支持switch语句的,但是1.5以上的却支持,它是怎么做到的呢?我们探究以下,借用上面的枚举类型的java代码,我们实现一个switch语句。
public class Main {
public static void main(String[] args) {
Vehicle vehicle = Vehicle.CAR;
switch (vehicle){
case BUS:
System.out.println("BUS");
break;
case CAR:
System.out.println("CAR");
break;
default:
System.out.println("default");
}
}
}
class反编译之后的结果:
public class Main
{
public Main()
{
}
public static void main(String args[])
{
Vehicle vehicle = Vehicle.CAR;
static class _cls1
{
static final int $SwitchMap$Vehicle[];
static+
{
$SwitchMap$Vehicle = new int[Vehicle.values().length];
try
{
$SwitchMap$Vehicle[Vehicle.BUS.ordinal()] = 1;
}
catch(NoSuchFieldError nosuchfielderror) { }
try
{
$SwitchMap$Vehicle[Vehicle.CAR.ordinal()] = 2;
}
catch(NoSuchFieldError nosuchfielderror1) { }
}
}
switch(_cls1..SwitchMap.Vehicle[vehicle.ordinal()])
{
case 1: // '\001'
System.out.println("BUS");
break;
case 2: // '\002'
System.out.println("CAR");
break;
default:
System.out.println("default");
break;
}
}
}
通过反编译之后的代码我们就能知道,原来枚举类型的语法糖生成的代码是通过一个数组实现,会把枚举变量的原始值作为下标,存的值是1、2、3…从1到枚举个数的整数值,最终还是使用的int型做switch操作。
字符串的switch支持
public static void main(String[] args) {
String str = "bbb";
switch (str){
case "aaa":
System.out.println("a");
break;
case "bbb":
System.out.println("b");
break;
case "ccc":
System.out.println("b");
break;
default:
System.out.println("default");
}
}
jad反编译之后的结果:
public class Main
{
public Main()
{
}
public static void main(String args[])
{
String s = "b";
String s1 = s;
byte byte0 = -1;
switch(s1.hashCode())
{
case 97: // 'a'
if(s1.equals("a"))
byte0 = 0;
break;
case 98: // 'b'
if(s1.equals("b"))
byte0 = 1;
break;
case 99: // 'c'
if(s1.equals("c"))
byte0 = 2;
break;
}
switch(byte0)
{
case 0: // '\0'
System.out.println("a");
break;
case 1: // '\001'
System.out.println("b");
break;
case 2: // '\002'
System.out.println("b");
break;
default:
System.out.println("default");
break;
}
}
}
很容易就看明白了字符串(String)的switch的实现逻辑,先通过字符串的hashCode值定位到要case的字符串,再通过equal()确认这个字符串,确认后给一个int型(编译器能自己转换为int的类型)中间变量赋值,这个中间变量的值是从0到case的个数n-1。最终在通过switch这个int型(编译器能自己转换为int的类型)中间变量做switch,本例的中间变量是byte型。
try语句的定义和关闭资源
这种方式我们用的比较少,也介绍一下吧。
public class Main {
public static void main(String[] args) throws Exception {
String s = "CYF";
try (//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.bin"));
//创建对象输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.bin"));
)
{
//序列化java对象
oos.writeObject(s);
oos.flush();
}
}
}
反编译之后的结果:
import java.io.*;
public class Main
{
public Main()
{
}
public static void main(String args[])
throws Exception
{
String s;
ObjectOutputStream objectoutputstream;
Throwable throwable;
s = "CYF";
objectoutputstream = new ObjectOutputStream(new FileOutputStream("b.bin"));
throwable = null;
ObjectInputStream objectinputstream;
Throwable throwable3;
objectinputstream = new ObjectInputStream(new FileInputStream("b.bin"));
throwable3 = null;
try
{
objectoutputstream.writeObject(s);
objectoutputstream.flush();
}
catch(Throwable throwable5)
{
throwable3 = throwable5;
throw throwable5;
}
if(objectinputstream != null)
if(throwable3 != null)
try
{
objectinputstream.close();
}
catch(Throwable throwable4)
{
throwable3.addSuppressed(throwable4);
}
else
objectinputstream.close();
break MISSING_BLOCK_LABEL_139;
Exception exception;
exception;
if(objectinputstream != null)
if(throwable3 != null)
try
{
objectinputstream.close();
}
catch(Throwable throwable6)
{
throwable3.addSuppressed(throwable6);
}
else
objectinputstream.close();
throw exception;
if(objectoutputstream != null)
if(throwable != null)
try
{
objectoutputstream.close();
}
catch(Throwable throwable1)
{
throwable.addSuppressed(throwable1);
}
else
objectoutputstream.close();
break MISSING_BLOCK_LABEL_215;
Throwable throwable2;
throwable2;
throwable = throwable2;
throw throwable2;
Exception exception1;
exception1;
if(objectoutputstream != null)
if(throwable != null)
try
{
objectoutputstream.close();
}
catch(Throwable throwable7)
{
throwable.addSuppressed(throwable7);
}
else
objectoutputstream.close();
throw exception1;
}
}
反编译之后的代码很长,长的都不想阅读了,简单的来说把try和关闭资源的代码分类,try代码会生成try/catch语句,和一个异常中奖变量,用这个中间变量保存异常信息,在最后关闭资源的代码执行后再抛出这个异常中奖变量保存的异常。
总而言之,语法糖是编译器实现的一些“小把戏”,这些“小把戏”能提升我们的工作效率,但是我也应该要明白这些“小把戏”背后的原理,这样我们才能更好的利用他们,而不是被他们迷惑。