异常

一.处理异常:

注意:

●若由于错误而使程序没有正常完成,程序应该:
(1)返回到一种安全状态,并能够让用户执行一些其他的命令。或者:
(2)允许用户保存所有操作的结果,并以适当的方式终止程序。

●异常处理的任务:将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。

●问题分类:
(1)用户输入错误:除了那些不可避免的打字录入外,有些用户喜欢用自己的方式,不遵守程序的要求。
(2)设备错误:硬件出故障,浏览器,打印机等问题。
(3)物理限制:磁盘满了,可用的存储空间被用完了。
(4)代码错误:程序方法有可能无法正常的执行。

●异常具有自己的语法和特定的继承结构。

1.异常分类:

(1)在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例。

(2)如果Java内置的异常类,不能够满足需求,用户可以创建自己的异常类。

(3)Exception分为RuntimeException即运行时异常,另一个是编译时异常。

(4)常见RuntimeException即运行时异常:
1)错误的类型转换。
2)数组访问越界。
3)访问空指针。

(5)常见编译时异常:
1)试图在文件尾部后面读取数据。
2)试图打开一个错误格式的URL。
3)试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。

(6)异常:
●未检查异常:Javaz语言规范将派生于Error类或RuntimeException类的所有异常。
●已检查异常:所有其他的异常。

2.声明已检查异常:

(1)应该抛出异常的情况:

●调用一个抛出已检查异常的方法。例如:FileInputStream构造器。
●程序运行过程中发现错误,并且利用throw语句抛出一个已检查异常。
●程序出现错误,例如:a[-2]=0会抛出一个ArrayIndexOutOfBoundsException这样的一个未检查异常。
●Java虚拟机和运行时库出现的内部异常。

(2)对于那些可能被别人使用的Java方法,应该根据异常规范,在方法的首部声明这个方法可能抛出的异常。

(3)如果一个人方法有可能抛出多个已检查异常,就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。

(4)除了声明异常,还可以捕获异常,这样可以使异常不被抛到方法之外,也不需要throws规范。

(5)如果在子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能超过超类方法中声明的异常范围。

(6)如果类中的一个方法声明将会抛出一个异常,而这个异常是某个特定类的实例时,则这个方法就有可能抛出一个这个类的异常,或者这个类的任意一个子类的异常。

3.如何抛出异常:

(1)对于一个已经存在的异常类,如何将其抛出:
●找到一个合适的异常类。
●创建这个类的一个对象。
●将对象抛出。
一旦方法跑出了异常,该方法就不可能返回到调用者,也就是说,不必为返回的默认值或错误代码担忧。

(2)在Java中,只能抛出Throwable子类的对象。

4.创建异常类:

相关方法:
Throwable():构造一个新的Throwable()对象,这个对象没有详细的描述信息。
Throwable(String message):构造一个新的throwable对象,这个对象带有特定的描述信息。习惯上,所有派生的异常类都支持一个默认的构造器和一个带有详细描述信息的构造器。
String getMessage():获得Throwable()对象的详细描述信息。

二.捕获异常

注意:

●如果某个异常发生的时候,没有在任何地方进行捕获,则程序就会终止执行,并在控制台上打印出异常信息,包括异常的类型和堆栈的内容。

●如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么:
(1)程序将跳过try语句块的其余代码。
(2)程序将执行catch子句中的处理器代码。
如果try语句块中的代码没有抛出任何异常,程序将跳过catch子句。
如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立刻退出。

●如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个已检查异常,不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围。

1.捕获多个异常:

try
{
    code that might throw exceptions
}
catch(MalformedURLException e1)
{
    emergency action for malformed URLs
}
catch(UnknownHostException e2)
{
    emergency action for unknown hosts  
}
catch(IOException e3)
{
    emergency action for all other I/O problems
}

异常对象(e1,e2,e3)可能包含与异常本身有关的信息。要想获得对象的更多信息,可以试着使用 e3.getMessage() 得到详细的错误信息,或者使用 e3.getClass().getName()得到异常对象的实际类型。

2.再次抛出异常与异常链:

(1)捕获异常并再次抛出的基本方法:

try
{
    access the database
}
catch(SQLException e)
{
        throw new ServletException("database error:" + e.getMessage());
}

当捕获到异常时,就可以使用下面这条语句重新得到原始异常:

Throwable e = throwable.getCause();

调用getCause方法可以得到异常链。

3.Finally子句:

(1)对于资源回收问题,使用finally子句。

(2)不管是否有异常被捕获,finally子句中的代码块都会被执行。

(3)例举:

Graphics g = image.getGraphics();
try
{
    //1
    code that might throw exceptions
    //2
} 
catch(IOException e)
{
    //3
    show error dialog
    //4
}
finally
{
    //5
    g.dispose();
}
//6

