读《Java核心技术 卷I》有感之第7章 异常、断言和日志

如今我现已毕业,即将踏上工作岗位,Java语言却才只看了这么一点,真的很是惭愧。现在的我,语言未通,业务无知,框架不懂,真的很是惶恐,惶恐于日后踏上工作岗位时一问三不知被同事耻笑,但又无能为力的尴尬感。姑且默默激励自己,因为我知道开始的艰辛和未知的技术必然会在今年下半年给予我重创,但是能否使自己蜕变和成长,这半年的效果也尤为重要。转行之路仍未结束,在自己独当一面之前,什么都仅仅是前行路上的成长点罢了。

  几个基本概念的明晰:

  • 异常处理:捕获可能造成程序崩溃的输入;
  • 断言:开启与关闭检测用程序;
  • 日志:存储相关运行信息。

7.1 处理错误

  这里简单表述了可能出现错误的原因:

  1. 用户输入错误:比如输错了个URL;
  2. 设备错误:比如打印机没纸了;
  3. 物理限制:比如磁盘的存储空间已经被用完了;
  4. 代码错误:比如计算索引不合法,查找不存在的记录等等。

7.1.1 异常分类

读《Java核心技术 卷I》有感之第7章 异常、断言和日志_第1张图片
  Error类层次结构描述了Java运行时系统内部错误和资源耗尽错误;Exception层次结构分为由程序错误(如错误的类型转换、数组访问越界以及访问null指针等)所导致的RuntimeException,以及程序本身没有问题而是因为其他原因导致的异常(典型的如I/O错误IOException)。
  通俗一点来说的话,就是RuntimeException异常必然是程序员所导致的问题。Error类或RuntimeException类的所有异常都被称为非受查异常,所有其他的异常都被称为受查异常。我们常说的异常处理,都是针对受查异常进行实现的

7.1.2 声明受查异常

  一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制,要么就应该避免发生。这里特别注意java的异常抛出与throws后面的息息相关,只会抛出throws说明了的受查异常,如果没写或有其他受查异常则不会抛出(注意是不抛出受查异常,非受查异常仍然会抛出,无法人为控制)。

7.1.3 如何抛出异常(如何抛出受查异常)

  两种方式,不过一定要记得在方法前加上throws说明符:

throw new IOException();

IOException e = new IOException();
throw e;

7.1.4 创建异常类

  从Exception或者Exception的子类进行派生即可,注意所派生的类需要实现两个构造器:默认的构造器和带有详细描述信息的构造器(使用super类的构造器实现):

class testException extends Exception
{
	public testException();
	public testException(String gripe)
	{
		super(gripe);
	}
}

7.2 捕获异常

  抛出和捕获是异常处理的两大过程。

7.2.1 抛出异常

  抛出异常的方法只负责抛出异常,异常捕获和处理由调用该方法的调用者去操心(使用try catch语句进行捕获与处理,或者在调用者方法前声明对应的throws说明符以继续传递异常)

7.2.2 捕获多个异常

  多个catch语句即可。

7.2.3 再次抛出异常和异常链

  java可以将原始异常设置为新异常的“原因”,使用这种包装技术,可以使用户抛出子系统中的高级异常,而不会丢失原始异常的细节(比如在不允许抛出受查异常的地方,将受查异常包装为运行时异常),当然也可以不用包装原始异常,直接用throw语句即可再次抛出(throw 原异常;):

Throwable e = se.getCause();
throw e;

7.2.4 finally子句

  不论异常是否被捕获,finally子句中的代码都将会被执行。这里务必注意finally子句的执行特殊性,其主要用于在其中关闭try块或者之前的资源,但是如果涉及到退出语句比如return之类的有意想不到的后果,在编写时务必注意。

7.2.5 带资源的try语句

  假设资源属于一个实现了AutoCloseable接口的类,Java SE7为这种代码模式提供了一个快捷的编写方式如下,在try块退出时会自动地调用AutoCloseable接口中的close()方法:

