Java提供了多种 不同口味的语法糖,每种口味的糖都有不同的食用方法和魔力,你知道哪几种口味的糖呢? 让我们一起来尝一尝吧
泛型和类型擦除
-
Java中的泛型只存在于编译阶段,因为只有在编译的时候会对泛型的类型进行校验,在运行期,其实是没有泛型的概念,在经过javac编译之后,就已经将泛型擦除了。
-
示例1——判断字节码对象
public class Test { // public static void main(String[] args) { ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); // true 不同的泛型,但是class对象是相同的 System.out.println(list1.getClass() == list2.getClass()); } }
-
示例2——反射获取运行期对象
public static void main(String[] args) throws Exception { List<String> list = new ArrayList<>(); list.add("一"); Method method = list.getClass().getMethod("add", Object.class); method.invoke(list, 2); System.out.println(list); // [一, 2] }
通过上面的示例,可以看到在运行期可以直接将不同的类型设置到集合中,虽然该集合的泛型指定的是其他的类型,泛型只会在编译期对类型进行限制
自动装箱、拆箱
-
在使用Integer、int等包装类和基础类型进行判断的时候,我们无需进行引用类型和基础类型的转换就可以直接比较,众所周知,引用类型如果直接判断,那么比较的是内存地址,那么是怎么判断的呢?
-
原理
// Integer 和 int 直接进行比较 public static void main(String[] args) throws Exception { Integer a = new Integer(1); int b = 1; System.out.println(a == b); // true } // Integer的自动拆箱原理 public static void main(String[] args) throws Exception { Integer a = new Integer(1); int b = 1; // 拆箱 int i = a.intValue(); } // Integer的自动装箱 public static void main(String[] args) throws Exception { Integer c = 1; // c 经过装箱为Integer对象 Integer d = Integer.valueOf(1); }
实际上自动装箱和拆箱是通过调用
Integer
的intValue()
和valueOf()
方法完成,在包装类需要转换为基础类型 或 基础类型需要转换为包装类的时候,JVM会自动调用方法帮助我们完成转换。
遍历循环
-
forEach
增强for循环是我们在遍历数组或者集合中最常用的方式了,那么forEach是如何做到支持简洁的代码实现循环的呢?其实,forEach
也是一种语法糖~ -
示例:
- 原始遍历方式:
public static void main(String[] args) throws Exception { List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); for (String s : list) { System.out.println(s); } }
- 编译之后的代码:
public static void main(String[] args) throws Exception { List<String> list = new ArrayList(); list.add("1"); list.add("2"); list.add("3"); Iterator var2 = list.iterator(); while(var2.hasNext()) { String s = (String)var2.next(); System.out.println(s); } }
通过反编译的代码可以看到,其实
forEach
就是使用迭代器来进行循环的,只是迭代器繁琐的操作交给JVM去做了,我们只需要使用简洁的forEach
就可以完成循环操作
条件编译
-
JVM会对if语句的条件进行判断,将不会成立的分支的代码消除掉,减少代码指令
-
示例:
- 源代码:
public static void main(String[] args) throws Exception { if(true) { System.out.println("true"); } else { System.out.println(false); } }
- 反编译代码:
public static void main(String[] args) throws Exception { System.out.println("true"); }
反编译后,消除了不可能成立的分支代码,将5行代码变为了1行代码,这也是JVM对代码的优化方式,条件编译只可能出现在判断条件为常量的形式中
swtich支持字符串
-
在jdk1.7之前,switch支持的类型有:
byte
、short
、char
、int
这几个基本数据类型和对应的包装类。其实switch后面只能写int
,但是byte
、short
、char
可以转换为int(向大的数据类型提升),对于大数据类型long
、double
、float
等必须强制转换为int
才可以。但是string
是不可以的会报错,那么jdk1.8之后,string
是怎么支持的呢? -
原理:
jdk1.8并没有新增指令来处理
string
类型,而是通过string#hashcode
计算出每个字符串的hash
值,对switch
中的case进行匹配看到这里,你可能会有疑问了,如果发生hash冲突怎么办? JVM开发团队当然也想到了,和HashMap的解决方案相同,只需要通过
hashcode
和equals
方法就可以保证在hash冲突的情况下判断是否相等 -
示例:
- 源代码:
public static void main(String[] args) { String str = "a"; switch (str) { case "a": System.out.println("a"); break; case "b": System.out.println("b"); break; } }
- 反编译代码:
public static void main(String[] args) { String str = "a"; byte var3 = -1; // 先判断hashcode的值,然后判断equals是否相等,如果相等,则赋值int变量 switch(str.hashCode()) { case 97: // ‘a’的hashcode计算出来为97 if (str.equals("a")) { var3 = 0; } break; case 98: if (str.equals("b")) { var3 = 1; } } // 还是需要通过int的switch进行判断 switch(var3) { case 0: System.out.println("a"); break; case 1: System.out.println("b"); } }
try-with-resource
-
先看看try-catch-finally在操作流时的代码
-
示例:读取文件并正确关闭流的操作
public static void main(String[] args) { InputStream in = null; try { in = new FileInputStream("D://file.txt"); int read = in.read(); } catch (Exception e) { e.printStackTrace(); } finally { if(in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } }
啊这~ 读取文件就一行代码,但是保证流安全的代码竟然这么多
-
try-with-resource示例
public static void main(String[] args) { try(InputStream in = new FileInputStream("D://file.txt")) { int read = in.read(); } catch (Exception e) { e.printStackTrace(); } }
将读取代码操作放到try语句块中,自动帮我们安全的关闭流,如何做到的呢?其实和上面的原始的
try-catch-finally
基本是一样的,只不过这些繁琐的操作由JVM来搞定了而已- 反编译代码
public static void main(String[] args) { try { InputStream in = new FileInputStream("D://file.txt"); Throwable var2 = null; try { int var3 = in.read(); } catch (Throwable var12) { var2 = var12; throw var12; } finally { if (in != null) { if (var2 != null) { try { in.close(); } catch (Throwable var11) { var2.addSuppressed(var11); } } else { in.close(); } } } } catch (Exception var14) { var14.printStackTrace(); } }
和上面的自己处理异常基本是一致的,不过JVM处理的更加完善,并且提供了甜甜的糖衣来使用
变长参数
-
当给方法传递参数的时候,不确定传递多少个的时候,就可以使用变长参数来作为方法的参数,不过变长参数实质上就是数组,所以可以接收多个参数,在方法中处理的时候,需要将变长参数作为数组进行处理
-
示例
- 变长参数的类型:
public static void main(String[] args) { int a = 1; int b = 2; int c = 3; handle(a, b, c); } private static void handle(int a, int... arr) { System.out.println(arr.getClass().getSimpleName()); // int[] }
可以看到变长参数的类型为
int[]
,可以说明虽然传递了多个参数,但是将这些参数都保存到数组中了,需要注意的是,变长参数只能放到参数列表的最后一个,并且一个方法中只能有一个变长参数
枚举
-
你知道枚举的实质是
class
吗?其实枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自java.lang.Enum
,并自动添加了values和valueOf方法我们来一起看看枚举反编译之后的样子吧
-
源代码:
public enum Skip { ADD, SUBTRACT, MULTIPLY, DIVIDE }
-
反编译代码:
- 第一步编译:
javac Skip.java
- 第二步反编译:
javap -c -v Skip.class
Compiled from "Skip.java" public final class Skip extends java.lang.Enum<Skip> minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM Constant pool: #1 = Fieldref #4.#38 // Skip.$VALUES:[LSkip; #2 = Methodref #39.#40 // "[LSkip;".clone:()Ljava/lang/Object; #3 = Class #23 // "[LSkip;" #4 = Class #41 // Skip #5 = Methodref #16.#42 // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; #6 = Methodref #16.#43 // java/lang/Enum."<init>":(Ljava/lang/String;I)V #7 = String #17 // ADD #8 = Methodref #4.#43 // Skip."<init>":(Ljava/lang/String;I)V #9 = Fieldref #4.#44 // Skip.ADD:LSkip; #10 = String #19 // SUBTRACT #11 = Fieldref #4.#45 // Skip.SUBTRACT:LSkip; #12 = String #20 // MULTIPLY #13 = Fieldref #4.#46 // Skip.MULTIPLY:LSkip; #14 = String #21 // DIVIDE #15 = Fieldref #4.#47 // Skip.DIVIDE:LSkip; #16 = Class #48 // java/lang/Enum #17 = Utf8 ADD #18 = Utf8 LSkip; #19 = Utf8 SUBTRACT #20 = Utf8 MULTIPLY #21 = Utf8 DIVIDE #22 = Utf8 $VALUES #23 = Utf8 [LSkip; #24 = Utf8 values #25 = Utf8 ()[LSkip; #26 = Utf8 Code #27 = Utf8 LineNumberTable #28 = Utf8 valueOf #29 = Utf8 (Ljava/lang/String;)LSkip; #30 = Utf8 <init> #31 = Utf8 (Ljava/lang/String;I)V #32 = Utf8 Signature #33 = Utf8 ()V #34 = Utf8 <clinit> #35 = Utf8 Ljava/lang/Enum<LSkip;>; #36 = Utf8 SourceFile #37 = Utf8 Skip.java #38 = NameAndType #22:#23 // $VALUES:[LSkip; #39 = Class #23 // "[LSkip;" #40 = NameAndType #49:#50 // clone:()Ljava/lang/Object; #41 = Utf8 Skip #42 = NameAndType #28:#51 // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; #43 = NameAndType #30:#31 // "<init>":(Ljava/lang/String;I)V #44 = NameAndType #17:#18 // ADD:LSkip; #45 = NameAndType #19:#18 // SUBTRACT:LSkip; #46 = NameAndType #20:#18 // MULTIPLY:LSkip; #47 = NameAndType #21:#18 // DIVIDE:LSkip; #48 = Utf8 java/lang/Enum #49 = Utf8 clone #50 = Utf8 ()Ljava/lang/Object; #51 = Utf8 (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; { public static final Skip ADD; descriptor: LSkip; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final Skip SUBTRACT; descriptor: LSkip; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final Skip MULTIPLY; descriptor: LSkip; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static final Skip DIVIDE; descriptor: LSkip; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM public static Skip[] values(); descriptor: ()[LSkip; flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #1 // Field $VALUES:[LSkip; 3: invokevirtual #2 // Method "[LSkip;".clone:()Ljava/lang/Object; 6: checkcast #3 // class "[LSkip;" 9: areturn LineNumberTable: line 5: 0 public static Skip valueOf(java.lang.String); descriptor: (Ljava/lang/String;)LSkip; flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: ldc #4 // class Skip 2: aload_0 3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; 6: checkcast #4 // class Skip 9: areturn LineNumberTable: line 5: 0 static {}; descriptor: ()V flags: ACC_STATIC Code: stack=4, locals=0, args_size=0 0: new #4 // class Skip 3: dup 4: ldc #7 // String ADD 6: iconst_0 7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 10: putstatic #9 // Field ADD:LSkip; 13: new #4 // class Skip 16: dup 17: ldc #10 // String SUBTRACT 19: iconst_1 20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 23: putstatic #11 // Field SUBTRACT:LSkip; 26: new #4 // class Skip 29: dup 30: ldc #12 // String MULTIPLY 32: iconst_2 33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 36: putstatic #13 // Field MULTIPLY:LSkip; 39: new #4 // class Skip 42: dup 43: ldc #14 // String DIVIDE 45: iconst_3 46: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V 49: putstatic #15 // Field DIVIDE:LSkip; 52: iconst_4 53: anewarray #4 // class Skip 56: dup 57: iconst_0 58: getstatic #9 // Field ADD:LSkip; 61: aastore 62: dup 63: iconst_1 64: getstatic #11 // Field SUBTRACT:LSkip; 67: aastore 68: dup 69: iconst_2 70: getstatic #13 // Field MULTIPLY:LSkip; 73: aastore 74: dup 75: iconst_3 76: getstatic #15 // Field DIVIDE:LSkip; 79: aastore 80: putstatic #1 // Field $VALUES:[LSkip; 83: return LineNumberTable: line 6: 0 line 7: 13 line 8: 26 line 9: 39 line 5: 52 } Signature: #35 // Ljava/lang/Enum<LSkip;>; SourceFile: "Skip.java"
通过反编译的代码可以看到,
Skip extends java.lang.Enum
定义的枚举类就是一个Class
对象,并且继承java.lang.Enum
类,自动生成了values
方法和valueOf
方法。每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。另外通过把clone、readObject、writeObject这三个方法定义为final的,同时实现是抛出相应的异常。这样保证了每个枚举类型及枚举常量都是不可变的。可以利用枚举的这两个特性来实现线程安全的单例。
- 第一步编译:
说了这么多关于语法糖,那你知道解语法糖是在javac编译的哪个阶段解除糖衣的吗?没错就是在分析与字节码生成阶段,在这个阶段中javac编译器会专门对语法糖进行解除,将糖衣还原为原始代码
下次在吃语法糖的时候,不要忘记在每个糖衣下面的源代码哦~
微信公众号「指尖上的代码」,欢迎关注~
原创不易, 点个赞再走呗~ 欢迎关注,给你带来更精彩的文章!
你的点赞和关注是写文章最大的动力~