以下程序执行过程中发生了不正常的情况,这种情况叫做:异常

java把异常信息打印到控制台,供程序员参考,程序员看到异常信息后可以对程序进行修改,让程序更加健壮

public class ExceptionTest01 {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        int c = a / b;
        System.out.println(c);
    }
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.bjpowernode.exception.ExceptionTest01.main(ExceptionTest01.java:7)

执行过程中由JVM打印异常信息,可以进行修改:

public class ExceptionTest01 {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;
        if (b == 0){
            System.out.println(" can not by zero ");
            return;
        }
        int c = a / b;
        System.out.println(c);
    }
}

异常的存在形式

异常在Java中以类的形式存在,每一个异常类都可以创建Java对象

在上例中执行到int c = a / b;时 JVM会创建一个ArithmeticException异常对象:new ArithmeticException("by zero");

并且JVM将对象抛出 打印输出信息到控制台了

public class ExceptionTest02 {
    public static void main(String[] args) {
        //通过异常类实例化异常对象
        NumberFormatException numberFormatException = new NumberFormatException("数字格式化异常");
        System.out.println(numberFormatException);
        
        //自动调用toString方法
        /*java.lang.NumberFormatException: 数字格式化异常*/
        
    }
}

异常对应到现实生活中:

火灾:(异常类)

  • 2008年8月8日,小明家着火了(异常对象)
  • 2009年8月8日,小明家着火了(异常对象)
  • 2008年9月8日,小明家着火了(异常对象)

类是模板 对象是实际存在的个体

异常的继承结构

  • Object

    • Throwable:不管是错误还是异常都是可抛出的

      • Error:所有错误一旦发生,Java程序立即终止,错误不可被处理

        • VirtualMachineError

        • IOError

      • Exception:Exception是可以处理的

        • ExceptionSubClass:Exception的直接子类,叫做编译时异常 1

        • RunTimeException:所有的RunTimeException及子类属于运行时异常 2

          • NullPointerException

[ 2]  运行时异常在编写程序阶段可以选择处理或不处理 也被称为 未受检异常

[ 1]  不是在编译阶段发生的,在编写程序前必须对这种异常进行处理,如果不处理编译器报错 也被称为 受检异常

在编译阶段Java不会运行代码,只会检查语法是否错误,或者做一些性能的优化,例如字符串的拼接

但是如果遇到了索引越界,在编译阶段是不知道的,只能在运行时判断是否越界

编译时异常更多的是提醒程序员检查本地信息

运行时异常是代码出错导致的

作用:

  1. 查询bug的参考信息

  2. 作为方法内部一种特殊返回值,以便通知调用者底层的执行情况

异常的处理方式

只要异常没有捕捉 采取上报的方式抛给JVM,此方法的后续代码都不会执行

try语句块中某一块代码出现异常 try内后续代码不能执行

  • JVM默认的处理方案

将异常名称、异常原因、异常出现的位置等信息输出在控制台上,发生异常位置之下的代码不会执行。

throws抛出

在方法上将异常向上一级抛出,抛给上一级调用者,调用者需要对异常继续进行处理,如果main继续上抛给JVM,JVM就会终止执行程序:

public static void main(String[] args) {
    System.out.println(100 / 0);
    System.out.println("Hello World"); /*在上一行代码程序就停止执行了*/
}
public class ExceptionTest04 {
    public static void main(String[] args) {
        doSome();
    }

    /**
     * doSome方法在方法声明的时候使用了throws,表示代码执行过程中可能会出现ClassNotFoundException
     * 这个异常的直接父类是Exception,所以这是个编译时异常
     * @throws ClassNotFoundException
     */
    public static void doSome() throws ClassNotFoundException{
        System.out.println("doSome");
    }
}
        

因为方法声明位置上有 throws ClassNotFoundException;ClassNotFoundException属于Exception的直接子类 也就是编译时异常,在调用时就必须对这种异常进行预先的处理 不处理编译器就会报错

继续上抛:

public class ExceptionTest05 {
    public static void main(String[] args) throws ClassNotFoundException {
        doSome();
    }
    
    public static void doSome() throws ClassNotFoundException{
        System.out.println("doSome");
    }
}
package exception_test;  
  
