Es gibt drei Mechanismen für Java-Ausnahmen:
Die Error-Klasse und ihre Unterklassen stellen Fehler dar. Es handelt sich um eine Ausnahme, die vom Programmierer nicht behandelt werden muss und nicht behandelt werden kann, z. B. VirtualMachineError, Fehler der virtuellen Maschine. ThreadDeath-Thread-Zombie usw. .
Die RunTimeException-Klasse und ihre Unterklassen stellen Ausnahmen dar, die vom System ausgelöst werden können oder nicht. Die klassischsten Ausnahmen sind NullPointException und IndexOutOfBoundsException.
Die Ausnahmeklasse und ihre Unterklassen (mit Ausnahme ungeprüfter Ausnahmen) stellen geprüfte Ausnahmen dar. Wenn sie nicht behandelt werden, kann das Programm nicht kompiliert werden Zugriffsausnahme, dargestellt durch SQLException.
Wir wissen, dass der Erstellungsprozess eines Objekts die Speicherzuweisung, die Initialisierung des statischen Codes, die Ausführung des Konstruktors usw. durchläuft. Der Schlüsselschritt bei der Objektgenerierung ist der Konstruktor. Darf er auch Ausnahmen im Konstruktor auslösen? ? Aus Sicht der Java-Syntax können Sie im Konstruktor alle drei Arten von Ausnahmen auslösen. Aus Sicht des Systemdesigns und der Entwicklung sollten Sie jedoch versuchen, keine Ausnahmen im Konstruktor auszulösen. Ausnahme zur Veranschaulichung.
(1). Im Konstruktor ausgelöste Fehler können vom Programmierer nicht behandelt werden.
Wenn beim Ausführen des Konstruktors ein VirtualMachineError-Fehler in der virtuellen Maschine auftritt, gibt es keine Lösung kann das Auftreten solcher Fehler nicht vorhersagen und sie nicht erkennen.
(2) Der Konstruktor sollte keine ungeprüften Ausnahmen auslösen
Schauen wir uns ein solches Beispiel an:
class Person { public Person(int _age) { // 不满18岁的用户对象不能建立 if (_age < 18) { throw new RuntimeException("年龄必须大于18岁."); } } public void doSomething() { System.out.println("doSomething......"); } }
Die Absicht dieses Codes Offensichtlich können Benutzer unter 18 Jahren kein Person-Instanzobjekt generieren. Ohne ein Objekt kann die Methode „DoSomething“ nicht ausgeführt werden. Dies führt jedoch zu unvorhersehbaren Ergebnissen Klasse wie diese:
public static void main(String[] args) { Person p = new Person(17); p.doSomething(); /*其它的业务逻辑*/ }
Offensichtlich kann das p-Objekt nicht erstellt werden, da es eine RunTimeException ist. Entwickler können es abfangen oder nicht. Der Code scheint logisch korrekt zu sein und weist keine Fehler auf, aber tatsächlich ist dieses Programm wird eine Ausnahme auslösen, die nicht ausgeführt werden kann. Dieser Code gibt uns zwei Warnungen:
erhöht die Belastung für Codeschreiber auf höherer Ebene: Fangen Sie diese RuntimeException-Ausnahme ab. Wer wird mir dann von dieser Ausnahme erzählen? Nur durch Dokumenteinschränkungen kann die Hauptmethode den Test ohne Änderung bestehen, sobald der Konstruktor der Person-Klasse umgestaltet ist und andere ungeprüfte Ausnahmen auslöst. Es kann jedoch zu versteckten Fehlern kommen, und es ist immer noch sehr schwierig, Fehler zu schreiben schwierig zu reproduzieren. Dies ist unsere übliche Idee, diese RuntimeException nicht abzufangen. Da sie als ungeprüfte Ausnahme geschrieben wurde, kann der Codierer der Hauptmethode diese Ausnahme überhaupt nicht verarbeiten. Dies ist sehr gefährlich. Sobald eine Ausnahme auftritt, wird der gesamte Thread nicht mehr ausgeführt, die Verbindung wird nicht geschlossen, die Daten werden nicht in die Datenbank geschrieben oder es tritt eine Speicherausnahme auf, die sich auf die Ausführung auswirkt Gesamtsystem.
Der nachfolgende Code wird nicht ausgeführt: Der Implementierer der Hauptmethode wollte ursprünglich die Einrichtung des p-Objekts als Teil seiner Codelogik verwenden. Nach der Ausführung der doSomething-Methode muss andere Logik vervollständigt werden. aber weil es keine nicht überprüfte Ausnahme gibt, wird sie abgefangen und schließlich an die JVM geworfen. Dies führt dazu, dass der gesamte nachfolgende Code nicht mehr ausgeführt wird, nachdem die Ausführung des gesamten Threads abgeschlossen ist fatale Auswirkungen auf die Geschäftslogik.
(3) Der Konstruktor sollte versuchen, keine geprüften Ausnahmen auszulösen
Schauen wir uns das folgende Beispiel an:
//父类 class Base { // 父类抛出IOException public Base() throws IOException { throw new IOException(); } } //子类 class Sub extends Base { // 子类抛出Exception异常 public Sub() throws Exception { } }
Es ist So ein einfacher Absatzcode, der drei Nachteile des Auslösens geprüfter Ausnahmen in Konstruktoren aufzeigt:
Verursacht eine Aufblähung der Unterklasse: In unserem Beispiel kann der Konstruktor ohne Argumente der Unterklasse nicht weggelassen werden, da die übergeordnete Klasse den Parameterlosen Konstruktor auslöst eine IOException. Der parameterlose Konstruktor der Unterklasse ruft standardmäßig den Konstruktor der übergeordneten Klasse auf, daher muss der parameterlose Konstruktor der Unterklasse auch eine IOException oder ihre übergeordnete Klasse auslösen.
Verstößt gegen das Liskov-Substitutionsprinzip: „Das Liskov-Substitutionsprinzip“ bedeutet, dass Unterklassen dort erscheinen können, wo die übergeordnete Klasse erscheinen kann, und das Ersetzen der übergeordneten Klasse durch die Unterklasse keine Ausnahme verursacht. Dann schauen wir zurück und schauen, ob die Sub-Klasse die Base-Klasse ersetzen kann. Unser übergeordneter Code ist beispielsweise so geschrieben:
public static void main(String[] args) { try { Base base = new Base(); } catch (Exception e) { e.printStackTrace(); } }
Dann erwarten wir, new Base() durch new zu ersetzen Sub() und der Code können normal kompiliert und ausgeführt werden. Schade, dass die Kompilierung fehlschlägt, da der Unterkonstruktor eine Ausnahme auslöst als der Konstruktor der übergeordneten Klasse und eine größere Auswahl an Ausnahmen aufweist. Zur Lösung des Problems muss ein neuer Catch-Block hinzugefügt werden.
Sie fragen sich vielleicht, warum der Konstruktor von Java dem Konstruktor einer Unterklasse erlaubt, eine größere Auswahl an Ausnahmeklassen auszulösen? Dies ist genau das Gegenteil des Ausnahmemechanismus von Klassenmethoden, der Folgendes erfordert:
// 父类 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语法机制。
将以上三种异常类型汇总起来,对于构造函数,错误只能抛出,这是程序人员无能为力的事情;非受检异常不要抛出,抛出了 " 对己对人 " 都是有害的;受检异常尽量不抛出,能用曲线的方式实现就用曲线方式实现,总之一句话:在构造函数中尽可能不出现异常。
注意 :在构造函数中不要抛出异常,尽量曲线实现。
建议115:使用Throwable获得栈信息
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方法外,其它方法调用都会产生异常,该方法常用作离线注册码校验,让破解者视图暴力破解时,由于执行者不是期望的值,因此会返回一个经过包装和混淆的异常信息,大大增加了破解难度。
回到顶部
建议116:异常只为异常服务
异常只为异常服务,这是何解?难道异常还能为其它服务不成?确实能,异常原本是正常逻辑的一个补充,但是有时候会被当做主逻辑使用,看如下代码:
//判断一个枚举是否包含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异常),则认为是不包含,若不抛出异常则可以认为包含该枚举项,看上去这段代码很正常,但是其中有是哪个错误:
异常判断降低了系统的性能
降低了代码的可读性,只有详细了解valueOf方法的人才能读懂这样的代码,因为valueOf抛出的是一个非受检异常
隐藏了运行期可能产生的错误,catch到异常,但没有做任何处理。
我们这段代码是用一段异常实现了一个正常的业务逻辑,这导致代码产生了坏味道。要解决从问题也很容易,即不在主逻辑中实使用异常,代码如下:
// 判断一个枚举是否包含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异常出现的几率,提高了程序的性能和稳定性。
回到顶部
建议117:多使用异常,把性能问题放一边
我们知道异常是主逻辑的例外逻辑,举个简单的例子来说,比如我在马路上走(这是主逻辑),突然开过一辆车,我要避让(这是受检异常,必须处理),继续走着,突然一架飞机从我头顶飞过(非受检异常),我们可以选在继续行走(不捕捉),也可以选择指责其噪音污染(捕捉,主逻辑的补充处理),再继续走着,突然一颗流星砸下来,这没有选择,属于错误,不能做任何处理。这样具备完整例外场景的逻辑就具备了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毫秒左右),难道我们的系统连如此微小的性能消耗都不予许吗?