>类库下载 >java类库 >고품질 코드 작성: Java 프로그램 개선을 위한 팁: 예외

고품질 코드 작성: Java 프로그램 개선을 위한 팁: 예외

高洛峰
高洛峰원래의
2016-10-14 15:53:071636검색

Java 예외에는 세 가지 메커니즘이 있습니다.

Error 클래스와 해당 하위 클래스는 오류를 나타냅니다. 이는 VirtualMachineError 가상 머신 오류와 같이 프로그래머가 처리할 필요가 없고 처리할 수 없는 예외입니다. ThreadDeath 스레드 좀비 등.

RunTimeException 클래스와 해당 하위 클래스는 시스템에서 처리할 수 있는 예외인 확인되지 않은 예외를 나타냅니다. 가장 일반적인 예외는 범위를 벗어난 NullPointException 및 IndexOutOfBoundsException입니다.

예외 클래스와 그 하위 클래스(검사되지 않은 예외 제외)는 프로그래머가 처리해야 하는 예외입니다. 예를 들어 IOException은 I/O 예외를 나타냅니다. SQLException으로 표시되는 액세스 예외입니다.

객체 생성 과정은 메모리 할당, 정적 코드 초기화, 생성자 실행 등을 거친다는 것을 알고 있습니다. 객체 생성의 핵심 단계는 생성자에서도 예외를 발생시킬 수 있나요? ? Java 구문 관점에서는 세 가지 유형의 예외가 모두 허용되지만 시스템 설계 및 개발 관점에서는 세 가지 유형의 예외를 사용하지 않도록 노력합니다. 이를 설명하기 위한 예외입니다.

(1) 생성자에서 발생한 오류는 프로그래머가 처리할 수 없습니다.

생성자가 실행될 때 VirtualMachineError 가상 머신 오류가 발생하면 프로그래머는 해결책이 없습니다. 에서는 이러한 오류의 발생을 예측할 수 없으며 이를 포착하고 처리할 수 없습니다.

(2) 생성자는 확인되지 않은 예외를 발생시키지 않아야 합니다.

이러한 예를 살펴보겠습니다.

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

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

이 코드의 의도는 다음과 같습니다. 분명히 18세 미만의 사용자는 Person 인스턴스 객체를 생성하지 않을 것입니다. 객체가 없으면 클래스 동작 doSomething 메서드를 실행할 수 없습니다. 그러나 이는 예측할 수 없는 결과를 초래합니다.

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

당연히 p 객체는 RunTimeException이기 때문에 생성할 수 없습니다. 개발자가 이를 포착할 수도 있고 그렇지 않을 수도 있습니다. 코드가 논리적으로 정확하고 결함이 없는 것처럼 보이지만 실제로는 이 프로그램입니다. 예외가 발생하여 실행할 수 없습니다. 이 코드는 두 가지 경고를 제공합니다.

상위 수준 코드 작성자의 부담을 증가시킵니다. 이 RuntimeException 예외를 포착하면 누가 이 예외에 대해 알려줄까요? 문서 제약 조건을 통해서만 Person 클래스의 생성자가 리팩터링되고 확인되지 않은 다른 예외가 발생하면 기본 메서드는 수정 없이 테스트를 통과할 수 있지만 여기에는 숨겨진 결함이 있을 수 있으며 여전히 결함을 작성하기가 매우 어렵습니다. 재현하기 어렵습니다. 이는 RuntimeException을 포착하지 않는다는 우리의 일반적인 생각입니다. 확인되지 않은 예외로 작성되었기 때문에 메인 메소드의 코더는 이 예외를 전혀 처리할 수 없습니다. 최악의 경우 Person 메소드가 실행되지 않습니다. 이는 매우 위험합니다. 예외가 발생하면 전체 스레드가 더 이상 실행되지 않거나 링크가 닫히지 않거나 데이터가 데이터베이스에 기록되지 않거나 메모리 예외가 발생하여 시스템에 영향을 미칩니다. 전체 시스템.

다음 코드는 실행되지 않습니다. 기본 메소드의 구현자는 원래 p 객체의 설정을 코드 로직의 일부로 사용하기를 원했습니다. doSomething 메소드를 실행한 후 다른 로직을 완료해야 합니다. 하지만 검사되지 않은 항목이 없기 때문에 예외가 발생하고 결국 JVM에 예외가 발생하게 됩니다. 이로 인해 전체 스레드 실행이 끝난 후 모든 후속 코드가 계속 실행되지 않게 되어 치명적인 영향을 미칩니다. 비즈니스 로직에 대해.

(3) 생성자는 가능한 한 확인된 예외를 발생시키지 않도록 노력해야 합니다.

다음 예를 보면 코드는 다음과 같습니다.

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

    }
}

생성자에서 검사된 예외를 던질 때의 세 가지 단점을 보여주는 간단한 단락 코드입니다.

하위 클래스 팽창을 유발합니다. 이 예에서 하위 클래스의 인수 없는 생성자는 부모 클래스의 매개 변수가 없기 때문에 생략할 수 없습니다. 생성자는 IOException을 발생시킵니다. 하위 클래스의 매개 변수 없는 생성자는 기본적으로 상위 클래스의 생성자를 호출하므로 하위 클래스의 매개 변수 없는 생성자는 IOException 또는 해당 상위 클래스도 발생시켜야 합니다.

리스코프 대체 원칙 위반: "리스코프 대체 원칙"은 상위 클래스가 나타날 수 있는 곳에 하위 클래스가 나타날 수 있으며 상위 클래스를 하위 클래스로 대체해도 예외가 발생하지 않음을 의미합니다. 그런 다음 다시 살펴보고 Sub 클래스가 Base 클래스를 대체할 수 있는지 확인합니다. 예를 들어 상위 수준 코드는 다음과 같이 작성됩니다.

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

그런 다음 new Base()를 new로 대체할 것으로 예상됩니다. Sub(), 그리고 코드를 정상적으로 컴파일하고 실행할 수 있습니다. Sub 생성자가 예외를 발생시키기 때문에 컴파일이 실패하는 것은 유감스러운 일입니다. 부모 클래스의 생성자보다 더 많은 예외가 발생하고 이를 해결하려면 새로운 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语法机制。

  将以上三种异常类型汇总起来,对于构造函数,错误只能抛出,这是程序人员无能为力的事情;非受检异常不要抛出,抛出了 " 对己对人 " 都是有害的;受检异常尽量不抛出,能用曲线的方式实现就用曲线方式实现,总之一句话:在构造函数中尽可能不出现异常。

  注意 :在构造函数中不要抛出异常,尽量曲线实现。

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


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.