JavaSE&Java的异常

简介: JavaSE&Java的异常

8.1 异常概述


在使用计算机语言进行项目开发的过程中,即使程序员把代码写得 尽善尽美,在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式,读取文件是否存在,网络是否始终保持通畅等等。


异常 :指的是程序在执行过程中,出现的非正常的情况,如果不处理最终会导致JVM的非正常停止。

异常指的并不是语法错误。语法错了,编译不通过,不会产生字节码文件,根本不能运行。


异常也不是指逻辑代码错误而没有得到想要的结果,例如:求a与b的和,你写成了a-b


对于异常的发生,要么直接终止程序的运行,这不是我们想要的结果;要么在编写程序时,预判到可能会出现异常的地方,做好针对性处理措施,当异常发生时,经异常处理后,可以保证程序还可以继续执行。


Java中如何描述异常并且都有什么样的异常?异常发生时,程序员如何得知?然后又该如何处理异常?


Java中把常见的不同异常用不同的类表示,当发生某种异常时,JVM会创建该异常类型的对象(其中包含了异常详细信息),并且抛出来,然后程序员可以catch到这个异常对象,并根据需要进行相应处理,如果无法catch到这个异常对象,说明没有针对这个异常预备处理措施,那么这个异常对象将会导致程序终止。


8.2 异常体系


异常的根类是java.lang.Throwable,Java提供的所有异常类均继承自此类,其下有两个子类:java.lang.Error与java.lang.Exception,平常所说的异常指java.lang.Exception。

Throwable体系:


  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。

例如: StackOverflowError、OutOfMemoryError。

  • Exception:表示异常,其它因编程错误或偶然的外在因素导致的一般性问题,程序员可以通过相应预防处理措施,使程序发生异常后还可以继续运行。好比感冒、阑尾炎。

例如:空指针访问、试图读取不存在的文件、网络连接中断、数组角标越界


Throwable中的常用方法:


  • public void printStackTrace():打印异常的详细信息。


包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。


  • public String getMessage():获取发生异常的原因。


提示给用户的时候,就提示错误原因。


出现异常,不要紧张,把异常的简单类名,拷贝到API中去查。

image.png


8.3 异常分类


由于Error情况发生是我们无法处理的,一般因为是内存不足或程序存在严重逻辑问题,只能通过扩大内存或重新修改代码解决。


而我们平常所遇到的大多数是Exception类型的异常,也是我们通常说的异常和异常处理。Exception异常又通常分两大类:


运行期异常(unchecked Exception): 这类异常的发生多数是因为程序员编写的代码逻辑不够严谨造成的(如数组脚标越界异常),可以选择进行处理或不处理,最好是通过修正、优化代码避免异常的发生(或者使用异常处理简化复杂的逻辑判断代码)。


编译期异常(checked Exception): 这类异常一般由程序之外的因素引起的(如程序读取的文件不存在、网络中断),而不是程序员写的代码逻辑有问题,所以程序员容易忽略对这类异常的处理,而恰恰这类异常又很常发生,所以Java要求针对这类可能发生的异常必须进行处理,否则编译无法通过。(只有java语言有需强制处理的异常)


常见的错误和异常演示示例


  1. VirtualMachineError

最常见的就是:


StackOverflowError:虚拟机栈内存不足,无法分配栈帧所需空间。

OutOfMemoryError:没有足够的内存空间可以分配。

  @Test
  public void test01(){
    //StackOverflowError
    digui();
  }
  
  public void digui(){
    digui();
  }
  @Test
  public void test02(){
    //OutOfMemoryError
    //方式一:
    int[] arr = new int[Integer.MAX_VALUE];
  }
  @Test
  public void test03(){
    //OutOfMemoryError
    //方式二:
    StringBuilder s = new StringBuilder();
    while(true){
      s.append("atguigu");
    }
  }
2. 运行时异常
  @Test
  public void test01(){
      //NullPointerException
    int[] arr=null;
    System.out.println(arr.length);
  }
  
  @Test
  public void test02(){
    //ClassCastException
    Person p = new Man();
    Woman w = (Woman) p;
  }
  
  @Test
  public void test03(){
    //ArrayIndexOutOfBoundsException
    int[] arr = new int[5];
    for (int i = 1; i <= 5; i++) {
      System.out.println(arr[i]);
    }
  }
  
  @Test
  public void test04(){
    //InputMismatchException
    Scanner input = new Scanner(System.in);
    System.out.print("请输入一个整数:");
    int num = input.nextInt();
  }
  
  @Test
  public void test05(){
    int a = 1;
    int b = 0;
    //ArithmeticException
    System.out.println(a/b);
  }
