简单易懂的Java异常

214 阅读6分钟

我是好吃懒做的prgers, 在iOS摸鱼了好几年, 突然心血来潮, 想去Java摸鱼玩

1 开发中的错误

在开发Java程序的过程中, 会遇到各种各样的错误, 比如:

  • 语法错误
    • 导致编译失败, 程序无法正常运行
  • 逻辑错误
    • 比如需要执行加法操作时, 不小心写成了减法操作
  • 运行时错误
    • 在程序运行过程中产生的意外, 会导致程序终止运行, 在Java中也叫做异常

程序产生了异常, 一般称之为: 抛出异常, 如果没有主动去处理它, 会导致程序终止运行

思考: 下面代码的打印结果是什么?

public static void main(String[] args) {
  System.out.println(1);
  Integer i1 = new Integer("123");
  System.out.println(2);
  Integer i2 = new Integer("abc");
  System.out.println(3);
}

由于abc无法转换成整数, new Integer("abc") 会抛出一个异常
1. 异常类型: java.lang.NumberFormatException
2. 由于没有主动去处理这个异常,所以导致程序终止运行
打印结果: 1, 2

2 异常

Java中有各种各样的异常, 所有的异常最终都继承自java.lang.Throwable

2.1 异常的种类

异常分为两种:检查型异常非检查型异常

检查型异常

这类异常一般难以避免,编译器会进行检查,如果开发没有处理这类异常,编译器会报错

哪些异常是检查型异常?

除Error、RuntimeException以外的异常

非检查型异常

这类异常一般可以避免,编译器不会进行检查,如果开发者没有处理这类异常,编译器将不会报错

哪些异常是非检查型异常?

Error、RuntimeException

2.2 常见的检查型异常

//java.io.FileNotFoundException 文件不存在
FileOutputStream fos = new FileOutputStream("F:/prgers/1.text");

FileOutputStream fmt = new SimpleDateFormat("yyyy-MM-dd");
//java.text.ParseException 字符串的格式不对
Date date = fmt.parse("2066/06/06");

//java.lang.InterruptedException
Thread.sleep(1000);

//java.lang.ClassNotFoundException 不存在这个类
Class cls = Class.forName("Dog");

//java.lang.InstantiationException 没有无参构造方法
//java.lang.IllegalAccessException 没有权限访问构造方法
Dog dog = (Dog)cls.newInstance();

2.3 常见的非检查型异常 - Error

for (int i = 0; i < 200; i++) {
  //java.lang.OutOfMemoryError 内存不够用
  long[] a = new long[1000000000];
}

public static void test() {
  test();
}

public static void main(String[] args) {
  //java.lang.StackOverflowError 栈内存溢出
  test();
}

2.4 常见的非检查型异常 - RuntimeException

//java.lang.NullPointerException 使用可空指针
StringBUilder s = null;
s.append("abc");

//java.lang.NumberFormatException 数字的格式不对
Integer i = new Integer("abc");

Int[] array = { 11, 22, 33 };
//java.lang.ArrayIndexOutOfBoundsException 数组越界
array[4] = 44;

Object obj = "123.4";
//java.lang.ClassCastException 类型不匹配
Double d = (Double) obj;

在了解了常用的异常以后,如何防止程序因为抛出异常导致终止运行呢?

3 异常处理方式

3.1 try-catch

try {
  代码1
  代码2(可能会抛出异常)
  代码3
} catch(异常A e) {
  //当抛出【异常A】类型的异常时,会进入这个代码块
} catch(异常B e) {
  //当没有抛出【异常A】类型
  //但抛出【异常B】类型的异常时,会进入这个代码块
} catch(异常C e) {
  //当没有抛出【异常A】类型,【异常B】类型
  //但抛出【异常C】类型的异常时,会进入这个代码块
}
代码4

伪代码解释

如果【代码2】没有抛出异常
1【代码1、3】都会被执行
2 所有的catch都不会被执行
3 【代码 4】会被执行
如果【代码2】抛出了异常
1【代码1】会被执行,【代码3】不会被执行
2 会选择匹配的catch来执行代码
3 【代码 4】会被执行

注意

父类型的异常必须写在子类型的后面
1【异常A】不可以是【异常B、C】的父类型
2【异常B】不可以是【异常C】的父类型

举个例子

