Java编程的逻辑 (24) - 异常 (上)

之前我们介绍的基本类型、类、接口、枚举都是在表示和操作数据,操作的过程中可能有很多出错的情况,出错的原因可能是多方面的,有的是不可控的内部原因,比如内存不够了、磁盘满了,有的是不可控的外部原因,比如网络连接有问题,更多的可能是程序的编程错误,比如引用变量未初始化就直接调用实例方法。

这些非正常情况在Java中统一被认为是异常,Java使用异常机制来统一处理,由于内容较多,我们分为两节来介绍,本节介绍异常的初步概念,以及异常类本身,下节主要介绍异常的处理。

我们先来通过一些例子认识一下异常。

初始异常

NullPointerException (空指针异常)

我们来看段代码:

public class ExceptionTest {
    public static void main(String[] args) {
        String s = null;
        s.indexOf("a");
        System.out.println("end");
    }
}

变量s没有初始化就调用其实例方法indexOf,运行,屏幕输出为:

Exception in thread "main" java.lang.NullPointerException
    at ExceptionTest.main(ExceptionTest.java:5)

输出是告诉我们:在ExceptionTest类的main函数中,代码第5行,出现了空指针异常(java.lang.NullPointerException)。

但,具体发生了什么呢?当执行s.indexOf("a")的时候,Java系统发现s的值为null,没有办法继续执行了,这时就启用异常处理机制,首先创建一个异常对象,这里是类NullPointerException的对象,然后查找看谁能处理这个异常,在示例代码中,没有代码能处理这个异常,Java就启用默认处理机制,那就是打印异常栈信息到屏幕,并退出程序。

在介绍函数调用原理的时候,我们介绍过栈,异常栈信息就包括了从异常发生点到最上层调用者的轨迹,还包括行号,可以说,这个栈信息是分析异常最为重要的信息。

Java的默认异常处理机制是退出程序,异常发生点后的代码都不会执行,所以示例代码中最后一行System.out.println("end")不会执行。

NumberFormatException (数字格式异常)

我们再来看一个例子,代码如下:

public class ExceptionTest {
    public static void main(String[] args) {
        if(args.length<1){
            System.out.println("请输入数字");
            return;
        }
        int num = Integer.parseInt(args[0]);
        System.out.println(num);
    }
}

args表示命令行参数,这段代码要求参数为一个数字,它通过Integer.parseInt将参数转换为一个整数,并输出这个整数。参数是用户输入的,我们没有办法强制用户输入什么,如果用户输的是数字,比如123,屏幕会输出123,但如果用户输的不是数字,比如abc,屏幕会输出:

Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:492)
    at java.lang.Integer.parseInt(Integer.java:527)
    at ExceptionTest.main(ExceptionTest.java:7)

出现了异常NumberFormatException。这个异常是怎么产生的呢?根据异常栈信息,我们看相关代码:

这是NumberFormatException类65行附近代码:

64 static NumberFormatException forInputString(String s) {
65     return new NumberFormatException("For input string: \"" + s + "\"");
66 }

这是Integer类492行附近代码:

490 digit = Character.digit(s.charAt(i++),radix);
491 if (digit < 0) {
492     throw NumberFormatException.forInputString(s);
493 }
494 if (result < multmin) {
495     throw NumberFormatException.forInputString(s);
496 }

将这两处合为一行,主要代码就是:

throw new NumberFormatException(...)

new NumberFormatException(...)是我们容易理解的,就是创建了一个类的对象,只是这个类是一个异常类。throw是什么意思呢?就是抛出异常,它会触发Java的异常处理机制。在之前的空指针异常中,我们没有看到throw的代码,可以认为throw是由Java虚拟机自己实现的。

throw关键字可以与return关键字进行对比,return代表正常退出,throw代表异常退出,return的返回位置是确定的,就是上一级调用者,而throw后执行哪行代码则经常是不确定的,由异常处理机制动态确定。

