随笔 - 148  文章 - 3  评论 - 2  阅读 - 11万

读后笔记 -- 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 分析堆栈轨迹元素

   堆栈轨迹( stack trace ):是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。对找出程序异常非常有用。当 Java 程序因一个未捕获的异常而终止时,就会显示堆栈轨迹。
复制代码
// 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 断言的概念

  断言机制:测试时 启用,生产发布时 禁用。目的:不用像加入一堆类似 if 判断来影响效率。
// 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 虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。

 

posted on   bruce_he  阅读(90)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

点击右上角即可分享
微信分享提示

目录导航