Java 系列之异常与反射

本文深入探讨了Java与JavaScript在异常处理方面的差异,并详细解释了JavaWeb中异常设计的一般流程,包括如何区分显式异常与隐式异常,以及如何通过反射API创建对象和调用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

入门

和 JS 异常的比较

异常是 Java 语言的大一特点,Java 拥有比较完善的异常机制。首先一点,Java 可以自定义异常类型,这个比 JS 强大多。虽然 Java 和 JS 一样,使用 try...catch 语句可以捕获任意类型的异常,而且尽管 JS 也不是不可以自定义异常类型,但是 catch 语句不能自动辨别类型。所以某些大型的 Java 系统,便有自己的异常框架和许许多多的自定义异常类型。Java catch() 参数中如果 e 为所有异常的父类 Exception 或者接口 throwable,那么就可以在 catch 子语句中获取特定的异常。这样的做法缺点是不具体区分特定的异常,相当于强类型转换,会损失特定的方法或成员。

另外,Java 不像 JS,可以直接执行 throw 语句抛出异常(除了下面介绍的隐式异常外),要么在 try...catch 中包裹着,要么在方法声明中 throws 对应的异常,否则 Java 会认为你没有异常(程序员偷懒了)。JS 中,同样有 try..catch 语句,不同的是,throw 可以在任意位置,只不过由 try...catch 堆栈中的最接近的那个接受这个异常。

相对来说,js 比较自由,Java 的强制性会大一些,而且提供了两种处理异常的基本机制:要么在 try...catch 中包裹着,要么在方法声明中 throws 对应的异常。我是这样理解的,如果编写一个方法,行数很多,try……catch 很多,那么一种简便的方法就是 throws 所有的异常,这样代码会显得清爽很多。throws 的作用,就是当前我已经预计了有这么一些异常,但先不处理,留给调用这个方法者来处理异常,也就是用 try...catch处理(这一步是必须的)。

Java Web 异常设计