在上面的这段代码中,有三种情况会执行finally块子句:

●代码没有抛出异常。在这种情况下,程序首先执行try语句块中的全部代码,然后执行finally子句中的代码,随后继续执行try语句块之后的第一条语句。也就是说执行标注的1、2、5、6处。
●抛出一个在catch子句中捕获的异常。也就是上例中的IOException。这种情况下,程序将执行try语句块中的所有代码,直到发生异常为止。因此,将跳过try语句块中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally子句中的代码。
如果catch子句没有抛出异常,程序将执行try语句块之后的第一条语句,在这里,执行标注1、3、4、5、6处的语句。
如果catch子句抛出了一个异常,异常将被抛回这个方法的调用者,在这里,执行1、3、5处的语句。
●代码抛出了一个异常,但这个异常不是由catch子句捕获的。在这种情况下,程序将执行try语句块中的所有语句,直到有异常被抛出为止。此时,将跳过try语句块中的剩余代码,然后执行finally子句中的语句,并将异常抛给这个方法的调用者。在这里,执行标注1、5处的语句。

(4)try预计可以只有finally子句,而没有catch子句。

(5)无论在try语句块中是否有异常发生,finally子句中的close()语句都会被执行。

4.分析堆栈跟踪元素:

(1)堆栈跟踪(Stack Trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。当Java程序正常终止而没有捕获异常时,这个列表就会显示出来。

(2)可以通过调用 getStackTrace 方法获得一个 StackTraceElement 对象的数组,并在程序中对它进行分析。如:

Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame: frames)
    analyze frame

StackTraceElement类含有能够获得文件名和当前执行的代码行号的方法,同时还含有能够获得类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获得的信息。

(3)静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈跟踪。如何使用:

Map<Thread, StackTraceElement[]> map = Thread.getAllStackTrace();
for(Thread t: map.keySet())
    StackTraceElement[] frames = map.get(t);
    analyze frames

5.使用异常机制的建议:
●异常处理不能代替简单的测试。
●不要过分地细化异常。
●利用异常层次结果。
①不要只抛出RuntimeException异常,应该寻找更加适当的子类或创建自己的异常类。
②不要只捕获Throwable异常,否则,程序代码会更难度,更难维护。
③将一种异常转换成另一种更加适合的异常时不要犹豫。
●不要压制异常。
●在检测错误时,“苛刻”要比放任要好。
●不要羞于传递异常。

三.断言:

注意:

●在一个具有自我保护能力的程序中,断言是一个常用的习语。假设确信某个属性符合要求,并且代码的执行依赖于这个属性。

●断言机制允许在测试期间向代码中插入一些检查语句。当代码发布时,这些插入的检测语句将会被自动地移走。

●Java语言引入关键字assert,该关键字有两种形式:
①assert 条件;
②assert 条件:表达式;
这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。

●“表达式”部分唯一的目的是产生一个消息字符串。AssertionError对象并不存储表达式的值,因此,不可能在以后得到它。

1.启用和禁用断言:

(1)在默认情况下,断言被禁用。

(2)可以在运行程序时用可以用-enableassertions或-ea选项启用它。
java -enableassertions MyApp

(3)在启用或禁用断言时不必重新编译程序。启用和禁用断言是类加载器的功能。当断言被禁用时,类加载器将跳过断言代码,因此,不会降低程序运行的速度。

(4)也可以在某个类或某个包中使用断言。
如:java -ea:MyClass -ea:com.company.mylib... MyApp
这条命令将开启MyClass类以及在com.company.mylib包和它的子包中的所有类的断言。

(5)选项 -ea 将开启默认包中的所有类的断言。

(6)也可以用选项 -disableassertions 或 -da 禁用某个特定类或包的断言。
java -ea:... -da:MyClass MyApp

(7)有些类不是由加载器加载,而是直接由虚拟机加载。可以使用这些开关有选择的启用或禁用那些类中的断言。

(8)启用和禁用所有断言的 -ea 和 -da 不能应用到那些没有类加载器的“系统类”上。对于这些系统类来说,需要使用 -enablesystemassertions / -esa 开关启用断言。

(9)在程序中也可以控制类加载器的断言状态。

2.使用断言的建议:

(1)三种处理系统错误的机制:
●抛出一个异常
●日志
●使用断言

(2)什么时候使用断言:
●断言失败是致命的、不可恢复的错误。
●断言检查只用于开发和测试阶段。

(3)断言只应用于在测试阶段确定程序内部的错误位置。

