入门
和 JavaScript 异常的比较
异常是 Java 语言的大一特点,Java 拥有比较完善的异常机制。首先一点,Java 可以自定义异常类型,这个比 js 强大多。虽然 Java 和 jd 一样,使用 try...catch 语句可以捕获任意类型的异常,而且尽管 js 也不是不可以自定义异常类型,但是 catch 语句却不能自动辨别类型。所以某些大型的 Java 系统,便有自己的异常框架和许许多多的自定义异常类型。Java catch() 参数中如果 e 为所有异常的父类 Exception 或者接口 throwable,那么就可以在 catch 子语句中获取特定的异常。这样的做法缺点是不具体区分特定的异常,相当于强类型转换,会损失特定的方法或成员。
相对来说,js 比较*,Java 的强制性会大一些,而且提供了两种处理异常的基本机制:要么在 try...catch 中包裹着,要么在方法声明中 throws 对应的异常,否则 Java 会认为你没有异常(程序员偷懒了)。我是这样理解的,如果编写一个方法,行数很多,try……catch 很多,那么一种简便的方法就是 throws 所有的异常,这样代码会显得清爽很多。throws 的作用,就是当前我已经预计了有这么一些异常,但先不处理,留给调用这个方法者来处理异常,也就是用 try...catch处理(这一步是必须的)。
Java 可以自定义异常类型,这个比 js 强大多。尽管 js 也不是不可以自定义异常类型,但是 catch 语句不能自动辨别类型。所以某些大型的 Java 系统,便有自己的异常框架和许许多多的自定义异常类型。但是在我这个框架中,则没那么多的异常类型。主要围绕以下几个点来设计。
- 一般程序程序错误,例如除数不能为零,不能读取某个磁盘文件等等
- 缺少某个参数,包括表单验证不通过
- 业务错误,例如用户登录密码错误,也视为异常抛出
我有一点疑惑:使用 e.printStack() 方法,不知道为什么,只能在当前上下文的 catch 语句中使用,才有出错堆栈打印出来。把 e.printStack() 放置其他地方貌似不能打印。
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 的话就会抛出这个空指针异常。拿一个最二的例子看看:
String s=null; boolean eq=s.equals(""); // NullPointerException
毫无疑问代码会出错,s 为 null 势必抛出 NullPointerException。这里我们没有处理异常。下面又是一个我们常见写代码的样子。
public int getNumber(String str){ if(str.equals("A")) return 1; else if(str.equals("B")) return 2; }
一般调用 getNumber 没问题,起码逻辑说得过去。但是,万一 str 为 null 呢?谁能保证不是?我们希望 str 不为 null,这就要额外判断了(这里我们同样没有处理异常)。这时候 NullPointerException 就被派上场了。如下
public int getNumber(String str){ if(str == null) throw new NullPointerException("参数不能为空"); //你是否觉得明白多了 if(str.equals("A")) return 1; else if(str.equals("B")) return 2; }
也就是说,如果我们感觉不放心、为了代码的健壮性,还是要写写的,例如这里对参数进行 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 中,反射是简单的。要知道一个对象有什么属性和方法?如下例即可:
var obj = {foo : 'a', bar: 123}; for( var i in obj) console.log(i); // i = 'foo' or 'bar'
上下文知道类字符串,接着怎么转为对象呢?众所周知,eval() 很简单的。
console.log(eval('obj.foo')); //显示 a
介绍 JavaScript 的反射只是在这里作抛砖引玉用的,今天我们重点谈谈 Java 的。
通过反射创建对象
正式开始之前,先说说源码位置。在 SVN 上面——地址是 点击打开链接。
实例化对象 by 类名
比如说知道一个完整的类名,是 String,怎么返回这个类名对应的实例呢?用下面这个方法就可以了。
/** * 无构造参数的实例化对象 rhino 里面也可以调用,如: var obj = * com.ajaxjs.Bridge.newInstanceByClassName("ajaxjs.data.entry.News"); * println(obj); * * @param className * 类全称 * @return 对象实例,因为传入的类全称是字符串,无法创建泛型 T,所以统一返回 Object */ public static Object newInstance(String className);
Java 是支持构造器重载的,而上面的例子,只能对无参的构造器实例化。要支持不同构造器实例化,传入不同的参数列表即可。
于是我们把上面的方法重载,如下。
/** * 根据类全称创建实例 * * @param className * 类全称 * @param args * 根据构造函数,创建指定类型的对象,传入的参数个数需要与上面传入的参数类型个数一致 * @return 对象实例,因为传入的类全称是字符串,无法创建泛型 T,所以统一返回 Object */ public static Object newInstance(String className, Object... args);相比而言,多了 args 在这个多项参数。这实际上一个数组。我们把构造器的参数传进去就可以了。如
Object obj = newInstance("com.xx.Bean", "hi"); Object obj2 = newInstance("com.xx.Bean", "hi", "Jack");
Ok,创建实例很简单~接着我们用类对象创建实例吧~
实例化对象 by 类对象
知道类对象的话,实例化更简单了。我把完整的代码贴出来——所谓完整代码,也是寥寥几行,故所以说,简单。
/** * 根据类创建实例 * * @param clazz * 类对象 * @return 对象实例 */ public static <T> T newInstance(Class<T> clazz) { try { return clazz.newInstance(); // 实例化 bean } catch (InstantiationException | IllegalAccessException e) { if (com.ajaxjs.core.Util.isEnableConsoleLog) e.printStackTrace(); return null; } }
我们对静态方法实施了泛型。嗯,那非常好,妈妈再也不用担心我要不要强类型转换啦。注意实例化没有构造器传参,下面这个方法则支持多参数构造器。
/** * 根据类对象创建实例 * * @param clazz * 类对象 * @param args * 获取指定参数类型的构造函数,这里传入我们想调用的构造函数所需的参数 * @return 对象实例 */ public static <T> T newInstance(Class<T> clazz, Object... args);本来只保留 newInstance(Class<T> clazz, Object... args); 即可,但 newInstance(Class<T> clazz) 实现机制有点不一样,于是保留之。
实例化对象 by 构造器
同样也是一句话能够搞定的事情。
/** * 根据构造器创建实例 * * @param constructor * 类构造器 * @return 对象实例 */ public static <T> T newInstance(Constructor<T> constructor, Object... args);
不过,这个构造器对象从哪里来呢?于是,又牵涉到另外一个函数。
/** * 获取类的构造器,可以支持重载的构造器(不同参数的构造器) * * @param clazz * 类对象 * @param classes * 获取指定参数类型的构造函数,这里传入我们想调用的构造函数所需的参数类型 * @return 类的构造器 */ public static <T> Constructor<T> getConstructor(Class<T> clazz, Class<?>... classes);
如阁下所见,getConstructor 同样支持构造器重载的。而且明显看到了,这里不是传参数本身,而是参数其类型的类对象(!?有点拗口?能看明白就好)。
小结一下,创建实例有以下方法:
public static Object newInstance(String className, Object... args); public static <T> T newInstance(Class<T> clazz); // 特别版本 public static <T> T newInstance(Class<T> clazz, Object... args); 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 形式,那就先要调出方法对象出来先。
/** * 根据类和参数列表获取方法对象,支持重载的方法 * 获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法 * @param clazz * 类对象 * @param method * 方法名称 * @param args * 对应重载方法的参数列表 * @return 匹配的方法对象,null 表示找不到 */ public static Method getMethod(Class<?> clazz, String method, Class<?>... args);
执行方法
有 Method 对象后,自然可以执行,就像 obj.xxx(); 那样。
/** * 调用方法 * * @param instance * 对象实例 * @param method * 方法对象 * @param args * 参数列表 * @return 执行结果 */ public static Object executeMethod(Object instance, Method method, Object... args);
被签名参数搞的有点晕?一时 Obejct、一时 Class……确实有点晕,不过必须说明的是,之所以这样安排是有根据的……然后……下面还有个重载的方法,更晕……
/** * 调用方法 * * @param instnace * 对象实例 * @param method * 方法对象名称 * @param args * 参数列表 * @return 执行结果 */ public static Object executeMethod(Object instnace, String method, Object... args);
这个重载的版本是为了不知道 Method 对象而设的……具体原理很简单,请过目源码……
调用 private/protect 方法
Jvava 反射 API 的 getMethod 虽然可以调出父类或接口的方法,但有个缺点,就是不能调用 private/protect 方法——什么?你要调这些方法?恩——的确有违设计初衷和原则,故所以也比较少用。但比较 Java 反射 API 允许我们这样做的,就是利用 getDeclaredMethod。
/** * 用 getMethod 代替更好? 循环 object 向上转型, 获取 hostClazz 对象的 DeclaredMethod * getDeclaredMethod()获取的是类自身声明的所有方法,包含public、protected和private方法。 * * @param hostClazz * @param method * 方法名称 * @param arg * 参数对象,可能是子类或接口,所以要在这里找到对应的方法,当前只支持单个参数 * @return 匹配的方法对象,null 表示找不到 */ public static Method getDeclaredMethod(Class<?> hostClazz, String method, Object arg);不过有个限制——getDeclaredMethod 不支持父类或接口。
前面的 getMehtod 亦有个限制,对参数类型,不能自动转换。就算说,我设计方法的时候,是父类或者接口,本来调用的时候应该自动转换的,是允许的。但反正居然不能识别出来。于是,找了方法解决了这个问题。上述方法对 Object arg 的类型可以支持父类了。接口的话,也要另外一个方法。
/** * 循环 object 向上转型(接口), 获取 hostClazz 对象的 DeclaredMethod * * @param hostClazz * @param method * 方法名称 * @param arg * 参数对象,可能是子类或接口,所以要在这里找到对应的方法,当前只支持单个参数 * @return */ public static Method getDeclaredMethodByInterface(Class<?> hostClazz, String method, Object arg);
结语
最后臆想一下,能不能把 getMethod() 统一起来,打造一个既可以访问 private/protect 又可以穿梭于父类、子类或参数父类、子类、接口不限定的反射方法呢?——嗯,那一定很强大!
附:单元测试
源文件 点击打开链接
package test.core; import static org.junit.Assert.*; import org.junit.Test; import static com.ajaxjs.core.Reflect.*; public class ReflectTest { public static class Foo{ public Foo(){} public Foo(String str, String str2){} public void Bar() { } public void CC(String cc) { } public String Bar2() { return "bar2"; } public String Bar3(String arg) { return arg; } } @Test public void testNewInstance(){ assertNotNull(newInstance(ReflectTest.class)); assertNotNull(newInstance("test.core.ReflectTest")); assertNotNull(newInstance(getConstructor(Foo.class))); assertNotNull(newInstance(getConstructor(Foo.class, String.class, String.class), "a", "b")); assertNotNull(getClassByName("test.core.ReflectTest")); } @Test public void testGetMethod(){ assertNotNull(getMethod(Foo.class, "Bar")); assertNotNull(getMethod(Foo.class, "CC", String.class)); } @Test public void testExecuteMethod(){ Foo foo = new Foo(); executeMethod(foo, "Bar"); executeMethod(foo, "CC", "fdf"); assertNotNull(executeMethod(foo, "Bar2")); assertNotNull(executeMethod(foo, "Bar3", "fdf")); } public static class A{ public String foo(A a){ return "A.foo"; } public String bar(C c){ return "A.bar"; } } public static class B extends A{ } public static interface C{ } public static class D implements C{ } @Test public void testDeclaredMethod(){ assertNotNull(getDeclaredMethod(A.class, "foo", new A())); assertNotNull(getDeclaredMethod(A.class, "foo", new B())); assertNotNull(getDeclaredMethodByInterface(A.class, "bar", new D())); } }