读后笔记 -- Java核心技术(第10版 卷I ) Chapter7 异常、断言和日志


Chapter7 异常、断言和日志

7.1 处理错误

7.1.1. 异常分类

1)Error 类:运行时系统的内部错误和资源耗尽错误。 应用程序不应该抛出这种类型的对象;

2)派生于 RuntimeException 的异常(程序错误导致的异常,一定是自己的问题)包含:

  • 错误的类型转换;
  • 数组访问越界;
  • 访问 null 指针;

3)不是派生于 RuntimeException 的异常包含:

  • 试图在文件尾部后面读取数据;
  • 试图打开一个不存在的文件;
  • 试图根据给定的字符串查找 Class 对象,而这个字符串表示的类并不存在;
非受查异常: 派生于 Error 类 或 RuntimeException 类的异常;
受查异常:  其他异常;

7.1.2/3 声明 受查异常 + 抛出异常

遇到下列情况时应该抛出异常1)调用一个抛出受査异常的方法,如 FilelnputStream 构造器;
2)程序运行过程中发现错误, 并且利用 throw语句抛出一个受查异常;
3)程序出现错误, 如 a[-1]=0 会抛出一个 ArraylndexOutOfBoundsException 这样的非受查异常;
4)Java 虚拟机和运行库出现的内部错误;
1)一个方法必须声明所有可能的抛出的 受查异常;
2)不需要声明 非受查异常(继承自 Error, RuntimeException);

  !!! 如果在子类中覆盖了超类的一个方法, 子类方法中声明的受查异常不能比超类方法中声明的异常更通用(即,子类方法中可以抛出更特定的异常,或根本不抛出任何异常)。   特别需要说明的是, 如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。
抛出异常:
class MyAnimation
{
    ...
    public Image loadImage(String s) throws FileNotFoundException, EOFException
    {
        ...
throw new EOFException(); } }

7.1.4 创建异常类

  有时需要重新定义一个派生于 Exception类 或 Exception子类 的类。

定义的类应该包含两个构造器, 一个是默认的构造器;另一个是带有详细描述信息的构造器(超类 Throwable 的 toString 方法将会打印出这些详细信息,这在调试中非常有用)。
class FileFormatException extends IOException
{
    public FileFormatException() {}
    public FileFormatException(String gripe) {
        super(gripe); 
    } 
}

7.2 捕获异常

7.2.1 捕获异常

try
{
    code
    more code
}
catch (Exception e)
{
    handler for this type
}
规则:
如果编写一个覆盖超类的方法,而这个方法又没有抛出异常(如 JComponent 中的 paintComponent ), 那么这个方法就必须捕获方法代码中出现的每一个受查异常。
不允许在子类的 throws 说明符中出现超过超类方法所列出的异常类范围。

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 赋不同的值

7.2.3 再次抛出异常与异常链

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)这种包装技术的好处:让用户抛出子系统中的高级异常,而不会丢失原始异常的细节;

7.2.4 finally 子句

// 1)finally 普通形式 try/catch/finally:
try
{
    code that might throw exceptions
}
catch (IOException e)
{
    show error message
}
finally
{
    //
}

// 2)finally 推荐形式(try/catch 和 try/finally):
try
{
    try
    {
        code that might throw exceptions
    }
    finally
    {
        in.close();
    }
}
catch (IOException e)
{
    show error message
}

7.2.5 带资源的 try 语句

带资源的最简形式:
try (Resource res = ...)
{
    work with res
}
// try 块退出时,会自动调用 res.close()。

例如:
try (Scanner in = new Scanner(new FileInputSystem("/usr/share/dict/words")), "UTF-8")
{
    while (in.hasNext()) {
        System.out.println(in.next());
    }
}

7.2.6 分析堆栈轨迹元素

   堆栈轨迹( stack trace ):是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)
{
    analyze frame
}

7.3 使用异常机制的技巧

1. 异常处理不能代替简单的测试;

 因为 异常处理 花费的时间大大超过测试时间,所以,只在异常情况下使用异常机制。

2. 不要过分地细化异常;

 异常处理机制的其中一个目标:将正常处理与错误处理分开

3. 利用异常层次结构;

  • 不要只抛出 RuntimeException 异常,应该寻找更加适当的子类或创建自己的异常类;
  • 不要只捕获 Thowable 异常, 否则,会使程序代码更难读、 更难维护;

4. 不要压制异常;

5/6:早抛出,晚捕获;

7.4 使用断言

7.4.1 断言的概念

  断言机制:允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插人的检测语句将会被自动地移走。
