异常分类
-
Error:Java运行时系统的内部错误以及资源耗尽的错误。程序中基本不用处理,如果出现了该类错误,告知用户并安全退出即可。
-
RuntimeException:程序编写中的错误,不需要try…catch捕获,修改程序即可避免的错误。也叫uncheckedException,所谓的unchecked是指编译过程中,编译器无法检测出来的错误。比如:数组越界、访问空指针。
-
IOException:类中抛出的、需要try…catch捕获的大多是这类异常。对应的也成为checkedException,编译器会强制检查是否处理的异常。
抛出异常
-
声明受查异常:在方法签名后,用关键字throws接异常类名。
-
抛出异常:在方法代码中,用关键字throw接异常对象。
-
自定义异常类:创建一个继承自Exception或Exception子类的自定义类,规范化的包含两个构造器,一个无参无内容构造器,一个含参数调用super(String)构造器。
public class Main{
public class myException extends Exception{
public myException(){}
public myException(Integer i, String s){
super("文件名长" + String.valueOf(i) + " 文件名为" + s + " WEONG!");
}
}
public void myPrint(String filename) throws myException {
if (filename.length() > 5){
throw new myException(filename.length(), filename);
}
//...
return;
}
public static void main(String[] args){
Main m = new Main();
try {
m.myPrint("abcdef");
} catch (myException e) {
System.out.println(e.getMessage());
//e.printStackTrace();
}
}
}
捕获异常
//try...catch
public static void main(String[] args) {
File f = new File("a.txt");
try {
FileInputStream fin = new FileInputStream(f);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
//IDEA整体缩进快捷键:Shift + Tab
两种执行结果:
try块中没有异常,跳过catch块;
try块中遇到异常,跳过剩余try块语句,执行catch块。
应用程序中,不可避免的会用到文件操作IO流或者是数据库连接,诸如此类开销比较大的资源。这个时候就需要在使用完后,手动进行资源的及时销毁,避免内存泄露。try…catch有三种写法可以完成上述功能。
//try...catch
public static void main(String[] args) {
File f = new File("a.txt");
try {
FileInputStream fin = new FileInputStream(f);
fin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
}
但如果在对象创建之后,遇到了异常跳入某个catch块中,这样close的代码就执行不到。由此引出了finally块,即无论try…catch如何执行,最后一定会执行finally块。
//try...catch...finally
public static void main(String[] args) {
File f = new File("a.txt");
FileInputStream fin = null;
try {
fin = new FileInputStream(f);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
试想如果变量在创建的时候就报异常,导致变量没有创建仍是空值,就会在finally块中报空值引用错误。而且这种写法,在finally块中遇到异常时,还需要再使用一次try…catch,略显臃肿。
//try-with-resources
public static void main(String[] args) {
File f = new File("a.txt");
try(FileInputStream fin =new FileInputStream(f)) {
...
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
}
}
try-with-resources写法的原理,会在try块退出的时候执行一次资源关闭。实测不会出现空指针情况,而且写法简洁,最为推荐。
-
异常链:在实际的开发背景中,还会在catch块中再次抛出异常。用来改变异常类型,或者使用自定的详述的异常类型。也可以用Java提供的initCause(e)和getCause(),来构建一个因果的异常链。
-
堆栈轨迹元素:常用的e.printStackTrace()方法打印方法调用过程,也可以使用e.getStackTrace()返回一个StackTraceElement[]数组,用来逐个分析异常文件名和代码行数。
断言
断言是在开发测试阶段使用,用来确定每一部分的前置条件是否满足,并以此来确定程序错误出现的区间,以便快速定位BUG。相较于异常的优势在于,可以在运行的时候选择是否执行断言,并且不用重新编译。
public static void main(String[] args) {
File f = null;
assert f != null:f;
System.out.println(f.getName());
}
//定位到错误在File对象没有初始化
assert有两种形式:
assert 条件;
assert 条件 : 表达式;
//含有的表达式会在输出中显示值,核心就是报一个AssertionError
Exception in thread "main" java.lang.AssertionError: null
at Main.main(Main.java:23)
- 启用断言:在IDEA中修改运行配置,加入-ea的参数即可。我开始还以为在程序参数中加,但是发现程序参数是主函数的args形参。应该调出VM参数,加入-ea。同时可以用-ea:或者-da:启用或抑制某个包中的断言。还有就是在程序中使用ClassLoader类加载器的时候,手动启用或是移除断言。
记录日志
在JDK1.3以前,日志都是直接print到STDOUT流中。后来出现了log4j,以及性能更优的slf4j框架。
- 基本日志:日志记录器有7个级别,分别为:SEVERE>WARNING>INFO>CONFIG>FINE>FINER>FINEST。
//全局日志记录器
public static void main(String[] args) {
int a = 1;
//Logger.getGlobal().setLevel(Level.WARNING);
//设置最低显示级别
Logger.getGlobal().info("赋值a为1。");
}
- 高级日志:也就是使用自定义的日志记录器。还可以有不同类型的日志,比如entering函数步入以及exiting函数退出。
public class Main{
private static final Logger mylog = Logger.getLogger("com.mylog");
//用静态变量存储日志记录器,防止没有使用而被垃圾回收器回收。
public static void main(String[] args) {
int a = 1;
mylog.warning("a赋值为1");
}
}
public class Main{
private static final Logger mylog = Logger.getLogger("com.mylog");
public static void main(String[] args) {
mylog.setLevel(Level.FINER);
mylog.setUseParentHandlers(false);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINER);
mylog.addHandler(handler);
mylog.entering("com", "main");
int a = 1;
mylog.exiting("com", "read", a);
}
}
//这里涉及到了处理器的内容,之后会详述。
- 处理器:日志记录器会将记录发送到日志处理器中,由处理器决定如何输出日志。默认情况下,会发送到ConsoleHandler中,输出到System.err中。(关于System.err与System.out相似,为标准错误输出,体现在编译器中就是输出内容为红色)值得注意的是,自定义处理器时,会默认将日志再传一份给命名为“”的父处理器,这也是为什么要设置setUseParentHandlers为false,目的是只有一遍输出即可。记录器有记录级别,同时处理器也有处理级别,所以能显示的日志为两者的最小阈值。类似的,还可以将日志输出到文件或者流数据中,分别对应了FileHandler和StreamHandler。
public class Main{
private static final Logger mylog = Logger.getLogger("com.mylog");
public static void main(String[] args) {
try {
Handler handler = new FileHandler("src/1.log");
//默认路径在C盘用户里
mylog.addHandler(handler);
mylog.setLevel(Level.WARNING);
mylog.warning("BUG!!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
文件处理器配置参数(java.util.logging.Handler) | 作用 |
---|---|
append | 追加文件末尾或是每个程序打开一个文件 |
limit | 文件最大允许大小,超过则新建 |
pattern | 文件路径以及文件名模式(如%g循环数值) |
count | 文件名循环数(1不循环) |
-
过滤器:每个记录器和处理器都可以有一个过滤器,默认情况下是根据日志的级别过滤。如果想要自定义的话,可以实现Filter接口,重写isLoggable方法,使用setFilter绑定。
-
格式化器:同样的,自定义时需要实现Formatter接口,用setFormatter绑定。
-
修改配置文件:文件路径在jdk/conf/logging.properties中,修改相应字段,程序中的修改优先级更高。可以指定自定义部件属性值,也可以直接修改全局值。
//补充配置文件相关,实质上Properties抽象出String类型的键值对
try {
Properties properties = new Properties();
FileInputStream fileInputStream = new FileInputStream("1.txt");
properties.load(fileInputStream);
//还可以通过store存储
String name = properties.getProperty("name");
System.out.println(name);
} catch (Exception e) {
e.printStackTrace();
}
- 本地化:使用logmessages资源包,显示信息必须用包中定义的,可以完成不同语言的映射,适用于全球化编程。
本文中的日志均使用的是java.util.logging自带的框架,可以使用功能更为强大的log4j或者是commons-logging。
调试技巧
-
日志代理:new对象的时候,使用匿名类重写要使用的方法,调用super方法,加入日志信息。
-
-verbose启动VM,可以看到类的加载路径。-Xlint对常见错误的检查。
-
Windows下打开bin下的jconsole监控内存、线程、类加载等情况。
-
jmap工具查看堆中变量值。(使用jps命令查看运行的所有java程序及其虚拟标识符)
PS
-
print中如果出现多个变量的字符串拼接,最好使用printf格式化输出,否则会创建很多的String变量。
-
打开文件的路径框中输入cmd,可以直接在目标路径下打开命令行。