恶补 Java 系列之异常与反射

入门

和 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 了进来。

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

最后一图胜千言:

恶补 Java 系列之异常与反射

如图所示。

反射

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




上一篇:软件事务内存导论(六)配置Akka事务


下一篇:Fragment开发实战(一)