Java 语法糖

语法糖

        语法糖(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 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。

糖块一、 switch 支持 String 与枚举

        从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 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:

        虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List.class或是List.class,而只有List.class。

糖块三、 自动装箱与拆箱

        自动装箱就是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);
  }
}

        反编译后就是把_删除了。也就是说编译器并不认识在数字字面量中的_,需要在编译阶段把他去掉。

糖块九 、 for-each

        在 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 循环通常是首选的遍历方式。

糖块十 、 try-with-resource

        在 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();
}

        这里的 BufferedReaderFileReader 都是需要关闭的资源,因此它们被包装在 try 语句中。在 try 块结束时,Java 会自动调用它们的 close() 方法来关闭这些资源。这样,在出现异常或其他错误时,也能保证这些资源得以正确关闭。

        需要注意的是,只有实现了 AutoCloseable 接口的资源才可以使用 try-with-resources 来自动关闭。这个接口只包含了一个 close() 方法,用于关闭资源并释放与之相关的系统资源。

        使用 try-with-resources 语法糖,可以避免手动关闭资源带来的繁琐和容易出错,提高了代码的可读性和可维护性。它是一个非常实用的语法糖,在处理需要关闭资源的情况下,通常是首选的方式。

糖块十一、Lambda表达式

        在 Java 8 中,引入了 Lambda 表达式语法糖,它是一种更简洁、更灵活的编程方式。Lambda 表达式是一个匿名函数,它可以作为参数传递给方法或存储在变量中,并且可以在需要时执行。

        使用 Lambda 表达式,可以通过以下方式来定义一个简单的函数:(x, y) -> x + y

        这个 Lambda 表达式表示一个接受两个整数参数 x 和 y,然后返回它们的和。其中 (x, y) 是参数列表,x + y 是函数体。

        在实际应用中,Lambda 表达式通常会与函数式接口一起使用。函数式接口是指只包含一个抽象方法的接口,它们可以被 Lambda 表达式所实现。例如,Java 中的 java.util.function 包中就包含了很多常用的函数式接口,如 PredicateFunctionConsumer 等等。

        使用 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,擦除动作导致这两个方法的特征签名变得一模一样。

泛型——当泛型遇到catch 

泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException和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循环

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认识的语法。

        当把语法糖解糖之后,你就会发现其实日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。

        有了这些语法糖,在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。

更多消息资讯,请访问昂焱数据。

你可能感兴趣的:(java,java,开发语言)