目录
1、处理错误
异常分类
声明受查异常
如何抛出异常
创建异常类
2、捕获异常
捕获异常
捕获多个异常
再次抛出异常与异常链
finally子句
带资源的 try 语句
分析堆栈轨迹元素
3、使用异常机制的技巧
4、使用断言
断言的概念
启用和禁用断言
使用断言完成参数检查
记录日志
基本日志
高级日志
修改日志过滤器配置
本地化
日志记录总结说明
在现实状况中,存在程序出错或外部环境影响带来用户数据的丢失,从而损失客源。为避免这类事情的发生,至少应该做到以下几点:
1、向用户通告错误
2、保存所有工作结果
3、允许用户以妥善的形式推出程序
应该注意的错误类型:
1、用户输入错误
2、设备错误
3、物理限制
4、代码错误
对于方法中的一个错误,传统的做法是返回一个特殊的错误码,但并不是在任何情况下都能够返回一个错误码。方法在不能正常完成它的任务时,就可以通过另一种途径退出方法。在这种情况下,方法并不返回任何值,而是抛出(throw)一个封装了错误信息的对象,而这个方法会立刻退出,且调用该方法的代码也将无法继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器(exception handler)
在 Java 中,异常对象都是派生于 Throwable 类的一个实例,如果 Java 中内置的异常处理类不能满足要求,用户还可以自定义异常处理类(详见下文 创建异常类)。
继承层次:最高层:Throwable;第二层:Error 和 Exception;第三层:IOException 和 RuntimeException 继承 Exception
1、Error类描述了 Java 运行时系统的内部错误和资源耗尽错误。应用程序不应该抛出这种类型的对象,而是通知用户,并尽力使程序安全的终止,除此之外,没有别的办法。
2、在设计Java 时,需要关注Exception层次结构。由程序错误导致的异常属于 RuntimeException;而程序本身没问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。
有一条规则是:如果出现 RuntimeException,那就一定是你的问题。
通过检查数组下标避免 ArrayIndexOutOfBoundsException,通过检查是否为空来杜绝 NullPointerException。
Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常。编译器将核查是否为所有的受查异常提供了异常处理器。
一个方法需要告知编译器有可能会发生什么样的错误。如 public FileInputStream(String name) throws FileNotFoundException。
在遇到下面 4 种情况时,需要使用 throws 子句声明和抛出异常:
1、调用一个抛出受查异常的方法,例如 FileInputStream 构造器
2、程序运行过程中发现错误,并且利用 throw 语句抛出一个受查异常
3、程序出现错误,如数组下标错误导致的可能抛出一个ArrayIndexOutOfBoundsException 这样的非受查异常
4、Java 虚拟机和运行时库出现的内部错误
若出现前两种情况之一,就需要告诉调用该方法的程序员有可能抛出异常,跟域异常规范(exception specification),在犯法的首部声明这个方法可能抛出的异常,若有多个,使用逗号(,)分隔。
一个方法必须声明所有可能抛出的受查异常,不需要声明内部错误(即派生于Error)或继承自RuntimeException类的非受查异常,因为非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)而不是花费精力去声明或捕获它。如果方法没有声明所有可能发生的受查异常,编译器就会发出一个错误信息。
若子类覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类的更通用(更抽象);若超类方法没有抛出异常,子类也不能抛出任何受查异常。
String readData(Scanner in) throws EOFException{
...
while(...){
if(!in.hasNext()){ //EOF encountered
if(n < Len)
throw new EOFException();
/* 1、or:
* EOFException e = new EOFException();
* throw e;
* 2、除此之外,EOFException还有一个一个参数的构造方法,可以详细描述错误信息:
* String gripe - "Content-length: " + len + ",Received: " + n;
* throw new EOFException(gripe);
*/
}
...
}
return s;
}
一旦方法抛出了异常,这个方法就不可能返回到调用者,也就不用担心返回的默认值或错误代码的担忧。
前文在异常分类中讲到,Java 中内置的异常处理类不能满足要求,用户还可以自定义异常类。
需要做的只是定义一个派生于Exception的类,或派生于Exception子类的类。通常定义两个构造器,一个是默认的构造器,一个时带有详细信息的构造器。
class FileFormatException extendsIOException{
public FileFormatException(){}
public FileFormatException(String gripe){
super(gripe);
}
}
这样就可以抛出自己定义的异常类型了。
若在异常发生时没有在任何地方进行捕获,程序就会终止执行,并在控制台上打印出错误信息,其中包括异常的类型和堆栈的内容。
try {
...
code
...
}catch (ExceptionType e){
handle for this type
}
1、try 语句中任何代码抛出了在 catch 子句中说明的异常类,那么:
1)程序将跳过 try 语句块的其他代码。
2)程序将执行 catch 子句中的处理器代码。
2、若 try 语句中没有抛出任何异常,将跳过 catch 子句。
3、若在 try 中抛出的异常没有在 catch 中指出,方法将立刻退出。
通常,应该捕获那些知道处理的异常,而将那些不知道怎么处理的异常继续进行传递(即使用throws进行声明)
try {
...
}catch (FileNotFoundException e){
...
}catch (UnknownHostException e){
...
}catch (IOException e){
...
}
异常对象可能包含域异常本身有关的信息,要想获得对象的更多信息,可以试着使用 e.getMessage() 得到详细的错误信息,活着使用 e.getClass().getName() 得到异常对象的实际类型。
若异常处理动作是一样的,可以在catch子句中放置多个错误,如:
catch( FileNotFoundException | UnknownHostException ){. . .}
有时候希望捕获异常后做一些处理再行抛出异常,就可以使用以下方式:
try {
...
} catch(SQLException e) {
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
当捕获到异常时,可以使用下面这条语句重新得到原始异常:
Throwable e = se.getCause();
强烈建议使用这种包装技术,这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
若一个方法发生了一个受查异常,但不允许抛出,那么我们可以捕获这个异常,并将它包装成一个运行时异常。
有时也可能只想记录一下异常再重新抛出,而不做任何改变。
try {
...
} catch(Exception e){
logger.log(level, message, e);
throw e;
}
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出前必须被回收,那么就会产生资源回收问题。
下面介绍 Java 中如何恰当的关闭一个文件。如果使用 Java 编写数据库程序,就需要使用同样的技术关闭与数据库的连接。
不管是否捕获异常,finally 子句中的代码都将被执行。在下面的示例中,程序将在所有情况下关闭文件.
InputStream in = new FileInputStream(...);
try {
//1
...//code might have exception
//2
} catch(IOException e) {
//3
...
//4
} finally {
//5
in.close();
}
//6
以上代码中,有三种情况会执行finally子句:
1、代码没有抛出异常。会执行1,2,5,6
2、抛出一个在 catch 中捕获的异常。
1)若 catch 子句没有抛出异常,会执行1,3,4,5,6
2)若 catch 子句抛出异常,会执行1,3,5
3、代码抛出了一个异常,但不是由 catch 捕获的。会执行1,5
在 try 语句中,可以只有 finally,没有 catch。无论 try 中有没有异常,finally 的语句都会被执行
强烈建议解耦合 try/catch 和 try/finally 语句块。这样可以提高代码的清晰度
InputStream in = ...; try { try { ...//code might have exception } finally { in.close(); } } catch (IOException e) { ... }
内层的 try/catch 语句块的唯一职责:确保关闭输出流。
外层的 try/catch 语句块的唯一职责:且包报告出现的错误(这也将会报告 finally 中的错误)
try (Resource res = . . .) {
...
}
try 块退出时,会自动调用 res.close()。示例:
try (Scanner in = new Scanner(new FileInputStream("..."),"UTF-8");
PrintWriter out = new PrintWriter("out.txt")) {
while (in.hasNext())
out.println(in.next().toUpperCase());
}
不论这个块如何退出,in 和 out 都会关闭。
使用上一节中的解耦合,如果 try 块抛出一个异常,而且close方法也抛出一个异常,会丢失原始异常。而带资源的 try 语句可以很好的处理这种情况。原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。可以调用 getSuppressed 方法得到从 close 方法抛出并被抑制的异常列表。
堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。
打印递归阶乘函数的堆栈情况 源代码:
package com.company;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Scanner;
/**
* This program demonstrates the use of
*/
public class Main {
public static int factorial(int n){
System.out.println("factorial(" + n + " ):");
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement ste : frames){
System.out.println(ste);
}
int r;
if(n <= 1) r = 1;
else r = n * factorial(n - 1);
System.out.println("return " + r);
return r;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("Enter n: ");
int n = in.nextInt();
factorial(n);
}
}
1、异常处理不能代替简单的测试
1)if (!s.empty()) s.pop();
2)try {s.pop();} catch (EmptyStackException e) {...}
第一个用时 646 毫秒,第二种用时 21 739 毫秒。
2、不要过分地细化异常
3、利用异常层次结构
4、不要压制异常
5、在检测错误时,“苛刻“比放任好
6、不要羞于传递异常
在测试阶段,若编写过多的抛出或捕获异常的代码,即使测试完毕了也不会自动删除,这对于程序运行效率有非常大的影响。
断言机制允许在测试期间向代码中插入一些检查语句。当代码发布时,这些插入的检测语句将会被自动地移走。
Java 语言引入了关键字 assert,有以下两种形式:
1、assert 条件;
2、assert 条件:表达式;
如果条件检测结果为 false,将会抛出一个 AssertionError 的异常。在第二种形式中,表达式将被传入 AssertionError 的构造器,并转换成一个消息字符串,这也是该表达式的唯一作用。
例如:要想断言 x 是一个非负值:
assert x >= 0; 或将x值传递给AssertionError对象,从而在后面显示出来: assert x >= 0 : x ;
在默认情况下,断言是被禁用的。可以在运行程序时用 -enableassertions 或 -ea 选项启用:
java -enableassertions MyApp
也可以在某个类或整个包中使用断言:
java -ea:MyClass -ea:com.mycompany.mylib... MyApp
需要注意的是,启用或禁用断言不需要重新编译(javac)程序。启用或禁用断言时类加载器(class loader)的功能,当禁用断言时,断言代码将被跳过,因此不会影响程序运行效率。
可以使用 -disableassertions 或 -da 禁用某个特定的类和包的断言。
但是,对于那些没有类加载器的“系统类”,这个开关就不适用了。而需要使用 -enablesystemassertions 或 -esa 开启断言
在 Java 中,有三种处理系统错误的机制:1、抛出一个异常;2、记录日志;3、使用断言。
关于断言,需要记住:
1、断言失败是致命的、不可恢复的错误
2、断言检查只用于开发和检测阶段
因此,不应该使用断言想起他程序通告发生可可恢复性的错误,而只应该用于在测试阶段确定程序程序内部的错误位置。
看一个例子
//是否应该断言数组下标合法/非null值?
/**
* ...
* @param a the array to be sorted
* @throws IlleagalArgumentException if fromIndex > toIndex
* @throws ArrayIndexOutOfBoundsException if fromIndex < 0 or toIndex > a.length
* ...
*/
static void sort(int[] a, int fromIndex, int toIndex)
//对于这个方法,文档有指出,若下标不合法,将抛出异常。因此,这里使用断言就不合适
/**
* ...
* @param a the array to be sorted (must not be null)
* @throws IlleagalArgumentException if fromIndex > toIndex
* @throws ArrayIndexOutOfBoundsException if fromIndex < 0 or toIndex > a.length
* ...
*/
static void sort(int[] a, int fromIndex, int toIndex)
//对这个方法的文档进行了一点小改动之后,这个方法的调用者就必须注意,不允许使用null数组,所以需要在方法的开头使用断言:assert a != null;
计算机科学见将这种约定称为前置条件(Precondition)。上例中的原方法没有任何的前置条件,即说明对于任何情况,方法都能正确的返回,给予正确的执行。修订后的方法有一个前置条件,即a非空。
断言——在测试和调试阶段所使用的战术性工具
日志——在程序的整个生命周期都可以使用的策略性工具
我们时常在代码运行出现问题时通过 System.out.print 来进行根因追溯。而记录日志API就是为了解决这个问题而设计的。以下是该API的优点:
1、可以很容易地取消全部日志记录,或仅仅取消某个级别的日志,而且打开和关闭这个操作也很容液
2、可以简单地禁止日志记录的输出,因此将日志代码留在程序中的花销很小
3、日志记录可以定向到不同的处理器,用于在控制套中显示、存储进文件中等
4、日志记录器和处理器都可以对记录进行过滤。丢弃无用的记录项
5、日志记录可以采用不同的方式格式化、
6、应用程序可以使用多个日志记录器
7、在默认情况下,日志系统的配置由配置文件控制
要生成简单的日志记录,可以使用全局日志记录器(global logger)并调用其 info 方法:
Logger.getGlobal().ingo("File-xOpen menu item selected");
该条记录将会显示以下内容:
Time LoggingImageViewer fileOpen
INFO: File-xOpen menu item selected
但是,如果在合适的地方(如main开始)调用:Logger.getGlobal().setLevel(Level.OFF)
就会取消所有的日志。
企业级(industrial-strength)日志。在一个专业的应用程序中,不要将所有的日志记录在同一个全局日志记录器中,而是可以自定义日志记录器,使用 getLogger 方法创建或获取日志记录器。
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");)
(未被命名的日志记录器会被垃圾回收,所以需要同上赋值给一个静态变量存储其引用)
与包名(--.--.--)类似,日志记录器名也有层次结构,而且不同的是,日志记录器名有语义关系,即日志记录器的父与子之间将共享某些属性,如日志级别的继承。
日志级别:1、SERVERE;2、WATNING;3、INFO;4、CONFIG;5、FINE;6、FINER;7、FINEST;
默认情况下,只记录前三个级别。也可以设置其他级别,如:logger.setLevel(Level.FINE); 这样 FINE 和更高级别的记录都可以记录下来。还能使用 Level.ALL 开启所有级别的目录、Level.OFF 关闭所有的级别的记录
记录方法(对所有级别):logger.warning(message); 或 logger.fine(message); 或 logger.log(level.FINE, message);
日志记录中的常用方法:
1、一些跟踪执行流的方法:
void entering (String className, String methodName);
void entering (String className, String methodName, Object param);
void entering (String className, String methodName, Objectp[ params);
void exiting (String className, String methodName);
void exiting (String className, String methodName, Object result);
例如:
int read(String file, String pattern) {
logger.entering("com.mycompany.mylib.reader", "read", new Object[] { file, pattern });
...
logger.exiting("com.mycompany.mylib.reader", "read", count);
return count;
}
这些调用将生成 FINDER 级别和以字符串 ENTRY 和 RETURN 开始的日志记录
2、提供日志记录中包含的异常描述内容
void throwing (String className, String methodName, Throwable t)
void log (Level l, String message, Throwable t)
例如:
if(...) {
IOException exception = new IOException("...");
logger.throwing("com.mycompany.mylib.reader", "read", exception);
throw exception;
}
//or
try {
...
} catch (IOException e) {
Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e);
}
可以通过编辑配置文件来修改日志系统的各种属性,该配置文件默认路径是:
/jre/lib/logging.properties
要想使用另一个配置文件,就要将 java.util.logging.congif.file 的值设置为新配置文件的存储路径,并用下列命令启动应用程序:java -Djava.util.logging.config.file=configFile MainClass
修改默认日志级别:修改配置文件,并修改以下命令行:.level = INFO。也就是说,在日志记录器名后面添加后后缀 .level=...。
日志记录并不将消息发送到控制台上,这是处理器的任务,要想在控制台上看到FINE级别的消息,就需要进行以下配置:java.util.logging.ConsoleHandler.lecel=FINE
我们可能希望将日志消息本地化,以便让全球用户都可以阅读它。
本地化的应用程序包含资源包(resource bundle)中的本地特定信息。一个程序可能包含多个资源包,一个用于菜单,其他用于日志消息。
要想将一个映射添加到资源包中,需要为每个地区创建一个文件,英文消息映射位于 com/mycompany/logmessages_en.properties 文件中;德文消息映射位于 com/myconpany/logmessages_de.properties 文件中。可以将这些文件与应用程序的类文件放在一起,以便 ResourceBundle 类自动地对他们进行定位
在请求日志记录器时,可以制定一个资源包:Logger logger = Logger.getLogger(loggerName, "com.mycompany.logmessages"); 然后,为日志消息指定资源包的关键字,而不是实际的日志消息字符串:logger.info("readingFile");
通常需要在本地化的消息中增加一些参数,因此,消息应该包括占位符 {0},{1} 等,例如,想再日志消息中包含文件名:
//使用占位符
Reading file {0}.
Achtung! Datei {0} wird eingelesen.
//然后传递具体的值给占位符
logger.log (Level.INFO, "readimgFile", fileName);
logger.log (Level.INFO, "renamingFile", new Object[] { oldName, newName });
1、为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字。另外,可以通过调用以下方法得到日志记录器:private static final Logger logger = Logger.getLogger("loggerName");
2、默认的日志配置将级别>=INFO的消息记录到控制台。最好在应用程序中安装一个更加适宜的默认配置。
以下代码确保将所有的消息记录到应用程序特定的文件中,可放在main方法体内:
if(System.getProperty("java.util.logging.config.class") == null
&& System.getProperty("java.util.logging.config.file") == null){
try {
Logger.getLogger("").setLevel(Level.ALL);
final int LOC_ROTATION_COUNT = 10;
Handler handler = new FileHandler("%h/myapp.log",0, LOC_ROTATION_COUNT);
Logger.getLogger("").addHandler(handler);
} catch (IOException e) {
logger.log(Level.SEVERE, "Can't create log file handler", e);
}
}
3、现在,可以记录自己想要的内容了。但要牢记,所有级别>=INFO的消息都将显示到控制台上,因此,最好只把对用户有意义的消息设置为这三个级别(SEVERE、WARNING、INFO),将程序员想要的记录,设定为FINE是一个好的选择。
以下结合上述说明,实现:日志记录消息也显示在日志窗口中
package com.company;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.logging.*;
import javax.swing.*;
import javax.swing.text.html.ImageView;
/**
* A modification of the image viewer program that logs various events
* @author Cay Horstman
*/
public class Main {
public static void main(String[] args) {
if(System.getProperty("java.util.logging.config.class") == null
&& System.getProperty("java.util.logging.config.file") == null){
try {
Logger.getLogger("com.horstman.corejava").setLevel(Level.ALL);
final int LOC_ROTATION_COUNT = 10;
Handler handler = new FileHandler("%h/myapp.log",0, LOC_ROTATION_COUNT);
Logger.getLogger("com.horstman.corejava").addHandler(handler);
} catch (IOException e) {
Logger.getLogger("com.horstman.corejava").log(Level.SEVERE, "Can't create log file handler", e);
}
}
EventQueue.invokeLater(() -> {
Handler windowHandler = new WindowHandler();
windowHandler.setLevel(Level.ALL);
Logger.getLogger("com.horstman.corejava").addHandler(windowHandler);
JFrame frame = new ImageViewerFrame();
frame.setTitle("LoggingImageViewer");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Logger.getLogger("com.horstman.corejava").fine("Showing frame");
frame.setVisible(true);
});
}
}
/**
* The frame that shows the image
*/
class ImageViewerFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 400;
private JLabel label;
private static Logger logger = Logger.getLogger("com.horstman.corejava");
public ImageViewerFrame(){
logger.entering("ImageViewerFrame","");
setSize(DEFAULT_WIDTH,DEFAULT_HEIGHT);
//set up menu bar
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu menu = new JMenu("File");
menu.add(menu);
JMenuItem openItem = new JMenuItem("Open");
menu.add(openItem);
openItem.addActionListener(new FileOpenListener());
JMenuItem exitItem = new JMenuItem("Exit");
menu.add(exitItem);
exitItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
logger.fine("Exiting");
System.exit(0);
}
});
//use a label to display the image
label = new JLabel();
add(label);
logger.exiting("ImageViewerFrame","");
}
private class FileOpenListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
logger.entering("ImageViewerFrame.FileOpenListener","actionPerformed", e);
//set up file chooser
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
//accept all files ending with .gif
chooser.setFileFilter(new javax.swing.filechooser.FileFilter(){
@Override
public boolean accept(File f) {
return f.getName().toLowerCase().endsWith(".gif") || f.isDirectory();
}
@Override
public String getDescription() {
return "GIF images";
}
});
//show file chooser dialog
int r = chooser.showOpenDialog(ImageViewerFrame.this);
//if image file accepted, set it as icon of the label
if(r == JFileChooser.APPROVE_OPTION){
String name = chooser.getSelectedFile().getPath();
logger.log(Level.FINE, "Reading file {0}",name); //使用占位符
label.setIcon(new ImageIcon(name));
}
else {
logger.fine("File open dialog canceled,");
}
logger.exiting("ImageViewerFrame.FileOpenListener","actionPerformed");
}
}
}
/**
* A handler for the displaying log records in a window
*/
class WindowHandler extends StreamHandler{
private JFrame frame;
public WindowHandler(){
frame = new JFrame();
final JTextArea output = new JTextArea();
output.setEditable(false);
frame.setSize(200,200);
frame.add(new JScrollPane(output));
frame.setFocusableWindowState(false);
frame.setVisible(true);
setOutputStream(new OutputStream() {
@Override
public void write(int b) throws IOException {
//not called
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
output.append(new String(b,off,len));
}
});
}
public void publish(LogRecord record){
if(!frame.isVisible()) return;
super.publish(record);
flush();
}
}