首页 > 其他 > 详细

07-多态&异常&前端编译与优化(待补充)

时间:2021-05-20 00:09:37      阅读:40      评论:0      收藏:0      [点我收藏+]

1. 多态

1.1 示例代码

/**
 * 禁用指针压缩:
 *  -XX:-UseCompressedOops
 *  -XX:-UseCompressedClassPointers
 */
public class DuoTaiTest {
    public static void test(Animal animal) {
        animal.eat();
        System.out.println(animal.toString());
    }

    public static void main(String[] args) throws IOException {
        test(new Cat());
        test(new Dog());
        System.in.read();
    }
}

abstract class Animal {
    public abstract void eat();

    @Override
    public String toString() {
        return "我是" + this.getClass().getSimpleName();
    }
}
class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗吃肉");
    }
}
class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }
}

1.2 运行时分析

(1) 停在 System.in.read() 方法上,这时运行 jps 获取进程 id;

(2) 进入 JDK 安装目录,执行 java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB,进入图形界面 attach <进程id>;

技术分享图片

(3) 打开 Tools → Find Object By Query,输入 select d from cn.itcast.jvm.t3.bytecode.Dog d 后点击 Execute 执行;

技术分享图片

(4) 查看对象内存结构:点击超链接可以看到对象的内存结构,此对象没有任何属性,因此只有对象头的 16 字节,前 8 字节是 MarkWord,后 8 字节就是对象的 Class 指针,但目前看不到它的实际地址。此时,可以通过 Windows → Console 进入命令行模式,执行 mem 0x00000001299b4978 2,参数 1 是对象地址,参数 2 是查看 2 行(即 16 字节),结果中的第 2 行 0x000000001b7d4028 即为 Class 的内存地址。

(5) 查看类的 vtable:① Alt+R 进入 Inspector 工具,输入刚才的 Class 内存地址,看到如下界面;② Tools → Class Browser 输入 Dog 查找,可以得到相同的结果。无论通过哪种方法,都可以找到 Dog.Class 的 vtable 长度为 6,意思就是 Dog 类有 6 个虚方法(多态相关的,final/static 不会列入)。那么这 6 个方法都是谁呢?

技术分享图片

(6) 可以看到视图中并未提供 vtable 的起始地址,但是已知从 Class 的起始地址开始算,固定偏移 0x1B8 就是 vtable 的起始地址。所以,通过 Windows → Console 进入命令行模式,执行 mem 0x000000001b7d41e0 6,就能得到 6 个虚方法的入口地址。

(7) 验证方法地址,通过 Tools → Class Browser 查看每个类的方法定义,比较可知:

技术分享图片

1.3 小结

当执行 invokevirtual 指令时:

  1. 先通过栈帧中的对象引用找到对象;
  2. 分析对象头,找到对象的实际 Class;
  3. Class 结构中有 vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了;
  4. 查表得到方法的具体地址;
  5. 执行方法的字节码。

2. 异常

2.1 try-catch

public class Test {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}

如下只摘出了部分字节码:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=1, locals=3, args_size=1
     0: iconst_0
     1: istore_1
     2: bipush        10
     4: istore_1
     5: goto          12
     8: astore_2
     9: bipush        20
    11: istore_1
    12: return
  Exception table:
     from    to  target type
         2     5     8   Class java/lang/Exception
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        9       3     2     e   Ljava/lang/Exception;
        0      13     0  args   [Ljava/lang/String;
        2      11     1     i   I
  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号。
  • L8 的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置。

2.2 多个 single-catch

public class Test {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 30;
        } catch (NullPointerException e) {
            i = 40;
        } catch (Exception e) {
            i = 50;
        }
    }
}