3. 编译时异常
  @Test
  public void test06() throws InterruptedException{
    Thread.sleep(1000);//休眠1秒
  }
  
  @Test
  public void test07() throws FileNotFoundException{
    FileInputStream fis = new FileInputStream("Java学习秘籍.txt");
  }
  
  @Test
  public void test08() throws SQLException{
    Connection conn = DriverManager.getConnection("....");
  }


8.4 异常的生成与抛出机制throw


Java程序的执行过程中如出现异常,会生成一个异常类对象,然后该异常对象会被提交给Java运行时系统,这个过程称为抛出(throw)异常。异常对象的生成与抛出有两种方式:


  1. 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,就会在后台自动创建一个对应异常类的实例对象并自动抛出。


我们通过示例分析下一次产生的过程:


运行以下程序会产生一个数组索引越界异常ArrayIndexOfBoundsException。

// 工具类
public class ArrayTools {
    // 对给定的数组通过给定的角标获取元素。
    public static int getElement(int[] arr, int index) {
        int element = arr[index];
        return element;
    }
}
// 测试类
public class ExceptionDemo {
    public static void main(String[] args) {
        int[] arr = { 34, 12, 67 };
        intnum = ArrayTools.getElement(arr, 4)
        System.out.println("num=" + num);
        System.out.println("over");
    }
}

由此看出,异常对象被JVM创建后,在产生异常的方法中会自动抛出,抛给方法的调用者,抛给main方法,最后抛给虚拟机,虚拟机打印异常信息后终止程序。


  1. 由开发人员手动创建:Exception exception = new ClassCastException();——创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样,手动创建的异常对象需要手动抛出,才会对程序产生影响。


在Java中,使用关throw关键字手动抛出一个异常对象,throw用在方法内,将这个异常对象传递到方法调用者处,同时结束当前方法的执行。

使用格式:

throw new 异常类名(参数);

例如:

throw new 异常类名(参数);

throw的使用示例:

public class ThrowDemo {
    public static void main(String[] args) {
        //创建一个数组 
        int[] arr = {2,4,52,2};
        //根据索引找对应的元素 
        int index = 4;
        int element = getElement(arr, index);

        System.out.println(element);
        System.out.println("over");
    }
    /*
     * 根据 索引找到数组中对应的元素
     */
    public static int getElement(int[] arr,int index){ 
        if(arr == null){
            /*
             判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。
             这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。 
              */
            throw new NullPointerException("要访问的arr数组不存在");
        }
        //判断  索引是否越界
        if(index<0 || index>arr.length-1){
             /*
             判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。
             这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。 
              */
             throw new ArrayIndexOutOfBoundsException("哥们,角标越界了~~~");
        }
        int element = arr[index];
        return element;
    }
}

注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。


那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用throws声明处理。


练习1


1、声明Husband类,包含姓名和妻子属性,属性私有化,提供一个Husband(String name)的构造器,重写toString方法,返回丈夫姓名和妻子的姓名


2、声明Wife类,包含姓名和丈夫属性,属性私有化,提供一个Wife(String name)的构造器,重写toString方法,返回妻子的姓名和丈夫的姓名


3、声明TestMarry类,在main中,创建Husband和Wife对象后直接打印妻子和丈夫对象,查看异常情况,看如何解决


练习2


1、声明银行账户类Account


(1)包含账号、余额属性,要求属性私有化,提供无参和有参构造,


(2)包含取款方法,当取款金额为负数时,抛出IllegalArgumentException,异常信息为“取款金额有误,不能为负数”,当取款金额超过余额时,抛出UnsupportedOperationException,异常信息为“取款金额不足,不支持当前取款操作”


(3)包含存款方法,当取款金额为负数时,抛出IllegalArgumentException,异常信息为“存款金额有误,不能为负数”


2、编写测试类,创建账号对象,并调用取款和存款方法,并传入非法参数,测试发生对应的异常。


8.5 异常的处理机制


如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。


8.5.1 捕获异常try…catch


捕获异常:Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理。


捕获异常语法如下:

