1. 语法糖

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

1.1. 默认构造方法

如果没有写任何构造方法,编译器会自动生成一个默认的无参构造方法

  1. public class Test {
  2. }

编译成 class 后的代码:

  1. public class Test {
  2. // 这个无参构造是编译器帮助我们加上的
  3. public Test() {
  4. // 即调用父类Object的无参构造方法,即调用java/lang/0bject. "<init>":()V
  5. super();
  6. }
  7. }

1.2. 自动拆装箱

包装类型和基本类型,在 JDK5 之后能自动转换。

  1. public class Test {
  2. public static void main(String[] args) {
  3. Integer x = 1;
  4. int y = x;
  5. }
  6. }

编译成 class 后的代码:

  1. public class Test {
  2. public static void main(String[] args) {
  3. Integer x = Integer.valueOf(1); // 自动装箱
  4. int y = x.intValue(); // 自动拆箱
  5. }
  6. }

1.3. 泛型取值

JDK5 在编译泛型代码时,会执行类型擦除操作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理。

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

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

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

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

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

虽然会有类型擦除,但是会在 class 文件中的 LocalVariableTypeTable 中,保存泛型信息,用来做自动类型转换。因此可以使用反射获取泛型信息。

  1. public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
  2. }
  1. Method test = Test.class.getMethod("test", List.class, Map.class);
  2. Type[] types = test.getGenericParameterTypes();
  3. for (Type type : types) {
  4. if (type instanceof ParameterizedType) {
  5. ParameterizedType parameterizedType = (ParameterizedType) type;
  6. Ѕуѕtеmutrіntln("原始类型 - " + раrаmеtеrіzеdТуре.gеtRаwТуре());
  7. Type[] arguments = parameterizedType.getActualTypeArguments() ;
  8. for (int i = 0; i < arguments.length; i++) {
  9. System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
  10. }
  11. }
  12. }

输出

原始类型 - interface java.util.List

泛型参数[0] - class java.lang.String

原始类型 - interface java.util.Map

泛型参数[0] - class java.lang.Integer

泛型参数[1] - class java.lang.Object

1.4. 可变参数

可变参数 String... args 会在编译期转换为 String[] args

  1. public class Test {
  2. public static void main(String[] args) {
  3. // 实际上是 test(new String[]{"Hello", "World!"});
  4. test("Hello", "World!");
  5. // 实际上是 test(new String[]{});
  6. test();
  7. }
  8. public static void test(String... args) {
  9. String[] array = args;
  10. System.outrіntln(array);
  11. }
  12. }

1.5. foreach 循环

  1. public class Test {
  2. public static void main(String[] args) {
  3. int[] array = {1, 2, 3, 4, 5};
  4. for (int e : array) {
  5. System.out.println(e);
  6. }
  7. List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
  8. for (Integer e : list) {
  9. System.out.println(e);
  10. }
  11. }
  12. }

编译后:

  1. public class Test {
  2. public Test() {
  3. }
  4. public static void main(String[] args) {
  5. // 对于数组,直接使用下标迭代
  6. int[] array = new int[]{1, 2, 3, 4, 5};
  7. for(int i = 0; i < array.length; ++i) {
  8. int e = array[i];
  9. System.out.println(e);
  10. }
  11. // 对于集合,则使用迭代器迭代
  12. List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
  13. Iterator iter = list.iterator();
  14. while(iter.hasNext()) {
  15. Integer e = (Integer)iter.next();
  16. System.out.println(e);
  17. }
  18. }
  19. }

1.6. switch 字符串

从 JDK7 开始,switch 语句可以作用于字符串和枚举类。

  1. public class Test {
  2. public void test(String s) {
  3. switch (s) {
  4. case "你好": {
  5. System.out.println("你好");
  6. break;
  7. }
  8. case "世界": {
  9. System.out.println("世界");
  10. break;
  11. }
  12. }
  13. }
  14. }

编译后:

  1. public class Test {
  2. public Test() {
  3. }
  4. public void test(String s) {
  5. byte var2 = -1;
  6. // 先计算字符串的hash值,再匹配得到一个变量值
  7. switch(s.hashCode()) {
  8. case 649718:
  9. if (s.equals("世界")) var2 = 1;
  10. break;
  11. case 652829:
  12. if (s.equals("你好")) var2 = 0;
  13. }
  14. // 再匹配变量值
  15. switch(var2) {
  16. case 0:
  17. System.out.println("你好");
  18. break;
  19. case 1:
  20. System.out.println("世界");
  21. }
  22. }
  23. }

执行了两遍 switch 语句,第一遍是根据字符串的 hashCodeequals 将字符串的转换为相应 byte 类型,第二遍才是利用 byte 执行进行比较。

