JVM -- 编译器处理;语法糖(六)

骑猪看日落 2023-09-26 10:33 143阅读 0赞

语法糖(Syntactic sugar),其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成 和转换的一些代码

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。——百度百科

一、默认构造器

  1. //java源码
  2. public class Candy {
  3. }
  4. //编译成class后的代码
  5. public class Candy {
  6. // 这个无参构造是编译器帮助我们加上的
  7. public Candy() {
  8. super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."
  9. //<init>":()V
  10. }
  11. }

二、自动拆装箱

JDK1.5之后,新增了自动拆、装箱功能

简化基本类型和对象转换的写法

装箱:基本类型的值被封装为一个包装类对象

拆箱:一个包装类对象被拆开并获取相应的值

这是编译器的工作,在class中已经添加,虚拟机没有自动装箱和拆箱的语句

==:基本类型是内容相同,对象时指针是否相同(内存同一个区域)

基本类型没有空值,对象有null

当一个基础数据类型与封装类进行==、+、-、*、/运算时,会将封装类进行拆箱,然后再运算

如以前创建一个Integer对象,需要 使用 “new”关键字

而现在Java中可以直接赋值如下:

Integer不是new出Integer对象,而是直接赋值,就是自动装箱过程

  1. Integer a = new Integer("100");
  2. //JDK1.5之后
  3. Integer b = 100;
  4. int x = b;

三、泛型集合取值

泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息 在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理

  1. public static void main(String[] args) {
  2. List<Integer> list = new ArrayList<>();
  3. list.add(10); // 实际调用的是 List.add(Object e)
  4. Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
  5. }

以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作

  1. // 需要将 Object 转为 Integer
  2. Integer x = (Integer)list.get(0);

如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是

  1. // 需要将 Object 转为 Integer, 并执行拆箱操作
  2. int x = ((Integer)list.get(0)).intValue();