try(Scanner in = new Scanner(...)  //Scanner类比于各种资源类
{
	work with in;
}

7.2.6 分析堆栈轨迹元素

  堆栈轨迹是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特点位置。在实际使用中最简单的是通过所捕获的异常e,调用e的方法printStackTrace(),从而得到当前异常e的所有堆栈轨迹。
  而静态的Thread.getAllStackTrace()方法可以产生所有线程的堆栈轨迹(该方法获取的每个线程的堆栈轨迹数据结构为StackTraceElement[]数组,对于StackTraceElement类,可以通过其内部的toString函数直接获取想相关的信息)。

7.3 使用异常机制的技巧

  这里有个很好玩的事情,就是对于使用异常与不使用异常的党争问题,以及对于大量使用异常与少量使用异常的党争问题,我不配做过多评价,不过我支持多使用异常处理。

  1. 异常处理不能代替简单的测试;(异常捕获的耗时往往较长,尽量只在异常情况下使用)
  2. 不要过分地细化异常;(尽量使用总分形式,即try包代码块,后面的catch依次撰写)
  3. 利用异常层次结构;(让异常的结构尽可能的细化,便于异常问题定位)
  4. 不要压制异常;(可能抛出受查异常的代码块需要写throws表。但如果不想写throw表,也就是不想让调用本方法的调用者做try catch异常处理,则应该不写throws表,而是通过对可能抛出受查异常的代码块进行try catch异常处理,并通过Exception大类来进行捕获)
public Image loadImage(String s)
{
	try
	{
		//code that maybe throw checked exceptions
	}
	catch(Exception e)
	{
		//异常处理
	}
}
  1. 在检测错误时,“苛刻”要比放任更好;(大意就是尽可能用最符合实际情况的异常来抛出,即尽早抛出异常)
  2. 不要羞于传递异常;(让高层次的用户获取异常,便于统一的异常处理过程,即尽晚捕获异常)

7.4 使用断言

7.4.1 断言的概念

  断言是一种测试和调试阶段所使用的战术性工具,通过关键字assert来实现此功能,有两种形式如下。这两种形式都会对条件进行检测,如果结果为false,第一种形式会抛出一个AssertionError异常,第二种形式中的表达式会被传入AssertionError的构造器中后,再将AssertionError抛出。

assert 条件;
assert 条件 : 表达式;

7.4.2 启用和禁用断言

  在IDEA中,在项目配置的虚拟机选项里加入-enableassertions或-ea即可开启断言,加入(或者直接不写)-disableassertions或-da即可禁用断言,开启与禁用断言不需要重新编译程序,相关代码是否执行与类加载器直接相关。

7.4.3 使用断言完成参数检查

  断言失败是致命的、不可恢复的错误,断言检查只用于开发和测试阶段。只应该用于在测试阶段确定程序内部的错误位置。通俗来说,就是断言最好用于进行参数的格式检查,以此快速确定参数的格式问题(如前置条件 assert a != null;)。

7.4.4 为文档假设使用断言

  这里的意思是将注释中的假设情况通过断言的方式来实现,感觉都可以。

7.5 记录日志

  书本的意思是建议使用日志记录来代替传统的System.out.println这样的控制台打印语句,这样可以充分利用日志的统一管理能力,整体功能效果优于简单的打印。

7.5.1 基本日志

  如果当前程序较为简单,所有的数据都只需要记录到一个日志记录器中,那么使用全局日志记录器即可,具体如下,不过全局日志记录器也是可以设置记录级别的。

Logger.getGlobal().info("info");

7.5.2 高级日志

  其实也没有所谓的“高级”一说,就是通过对日志记录器进行创建与命名,从而通过不同的日志记录器来记录彼此特定的信息罢了。实现方式有两种,一种是在类的内部建立一个私有常量静态域,一种是通过Logger.getLogger()方法来不断调用。

//由于日志记录器往往不会被任何其他变量引用,所以如果不将其设为static类型,会导致可能在完成时被垃圾回收,导致不必要的麻烦
private static final Logger myLogger = Logger.getLogger("custom name");
Logger.getLogger("custom name").(调用方法);

  而在代码中对日志记录时,对应到日志本身的级别具体如下,从左到右级别依次降低。日志记录器默认只记录级别最高的三个级别,如果想要日志记录器记录对应级别的日志,最好将日志处理器的级别阈值进行重新设置。

SEVERE;WARNING;INFO;CONFIG;FINE;FINER;FINEST
myLogger.setLevel(Level.ALL);

  在进行实际的日志记录时,两种最常规的日志记录器调用方式如下,两种方式的效果相同。

myLogger.warning(message);
myLogger.log(Level.WARNING, message);

  在进行实际的日志记录时,跟踪执行流的调用方式如下,这些调用会生成FINER级别和以字符串ENTRY和RETURN开始的日志记录,通常用于方法的开始与结尾,代表方法的正常进入与退出

myLogger.entering("className", "methodName");
myLogger.entering("className", "methodName", Object param);
myLogger.entering("className", "methodName", Object[] params);

myLogger.exiting("className", "methodName");
myLogger.exiting("className", "methodName", Object result);

  在进行实际的日志记录时,记录异常与异常抛出的调用方式如下。其中异常的日志记录会记录异常发生的位置,与产生该异常的方法调用所在处;异常抛出的日志记录会得到一条FINER级别的记录(包括一个以THROW开始的信息),所以如果没有对日志记录级别进行配置,这条FINER级别的日志将不会被记录

myLogger.log(Level l, "message", Throwable t);
myLogger.throwing("className", "methodName", Throwable t);

7.5.3 修改日志管理器配置

  这是通过编辑文件来修改日志记录器的默认属性,不过操作较为繁琐。

7.5.4 本地化

  书本中意思是在请求日志管理器时指定资源包,这样就可以将一些特定词根据资源包的映射实现存储:

Logger logger = Logger.getLogger(loggerName, resourceBundleName);

7.5.5 处理器

  日志记录器在将日志记录后,会把所有的日志记录发送到自己的日志处理器以及自己的日志处理器的父处理器中进行处理,这里的所有日志处理器也有自己的处理级别,低于其处理级别的日志记录其不会处理。
  常用的日志处理器有控制台日志处理器ConsoleHandler(日志在控制台中记录),文件日志处理器FileHandler(日志在文件中记录),套接字日志处理器SocketHandler(日志通过套接字传输),以及流日志处理器StreamHandler(日志以流的形式记录),这些日志处理器通过new出自己的实例后,将实例添加到日志记录器的处理器列表中即可。对于每一种类型的日志处理器,都对应同一个最终的ConsoleHandler(控制台日志处理器)父处理器,这个最终处理器的处理级别为INFO及以上

FileHandler handler = new FileHandler();
myLogger.addHandler(handler);
  1. 对于控制台日志处理器ConsoleHandler。由于最终的ConsoleHandler(控制台日志处理器)父处理器存在,如果想要处理INFO级别以下的日志记录,则需要修改配置文件中的这个最终处理器的处理级别。不过由于配置文件更改繁琐,还有一种方法就是为了日志记录器安装一个低处理级别的自定义(new)控制台日志处理器,并设置日志记录器不向自己的处理器的父处理器发送日志记录(即阻止最终处理器接受数据),从而实现处理低级别记录日志的功能。
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);
  1. 对于文件日志处理器FileHandler,其在进行构造时有一系列的参数设计,比如append、limit、pattern等等,这些参数的设计将直接影响所储存文件的方式、位置与策略等等。
//%1为pattern参数,%2为append参数
FileHandler fileHandler = new FileHandler("%h/Desktop/simpleExcept%u.txt", true);
  1. 对于流日志处理器StreamHandler,最好是继承StreamHandler后拓展一个类,并自定义其中的publish、flush和close方法.。(比如处理器会缓存数据,并且只有在缓存区满的时候才把它们写入流中,因此覆盖publish方法的意义是每当处理器获取日志记录时,立即刷新缓冲区)

7.5.6 过滤器

  过滤器是日志记录器和日志处理器都可以使用的一种接口,在为两者添加过滤器时,通过设计一个实现了Filter接口中isLoggable方法的类(根据情况lambda表达式或者内部类应该都可以),再借助日志记录器/处理器的setFilter方法传参即可实现。

7.5.7 格式化器

  格式化器用于给处理器的输出进行格式调整,在为处理器添加格式化器时,需要拓展Formatter类并覆盖其中的format方法得到一个自定义类,再借助日志记录器的setFormatter方法传参即可实现。

7.5.8 日志记录说明

  总结而言,日志系统中的日志记录器、日志处理器、过滤器与格式化器的彼此关系如下图所示。
在这里插入图片描述
  “最常用”的日志创建操作具体如下:

  1. 为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字;
Logger logger = Logger.getLogger("com.mycompany.myapp");
private static final Logger logger = Logger.getLogger("com.mycompany.myapp");
  1. 默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台。用户可以覆盖默认的配置文件,但是改变配置的相关工作较多。因此,最好在应用程序中安装一个更加适宜的默认配置(也就是前面所写过的,自定义一个ConsoleHandler后添加给日志记录器,并关闭日志记录器的父处理器传递功能);
  2. 所有级别为INFO、WARNING和SEVERE的消息都将显示到控制台上(因为默认的最终处理器的处理级别就是这三个)。因此,最好只将对程序用户有意义的消息设置为这几个级别,而程序员想要的日志记录设置为FINE最佳,因为System.out.println()方法会生成一条FINE类型的日志。(这里所表达的意思为,日志记录器最好全级别记录,而文件日志处理器的处理级别最好设为FINE,这样就可实现INFO级别及以上的日志在控制台显示,FINE级别及以上的日志在文件中存储)

7.6 调试技巧

  这里主要讲了一些Java中的调试技巧:

  1. 打印或记录任意变量的值,无论变量是基本类型还是指针形式;
    System.out.println("x=" + x);
  2. 在每个类中设计一个单独的main方法用于单独测试;
  3. 使用JUnit做单元测试;
  4. 日志代理方法:在对象进行实例化时的操作时,重载需要检测的方法,在其中直接使用super类的方法实行功能的同时,添加相关的日志记录过程,从而实现对方法调用的日志截获;
  5. 使用Throwable类提供的printStackTrace方法,从任何一个异常对象中获得堆栈轨迹情况;
  6. 异常类中所得的堆栈轨迹处于System.err流(错误流)中,可以将其转换到一个字符串中进行记录或正常显示(错误流在控制台中显示为红色,输出流System.out在控制台中显示为黑/白色);
  7. 错误信息处于错误流中,最好将这些错误流的信息也单独保存到文件中(错误流和输出流的截取方式略有不同);
  8. 最好将非捕获异常的相关堆栈轨迹不要放在System.err流中跟随打印,而是只存入到文件中;
  9. 通过java -verbose可以启动Java虚拟机来观察类的加载过程;
  10. 使用-Xlint选项告诉编译器对一些普遍容易出现的代码问题进行检查;
  11. 运行jconsole程序对Java应用程序进行监控和管理
    jconsole processID
  12. 使用jmap工具获得一个堆的转储(也就是dmp文件);
  13. 使用-Xprof标志运行Java虚拟机,就会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法。

你可能感兴趣的:(Java学习)