欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

【JVM】JVM06

时间:2023-07-11

⭐️写在前面


这里是温文艾尔の学习之路如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ博客主页 温文艾尔の学习小屋⭐️更多文章‍请关注温文艾尔主页文章发布日期:2022.02.08java学习之路!欢迎各位点赞评论收藏⭐️新年快乐朋友们jvm学习之路!⭐️上一篇内容:【JVM】JVM05(从字节码角度分析i++和++i的执行流程))

文章目录

⭐️1编译期处理(语法糖)⭐️1.1默认构造器⭐️1.2自动拆装箱⭐️1.3泛型集合取值⭐️1.4可变参数⭐️1.5 foreach循环⭐️1.6switch字符串⭐️1.7switch枚举⭐️1.8枚举类⭐️1.9 try-with-resources⭐️1.10方法重写时的桥接方法⭐️1.11匿名内部类


⭐️1编译期处理(语法糖)

所谓语法糖,其实就是指java编译器把*.java源码编译为*.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利(给糖吃)

注意以下代码的分析,借助javap工具,idea的反编译功能,idea插件jclasslib等工具。另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的java源码方式,并不是编译器还会转换出中间的java源码,切记。

⭐️1.1默认构造器

public class Candy1{}

编译成class后的代码:

public class Candy1{ //这个无参构造器是编译器帮助我们加上的 public Candy1(){ super();//即调用父类Object的无参构造方法 }}

⭐️1.2自动拆装箱

这个特性是JDK5开始加入的,代码片段1:

public class Demo01 { public static void main(String[] args) { Integer x = 1; int y = x; }}

上面代码在JDK5之前是无法编译通过的,必须改写为代码片段2:

public class Demo01 { public static void main(String[] args) { Integer x = Integer.valueOf(1); int y = x.intValue(); }}

显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转化的事情在JDK5以后都由编译器在编译阶段完成。即代码片段1都会在编译阶段被转换为代码片段2

⭐️1.3泛型集合取值

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

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

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

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

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

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

这些事情现在都不用我们自己做

擦除的是字节码上的泛型信息,可以看到LocalVariableTypeTable仍然保留了方法参数泛型的信息

Compiled from "Demo02.java"public class com.wql.jvm.Candy.Demo02 minor version: 0 major version: 52 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #8 // com/wql/jvm/Candy/Demo02 super_class: #9 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1Constant pool: #1 = Methodref #9.#29 // java/lang/Object."":()V #2 = Class #30 // java/util/ArrayList #3 = Methodref #2.#29 // java/util/ArrayList."":()V #4 = Methodref #7.#31 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #5 = InterfaceMethodref #32.#33 // java/util/List.add:(Ljava/lang/Object;)Z #6 = InterfaceMethodref #32.#34 // java/util/List.get:(I)Ljava/lang/Object; #7 = Class #35 // java/lang/Integer #8 = Class #36 // com/wql/jvm/Candy/Demo02 #9 = Class #37 // java/lang/Object #10 = Utf8 #11 = Utf8 ()V #12 = Utf8 Code #13 = Utf8 LineNumberTable #14 = Utf8 LocalVariableTable #15 = Utf8 this #16 = Utf8 Lcom/wql/jvm/Candy/Demo02; #17 = Utf8 main #18 = Utf8 ([Ljava/lang/String;)V #19 = Utf8 args #20 = Utf8 [Ljava/lang/String; #21 = Utf8 list #22 = Utf8 Ljava/util/List; #23 = Utf8 x #24 = Utf8 Ljava/lang/Integer; #25 = Utf8 LocalVariableTypeTable #26 = Utf8 Ljava/util/List; #27 = Utf8 SourceFile #28 = Utf8 Demo02.java #29 = NameAndType #10:#11 // "":()V #30 = Utf8 java/util/ArrayList #31 = NameAndType #38:#39 // valueOf:(I)Ljava/lang/Integer; #32 = Class #40 // java/util/List #33 = NameAndType #41:#42 // add:(Ljava/lang/Object;)Z #34 = NameAndType #43:#44 // get:(I)Ljava/lang/Object; #35 = Utf8 java/lang/Integer #36 = Utf8 com/wql/jvm/Candy/Demo02 #37 = Utf8 java/lang/Object #38 = Utf8 valueOf #39 = Utf8 (I)Ljava/lang/Integer; #40 = Utf8 java/util/List #41 = Utf8 add #42 = Utf8 (Ljava/lang/Object;)Z #43 = Utf8 get #44 = Utf8 (I)Ljava/lang/Object;{ public com.wql.jvm.Candy.Demo02(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/wql/jvm/Candy/Demo02; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."":()V 7: astore_1 8: aload_1 9: bipush 10 11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z 19: pop 20: aload_1 21: iconst_0 22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 27: checkcast #7 // class java/lang/Integer 30: astore_2 31: return LineNumberTable: line 14: 0 line 15: 8 line 16: 20 line 17: 31 LocalVariableTable: Start Length Slot Name Signature 0 32 0 args [Ljava/lang/String; 8 24 1 list Ljava/util/List; 31 1 2 x Ljava/lang/Integer; //局部变量类型表 ,包含了我们方法参数的一些泛型信息 LocalVariableTypeTable: Start Length Slot Name Signature 8 24 1 list Ljava/util/List;}SourceFile: "Demo02.java"

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

public Set test(List list, Map map) {}

Method test = Candy3.class.getMethod("test", List.class, Map.class);Type[] types = test.getGenericParameterTypes();for (Type type : types) { if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; System.out.println("原始类型 - " + parameterizedType.getRawType()); Type[] arguments = parameterizedType.getActualTypeArguments(); for (int i = 0; i < arguments.length; i++) { System.out.printf("泛型参数[%d] - %sn", i, arguments[i]); } }}