异常处理机制会从当前函数开始查找看谁"捕获"了这个异常,当前函数没有就查看上一层,直到主函数,如果主函数也没有,就使用默认机制,即输出异常栈信息并退出,这正是我们在屏幕输出中看到的。

对于屏幕输出中的异常栈信息,程序员是可以理解的,但普通用户无法理解,也不知道该怎么办,我们需要给用户一个更为友好的信息,告诉用户,他应该输入的是数字,要做到这一点,我们需要自己"捕获"异常。

"捕获"是指使用try/catch关键字,我们看捕获异常后的示例代码:

public class ExceptionTest {
    public static void main(String[] args) {
        if(args.length<1){
            System.out.println("请输入数字");
            return;
        }
        try{
            int num = Integer.parseInt(args[0]);
            System.out.println(num);
        }catch(NumberFormatException e){
            System.err.println("参数"+args[0]
                    +"不是有效的数字,请输入数字");
        }
    }
}

我们使用try/catch捕获并处理了异常,try后面的大括号{}内包含可能抛出异常的代码,括号后的catch语句包含能捕获的异常和处理代码,catch后面括号内是异常信息,包括异常类型和变量名,这里是NumberFormatException e,通过它可以获取更多异常信息,大括号{}内是处理代码,这里输出了一个更为友好的提示信息。

捕获异常后,程序就不会异常退出了,但try语句内异常点之后的其他代码就不会执行了,执行完catch内的语句后,程序会继续执行catch大括号外的代码。

这样,我们就对异常有了一个初步的了解,异常是相对于return的一种退出机制,可以由系统触发,也可以由程序通过throw语句触发,异常可以通过try/catch语句进行捕获并处理,如果没有捕获,则会导致程序退出并输出异常栈信息。异常有不同的类型,接下来,我们来认识一下。

异常类

Throwable

NullPointerException和NumberFormatException都是异常类,所有异常类都有一个共同的父类Throwable,它有4个public构造方法:

  1. public Throwable()
  2. public Throwable(String message)
  3. public Throwable(String message, Throwable cause)
  4. public Throwable(Throwable cause)

有两个主要参数,一个是message,表示异常消息,另一个是cause,表示触发该异常的其他异常。异常可以形成一个异常链,上层的异常由底层异常触发,cause表示底层异常。

Throwable还有一个public方法用于设置cause:

Throwable initCause(Throwable cause)

Throwable的某些子类没有带cause参数的构造方法,就可以通过这个方法来设置,这个方法最多只能被调用一次。

所有构造方法中都有一句重要的函数调用:

fillInStackTrace();

它会将异常栈信息保存下来,这是我们能看到异常栈的关键。

Throwable有一些常用方法用于获取异常信息:

void printStackTrace()

打印异常栈信息到标准错误输出流,它还有两个重载的方法:

void printStackTrace(PrintStream s)
void printStackTrace(PrintWriter s)

打印栈信息到指定的流,关于PrintStream和PrintWriter我们后续文章介绍。

String getMessage()
Throwable getCause()

获取设置的异常message和cause

StackTraceElement[] getStackTrace()

获取异常栈每一层的信息,每个StackTraceElement包括文件名、类名、函数名、行号等信息。

异常类体系

以Throwable为根,Java API中定义了非常多的异常类,表示各种类型的异常,部分类示意如下:

Throwable是所有异常的基类,它有两个子类Error和Exception。

Error表示系统错误或资源耗尽,由Java系统自己使用,应用程序不应抛出和处理,比如图中列出的虚拟机错误(VirtualMacheError)及其子类内存溢出错误(OutOfMemoryError)和栈溢出错误(StackOverflowError)。

Exception表示应用程序错误,它有很多子类,应用程序也可以通过继承Exception或其子类创建自定义异常,图中列出了三个直接子类:IOException(输入输出I/O异常),SQLException(数据库SQL异常),RuntimeException(运行时异常)。