import java.io.FileInputStream;  
import java.io.FileNotFoundException;  
  
/**  
 * @author LiuYiBo * @ClassName ExceptionTest06 * @description: TODO  
 * @date 2023/11/08  
 * @version: 1.0 */public class ExceptionTest06 {  
    public static void m3() throws FileNotFoundException {  
        System.out.println("m3 begin");  
        //创建一个输入流对象  
        //构造方法会抛出编译时异常 FileNotFoundException->IOException->Exception 必须进行预先处理  
        new FileInputStream("\"D:\\新建文件夹\\MarkDown\\NoSQL\\Redis\\RedisOnLinux\\redis安装.doc\"");  
  
        System.out.println("m3 over");//出现异常此处就无法执行了  
    }  
  
    public static void m2() throws FileNotFoundException {  
        System.out.println("m2 begin");  
        m3();  
        System.out.println("m2 over");  
    }  
  
    public static void m1() throws FileNotFoundException {  
        System.out.println("m1 begin");  
        m2();  
        System.out.println("m1 over");  
  
    }  
  
    public static void main(String[] args) {  
        System.out.println("main begin");  
  
        try {  
            m1();  
            System.out.println("main over");//此处无法执行  
        } catch (FileNotFoundException e) {  
            System.out.println("catch over");  
        }  
    }  
}

此处抛出异常时因为构造方法中检测到file不合法:

public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    @SuppressWarnings("removal")
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    fd = new FileDescriptor();
    fd.attach(this);
    path = name;
    open(name);
    FileCleanable.register(fd);       // open set the fd, register the cleanup
}

在第12行 通过 throw new FileNotFoundException("Invalid file path")创建异常对象抛出

此处通过new创建异常对象就是为了上抛(如果是try-catche就没有创建对象的意义了),所以在方法名上才会throws

try-catch异常捕捉

希望调用者进行处理的 选择该种方式

不建议在main方法中继续上抛异常,上抛到JVM就会导致程序终止

public static void main(String[] args){
    try{
        //try尝试
        System.out.println(100 / 0);
        /*出现异常try内后面的代码都无法执行,直接跳转到cache语句块*/
    }
    catch(ArithmeticException e){
        //cache是捕捉异常之后走的分支,在此处处理异常   
        //这个分支中可以使用e引用 保存的是异常对象的内存地址
        System.out.println(e);//重写了toString方法
    }
    
    System.out.println("Exception has been cached")
    /*try-cache把异常捕捉之后 此处的代码会继续执行 这也就是异常让程序更健壮的原因*/
}

catch到的异常都是在调用的方法中创建出的,JVM将该异常赋值给catch后的变量中

深入try-catch

  • catch只能捕捉声明类型的异常
public static void main(String[] args) {
    try {
        FileInputStream inputStream = new FileInputStream("D:\\新建文件夹\\高级网页设计-大作业.doc");
    } catch (NullPointerException e) {
        /*如果捕捉的异常不是抛出的 编译器就会报错
        必须处理抛出的异常
        */
    }
}
  • catche可以捕捉声明类型的异常,或者该异常的子类型
try {
        FileInputStream inputStream = new FileInputStream("D:\\新建文件夹\\高级网页设计-大作业.doc");
        System.out.println("以上出现异常 这里无法执行");
    } catch (IOException e) {
        System.out.println("文件不存在!");
    }
    System.out.println("hello world");
}

IOException e = new FileNotFoundException()

  • 虽然catch可以捕捉声明异常的子类型, 但是建议写多个catch,精确处理每个异常
try {
    FileInputStream inputStream = new FileInputStream("D:\\新建文件夹\\高级网页设计-大作业.doc");
    inputStream.read(); //抛出IOException
    System.out.println("以上出现异常 这里无法执行");
} catch (FileNotFoundException e) {
    System.out.println("文件不存在!");
} catch (IOException e){
	System.out.println("IO出错");
}

也可以把抛出的 多个异常 都用cache他们的父类处理:

try {
    FileInputStream inputStream = new FileInputStream("D:\\新建文件夹\\高级网页设计-大作业.doc");
    inputStream.read();
    System.out.println("以上出现异常 这里无法执行");
} catch (IOException e){

}