依旧只放部分重点字节码:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=1, locals=3, args_size=1
     0: iconst_0
     1: istore_1
     2: bipush        10
     4: istore_1
     5: goto          26
     8: astore_2
     9: bipush        30
    11: istore_1
    12: goto          26
    15: astore_2
    16: bipush        40
    18: istore_1
    19: goto          26
    22: astore_2
    23: bipush        50
    25: istore_1
    26: return
  Exception table:
     from    to  target type
         2     5     8   Class java/lang/ArithmeticException
         2     5    15   Class java/lang/NullPointerException
         2     5    22   Class java/lang/Exception
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        9       3     2     e   Ljava/lang/ArithmeticException;
       16       3     2     e   Ljava/lang/NullPointerException;
       23       3     2     e   Ljava/lang/Exception;
        0      27     0  args   [Ljava/lang/String;
        2      25     1     i   I

因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被复用。

2.3 multi-catch

public class Test {
  public static void main(String[] args) {
    try {
        Method test = Test.class.getMethod("test");
        test.invoke(null);
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
  }

  public static void test() {
    System.out.println("TREE");
  }
}

这种新型异常书写方式对应的字节码为:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=3, locals=2, args_size=1
     0: ldc           #2       // class Test
     2: ldc           #3       // String test
     4: iconst_0
     5: anewarray     #4       // class java/lang/Class (如下 * 代表全类名,省略了...)
     8: invokevirtual #5       // Method java/lang/Class.getMethod:(L*String;[L*Class;)L*Method;
    11: astore_1
    12: aload_1
    13: aconst_null
    14: iconst_0
    15: anewarray     #6       // class java/lang/Object
    18: invokevirtual #7       // Method java/lang/reflect/Method.invoke:(L*Object;[L*Object;)L*Object;
    21: pop
    22: goto          30
    25: astore_1
    26: aload_1
    27: invokevirtual #11      // Method java/lang/ReflectiveOperationException.printStackTrace:()V
    30: return
  Exception table:
     from    to  target type
         0    22    25   Class java/lang/NoSuchMethodException
         0    22    25   Class java/lang/IllegalAccessException
         0    22    25   Class java/lang/reflect/InvocationTargetException
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
       12      10     1  test   Ljava/lang/reflect/Method;
       26       4     1     e   Ljava/lang/ReflectiveOperationException;
        0      31     0  args   [Ljava/lang/String;

2.4 finally

public class Test {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}

字节码:

技术分享图片

  • 可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
  • 如果抛出了“剩余的异常类型”,即从 21 开始往下执行,最后会将这个异常抛出去(26: athrow)。

下面来看两个关于 finally-return 的例子:

(1) finally 中有 return

技术分享图片

  • 由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准;
  • 至于字节码中第 2 行,似乎没啥用,暂且留个伏笔,看下个例子~
  • 跟上例中的 finally 相比,这里在捉到 any 之后没有 athrow,这告诉我们:如果在 finally 中出现了 return,它会吞掉异常(athrow)!

(2) finally 对返回值影响

技术分享图片

3. synchronized

先 review 上一篇《字节码指令》的 #2.9。

public class Test {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}

字节码:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=2, locals=4, args_size=1
    -------------------- Object lock = new Object() --------------------
     0: new           #2     // class java/lang/Object
     3: dup                  // 复制栈顶元素,再压到栈顶(相当于栈顶有两个一样的数据)
     4: invokespecial #1     // Method java/lang/Object."<init>":()V
     7: astore_1             // lock_Reference -> <slot_1>(lock_Reference)
    ------------------------ synchronized start ------------------------
     8: aload_1              // <- <slot_1>(lock_Reference) 入栈
     9: dup                  // 复制(又成俩了)
    10: astore_2             // lock_Reference -> <slot_2>
    11: monitorenter         // monitorenter(还剩一个 lock_Reference 配合 monitorenter 使用)
    12: getstatic     #3     // Field java/lang/System.out:Ljava/io/PrintStream;
    15: ldc           #4     // String ok
    17: invokevirtual #5     // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    20: aload_2              // <- <slot_2>(lock_Reference) 入栈
    21: monitorexit          // monitorexit (lock_Reference) 配合栈顶lock解锁
    22: goto 30
    25: astore_3             // any -> <slot_3>
    26: aload_2              // <- <slot_2>(lock_Reference) 入栈
    27: monitorexit          // monitorexit (lock_Reference) 配合栈顶lock解锁
    28: aload_3
    29: athrow               // 抛出"any"异常
    30: return
  Exception table:
     from    to  target type
        12    22    25   any
        25    28    25   any
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      31     0  args   [Ljava/lang/String;
        8      23     1  lock   Ljava/lang/Object;

方法级别的 synchronized 不会在字节码指令中有所体现

4. 前端编译与优化

4.1 概述

在 Java 技术下谈“编译期”而没有具体上下文语境的话,其实是一句很含糊的表述:

  • 因为它可能是指一个前端编译器(叫“编译器的前端”更准确一些)把 *.java 文件转变成 *.class 文件的过程;
  • 也可能是指 Java 虚拟机的即时编译器(常称 JIT 编译器,Just In Time Compiler)运行期把字节码转变成本地机器码的过程;
  • 还可能是指使用静态的提前编译器(常称 AOT 编译器,Ahead Of Time Compiler)直接把程序编译成与目标机器指令集相关的二进制代码的过程。

下面笔者列举了这 3 类编译过程里一些比较有代表性的编译器产品:

  1. 前端编译器:JDK 的 Javac、Eclipse JDT中的增量式编译器(ECJ)。
  2. 即时编译器:HotSpot 虚拟机的 C1、C2 编译器,Graal 编译器。
  3. 提前编译器:JDK 的 Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET。

这 3 类过程中最符合普通程序员对 Java 程序编译认知的应该是第 1 类,本章标题中的“前端”指的也是这种由前端编译器完成的编译行为。

4.2 Javac 编译器

4.2.1 Javac的源码与调试

4.2.2 解析与填充符号表

4.2.3 注解处理器

4.2.4 语义分析与字节码生成

4.3 语法糖

Java 虚拟机设计团队选择把对性能的优化全部集中到运行期的即时编译器中,这样可以让那些不是由 Javac 产生的 Class 文件(如 JRuby、Groovy 等语言的 Class 文件)也同样能享受到编译器优化措施所带来的性能红利。

但是,如果把“优化”的定义放宽,把对开发阶段的优化也计算进来的话,Javac 确实是做了许多针对 Java 语言编码过程的优化措施来降低程序员的编码复杂度、提高编码效率。相当多新生的Java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖字节码或者 Java 虚拟机的底层改进来支持。

我们可以这样认为,Java 中即时编译器在运行期的优化过程,支撑了程序执行效率的不断提升;而前端编译器在编译期的优化过程,则是支撑着程序员的编码效率和语言使用者的幸福感的提高~

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

4.3.1 默认构造器

public class Candy1 {}

编译成 class 后的代码:

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

4.3.2 自动拆装箱

这个特性是 JDK 5 开始加入的:

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

这段代码在 JDK 5 之前是无法编译通过的,必须改写为如下格式:

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

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

4.3.3 泛型集合取值

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

public class Candy3 {
    public static void main(String[] args) {
        List<Integer> 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 转为 Integer, 并执行拆箱操作
int x = ((Integer) list.get(0)).intValue();

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

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: 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."<init>":()V
     7: astore_1
     8: aload_1
     9: bipush        10         // <- [装箱] -v
    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                      // 这步是把返回值出栈:boolean add(E e)
    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
  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<Ljava/lang/Integer;>;

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

public Set<Integer> test(List<String> list, Map<Integer, Object> 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] - %s\n", i, arguments[i]);
        }
    }
}

打印输出:

![](_v_images/20210507220555853_1450.png =360x)

4.3.4 可变参数

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

public class Candy4 {
    public static void foo(String... args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }

    public static void main(String[] args) {
        foo("hello", "world");
    }
}

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

public class Candy4 {
    public static void foo(String[] args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo(new String[]{"hello", "world"});
    }
}

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

4.3.5 foreach 循环

仍是 JDK 5 开始引入的语法糖:

(1) 数组的循环

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

    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);
        }
    }
}

