读后笔记 -- Java核心技术(第11版 卷I ) Chapter7 异常、断言和日志
7.1 处理错误
异常处理的目标:
- 1)向用户通知错误;
- 2)保存所有的工作;
- 3)允许用户妥善地退出程序;
异常处理的任务:将控制权从产生错误的地方转移到能处理异常情况的错误处理器。
7.1.1. 异常分类
1)Error 类:运行时系统的内部错误和资源耗尽错误。 应用程序不应该抛出这种类型的对象;
2)派生于 RuntimeException 的异常(程序错误导致的异常,一定是自己的问题。程序中的逻辑错误)包含:
- 错误的强制类型转换;
- 数组访问越界;
- 访问 null 指针;
3)不是派生于 RuntimeException 的异常(程序本身没有问题,但由于类似 I/O 错误导致的异常。程序不能控制的错误,如网络连接等)包含:
- 试图超越文件末尾继续读取数据;
- 试图打开一个不存在的文件;
- 试图根据给定的字符串查找 Class 对象,而这个字符串表示的类并不存在;
非检查型异常(UncheckedException):1)Error、RuntimeException 的子类; 2)函数声明中不必加 throws,调用上也不强制; 3)(建议)使用 try...catch 处理异常;
检查型异常(CheckedException): 1)Exception 的子类,但不是 RuntimeException 的子类; 2)函数声明中必须加 throws,如不处理必须在调用函数上声明 throws;
7.1.2/3 声明 检查异常 + 抛出异常
应该抛出异常(使用 throws)的情况包含: 1)调用一个抛出检查型异常的方法,如 FilelnputStream 构造器(it will throws FileNotFoundException); 2)检测到一个错误, 并且利用 throw 语句抛出一个检查异常; 3)程序出现错误, 如 a[-1]=0 会抛出一个 ArraylndexOutOfBoundsException 这样的非检查异常; 4)Java 虚拟机和运行时库出现的内部错误;
异常声明的原则:
1)所有的检查型异常,必须方法的首部抛出,并用 “,” 隔开。如果没有 trhows 说明符的方法根本不能抛出任何检查型异常;
2)不需要声明 非检查异常(继承自 Error、RuntimeException);
void drawImage(int i) throws ArrayIndexOutOfBoundsException // bad style, (don't declare unchecked exceptions)
3)如果在子类中覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用(即,子类方法中抛出可以更特定的异常,或不抛任何异常)。
特别需要说明的是, 如果超类方法没有抛出任何检查异常,子类也不能抛出任何检查异常。
抛出异常的方法:
1.找到一个合适的异常类;
2.创建这个类的一个对象;
3.将对象抛出;
class MyAnimation { ... public Image loadImage(String s) throws FileNotFoundException, EOFException{ ...
throw new EOFException(); } }
7.1.4 创建异常类
自定义的异常类应该包含两个构造器: 1)默认的构造器; 2)带有详细描述信息的构造器(超类 Throwable 的 toString 方法将会打印详细信息,调试中很有用); class FileFormatException extends IOException { public FileFormatException() {} // 1)默认的无参数构造器 public FileFormatException(String gripe) { // 2)带字符串参数的详细信息构造器 super(gripe); } }
7.2 捕获异常
7.2.1 捕获异常
// 最简单的 try-catch 异常代码块 try { code more code } catch (ExceptionType e) { handler for this type }
异常捕获(catch)规则:
1. 如方法发生了异常,但没有任何地方捕获,则程序将终止,并在控制台打印消息(含:异常类型和堆栈轨迹);
2. 如方法的代码抛出一个 catch 子句没有声明的异常,则该方法立即退出;
3. 如果编写一个覆盖超类的方法,而这个方法又没有抛出(throws)异常(如 JComponent 中的 paintComponent ), 那么这个方法就必须 捕获(catch)方法代码中出现的每一个检查型异常;
4. 不允许在子类的 throws 说明符中出现 超类方法 所列出的异常类(就是 7.1.2 异常类声明的原则,另外这里指的是 throws,注意区别于第三点的 catch 规则);
5. 处理异常,有2种方式:1)捕获异常并处理 --- catch; 2)继续传播异常 --- throws
一般的经验:捕获知道如何处理的异常;继续传播不知道如何处理的异常;
6. 大多数情况下,不需要加 try/catch,应该只是让异常发生,并把它传播到调用方法;
7. 如果这个异常是检查型异常,可能需要用 thorws 子句标记方法,这种情况下,可能需要加 try/catch (因为必须要捕获)
7.2.2 捕获多个异常
try { code that might throw exceptions } catch (FileNotFoundException | UnknownHostException e) { emergency action for missing files and unknown hosts } catch (IOException e) { emergency action for all other I/O problems } ** 注意: 1)捕获的异常类型彼此之间不存在子类关系; 2)捕获多个异常时,异常变量隐含为 final 变量。所以,不能为上面例子的 e 赋不同的值;
3)多个catch 的异常顺序:从特定到一般, FileNotFoundException 是 IOException 的一个子类;
7.2.3 再次抛出异常与异常链
// 应用场景1:Sometimes you want to catch an exception and rethrow it as a different type.
// 解决方式1:
try { access the database } catch (SQLException e) { throw new ServletException("database error: " + e.getMessage()); } // 优点:获取了更详细的异常(目的:将检查型异常 转换为 非检查型异常,将 throws 变成了 try/catch,可以打印更详细的信息);
// 缺点:改变了原来的异常,原异常丢失 // 解决方式2(Better): Set the original exception as the cause try { access the database } catch (SQLException e) { // catch 子句中抛出异常,目的:改变异常类型。表示子系统的异常类型可能存在多种解释; Throwable se = new ServletException("database error"); se.initCause(e); // 同时,将原始异常设置为新异常的“原因” throw se; } // **** 1) 可通过 Throwable e = se.getCause(); 重新获取原始异常; 2)包装技术的好处:让用户抛出子系统中的高级异常,而不会丢失原始异常的细节;
3)如果一个方法中发生了一个检查型异常,但方法不允许抛出检查型异常,那么就可以捕获该检查型异常,并将其包装成一个运行时异常(即非检查型异常);
// 应用场景2:只记录异常,再将它重新抛出,而不做任何改变 // 解决方式: try { access the database } catch (Exception e) { logger.log(level, message, e); throw e; }
7.2.4 finally 子句
// 1)形式一:普通形式 try-catch-finally。
// finally 的用途:在异常发生时,剩余的处理需要在 finally 进行解决,如资源清理。java 7 中更快的方式是使用了 7.2.5 的 try-with-resources 语句 try { code that might throw exceptions } catch (IOException e) { show error message } finally { // } // 2)形式二:推荐形式 try-catch 和 try-finally。
// 内层的 try:确保关闭输入流; 外层的try:确保报告出现的错误 try { try { code that might throw exceptions } finally { in.close(); } } catch (IOException e) { show error message }
// 注意:finally 子句的体要用于清理资源。不要把改变控制流的语句(return, throw, break, continue)放在 finally 子句中。
7.2.5 带资源的 try 语句(try-with-resources)-- 很有用
// try-with-resources:带资源的 try 语句,不需要加 finally 语句
// 做法: 在 try 语句中声明一个或多个变量,即以后想要关闭的变量,然后把处理这些资源的代码放在大括号里;
// 前提条件: 这些 Resource 类必须实现 AutoClosable 接口,这个接口只有一个名为 close 的方法 try (Resource res = ...) { work with res } // try 块退出时,会自动调用 res.close() // 应用场景:使用了 2 个资源 Scanner 和 PrinterWriter,无论发生什么问题,out 和 in 都会关闭;如果使用常规方式,就得需要两个 try/finally 语句
// (注意:)关闭资源的顺序 与 打开的 正好相反 try (var in = new Scanner(new FileInputSystem("/usr/share/dict/words"), StandardCharsets.UTF_8);
var out = new PrinterWriter("out.txt", StandardCharsets.UTF_8)) { while (in.hasNext()) {
out.println(in.next().toUpperCase()); } } // 不论这个块如何退出,in 和 out 都会关闭
// Java 9 中,可以在 try 首部中提供之前声明的事实最终变量(try-with-resources can be used with an effectively final variable): void print(PrinteWriter out, String[] lines) { try (out) { // effectively final variable,上面print()方法定义的变量 for (String line : lines) { out.println(line.toLowerCase()); } } // out.close() called here }
7.2.6 分析堆栈轨迹元素
// 1. 打印栈轨迹 printStackTrace,可打印 throwable 栈轨迹 catch (Exception e) { e.printStackTrace(); } // 2. 将栈轨迹保存到一个 string var t = new Throwable(); var out = new StringWriter(); t.printStackTrace(new PrinterWriter(out)); String description = out.toString(); // 3. (Java 9之前的方法)获取栈轨迹 getStrackTrace(),并分析。
// Throwable.getStackTrace() 会生成一个 StackTraceElement[] 数组,不过调用的效率不高,原因下面2点:
// 1) 会得到整个堆栈,但调用者可能只需要几个栈帧;
// 2) 只允许访问挂起方法的类名,而不能访问类对象; Throwable t = new Throwable(); StackTraceElement[] frames = t.getStackTrace(); for (StackTraceElement frame : frames) { System.out.println(frame); } // 4. (相对于第3点的 getStackTrace())更灵活的方式,使用 StackWalker 类 StackWalker walker = StackWalker.getInstance(); walker.foreach(frame -> analyze frame) // 如果以懒方式处理 Stream<StackWalker.StackFrame>,可以调用: walker.walk(stream -> process stream)
7.3 使用异常机制的技巧
1. 异常处理不能代替简单的测试;
基本原则:异常处理 花费时间大大超过测试时间,所以,只在异常情况下使用异常机制。
2. 不要过分地细化异常;
目标:将正常处理与错误处理分开
3. 利用异常层次结构;
- 不要只抛出 RuntimeException 异常,应该找一个合适的子类或创建自己的异常类;
- 不要只捕获 Throwable 异常, 否则,会使代码更难读、 更难维护;
- 考虑 检查型异常 与 非检查型异常 的区别。检查型异常很庞大,不要为逻辑错误抛出这些异常;
- 如能将一种异常转换成更加适合的异常,就不要犹豫;
4. 不要压制异常;
5/6:早抛出,晚捕获;
7.4 使用断言
7.4.1 断言的概念
// asset 的两种形式: 1)assert 条件; // assert x >= 0; 2)assert 条件: 表达式; // assert x >= 0: x; 将 x 的实际值传递给 AssertionError 对象,以便以后显示 ** 1)、2):如结果 false,则报 AssertionError 异常
表达式唯一的目的就是产生一个消息字符串,AssertionError 对象并不存储具体的表达式值。
7.4.2 启用和禁用 断言
// 启用 断言 java -enableassertions / -ea MyApp 如: java -ea:MyClass -ea:com.mycompany.mylib MyApp // 开启 MyClass 类及在 com.mycompany.mylib包和它的子包中的所有类的断言 // 禁用 断言 (默认) java -disableassertions / -da MyApp 如: java -ea:... -da:MyClass MyApp // -ea:开启默认包中的所有类的断言,禁用 MyClass 类的断言
// 注意:
1. -ea / -da 是类加载器的功能,所以代码无需重新编译;
2. 开关不能应用到没有类加载器的“系统类”;
3. 针对“系统类”,需要使用 -enablesystemassertions / -esa 开启
// 案例: public class AssertionTest { public static void main(String[] args) { var in = new Scanner(System.in); System.out.println("Enter x to calculate its sqrt: "); int x = in.nextInt(); assert x >= 0; double y = Math.sqrt(x); System.out.println("sqrt is: " + y); } }
使用断言的 2 种常用场景:
- 1. 检查参数; 如参数 不为 null, assert a != null;
- 2. 用断言检查假设; 如计算平方根, assert i >= 0;
7.4.3 使用断言完成参数检查
1)Java 处理系统错误的机制:
- 抛出一个异常;
- 日志;
- 使用断言;
2)使用断言 需要记住的 2 点:
- 断言失败是致命的、不可恢复的错误;
- 断言检查仅用于 开发和测试 阶段;
7.5 记录日志
7.5.1 基本日志
// 全局日志记录器 global logger Logger.getGlobal().info("info"); // 日志输出 info Logger.getGlobal().setLevel(Level.OFF); // 取消所有日志,位置放在所有日志记录之前,如 main 的最前面
7.5.2 高级日志
// 1. 自定义日志记录器 private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp"); // 使用静态变量存储日志记录器的引用,因为未被引用的日志记录器可能会被垃圾回收
// 日志记录器与包类似,也有层次级别,如果设置了 "com.mycompany" 日志记录器的日志级别,则其子日志记录器 com.mycompany.myapp 会继承这个级别。
// 2. 记录 1) myLogger.setLevel(Level.FINE); // 设置日志级别
2) myLogger.warning(message); // 记录日志 3) myLogger.log(Level.FINE, message); // log() 指定级别,默认只记录前 3 个级别(默认级别 INFO)。共 7 个日志级别: SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST
// 3. 跟踪执行流的方便方法,entering() 和 exiting():
int read(String file, String pattern) {
// logger.entering() 下面的参数形式需要提供:类名,方法名,方法参数;必须将 方法参数打包到一个数组中
logger.entering("com.mycompany.mylib.Reader", "read", new Object[] {file, pattern});
...
// logger.exiting() 下面的参数形式需要提供:类名,方法名,要返回的值
logger.exiting("com.mycompany.mylib.Reader", "read", count);
return count;
}
// 4. 异常日志的记录方法: void throwing(String className, String methodName, Throwable t) void log(Level l, String message, Throwable t) // 典型的用法: if (...) { var e = new IOException("..."); logger.throwing("com.mycompany.mylib.Reader", "read", e); throw e; } try { ... } catch (IOException e) { Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e); }
如果想要看到比默认的 INFO 更详细的日志,需要修改 2个地方:1)日志记录器的级别; 2)日志处理器的级别
7.5.3 修改日志管理器配置
// 1)默认 Default(system-wide) configuration file: jre/lib/logging.properties // before Java 9
conf/logging.properties // after Java 9 // 2)配置文件 (can set other file at program startup) java -Djava.util.logging.config.file=configFile MainClass
// 可以如下面设置自定义配置文件: .level=INFO // change the default logging level com.mycomany.myapp.level=FINE // configure your own loggers
日志记录器仅记录日志到日志文件,不会发送消息到控制台
因为日志处理器在虚拟机启动时初始化,如定制日志属性,有两种方式:
1)命令行启动:java -Djava.util.logging.config.file=configFile MainClass
2)程序中调用:System.setProperty("java.util.logging.config.file", file);
LogManager.getLogManager().updateConfiguration(mapper); // Java 9
LogManager 配置文件的更详细描述,参考:https://www.cnblogs.com/bruce-he/p/17466560.html
7.5.4 本地化
本地化的应用程序包含资源包(resource bundle) 中的本地特定信息。一个程序可以包含多个资源包,例如一个用于菜单,另一个用于日志消息。每个资源包都有一个名字(如 com.mycompany.logmessages)。要想为资源包添加映射,
需为每个本地化环境提供一个文件。英文消息映射位于 com/mycompany/logmessages_en.properties 文件中。可将这些文件与应用程序的类文件放在一起,以便 ResourceBundle 类自动地对它们进行定位。 1. 请求日志记录器时,可指定一个资源包: Logger logger = Logger.getLogger(loggerName, "com.mycompany.logmessages"); 2. 为日志消息指定资源包的键,而不是实际的日志消息字符串: logger.info("readingFi1e");
7.5.5 处理器
默认情况下, 日志记录器 将记录 发送给 ConsoleHandler,再输出到 System.err 流(即,Logger -> Handler -> System.err)。
// 1. 要修改日志记录的级别,需要修改 Logger 和 Handler,只有2者都修改了,输出到最终的才有效。如记录 FINE 级别的日志: Logger logger = Logger.getLogger("com.mycompany.myapp"); // 1)修改日志记录器 logger.setLevel(LEVEL.FINE); logger.setUseParentHandlers(false); // 不用看到两次日志 Handler handler = new ConsoleHandler(); // 2)修改日志处理器 handler.setLevel(LEVEL.FINE); logger.addHandler(handler); // 将日志处理器绑定到日志记录器上
// 2. 日志处理器默认的配置,如调整,可修改该配置:
java.util.logging.ConsoleHandler.level=INFO // 3. 日志输出到其他地方时,需要添加其他的处理器: 1)SocketHandler:发送到特定的主机和端口; 2)FileHandler:收集文件中的记录 FileHandler handler = new FileHandler(); logger.addHandler(handler);
7.5.6 过滤器
1. 默认情况下,过滤器根据日志记录的级别进行过滤;
2. 每个 日志记录器 和 处理器 都可以有一个可选的过滤器来完成附加的过滤,方法如下:
- 1)实现 Filter 接口;
- 2)定义方法 boolean isLoggable(LogRecord record);
3. 调用 setFilter 方法 将自定义的过滤器安装到 日志记录器 或 处理器中。 注意,同一时刻 仅有一个 过滤器;
7.5.7 格式化器
1. ConsoleHandler 类 和 FileHandler 类可生成 文本和 XML 格式的日志文件;
2. 另外,也可以自定义格式,这就需要:
- 1)扩展 Formatter 类;
- 2)并覆盖其方法 String format(LogRecord record);
3. 调用 setFormatter 方法 将格式化器 安装到处理器中;
7.5.8 日志技巧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 1. 对一个简单的应用,选择一个日志记录器,并命名与主应用程序包一样的名字: private static final Logger logger = Logger.getLogger( "com.mycompany.myprog" ); // 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 LOG_ROTATION_COUNT = 10 ; Handler handler = new FileHandler( "%h/myapp.log" , 0 , LOG_ROTATION_COUNT); Logger.getLogger( "" ).addHandler(handler); } catch (IOException e) { logger.log(Level.SEVERE, "Can't create log file handler" , e); } } // 3. 打印日志(因为上面已经打开所有级别日志,所以具体内容输出可以选择合适的日志级别), logger.info("info test"); |
7.6 调试技巧
1. 记录或打印变量值
System.out.println("x=" + x); // 1)打印变量 Logger.getGlobal().info("x=" + x); // 2)记录变量 Logger.getGlobal().info("this=" + this); // 3)记录 this 对象的状态
2. 类中 使用 main 方法进行单元测试(常用的单元测试方式);
3. JUnit 单元测试;
4. 超类方法 + 子类的对象 做日志代理,如
// 日志代理是一个子类的对象,它可以捕获方法调用,记录日志,然后调用超类的方法
var generator = new Random() {
@Override public double nextDouble() { double result = super.nextDouble(); Logger.getGlobal().info("nextDouble: " + result); return result; } }; // 调用 nextDouble 方法时,将会输出日志 double t = generator.nextDouble(); ====== Output ======= 五月 27, 2022 3:41:16 下午 debug.DebugTest$1 nextDouble 信息: nextDouble: 0.9236715388542688
5. 打印异常
// 方式1:捕获异常
try { ... } catch (Throwable t) { t.printStackTrace(); throw t; } // 方式2:直接 dump Thread.dumpStack();
6. 异常输出到字符串 或 文件
// 输出到字符串 var out = new StringWriter(); new Throwable().printStackTrace(new PrintWriter(out)); String description = out.toString(); // 输出到文件 new Throwable().printStackTrace(new PrintWriter("E:\\Temp\\myfile.txt", StandardCharsets.UTF_8));
7. 导出错误信息
// 捕获错误流 (bash 和 Windows shell): java MyProgram 2 > errors.txt // 同一文件中同时捕获 System.err 和 System.out: java MyProgram 1 > errors.txt 2 > &1
9. 观察类的加载过程: 启动 java 虚拟机时使用 -verbose
10. -Xlint 选项对代码进行检查,如 javac -Xlint sourceFiles
11. ps (linux) / jconsole (windows) 对 java 运行程序(如下图,其包含了:内存消耗、线程使用、类加载、CPU使用率)进行监控
12. jmap 实用工具获得一个堆的转储, 其中显示了堆中的每个对象。
1)jmap -dump:format=b, file=dumpFileName processID 2)jhat dumpFileName 3)浏览器进入 localhost:7000
13. -Xprof 标志 运行 Java 虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战