但是这种处理方式不够精确。

  • 不能将父类型异常写在子类型异常上方
try {
    FileInputStream inputStream = new FileInputStream("D:\\新建文件夹\\高级网页设计-大作业.doc");
    inputStream.read();
    System.out.println("以上出现异常 这里无法执行");
}catch (IOException e){

}catch (FileNotFoundException e){
    /*此处会报错*/
}

子类异常已经 has already been caught,出现子类异常后自动从上到下匹配,直接被父类cache捕捉到;也就是子类cache是捕捉不到的

  • JDK8的多个异常捕获方式
try{
           FileInputStream fis = new FileInputStream("null");
           System.out.println(100 / 0); //运行时异常 编写程序时可以不处理
       }catch (ArithmeticException | FileNotFoundException e){
           
       }

异常的常用方法

getMessage()

获取异常详细消息字符串

public static void main(String[] args) {
    NullPointerException e = new NullPointerException("NullPointerException");
    String message = e.getMessage();
    System.out.println(message);
}

创建异常时构造方法的参数就是 getMessage()方法传递的信息

printStackTrace()

 public static void main(String[] args) {
        NullPointerException e = new NullPointerException("HereIsNullPointerException");
        e.printStackTrace();
        
        System.out.println("Hello World");
    }
/*
java.lang.NullPointerException: HereIsNullPointerException
	at com.bjpowernode.exception.ExceptionMethod.main(ExceptionMethod.java:9)
Hello World
*/

HelloWorld 可能会出现在异常信息的上方,这是因为后台某个线程在做这件事(异步线程)

