Java异常处理——Java教案(九)

异常处理

概述

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

可避免的错误

  1. 代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error;

  2. 使用System.out.println(11/0)

    1. System.out.println(11/0) //java.lang.ArithmeticException: / by zero
      
  3. 强制类型转换

    1. // 假设用户输入了abc:
      String s = "abc";
      int n = Integer.parseInt(s); // NumberFormatException!
      
  4. 获取一个已经删除或者不存在的文件

    1. // 用户删除了该文件:
      InputStream inputStream = new FileInputStream("src/lession12_01/Main.java");//FileNotFoundException!
      

不可避免的错误

  1. 网络突然断了,连接不到远程服务器;
  2. 内存耗尽,程序崩溃了;
  3. 用户点“打印”,但根本没有打印机;

处理错误的方式

一个健壮的程序必须处理各种各样的错误。即调用某个方法,如果出现错误,给调用方提示。

方式一:约定返回码

读取一个文件的时候,如果成功返回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:
}

异常的种类

  • **检查性异常:**最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。
  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

image-20211130162117112

Error:表示严重的错误,系统崩溃。

  • OutOfMemoryError:内存耗尽
  • NoClassDefFoundError:无法加载某个Class
  • StackOverflowError:栈溢出

Exception:运行时的错误,它可以被捕获并处理。

  1. RuntimeException以及它的子类;
  2. 非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

image-20211130204655939

在方法上抛出异常

运行不通过。

原因: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");
        }

}

image-20211130205039620

改进措施:

  1. 在main方法的方法内捕获异常

    1.     public static void main(String[] args) {
                  try {
                      byte[] bs = toGBK("中文");
                      System.out.println(Arrays.toString(bs));
                  } catch (UnsupportedEncodingException e) {
                      System.out.println(e);
                  }
              }
      
  2. 在main方法上抛出异常

    1.     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代码块,然后不再继续匹配。

注意:

  1. 异常捕获时,一定要先捕获小异常,再捕获大异常。
  2. 如果捕获到某个异常的父类,在这个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之前。

image-20211130212722245

访问异常信息

如果程序需要在catch 块中访问异常对象的相关信息,则可以通过访问catch 块的后异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。
所有的异常对象都包含了如下几个常用方法。

  1. getMessage():返回该异常的详细描述字符串。
  2. printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
  3. printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
  4. 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("测试资源");
		}
	}
}

image-20211130221525431

小结

使用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
    }

}

image-20211130221953147

printStackTrace()对于调试错误非常有用,上述信息表示:NumberFormatException是在java.lang.Integer.parseInt方法中被抛出的,从下往上看,调用层次依次是:

  1. main()调用process1();
  2. process1()调用process2();
  3. process2()调用Integer.parseInt(String);
  4. Integer.parseInt(String)调用Integer.parseInt(String, int)。

抛出步骤

发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。

如何抛出异常?参考Integer.parseInt()方法,抛出异常分两步:

  1. 创建某个Exception的实例;
  2. 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();
        }
    }
}

image-20211130222639164

示例

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基类。

定义异常类时通常需要提供两个构造器:

  1. 无参数的构造器;
  2. 带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的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());的问题。

  1. 打印的时候需要进行标记
  2. 打印后,还得删除
  3. 服务器上不显示

使用日志的好处

  1. 可以设置输出样式,避免自己每次都写"ERROR: " + var
  2. 可以设置输出级别,禁止某些级别输出。例如,只输出错误日志;
  3. 可以被重定向到文件,这样可以在程序运行结束后查看日志;
  4. 可以按包名控制日志级别,只输出某些包打的日志;
  5. 自动打印时间、调用类、调用方法等很多有用的信息。
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
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

See you !

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值