(2) 集合的循环

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

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

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

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

4.3.6 switch

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

public class Candy6_1 {
  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 Candy6_1 {
  public Candy6_1() {}

  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 冲突,例如 BMC. 这两个字符串的 hashCode 值都是 2123 ,如此便有了如下代码:

public class Candy6_2 {
  public static void choose(String str) {
    switch (str) {
      case "BM": {
        System.out.println("h");
        break;
      }
      case "C.": {
        System.out.println("w");
        break;
      }
    }
  }
}

会被编译器转换为:

public class Candy6_2 {
    public Candy6_2() {}

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

当使用枚举时:

enum Sex {
    MALE, FEMALE
}

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

上述代码会被编译器转换为:

public class Candy7 {
    /**
     * 定义一个合成类(仅 jvm 使用,对我们不可见)
     * 用来映射枚举的 ordinal 与数组元素的关系,枚举的 ordinal 表示枚举对象的序号,从 0 开始
     * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
     */
    static class $MAP {
        // 数组大小即为枚举元素个数,里面存储 case 用来对比的数字,如 L17 所示
        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;
        }
    }
}

4.3.7 枚举类

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

enum Sex {
    MALE, FEMALE
}

转换后代码为:

public final class Sex extends Enum<Sex> {
    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", 1);
        $VALUES = new Sex[]{MALE, FEMALE};
    }

    /**
     * Sole constructor. Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     * @param name - The name of this enum constant, which is the identifier
     * used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position
     * in the enum declaration, where the initial constant is assigned.
     */

    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);
    }
}

