Home >类库下载 >java类库 >Writing Quality Code: Tips for Improving Java Programs: Exceptions

Writing Quality Code: Tips for Improving Java Programs: Exceptions

高洛峰
高洛峰Original
2016-10-14 15:53:071636browse

 There are three mechanisms for Java exceptions:

The Error class and its subclasses represent errors. They are exceptions that do not need to be handled by programmers and cannot be handled, such as VirtualMachineError virtual machine errors, ThreadDeath thread zombies, etc.

The RunTimeException class and its subclasses represent unchecked exceptions, which are exceptions that may be thrown by the system. Programmers can handle them or not. The most classic ones are NullPointException and IndexOutOfBoundsException out-of-bounds exceptions.

Exception class and its subclasses (excluding unchecked exceptions) represent checked exceptions. This is an exception that programmers must handle. If not handled, the program cannot be compiled. For example, IOException represents an I/O exception. , the database access exception represented by SQLException.

We know that the creation process of an object goes through memory allocation, static code initialization, constructor execution, etc. The key step in object generation is the constructor. Is it also allowed to throw exceptions in the constructor? From a Java syntax perspective, you can throw exceptions in the constructor. All three types of exceptions are acceptable. However, from the perspective of system design and development, try not to throw exceptions in the constructor. We use three different types of exceptions. exception to illustrate it.

(1). Errors thrown in the constructor cannot be handled by programmers.

When the constructor is executed, if a VirtualMachineError occurs, there is no other way but to throw it. The programmer cannot predict this. The occurrence of class errors cannot be caught and handled.

(2). The constructor should not throw unchecked exceptions

Let’s take a look at such an example. The code is as follows:

class Person {
    public Person(int _age) {
        // 不满18岁的用户对象不能建立
        if (_age < 18) {
            throw new RuntimeException("年龄必须大于18岁.");
        }
    }

    public void doSomething() {
        System.out.println("doSomething......");
    }
}

The intention of this code is obvious. Users under the age of 18 will not generate a Person. Instance objects, without objects, the class behavior doSomething method cannot be executed. The idea is good, but this will lead to unpredictable results. For example, we refer to the Person class like this:

public static void main(String[] args) {
        Person p =  new Person(17);
        p.doSomething();        /*其它的业务逻辑*/
    }

 Obviously, the p object cannot be created because it is a RunTimeException. , developers can capture it or not. The code seems logically correct and has no flaws, but in fact, this program will throw an exception and cannot be executed. This code gives us two warnings:

Increases the burden on upper-level code writers: catch this RuntimeException exception, then who will tell me about this exception? Only through document constraints, once the constructor of the Person class is refactored and throws other unchecked exceptions, the main method can be tested without modification, but there may be hidden defects here, and it is still very difficult to write. Defects that are difficult to reproduce. This is our usual idea of ​​not catching this RuntimeException. Since it has been written as an unchecked exception, the coder of the main method does not need to handle this exception at all. At worst, the Person method will not be executed! This is very dangerous. Once an exception occurs, the entire thread will no longer continue to execute, or the link is not closed, or the data is not written to the database, or a memory exception occurs, which will have an impact on the entire system.

The subsequent code will not be executed: the implementer of the main method originally wanted to use the establishment of the p object as part of its code logic. After executing the doSomething method, other logic needs to be completed, but because unchecked exceptions are not captured, The exception will eventually be thrown to the JVM, which will cause all subsequent code to no longer continue to execute after the execution of the entire thread ends, which will have a fatal impact on the business logic.

(3). The constructor should try not to throw a checked exception as much as possible

Let’s take a look at the following example, the code is as follows:

//父类
class Base {
    // 父类抛出IOException
    public Base() throws IOException {
        throw new IOException();
    }
}
//子类
class Sub extends Base {
    // 子类抛出Exception异常
    public Sub() throws Exception {

    }
}

It’s just such a simple piece of code that shows how to throw a checked exception in the constructor Three disadvantages:

Causes subclass expansion: In our example, the parameterless constructor of the subclass cannot be omitted. The reason is that the parameterless constructor of the parent class throws an IOException, and the parameterless constructor of the subclass defaults to The constructor of the parent class is called, so the parameterless constructor of the subclass must also throw IOException or its parent class.

Violates the Liskov substitution principle: The "Liskov substitution principle" means that subclasses can appear where the parent class can appear, and replacing the parent class with the subclass will not cause any exception. Then we look back and see if the Sub class can replace the Base class. For example, our upper-level code is written like this:

public static void main(String[] args) {
        try {
            Base base = new Base();
        } catch (Exception e) {    
            e.printStackTrace();
        }
    }

Then, we expect to replace new Base() with new Sub(), and the code can compile and run normally. It's a pity that the compilation fails because the Sub constructor throws an Exception. It throws more exceptions than the parent class's constructor and has a wider range of exceptions. A new catch block must be added to solve it. ​

​ You may want to ask, why does Java's constructor allow the constructor of a subclass to throw a wider range of exception classes? This is exactly the opposite of the exception mechanism of class methods. The exceptions of class methods require this:

// 父类
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毫秒左右),难道我们的系统连如此微小的性能消耗都不予许吗?


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn