文章目录
异常处理
概述
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。
可避免的错误
-
代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;
-
使用System.out.println(11/0)
-
System.out.println(11/0) //java.lang.ArithmeticException: / by zero
-
-
强制类型转换
-
// 假设用户输入了abc: String s = "abc"; int n = Integer.parseInt(s); // NumberFormatException!
-
-
获取一个已经删除或者不存在的文件
-
// 用户删除了该文件: InputStream inputStream = new FileInputStream("src/lession12_01/Main.java");//FileNotFoundException!
-
不可避免的错误
- 网络突然断了,连接不到远程服务器;
- 内存耗尽,程序崩溃了;
- 用户点“打印”,但根本没有打印机;
处理错误的方式
一个健壮的程序必须处理各种各样的错误。即调用某个方法,如果出现错误,给调用方提示。
方式一:约定返回码
读取一个文件的时候,如果成功返回0,失败则返回1
int code = processFile("C:\\test.txt");
if (code == 0) {
// ok:
} else {
// error:
switch (code) {
case 1:
// file not found:
case 2:
// no read permission:
default:
// unknown error:
}
}
方式二:提供一个异常处理机制。
Java内置了一套异常处理机制,总是使用异常来表示错误。
异常是一种class,因此它本身带有类型信息。
异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了:
try {
String s = processFile(“C:\\test.txt”);
// ok:
} catch (FileNotFoundException e) {
// file not found:
} catch (SecurityException e) {
// no read permission:
} catch (IOException e) {
// io error:
} catch (Exception e) {
// other error:
}
异常的种类
- **检查性异常:**最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
- 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
- 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。
Error:表示严重的错误,系统崩溃。
- OutOfMemoryError:内存耗尽
- NoClassDefFoundError:无法加载某个Class
- StackOverflowError:栈溢出
Exception:运行时的错误,它可以被捕获并处理。
- RuntimeException以及它的子类;
- 非RuntimeException(包括IOException、ReflectiveOperationException等等)
必须捕获的异常
- 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。
- 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。
捕获异常
捕获异常使用try…catch语句,把可能发生异常的代码放到try {…}中,然后使用catch捕获对应的Exception及其子类:
在方法内部解决异常
// try...catch
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) {
try {
// 用指定编码转换String为byte[]:
return s.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
// 如果系统不支持GBK编码,会捕获到UnsupportedEncodingException:
System.out.println(e); // 打印异常信息
return s.getBytes(); // 尝试使用用默认编码
}
}
}
不使用try catch
在方法上抛出异常
运行不通过。
原因:toGBK方法抛出了异常,但是并没有地方捕获这个异常并抛出去。
public class Main {
public static void main(String[] args) {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) throws UnsupportedEncodingException {
// 用指定编码转换String为byte[]:
return s.getBytes("GBK");
}
}
改进措施:
-
在main方法的方法内捕获异常
-
public static void main(String[] args) { try { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); } catch (UnsupportedEncodingException e) { System.out.println(e); } }
-
-
在main方法上抛出异常
-
public static void main(String[] args) throws UnsupportedEncodingException { byte[] bs = toGBK("中文"); System.out.println(Arrays.toString(bs)); }
-
记录异常
出现异常后,我们进行捕获,捕获后,可以什么也不做,也可以记录异常。
什么也不干
static byte[] toGBK(String s) {
try {
return s.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
// 什么也不干
}
return null;
记录异常
static byte[] toGBK(String s) {
try {
return s.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
// 先记下来再说:
e.printStackTrace();
}
return null;
}
所有异常都可以调用printStackTrace()方法打印异常栈,这是一个简单有用的快速打印异常的方法。
小结
Java使用异常来表示错误,并通过try … catch捕获异常;
Java的异常是class,并且从Throwable继承;
Error是无需捕获的严重错误,Exception是应该捕获的可处理的错误;
RuntimeException无需强制捕获,非RuntimeException(Checked Exception)需强制捕获,或者用throws声明;
不推荐捕获了异常但不进行任何处理。
课堂展示
public class ExceptionTest {
public static void main(String[] args) {
// //1. 0作为除数
int a =5;
try{
a=a/0;
}catch (ArithmeticException e){
e.printStackTrace();
System.out.println("这里产生了异常,异常信息是"+e.getMessage());
System.out.println(e.getMessage());
Throwable throwable = e.fillInStackTrace();
System.out.println(throwable);
StackTraceElement[] stackTrace = e.getStackTrace();
System.out.println(stackTrace);
}
System.out.println("捕获了异常"+123456);
System.out.println(5/0);
System.out.println("未捕获异常"+123456);
//2.IO流读取
try {
InputStream inputStream = new FileInputStream("src/lession12_01/Main.java");
System.out.println(inputStream.read());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(123465);
//3. 强制类型转换
try {
String a = "dsjfhsdk";
Integer integer = Integer.valueOf(a);
System.out.println(integer);
}catch (NumberFormatException e){
e.printStackTrace();
}
System.out.println(132);
//内存溢出
int a =0;
Integer b = 1;
List e = new ArrayList();
while (a==0){
e.add(b);
b++;
}
//栈溢出
ceshi();
}
public static void ceshi(){
int a =1;
ceshi();
}
}
捕获异常
在Java中,凡是可能抛出异常的语句,都可以用try … catch捕获。把可能发生异常的语句放在try { … }中,然后使用catch捕获对应的Exception及其子类。
多Catch语句
如果程序中的某一段代码可能产生多个异常时,我们可以使用多个catch语句进行捕捉异常。每个catch分别捕获对应的Exception及其子类。JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。
注意:
- 异常捕获时,一定要先捕获小异常,再捕获大异常。
- 如果捕获到某个异常的父类,在这个catch之后还有当前这个异常,则不会执行之后的异常,因此RunTimeException和Exception要放在其他异常捕获的之后。
public class DivTest
{
public static void main(String[] args)
{
try
{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException ie)
{
System.out.println("数组越界:运行程序时输入的参数个数不够");
}
catch (NumberFormatException ne)
{
System.out.println("数字格式异常:程序只能接受整数参数");
}
catch (ArithmeticException ae)
{
System.out.println("算术异常");
}
catch (Exception e)
{
System.out.println("未知异常");
}
}
}
如果我们把Exception卸载IndexOutOfBoundsException之前。
访问异常信息
如果程序需要在catch 块中访问异常对象的相关信息,则可以通过访问catch 块的后异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。
所有的异常对象都包含了如下几个常用方法。
- getMessage():返回该异常的详细描述字符串。
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
- printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
- getStackTrace():返回该异常的跟踪栈信息。
public class AccessExceptionMsg
{
public static void main(String[] args)
{
try
{
FileInputStream fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
}
}
finally语句
无论是否有异常发生,如果我们都希望执行一些语句,可以用来回收资源操作。
如:try块中我们进行了一些数据库连接,网络连接和磁盘文件等操作,如果发生异常时,我们希望回收这些物理资源。
try{
//业务实现代码
}
catch (SubException e){
//异常处理块1
}
catch (SubException2 e){
//异常处理块2
}
finally{
//资源回收块
}
异常处理语法结构中只有try 块是必需的,也就是说,如果没有try块,则不能有后面的catch块和finally 块; catch 块和 finally 块都是可选的,但 catch 块和 finally 块至少出现其中之一,也可以同时出现;可以有多个catch块,捕获父类异常的catch 块必须位于捕获子类异常的后面;但不能只有try块,既没有catch 块,也没有finally 块:多个catch块必须位于try块之后,finally块必须位于所有的catch块之后。看如下程序。
public class FinallyTest
{
public static void main(String[] args)
{
FileInputStream fis = null;
try
{
fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
// return语句强制方法返回
return ; // ①
// 使用exit来退出虚拟机
// System.exit(1); // ②
}
finally
{
// 关闭磁盘文件,回收资源
if (fis != null)
{
try
{
fis.close();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
System.out.println("执行finally块里的资源回收!");
}
}
}
虽然我们使用过了return结束方法,但是此时并不会,而是先执行finallly块里的代码。
如果我们使用System.exit(1); 来退出虚拟机,则不会执行finally块。
finally块中不要使用return和throw等导致方法终止的语句。
异常处理的嵌套
Java7之前关闭数据库连接或文件资源。
FileInputStream file = null;
try{
file = new FileInputStream("a.txt");
}
finally{
if(fis!=null){
fis.close();
}
}
问题:代码臃肿。
Java7之后
try的后面可以跟一个圆括号,括号里面可以声明,初始化一个或多个资源。这里的资源指文件流或数据库连接需要显示关闭的资源。
注意:保证try能正常关闭资源,这些资源类必须实现AutoCloseable或Closeable接口,实现这两个接口必须实现close方法。
public class AutoCloseTest
{
public static void main(String[] args)
throws IOException
{
try (
// 声明、初始化两个可关闭的资源
// try语句会自动关闭这两个资源。
BufferedReader br = new BufferedReader(
new FileReader("AutoCloseTest.java"));
PrintStream ps = new PrintStream(new
FileOutputStream("a.txt")))
{
// 使用两个资源
System.out.println(br.readLine());
ps.println("测试资源");
}
}
}
小结
使用try … catch … finally时:
- 多个catch语句的匹配顺序非常重要,子类必须放在前面;
- finally语句保证了有无异常都会执行,它是可选的;
- 一个catch语句也可以匹配多个非继承关系的异常。
抛出异常
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个try … catch被捕获为止:
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
process2();
}
static void process2() {
Integer.parseInt(null); // 会抛出NumberFormatException
}
}
printStackTrace()对于调试错误非常有用,上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的,从下往上看,调用层次依次是:
- main()调用process1();
- process1()调用process2();
- process2()调用Integer.parseInt(String);
- Integer.parseInt(String)调用Integer.parseInt(String, int)。
抛出步骤
发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。
如何抛出异常?参考Integer.parseInt()
方法,抛出异常分两步:
- 创建某个
Exception
的实例; - 用
throw
语句抛出。
void process2(String s) {
if (s==null) {
NullPointerException e = new NullPointerException();
throw e;
}
}
或者
statoc void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
public class AutoCloseTest {
public static void main(String[] args)
throws IOException {
try {
process1("123");
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1(String s) {
try {
process2(null);
} catch (NullPointerException e) {
throw new IllegalArgumentException();
}
}
static void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
}
示例
public class ThrowTest
{
public static void main(String[] args)
{
try
{
// 调用声明抛出Checked异常的方法,要么显式捕获该异常
// 要么在main方法中再次声明抛出
throwChecked(-3);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
// 调用声明抛出Runtime异常的方法既可以显式捕获该异常,
// 也可不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception
{
if (a > 0)
{
// 自行抛出Exception异常
// 该代码必须处于try块里,或处于带throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a)
{
if (a > 0)
{
// 自行抛出RuntimeException异常,既可以显式捕获该异常
// 也可完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}
自定义异常
在通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常也包含了该异常的有用信息。所以在选择抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常。
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。
定义异常类时通常需要提供两个构造器:
- 无参数的构造器;
- 带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。
public class AuctionException extends Exception
{
// 无参数的构造器
public AuctionException(){}
// 带一个字符串参数的构造器
public AuctionException(String msg)
{
super(msg);
}
}
class AuctionException extends Exception
{
// 无参数的构造器
public AuctionException(){}
// 带一个字符串参数的构造器
public AuctionException(String msg)
{
super(msg);
}
}
public class AuctionTest
{
private double initPrice = 30.0;
// 因为该方法中显式抛出了AuctionException异常,
// 所以此处需要声明抛出AuctionException异常
public void bid(String bidPrice)
throws AuctionException
{
double d = 0.0;
try
{
d = Double.parseDouble(bidPrice);
}
catch (Exception e)
{
// 此处完成本方法中可以对异常执行的修复处理,
// 此处仅仅是在控制台打印异常跟踪栈信息。
e.printStackTrace();
// 再次抛出自定义异常
throw new AuctionException("竞拍价必须是数值,"
+ "不能包含其他字符!");
}
if (initPrice > d)
{
throw new AuctionException("竞拍价比起拍价低,"
+ "不允许竞拍!");
}
initPrice = d;
}
public static void main(String[] args)
{
AuctionTest at = new AuctionTest();
try
{
at.bid("df");
}
catch (AuctionException ae)
{
// 再次捕捉到bid方法中的异常。并对该异常进行处理
System.err.println(ae.getMessage());
}
}
}
使用日志
使用System.out.println(e.getMessage());的问题。
- 打印的时候需要进行标记
- 打印后,还得删除
- 服务器上不显示
使用日志的好处
- 可以设置输出样式,避免自己每次都写
"ERROR: " + var
; - 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
- 可以被重定向到文件,这样可以在程序运行结束后查看日志;
- 可以按包名控制日志级别,只输出某些包打的日志;
- 自动打印时间、调用类、调用方法等很多有用的信息。
public class Hello {
public static void main(String[] args) {
Logger logger = Logger.getGlobal();
logger.info("start process...");
logger.warning("memory is running out...");
logger.fine("ignored.");
logger.severe("process will be terminated...");
}
}
结果:
十一月 30, 2021 11:38:21 下午 exceptions.AuctionTest main
信息: start process…
十一月 30, 2021 11:38:22 下午 exceptions.AuctionTest main
警告: memory is running out…
十一月 30, 2021 11:38:22 下午 exceptions.AuctionTest main
严重: process will be terminated…
注意:
fine没有打印。
日志级别
JDK的Logging定义了7个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO(默认,级别比它低的日志打印不出来)
- CONFIG
- FINE
- FINER
- FINEST