[Java Training]Week 6

0x00

​ 本系列-Java集训系列

​ 变成了自己学习一遍Java- -。

0x01

​ 第七章 异常、断言和日志

0x02

异常

  1. 程序总会产生错误,如果是在开发阶段,那么还可以看到错误去修复,然后重新启动,但是在程序的运行阶段,就不可以这样,所以当错误产生的时候,至少做到以下几点:

    • 向用户通告错误
    • 保存所有的工作结果
    • 允许用户以妥善的形式退出程序
  2. 将程序可能产生的错误做一个归类

    • 用户输入错误:用户不可能总是按照软件开发者的预期进行输入,会经常产生各种奇怪的输入,程序如果没有进行检查,那么就会产生错误
    • 设备错误:硬件可能在你发出指令之后不会正常执行,比如没有接上打印机,比如打印机没纸了
    • 物理限制:磁盘满了
    • 代码错误:代码无法正确执行,比如调用了错误的方法,方法返回的是null,然后对null进行了方法调用等
  3. 不论是系统还是语言,都会有一个异常处理机制,以便程序返回一种安全状态,并能够让用户执行一些其他的命令;或者允许用户保存所有操作的结果,并以妥善的方式终止程序

  4. 异常分类

    • 分类如下图

    Java异常层次结构

    • 所有的异常都是由Throwable继承而来,下一层分解为Error和Exception

    • Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误

    • Exception类下属两个分支,RuntimeException是由程序错误导致的异常;IOException是程序本身没有问题,但由于像I/O错误等这类问题导致的异常属于其他异常

    • 重点关注的是Exception,而Error应该尽量避免,也无法处理Error的错误

    • Exception中,派生于RuntimeException的异常包括以下几种情况:

      1. 错误的类型转换
      2. 数组访问越界
      3. 访问null指针
    • 不是派生于RuntimeException的异常包括:

      1. 试图在文件尾部后面读取数据
      2. 试图打开一个不存在的文件
      3. 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在
    • 派生于Error类或RuntimeException类的所有异常成为非受查异常,IOException在内的所有其他异常成为受查异常,其中受查异常是需要处理的,即抛出、捕获等

      想必看完上面已经看懵了,这里我用我自己粗浅的理解解释一下,简单说呢,非受查异常(Error和RuntimeException)是外界原因或者代码原因,Error不可控,没办法检查;RuntimeException是可控,最好的办法就是通过代码限制来规避,不需要检查。所以剩下的就是受查异常(IOException)

  5. 声明受查异常。

    • 方法应该在其首部生命所有可能抛出的异常。下面是一个例子

      public FileInputStream(String name) throws FileNotFoundException

    • 自己编写方法时,不必将所有可能抛出的异常都进行声明。以下四种情况需要抛出异常:

      1. 调用一个抛出受查异常的方法,例如,FileInputStream构造器。

      2. 程序运行过程中发现错误,并且利用throw语句抛出一个受查异常。

      3. 程序出现错误,例如,a[-1]=0会抛出一个ArrayIndexOutOfBoundsException这样的非受查异常。

      4. Java虚拟机和运行时库出现的内部错误。

        如果出现前两种情况,则必须告诉调用这个方法的程序员有可能抛出异常。

    • 对于那些可能被他人使用的Java方法,应该根据异常规范,在方法的首部声明这个方法可能抛出的异常

      1
      2
      3
      4
      5
      6
      7
      8
      class MyAnimation
      {
      //....
      public Image loadIeg(String s) throws IOException
      {
      //....
      }
      }

      如果抛出多个异常,那么就用逗号分隔

      1
      2
      3
      4
      5
      6
      7
      8
      class MyAnimation
      {
      //....
      public Image loadImage(String s) throws FileNotFoundException,EOFException
      {
      //....
      }
      }
    • 声明的应该都是受查异常,非受查异常不应该声明。所以,也不应该声明从RuntimeException类派生的异常,而是应该在代码上阻止产生RuntimeException异常。

  6. 也可以在逻辑有错误的时候主动抛出异常,抛出方法

    1
    throw new EOFException();

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    String readData(Scanner in) throws EOFException
    {
    while(True)
    {
    if (!in.hasNext()) // EOF encountered
    {
    if (n < len)
    throw new EOFException();
    }
    }
    retrn s;
    }
  7. 根据上一点,发现抛出已存在的异常很轻松:

    • 找到一个合适的异常类
    • 创建这个类的一个对象
    • 将对象抛出
  8. 创建自己的异常类:

    1
    2
    3
    4
    5
    6
    7
    8
    class FileFormatException extends IOException
    {
    public FileFormatException() {}
    public FileFormatException(String gripe)
    {
    super(gride);
    }
    }

    很多时候需要抛出一些自定义异常,来准确的描述发生的情况

    首先是一定要继承一个Exception类,然后要有默认构造方法和一个带详细描述参数的构造方法,其他方法视情况添加

    抛出自定义异常的方法并没有区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    String readData(BufferedReader in) thros FileFormatException
    {
    while(true)
    {
    if (ch == -1) // EOF encountered
    {
    if (n < len)
    {
    throw new FileFormatException();
    }
    }
    }
    }
  9. 抛出异常只是把异常的处理交给调用者,并没有真正的处理。如果不进行捕获(处理),程序就会终止运行。捕获异常之后,就可以在代码块中处理异常。

    捕获异常的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
       try
    {
    // 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
    16
    public 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
    16
    try
    {
    // 受查异常的代码
    }
    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
    12
    try
    {
    // code
    }
    catch (FileNotFoundException | UnknownHostException e)
    {
    // code
    }
    catch (IOException e)
    {
    // code
    }
  10. 在异常中抛出另一种异常,即改变异常类型,可以在catch中重新抛出另一个异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try
    {
    // code
    }
    catch (SQLException e)
    {
    Throwable se = new ServletException("database error");
    se.initCause(e);
    throw se;
    }

    这样既不丢失原始信息,还可以随意定义异常,想看原始信息通过以下方法

    Throwable e = se.getCause();

  11. 保证不论是否触发异常都会执行的代码块,finally,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    InputStream in = new FileInputStream();
    try
    {
    // code
    }
    catch (IOException e)
    {
    // code
    }
    finally
    {
    in.close();
    }

    上面这段代码中,不论怎么样都会执行finally中的 in.close()

  12. 如果想要只是关闭资源链接,那么可以用带资源的try语句

    1
    2
    3
    4
    5
    6
    try(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,如果需要关闭资源,那么尽量用这种方式

  13. 当产生异常的时候,需要分析产生原因,这时候可以使用分析堆栈的方法

    可以调用Throwable类的printStackTrace方法

    1
    2
    3
    4
    Throwable 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
2
3
4
5
6
7
8
9
10
11

静态的Thread.getAllStackTrace方法,可以产生所有线程的堆栈轨迹

```java
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Thread t : map.keySet())
{
StackTraceElement[] frames = map.get(t);
// code
}

  1. 使用异常的一些技巧

    • 异常处理不能代替简单的测试
    • 不要过分的细化异常
    • 利用异常层次结构
    • 不要压制异常
    • 在检测错误时,苛刻要比放任更好

断言

  1. 断言的关键字时assert

  2. 断言是对一个条件进行判断,如果不通过(即为false),则抛出一个AssertionError异常

  3. 断言有如下两种形式

    assert 条件;

    assert 条件 : 表达式;

  4. 断言是被用来在开发的时候进行测试的,默认情况下是不启用的,放在代码里也不会对速度产生影响,在运行程序时用 -enableassertions 或 -ea 选项启用断言

    java -enableassertions MyApp

    java -ea MyApp

    也可以在某个类或整个包中使用断言

    java -ea:MyClass -ea:com.mycompany.mylib… MyApp

    启用断言不需要重新编译程序

  5. 关于断言需要注意的点

    • 断言失败是致命的、不可恢复的错误
    • 断言检查只用于开发和测试阶段
  6. 断言使用示例

    1
    2
    // 当i小于0的时候,就会报错并且退出程序
    assert i >= 0;

日志

  1. 日志的意义:初学者会发现调试神器 System.out.println() ,但是每次都要在调试地点插入这句话,然后在调试完成之后再注释掉或者删除,这样是十分不优雅的,日志就是一个十分优雅的解决方案

    • 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易
    • 可以很简单的禁止日志记录的输出,因此,将这些日志代码留在程序中的开销很小
    • 日志记录可以被定向到不同的处理器,用于再控制台中显示,用于存储在文件中等
    • 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项
    • 日志记录可以采用不同的方式格式化,例如,纯文本或XML
    • 应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,例如,com.mycompany.myapp
    • 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置
  2. 最简单的日志使用方法

    使用全局日志记录器(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);

    会取消所有的日志

  3. 高级使用日志方法

    可以自定义日志记录器,而不是所有都用全局日志记录器

    1
    private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");

    与包名类似,日志记录器名字也有层次结构,而且日志记录器还可以继承设置,比如com.mycompany 设置的级别是FINE,那么子包都会默认级别是FINE,如 com.mmycompany.abc

  4. 通常有以下日志记录器级别:

    • SEVERE
    • WARNING
    • INFO
    • CONFIG
    • FINE
    • FINER
    • FINEST

    默认情况下只记录前三个级别,也可以手动设置级别:

    1
    2
    // 在日志记录之前加上这行代码,就成功将日志级别改成FINE
    logger.setLevel(Level.FINE);

    这样在日志中就记录FINE以上的级别的日志

  5. 记录不同级别的日志

    1
    2
    3
    logger.warning(message);
    logger.fine(message);
    logger.log(Level.FINE, message);
  6. 跟踪调用的方法

    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
    7
    int 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;
    }
  7. 在代码中进行配置不容易更改,也不容易复用。更好的解决方案是使用配置文件。

    默认情况下,配置文件存在于 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
  8. 日志本地化,即根据不同地区显示不同的语言,实现原理是单词映射

    首先是资源包,需要为每个地区创建一个资源包,然后放到固定位置,比如

    com/mycompany/logmessages_en.properties

    com/mycompany/logmessages_de.properties

    com/mycompany/logmessages_zh.properties

    文件里的内容是

    1
    2
    readingFile = Achtung! Datei wird eingelesen
    renamingFile = Datei wird umbenannt

    然后记录日志的时候指定资源包

    1
    Logger logger = Logger.getLogger(loggerName, "com.mycompany.logmessages");

    在记录的日志中使用关键词就可以本地化啊(非关键词不会本地化):

    1
    logger.info("readingFile");

    在配置文件中使用占位符

    1
    2
    Reading file {0}.
    Achtung! Datei {0} wird eingelesen

    然后使用的时候这样用

    1
    2
    logger.log(Level.INFO, "readingFile", fileName);
    logger.log(Level.INFO, "renamingFile", new Object[] { oldName, newName});
  9. 日志处理器,即日志的处理程序,日志记录器将日志记录下来之后会发送到日志处理器中,一般以xxxHandler命名,如自带的:ConsoleHandler、FileHandler、SocketHandler。分别表示 控制台处理器、文件处理器、Socket处理器。

    日志记录器默认会将记录发送到ConsoleHandler中,而且日志记录器还会将记录发送到父处理器中。

    处理器也有日志记录级别。所以日志的实际记录级别是高于日志记录器和处理器的阀值。处理器的默认记录级别也是INFO

    java.util.logging.ConsoleHandler.level=INFO

    还可以自定义日志记录器,然后使用

    1
    2
    3
    4
    5
    6
    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); // 将自定义的处理器添加到日志中,使用

    其中,FileHandler是将日志发送到文件中的处理器,默认是发送到用户主目录的javan.log的文件中,以xml方式保存。

    可以通过更改FileHandler的设置,来重点记录想关注的。

  10. 过滤器,即对日志进行过滤,来对重点内容进行关注。

    通过实现Filter接口,并实现下列方法:

    1
    boolean isLoggable(LogRecord record)

    实现方法:接收的参数是日志的一行记录,在方法中对记录进行操作,比如进行判断是否包含err,返回Boolean值,如果为true就是记录下来的,返回为false就是过滤掉的。

    将过滤器安装到记录器或者处理器中,调用setFilter即可,同一时刻只能有一个过滤器。

  11. 格式化器,对日志记录的格式进行设置的处理程序。

    日志处理器的默认格式可能会缺少一些关键信息,可以通过自定义格式化器来覆盖默认的配置。

    扩展Formatter类并覆盖下面这个方法:

    1
    String format(LogRecord record)

    实现方法:接受的参数是日志的一行记录,在方法中对记录进行格式化,然后return格式化之后的日志。

    格式化可能会用到这个方法:

    1
    String formatMessage(LogRecord record)

    加头和加尾:

    1
    2
    String getHead(Handler h)
    String getTail(Handler h)

    最后,调用setFormatter方法将格式化器安装到处理器中。

  12. 日志记录总结:

    • 程序尽可能的都要使用日志,在使用日志记录器的时候,保持日志记录器命名为主应用程序包名。
    • 要为日志提供一个配置,推荐使用配置文件。
    • 日志记录的级别要谨慎使用,如果记录太多可能导致日志中存在大量无用信息。
  13. 调试技巧:

    • 在可能出问题的附近使用打印变量,查看是否符合预期

      1
      System.out.println("x=" + x);
    • 每一个类中都放一个main,即单元测试,通过调用来测试方法的输入和输出是否符合预期

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public class MyClass
      {
      // code

      public static void main(String[] args)
      {
      // test code
      }
      }
    • 单元测试有单元测试框架,可以省去每个方法创建main手动写代码以及擦除的过程

    • 日志代理(logging proxy)是一个子类的对象,可以截获方法调用,并进行日志记录,然后调用超类中的方法

      1
      2
      3
      4
      5
      6
      7
      8
      Random generator = new Random(){
      public double nextDouble()
      {
      double result = super.nextDouble();
      Logger.getGlobal().info("nextDouble: "+ result);
      return result;
      }
      }
    • Throwable类中的printStackTrace方法,可以获取调用的堆栈

      1
      2
      3
      4
      5
      6
      try{
      //
      }catch (Throwable t){
      t.printStackTrace();
      throw t;
      }

      也可以不捕获异常直接调用获取堆栈信息

      1
      Thread.dumpStack();
    • 堆栈轨迹默认显示在System.err上,也可以利用printStackTrace(PrintWriter s)方法将堆栈信息发送到文件中。也可以采用以下方法将堆栈信息放到字符串中:

      1
      2
      3
      StringWriter 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
      8
      Thread.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 协议 ,转载请注明出处!