Java学习笔记
异常处理
异常处理机制
- Java的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理”分离,提供更高的可读性。
使用try…catch捕获异常
if (正常)
{
//业务实现代码
...
}
else
{
alert 输入不合法
goto retry
}
-
上面代码中的if块依然不可表示——一切正常都是很抽象的,无法转换为计算机可识别的代码,在这种情形下,Java提出了一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码在try块中定义,所有的异常处理逻辑放在catch块中进行处理。下面是Java异常处理机制的语法结构
try { //业务实现代码 ... } catch (Exception e) { alert 输入不合法 goto retry }
异常类的继承体系
- 当Java运行时环境接收到异常对象时,如何为该异常对象寻找catch块呢?我们注意到catch关键字的形式:(Exception e),这意味着每个catch块都是专门用于处理该异常类及其子类的异常实例。
- 当Java运行时环境接收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理该异常;否则再次拿该异常对象和下一个catch块里的异常类进行比较。
- Java提供了非常丰富的异常类,这些异常类之间有严格的继承关系。、
- 由该图可以看出,Java把所有的非正常情况分为两种,异常(Exception)和错误(Error),它们都继承Throwable父类。
- Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态连接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类,下面看几个简单的异常捕获例子。
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 a)
{
System.out.println("未知异常");
}
}
}
- 执行结果如下:
lancibe@lancibe-PC:~/java/test/src/chapter10/src$ java DivTest
数组越界:运行程序时输入的参数个数不够
lancibe@lancibe-PC:~/java/test/src/chapter10/src$ java DivTest 1 3
您输入的两个数相除的结果是:0
lancibe@lancibe-PC:~/java/test/src/chapter10/src$ java DivTest 1 2.8
数字格式异常:程序只能接受整数参数
lancibe@lancibe-PC:~/java/test/src/chapter10/src$ java DivTest 1 0
算术异常
import java.util.Date;
public class NullTest {
public static void main(String[] args) {
Date d = null;
try
{
System.out.println(d.after((new Date())));
}
catch (NullPointerException ne)
{
System.out.println("空指针异常");
}
catch (Exception e)
{
System.out.println("未知异常");
}
}
}
- 上面程序针对NullPointerException异常提供了专门的异常处理块。上面程序调用一个null对象的after()方法,这将引发NullPointerException异常(当试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常),Java运行时将会调用该异常对应的catch块来处理该异常;如果程序遇到其它异常,Java运行时将会调用最后的catch块来处理异常。
- 将Exception类的catch块放在最后,因为他是所有异常类的父类,如果在前面,则后面的catch块永远得不到执行的机会。
- 实际上,进行异常捕获时不仅需要把Exception类对应的catch块放在最后,而且所有父类的catch块都应该排在子类异常catch块的后面(简称:先处理小异常,再处理大异常
Java 7 新增的多异常捕获
- 在Java 7 以前,每个catch块只能捕获一种类型的异常;但从Java 7开始,一个catch块可以捕获多种类型的异常。
- 使用一个catch块捕获多种类型的异常时需要注意如下两个地方:
- 捕获多种类型的异常时,多种类型的异常之间用竖线|隔开
- 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值
- 下面程序示范了Java 7提供的多异常捕获
public class MultiExceptionTest {
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|NumberFormatException|ArithmeticException ie)
{
System.out.println("程序发生了数组越界、数字格式异常、算数异常之一");
//有final修饰 故下面代码错误
//ie = new ArithmeticException("test");
}
catch (Exception e)
{
System.out.println("未知异常");
//没有final修饰 故下面代码正确
e = new RuntimeException("test");
}
}
}
- 运行结果如下
ancibe@lancibe-PC:~/java/test/src/chapter10/src$ java MultiExceptionTest 1 3
您输入的两个数相除的结果是:0
lancibe@lancibe-PC:~/java/test/src/chapter10/src$ java MultiExceptionTest
程序发生了数组越界、数字格式异常、算数异常之一
访问异常信息
- 如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序即可通过该参数来获得异常的相关信息。
- 所有的异常对象都包含了如下几个常用的方法
getMessage():
返回该异常的详细描述字符串printStackTrace():
将该异常的跟踪栈信息输出到标准错误输出printStackTrace(PrintStream s):
将该异常的跟踪栈信息输出到指定输出流getStackTrace():
返回该异常的跟踪栈信息
- 下面例子演示了程序如何访问异常信息:
import java.io.FileInputStream;
import java.io.IOException;
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();
}
}
}
- 运行结果如下
lancibe@lancibe-PC:~/java/test/src/chapter10/src$ java AccessExceptionMsg
a.txt (没有那个文件或目录)
java.io.FileNotFoundException: a.txt (没有那个文件或目录)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:211)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:153)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:108)
at AccessExceptionMsg.main(AccessExceptionMsg.java:8)
- 系统找不到指定的文件,这就是调用异常的getMessage()方法返回的字符串,下面更详细的信息是该异常的跟踪栈信息。
使用 finally 回收资源
- 有时候,程序在try里打开了一些物理资源,例如数据库连接、网络连接和磁盘文件等,这些物理资源都必须显式回收。
Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存
- 为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。完整的Java异常处理语法结构如下:
try
{
//业务实现代码
...
}
catch (SubException e)
{
//异常处理块1
...
}
catch (SubException2 e)
{
//异常处理块2
...
}
...
finally
{
//资源回收块
...
}
- 异常处理语法结构中,只有try块是必须的,finally块必须位于所有catch块之后
import java.io.FileInputStream;
import java.io.IOException;
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;
}
finally {
// 关闭磁盘文件,回收资源
if(fis != null)
{
try
{
fis.close();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
System.out.println("执行finally块里的资源回收");
}
}
}
Java 9里增强的自动关闭资源的try语句
- 在前面程序中看到,当程序使用finally块关闭资源时,程序显得异常臃肿。可以使用自动关闭资源的try语句:
import java.io.*;
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("123123");
}
}
}
- 上面程序中粗体字代码分别声明、初始化了两个IO流,由于BufferedReader、PrintStream都实现了Closeable接口,而且他们放在try语句中声明、初始化,所以try语句会自动关闭它们。因此上面这个程序是安全的。
- 如果程序需要,自动关闭资源的try语句后也可以带多个catch块和一个finally块。
Checked异常和Runtime异常体系
-
Java的异常被分为两大类:Checked异常和Runtime异常。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常被称为Checked异常。
-
对于Checked异常的处理方式有如下两种:
- 当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修复该异常
- 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常
-
Runtime异常则更加灵活,无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。
使用throws声明抛出异常
- 使用throws声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对待异常的处理方法是:打印异常的跟踪栈信息,并终止程序运行,这就是前面程序在遇到异常后自动结束的原因。
- 一旦使用throws抛出某异常时,程序就无需使用try…catch语句来捕获该异常了:
import java.io.FileInputStream;
import java.io.IOException;
public class ThrowsTest {
public static void main(String[] args)
throws IOException
{
FileInputStream fis = new FileInputStream("b.txt");
}
}
- 如果某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try块中显式捕获该异常,要么放在另一个带throws声明抛出的方法中:
import java.io.FileInputStream;
import java.io.IOException;
public class ThrowsTest2 {
public static void main(String[] args)
throws Exception
{
//因为test()方法声明抛出IOException异常
//所以调用该方法的代码要么处于try...catch块中,要么处于另一个throws声明抛出的方法中
test();
}
public static void test()
throws IOException
{
FileInputStream fis = new FileInputStream("b.txt");
}
}
方法重写时声明抛出异常的限制
- 使用throws声明抛出异常时有一个限制,就是方法重写时“两小”中的一条规则:子类方法抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同。
使用throw抛出异常
- 当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,使用throw语句完成(注意这里是throw,和前面的throws不同)
抛出异常
- 如果需要在程序中自行抛出异常,则应该使用throw语句,它可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句语法格式如下:
throw ExceptionInstance
- 可以利用throw语句改写五子棋游戏中用户输入的代码:
try
{
//将用户输入的字符串分隔开
String[] posStrArr = inputStr.split(",");
//将两个字符串转换成用户下棋的坐标
int xPos = Integer.parseInt(posStrArr[0]);
int yPos = Integer.parseInt(posStrArr[1]);
//如果用户试图下棋的坐标点已经有棋了,程序自行抛出异常
if (!gb.board[xPos - 1][yPos - 1].equals("+"))
{
throw new Exception("您试图下棋的坐标点已经有棋子了");
}
//把对应的数组元素赋值为O
gb.board[xPos - 1][yPos - 1] = "O";
}
catch (Exception e)
{
System.out.println("您输入的坐标不合法");
continue;
}
- 如果throw语句抛出的异常类型是Checked异常,则该throw语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无需放在try块里,也无需放在带throws声明抛出的方法中;程序既可以显示使用try…catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。
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);
}
}
catch和throw同时使用
- 前面介绍的异常处理机制有两种
- 在出现异常的方法内捕捉并处理异常,该方法的调用者将不能再次捕获该异常
- 该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理
- 实际应用中往往需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。
- 为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成:
public class AuctionTest {
private double initPrice = 30.0;
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());
}
}
}
Java 7增强的throw语句
- 对于如下代码:
try
{
new FileOutputStream("a.txt");
}
catch (Exception ex)
{
ex.printStackTrace();
throw ex;
}
- 上面代码片段中的throw语句再次抛出了捕获到的异常,但这个ex对象的情况比较特殊:程序捕获该异常时,声明该异常的类型为Exception;但实际上try块中可能只调用了FileOutputStream构造器,这个构造器声明只是抛出了FileNotFoundException异常。
- 在Java 7以前,Java编译器的处理“简单粗暴”——由于在捕获该异常时声明ex的类型是Exception,因此Java编译器认为这段代码可能抛出Exception异常,所以包含这段代码的方法通常需要声明抛出Exception异常。
import java.io.FileOutputStream;
public class ThrowTest2 {
public static void main(String[] args)
throws Exception
{
try
{
new FileOutputStream("b.txt");
}
catch (Exception ex)
{
ex.printStackTrace();
throw ex;
}
}
}
- 从Java 7开始,Java编译器会执行更加细致的检查,Java编译器会检查throw语句抛出异常的实际类型,这样编译器知道throw处实际上只可能抛出FileNotFoundException异常,因此在方法签名中只要声明抛出FileNotFoundException异常即可。
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
public class ThrowTest3 {
public static void main(String[] args)
throws FileNotFoundException
{
try
{
new FileOutputStream("b.txt");
}
catch (Exception ex)
{
ex.printStackTrace();
throw ex;
}
}
}
异常链
- 对于真实的企业级应用来说,常常有严格的分层款西,层与层之间有非常清晰的划分,上层功能的实现依赖于下层的API,也不会跨层访问。
- 当业务逻辑层访问持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面。有两个原因
- 对于正常用户而言,他们不想看到底层SQLException异常,SQLException异常对他们使用该系统没有任何帮助
- 对于恶意用户而言,将SQLException暴露出来不安全
- 把底层的原始异常直接传给用户是一种不负责任的表现。通常的做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了用户的提示信息,这种处理方式被称为异常转译。假设程序需要实现工资结算的方法,则程序应该采用如下结构的代码来实现该方法
public void calSal() throws SalException
{
try
{
//实现工资结算的业务逻辑
...
}
catch (SQLException sqle)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的message就是对用户的提示
throw new SalException("访问底层数据库出现异常");
}
catch (Exception e)
{
//把原始数据记录下来,留给管理员
...
//下面异常中的message就是对用户的提示
throw new SlaException("系统出现未知异常");
}
}
- 在JDK1.4以前,程序员必须自己编写代码来保持原始异常信息。从JDK1.4往后,所有Throwable的子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。例如上面的代码可以改写为如下形式:
public void calSal() throws SalException
{
try
{
//实现结算工资的业务逻辑
...
}
catch(SQLException sqle)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的sqle就是原始异常
throw new SalException(sqle);
}
catch(Exception e)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的e就是原始异常
throw new SalException(e);
}
}
- 上面程序中粗体字代码创建SalException对象时,传入了一个Exception对象,而不是传入了一个String对象,这就需要SalException类有相应的构造器。从JDK1.4以后,Throwable基本已有了一个可以接收Exception参数的方法,所以可以采用如下代码来定义SalException类。
public class SalException extends Exception {
public SalException(){
}
public SalException(String msg)
{
super(msg);
}
//创建一个可以接收Throwable参数的构造器
public SalException(Throwable t)
{
super(t);
}
}
- 创建了这个SalException业务异常类后,就可以用它来封装原始异常,从而实现对异常的链式处理。
Java的异常跟踪栈
- 异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据该方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
class SelfException extends RuntimeException
{
SelfException(){
}
SelfException(String msg)
{
super(msg);
}
}
public class PrintStackTraceTest {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod()
{
secondMethod();
}
public static void secondMethod()
{
thirdMethod();
}
public static void thirdMethod()
{
throw new SelfException("自定义异常信息");
}
}
- 运行结果如下:
lancibe@lancibe-PC:~/java/test/src/chapter10/src$ java PrintStackTraceTest
Exception in thread "main" SelfException: 自定义异常信息
at PrintStackTraceTest.thirdMethod(PrintStackTraceTest.java:23)
at PrintStackTraceTest.secondMethod(PrintStackTraceTest.java:19)
at PrintStackTraceTest.firstMethod(PrintStackTraceTest.java:15)
at PrintStackTraceTest.main(PrintStackTraceTest.java:11)
- 下面例子示范了多线程程序中发生异常的情形:
public class ThreadExceptionTest implements Runnable {
@Override
public void run() {
firstMethod();
}
public void firstMethod()
{
secondMethod();
}
public void secondMethod()
{
int a = 5;
int b = 0;
int c = a/b;
}
public static void main(String[] args) {
new Thread(new ThreadExceptionTest()).start();
}
}
- 程序运行结果如下:
lancibe@lancibe-PC:~/java/test/src/chapter10/src$ java ThreadExceptionTest
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at ThreadExceptionTest.secondMethod(ThreadExceptionTest.java:14)
at ThreadExceptionTest.firstMethod(ThreadExceptionTest.java:8)
at ThreadExceptionTest.run(ThreadExceptionTest.java:4)
at java.base/java.lang.Thread.run(Thread.java:832)
异常处理规则
- 前面介绍了使用异常处理的优势、便捷之处,本节将进一步从程序性能优化、结构优化的角度给出异常处理的一般规则。成功的异常处理应该实现如下四个目标:
- 使程序代码混乱最小化
- 捕获并保留诊断信息
- 通知合适的人员
- 采用合适的方式结束异常活动
不要过度使用异常
- 不可否认,Java的异常机制确实方便,但滥用异常机制也会带来一些负面影响。过度使用异常主要有两个方面:
- 把异常和普通错误混淆在一起,不在编写任何错误处理代码,而是以简单地抛出异常来代替所有的错误处理。
- 使用异常处理来代替流程控制
- 熟悉了异常使用方法后,程序员可能不再愿意编写繁琐的错误处理代码,而是简单的抛出异常,这样做是不对的。对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性;对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有对外部的、不能确定的和预知的运行时错误才使用异常。
- 另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的程序流程控制。
不要使用过于庞大的try块
- 在一个try块里放置大量的代码看上去“很简单”,但这种简单只是一种假象,只是在编写程序时看上去简单。但因为try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也增加。
- 而且当try块过于庞大时,就难免在try块后紧跟大量的catch块才可以针对不同的异常提供不同的处理逻辑。同一个try块后紧跟大量的catch块则需要分析他们之间的逻辑关系,反而增加了编程复杂度。
避免使用Catch All语句
- 所谓Catch All语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常:
try
{
//可能引发Checked异常的代码
}
catch (Throwable t)
{
//进行异常处理
t.printStackTrace();
}
- 有两个不足之处:
- 所有异常采用了相同的处理方式
- 如果出现了一些“关键”异常,会被“忽略”掉。
不要忽略捕获到的异常
- 既然已经捕获到异常,那catch块理应做一些有用的事——处理并修复这个错误。catch块整个为空,或者仅仅打印出错信息都是不妥的。