输出

⭐️1.4可变参数

可变参数也是JDK5开始加入的新特性:

例如:

public class Demo03 { public static void foo(String..、args){ String[] array = args; System.out.println(array); } public static void main(String[] args) { foo("hello","java"); }}

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

public class Demo03 { public static void foo(String[] args){ String[] array = args; System.out.println(array); } public static void main(String[] args) { foo(new String[]{"hello","java"}); }}

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

⭐️1.5 foreach循环

JDK5开始引入的语法糖,数组的循环:

public class Demo04 { public static void main(String[] args) { int[] array = {1,2,3,4,5};//数组赋初值的简化写法也是语法糖 for (int e:array){ System.out.println(e); } }}

会被编译器转换为:

public class Demo04 { public static void main(String[] args) { int[] array = new int[]{1,2,3,4,5};//数组赋初值的简化写法也是语法糖 for (int i = 0; i < array.length; ++i) { int e =array[i]; System.out.println(e); } }}

而集合的循环:

public class Demo05 { public static void main(String[] args) { List list = Arrays.asList(1, 2, 3, 4, 5); for (Integer i : list) { System.out.println(i); } }}

实际被编译器转换为对迭代器的调用:

public class Demo05 { public static void main(String[] args) { List list = Arrays.asList(1, 2, 3, 4, 5); Iterator iter = list.iterator(); while (iter.hasNext()){ Integer e = iter.next(); System.out.println(e); } }}

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

⭐️1.6switch字符串

从JDK7开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:

public class Demo06 { public static void choose(String str){ switch (str){ case "hello":{ System.out.println("h"); break; } case "world":{ System.out.println("w"); break; } } }}

注意:

switch配合String和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚会被编译器转换为:

public class Demo06 { public static void choose(String str){ byte x = -1; switch (str.hashCode()){ case 99162322://hello的hashCode if (str.equals("hello")){ x=0; } break; case 113318802://world的hashCode if (str.equals("world")){ x=1; } } switch (x){ case 0: System.out.println("h"); break; case 1: System.out.println("w"); } }}

可以看到,执行了两边switch,第一遍是根据字符串的hashCode和equals将字符串的转换为相应byte类型,第二遍才是利用byte执行进行比较

为什么第一遍时必须比较hashCode,有利用equals作比较呢?hashCode是为了提高效率,减少可能的比较;而equals是为了防止hashCode冲突,例如BM和C,这两个字符串的hashCode值都是2123,如果有如下代码:会被编译器转换为:

public class Demo07 { public static void choose(String str) { byte x = -1; switch (str.hashCode()){ case 2123: //hashCode值相等,需要进一步用equals比较 if (str.equals("C")){ x=0; }else if (str.equals("BM")){ x=0; } default: switch (x){ case 0: System.out.println("h"); break; case 1: System.out.println("w"); } } }}

⭐️1.7switch枚举

switch枚举的例子,原始代码:

enum Sex { MALE,FEMALE}public class Demo08 { public static void foo(Sex sex){ switch (sex){ case MALE: System.out.println("男");break; case FEMALE: System.out.println("女");break; } }}

转换后代码:

public class Demo09 { static class $MAP{ static int[] map = new int[2]; static { map[Sex.MALE.ordinal()]=1; map[Sex.FEMALE.ordinal()]=2; } } public static void foo(Sex sex){ int x = $MAP.map[sex.ordinal()]; switch (x){ case 1: System.out.println("男"); break; case 2: System.out.println("女"); break; } }}

⭐️1.8枚举类

JDK7新增了枚举类,以前面的性别枚举为例:

enum Sex { MALE,FEMALE}

转换后代码:

public final class Sex extends Enum{ public static final Sex MALE; public static final Sex FEMALE; private static final Sex[] $VALUES; static { MALE = new Sex("MALE",0); FEMALE = new Sex("FEMALE",0); $VALUES = new Sex(MALE,FEMALE); } private Sex(String name, int ordinal) { super(name, ordinal); } public static Sex[] values(){ return $VALUES.clone(); } public static Sex valueOf(String name){ return Enum.valueOf(Sex.class,name); }}

⭐️1.9 try-with-resources

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

try(资源变量 = 创建资源对象){ }catch(){ }

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

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

会被转换为:

public static void main(String[] args) { try { InputStream is = new FileInputStream("d:\1.txt"); Throwable t = null; try { System.out.println(is); } catch (Throwable e1) { // t 是我们代码出现的异常 t = e1; throw e1; } finally { // 判断了资源不为空 if (is != null) { // 如果我们代码有异常 if (t != null) { try { is.close(); } catch (Throwable e2) { // 如果 close 出现异常,作为被压制异常添加 t.addSuppressed(e2); } } else { // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e is.close(); } } } } catch (IOException e) { e.printStackTrace(); } }}

希望在关闭异常时也有可能发生异常,try中的异常和资源关闭中的异常都想保留就需要t.addSuppressed(e2);

为什么要设计一个t.addSuppressed(e2);(添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想try-with-resources生成的finally中如果抛出了异常):

例如

public class Demo12 { public static void main(String[] args) { try(MyResource resource = new MyResource()) { int i = 1/0; }catch (Exception e){ e.printStackTrace(); } }}class MyResource implements AutoCloseable{ public void close() throws Exception{ throw new Exception("close 异常"); }}

外层异常为int i = 1/0;,内层异常为资源关闭时的异常

可以看到两个异常都没有丢

⭐️1.10方法重写时的桥接方法

我们都知道,方法重写时对返回值分两种情况:

父子类的返回值完全一致子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)

class A{ public Number m(){ return 1; }}class B extends A { @Override //子类m方法的返回值是Integer是父类m方法返回值Number的子类 public Integer m() { return 2; }}

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

class B extends A { public Integer m(){ return 2; } //此方法才是真正重写了父类public Number m()方法 public synthetic bridge Number m(){ //调用public Integer m() return m(); }}

synthetic bridge的含义是合成方法,对程序员是不可见的

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

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

会输出

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

⭐️1.11匿名内部类

源代码:

public class Demo14 { public static void main(String[] args) { Runnable runnable = new Runnable(){ @Override public void run() { System.out.println("ok"); } }; }}

转换后代码:

// 额外生成的类final class Candy11$1 implements Runnable { Candy11$1() { } public void run() { System.out.println("ok"); }}

public class Candy11 { public static void main(String[] args) { Runnable runnable = new Candy11$1(); }}

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

public static void test(final int x){ Runnable runnable = new Runnable(){ @Override public void run() { System.out.println("ok"+x); } }; }

转换后代码

// 额外生成的类final class Candy11$1 implements Runnable { int val$x; Candy11$1(int x) { this.val$x = x;} public void run() { System.out.println("ok:" + this.val$x); }}public class Candy11 { public static void test(final int x) { Runnable runnable = new Candy11$1(x); }}

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

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。