RuntimeException(运行时异常)比较特殊,它的名字有点误导,因为其他异常也是运行时产生的,它表示的实际含义是unchecked exception (未受检异常),相对而言,Exception的其他子类和Exception自身则是checked exception (受检异常),Error及其子类也是unchecked exception。

checked还是unchecked,区别在于Java如何处理这两种异常,对于checked异常,Java会强制要求程序员进行处理,否则会有编译错误,而对于unchecked异常则没有这个要求。下节我们会进一步解释。

RuntimeException也有很多子类,下表列出了其中常见的一些:

异常 说明
NullPointerException 空指针异常
IllegalStateException 非法状态
ClassCastException 非法强制类型转换
IllegalArgumentException 参数错误
NumberFormatException 数字格式错误
IndexOutOfBoundsException 索引越界
ArrayIndexOutOfBoundsException 数组索引越界
StringIndexOutOfBoundsException 字符串索引越界

这么多不同的异常类其实并没有比Throwable这个基类多多少属性和方法,大部分类在继承父类后只是定义了几个构造方法,这些构造方法也只是调用了父类的构造方法,并没有额外的操作。

那为什么定义这么多不同的类呢?主要是为了名字不同,异常类的名字本身就代表了异常的关键信息,无论是抛出还是捕获异常时,使用合适的名字都有助于代码的可读性和可维护性。

自定义异常

除了Java API中定义的异常类,我们也可以自己定义异常类,一般通过继承Exception或者它的某个子类,如果父类是RuntimeException或它的某个子类,则自定义异常也是unchecked exception,如果是Exception或Exception的其他子类,则自定义异常是checked exception。

我们通过继承Exception来定义一个异常,代码如下:

public class AppException extends Exception {
    public AppException() {
        super();
    }

    public AppException(String message,
            Throwable cause) {
        super(message, cause);
    }

    public AppException(String message) {
        super(message);
    }

    public AppException(Throwable cause) {
        super(cause);
    }
}

和很多其他异常类一样,我们没有定义额外的属性和代码,只是继承了Exception,定义了构造方法并调用了父类的构造方法。

小结

本节,我们通过两个例子对异常做了基本介绍,介绍了try/catch和throw关键字及其含义,同时介绍了Throwable以及以它为根的异常类体系。

下一节,让我们进一步探讨异常。

原文地址:https://www.cnblogs.com/ivy-xu/p/12387614.html

时间: 2024-10-08 11:26:29

Java编程的逻辑 (24) - 异常 (上)的相关文章

Java编程的逻辑 (35) - 泛型 (上) - 基本概念和原理

之前章节中我们多次提到过泛型这个概念,从本节开始,我们就来详细讨论Java中的泛型,虽然泛型的基本思维和概念是比较简单的,但它有一些非常令人费解的语法.细节.以及局限性,内容比较多. 所以我们分为三节,逐步来讨论,本节我们主要来介绍泛型的基本概念和原理,下节我们重点讨论令人费解的通配符,最后一节,我们讨论一些细节和泛型的局限性. 后续章节我们会介绍各种容器类,容器类可以说是日常程序开发中天天用到的,没有容器类,难以想象能开发什么真正有用的程序.而容器类是基于泛型的,不理解泛型,我们就难以深刻理解

Java编程的逻辑 (26) - 剖析包装类 (上)

包装类 Java有八种基本类型,每种基本类型都有一个对应的包装类. 包装类是什么呢?它是一个类,内部有一个实例变量,保存对应的基本类型的值,这个类一般还有一些静态方法.静态变量和实例方法,以方便对数据进行操作. Java中,基本类型和对应的包装类如下表所示: 基本类型 包装类 boolean Boolean byte Byte short Short int Integer long Long float Float double Double char Character 包装类也都很好记,除

Java编程的逻辑 (79) - 方便的CompletionService

上节,我们提到,在异步任务程序中,一种常见的场景是,主线程提交多个异步任务,然后希望有任务完成就处理结果,并且按任务完成顺序逐个处理,对于这种场景,Java并发包提供了一个方便的方法,使用CompletionService,这是一个接口,它的实现类是ExecutorCompletionService,本节我们就来探讨它们. 基本用法 接口和类定义 与77节介绍的ExecutorService一样,CompletionService也可以提交异步任务,它的不同是,它可以按任务完成顺序获取结果,其具

