【Exception】Java 中的异常处理

1. 异常简介

背景

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

异常:在 JAVA 语言中,将程序执行中发生的不正常情况称为“异常”。

Java 程序在执行过程中所发生的异常事件可分为两类:

  1. Error:JVM 无法解决的严重问题。JVM 系统内部错误(StackOverflowEroor)、资源耗尽(OOM)等严重情况。一般不编写针对性代码进行处理。
  2. Exception:其它因编程错误或偶然的外在因素导致的一般性问题。可使用针对性的代码进行处理。如:空指针异访问、数组下标越界等。

StackOverflowEroor 栈溢出:

public class Test {
    public static void main(String[] args) {
        main(args);
    }
}

上述代码中,递归调用了 main()方法,最终抛出了 StackOverflowEroor 错误。
因为,在 JVM 中,栈是有默认深度的,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,就会超过栈的默认深度而导致栈溢出。

OOM 堆溢出(内存溢出):

public class Test {
    public static void main(String[] args) {
        Integer[] arr = new Integer[1024 * 1024 * 1024]
    }
}

上述代码中,new 了一个数组,数组长度是 1G,又因为是 Integer 类型,一个元素占 4 个字节,所以,这个数组占用内存 4GB。最后,造成堆内存空间不足,抛出了 OOM。

对于那些 Error,我们一般不做处理;但对于那些 Exception,就要进行相应的处理了。

2. 异常的处理方案

对于那些 Exception,一般有两种解决方案:一是一遇见错误就终止程序运行;另一种是处理异常:程序员在编写程序时,就考虑到错误的检测、错误消息的提示、错误的处理。但我们一般选择后者。

异常的分类

异常分为两类:编译时异常(受检异常)、运行时异常 (非受检异常)。

编译时异常是在代码书写时,编译器给你检查提示你要进行 try-catch 或 throws 处理;对于运行时异常,编译器不会帮你自动检查,当你运行程序时,虚拟机才会给你报出错误让你去处理,这个往往是我们编码逻辑或不规范导致的。

异常处理机制

背景

        在编写程序时,经常要在可能出现错误的地方加上检测的代码。如:x / y 时,要检测分母为 0、数据为空、输入的不是数字而是字符等。过多的 if-else 分支会导致程序的代码加长、臃肿、可读性差。因此要采用异常处理机制。

异常处理机制:JAVA 采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅、并易于维护

异常处理机制有两种方式:

  1. try-catch-finally
  2. throws + 异常类型

try-catch-finally

try-catch-finally 结构:

try {
    // 可能出现异常的代码
} catch(异常类型1 变量名1) {
    // 处理异常
} catch(异常类型2 变量名2) {
    // 处理异常
} finally {
    // 一定会执行的代码
}

当我们要将一个数值型的字符串转换为整形时,需要对应进行 try-catch。如:

public class ExceptionTest {

    public static void main(String[] args) {
        String str = "123";
        String strTwo = "abc";

        int num = 0;
        try {
            num = Integer.parseInt(strTwo);
        } catch (NumberFormatException e) {
            System.out.println("数据格式转换异常");
        }catch (Exception e) {
            System.out.println("抛出异常了");
        }
        System.out.println(num);
    }
}

当给方法 Integer.parseInt(strTwo) 的入参传的值是 “数字型”时,则不会抛出异常;否则,会抛出 NumberFormatException异常。

说明:

  1. finally 语句:可选的
  2. 使用 try 将可能出现的异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去 catch 中进行匹配
  3. 一旦 try 中的异常对象匹配到某一个 catch 时,就会进入到 catch 中进行异常处理。一旦处理完成,就跳出当前的 try-catch 结构,继续执行其后的代码(只会执行 NumberFormatException 异常,并不会执行 Exception 异常)
  4. catch 中的异常类型如果没有子父关系,则谁声明在上、声明在下无所谓;如果满足了父子继承关系,则要求子类一定声明在父类的上面,否则,就会报错(在 catch 语句中,不能把 Exception 异常放在 NumberFormatException 异常之上)
  5. 在 catch 中,常用的处理异常方式:e.getMessage()、e.printStackTrace();

finally 语句
finally语句的作用:finally 语句里面的代码是一定会被执行的。即使 catch 语句中又出现异常了、try 中有 return 语句、catch 中有 return 语句等。

例1:

public class FinallyTest {

    public static void main(String[] args) {
        try {
            int a = 6;
            int b = 0;

            int result = a / b;
        } catch (ArithmeticException e) {
            System.out.println("抛出算术异常");
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 依旧会被执行
        System.out.println("try-catch 结构之外的语句被执行");
    }
}

当执行上述代码后,屏幕打印:

抛出算术异常
try-catch 结构之外的语句被执行

try-catch 结构之外的语句被执行 打印出这句话表明:抛出异常后,依旧会执行 try-catch 语句之外的代码。

但我们现在在 catch 语句中加一些代码,使之也抛出异常,即:

public class FinallyTest {

