Java异常的机制有三种:程序员
咱们知道,一个对象的建立过程通过内存分配,静态代码初始化、构造函数执行等过程,对象生成的关键步骤是构造函数,那是否是也容许在构造函数中抛出异常呢?从Java语法上来讲,彻底能够在构造函数中抛出异常,三类异常均可以,可是从系统设计和开发的角度来分析,则尽可能不要在构造函数中抛出异常,咱们以三种不一样类型的异常来讲明之。数据库
(1)、构造函数中抛出错误是程序员没法处理的编程
在构造函数执行时,若发生了VirtualMachineError虚拟机错误,那就没招了,只能抛出,程序员不能预知此类错误的发生,也就不能捕捉处理。缓存
(2)、构造函数不该该抛出非受检异常ide
咱们来看这样一个例子,代码以下:函数
class Person { public Person(int _age) { // 不满18岁的用户对象不能创建 if (_age < 18) { throw new RuntimeException("年龄必须大于18岁."); } } public void doSomething() { System.out.println("doSomething......"); } }
这段代码的意图很明显,年龄不满18岁的用户不会生成一个Person实例对象,没有对象,类行为doSomething方法就不可执行,想法很好,但这会致使不可预测的结果,好比咱们这样引用Person类: 性能
public static void main(String[] args) { Person p = new Person(17); p.doSomething(); /*其它的业务逻辑*/ }
很显然,p对象不能创建,由于是一个RunTimeException异常,开发人员能够捕捉也能够不捕捉,代码看上去逻辑很正确,没有任何瑕疵,可是事实上,这段程序会抛出异常,没法执行。这段代码给了咱们两个警示:测试
(3)、构造函数尽量不要抛出受检异常优化
咱们来看下面的例子,代码以下:this
//父类 class Base { // 父类抛出IOException public Base() throws IOException { throw new IOException(); } } //子类 class Sub extends Base { // 子类抛出Exception异常 public Sub() throws Exception { } }
就这么一段简单的代码,展现了在构造函数中抛出受检异常的三个不利方面:
public static void main(String[] args) { try { Base base = new Base(); } catch (Exception e) { e.printStackTrace(); } }
而后,咱们指望把new Base()替换成new Sub(),并且代码可以正常编译和运行。很是惋惜,编译不经过,缘由是Sub的构造函数抛出了Exception异常,它比父类的构造函数抛出更多的异常范围要宽,必须增长新的catch块才能解决。
可能你们要问了,为何Java的构造函数容许子类的构造函数抛出更普遍的异常类呢?这正好与类方法的异常机制相反,类方法的异常是这样要求的:
// 父类 class Base { // 父类方法抛出Exception public void testMethod() throws Exception { } } // 子类 class Sub extends Base { // 父类方法抛出Exception @Override public void testMethod() throws IOException { } }
子类的方法能够抛出多个异常,但都必须是覆写方法的子类型,对咱们的例子来讲,Sub类的testMethod方法抛出的异常必须是Exception的子类或Exception类,这是Java覆写的要求。构造函数之因此于此相反,是由于构造函数没有覆写的概念,只是构造函数间的引用调用而已,因此在构造函数中抛出受检异常会违背里氏替换原则原则,使咱们的程序缺少灵活性。
3.子类构造函数扩展受限:子类存在的缘由就是指望实现扩展父类的逻辑,但父类构造函数抛出异常却会让子类构造函数的灵活性大大下降,例如咱们指望这样的构造函数。
// 父类 class Base { public Base() throws IOException{ } } // 子类 class Sub extends Base { public Sub() throws Exception{ try{ super(); }catch(IOException e){ //异常处理后再抛出 throw e; }finally{ //收尾处理 } } }
很不幸,这段代码编译不经过,缘由是构造函数Sub没有把super()放在第一句话中,想把父类的异常从新包装再抛出是不可行的(固然,这里有不少种 “曲线” 的实现手段,好比从新定义一个方法,而后父子类的构造函数都调用该方法,那么子类构造函数就能够自由处理异常了),这是Java语法机制。
将以上三种异常类型汇总起来,对于构造函数,错误只能抛出,这是程序人员无能为力的事情;非受检异常不要抛出,抛出了 " 对己对人 " 都是有害的;受检异常尽可能不抛出,能用曲线的方式实现就用曲线方式实现,总之一句话:在构造函数中尽量不出现异常。
注意 :在构造函数中不要抛出异常,尽可能曲线实现。
AOP编程能够很轻松的控制一个方法调用哪些类,也可以控制哪些方法容许被调用,通常来讲切面编程(好比AspectJ),只能控制到方法级别,不能实现代码级别的植入(Weave),好比一个方法被类A的m1方法调用时返回1,在类B的m2方法调用时返回0(同参数状况下),这就要求被调用者具备识别调用者的能力。在这种状况下,可使用Throwable得到栈信息,而后鉴别调用者并分别输出,代码以下:
class Foo { public static boolean method() { // 取得当前栈信息 StackTraceElement[] sts = new Throwable().getStackTrace(); // 检查是不是methodA方法调用 for (StackTraceElement st : sts) { if (st.getMethodName().equals("methodA")) { return true; } } return false; } } //调用者 class Invoker{ //该方法打印出true public static void methodA(){ System.out.println(Foo.method()); } //该方法打印出false public static void methodB(){ System.out.println(Foo.method()); } }
注意看Invoker类,两个方法methodA和methodB都调用了Foo的method方法,都是无参调用,返回值却不一样,这是咱们的Throwable类发挥效能了。JVM在建立一本Throwable类及其子类时会把当前线程的栈信息记录下来,以便在输出异常时准肯定位异常缘由,咱们来看Throwable源代码。
public class Throwable implements Serializable { private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0]; //出现异常记录的栈帧 private StackTraceElement[] stackTrace = UNASSIGNED_STACK; //默认构造函数 public Throwable() {
//记录栈帧 fillInStackTrace(); } //本地方法,抓取执行时的栈信息 private native Throwable fillInStackTrace(int dummy); public synchronized Throwable fillInStackTrace() { if (stackTrace != null || backtrace != null /* Out of protocol state */) { fillInStackTrace(0); stackTrace = UNASSIGNED_STACK; } return this; } }
在出现异常时(或主动声明一个Throwable对象时),JVM会经过fillInStackTrace方法记录下栈帧信息,而后生成一个Throwable对象,这样咱们就能够知道类间的调用顺序,方法名称及当前行号等了。
得到栈信息能够对调用者进行判断,而后决定不一样的输出,好比咱们的methodA和methodB方法,一样地输入参数,一样的调用方法,可是输出却不一样,这看起来很想是一个bug:方法methodA调用method方法正常显示,而方法methodB调用却会返回错误数据,所以咱们虽然能够根据调用者的不一样产生不一样的逻辑,但这仅局限在对此方法的普遍认知上,更多的时候咱们使用method方法的变形体,代码以下:
class Foo { public static boolean method() { // 取得当前栈信息 StackTraceElement[] sts = new Throwable().getStackTrace(); // 检查是不是methodA方法调用 for (StackTraceElement st : sts) { if (st.getMethodName().equals("methodA")) { return true; } } throw new RuntimeException("除了methodA方法外,该方法不容许其它方法调用"); } }
只是把“return false” 替换成了一个运行期异常,除了methodA方法外,其它方法调用都会产生异常,该方法经常使用做离线注册码校验,让破解者视图暴力破解时,因为执行者不是指望的值,所以会返回一个通过包装和混淆的异常信息,大大增长了破解难度。
异常只为异常服务,这是何解?难道异常还能为其它服务不成?确实能,异常本来是正常逻辑的一个补充,可是有时候会被当作主逻辑使用,看以下代码:
//判断一个枚举是否包含String枚举项 public static <T extends Enum<T>> boolean Contain(Class<T> clz,String name){ boolean result = false; try{ Enum.valueOf(clz, name); result = true; }catch(RuntimeException e){ //只要是抛出异常,则认为不包含 } return result; }
判断一个枚举是否包含指定的枚举项,这里会根据valueOf方法是否抛出异常来进行判断,若是抛出异常(通常是IllegalArgumentException异常),则认为是不包含,若不抛出异常则能够认为包含该枚举项,看上去这段代码很正常,可是其中有是哪一个错误:
咱们这段代码是用一段异常实现了一个正常的业务逻辑,这致使代码产生了坏味道。要解决从问题也很容易,即不在主逻辑中实使用异常,代码以下:
// 判断一个枚举是否包含String枚举项 public static <T extends Enum<T>> boolean Contain(Class<T> clz, String name) { // 遍历枚举项 for (T t : clz.getEnumConstants()) { // 枚举项名称是否相等 if (t.name().equals(name)) { return true; } } return false; }
异常只能用在非正常的状况下,不能成为正常状况下的主逻辑,也就是说,异常是是主逻辑的辅助场景,不能喧宾夺主。
并且,异常虽然是描述例外事件的,但能避免则避免之,除非是确实没法避免的异常,例如:
public static void main(String[] args) { File file = new File("a.txt"); try { FileInputStream fis = new FileInputStream(file); // 其它业务处理 } catch (FileNotFoundException e) { e.printStackTrace(); // 异常处理 } }
这样一段代码常常在咱们的项目中出现,但常常写并不表明不可优化,这里的异常类FileNotFoundException彻底能够在它诞生前就消除掉:先判断文件是否存在,而后再生成FileInputStream对象,这也是项目中常见的代码:
public static void main(String[] args) { File file = new File("a.txt"); // 常常出现的异常,能够先作判断 if (file.exists() && !file.isDirectory()) { try { FileInputStream fis = new FileInputStream(file); // 其它业务处理 } catch (FileNotFoundException e) { e.printStackTrace(); // 异常处理 } } }
虽然增长了if判断语句,增长了代码量,可是却减小了FileNotFoundException异常出现的概率,提升了程序的性能和稳定性。
咱们知道异常是主逻辑的例外逻辑,举个简单的例子来讲,好比我在马路上走(这是主逻辑),忽然开过一辆车,我要避让(这是受检异常,必须处理),继续走着,忽然一架飞机从我头顶飞过(非受检异常),咱们能够选在继续行走(不捕捉),也能够选择指责其噪音污染(捕捉,主逻辑的补充处理),再继续走着,忽然一颗流星砸下来,这没有选择,属于错误,不能作任何处理。这样具有完整例外场景的逻辑就具有了OO的味道,任何一个事务的处理均可能产生非预期的效果,问题是须要以何种手段来处理,若是不使用异常就须要依靠返回值的不一样来进行处理了,这严重失去了面向对象的风格。
咱们在编写用例文档(User case Specification)时,其中有一项叫作 " 例外事件 ",是用来描述主场景外的例外场景的,例如用户登陆的用例,就会在" 例外事件 "中说明" 连续3此登陆失败即锁定用户帐号 ",这就是登陆事件的一个异常处理,具体到咱们的程序中就是:
public void login(){ try{ //正常登录 }catch(InvalidLoginException lie){ // 用户名无效 }catch(InvalidPasswordException pe){ //密码错误的异常 }catch(TooMuchLoginException){ //屡次登录失败的异常 } }
如此设计则可让咱们的login方法更符合实际的处理逻辑,同时使主逻辑(正常登陆,try代码块)更加清晰。固然了,使用异常还有不少优势,可让正常代码和异常代码分离、能快速查找问题(栈信息快照)等,可是异常有一个缺点:性能比较慢。
Java的异常机制确实比较慢,这个"比较慢"是相对于诸如String、Integer等对象来讲的,单单从对象的建立上来讲,new一个IOException会比String慢5倍,这从异常的处理机制上也能够解释:由于它要执行fillInStackTrace方法,要记录当前栈的快照,而String类则是直接申请一个内存建立对象,异常类慢一筹也就在所不免了。
并且,异常类是不能缓存的,指望先创建大量的异常对象以提升异常性能也是不现实的。
难道异常的性能问题就没有任何能够提升的办法了?确实没有,可是咱们不能由于性能问题而放弃使用异常,并且通过测试,在JDK1.6下,一个异常对象的建立时间只需1.4毫秒左右(注意是毫秒,一般一个交易是在100毫秒左右),难道咱们的系统连如此微小的性能消耗都不予许吗?
注意:性能问题不是拒绝异常的借口。