public static void main(String[] args) {
    try {
        m1();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    System.out.println("此处的代码不会因为发生异常而宕机");
}

printStackTrace()以红色字体输出,因为:

底层调用了err的输出方式,使用err方式输出的就是红色字体

toString()

返回此可抛出的简短描述

try-catch-finally

finally子句中的代码是最后执行的,并且是一定会执行的,即使try中的代码出现了异常

try {
    //创建输入流对象
    FileInputStream fis = new FileInputStream("");
    
    String s = null;
    s.toString(); //出现空指针异常
    
    //流使用完需要关闭
    fis.close();
    
}catch (IOException e){
    e.printStackTrace();
}

如果在第九行进行关闭 ,在第六行一定会出现空指针异常;进而导致fis关闭失败

如果在catch外关闭:

try {
    //创建输入流对象
    FileInputStream fis = new FileInputStream("");
    
    String s = null;
    s.toString(); //出现空指针异常
    
}catch (IOException e){
    e.printStackTrace();
}

    //流使用完需要关闭
    fis.close();

如果在fis.close()前出现异常,也无法关闭

但是流是占用资源的,使用完必须关闭。

public static void main(String[] args) {
    FileInputStream fis = null;
    try {
        //创建输入流对象
        fis = new FileInputStream("");

        String s = null;
        s.toString(); //出现空指针异常

    }catch (IOException e){
        e.printStackTrace();
    }catch (NullPointerException e){
        e.printStackTrace();
    }finally {
        if(fis != null){ //避免空指针异常
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

在第十四行一定会执行finally语句块,流的关闭放在finally比较安全

try-finally

public static void main(String[] args) {
    try{
        System.out.println("try...");
        return;
    }finally {
        System.out.println("finally...");
    }
    /*此处的代码就无法执行了*/
}
/*
try...
finally...
*/

先执行try …

再执行finally …

最后执行 return …

具有返回值方法的try-finally

public static void main(String[] args) {  
    System.out.println(div(10, 2)); //2147483647  
    System.out.println(div(10, 2)); //2147483647  
}  
  
public static int div(int a, int b){  
  
    try{  
        int c = a / b;  
        return c;  
    }catch (Exception e){  
        e.printStackTrace();  
    } finally {  
        return Integer.MAX_VALUE;  
    }  
  
}

finally语句块一定在return后执行,所以finally的语句块中内容一定会先return

finally面试题

public static void main(String[] args) {
    System.out.println(m());
}
public static int m(){
    int i = 100;
    try{
        return i;
    }finally {
        i++;
    }
}

结果是100

Java中规定:方法体中的代码必须自上而下的顺序逐行执行,这是亘古不变的。

return出现在int i = 100的下方,返回的结果必须是100才会保证这个规则

自定义异常类

  • throws:写在方法定义处,表示声明一个异常告诉调用者,使用本方法可能会有哪些异常(运行时异常可以不处理)

  • throw:写在方法内,结束方法,手动抛出异常对象交给调用者

步骤:

  1. 继承Exception
  2. 声明有参和无参构造方法
  3. 在可能出现异常的位置throw new ;并且在方法声明位置throws
public class MyStackOperationException extends Exception {
    public MyStackOperationException() {
    }

    public MyStackOperationException(String message) {
        super(message);
    }
}
import java.util.Arrays;

public class Stack {
    int[] ints;
    int index = -1;

    public Stack(int[] ints) {
        this.ints = ints;
    }

    public void push(int i) throws MyStackOperationException {
        if (index >= ints.length - 1){
            //创建异常对象 手动抛出
            throw new MyStackOperationException("error:stack is full");
        }
        ints[++index] = i;
        System.out.println("input succeed " + " index : " + index + " elem : " + i);
    }
    public void pop() throws MyStackOperationException {
        if (index <= -1){
            throw new MyStackOperationException("error: stack is empty");
        }
        int ret = ints[index--];
        System.out.println("index : " + (index + 1) + " elem : " + ret + " output succeed");
    }
    public void printArray(){
        System.out.println(Arrays.toString(ints));
    }
}

测试:

public class StackTest {
    public static void main(String[] args) {
        Stack stack = new Stack(new int[3]);

        try{
        stack.push(1);
        stack.push(2);
        stack.push(3);
        stack.push(4);}
        catch (MyStackOperationException e){
            System.out.println(e.getMessage());
        } //捕捉到之后程序向下走
        stack.printArray();
        try{
        stack.pop();
        stack.pop();
        stack.pop();
        stack.pop();
        }catch (MyStackOperationException e){
            System.out.println(e.getMessage());
        }

    }
}
/*
input succeed  index : 0 elem : 1
input succeed  index : 1 elem : 2
input succeed  index : 2 elem : 3
error:stack is full
[1, 2, 3]
index : 2 elem : 3 output succeed
index : 1 elem : 2 output succeed
index : 0 elem : 1 output succeed
error: stack is empty
*/

方法重写中的异常

之前在[[005-面向对象核心#方法覆盖|方法覆盖]]中提到过,重写之后的方法不能比重写之前的方法抛出更多 / 更宽泛的异常

class Animal{
    public void doSome(){
        
    }
}
class Cat extends Animal{
    @Override
    public void doSome() throws Exception{
        super.doSome();
    }
}

编译报错;编译器检测到的父类是没有异常抛出的,而在子类执行期间却抛出了编译时异常

但是对于RunTimeException来说:

class Animal{
    public void doSome(){

    }
}
class Cat extends Animal{
    @Override
    public void doSome() throws RuntimeException{
        super.doSome();
    }
}

这是不会报错的,因为不要求对运行时异常进行处理

try-with-resources

Java 7引入了try-with-resources语句,用于自动关闭实现了AutoCloseable接口的资源。在try-with-resources语句中,我们可以在try关键字后面声明一个或多个资源,这些资源将在代码执行完毕后自动关闭。

如果对象是在try-with-resources代码块中声明的, AutoCloseable对象的close()方法会被自动执行。这种构造方式保证了最快的资源释放,避免资源耗尽异常。

try (Resource1 res1 = new Resource1();
     Resource2 res2 = new Resource2()) {
    // 使用资源的代码
} catch (ExceptionType e) {
    // 处理异常
}

try-with-resources 声明在 JDK 9 已得到改进。如果你已经有一个资源是 final 或等效于 final 变量,可以在 try-with-resources 语句中使用该变量,而无需在 try-with-resources 语句中声明一个新变量。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
 
public class Tester {
   public static void main(String[] args) throws IOException {
      System.out.println(readData("test"));
   } 
   static String readData(String message) throws IOException {
      Reader inputString = new StringReader(message);
      BufferedReader br = new BufferedReader(inputString);
      try (br) {
         return br.readLine();
      }
   }
}

验证Autocloseable