为什么泛型类不能extends Exception/Throwable,也不能通过catch捕获泛型类对象

3,341 阅读3分钟

问题1

倘若我们在 IDE 中写上类似 public class CustomException<T> extends Throwable 的代码,那么 IDE 会提示我们 Genergic class may not extends ‘java.lang.Throwable’。

原因

  • 以下为错误代码
try {
    // may throws exception
    doSomeStuff();
} catch (SomeException<Integer> e) {
    doA();
} catch (SomeException<String> e) {
    doB();
}

  • 假设当前我们有两个类 —— SomeException<Integer> 类和 SomeException<String> 类,它们都是继承自 Throwable 类的。
  • 代码中的 doSomeStuff() 方法可能是抛出 SomeException<Integer> 异常或 SomeException<String> 异常.
  • 我们针对不同的异常做出不同的逻辑操作。这样看似完全没有问题.
  • 但是熟悉泛型的小伙伴都知道,还有一种叫做类型擦除机制的存在,何为类型擦除?此处不扩展了.
  • 通俗点说:java 中不存在泛型代码,泛型代码是写给我们看的,编译器会将泛型代码转换成普通类代码。所以无论是 SomeException<Integer> 或者是 SomeException<String> 经过编译器的类型擦除后都将会变成 SomeException。
  • 故上述代码是不可以运行的,因为当代码抛出异常时编译器是无法判断走哪个 catch 分支的,所以 java 为了避免这样的问题出现,故泛型类是无法继承自 Throwable 类的。

注意

  • 泛型类无法继承自 Exception/Throwable,而泛型可以继承自 Exception/Throwable
  • 泛型类 class Generic<T>{}
  • 泛型 T,K,V,E.....
//Error 这是错误的不可以的,因为Generic是泛型类他继承Exception
class Generic<T> extends Exception{
    
}
//Success 这是可以的因为Generic虽然是泛型类但是他没有继承Exception,它只是对T进行限定类型变量为Exception本身或子类
class Generic<T extends Exception>{
    
}

问题2

  • 以下是错误代码
    //不能捕获泛型类对象
    public <T extends Throwable> void doWork(T x){
        try{

        }catch(T x){
            //do sth;
        }
    }

原因

  • 还是类型擦除的问题,我们知道catch是可以捕获多个异常的,而且异常是在运行时异常发生时捕获的,泛型异常被擦除后,有可能和另一个捕获的同一个,如下代码
  • 以下是错误代码:
    // 如果调用这个方法的时候传入一个RuntimeException 实例,
    // 那么就会出现同时捕获两个RuntimeException,而具体走哪个分支?JVM都蒙圈了
    public <T extends Throwable> void doWork(T x){
        try{

        }catch(T x){
            //do sth1;
        }catch (RuntimeException e){
            //do sth2;
        }
    }

解决

  • 虽然我们不能去catch它,但我们可以通过下面的方法区throws它
    public <T extends Throwable> void doWorkSuccess(T x) throws T{
        try{

        }catch(Throwable e){
            throw x;
        }
    }

总结思考

其实这两个问题都是源于泛型的擦除机制导致的,也就是说Java的泛型是JDK1.5后加入的,为了兼容之前的版本,其实他是一种伪泛型机制,在编译器把.java文件编译成.class文件后,泛型被擦除,变为原生类型,最能检验的就是我们都知道Java的重载,如下代码:

    //可以重载
    public void setData(String data){}
    public void setData(Integer data){}
    
    //不可以重载,IDE会报错
    public void setData2(ArrayList<String> data){}
    public void setData2(ArrayList<Integer> data){}

因为setData2在类型擦除后,他们的参数都变成了ArrayList的原生类型也就是ArrayList,所以这两个方法不是重载方法,而是一模一样的两个方法。

  • 在使用泛型的时候一定要注意泛型的类型擦除机制。