阅读 164

语法糖甜不甜?反编译一下 你就知道!

关于编译与反编译,可以看这里 :juejin.im/post/684490…

语法糖是什么?

语法糖,又称糖衣语法,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。之所以称作是“糖”,是因为它可以使代码写起来更方便,看起来更简洁,就像是给代码里面加了糖一样,越写越开心。

与之相对的是语法盐,就是虽然使用这种语法特性能够使写出坏代码的可能性降低,但这些特性会强迫程序员做出一些基本不用描述程序行为,而是用来证明他们知道自己在做什么的额外举动,总之就是咸得让人不快乐。

不过,虽然语法糖的存在能使开发变得更加方便,但实际上 Java 虚拟机并不支持这些语法糖。它们在编译阶段就会被还原成 Java 的基础语法结构,这个过程也被称作解语法糖。

那么就让我们来解一下 Java 中的语法糖,看看这些糖块的真面目吧!(o゚ω゚o)

Java中的语法糖

switch 对 String 与 enum 的支持

在 Java7 以前,能被 switch 支持的参数类型仅有 intshortcharbyte枚举 这五种。对于编译器来说,switch 中其实只能使用整型参数,而它对 char 类型的支持,也是通过对其 ASCII 码进行比较来实现的。

不过,从 JDK1.7 开始,switch 中又添加了对 String 的支持,我们来写段代码看一下 :

package demo;

public class Demo {
    public static void main(String[] args) {
        String s = "hello";
        switch (s) {
            case "hello" :
                System.out.println("hello");
                break;
            case "world" :
                System.out.println("world");
                break;
            default:
        }
    }
}
复制代码

对上述代码进行反编译后,得到了如下结果 :

可以看到,对字符串的 switch 支持其实是通过 hashCode()equals() 实现的。值得注意的是,这里用 equals() 进行了必要的二次校验,这是为了防止哈希碰撞,即哈希码相同而对象不同的情况。

再来写一段 enum 的 :

package demo;

public class Demo {
    public static void main(String[] args) {
        DemoEnum e = DemoEnum.UP;
        switch (e) {
            case UP :
                System.out.println("hello");
                break;
            case DOWN :
                System.out.println("world");
                break;
            default:
        }
    }
}

enum DemoEnum {
    UP, DOWN, LEFT, RIGHT
}
复制代码

这个要结合枚举类的反编译看,后面会详细讲到。枚举类的实现原理是相当于给它补了一个 int 类型的 code码,而 switch 在对 enum 进行比较时,实际上就是使用的这个 code 码。

泛型

对 Java 虚拟机来说,“泛型” 是一种不存在的东西。像类似 List<String> 这样的语法,在编译期间就已经进行了名为类型擦除的解语法糖。

类型擦除主要分为两个步骤 :

  1. 将所有泛型参数用其顶级父类 ( 一般是 Object ) 替换
  2. 移除所有的类型参数

所以,在对一段使用了泛型的代码进行反编译后,我们会得到这样的结果 :

package demo;

import java.util.ArrayList;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        list.add(new Student("a", 1));
    }
}

class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
复制代码

List<Student> 被完全擦除变为 List,但这并非没有意义的。使用泛型能够在编译期间就对代码规范进行限制,从而避免了一个声明 Student 的 List 里面被误加入了一个 Teacher。

自动装箱与拆箱

自动装箱是指将 Java 中的原始类型自动转换为对应的封装类型,自动拆箱则反之。具体的类型对应有以下八种 :byte - Byteshort - Shortchar - Characterint - Integerlong - Longfloat - Floatdouble - Doubleboolean - Boolean

package demo;

public class Demo {
    public static void main(String[] args) {
        // 装箱
        Integer a = 11;
        System.out.println(a);

        // 拆箱
        int b = a;
        System.out.println(b);
    }
}
复制代码

撸一个简单的装箱拆箱就可以看出来,装箱是通过调用包装器的 valueOf() 方法实现的,而拆箱是通过调用 xxxValue() 实现的。

方法变长参数

可变参数是在 Java5 中引入的一个特性,它允许一个方法把任意数量的值作为参数。

package demo;

public class Demo {
    public static void main(String[] args) {
    }

    private static void demo(String... args) {
        for(String s : args) {
            System.out.println(s);
        }
    }
}
复制代码