一般会把接收的异常放到 request 对象中,如 request.setAttribute('err', 上传图片不正确')

显式异常、隐式异常

我觉得下面说得很对:

“不管是什么程序开发都可能会出现各种各样的异常。可能是程序错误,也可能是业务逻辑错误。针对这个各个开发人员都有自己的处理方式,不同的风格增加了业务系统的复杂度和维护难度。所以定义好一个统一的异常处理框架还是需要的。我们开发框架采用 Java 实现,Java 中的异常一般分为两种,检查异常和运行时异常。检查异常(checked exception)有可能是程序的业务异常,这种异常一般都是开发人员自定义的、知道什么时候会抛出什么异常并进行捕捉处理。也可以是系统的异常,不捕捉编译不会通过,如  IOException、SQLException、ClassNotFoundException , 这种是必须要捕捉的并且大多都是继承 Exception。运行时异常一般都是系统抛出来的异常,这种异常不捕捉处理也不会报编译错误,如 NullPointerException,ClassCastException。运行异常都是继承至 RuntimeException。不管是检查异常还是运行时异常都是继承至 Exception。另外还有一种异常是系统错误 Error,这种异常是系统出现了故障抛出来的不能捕捉,如 OutOfMemoryError。Exception 和 Error 都是继承至 Throwable。”

  • Exception 为显式异常,需要我们要自己捕捉,也称为“受检查的异常(checked exceptions)”。
  • RuntimeException 为隐式异常,不需要我们要自己捕捉。

什么叫显式异常、隐式异常呢?如果每个异常都要我们一次次地去捕捉,不仅麻烦而且代码也显得不清爽(本来 Java 够罗嗦的了)。于是,Java 设计者就这么想,一些关键的、明显的、鲜明的异常就应该显式抛出,那些是明显能够意料到的。另外一些低级异常的,不是不能预料到,只是每次都要为这些低级的异常去抛异常、处理它们,实在够烦的。所以异常这里区分了 Exception 和 RuntimeException,其中 RuntimeException 不需要特别抛出异常(不用每次写 try… catch 或 throws ...),但是当然,尽管不用抛出,而你要接受这些 RuntimeException 还是可以的(用 try… catch(RuntimeException e)...)。常见的 RuntimeException 派生的子类有 NullPointerException、IllegalArgumentException、UnsupportedOperationException 这些等等,都是 VM 定义的。下面逐一看看。

NullPointerException 空指针

拿我们最最常见的 NullPointerException 为例子,一般对象如果是 null 的话就会抛出这个空指针异常。拿一个最二的例子看看:

  1. String s=null;  
  2. boolean eq=s.equals(""); // NullPointerException  

毫无疑问代码会出错,s 为 null 势必抛出 NullPointerException。这里我们没有处理异常。下面又是一个我们常见写代码的样子。

  1. public int getNumber(String str){    
  2.     if(str.equals("A")) return 1;    
  3.     else if(str.equals("B")) return 2;  
  4. }  

一般调用 getNumber 没问题,起码逻辑说得过去。但是,万一 str 为 null 呢?谁能保证不是?我们希望 str 不为 null,这就要额外判断了(这里我们同样没有处理异常)。这时候 NullPointerException 就被派上场了。如下

  1. public int getNumber(String str){    
  2.      if(str == nullthrow new NullPointerException("参数不能为空"); //你是否觉得明白多了     
  3.      if(str.equals("A")) return 1;     
  4.      else if(str.equals("B")) return 2;   
  5. }  

也就是说,如果我们感觉不放心、为了代码的健壮性,还是要写写的,例如这里对参数进行 null 检测。在一些特别关键、重要的变量或者参数,多写点还是必须的。

IllegalArgumentException 非法参数

实际上,跟 NullPointerException 类似的有 IllegalArgumentException,它也是 RuntimeException(有相同的特性)。不过从语义上讲 NullPointerException 仅限于空指针咯; 而 IllegalArgumentException,顾名思义,非法参数,语义上更广泛一些。还是得看你代码上下文环境来决定怎么样用,怎么让代码看的时候更清晰,——最好秒懂~哈哈。实际上,个人感觉 RuntimeException 都是这样子,按上下文决定异常类型。不是每一次都写异常,但关键的时候可以写。当然写了之后最好就要处理这个异常(不处理也行,起码出错时候能够抛出,显得更具体些)。

UnsupportedOperationException 不支持操作

UnsupportedOperationException: 该***作不被支持,如果我们希望不支持这个方法,可以抛出这个异常。既然不支持还要这个干吗?有可能子类中不想支持父类中有的方法,可以直接抛出这个异常。

NumberFormatException、ClassCastException 类型转换的

  • NumberFormatException:继承 IllegalArgumentException,字符串转换为数字时。比如 int i= Integer.parseInt("ab3");
  • ClassCastException: 类型转换错误比如 Object obj=new Object(); String s=(String)obj;

ArrayIndexOutOfBoundsException、StringIndexOutOfBoundsException 数组越界的

  • ArrayIndexOutOfBoundsException:数组越界 比如 int[] a=new int[3]; int b=a[3];
  • StringIndexOutOfBoundsException:字符串越界 比如 String s="hello"; char c=s.chatAt(6);
  • 另外还有 ArithmeticException:算术错误,典型的就是0作为除数的时候。

小结

这些异常一目了然,可以说一看到名字就知道是怎么回事了。

另外支持一点,这些隐式的异常大多在 java.lang.* 包下,所以,被形容为隐式可见是准确的,默认都 import 了进来。

前面我说了好好利用这些异常,但是如果这些异常不符合我的需求(语义上不够用)?怎么办?——那就扩展异常呗,又没说不行~呵呵

最后一图胜千言:

如图所示。

反射

在 JavaScript 中,反射是简单的。要知道一个对象有什么属性和方法?如下例即可:

[javascript] view plain copy
  1. var obj = {foo : 'a', bar: 123};  
  2. forvar i in obj)  
  3.     console.log(i); // i = 'foo' or 'bar'  

上下文知道类字符串,接着怎么转为对象呢?众所周知,eval() 很简单的。

[javascript] view plain copy
  1. console.log(eval('obj.foo')); //显示 a  

介绍 JavaScript 的反射只是在这里作抛砖引玉用的,今天我们重点谈谈 Java 的。

通过反射创建对象

正式开始之前,先说说源码位置。在 SVN 上面——地址是 点击打开链接

实例化对象 by 类名

比如说知道一个完整的类名,是 String,怎么返回这个类名对应的实例呢?用下面这个方法就可以了。

  1. /** 
  2.  * 无构造参数的实例化对象 rhino 里面也可以调用,如: var obj = 
  3.  * com.ajaxjs.Bridge.newInstanceByClassName("ajaxjs.data.entry.News"); 
  4.  * println(obj); 
  5.  *  
  6.  * @param className 
  7.  *            类全称 
  8.  * @return 对象实例,因为传入的类全称是字符串,无法创建泛型 T,所以统一返回 Object 
  9.  */  
  10. public static Object newInstance(String className);  

Java 是支持构造器重载的,而上面的例子,只能对无参的构造器实例化。要支持不同构造器实例化,传入不同的参数列表即可。

于是我们把上面的方法重载,如下。

  1. /** 
  2.  * 根据类全称创建实例 
  3.  *  
  4.  * @param className 
  5.  *            类全称 
  6.  * @param args 
  7.  *            根据构造函数,创建指定类型的对象,传入的参数个数需要与上面传入的参数类型个数一致 
  8.  * @return 对象实例,因为传入的类全称是字符串,无法创建泛型 T,所以统一返回 Object 
  9.  */  
  10. public static Object newInstance(String className, Object... args);  
相比而言,多了 args 在这个多项参数。这实际上一个数组。我们把构造器的参数传进去就可以了。如
  1. Object obj  = newInstance("com.xx.Bean""hi");  
  2. Object obj2 = newInstance("com.xx.Bean""hi""Jack");  

Ok,创建实例很简单~接着我们用类对象创建实例吧~

实例化对象 by 类对象

知道类对象的话,实例化更简单了。我把完整的代码贴出来——所谓完整代码,也是寥寥几行,故所以说,简单。

  1. /** 
  2.  * 根据类创建实例 
  3.  *  
  4.  * @param clazz 
  5.  *            类对象 
  6.  * @return 对象实例 
  7.  */  
  8. public static <T> T newInstance(Class<T> clazz) {  
  9.     try {  
  10.         return clazz.newInstance(); // 实例化 bean  
  11.     } catch (InstantiationException | IllegalAccessException e) {  
  12.         if (com.ajaxjs.core.Util.isEnableConsoleLog) e.printStackTrace();  
  13.         return null;  
  14.     }  
  15. }  

我们对静态方法实施了泛型。嗯,那非常好,妈妈再也不用担心我要不要强类型转换啦。注意实例化没有构造器传参,下面这个方法则支持多参数构造器。

  1. /** 
  2.  * 根据类对象创建实例 
  3.  *  
  4.  * @param clazz 
  5.  *            类对象 
  6.  * @param args 
  7.  *            获取指定参数类型的构造函数,这里传入我们想调用的构造函数所需的参数 
  8.  * @return 对象实例 
  9.  */  
  10. public static <T> T newInstance(Class<T> clazz, Object... args);  
本来只保留 newInstance(Class<T> clazz, Object... args); 即可,但 newInstance(Class<T> clazz) 实现机制有点不一样,于是保留之。

实例化对象 by 构造器

同样也是一句话能够搞定的事情。

  1. /** 
  2.  * 根据构造器创建实例 
  3.  *  
  4.  * @param constructor 
  5.  *            类构造器 
  6.  * @return 对象实例 
  7.  */  
  8. public static <T> T newInstance(Constructor<T> constructor, Object... args);  

不过,这个构造器对象从哪里来呢?于是,又牵涉到另外一个函数。

  1. /** 
  2.  * 获取类的构造器,可以支持重载的构造器(不同参数的构造器) 
  3.  *  
  4.  * @param clazz 
  5.  *            类对象 
  6.  * @param classes 
  7.  *            获取指定参数类型的构造函数,这里传入我们想调用的构造函数所需的参数类型 
  8.  * @return 类的构造器 
  9.  */  
  10. public static <T> Constructor<T> getConstructor(Class<T> clazz, Class<?>... classes);  

如阁下所见,getConstructor 同样支持构造器重载的。而且明显看到了,这里不是传参数本身,而是参数其类型的类对象(!?有点拗口?能看明白就好)。

小结一下,创建实例有以下方法:

  1. public static Object newInstance(String className, Object... args);   
  2. public static <T> T newInstance(Class<T> clazz); // 特别版本   
  3. public static <T> T newInstance(Class<T> clazz, Object... args);   
  4. public static <T> T newInstance(Constructor<T> constructor, Object... args);   
又因为 Java 的可变参数支持不显示 null 声明,所以省去的了重载方法,也就是说,调用 newInstance(String className, Object... args) 和 newInstance(String className) 的时候是一样的,都是同一个方法,却不用额外书写 public static Object newInstance(String className);,其他但凡有 args 如此类推。

通过反射调出、执行方法

Java 中方法也是一种对象,为 java.lang.reflect.Method 类型。

调出方法

如果只知道方法的 String 形式,那就先要调出方法对象出来先。

  1. /** 
  2.  * 根据类和参数列表获取方法对象,支持重载的方法 
  3.  * 获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法 
  4.  * @param clazz 
  5.  *            类对象 
  6.  * @param method 
  7.  *            方法名称 
  8.  * @param args 
  9.  *            对应重载方法的参数列表 
  10.  * @return 匹配的方法对象,null 表示找不到 
  11.  */  
  12. public static Method getMethod(Class<?> clazz, String method, Class<?>... args);  

执行方法

有 Method 对象后,自然可以执行,就像 obj.xxx(); 那样。

  1. /** 
  2.  * 调用方法 
  3.  *  
  4.  * @param instance 
  5.  *            对象实例 
  6.  * @param method 
  7.  *            方法对象 
  8.  * @param args 
  9.  *            参数列表 
  10.  * @return 执行结果 
  11.  */  
  12. public static Object executeMethod(Object instance, Method method, Object... args);  

被签名参数搞的有点晕?一时 Obejct、一时 Class……确实有点晕,不过必须说明的是,之所以这样安排是有根据的……然后……下面还有个重载的方法,更晕……

  1. /** 
  2.  * 调用方法 
  3.  *  
  4.  * @param instnace 
  5.  *            对象实例 
  6.  * @param method 
  7.  *            方法对象名称 
  8.  * @param args 
  9.  *            参数列表 
  10.  * @return 执行结果 
  11.  */  
  12. public static Object executeMethod(Object instnace, String method, Object... args);  

这个重载的版本是为了不知道  Method 对象而设的……具体原理很简单,请过目源码……

调用 private/protect 方法

Jvava 反射 API 的 getMethod 虽然可以调出父类或接口的方法,但有个缺点,就是不能调用 private/protect 方法——什么?你要调这些方法?恩——的确有违设计初衷和原则,故所以也比较少用。但比较 Java 反射 API 允许我们这样做的,就是利用 getDeclaredMethod。

  1. /** 
  2.  * 用 getMethod 代替更好? 循环 object 向上转型, 获取 hostClazz 对象的 DeclaredMethod 
  3.  * getDeclaredMethod()获取的是类自身声明的所有方法,包含public、protected和private方法。 
  4.  *  
  5.  * @param hostClazz 
  6.  * @param method 
  7.  *            方法名称 
  8.  * @param arg 
  9.  *            参数对象,可能是子类或接口,所以要在这里找到对应的方法,当前只支持单个参数 
  10.  * @return 匹配的方法对象,null 表示找不到 
  11.  */  
  12. public static Method getDeclaredMethod(Class<?> hostClazz, String method, Object arg);  
不过有个限制——getDeclaredMethod 不支持父类或接口。

前面的 getMehtod 亦有个限制,对参数类型,不能自动转换。就算说,我设计方法的时候,是父类或者接口,本来调用的时候应该自动转换的,是允许的。但反正居然不能识别出来。于是,找了方法解决了这个问题。上述方法对 Object arg 的类型可以支持父类了。接口的话,也要另外一个方法。

  1. /** 
  2.  * 循环 object 向上转型(接口), 获取 hostClazz 对象的 DeclaredMethod 
  3.  *  
  4.  * @param hostClazz 
  5.  * @param method 
  6.  *            方法名称 
  7.  * @param arg 
  8.  *            参数对象,可能是子类或接口,所以要在这里找到对应的方法,当前只支持单个参数 
  9.  * @return 
  10.  */  
  11. public static Method getDeclaredMethodByInterface(Class<?> hostClazz, String method, Object arg);  

结语

最后臆想一下,能不能把 getMethod() 统一起来,打造一个既可以访问 private/protect 又可以穿梭于父类、子类或参数父类、子类、接口不限定的反射方法呢?——嗯,那一定很强大!

附:单元测试

源文件 点击打开链接

  1. package test.core;  
  2.   
  3. import static org.junit.Assert.*;  
  4. import org.junit.Test;  
  5. import static com.ajaxjs.core.Reflect.*;  
  6.   
  7. public class ReflectTest {  
  8.     public static class Foo{  
  9.         public Foo(){}  
  10.           
  11.         public Foo(String str, String str2){}  
  12.           
  13.         public void Bar() {  
  14.         }  
  15.           
  16.         public void CC(String cc) {  
  17.         }  
  18.           
  19.         public String Bar2() {  
  20.             return "bar2";  
  21.         }  
  22.         public String Bar3(String arg) {  
  23.             return arg;  
  24.         }  
  25.     }  
  26.       
  27.     @Test  
  28.     public void testNewInstance(){  
  29.         assertNotNull(newInstance(ReflectTest.class));  
  30.         assertNotNull(newInstance("test.core.ReflectTest"));  
  31.         assertNotNull(newInstance(getConstructor(Foo.class)));  
  32.         assertNotNull(newInstance(getConstructor(Foo.class, String.class, String.class), "a""b"));  
  33.         assertNotNull(getClassByName("test.core.ReflectTest"));  
  34.     }  
  35.       
  36.     @Test  
  37.     public void testGetMethod(){  
  38.         assertNotNull(getMethod(Foo.class"Bar"));  
  39.         assertNotNull(getMethod(Foo.class"CC", String.class));  
  40.     }  
  41.   
  42.     @Test  
  43.     public void testExecuteMethod(){  
  44.         Foo foo = new Foo();  
  45.         executeMethod(foo, "Bar");  
  46.         executeMethod(foo, "CC""fdf");  
  47.         assertNotNull(executeMethod(foo, "Bar2"));  
  48.         assertNotNull(executeMethod(foo, "Bar3""fdf"));  
  49.     }  
  50.       
  51.     public static class A{  
  52.         public String foo(A a){  
  53.             return "A.foo";  
  54.         }  
  55.         public String bar(C c){  
  56.             return "A.bar";  
  57.         }  
  58.     }  
  59.       
  60.     public static class B extends A{  
  61.     }  
  62.       
  63.     public static interface C{  
  64.     }  
  65.       
  66.     public static class D implements C{  
  67.     }  
  68.       
  69.     @Test  
  70.     public void testDeclaredMethod(){  
  71.         assertNotNull(getDeclaredMethod(A.class"foo"new A()));  
  72.         assertNotNull(getDeclaredMethod(A.class"foo"new B()));  
  73.         assertNotNull(getDeclaredMethodByInterface(A.class"bar"new D()));  
  74.     }  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值