3.为文档使用断言:
void setDefaultAssertionStatus(boolean b):对于通过类加载器加载的所有类来说,如果没有显式的说明类或包的断言状态,就启用或禁用断言。
void setClassAssertionStatus(String className, boolean b):对于给定的类和它的内部类,启用或禁用断言。
void setPackageAssertionStatus(String packageName, boolean b):对于给定包和其子包中的所有类,启用或禁用断言。
void clearAssertionStatus():移去所有类和包的显式断言状态设置,并禁用所有通过这个类加载器加载的类的断言。

四.记录日志:

1.基本日志

(1)记录日志API的优点:

●可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
●可以很简单地禁止日志记录的输出,因此,将这些日志代码留在程序中所付出的代价很小。
●日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等等。
●日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器指定的标准丢弃那些无用的记录项。
●日志记录可以采用不同的方式格式化。
●应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字。例如:com.mycompany.myapp
●在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置。

(2)日志系统管理着一个名为Logger.global的默认日志记录器,可以用System.out替换它,并通过调用info方法记录日志信息:Logger.global.info("File>Open menu item selected");在默认情况下,这条记录将会显示出如下所示的内容:
May 10, 2004 10:12:15 PM LoggingImageViewer fileOpen
INFO: File>Open menu item selected

注意:自动包含了时间、调用的类名和方法名。但是,如果在恰当的地方(例如,main开始)调用Logger.global.setLevel(Levle.OFF); 将会取消所有的日志。

2.高级日志

(1)在一个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义日志记录器。

(2)当第一次请求一个具有给定名字的日志记录器时,就会创建这个记录器。

Logger myLogger = Logger.getLogger("com.mycompany.myapp");

如果后面使用同一个名字调用日志记录器就会产生相同的日志记录器。

(3)与包名类似,日志记录器名也具有层次结构。与包名相比,日志记录器的层次性更强。对于包来说,一个包的名字与其父包的名字之间没有语义关系,但是日志记录器的父与子之间将共享某些属性。

(4)通常有以下7个日志记录器级别:
●SEVERE
●WARNING
●INFO
●CONFIG
●FINE
●FINER
●FINEST

(5)在默认情况下,只记录前三个级别。也可以设置其他的级别。例如: logger.setLevel(Level.FINE); 现在,FINE和更高级别的记录都可以记录下来。另外,还可以使用Level.ALL开启所有级别的记录,或者使用Level.OFF关闭所有级别的记录。

(6)对于所有的级别有下面几种记录方法:

logger.warning(message);
logger.fine(message);

同时还可以使用log方法指定级别,例如:

logger.log(Level.FINE, message);

(7)如果将记录级别设计为INFO或者更低,则需要修改日志处理器的配置。默认的日志处理器不会处理低于INFO级别的信息。

(8)默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。但是,如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。此时,可以调用logp方法获得调用类和方法的确切位置,这个方法的签名为:

void logp(Level 1, String className, String methodName, String message);

(9)跟踪执行流程的方法:
●void entering(String className, String methodName)
●void entering(String className, String methodName, Object param)
●void entering(String className, String methodName, Object[] params)
●void exiting(String className, String methodName)
●void exiting(String className, String methodName, Object result)

(10)记录日志的常见用途是记录那些不可预料的异常。下面两个方法提供日志记录中包含的异常描述内容:
●典型的用法是:

if(...) {
    IOException exception = new IOException("...");
    logger.throwing("com.mycompany.mylib.Reader", "reader", exception);
    throw exception;
}

try{
    ...
}catch(IOException e) {
    Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e);
}

调用throwing可以记录一条FINE级别的记录和一条以THROW开始的信息。

3.修改日志管理器配置

(1)可以通过编辑配置文件来修改日志系统的各种属性。在默认情况下,配置文件存在于:jre/lib/logging.properties,要想使用另一个配置文件,就要将 java.util.logging.config.file 特性设置为配置文件的存储位置,并用下列命令启动应用程序:

java -Djava.util.logging.config.file=configFile MainClass

(2)在main方法中调用System.setProperty(“java.util.logging.config.file”, file) 没有任何影响,其原因是日志管理器在VM启动过程中被初始化,它会在main之前执行。

(3)要想修改默认的日志记录级别,就需要编辑配置文件,并修改以下命令行:.level=INFO,可以通过添加以下内容来指定自己的记录级别com.mycompany.myapp.level=FINE,也就是,在日志记录后面添加后缀.level。

(4)日志记录并不将消息发送到控制台上,这是处理器的任务。另外,处理器也有级别。要想在控制台上看到FINE级别的消息,就需要进行下列设置:java.util.logging.ConsoleHandler.level=FINE