由上,可变参数在被使用时,将首先创建一个长度为实际传递的参数个数的数组,并将参数值放入该数组中;而被调用的方法声明的参数列表,实际上也被编译为了一个数组。

枚举

都说 Java 中一切皆对象,对象得有个类呀,那么,枚举的类在哪里呢?先来写一个简单的枚举类 :

package demo;

public enum DemoEnum {
    UP, DOWN, LEFT, RIGHT
}
复制代码

然后对它进行反编译 :

可以看到,枚举是由一个编译器自动创建的类 public final class XXEnum extends Enum 所维护的,而我们定义的具体枚举被声明为了类中的静态常量。该类继承了 Enum,同时用 final 关键字修饰着,这也是为什么说枚举类型不能被继承的根本原因所在。

内部类

内部类又称嵌套类,相当于外部类中的一个普通成员。

然而事实上,内部类仅仅是一个编译时期的概念。虽然是作为外部类的一个“成员”存在的,但在实际编译的过程中,它将作为一个独立的类存在,并生成一个命名为 外部类名$内部类名.class ,且不依存于外部类的 .class 文件。

package demo;

public class Demo {
    class InnerClass {
        private String name;
        private int age;
    }
}
复制代码

不过,当我们对外部类进行反编译的时候,还是会连着内部类的 .class 文件一起打包进行反编译的 OwO

条件编译

一般情况下,程序中的每行代码都是需要参与编译的。但有时出于对代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,而将不满足条件的舍弃,这就是条件编译。

比如下面这段代码。在已知 flag 为 true 的情况下,代码逻辑将一定不会进入输出 false 的分支,所以,在编译的时候,确认不会进入的分支代码将被直接舍弃,整个条件从句被简化为了直接执行 true 所在的分支。

package demo;

public class Demo {
    public static void main(String[] args) {
        final boolean flag = true;
        if(flag) {
            System.out.println("true");
        } else {
            System.out.println("false");
        }
    }
}
复制代码

断言

在 Java 中,assert 关键字是从 JAVA SE 1.4 开始引入的,为了避免和老板本中的一些冲突,Java 在执行的时候默认是不启动断言检查的,即默认忽略所有断言语句。如果需要开启断言,可以通过设置 -enableassertions-ea 来达到目的。

package demo;

public class Demo {
    public static void main(String[] args) {
        int a = 1, b = 1;
        assert a == b;
        System.out.println("a == b");
        assert a != b : "false";
        System.out.println("a != b");
    }
}
复制代码

从反编译之后的代码中可以看出,断言的底层实现就是 if 语句 :如果断言结果为真,则什么都不做,程序继续执行;如果断言的结果不为真,则抛出 AssertError 来打断执行。

数值字面量

数值字面量,指在数字 ( 整型或浮点数都可以 ) 之间插入任意多个下划线,以方便开发者的阅读,但不会影响程序的编译。它的原理就是,编译的时候把下划线删掉 QvQ

package demo;

public class Demo {
    public static void main(String[] args) {
        int a = 100_00;
        System.out.println(a);
    }
}
复制代码

for-each

增强 for 循环,能让 for 循环变得更加简洁明了的循环,它的实现原理是使用了普通的 for 循环和迭代器 qwq

package demo;

import java.util.ArrayList;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for(Integer i : list) {
            System.out.println(i);
        }
    }
}
复制代码

try-with-resource

带资源的 try-catch,能帮我们处理在进行一些操作,尤其是文件操作和数据库连接等时候,对其中使用到的一些资源的关闭。相比于使用普通 try-catch 时在 finally 中释放资源,使用 try-with-resource 能够避免这种繁琐且重复的 close() 工作,从而使代码变得简洁易读。

package demo;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class Demo {
    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("path"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {

        }
    }
}
复制代码

使用反编译对代码进行还原之后就会发现,其实 try-with-resource 的底层实现原理依然是传统的关闭方式,即是我们没有做的资源关闭工作,编译器帮我们干掉了。

lambda表达式

最后是我们的 lambda 表达式了!它的实现原理是调用了 JVM 底层提供的 lambda 相关 API。比如这里,就是调用了 java.lang.invoke.LambdaMetafactory#metafactory 方法,然后使用一个 lambda$main$0 方法进行输出 :

package demo;

import java.util.Arrays;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        String[] str = {"aa", "bb", "cc"};
        List<String> list = Arrays.asList(str);
        list.forEach((s) -> System.out.println(s));
    }
}
复制代码