Java编程的逻辑 (94) - 组合式异步编程

前面两节讨论了Java 8中的函数式数据处理,那是对38节到55节介绍的容器类的增强,它可以将对集合数据的多个操作以流水线的方式组合在一起.本节继续讨论Java 8的新功能,主要是一个新的类CompletableFuture,它是对65节到82节介绍的并发编程的增强,它可以方便地将多个有一定依赖关系的异步任务以流水线的方式组合在一起,大大简化多异步任务的开发. 之前介绍了那么多并发编程的内容,还有什么问题不能解决?CompletableFuture到底能解决什么问题?与之前介绍的内容有什么关系?

Java编程的逻辑 (19) - 接口的本质

数据类型的局限 之前我们一直在说,程序主要就是数据以及对数据的操作,而为了方便操作数据,高级语言引入了数据类型的概念,Java定义了八种基本数据类型,而类相当于是自定义数据类型,通过类的组合和继承可以表示和操作各种事物或者说对象. 但,这种只是将对象看做属于某种数据类型,并按该类型进行操作,在一些情况下,并不能反映对象以及对对象操作的本质. 为什么这么说呢?很多时候,我们实际上关心的,并不是对象的类型,而是对象的能力,只要能提供这个能力,类型并不重要.我们来看一些生活中的例子. 要拍个照片,很多

Java编程的逻辑 (87) - 类加载机制

上节,我们探讨了动态代理,在前几节中,我们多次提到了类加载器ClassLoader,本节就来详细讨论Java中的类加载机制与ClassLoader. 类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象.与之前介绍的反射.注解.和动态代理一样,在大部分的应用编程中,我们不太需要自己实现ClassLoader. 不过,理解类加载的机制和过程,有助于我们更好的理解之前介绍的内容,更好的理解Java.在反射一节,我们介绍过Class的静态方法Class.f

Java编程的逻辑 (38) - 剖析ArrayList

从本节开始,我们探讨Java中的容器类,所谓容器,顾名思义就是容纳其他数据的,计算机课程中有一门课叫数据结构,可以粗略对应于Java中的容器类,我们不会介绍所有数据结构的内容,但会介绍Java中的主要实现,并分析其基本原理和主要实现代码. 前几节在介绍泛型的时候,我们自己实现了一个简单的动态数组容器类DynaArray,本节,我们介绍Java中真正的动态数组容器类ArrayList. 我们先来看它的基本用法. 基本用法 新建ArrayList ArrayList是一个泛型容器,新建ArrayLi

Java编程的逻辑 (91) - Lambda表达式

在之前的章节中,我们的讨论基本都是基于Java 7的,从本节开始,我们探讨Java 8的一些特性,主要内容包括: 传递行为代码 - Lambda表达式 函数式数据处理 - 流 组合式异步编程 - CompletableFuture 新的日期和时间API 本节,我们先讨论Lambda表达式,它是什么?有什么用呢? Lambda表达式是Java 8新引入的一种语法,是一种紧凑的传递代码的方式,它的名字来源于学术界的λ演算,具体我们就不探讨了. 理解Lambda表达式,我们先回顾一下接口.匿名内部类和

Java编程的逻辑 (58) - 文本文件和字符流

上节我们介绍了如何以字节流的方式处理文件,我们提到,对于文本文件,字节流没有编码的概念,不能按行处理,使用不太方便,更适合的是使用字符流,本节就来介绍字符流. 我们首先简要介绍下文本文件的基本概念.与二进制文件的区别.编码.以及字符流和字节流的区别,然后我们介绍Java中的主要字符流,它们有: Reader/Writer:字符流的基类,它们是抽象类. InputStreamReader/OutputStreamWriter:适配器类,输入是InputStream,输出是OutputStream,