4.3.8 try-with-resources

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

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

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

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

会被转换为:

public class Candy9 {
  public Candy9() {}

  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 = e1; // t 是我们代码出现的异常
            throw e1;
        } finally {
            if (is != null) { // 判断了资源不为空
                if (t != null) { // 如果我们代码有异常
                    try {
                        is.close();
                    } catch (Throwable e2) {
                        t.addSuppressed(e2); // 如果 close 出现异常,作为被压制异常添加
                    }
                } else {
                    is.close(); // 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
  }
}

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

public class Test {
    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 异常");
    }
}

抛出的异常堆栈信息:

java.lang.ArithmeticException: / by zero
    at test.Test.main(Test.java:7)
    Suppressed: java.lang.Exception: close 异常
        at test.MyResource.close(Test.java:18)
        at test.Test.main(Test.java:6)

4.3.9 方法重写时的桥接方法

我们都知道,方法重写时对返回值分 2 种情况:① 父子类的返回值完全一致;② 子类返回值可以是父类返回值的子类。

class A {
    public Number m() {
        return 1;
    }
}

class B extends A {
    @Override
    public Integer m() { // 子类 m 方法的返回类型 Integer 是父类 m 方法返回类型 Number 的子类
        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();
    }
}

其中「桥接方法」比较特殊,仅对 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()

review:《类文件结构》#8.3-方法重写

技术分享图片

4.3.10 匿名内部类

(1)简单使用

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

转换后代码:

// 额外生成的类
final class Candy10$1 implements Runnable {
    Candy11$1() {}

    public void run() {
        System.out.println("ok");
    }
}
// ==============================================
public class Candy10 {
    public static void main(String[] args) {
        Runnable runnable = new Candy11$1();
    }
}

(2)引用局部变量的匿名内部类

public class Candy11 {
    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 的值赋值给了 Candy11$1 对象的 val$x 属性,所以 x 之后不应该再发生变化了。如果变化,那么 val$x 属性是没机会跟着一起变的。

07-多态&异常&前端编译与优化(待补充)

原文:https://www.cnblogs.com/liujiaqi1101/p/14786609.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!