try{
     编写可能会出现异常的代码
}catch(异常类型1  e){
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}catch(异常类型2  e){
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}
....

try: 捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的代码放在try语句块中。建议:此范围尽量小。


catch: 用来进行某种异常的捕获,实现对捕获到的异常进行处理。每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常。


可以有多个catch块,按顺序匹配。

如果多个异常类型有包含关系,那么小上大下


演示示例:

public class TestException {
  public static void main(String[] args)  {
    try {
      readFile("不敲代码学会Java秘籍.txt");
    } catch (FileNotFoundException e) {
//      e.printStackTrace();
//      System.out.println("好好敲代码,不要老是想获得什么秘籍");
      System.out.println(e.getMessage());
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    } 
    
    System.out.println("继续学习吧...");
  }
  
  // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
  public static void readFile(String filePath) throws FileNotFoundException, IllegalAccessException{
    File file = new File(filePath);
    if(!file.exists()){
      throw new FileNotFoundException(filePath+"文件不存在");
    }
    if(!file.isFile()){
      throw new IllegalAccessException(filePath + "不是文件,无法直接读取");
    }
    //...
  }
  
}

获取异常信息:


捕获到了异常对象,就可以获取异常对象中封装的异常信息,Throwable类中定义了一些方法用于获取异常对象中的信息:


public String getMessage():获取异常的描述信息。

public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。这些信息包含了异常的类型,异常信息,还包括异常出现的位置,在开发和调试阶段,建议使用printStackTrace。


8.5.2 finally块


finally:在finally代码块中存放的代码都是一定会被执行的。由于异常会引发程序跳转,导致后面有些语句执行不到,如果一定要执行这些语句就可以使用finally,finally常用于释放系统资源。


比如:当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),无论异常有没有发生,我们都要在使用完之后关闭已打开的资源,避免系统资源的浪费。


finally的语法:

 try{
     
 }catch(...){
     
 }finally{
     无论try中是否发生异常,也无论catch是否捕获异常,也不管try和catch中是否有return语句,都一定会执行
 }
 
  try{
     
 }finally{
     无论try中是否发生异常,也不管try中是否有return语句,都一定会执行。
 } 

注意:finally不能单独使用。

当只有在try或者catch中调用退出JVM的相关方法,例如System.exit(0),此时finally才不会执行,否则finally永远会执行。

finally代码IO流读取文件示例如下:(暂)

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

public class TestException {
  public static void main(String[] args)  {
    readFile("不敲代码学会Java秘籍.txt");
    System.out.println("继续学习吧...");
  }
  
  // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
  public static void readFile(String filePath) {
    File file = new File(filePath);
    FileInputStream fis = null;
    try {
      
      if(!file.exists()){
        throw new FileNotFoundException(filePath+"文件不存在");
      }
      if(!file.isFile()){
        throw new IllegalAccessException(filePath + "不是文件,无法直接读取");
      }
      fis = new FileInputStream(file);
      //...
    } catch (Exception e) {
      //抓取到的是编译期异常  抛出去的是运行期 
      throw new RuntimeException(e);
    }finally{
      System.out.println("无论如何,这里的代码一定会被执行");
      try {
        if(fis!=null){
          fis.close();
        }
      } catch (IOException e) {
        //抓取到的是编译期异常  抛出去的是运行期 
        throw new RuntimeException(e);
      }
    }
    
  }
}


8.5.3 finally与return (了解)


  1. 形式一:从try回来
public class TestReturn {
  public static void main(String[] args) {
    int result = test("12");
    System.out.println(result);
  }

  public static int test(String str){
    try{
      Integer.parseInt(str);
      return 1;
    }catch(NumberFormatException e){
      return -1;
    }finally{
      System.out.println("test结束");
    }
  }
}
2. 形式二:从catch回来
public class TestReturn {
  public static void main(String[] args) {
    int result = test("a");
    System.out.println(result);
  }

  public static int test(String str){
    try{
      Integer.parseInt(str);
      return 1;
    }catch(NumberFormatException e){
      return -1;
    }finally{
      System.out.println("test结束");
    }
  }
}
3. 形式三:从finally回来
public class TestReturn {
  public static void main(String[] args) {
    int result = test("a");
    System.out.println(result);
  }

