语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。
语法糖通常是指一种语法上的"甜头",它可以让程序员用更简洁、更易读的方式来表达相同的逻辑。这样的特性对于提高代码的可读性和可维护性非常有帮助,但并不会引入新的功能或改变语言的本质。
举个例子,Java 中的 foreach 循环就是一种语法糖。它简化了遍历数组或集合的操作,提高了代码的可读性,但并没有引入新的功能。另一个例子是Python中的列表推导式,它提供了一种简洁的方式来创建列表,但本质上仍然是对列表元素的迭代和转换。
所熟知的编程语言中几乎都有语法糖。 很多人说Java是一个“低糖语言”,其实从Java 7开始Java语言层面上一直在添加各种糖,主要是在“Project Coin”项目下研发。尽管现在Java有人还是认为现在的Java是低糖,未来还会持续向着“高糖”的方向发展。
总的来说,语法糖是一种让代码更易读、更简洁的语法形式,它并不会改变语言的功能和特性,但能够提高代码的可读性和编写效率。
语法糖的存在主要是方便开发人员使用。但其实,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。
当将语法糖"解糖",意味着要深入理解语法糖背后的实际实现和原理。需要了解语法糖所隐藏的底层代码,以及它所代表的真正的语言功能。通过"解糖",可以看到语法糖背后的真实机制,这有助对代码的执行过程和语言的底层工作方式有更深入的理解。
说到编译,Java语言中,javac命令可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。
去看com.sun.tools.javac.main.JavaCompiler的源码,会发现在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。
Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。
从Java 7 开始,Java语言中的语法糖在逐渐丰富,其中一个比较重要的就是Java 7中switch开始支持String。在开始coding之前先科普下,Java中的swith自身原本就支持基本类型。比如int、char等。对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。
所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(ackii码是整型)以及int。
那么接下来看下switch对String得支持,有以下代码:
public class switchDemoString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default:
break;
}
}
}
反编译后内容如下:
public class switchDemoString
{
public switchDemoString()
{
}
public static void main(String args[])
{
String str = "world";
String s;
switch((s = str).hashCode())
{
default:
break;
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
}
}
}
看到这个代码,原来字符串的switch是通过equals()和hashCode()方法来实现的。还好hashCode()方法返回的是int,而不是long。仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。
很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的。通常情况下,一个编译器处理泛型有两种方式:Code specialization和Code sharing。C++和C#是使用Code specialization的处理机制,而Java使用的是Code sharing的机制。
Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。
对于Java虚拟机来说,根本不认识Map
类型擦除的主要过程如下:
1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
2.移除所有的类型参数。
举例说明:
// 语法糖
Map map = new HashMap();
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");
ArrayList list = new ArrayList();
// 语法糖解糖
Map map1 = new HashMap();
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");
ArrayList list1 = new ArrayList<>();
举例2:
// 类型擦除前
public static > A max1(Collection xs) {
Iterator xi = xs.iterator();
A w = xi.next();
while (xi.hasNext()) {
A x = xi.next();
if (w.compareTo(x) < 0)
w = x;
}
return w;
}
//类型擦除后
public static Comparable max2(Collection xs) {
Iterator xi = xs.iterator();
Comparable w = (Comparable) xi.next();
while (xi.hasNext()) {
Comparable x = (Comparable) xi.next();
if (w.compareTo(x) < 0)
w = x;
}
return w;
}
虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List
自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。
因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。
原始类型byte, short, char, int, long, float, double 和 boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean。
举例:
private void test1(){
int i = 10;
Integer n = i;
Integer ii = 10;
int nn = ii;
// 反编译后代码如下
int i1 = 10;
Integer n1 = Integer.valueOf(i1);
Integer ii1 = Integer.valueOf(10);
int nn1= ii1.intValue();
}
从反编译得到内容可以看出,在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。
所以,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。
可变参数(variable arguments)是在Java 1.5中引入的一个特性。它允许一个方法把任意数量的值作为参数。在Java中,可变参数(Varargs)是一种语法糖,用于简化方法的调用和定义。通过以下几个特征,可以看出Java可变参数使用了语法糖:
// 1.方法定义中使用三个连续的点(...)表示可变参数:
public void foo(String... args) {
// 方法体
}
// 2.可变参数在方法内部被当作一个数组来处理:
public void foo(String... args) {
for (String arg : args) {
// 对每个参数进行处理
}
}
// 3.调用方法时,可以直接传递多个参数,而不需要显式创建数组:
foo("arg1", "arg2", "arg3");
// 4.可变参数可以与普通参数共存,但可变参数必须是最后一个参数:
public void bar(int num, String... args) {
// 方法体
}
// 5.编译器会自动将多个参数封装为一个数组,以便方法内部使用。
以上特征表明,Java的可变参数实际上是通过编译器在底层进行转换和处理的,它将多个参数封装成数组,并且提供了更简洁的调用方式。这就是Java可变参数使用了语法糖的体现。
Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。参考:Java的枚举类型用法介绍
要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?
答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类。
那么枚举是由什么类维护的呢,我们简单的写一个枚举:
public enum Weekday {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
这个枚举类型表示一周的7天。每个枚举常量都是Weekday类型的实例,可以通过名称来引用。在使用枚举时,可以像使用普通类一样使用枚举。例如:
Weekday today = Weekday.MONDAY;
if (today == Weekday.MONDAY) {
System.out.println("今天是周一");
}
枚举类型还可以定义构造函数、实例方法和静态方法等,以便更好地封装数据和提供功能。
总的来说,使用Java枚举的语法糖可以让开发者更加方便地定义和使用枚举类型,增加代码的可读性和可维护性。
Java内部类是一种定义在另一个类里面的类,它可以访问包含它的外部类的所有成员,包括私有成员。在Java 1.1之前,内部类的实现方式是通过生成额外的类文件来实现的,这种方式非常繁琐和不优雅。而从Java 1.1开始,引入了内部类的语法糖,使得定义和使用内部类更加简单和直观。
使用内部类的语法糖,可以通过以下方式定义一个内部类:
public class Outer {
private int x = 0;
public void doSomething() {
final int y = 1;
class Inner {
public void printX() {
System.out.println("x = " + x);
}
public void printY() {
System.out.println("y = " + y);
}
}
Inner inner = new Inner();
inner.printX();
inner.printY();
}
}
这个例子中,Outer类中定义了一个doSomething方法,该方法包含一个内部类Inner。Inner类可以访问外部类Outer的私有成员x和方法参数y。在doSomething方法中,创建了一个Inner类的实例,并调用其printX和printY方法。
使用内部类的语法糖可以方便地实现封装和隐藏,提高代码的可读性和可维护性。内部类还可以用于实现事件处理、迭代器等功能。但是,过多的内部类也会增加代码的复杂性,需要谨慎使用。
在Java中,assert关键字是从JAVA SE 1.4 引入的,为了避免和老版本的Java代码中使用了assert关键字导致错误,Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!)。
如果要开启断言检查,则需要用开关-enableassertions或-ea来开启。如果要禁用断言,可以使用"-da"选项进行编译和运行。
看一段包含断言的代码:
int value = -1;
assert value > 0 : "数值必须大于零";
很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了assert这个语法糖我们节省了很多代码。在这个例子中,assert关键字用于表达式的断言,如果表达式为false,则会抛出AssertionError异常,并输出指定的错误信息。在运行时,如果启用了断言,那么会对这些断言进行检查;反之,如果禁用了断言,那么这些断言会被忽略掉。
在java 7中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。
比如:
public class Test {
public static void main(String... args) {
int i = 10_000;
System.out.println(i);
}
}
反编译后:
public class Test
{
public static void main(String[] args)
{
int i = 10000;
System.out.println(i);
}
}
反编译后就是把_删除了。也就是说编译器并不认识在数字字面量中的_,需要在编译阶段把他去掉。
在 Java 中,for-each 循环是一种简化遍历集合或数组的语法糖,也称为增强型for循环。它提供了一种更加简洁和直观的方式来遍历集合或数组中的元素。
使用 for-each 语法糖,可以通过以下方式来遍历一个数组:
int[] numbers = {1, 2, 3, 4, 5};
for (int number : numbers) {
System.out.println(number);
}
这里的 int number : numbers
部分就是 for-each 循环的语法糖。它表示依次取出数组 numbers 中的元素,并将每个元素赋值给 number,然后执行循环体内的代码。
for-each 循环的语法糖在代码编写时更加简洁清晰,使得遍历集合或数组的代码更易读、易写。它隐藏了迭代器或数组索引的细节,减少了程序员编写遍历代码的工作量,并提高了代码的可读性。因此,在实际开发中,for-each 循环通常是首选的遍历方式。
在 Java 7 之后,引入了 try-with-resources 语法糖,它是一种可以自动关闭资源的语法糖。在使用 try-with-resources 时,可以将需要关闭的资源包装在 try 语句中,并在 try 块结束时自动关闭这些资源。
使用 try-with-resources 语法糖,可以通过以下方式来打开文件并读取其中的内容:
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
这里的 BufferedReader
和 FileReader
都是需要关闭的资源,因此它们被包装在 try 语句中。在 try 块结束时,Java 会自动调用它们的 close()
方法来关闭这些资源。这样,在出现异常或其他错误时,也能保证这些资源得以正确关闭。
需要注意的是,只有实现了 AutoCloseable
接口的资源才可以使用 try-with-resources 来自动关闭。这个接口只包含了一个 close()
方法,用于关闭资源并释放与之相关的系统资源。
使用 try-with-resources 语法糖,可以避免手动关闭资源带来的繁琐和容易出错,提高了代码的可读性和可维护性。它是一个非常实用的语法糖,在处理需要关闭资源的情况下,通常是首选的方式。
在 Java 8 中,引入了 Lambda 表达式语法糖,它是一种更简洁、更灵活的编程方式。Lambda 表达式是一个匿名函数,它可以作为参数传递给方法或存储在变量中,并且可以在需要时执行。
使用 Lambda 表达式,可以通过以下方式来定义一个简单的函数:(x, y) -> x + y
这个 Lambda 表达式表示一个接受两个整数参数 x 和 y,然后返回它们的和。其中 (x, y)
是参数列表,x + y
是函数体。
在实际应用中,Lambda 表达式通常会与函数式接口一起使用。函数式接口是指只包含一个抽象方法的接口,它们可以被 Lambda 表达式所实现。例如,Java 中的 java.util.function
包中就包含了很多常用的函数式接口,如 Predicate
、Function
、Consumer
等等。
使用 Lambda 表达式,可以通过以下方式来遍历一个集合并输出其中的偶数:
List numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
在这个例子中,filter
方法的参数是一个 Lambda 表达式,用于判断集合中的元素是否为偶数。而 forEach
方法则使用了方法引用语法 System.out::println
,表示将每个偶数打印到标准输出。
Lambda 表达式的语法糖在代码编写时更加简洁清晰,使得函数式编程的代码更易读、易写。它可以大大减少冗余代码和匿名内部类的使用,提高了代码的可读性和可维护性。因此,在实际开发中,Lambda 表达式已经成为 Java 开发中非常重要的一种编程方式。
public class GenericTypes {
public static void method(List list) {
System.out.println("invoke method(List list)");
}
public static void method(List list) {
System.out.println("invoke method(List list)");
}
}
上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List另一个是List,但是,这段代码是编译通不过的。因为我们前面讲过,参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException
public class StaticTest{
public static void main(String[] args){
GT gti = new GT();
gti.var=1;
GT gts = new GT();
gts.var=2;
System.out.println(gti.var);
}
}
class GT{
public static int var=0;
public void nothing(T x){}
}
以上代码输出结果为:2!由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。
public static void main(String[] args) {
Integer a = 1000;
Integer b = 1000;
Integer c = 100;
Integer d = 100;
System.out.println("a == b is " + (a == b));
System.out.println(("c == d is " + (c == d)));
}
输出结果:
a == b is false
c == d is true
在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。
适用于整数值区间-128 至 +127。只适用于自动装箱。使用构造函数创建对象不适用。
for (Student stu : students) {
if (stu.getId() == 2)
students.remove(stu);
}
会抛出ConcurrentModificationException异常。
Iterator是工作在一个独立的线程中,并且拥有一个 mutex 锁。Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException异常。
所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法remove()来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。
前面介绍了11种Java中常用的语法糖。由于篇幅问题,其他还有一些常见的语法糖比如字符串拼接其实基于 StringBuilder,Java10 里面的 var 关键字声明局部变量采用的是智能类型推断这里就不提了。
所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成JVM认识的语法。
当把语法糖解糖之后,你就会发现其实日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。
有了这些语法糖,在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。
更多消息资讯,请访问昂焱数据。