异常
1、异常概述
- 异常 :指的是程序在执行过程中,出现的非正常的情况,如果不处理最终会导致JVM的非正常停止。
异常指的并不是语法错误。语法错了,编译不通过,不会产生字节码文件,根本不能运行。
异常也不是指逻辑代码错误而没有得到想要的结果,例如:求a与b的和,你写成了a-b
对于异常的发生,要么直接终止程序的运行,要么在编写程序时,预判到可能会出现异常的地方,做好针对性处理措施,当异常发生时,经异常处理后,可以保证程序还可以继续执行。
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()
:获取发生异常的原因。提示给用户的时候,就提示错误原因。
3、异常分类
运行期异常(unchecked Exception):这类异常的发生多数是因为程序员编写的代码逻辑不够严谨造成的(如数组脚标越界异常),可以选择进行处理或不处理,最好是通过修正、优化代码避免异常的发生(或者使用异常处理简化复杂的逻辑判断代码)。
编译期异常(checked Exception):这类异常一般由程序之外的因素引起的(如程序读取的文件不存在、网络中断),而不是程序员写的代码逻辑有问题,所以程序员容易忽略对这类异常的处理,而恰恰这类异常又很常发生,所以Java要求针对这类可能发生的异常必须进行处理,否则编译无法通过。(只有java语言有需强制处理的异常)
常见的错误和异常演示示例
1、VirtualMachineError
最常见的就是:
StackOverflowError
:虚拟机栈内存不足,无法分配栈帧所需空间。
OutOfMemoryError
:没有足够的内存空间可以分配。
@Test
public void test01(){
//StackOverflowError
digui();
}
public void digui(){
digui();
}
@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("....");
}
4、异常的生成与抛出机制throw
异常对象的生成与抛出有两种方式:
由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,就会在后台自动创建一个对应异常类的实例对象并自动抛出。
由此看出,异常对象被JVM创建后,在产生异常的方法中会自动抛出,抛给方法的调用者,抛给main方法,最后抛给虚拟机,虚拟机打印异常信息后终止程序。
由开发人员手动创建: Exception exception = new ClassCastException();——创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样,手动创建的异常对象需要手动抛出,才会对程序产生影响。
在Java中,使用throw 关键字手动抛出一个异常对象,throw用在方法内,将这个异常对象传递到方法调用者处,同时结束当前方法的执行。
使用格式:
throw new 异常类名(参数);
例如:
throw new NullPointerException(“要访问的arr数组不存在”);
throw new ArrayIndexOutOfBoundsException(“该索引在数组中不存在,已超出范围”);
注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。
那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续讲问题声明出去,使用throws声明处理。
5、异常的处理机制
1、捕获异常try…catch
捕获异常语法如下:
try{
编写可能会出现异常的代码
}catch(异常类型1 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}catch(异常类型2 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
- 可以有多个catch块,按顺序匹配。
- 如果多个异常类型有包含关系,那么小上大下
获取异常信息:
捕获到了异常对象,就可以获取异常对象中封装的异常信息,Throwable类中定义了一些方法用于获取异常对象中的信息:
-
public String getMessage()
:获取异常的描述信息。 -
public void printStackTrace()
:打印异常的跟踪栈信息并输出到控制台。这些信息包含了异常的类型,异常信息,还包括异常出现的位置,在开发和调试阶段,建议使用printStackTrace。
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永远会执行。
面试题
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;
}
}
}
6、声明异常throws
throws:用在方法上,表明此方法可能会产生的异常类型。
如果在某方法内通过抛出了必须要处理的编译期异常,有两种选择:要么在当前方法进行捕获处理,要么通过throws在当前方法上进行声明,让方法的调用者去处理。
声明异常格式:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。
7、自定义异常
异常类如何定义:
一般自定义一个异常比如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);
}
}
代码演示:
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;//表示未注册的用户
}
}
小结:
- 自定义异常从Exception类或者它的子类派生一个子类即可,通常建议从RuntimeException派生。
- 自定义异常类通常至少包含2个构造器:一个是无参构造,另一个是带有详细信息的构造器
- 自定义的异常只能通过throw关键字抛出。
- 自定义异常最重要的是异常类的名字,当异常出现时,可以根据名字判断异常类型。
8、异常关键字和注意事项总结
异常处理注意事项
- 编译期异常必须处理,要么捕获处理,要么声明在方法上,让调用者处理。
- 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
- try语句范围要尽量小的包围在可能出现异常的一行或几行代码上,不要把大量无异常的代码一起包起来,虽然这样很省事。
- catch语句捕获的异常类型要尽量小,尽量精准,好针对性的做处理。
- 如果finally有return语句,永远返回finally中的结果,但要避免该情况.
- 如果父类方法抛出了多个异常,子类重写父类方法时不能抛出更大异常,可以抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。
- 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。