  public static int test(String str){
    try{
      Integer.parseInt(str);
      return 1;
    }catch(NumberFormatException e){
      return -1;
    }finally{
            System.out.println("test结束");
      return 0;
    }
  }
}
4. 面试题
  public static void main(String[] args) {
    int test = test(3,5);
    System.out.println(test);//8
  }

  public static int test(int x, int y){
    int result = x;
    try{
      if(x<0 || y<0){
        return 0;
      }
      result = x + y;
      return result;
    }finally{
      result = x - y;
    }
  }
public class Test04 {
  static int i = 0;
  public static void main(String[] args) {
    System.out.println(test());//2
  }

  public static int test(){
    try{
      return ++i;
    }finally{
      return ++i;
    }
  }
}

8.5.4 声明异常throws


throws:用在方法上,表明此方法可能会产生的异常类型。

如果在某方法内通过抛出了必须要处理的编译期异常,有两种选择:要么在当前方法进行捕获处理,要么通过throws在当前方法上进行声明,让方法的调用者去处理。

声明异常格式:

修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }  

声明异常的代码演示:

import java.io.File;
import java.io.FileNotFoundException;

public class TestException {
  public static void main(String[] args) throws FileNotFoundException {
    readFile("不敲代码学会Java秘籍.txt");
  }
  
  // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
  public static void readFile(String filePath) throws FileNotFoundException{
    File file = new File(filePath);
    if(!file.exists()){
      throw new FileNotFoundException(filePath+"文件不存在");
    }
  }
  
}

throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。

import java.io.File;
import java.io.FileNotFoundException;

public class TestException {
  public static void main(String[] args) throws FileNotFoundException,IllegalAccessException {
    readFile("不敲代码学会Java秘籍.txt");
  }
  
  // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
  public static void readFile(String filePath) throws FileNotFoundException,IllegalAccessException{
    File file = new File(filePath);
    if(!file.exists()){
      throw new FileNotFoundException(filePath+"文件不存在");
    }
    if(!file.isFile()){
      throw new IllegalAccessException(filePath + "不是文件,无法直接读取");
    }
    //...
  }
  
}

练习


1、声明银行账户类Account


(1)包含账号、余额属性,要求属性私有化,提供无参和有参构造,


(2)包含取款方法,当取款金额为负数时,抛出Exception,异常信息为“越取你余额越多,想得美”,当取款金额超过余额时,抛出Exception,异常信息为“取款金额不足,不支持当前取款操作”


(3)包含存款方法,当取款金额为负数时,抛出Exception,异常信息为“越存余额越少,你愿意吗?”


2、编写测试类,创建账号对象,并调用取款和存款方法,并传入非法参数,测试发生对应的异常。



8.6 自定义异常


  1. 为什么需要自定义异常类:


异常表示程序运行中出现的不正常的情况,我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,我们在使用也是尽量使用Java这些异常类型。但在大型系统的开发中总是有些不正常的情况是Java没有定义好的类不好表示的,此时我们可以根据自己业务的异常情况来自定义异常类。例如年龄负数问题,考试成绩负数问题等等,我们都可以通过自定义异常类来表示。


  1. 异常类如何定义:


保持一个合理的异常体系是很重要的,一般自定义一个异常比如UserException作为“根异常”,然后在此基础上再派生出不同的异常类型,自定义的“根异常”需要从一个合适的现有异常中派生出来,通常建议派生自java.lang.RuntimeException。


“根异常”:

public class UserException extends RuntimeException {
 }

其他异常从”根异常“派生出来:

public class UserExistedException extends UserException {
}

public class UserNotFoundException extends UserException {
}
...

自定义的“根异常”通常提供多个构造方法,直接调用父类的即可:

//用户异常类
public class UserException extends RuntimeException {
    public UserException() {
    }

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

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

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

}

派生出的异常,提供简单两个构造方法,够用即可:

//用户已经存在异常类
public class UserExistedException extends UserException {
    public UserExistedException() {
    }
  
    public UserExistedException(String message) {
        super(message);
    }
}


  1. 演示自定义异常:

要求:结合上面自定义的异常类,模拟注册操作,如果用户名已存在,则抛出异常并提示:亲,该用户名已经被注册。

模拟登陆操作,使用数组模拟数据库中存储的数据,并提供当前注册账号是否存在方法用于判断。

public class DemoUserException {
    //字符串数组模拟数据库已存在的账号
    static String[] names = {"tom", "jack", "rose"};

