[Java Training]Week 6
0x00
本系列-Java集训系列
变成了自己学习一遍Java- -。
0x01
第七章 异常、断言和日志
0x02
异常
程序总会产生错误,如果是在开发阶段,那么还可以看到错误去修复,然后重新启动,但是在程序的运行阶段,就不可以这样,所以当错误产生的时候,至少做到以下几点:
- 向用户通告错误
- 保存所有的工作结果
- 允许用户以妥善的形式退出程序
将程序可能产生的错误做一个归类
- 用户输入错误:用户不可能总是按照软件开发者的预期进行输入,会经常产生各种奇怪的输入,程序如果没有进行检查,那么就会产生错误
- 设备错误:硬件可能在你发出指令之后不会正常执行,比如没有接上打印机,比如打印机没纸了
- 物理限制:磁盘满了
- 代码错误:代码无法正确执行,比如调用了错误的方法,方法返回的是null,然后对null进行了方法调用等
不论是系统还是语言,都会有一个异常处理机制,以便程序返回一种安全状态,并能够让用户执行一些其他的命令;或者允许用户保存所有操作的结果,并以妥善的方式终止程序
异常分类
- 分类如下图
所有的异常都是由Throwable继承而来,下一层分解为Error和Exception
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误
Exception类下属两个分支,RuntimeException是由程序错误导致的异常;IOException是程序本身没有问题,但由于像I/O错误等这类问题导致的异常属于其他异常
重点关注的是Exception,而Error应该尽量避免,也无法处理Error的错误
Exception中,派生于RuntimeException的异常包括以下几种情况:
- 错误的类型转换
- 数组访问越界
- 访问null指针
不是派生于RuntimeException的异常包括:
- 试图在文件尾部后面读取数据
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在
派生于Error类或RuntimeException类的所有异常成为非受查异常,IOException在内的所有其他异常成为受查异常,其中受查异常是需要处理的,即抛出、捕获等
想必看完上面已经看懵了,这里我用我自己粗浅的理解解释一下,简单说呢,非受查异常(Error和RuntimeException)是外界原因或者代码原因,Error不可控,没办法检查;RuntimeException是可控,最好的办法就是通过代码限制来规避,不需要检查。所以剩下的就是受查异常(IOException)
声明受查异常。
方法应该在其首部生命所有可能抛出的异常。下面是一个例子
public FileInputStream(String name) throws FileNotFoundException
自己编写方法时,不必将所有可能抛出的异常都进行声明。以下四种情况需要抛出异常:
调用一个抛出受查异常的方法,例如,FileInputStream构造器。
程序运行过程中发现错误,并且利用throw语句抛出一个受查异常。
程序出现错误,例如,a[-1]=0会抛出一个ArrayIndexOutOfBoundsException这样的非受查异常。
Java虚拟机和运行时库出现的内部错误。
如果出现前两种情况,则必须告诉调用这个方法的程序员有可能抛出异常。
对于那些可能被他人使用的Java方法,应该根据异常规范,在方法的首部声明这个方法可能抛出的异常
1
2
3
4
5
6
7
8class MyAnimation
{
//....
public Image loadIeg(String s) throws IOException
{
//....
}
}如果抛出多个异常,那么就用逗号分隔
1
2
3
4
5
6
7
8class MyAnimation
{
//....
public Image loadImage(String s) throws FileNotFoundException,EOFException
{
//....
}
}声明的应该都是受查异常,非受查异常不应该声明。所以,也不应该声明从RuntimeException类派生的异常,而是应该在代码上阻止产生RuntimeException异常。
也可以在逻辑有错误的时候主动抛出异常,抛出方法
1
throw new EOFException();
例:
1
2
3
4
5
6
7
8
9
10
11
12String readData(Scanner in) throws EOFException
{
while(True)
{
if (!in.hasNext()) // EOF encountered
{
if (n < len)
throw new EOFException();
}
}
retrn s;
}根据上一点,发现抛出已存在的异常很轻松:
- 找到一个合适的异常类
- 创建这个类的一个对象
- 将对象抛出
创建自己的异常类:
1
2
3
4
5
6
7
8class FileFormatException extends IOException
{
public FileFormatException() {}
public FileFormatException(String gripe)
{
super(gride);
}
}很多时候需要抛出一些自定义异常,来准确的描述发生的情况
首先是一定要继承一个Exception类,然后要有默认构造方法和一个带详细描述参数的构造方法,其他方法视情况添加
抛出自定义异常的方法并没有区别
1
2
3
4
5
6
7
8
9
10
11
12
13String readData(BufferedReader in) thros FileFormatException
{
while(true)
{
if (ch == -1) // EOF encountered
{
if (n < len)
{
throw new FileFormatException();
}
}
}
}抛出异常只是把异常的处理交给调用者,并没有真正的处理。如果不进行捕获(处理),程序就会终止运行。捕获异常之后,就可以在代码块中处理异常。
捕获异常的代码:
1
2
3
4
5
6
7
8
9
10try
{
// code
// 需要捕获的异常的代码
}
catch(ExceptionType e) //
{
// code
// 如何处理捕获到的异常
}会抛出异常的代码在 try 代码块中,当try中的代码触发异常,并且抛出的异常的类型在 catch 中有,则直接进入 catch 中对应的代码块进行执行。
如果没有触发异常,则跳过 catch 中的代码。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public void read(String filename)
{
try
{
InputStream in = new FileInputStream(filename);
int b;
while ((b = in.read()) ! = -1)
{
// process input
}
}
catch (IOException exception)
{
exception.printStackTrace();
}
}可以在一个try中捕获多种异常,catch的方式类似于ifelse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16try
{
// 受查异常的代码
}
catch (FileNotFoundException e)
{
// 处理代码
}
catch (UnknownHostException e)
{
// 处理代码
}
catch (IOException e)
{
// 处理代码
}获取异常的信息
e.getMessage()
或者更详细的
e.getClass().getName()
也可以在同一个catch中匹配多个不同的异常类型
1
2
3
4
5
6
7
8
9
10
11
12try
{
// code
}
catch (FileNotFoundException | UnknownHostException e)
{
// code
}
catch (IOException e)
{
// code
}在异常中抛出另一种异常,即改变异常类型,可以在catch中重新抛出另一个异常
1
2
3
4
5
6
7
8
9
10try
{
// code
}
catch (SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}这样既不丢失原始信息,还可以随意定义异常,想看原始信息通过以下方法
Throwable e = se.getCause();
保证不论是否触发异常都会执行的代码块,finally,
1
2
3
4
5
6
7
8
9
10
11
12
13InputStream in = new FileInputStream();
try
{
// code
}
catch (IOException e)
{
// code
}
finally
{
in.close();
}上面这段代码中,不论怎么样都会执行finally中的 in.close()
如果想要只是关闭资源链接,那么可以用带资源的try语句
1
2
3
4
5
6try(Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8");
PrintWriter out = new PrintWriter("out.txt"))
{
while(in.hasNext())
out.println(in.next().toUpperCase());
}如上代码不论怎么样都会关闭in和out,如果需要关闭资源,那么尽量用这种方式
当产生异常的时候,需要分析产生原因,这时候可以使用分析堆栈的方法
可以调用Throwable类的printStackTrace方法
1
2
3
4Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
还有更灵活的 使用getStackTrace方法,会得到StackTraceElement对象的数组
```java
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)
// code
1 |
|
使用异常的一些技巧
- 异常处理不能代替简单的测试
- 不要过分的细化异常
- 利用异常层次结构
- 不要压制异常
- 在检测错误时,苛刻要比放任更好
断言
断言的关键字时assert
断言是对一个条件进行判断,如果不通过(即为false),则抛出一个AssertionError异常
断言有如下两种形式
assert 条件;
或
assert 条件 : 表达式;
断言是被用来在开发的时候进行测试的,默认情况下是不启用的,放在代码里也不会对速度产生影响,在运行程序时用 -enableassertions 或 -ea 选项启用断言
java -enableassertions MyApp
或
java -ea MyApp
也可以在某个类或整个包中使用断言
java -ea:MyClass -ea:com.mycompany.mylib… MyApp
启用断言不需要重新编译程序
关于断言需要注意的点
- 断言失败是致命的、不可恢复的错误
- 断言检查只用于开发和测试阶段
断言使用示例
1
2// 当i小于0的时候,就会报错并且退出程序
assert i >= 0;
日志
日志的意义:初学者会发现调试神器 System.out.println() ,但是每次都要在调试地点插入这句话,然后在调试完成之后再注释掉或者删除,这样是十分不优雅的,日志就是一个十分优雅的解决方案
- 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易
- 可以很简单的禁止日志记录的输出,因此,将这些日志代码留在程序中的开销很小
- 日志记录可以被定向到不同的处理器,用于再控制台中显示,用于存储在文件中等
- 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项
- 日志记录可以采用不同的方式格式化,例如,纯文本或XML
- 应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,例如,com.mycompany.myapp
- 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置
最简单的日志使用方法
使用全局日志记录器(global logger)并调用其info方法:
1
Logger.getGlobal().info("File->Open menu item selected");
1
2
3
4
5
6
7
8
9
10
11
在默认情况下,这条记录将会显示以下内容
> May 10, 2013 10:12:15 PM LoggingImageViewer fileOpen
>
> INFO: File->Open menu item selected
但是,如果在适当的地方(如main开始)调用
```java
Logger.getGlobal().setLevel(Level.OFF);会取消所有的日志
高级使用日志方法
可以自定义日志记录器,而不是所有都用全局日志记录器
1
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");
与包名类似,日志记录器名字也有层次结构,而且日志记录器还可以继承设置,比如com.mycompany 设置的级别是FINE,那么子包都会默认级别是FINE,如 com.mmycompany.abc
通常有以下日志记录器级别:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
默认情况下只记录前三个级别,也可以手动设置级别:
1
2// 在日志记录之前加上这行代码,就成功将日志级别改成FINE
logger.setLevel(Level.FINE);这样在日志中就记录FINE以上的级别的日志
记录不同级别的日志
1
2
3logger.warning(message);
logger.fine(message);
logger.log(Level.FINE, message);跟踪调用的方法
1
2
3
4
5
6
7
8// 获得调用类和方法的确切位置的方法
void logp(Level l, String className, String methodName, String message)
//跟踪执行流的方法
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)例子:
1
2
3
4
5
6
7int read(String file, String pattern)
{
logger.entering("com.mycompany.mylib.Reader", "read", new Object[] {file, pattern});
// code
logger.exiting("com.mycompany.mylib.Reader", "read", count);
return count;
}在代码中进行配置不容易更改,也不容易复用。更好的解决方案是使用配置文件。
默认情况下,配置文件存在于 jre/lib/logging.properties
使用指定日志配置文件启动程序
java -Djava.util.logging.config.file=
MainClass 也可以在main中调用
System.setProperty(“java.util.logging.config.file”, file)
在配置文件中配置显示级别
1
.level=INFO
指定的包的级别
1
com.mycompany.myapp.level=FINE
在控制台上显示日志
1
java.util.logging.ConsoleHandler.level=FINE
日志本地化,即根据不同地区显示不同的语言,实现原理是单词映射
首先是资源包,需要为每个地区创建一个资源包,然后放到固定位置,比如
com/mycompany/logmessages_en.properties
com/mycompany/logmessages_de.properties
com/mycompany/logmessages_zh.properties
文件里的内容是
1
2readingFile = Achtung! Datei wird eingelesen
renamingFile = Datei wird umbenannt然后记录日志的时候指定资源包
1
Logger logger = Logger.getLogger(loggerName, "com.mycompany.logmessages");
在记录的日志中使用关键词就可以本地化啊(非关键词不会本地化):
1
logger.info("readingFile");
在配置文件中使用占位符
1
2Reading file {0}.
Achtung! Datei {0} wird eingelesen然后使用的时候这样用
1
2logger.log(Level.INFO, "readingFile", fileName);
logger.log(Level.INFO, "renamingFile", new Object[] { oldName, newName});日志处理器,即日志的处理程序,日志记录器将日志记录下来之后会发送到日志处理器中,一般以xxxHandler命名,如自带的:ConsoleHandler、FileHandler、SocketHandler。分别表示 控制台处理器、文件处理器、Socket处理器。
日志记录器默认会将记录发送到ConsoleHandler中,而且日志记录器还会将记录发送到父处理器中。
处理器也有日志记录级别。所以日志的实际记录级别是高于日志记录器和处理器的阀值。处理器的默认记录级别也是INFO
java.util.logging.ConsoleHandler.level=INFO
还可以自定义日志记录器,然后使用
1
2
3
4
5
6Logger logger = Logger.getLogger("com.mycompany.myapp");
logger.setLevel(level.FINE);
logger.setUseParentHandlers(false); // 设置:不发送到父日志处理器中
Handler handler = new ConsoleHandler(); // 自定义一个处理器
handler.setLevel(Level.FINE); // 设置自定义处理器的级别
logger.addHandler(handler); // 将自定义的处理器添加到日志中,使用其中,FileHandler是将日志发送到文件中的处理器,默认是发送到用户主目录的javan.log的文件中,以xml方式保存。
可以通过更改FileHandler的设置,来重点记录想关注的。
过滤器,即对日志进行过滤,来对重点内容进行关注。
通过实现Filter接口,并实现下列方法:
1
boolean isLoggable(LogRecord record)
实现方法:接收的参数是日志的一行记录,在方法中对记录进行操作,比如进行判断是否包含err,返回Boolean值,如果为true就是记录下来的,返回为false就是过滤掉的。
将过滤器安装到记录器或者处理器中,调用setFilter即可,同一时刻只能有一个过滤器。
格式化器,对日志记录的格式进行设置的处理程序。
日志处理器的默认格式可能会缺少一些关键信息,可以通过自定义格式化器来覆盖默认的配置。
扩展Formatter类并覆盖下面这个方法:
1
String format(LogRecord record)
实现方法:接受的参数是日志的一行记录,在方法中对记录进行格式化,然后return格式化之后的日志。
格式化可能会用到这个方法:
1
String formatMessage(LogRecord record)
加头和加尾:
1
2String getHead(Handler h)
String getTail(Handler h)最后,调用setFormatter方法将格式化器安装到处理器中。
日志记录总结:
- 程序尽可能的都要使用日志,在使用日志记录器的时候,保持日志记录器命名为主应用程序包名。
- 要为日志提供一个配置,推荐使用配置文件。
- 日志记录的级别要谨慎使用,如果记录太多可能导致日志中存在大量无用信息。
调试技巧:
在可能出问题的附近使用打印变量,查看是否符合预期
1
System.out.println("x=" + x);
每一个类中都放一个main,即单元测试,通过调用来测试方法的输入和输出是否符合预期
1
2
3
4
5
6
7
8
9public class MyClass
{
// code
public static void main(String[] args)
{
// test code
}
}单元测试有单元测试框架,可以省去每个方法创建main手动写代码以及擦除的过程
日志代理(logging proxy)是一个子类的对象,可以截获方法调用,并进行日志记录,然后调用超类中的方法
1
2
3
4
5
6
7
8Random generator = new Random(){
public double nextDouble()
{
double result = super.nextDouble();
Logger.getGlobal().info("nextDouble: "+ result);
return result;
}
}Throwable类中的printStackTrace方法,可以获取调用的堆栈
1
2
3
4
5
6try{
//
}catch (Throwable t){
t.printStackTrace();
throw t;
}也可以不捕获异常直接调用获取堆栈信息
1
Thread.dumpStack();
堆栈轨迹默认显示在System.err上,也可以利用printStackTrace(PrintWriter s)方法将堆栈信息发送到文件中。也可以采用以下方法将堆栈信息放到字符串中:
1
2
3StringWriter out = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(out));
String description = out.toString();将错误信息保存到文件中的方法
java MyProram 2> errors.txt
同时显示在System.err和System.out中(即同时显示在控制台和保存在文件中):
java MyProgram 1> errors.txt 2> &1
将异常的堆栈轨迹只记录在文件中,不显示在控制台
1
2
3
4
5
6
7
8Thread.setDefaultUncaughtexceptionHandler(
new Thread.UncaughtExceptionHandler(){
public void uncaughtException(Thread t, Throwable e)
{
// 保存到文件中的信息
}
}
)在启动程序的时候使用 -verbose,可以观察到类的加载过程
-Xlint选项会对代码进行检查。(主要是语法问题)
javac -Xlint:fallthrough
jconsole的图形工具是对Java应用程序进行监控和管理的,可以看到内存消耗、线程使用、类加载等情况
jconsole processID
jmap是一个获得堆的转储工具,可以看到堆的信息
jmap -dump:format=b,file=dumpFileName processID
jhat dumpFileName
然后打开浏览器 localhost:700,就能看到堆的内容
-Xprof是一个剖析器,跟踪方法调用次数。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!