    public static void main(String[] args) {
        try {
            int a = 6;
            int b = 0;

            int result = a / b;
        } catch (ArithmeticException e) {
        	System.out.println("抛出算术异常");
        	
        	// 会抛出数组下标越界异常
            int[] arr = new int[10];
            System.out.println(arr[10]);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("try-catch 结构之外的语句被执行");
    }
}

执行上述代码后,发现屏幕打印出:

抛出算术异常

没有打印之前的那一行,说明:没有执行执行那一行代码.

给它加上 finally 语句试试,即:

public class FinallyTest {

    public static void main(String[] args) {
        try {
            int a = 6;
            int b = 0;

            int result = a / b;
        } catch (ArithmeticException e) {
            System.out.println("抛出算术异常");

            int[] arr = new int[10];
            System.out.println(arr[10]);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("finally 语句被执行");
        }
        System.out.println("try-catch 结构之外的语句被执行");
    }
}

执行代码后,发现屏幕打印出:

抛出算术异常
finally 语句被执行

例2:
大家可以猜猜 FinallyTest#method() 方法的结果是多少

public class FinallyTest {

    public static void main(String[] args) {
        FinallyTest finallyTest = new FinallyTest();
        
        int method = finallyTest.method();
        // 2
        System.out.println(method);
    }
}
public int method() {
    try {
        int[] arr = new int[10];
        System.out.println(arr[10]);
        return 1;
    } catch (ArrayIndexOutOfBoundsException e) {
        e.printStackTrace();
        return 2;
    }
}

执行上述代码后,发现屏幕打印出:“2”。

加上 finally 语句后,那执行结果又是多少呢?

public int method() {
    try {
        int[] arr = new int[10];
        System.out.println(arr[10]);
        return 1;
    } catch (ArrayIndexOutOfBoundsException e) {
        e.printStackTrace();
        return 2;
    } finally {
        System.out.println("我一定会执行");
        return 3;
    }
}

执行上述代码后,发现输出结果是:“3”。

通过这两个例子来体会到 finally 语句的作用了吧。

那么,finally 语句一般用于哪些场景呢?

资源释放。如:数据库连接、输入输出流、网络编程 Socket 等。例如:

场景:在当前用户目录下,新建一个 test.txt 文件,然后,往里面写入一些内容,写完后关闭相应的流

代码如下:

public class FileTest {

    public static void main(String[] args) {
        File file = new File("test.txt");
        boolean exists = file.exists();
        if (!exists) {
            try {
                boolean newFile = file.createNewFile();
                if (!newFile) {
                    System.out.println("创建文件失败,文件名为" + file.getName());
                    return;
                }
            } catch (IOException e) {
                System.out.println("创建文件失败,文件名为" + file.getName());
                return;
            }
        }

        FileOutputStream out = null;
        try {
            out = new FileOutputStream(file);

            String message = "我爱中国";
            // 写入内容
            out.write(message.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != out) {
                try {
                	// 关闭流
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

throws + 异常类型

  1. throws + 异常类型 写在方法的声明处,指明此方法执行时,可能会抛出的异常类型。一旦当方法执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足 throws 后异常类型时,就会被抛出,异常代码后续的代码就不再执行!
  2. throws 的方式只是将异常抛给了调用者,并没有真正的处理异常。try-catch 真正地将异常给处理掉了。
  3. 子类重写的方法抛出的编译时异常类型不大于父类被重写的方法抛出的异常类型(多态的使用)

如:

public class ThrowTest {

    public static void main(String[] args) {
        Student student = new Student();
        try {
            student.regist(-1);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}
class Student {
    private int id;

	// 手动抛出异常
    public void regist(int id) throws Exception{
        if (id > 0) {
            this.id = id;
        } else {
            throw new Exception("输入的是非法数据");
        }
    }
}

如何解读这句话:子类重写的方法抛出的编译时异常类型不大于父类被重写的方法抛出的异常类型(多态的使用)

给出如下例子:

public class Father {

    public void arth() throws ClassNotFoundException {

    }

    public static void main(String[] args) {
        Father father = new Son();
        try {
            father.arth();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
public class Son extends Father{
    @Override
    public void arth() throws ClassNotFoundException{

    }
}

对于上述代码,如果去掉这个限制 “子类重写的方法抛出的编译时异常类型不大于父类被重写的方法抛出的异常类型”,则修改 Son#arth() 方法成如下:

public class Son extends Father{
    @Override
    public void arth() throws Exception{

    }
}

那么,在 Father#main() 方法中调用 arth() 方法时,必定要进行 try-catch,那么 catch 中应该捕获什么异常对象呢?父类中的方法是没有 throws Exception 的啊。所以,这是不成立的,并且是必须要加那条限制的。

好了,下一期咱们来分享“如何自定义异常吧”~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值