    public static void main(String[] args) {
        registUser("tom");
    }

    //注册用户
    public static void registUser(String name) {
        int length = names.length;
        //验证是否注册过
        if (checkUserExist(name)) //已注册
            throw new UserExistException(name + "用户已存在,注册失败");
        else { //如果未注册过,才进行存储到数据库中(数组)
            System.out.println("注册成功!");
            names = Arrays.copyOf(names, length * 2);
            names[length] = name;
            for (int i = 0; i < names.length; i++) {
                System.out.println(names[i]);
            }
        }
    }
    //验证用户是否已注册
    public static boolean checkUserExist(String name) {
        for (int i = 0; i < names.length; i++) {
            if (names[i].equals(name))
                return true;//表示存在,已注册
        }
        return false;//表示未注册的用户
    }
}


  1. 小结:
  • 自定义异常从Exception类或者它的子类派生一个子类即可,通常建议从RuntimeException派生。
  • 自定义异常类通常至少包含2个构造器:一个是无参构造,另一个是带有详细信息的构造器
  • 自定义的异常只能通过throw关键字抛出。
  • 自定义异常最重要的是异常类的名字,当异常出现时,可以根据名字判断异常类型。


8.7 异常关键字和注意事项总结


  1. 异常处理中的5个关键字


  1. 异常处理注意事项
  • 编译期异常必须处理,要么捕获处理,要么声明在方法上,让调用者处理。
  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
  • try语句范围要尽量小的包围在可能出现异常的一行或几行代码上,不要把大量无异常的代码一起包起来,虽然这样很省事。
  • catch语句捕获的异常类型要尽量小,尽量精准,好针对性的做处理。
  • 如果finally有return语句,永远返回finally中的结果,但要避免该情况.
  • 如果父类方法抛出了多个异常,子类重写父类方法时不能抛出更大异常,可以抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。
  • 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。


相关文章
|
8天前
|
Java 编译器
Java一分钟之——异常分类:检查异常与运行时异常
【5月更文挑战第20天】Java异常处理分为检查异常(Checked Exceptions)和运行时异常(Unchecked Exceptions),两者在编译期处理方式不同。检查异常需捕获或声明,如`IOException`,而运行时异常如`NullPointerException`在运行时终止程序。常见问题包括不恰当的异常使用、过度捕获和忽略异常信息。避免策略包括正确区分异常类型、具体捕获和处理异常信息。示例代码展示了如何处理这两种类型的异常。理解并妥善处理异常能提升程序的健壮性和可维护性。
35 4
|
17小时前
|
Java 程序员 数据库连接
|
3天前
|
Java 数据安全/隐私保护
Java中的异常
Java中的异常
8 3
|
5天前
|
SQL Java
Java的异常类
Java的异常类
4 0
|
5天前
|
Java
【JAVA学习之路 | 提高篇】自定义异常类
【JAVA学习之路 | 提高篇】自定义异常类
|
5天前
|
Java
【JAVA学习之路 | 提高篇】异常的处理(throws)与手动抛出异常(throw)
【JAVA学习之路 | 提高篇】异常的处理(throws)与手动抛出异常(throw)
|
5天前
|
Java 编译器 数据库连接
【JAVA学习之路 | 提高篇】异常的处理(try-catch-finally)
【JAVA学习之路 | 提高篇】异常的处理(try-catch-finally)
|
5天前
|
运维 Java 程序员
【JAVA学习之路 | 提高篇】异常(Exception)
【JAVA学习之路 | 提高篇】异常(Exception)
|
8天前
|
Java
Java一分钟之——异常链:追踪错误源头
【5月更文挑战第20天】Java异常处理中的异常链机制有助于追踪错误源头。通过`initCause()`和`getCause()`方法,新异常与原始异常关联,提供丰富调试信息。常见问题包括忽略原始异常、过度包装和不正确调用`initCause()`。避免策略包括始终记录原始异常、适度创建自定义异常和正确使用`initCause()`。代码示例展示了如何在异常实例化时自动链接原始异常。异常链能改善错误追踪,加速问题解决。
32 3
|
13天前
|
前端开发 Java 应用服务中间件
【异常解决】java程序连接MinIO报错The request signature we calculated does not match the signature you provided.
【异常解决】java程序连接MinIO报错The request signature we calculated does not match the signature you provided.
38 0