hashCode 是为了提高效率,减少可能的比较;而 equals 为了防止 hashCode 冲突。

例如 BMC. 这两个字符串的 hashCode 值都是 2123。

  1. public class Test {
  2. public void test(String s) {
  3. switch (s) {
  4. case "BM": {
  5. System.out.println("BM");
  6. break;
  7. }
  8. case "C.": {
  9. System.out.println("C.");
  10. break;
  11. }
  12. }
  13. }
  14. }

编译后:

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

1.7. switch 枚举

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

编译后:

  1. public class Test {
  2. public Test() {
  3. }
  4. // 会生成一个合成的内部类(JVM可见,我们不可见)
  5. static class $MAP {
  6. // 数组大小即为枚举元素个数,里面存储case用来对比的数字
  7. static int[] map = new int[2];
  8. static {
  9. map[Sex.MALE.ordinal()] = 1;
  10. map[Sex.FEMALE.ordinal()] = 2;
  11. }
  12. }
  13. public void test(Sex sex) {
  14. // 从数组中取出对应的值
  15. int x = $MAP.map[sex.ordinal()];
  16. switch(x) {
  17. case 1:
  18. System.out.println("男");
  19. break;
  20. case 2:
  21. System.out.println("女");
  22. }
  23. }
  24. }

1.8. 枚举

  1. public enum Sex {
  2. MALE, FEMALE
  3. }

编译后:

  1. // 本质上是一个class
  2. public final class Sex extends Enum<Sex> [
  3. public static final Sex MALE; // 对应枚举值
  4. public static final Sex FEMALE; // 对应枚举值
  5. private static final Sex[] $VALUES;
  6. static {
  7. MALE = new Sex("MALE", 0); // 对应枚举值
  8. FEMALE = new Sex("FEMALE", 1); // 对应枚举值
  9. $VALUES = new Sex[]{MALE, FEMALE};
  10. }
  11. private Sex(String name, int ordinal) {
  12. super(name, ordinal);
  13. }
  14. public static Sex[] values() {
  15. return $VALUES.clone();
  16. }
  17. public static Sex valueOf(String name) {
  18. return Enum.valueOf(Sex.class, name);
  19. }
  20. }

1.9. try-with-resources

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

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

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

  1. public class Test {
  2. public void test() {
  3. try (InputStream is = new FileInputStream("D:\\test.txt")) {
  4. System.out.println(is);
  5. } catch (IOException e) {
  6. e.printStackTrace();
  7. }
  8. }
  9. }

编译后:

  1. public class Test {
  2. public void test() {
  3. try {
  4. FileInputStream is = new FileInputStream("D:\\test.txt");
  5. try {
  6. System.out.println(is);
  7. } catch (Throwable e1) {
  8. // 我们自己的代码出现异常
  9. try {
  10. // 尝试关闭
  11. is.close();
  12. } catch (Throwable e2) {
  13. // close出现异常,作为被压制的异常添加
  14. e1.addSuppressed(e2);
  15. }
  16. // 抛出异常
  17. throw e1;
  18. }
  19. is.close();
  20. } catch (IOException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }

1.10. 方法重写时的桥接方法

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

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

编译后:

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

1.11. 匿名内部类

  1. public class Test {
  2. // 匿名内部类实现接口并重写接口方法
  3. Runnable runnable = new Runnable() {
  4. @Override
  5. public void run() {
  6. }
  7. };
  8. // 匿名内部类重写类方法
  9. Object object = new Object() {
  10. };
  11. // 静态常量 + 匿名内部类实现接口并重写接口方法
  12. static final Runnable runnable2 = new Runnable() {
  13. @Override
  14. public void run() {
  15. }
  16. };
  17. }

编译后会生成三个内部类 class:

Test$1.classnew Runnable() {} 的内容:

  1. import com.company.Test;
  2. class Test$1 implements Runnable {
  3. // 持有外部类的引用
  4. final Test this$0;
  5. // 外部类的引用通过构造方法传入
  6. Test$1(Test this$0) {
  7. this.this$0 = this$0;
  8. }
  9. public void run() {}
  10. }

Test$2.classnew Object() {} 的内容:

  1. import com.company.Test;
  2. class Test$2 {
  3. // 持有外部类的引用
  4. final Test this$0;
  5. // 外部类的引用通过构造方法传入
  6. Test$1(Test this$0) {
  7. this.this$0 = this$0;
  8. }
  9. }

Test$3.classstatic final Runnable runnable2 = new Runnable() {} 的内容:

  1. class Test$3 implements Runnable {
  2. public void run() {}
  3. }

前两种写法,内部类会持有外部类的引用。如果内部类的生命周期比外部类长,则外部类容易导致内存泄漏。