asset 的两种形式:
1)assert 条件;           // assert x >= 0;
2)assert 条件: 表达式;    // assert x >= 0: x;   表达式将被传入 AssertionError 的构造器,并转换成一个消息字符串

** 1)、2):如结果 false,则报 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 类的断言

7.4.3 使用断言完成参数检查

1)Java 处理系统错误的机制:

  • 抛出一个异常;
  • 日志;
  • 使用断言;

2)使用断言 需要记住的 2 点:

  • 断言失败是致命的、不可恢复的错误;
  • 断言检查仅用于 开发和测试 阶段;

7.5 记录日志

7.5.1 基本日志

// 全局日志记录器 global logger
Logger.getGlobal().info("info");      // 日志输出 info
Logger.getGlobal().setLevel(Level.OFF);   // 取消所有日志

7.5.2 高级日志

1. 自定义日志记录器
private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");

2. 记录
1) myLogger.warning(message);    // 记录 日志
2) myLogger.log(Level.FINE, message);   // 设定级别

3. 异常日志的记录方法:
void throwing(String className, String methodName, Throwable t)
void log(Level l, String message, Throwable t)

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

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

7.5.3 修改日志管理器配置

1)默认:
  jre/lib/logging.properties

2)配置文件
  java -Djava.util.logging.config.file=configFile MainClass

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 流。

1. 要修改日志记录的级别,需要修改 日志记录器 和 日志处理器,如记录 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);

2. 日志输出到其他地方时,需要添加其他的处理器:
1)SocketHandler:发送到特定的主机和端口;
2)FileHandler:收集文件中的记录
    FileHandler handler = new FileHandler();
    logger.addHandler(handler);

7.5.6 过滤器

  • 1. 在默认情况下, 过滤器根据日志记录的级别进行过滤;
  • 2. 每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的过滤;
  • 3. 还可以自定义过滤器:1)实现 Filter 接口;2)定义 boolean isLoggable(LogRecord record)
  • 4. 调用 setFilter 方法 可以 将一个过滤器安装到一个日志记录器或处理器中。 注意,同一时刻 仅有一个 过滤器。

7.5.7 格式化器

  • 1. ConsoleHandler 类 和 FileHandler 类可生成 文本和 XML 格式的日志文件;
  • 2. 可自定义格式:1)扩展 Formatter 类;  2)覆盖方法 String format(LogRecord record);
  • 3. 调用 setFormatter 方法 将格式化器 安装到处理器中;

7.5.8 日志记录说明

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");
public class StackTraceTest {
    private static final Logger logger = Logger.getLogger("stacktrace");

    public static void main(String[] args) {
        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);
            }
        }
        
        Scanner in = new Scanner(System.in);
        System.out.println("Enter n: ");
        int n = in.nextInt();
        logger.info("Enter n: " + n);
        factorial(n);
    }
}

====================
默认的 C:\Users\Google_he\myapp.log.0 的内容为:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>



  2022-05-26T16:30:02
  1653553802987
  0
  stacktrace
  INFO
  stacktrace.StackTraceTest
  main
  1
  Enter n: 3

7.6 调试技巧

1. 记录或打印变量值

System.out.println("x=" + x);          // 打印变量
Logger.getGlobal().info("x=" + x);    // 记录变量
Logger.getGlobal().info("this=" + this);    // 记录 this 对象的状态

2. 类中 使用 main 方法进行单元测试;

3. JUnit 单元测试;

4. 超类方法 + 通过子类的对象 做日志代理,如

Random 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. 打印异常

try
{
    ...
}
catch (Throwable t)
{
    t.printStackTrace();
    throw t;
}

// 调用
Thread.dumpStack();

6. 异常输出到字符串 或 文件

// 输出到字符串
StringWriter out = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(out));
String description = out.toString();

7. 导出错误信息

// 捕获错误流 (bash 和 Windows shell):
java MyProgram 2 > errors.txt

// 同一文件中同时捕获 System.err 和 System.out:
java MyProgram 1 > errors.txt 2 > &1

9. 观察类的加载过程:- verbose 启动 java 虚拟机

10. -Xlint 选项对代码进行检查

11. ps (linux) / jconsole (windows) 对 java 运行程序进行监控

12. jmap 实用工具获得一个堆的转储, 其中显示了堆中的每个对象。

1)jmap -dump:format=b, file=dumpFileName processID
2)jhat dumpFileName
3)浏览器进入  localhost:7000

13. -Xprof 标志 运行 Java 虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。