LocalVariableTypeTable 仍然保留了方法参数泛型的信息

  1. public static void main(java.lang.String[]);
  2. descriptor: ([Ljava/lang/String;)V
  3. flags: ACC_PUBLIC, ACC_STATIC
  4. Code:
  5. stack=2, locals=3, args_size=1
  6. 0: new #2 // class java/util/ArrayList
  7. 3: dup
  8. 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
  9. 7: astore_1
  10. 8: aload_1
  11. 9: bipush 10
  12. 11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  13. 14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
  14. 19: pop
  15. 20: aload_1
  16. 21: iconst_0
  17. 22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
  18. 27: checkcast #7 // class java/lang/Integer
  19. 30: astore_2
  20. 31: return
  21. LineNumberTable:
  22. line 8: 0
  23. line 9: 8
  24. line 10: 20
  25. line 11: 31
  26. LocalVariableTable:
  27. Start Length Slot Name Signature
  28. 0 32 0 args [Ljava/lang/String;
  29. 8 24 1 list Ljava/util/List;
  30. 31 1 2 x Ljava/lang/Integer;
  31. LocalVariableTypeTable:
  32. Start Length Slot Name Signature
  33. 8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;

使用反射,仍然能够获得这些信息

  1. public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
  2. }
  3. Method test = Candy3.class.getMethod("test", List.class, Map.class);
  4. Type[] types = test.getGenericParameterTypes();
  5. for (Type type : types) {
  6. if (type instanceof ParameterizedType) {
  7. ParameterizedType parameterizedType = (ParameterizedType) type;
  8. System.out.println("原始类型 - " + parameterizedType.getRawType());
  9. Type[] arguments = parameterizedType.getActualTypeArguments();
  10. for (int i = 0; i < arguments.length; i++) {
  11. System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
  12. }
  13. }
  14. }

输出:

  1. 原始类型 - interface java.util.List
  2. 泛型参数[0] - class java.lang.String
  3. 原始类型 - interface java.util.Map
  4. 泛型参数[0] - class java.lang.Integer
  5. 泛型参数[1] - class java.lang.Object

四、可变参数

可变参数也是 JDK 5 开始加入的新特性

  1. public class Candy4 {
  2. public static void foo(String... args) {
  3. String[] array = args; // 直接赋值
  4. System.out.println(array);
  5. }
  6. public static void main(String[] args) {
  7. foo("hello", "world");
  8. }
  9. }

可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。 同样 java 编译器会在编译期间将上述代码变换为

  1. public class Candy4 {
  2. public static void foo(String[] args) {
  3. String[] array = args; // 直接赋值
  4. System.out.println(array);
  5. }
  6. public static void main(String[] args) {
  7. foo(new String[]{"hello", "world"});
  8. }
  9. }

注:

调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去

五、foreach 循环

foreach是 JDK 5 开始引入的语法糖,数组的循环

  1. public static void main(String[] args) {
  2. int[] array = {1, 2, 3, 4, 5}; // 数组赋初值的简化写法也是语法糖
  3. for (int e : array) {
  4. System.out.println(e);
  5. }
  6. }
  7. public static void main(String[] args) {
  8. List<Integer> list = Arrays.asList(1,2,3,4,5);
  9. for (Integer i : list) {
  10. System.out.println(i);
  11. }
  12. }

会被编译器转换为

  1. public static void main(String[] args) {
  2. int[] array = new int[]{1, 2, 3, 4, 5};
  3. for(int i = 0; i < array.length; ++i) {
  4. int e = array[i];
  5. System.out.println(e);
  6. }
  7. }
  8. public static void main(String[] args) {
  9. List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
  10. Iterator iter = list.iterator();
  11. while(iter.hasNext()) {
  12. Integer e = (Integer)iter.next();
  13. System.out.println(e);
  14. }
  15. }

注:

foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中 Iterable 用来获取集合的迭代器( Iterator )

六、switch 字符串

JDK 7 开始,switch 可以作用于字符串和枚举类

  1. public static void choose(String str) {
  2. switch (str) {
  3. case "hello": {
  4. System.out.println("h");
  5. break;
  6. }
  7. case "world": {
  8. System.out.println("w");
  9. break;
  10. }
  11. }
  12. }

注:

switch 配合 String 和枚举使用时,变量不能为null

会被编译器转换为

  1. public static void choose(String str) {
  2. byte x = -1;
  3. switch(str.hashCode()) {
  4. case 99162322: // hello 的 hashCode
  5. if (str.equals("hello")) {
  6. x = 0;
  7. }
  8. break;
  9. case 113318802: // world 的 hashCode
  10. if (str.equals("world")) {
  11. x = 1;
  12. }
  13. }
  14. switch(x) {
  15. case 0:
  16. System.out.println("h");
  17. break;
  18. case 1:
  19. System.out.println("w");
  20. }
  21. }

上述代码执行了两遍 switch

第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应 byte 类型

第二遍才是利用 byte 执行进行比较。

第一遍时必须既比较 hashCode,又利用 equals 比较,是因为hashCode 是为了提高效率,减少遍历次数;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C. 这两个字符串的hashCode值都是 2123

如下代码

  1. public static void choose(String str) {
  2. switch (str) {
  3. case "BM": {
  4. System.out.println("h");
  5. break;
  6. }
  7. case "C.": {
  8. System.out.println("w");
  9. break;
  10. }
  11. }
  12. }

会被编译器转换为

  1. public static void choose(String str) {
  2. byte x = -1;
  3. switch(str.hashCode()) {
  4. case 2123: // hashCode 值可能相同,需要进一步用 equals 比较
  5. if (str.equals("C.")) {
  6. x = 1;
  7. } else if (str.equals("BM")) {
  8. x = 0;
  9. }
  10. default:
  11. switch(x) {
  12. case 0:
  13. System.out.println("h");
  14. break;
  15. case 1:
  16. System.out.println("w");
  17. }
  18. }
  19. }

七、switch 枚举

  1. enum Sex {
  2. MALE, FEMALE
  3. }
  4. public static void foo(Sex sex) {
  5. switch (sex) {
  6. case MALE:
  7. System.out.println("男");
  8. break;
  9. case FEMALE:
  10. System.out.println("女");
  11. break;
  12. }
  13. }

转换后代码

  1. /**
  2. * 定义一个合成类(仅 jvm 使用,对我们不可见)
  3. * 用来映射枚举的 ordinal 与数组元素的关系
  4. * 枚举的 ordinal 表示枚举对象的序号,从 0 开始
  5. * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
  6. */
  7. static class $MAP {
  8. // 数组大小即为枚举元素个数,里面存储case用来对比的数字
  9. static int[] map = new int[2];
  10. static {
  11. map[Sex.MALE.ordinal()] = 1;
  12. map[Sex.FEMALE.ordinal()] = 2;
  13. }
  14. }
  15. public static void foo(Sex sex) {
  16. int x = $MAP.map[sex.ordinal()];
  17. switch (x) {
  18. case 1:
  19. System.out.println("男");
  20. break;
  21. case 2:
  22. System.out.println("女");
  23. break;
  24. }
  25. }

八、枚举类

JDK 7 新增了枚举类

转换后代码

  1. public final class Sex extends Enum<Sex> {
  2. public static final Sex MALE;
  3. public static final Sex FEMALE;
  4. private static final Sex[] $VALUES;
  5. static {
  6. MALE = new Sex("MALE", 0);
  7. FEMALE = new Sex("FEMALE", 1);
  8. $VALUES = new Sex[]{MALE, FEMALE};
  9. }
  10. /**
  11. * Sole constructor. Programmers cannot invoke this constructor.
  12. * It is for use by code emitted by the compiler in response to
  13. * enum type declarations.
  14. *
  15. * @param name - The name of this enum constant, which is the identifier
  16. * used to declare it.
  17. * @param ordinal - The ordinal of this enumeration constant (its position
  18. * in the enum declaration, where the initial constant is
  19. assigned
  20. */
  21. private Sex(String name, int ordinal) {
  22. super(name, ordinal);
  23. }
  24. public static Sex[] values() {
  25. return $VALUES.clone();
  26. }
  27. public static Sex valueOf(String name) {
  28. return Enum.valueOf(Sex.class, name);
  29. }
  30. }

九、try-with-resources

JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources

  1. try(资源变量 = 创建资源对象){
  2. } catch( ) {
  3. }

资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码

  1. public static void main(String[] args) {
  2. try(InputStream is = new FileInputStream("d:\\1.txt")) {
  3. System.out.println(is);
  4. } catch (IOException e) {
  5. e.printStackTrace();
  6. }
  7. }

会被转换为

  1. public static void main(String[] args) {
  2. try {
  3. InputStream is = new FileInputStream("d:\\1.txt");
  4. Throwable t = null;
  5. try {
  6. System.out.println(is);
  7. } catch (Throwable e1) {
  8. // t 是我们代码出现的异常
  9. t = e1;
  10. throw e1;
  11. } finally {
  12. // 判断了资源不为空
  13. if (is != null) {
  14. // 如果我们代码有异常
  15. if (t != null) {
  16. try {
  17. is.close();
  18. } catch (Throwable e2) {
  19. // 如果 close 出现异常,作为被压制异常添加
  20. t.addSuppressed(e2);
  21. }
  22. } else {
  23. // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
  24. is.close();
  25. }
  26. }
  27. }} catch (IOException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }

设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法,是为了防止异常信 息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常)

  1. public static void main(String[] args) {
  2. try (MyResource resource = new MyResource()) {
  3. int i = 1/0;
  4. } catch (Exception e) {
  5. e.printStackTrace();
  6. }
  7. }
  8. class MyResource implements AutoCloseable {
  9. public void close() throws Exception {
  10. throw new Exception("close 异常");
  11. }
  12. }

输出:

  1. java.lang.ArithmeticException: / by zero
  2. at test.Test6.main(Test6.java:7)
  3. Suppressed: java.lang.Exception: close 异常
  4. at test.MyResource.close(Test6.java:18)
  5. at test.Test6.main(Test6.java:6)

如以上代码所示,两个异常信息都不会丢

十、方法重写时的桥接方法

方法重写时对返回值分两种情况

1、父子类的返回值完全一致

2、子类返回值可以是父类返回值的子类

  1. class A {
  2. public Number m() {
  3. return 1;
  4. }
  5. }
  6. class B extends A {
  7. @Override
  8. // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
  9. public Integer m() {
  10. return 2;
  11. }
  12. }

对于子类,java 编译器会做如下处理

  1. class B extends A {
  2. public Integer m() {
  3. return 2;
  4. }
  5. // 此方法才是真正重写了父类 public Number m() 方法
  6. public synthetic bridge Number m() {
  7. // 调用 public Integer m()
  8. return m();
  9. }
  10. }

桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突,可以 用下面反射代码来验证

  1. for (Method m : B.class.getDeclaredMethods()) {
  2. System.out.println(m);
  3. }

输出

  1. public java.lang.Integer test.candy.B.m()
  2. public java.lang.Number test.candy.B.m()

十一、匿名内部类

  1. public static void main(String[] args) {
  2. Runnable runnable = new Runnable() {
  3. @Override
  4. public void run() {
  5. System.out.println("ok");
  6. }
  7. };
  8. }

转换后代码

  1. // 额外生成的类
  2. final class Candy11$1 implements Runnable {
  3. Candy11$1() {
  4. }
  5. public void run() {
  6. System.out.println("ok");
  7. }
  8. }
  9. public class Candy11 {
  10. public static void main(String[] args) {
  11. Runnable runnable = new Candy11$1();
  12. }
  13. }

引用局部变量的匿名内部类,源代码

  1. public class Candy11 {
  2. public static void test(final int x) {
  3. Runnable runnable = new Runnable() {
  4. @Override
  5. public void run() {
  6. System.out.println("ok:" + x);
  7. }
  8. };
  9. }
  10. }

转换后代码

  1. // 额外生成的类
  2. final class Candy11$1 implements Runnable {
  3. int val$x;
  4. Candy11$1(int x) {
  5. this.val$x = x;
  6. }
  7. public void run() {
  8. System.out.println("ok:" + this.val$x);
  9. }
  10. }
  11. public class Candy11 {
  12. public static void test(final int x) {
  13. Runnable runnable = new Candy11$1(x);
  14. }
  15. }

注:

这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Candy11$1 对象时,将 x 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 不应该再发生变 化了,如果变化,那么 val$x 属性没有机会再跟着一起变化

发表评论

表情:
评论列表 (有 0 条评论,143人围观)

还没有评论,来说两句吧...

相关阅读

    相关 语法2

    语法糖,意指那些没有给计算机语言添加新功能,而只是对人类来说更“sweet”的语法,意在使得编程风格更易读。C\2.0,3.0发布的新特性,除了泛型不是语法糖,其他所有的新特性

    相关 语法汇总

    首先需要声明的是“语法糖”这个词绝非贬义词,它可以给我带来方便,是一种便捷的写法,编译器会帮我们做转换;而且可以提高开发编码的效率,在性能上也不会带来损失。这让java开发人员

    相关 语法 语法

            语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添

    相关 Java语法

    > 语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某种语法,这种语法对语言本身功能来说没有什么影响,只是为了方便程序员的开发,提高开发效率。说白

    相关 java语法

    [java语法糖][java] 语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某种语法,这种语法对语言本身功能来说没有什么影响,只是为了方

    相关 Java语法

    Java中语法糖原理、解语法糖 语法糖:switch 支持 String 与枚举、泛型、自动装箱与拆箱、方法变长参数、枚举、内部类、条件编译、 断言、数值字面量、for-ea