这篇博文咱们就来谈谈 Java 中一个有趣的知识点 ------ 语法糖
那么,什么是语法糖呢?
语法糖:又称 “糖衣语法”。指:计算机语言中添加的某种语法。这种语法对语言的功能没有影响,只是为了方便程序员开发,提高开发效率,提高程序的可读性(语法糖的存在主要是方便开发人员使用)。
解语法糖:但是,JVM 并不支持语法糖的。语法糖在程序编译后就会被还原成最原始的基础语法结构,这个过程就是 解语法糖(Java 中的语法糖只存在于编译期)。
所以,在 Java 中,真正支持语法糖的是 Java 编译器。
程序编译完后,由 .java 文件变成了 .class 文件,里面是字节码二进制,是不能直接查看其内容的,需要借助反编译工具进行反编译,然后才能正常查看。这里推荐一款反编译工具:JAD
。 详细用法可以参考这篇博客
jad 类名.class
上述命令,就将 .class 文件反编译为 .java 文件,然后,就能查看语法糖被编译后的原始结构。
Java 中提供了很多语法糖,下面列举下几种主要的、常用的语法糖:
在 Java 中,支持如下形式的数值字面量:
另外,在 JDK1.7 中,不管是整数还是浮点数的数值字面量,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。比如:
下划线只能出现在数字中间,前后必须是数字。所以 “_100”、“0b_101“ 是不合法的,无法通过编译。
这样限制的动机就是可以降低实现的复杂度。有了这个限制,Java 编译器只需在扫描源代码的时候将所发现的数字中间的下划线直接删除就可以了。如果不添加这个限制,编译器需要进行语法分析才能做出判断。比如:_100,可能是一个整数字面量 100,也可能是一个变量名称。这就要求编译器的实现做出更复杂的改动。
代码如下:
public class TestOne {
public static void main(String[] args) {
// 十进制
int a = 10;
int aa = 10_000;
// 八进制
int b = 010;
// 十六进制
int c = 0X10;
// 二进制
int d = 0B10;
// 10
System.out.println(a);
// 10000
System.out.println(aa);
// 8
System.out.println(b);
// 16
System.out.println(c);
// 2
System.out.println(d);
}
}
将上述代码通过 jad
工具进行反编译:
public class TestOne {
public TestOne()
{
}
public static void main(String args[])
{
int a = 10;
int aa = 10000;
int b = 8;
int c = 16;
int d = 2;
System.out.println(a);
System.out.println(aa);
System.out.println(b);
System.out.println(c);
System.out.println(d);
}
}
由反编译代码知:编译器已经将下滑线删除;编译器已经将二进制,八进制,十六进制数转换成了十进制数
使用变长参数有两个条件:一是变长的那一部分参数具有相同的类型;二是变长参数必须位于方法参数列表的最后面。
变长参数同样是Java中的语法糖,其内部实现原理:编译器在编译源代码的时候将变长参数部分转换成了 Java 数组。
public class TestTwo {
public static void variable(String country, String... cities) {
System.out.println(country);
for(int i = 0; i < cities.length; i++) {
System.out.print(cities[i]);
}
System.out.println();
}
public static void main(String[] args) {
variable("china", "Beijing", "ShangHai", "ShenZhen");
}
}
反编译后:
public class TestTwo {
public TestTwo()
{
}
public static transient void variable(String country, String cities[])
{
System.out.println(country);
for(int i = 0; i < cities.length; i++)
System.out.print(cities[i]);
System.out.println();
}
public static void main(String args[])
{
variable("china", new String[] {
"Beijing", "ShangHai", "ShenZhen"
});
}
}
增强 for 循环的对象要么是一个数组,要么实现了 Iterable 接口。这个语法糖主要用来对数组或者集合进行遍历,其在循环过程中不能改变集合的大小。增强for循环主要使代码更加简洁,其背后的原理是编译器将增强 for 循环转换成了普通的 for 循环或者 while 循环。
public class TestThree {
public static void main(String[] args) {
String[] params = new String[]{"Java", "Python", "C++"};
for (String param : params) {
System.out.println(param);
}
}
}
反编译后:
public class TestThree {
public TestThree()
{
}
public static void main(String args[])
{
String params[] = {
"Java", "Python", "C++"
};
String args1[] = params;
int i = args1.length;
for(int j = 0; j < i; j++)
{
String param = args1[j];
System.out.println(param);
}
}
}
Java 中的 switch 原本就支持基本类型------整型:int
、char
、byte
、short
,并且,编译期最终会将类型(强制)转化为 int
类型。
但不支持 long
类型。因为 long 类型转换为 int 类型会丢失精度!!
对于 int
、byte
、short
类型而言,直接进行数值进行比较;对于 char
类型而言,则比较的是 ascii
码
后来,在 JDK1.5 出现了 switch 支持枚举类型;在 JDK1.7 中出现了 switch 支持字符串。
对于字符串 String 而言,switch 是通过 hashCode() 和 equals() 方法来实现的;对于枚举类而言,通过枚举定义的下标来实现的
字符串:
public class SwitchSugarTest {
public static void main(String[] args) {
String str = "Java";
switch (str) {
case "Java":
System.out.println("James Gosling is Java's father");
break;
case "C++":
System.out.println("Bjarne Stroustrup is C++'s fatherr");
break;
default:
break;
}
}
}
反编译后:
public class SwitchSugarTest
{
public SwitchSugarTest()
{
}
public static void main(String args[])
{
String str = "Java";
String s = str;
byte byte0 = -1;
switch(s.hashCode())
{
case 2301506:
if(s.equals("Java"))
byte0 = 0;
break;
case 65763:
if(s.equals("C++"))
byte0 = 1;
break;
}
switch(byte0)
{
case 0: // '\0'
System.out.println("James Gosling is Java's father");
break;
case 1: // '\001'
System.out.println("Bjarne Stroustrup is C++'s fatherr");
break;
}
}
}
字符串+号拼接原理:运行时,两个字符串str1,str2的拼接首先会 new 一个 StringBuilder 对象,然后分别对字符串进行 append 操作,最后调用toString()方法。
public class StringSugarTest {
public static void main(String[] args) {
String str1 = "a";
String str2 = "b";
String s = str1 + str2;
System.out.println(s);
}
}
反编译后:
public class StringSugarTest
{
public StringSugarTest()
{
}
public static void main(String args[])
{
String str1 = "a";
String str2 = "b";
String s = (new StringBuilder()).append(str1).append(str2).toString();
System.out.println(s);
}
}
但是如果在编译期能确定字符相加的结果,则会进行编译期优化。
String s = "a" + "b";
对于上面的表达式,编译器直接优化成
String s = "ab";
在 Java 中的8个基本类型和对应的包装类型之间是可以互相赋值的(这个过程叫自动装箱、拆箱过程)。
其实,这背后的原理是编译器做了优化:将基本类型赋值给包装类其实是调用了包装类的 valueOf()
方法创建了一个包装类再赋值给了基本类型;而包装类赋值给基本类型就是调用了包装类的 xxxValue()
方法拿到基本数据类型后再赋值的
自动装箱:
public class PackageSugarTest {
public static void main(String[] args) {
int a = 1;
int b = 2;
Integer c = a + b;
System.out.println(c);
}
}
反编译后:
public class PackageSugarTest
{
public PackageSugarTest()
{
}
public static void main(String args[])
{
int a = 1;
int b = 2;
Integer c = Integer.valueOf(a + b);
System.out.println(c);
}
}
自动拆箱:
public class PackageSugarTest {
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
int c = a + b;
System.out.println(c);
}
}
反编译后:
public class PackageSugarTest
{
public PackageSugarTest()
{
}
public static void main(String args[])
{
Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(2);
int c = a.intValue() + b.intValue();
System.out.println(c);
}
}
Java语言中之所以引入内部类,是因为有些时候一个类只想在一个类中有用,我们不想让其在另外一个地方被使用。内部类之所以是语法糖,是因为其只是一个编译时的概念,一旦编译完成,编译器就会为内部类生成一个单独的 class 文件,名为 outer$innter.class。
public class Outer {
class Inner {}
}
使用javac编译后,生成两个 class 文件:Outer.class、Outer$Inner.class
,反编译后,内容如下:
public class Outer
{
class Inner
{
final Outer this$0;
Inner()
{
this.this$0 = Outer.this;
super();
}
}
public Outer()
{
}
}
class Outer$Inner
{
final Outer this$0;
Outer$Inner()
{
this.this$0 = Outer.this;
super();
}
}
当一个外部资源的句柄对象实现了 AutoCloseable 接口,JDK7 中便可以利用 try-with-resource 语法更优雅的关闭资源,消除板式代码。
将外部资源的句柄对象的创建放在 try 关键字后面的括号中,当这个 try-catch 代码块执行完毕后,Java 会确保外部资源的 close() 方法被调用
public class TrySugarTest {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream(new File("pom.xml"))){
System.out.println(in.read());
} catch (Exception e) {
e.printStackTrace();
}
}
}
反编译后:
public class TrySugarTest
{
public TrySugarTest()
{
}
public static void main(String args[])
{
FileInputStream in;
Throwable throwable;
in = new FileInputStream(new File("pom.xml"));
throwable = null;
try
{
System.out.println(in.read());
}
catch(Throwable throwable2)
{
throwable = throwable2;
throw throwable2;
}
if(in != null)
if(throwable != null)
try
{
in.close();
}
catch(Throwable throwable1)
{
throwable.addSuppressed(throwable1);
}
else
in.close();
break MISSING_BLOCK_LABEL_108;
Exception exception;
exception;
if(in != null)
if(throwable != null)
try
{
in.close();
}
catch(Throwable throwable3)
{
throwable.addSuppressed(throwable3);
}
else
in.close();
throw exception;
Exception e;
e;
e.printStackTrace();
}
}
java 中类的定义使用 class,枚举类的定义使用 enum。但在Java的字节码结构中,其实并没有枚举类型,枚举类型只是一个语法糖,在编译完成后就会被编译成一个普通的类,也是用 class 修饰。这个类继承 java.lang.Enum
,并被 final 关键字修饰。
public enum EnumSugarTest {
APPLE
,
ORANGE
;
}
反编译后:
public final class EnumSugarTest extends Enum
{
public static EnumSugarTest[] values()
{
return (EnumSugarTest[])$VALUES.clone();
}
public static EnumSugarTest valueOf(String name)
{
return (EnumSugarTest)Enum.valueOf(com/tinady/sugar/EnumSugarTest, name);
}
private EnumSugarTest(String s, int i)
{
super(s, i);
}
public static final EnumSugarTest APPLE;
public static final EnumSugarTest ORANGE;
private static final EnumSugarTest $VALUES[];
static
{
APPLE = new EnumSugarTest("APPLE", 0);
ORANGE = new EnumSugarTest("ORANGE", 1);
$VALUES = (new EnumSugarTest[] {
APPLE, ORANGE
});
}
}
在 JDK5 中,Java 语言引入了泛型机制。但是这种泛型机制其实是通过类型擦除来实现的,即Java中的泛型只在程序源代码中有效(源代码阶段提供类型检查),在编译后的字节码中自动用强制类型转换进行替代。也就是说,Java 语言中的泛型机制其实就是一颗语法糖。
public class FanSugarTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("name", "zzc");
map.put("age", "22");
String name = map.get("name");
System.out.println(name);
}
}
反编译后:
public class FanSugarTest
{
public FanSugarTest()
{
}
public static void main(String args[])
{
Map map = new HashMap();
map.put("name", "zzc");
map.put("age", "22");
String name = (String)map.get("name");
System.out.println(name);
}
}
【参考资料】
Java中的语法糖
不了解这 12 个语法糖,别说你会 Java!