public static void main(String[] args) {
  System.out.println(1);
  try {
   System.out.println(2);
   Integer integer = new Integer("abc");
   System.out.println(3);
  } catch (NumberFormatException e) {
   System.out.println(4);
  }
  System.out.println(5);
 }
//打印结果: 1、2、4、5

一个catch捕获多种类型的异常

try {

} catch (异常A | 异常B | 异常C) {

}

Java 7开始,单个catch可以捕获多种类型的异常,如果并列的几个异常类型之间存在父子关系,保留父类型即可

异常对象的常用方法

  try {
   Integer integer = new Integer("abc");
  } catch (NumberFormatException e) {
     //异常的描述
     System.out.println(e.getMessage());
     //异常名称 + 异常描述
     System.out.println(e);
     //打印堆栈信息
     e.printStackTrace();
  }

3.2 finally

trycatch 正常执行完毕后,一定会执行finally中的代码

try {

}catch (异常 e) {

} finally {

}

finally可以和try-catch搭配使用,也可以只和try搭配使用

try {

}finally {

}

经常会在finally中编写一些关闭、释放资源的代码(比如关闭文件)

PrintWriter out = null;
try {
  out = new PrintWriter("F:/test.txt");
  out.print("My name is prgers");
}catch (异常 e) {
  e.printStackTrace();
} finally {
  if (out != null) {
    out.close();
  }
}

finally细节

如果在执行trycatch时,JVM退出或者当前线程被中断、杀死,finally可能不会被执行。
如果trycatch中使用了returnbreakcontinue等提前结束语句,finally会在returnbreakcontinue之前执行。

举个例子

public static void main(String[] args) {
  System.out.println(get());
}

static int get() {
  try {
   new Integer("abc");
   System.out.println(1);
   return 2;
  } catch (Exception e) {
   System.out.println(3);
   return 4;
  } finally {
   System.out.println(5);
  }
 }
//打印结果 3、5、4

3.3 throws

作用:将异常抛给上层方法

static void test() throws FileNotFoundException, ClassNotFoundException {
  PrintWriter out = new PrintWriter("/User/prgers/1.txt");
  Class cls = Class.forName("Person");
 }

如果throws后面的异常类型存在父子关系,保留父类型即可

static void test() throws Exception {
  PrintWriter out = new PrintWriter("/User/prgers/1.txt");
  Class cls = Class.forName("Person");
 }
  
static void test() throws Throwable {
  PrintWriter out = new PrintWriter("/User/prgers/1.txt");
  Class cls = Class.forName("Person");
}

可以一部分异常使用try-catch处理,另一部分异常使用throws处理

static void test() throws FileNotFoundException {
  PrintWriter out = new PrintWriter("/User/prgers/1.txt");
    try {
        Class cls = Class.forName("Person");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
  
 }

throws的细节

当父类的方法没有throws异常,子类的重写方法也不能throws异常
当父类的方法有throws异常,子类的重写方法可以
<1> 不throws异常
<2> throws跟父类一样的异常
<3> throws父类异常的子类型

public class Person {
    public void test1() {}
    public void test2() throws IOException{}
    public void test3() throws IOException{}
    public void test4() throws IOException{}
}
 
public class Student extends Person {
    @Override
    public void test1() {}
    @Override
    public void test2(){}
    @Override
    public void test3() throws IOException {}
    @Override
    public void test4() throws IOException {}
}

3.4 throw

使用throw可以抛出一个新建的异常

public class Person {
  public Person(String name) {
    if(name == null || name.length() == 0) {
      throw new IllegalArgumentException("name must not be empty");
    }
  }
}

4 自定义异常

在开发中,如果想要自定义异常类型, 基本都是以下两种做法

继承Exception

  • 使用起来代码会稍微复杂
  • 希望开发者重视这个异常,认真处理这个异常

继承RuntimeException

  • 使用起来代码会更加简洁
  • 不严格要求开发者去处理这个异常
public class EmptyNameException extends RuntimeException {
    public EmptyNameException() {
        super("name must not be empty");
    }
}

举个例子

public class Person {
  private String name;
  public Person(String name) {
      if(name == null || name.length() == 0) {
      throw new EmptyNameException();
      this.name = name;
    }
  }
}

5 使用异常的好处

  1. 将错误处理代码与普通代码区分开
  2. 能将错误信息传播到调用堆栈中
  3. 能对错误类型进行区分和分组