异常分类
由编程错误导致的异常属于RuntimeException。
派生于RuntimeException的异常包括以下问题:
1.错误的强制类型转换;
2.数组访问越界;
3.访问null指针。
不是派生于RuntimeException的异常包括:
1.试图超越文件末尾继续读取数据;
2.试图打开一个不存在的文件;
3.试图根据给定的字符串查找class对象,而这个字符串表示的类并不存在。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型(unchecked),所有其他的异常称为检查型(checked)异常。
声明检查型异常
例如下面是一个标准库中FileInputStream类的一个构造器的声明:
public FileInputStream(String name) throws FileNotFoundException
有些Java方法包含在对外提供的类中,对于这些方法,应该通过方法首部的异常规范(exception specification)声明这个方法可能抛出异常。
class MyAnimation
{
...
public Image loadImage(String s) throws IOException
{
...
}
}
如果一个方法有可能抛出多个检查型异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。
class MyAnimation
{
...
public Image loadImage(String s) throws FileNotFoundException,EOFException
{
...
}
}
但是,不需要声明Java的内部错误,即从Error继承的异常。任何程序代码都有可能抛出那些异常,而我们对此完全无法控制。
class MyAnimation
{
...
void drawImage(int i) throws ArrayIndexOutOfBoundsException //bad style
{
...
}
}
如何抛出异常
一个名为readData的方法正在读取一个文件,文件首部包含以下信息,承诺文件长度为1024个字符:Content-length:1024
String gripe = "Content-length:"+len+",Received:"+n;
throw new EOFException(gripe);
创建异常类
自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器:
class FileFormatException extends IOException
{
public FileFormatException() {}
public FileFormatException(String gripe)
{
super(gripe);
}
}
现在,就可以抛出你自己定义的异常类型:
String readData(BufferedReader in) throws FileFormatException
{
...
while(...)
{
if(ch==-1)
{
if(n<len)
throw new FileFormatException();
}
...
}
return s;
}
捕获异常
要想捕获一个异常,需要设置try/catch语句块:
try
{
code
more code
more code
}
catch (ExceptionType e)
{
handler for this type
}
如果try语句块中的任何代码抛出了catch子句中指定的一个异常类,那么
1.程序将跳过try语句块的其余代码;
2.程序将执行catch子句中的处理器代码。
为了展示捕获异常的处理过程,下面给出一个很典型的读取数据的代码:
public void read(String filename)
{
try
{
var in = new FileInputStream(filename);
int b;
while ((b=in.read()) != -1)
{
process input
}
}
catch (IOException exception)
{
exception.printStackTrace();
}
}
如果编写一个方法覆盖超类的方法,而这个超类方法没有抛出异常(如JComponent中的paintComponent),你就必须捕获你的方法代码中的出现的每一个检查型异常。不允许在子类的throws说明符中出现超类方法未列出的异常类。
捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不用类型的异常做出不同的处理。
要为每个异常类型使用一个单独的catch子句如下所示:
try
{
code that might throw exceptions
}
catch(FileNotFoundException e)
{
emergency action for missing files//缺少文件
}
catch(UnknownHostException e)
{
emergency action for unknown hosts//未知主机异常
}
catch(IOException e)
{
emergency action for all other I/O problems
}
再次抛出异常与异常链
可以在catch子句中抛出一个异常。通常,希望改变异常的类型时会这样做,如果开发了一个供其他程序员使用的子系统,可以使用一个指示子系统故障的异常类型,ServletException就是这样一个异常类型的例子,执行一个servlet的代码可能不想知道发生错误的细节原因,但希望明确地知道servlet是否有问题:
可以如下捕获异常并将它再次抛出:xinyichang
try
{
access the database
}
catch(SQLException)
{
throw new ServletException("database error:"+e.getMessage());
}
在这里,构造ServleException时提供了异常的消息文本。
不过,可以有一种更好的处理方法,可以把原始异常设置为新异常设置为新异常的“原因”:
try
{
access the database
}
catch (SQLException original)
{
var e = new ServletException("database error");
e.initCause(original);
throw e;
}
捕获到这个异常时,可以使用下面这条语句获取原始异常:
Throwable original = caughtException.getCause();
这种包装技术,可以在子系统中抛出高层异常,而不会丢失原始异常的细节。
finally子句
代码抛出一个异常时,就会停止处理这个方法中剩余的代码,并退出这个方法。
不管是否有异常被捕获,finally子句中的代码都会执行。在下例中,所有情况下程序都将被关闭输入流:
var in= new FileInputStream(...);
try
{
//1
code that might throw exceptions
//2
}
catch (IOException e)
{
//3
show error message
//4
}
finally
{
//5
in.close();
}
//6
以上有三种情况执行finally子句:
1.代码没有抛出异常。先执行try语句块的全部代码,然后执行finally子句中的代码,随后继续执行finally子句之后的第一条语句。也就是执行的顺序是1、2、5、6.
2.代码抛出一个异常,并在一个catch子句中捕获。如上就是IOException异常,在这种情况下,程序将执行try语句块中的所有代码,直到抛出异常为止。此时将跳过try语句块中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码。
如果catch子句没有抛出异常,程序将执行finally字句之后的第一条语句即顺序是1、3、4、5、6.
如果catch子句抛出了一个异常,异常将被抛回到这个方法的调用者,顺序为1、3、5.
3.代码抛出了一个异常,但没有任何catch子句捕获这个异常。在这种情况下,程序将执行try语句块中的所有语句,直到抛出异常为止。此时,将跳过try语句块的剩余代码,然后执行finally子句中的语句,并将异常抛回给这个方法的调用者。执行顺序只是1、5.
try-with-Resources语句
try-with-resources语句的最简形式:
try(*Resource* res = ...)
{
work with res
}
try块退出时,会自动调用res.close()。下面有个例子,这里要读取一个文件中的所有单词:
try (var in = new Scannner(
new FileInputStream("/usr/share/dict/words"),StandardCharsets.UTF_8))
{
while (in.hasNext())
System.out.println(in.next());
}
这个块正常退出时,或者存在一个异常时,都会调用in.close()方法,就好像使用了finally块一样。
还可以指定多个资源:
try(var in = new Scanner(
new FileInputStream("/usr/share/dict/words"),StandardCharsets.UTF_8);
var out = new PrintWriter("out.txt",StandardCharsets.UTF_8))
{
while (in.hasNext())
out.println(in.next().toUpperCase());
}
分析堆栈轨迹元素
堆栈轨迹(stack trace)是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。
var t = new Throwable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
使用异常的技巧
1.异常处理不能代替简单的测试
示例,编写一段代码,试着将一个空栈弹出10000000次,首先在弹栈之前先查看栈是否为空:
if(!s.empty()) s.pop();
接下来,我们强制要求不论栈是否为空都执行弹出操作,然后捕获EmptyStackException异常来指出我们不能这样做。
try
{
s.pop();
}
catch (EmptyStackException e)
{
}
2.不要过分地细化异常
很多程序员习惯将每一条语句都分装在一个独立的try语句块中。
PrintStream out;
Stack s;
try
{
for (i = 0;i<100;i++)
{
n=s.pop();
out.writeInt(n);
}
}
catch (EmptyStackException e)
{
//stack was empty
}
catch (IOException e)
{
//problem writing to file
}
3.充分利用异常层次结构
不要只抛出RuntimeException异常。应该寻找一个适合的子类或创建自己的异常类。
不要只捕获Throwable异常,否则,这会是你的代码更难读、更难维护。
4.不要压制异常
5.在检测错误时,“苛刻”要比放任更好
6.不要羞于传递异常
使用断言
假设确信某个属性符合要求,并且代码的执行依赖于这个属性。例如,需要计算double y = Math.sqrt(x);
你确信这里的x是一个非负数。原因是:x是另外一个计算的结果,而这个结果不可能是负值;或者x是一个方法的参数,这个方法要求它的调用者只能提供一个正数输入。不过,你可能还是想再做一次检查,不希望计算中潜入让人困惑的“不是一个数”(NaN)浮点值。当然,也可以抛出一个异常:
if(x<0) throw new IllgealArgumentException(“x<0”);
启用和禁言断言
在默认情况下,断言是禁用的。可以在运行程序时用-enableassertions或-ea选项启用断言:
java -enableassertions MyApp
启用或禁用断言是类加载器(class loader)的功能。禁用断言时,类加载器会去除断言代码,因此,不会降低程序运行速度。
也可以在某个类或整个包中启用断言,例如:
java -ea:MyClass -ea:com.mycompany.mylib MyApp
这条命令将为MyClass类以及com.mycompany.mylib包和它的子包中的所有类打开断言。选项-ea将打开无名包中所有类的断言。
也可以用选项-disableassertions或-da在某个特定类或包中禁用断言:
java -ea:... -da:MyClass MyApp
使用断言完成参数检查
断言只应该用于在测试阶段确定程序内部错误的位置。
使用断言提供假设文档
例如:
if(i % 3 == 0)
...
else if (i % 3 == 1)
...
else//(i % 3 == 2)
...
使用断言后为:
if(i % 3 ==0)
...
else if(i % 3 == 1)
...
else
{
assert i % 3 ==2;
...
}
日志
要生成简单的日志记录,可以使用全局日志记录器(global logger)并调用其info方法:
Logger.getGlobal().info("File->Open menu item selected");
在默认情况下,会如下打印这个记录:
May 10,2013 10:23:15 PM LoggingImageViewer fileOpen
INFO:File->Open menu item selected
但是,如果在适当的地方调用
Logger.getGlobal().setLevel(Level.OFF);
将会取消所有日志。
高级日志
可以调用getLogger方法创建或获取日志记录器:
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
默认的日志记录将显示根据调用堆栈得出的包含日志调用的类名和方法名。
可以用logp方法获得调用类和方法的确切位置,这个方法的签名为:
void logp(Level l, String className,String methodName, String message)
修改日志管理器配置
可以通过编辑配置文件来修改日志系统的各个属性。在默认情况下,配置文件位于:
conf/logging.properties
要想使用另一个配置文件,就要将java.util.logging.config.file属性设置为那个文件的位置,为此要用以下命令启动应用程序:
java -Djava.util.logging.config.file=configFile MainClass
要想修改默认的日志级别,就需要编辑配置文件,并修改以下命令行
.level=INFO
可以通过添加下面这一行来指定自定义日志记录器的日志级别:
com.mycompany.myapp.level=FINE
日志管理器在虚拟机启动时初始化,也就是main方法执行前,若想要定制日志属性,但是没有用-Djava.util.logging.config.file命令行选项启动应用。可以在程序中调用System.outProperty(“java.util.logging.config.file”,file)
处理器
在默认情况下,日志记录将记录发送到ConsoleHandler,并由它输出到System.err流。
这个处理器扩展了StreamHandler类,并安装了一个流,这个流的write方法将流输出显示到一个文本区中。
过滤器
在默认情况下,会根据日志记录的级别进行过滤。每个日志记录器和处理器都有一个可选的过滤器来完成附加的过滤。要定义一个过滤器,需要实现Filter接口并定义以下方法:
boolean isLoggable(LogRecord record)
格式化器
ConsoleHandler类和FileHandler类可以生成文本和XML格式的日志记录。也可以自定义格式,这需要扩展Formatter类并覆盖下面这个方法:
String format(LogRecord record)
可以根据自己的需要以任何方式对记录中的信息进行格式化,并返回结果字符串。在format方法中,可能会调用下面这个方法:
String formatMessage(LogRecord record)
日志技巧
1.对一个简单的应用,选择一个日志记录器。可以把日志记录器命名为与主应用包一样的名字,例如:com.mycompany.myprog,这是一个好主意。总是可以通过调用以下方法得到日志记录器。
Logger logger = Logger.getLogger("com.mycompany.myprog");
2.默认的日志配置会把级别等于或高于INFO的所有消息记录到控制台。用户可以覆盖这个默认配置。
3.现在,可以记录自己想要的内容了。但需要牢记:所有级别INFO、WARNING和SEVERE的消息都将显示到控制台上。
想要调用System.out.println时,可以换成发出以下的日志消息:
logger.fine("File open dialog canceled");
调试技巧
1.可以用下面的方法打印或记录任意变量的值:
System.out.println("x="+x);
或
Logger.getGlobal().info("x=" +x);
2.还有一个不太为人所知但非常有效的技巧,可以在每一个类中放置一个单独的main方法。这样就可以提供一个单元测试桩(stub),能够独立地测试类。