(5)在日志管理器配置的属性设置不是系统属性,因此,用-Dcom.mycompany.myapp.level=FINE启动应用程序不会对日志记录产生任何影响。

(6)日志属性文件由 java.util.logging.LogManager 类处理。可以通过将 java.util.logging.manager 系统属性设置为某个子类的名字来指定一个不同的日志管理器。另外,在保存标准日志管理器的同时,还可以从日志属性文件跳过初始化。还有另一种方式是将 java.util.logging.config.class 系统属性设置为某个类名,该类再通过其它方式设定日志管理器属性。

(7)在运行过程中,使用jconsole程序也可以改变日志记录的级别。

4.本地化

(1)本地化的应用程序包含资源包(resource bundle)中的本地说明信息。资源包由各个地区的映射集合组成。

(2)一个程序可以包含多个资源包,一个用于菜单,其他用于日志消息。每个资源包都有一个名字。

(3)要想将映射添加到一个资源包中,需要为每个地区创建一个文件。英文消息映射位于 com/mycompany/logmessages_en.properties 文件中,德文消息映射位于 com/mycompany/logmessages_de.properties 文件中。可以将这些文件与应用程序的类文件放在一起,以便 ResourceBundle 类自动地对它们进行定位。这些文件都是纯文本文件。其组成项如下所示:

readingFile=Achtung! Datei wird eingelesen
renamingFile=Datei wird umbenannt
...

(4)在请求日志记录器时,可以指定一个资源包:

Logger logger = Logger.getLogger(loggerName, "com.mycompany.logmessages");

然后为日志消息指定资源包的关键字,而不是实际的日志消息字符串。logger.info("readerFile");通常需要在本地化的消息中增加一些参数,因此,消息应该包括占位符{0}、{1}等等。

5.处理器

(1)在默认情况下,日志记录器将记录发送到 ConsoleHandler 中,并由它输出到 System.err 流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器(命名为“”)有一个ConsoleHandler。

(2)与日志记录器一样,处理器也有日志记录级别。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处理器的日志记录级别为java.util.logging.ConsoleHandler.level=INFO

(3)要想记录FINE级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。另外,还可以绕过配置文件,安装自己的处理器。
●Logger logger = Logger.getLogger(“com.mycompany.myapp”);
●logger.setLevel(Level.FINE);
●logger.setUseParentHandlers(false);
●Handler handler = new ConsoleHandler();
●handler.setLevel(Level.FINE);
●logger.addHandler(handler);

(4)要想将日志记录发送到其他地方,就要添加其他的处理器。日志API为此提供了两个很有用的处理器,一个是 FileHandler ,另一个是 SocketHandler 。SocketHandler 可以将记录发送到特定的主机和端口。FileHandler 可以收集文件中的记录,可以将记录直接发送到默认文件的处理器中:

FileHandler handler = new FileHandler();
logger.addHandler(handler);

这些记录被发送到用户主目录的 javan.log 文件中,n是文件名的唯一编号。如果用户系统设有主目录,文件就存储在这样的默认目录下。在默认情况下,记录被转化为XML。

6.过滤器

(1)在默认情况下,过滤器根据日志记录的级别进行过滤。每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的过滤。另外,可以通过实现Filter接口并定义下列方法来自定义过滤器。

(2)要想将一个过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法就可以了。同一时刻,最多只能有一个过滤器。

7.格式化器

(1)ConsoleHandler类和SocketHandler类可以生成文本和XML个数的日志记录。但是,也可以自定义格式。这需要扩展Formatter并覆盖下面这个方法:String format(logRecord record)

(2)可以根据自己的意愿对记录中的信息进行格式化,并返回结果字符串。在format方法中,有可能会调用下面这个方法:
String formatMessage(logRecord record)
这个方法对记录中的部分消息进行格式化、参数替换和本地化应用操作。

(3)很多文件格式(例如XML)需要在已格式化的记录的前后加上一个头部和尾部,在这个例子中,要覆盖下面两个方法:
String getHead(Headler h)
String getTail(Headler h)

最后,调用setFormatter方法将格式化器安装到处理器中。

8.日志记录说明

(1)日志记录最常用操作:

●为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字,例如com.mycompany.myprog,另外,可以通过下列方法得到日志记录器
Logger logger=Logger.getLogger("com.mycompany.myprog");
为了方便起见,可能希望利用一些日志操作将下面的静态域添加到类中:

private static final Logger logger=Logger.getLogger("com.mycompany.prog");

●默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台,用户可以覆盖默认的配置文件,而改变配置很麻烦,所以,最好在应用程序中安装一个更加适宜的默认配置。

●所有级别为INFO、WARNING和SEVERE的消息都将显示到控制台上。因此,最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为FINE是一个很好的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值