Java从零开始系列05:异常、断言和日志

学习目标

  • 处理错误
  • 捕获异常
  • 使用异常的技巧
  • 使用断言
  • 日志

遇到异常时,应该做到以下几点:

  • 向用户通知错误
  • 保存所有的工作
  • 允许用户妥善地退出程序

对于异常情况,Java使用了一种称为异常处理(exception handing)的错误捕获机制。

一、处理错误

用户期望错误出现时,程序能够采取合理的行为。如果由于出现错误而使得某些操作没有完成,程序应该:

  • 返回到一种安全状态,并能够让用户执行其他的命令;或者:
  • 允许用户保存所有工作的结果,并以妥善的方式终止程序。

异常处理的任务就是将控制权从错误的地方转移到能够处理这种情况的错误处理器。需要考虑以下情况:

  • 用户输入错误
  • 设备错误
  • 物理限制
  • 代码错误

对于方法的错误,传统的方法是返回一个特殊的错误码,由调用方法分析。在Java中,如果某个方法出现错误,可以抛出(throw)一个封装了错误信息的对象。这个方法会立即退出,并不返回正常值。异常处理机制开始搜索能够处理这种异常情况的异常处理器(exception handler)。

(一)异常分类

异常对象都是派生于Throwable类的一个类实例。Throwable类下层是Error和Exception。

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

Exception层次结构派生为RuntimeException和其他异常。一般规则是:由编程错误导致的异常属于Runtime Exception;如果程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。

派生于RuntimeExcption的异常包括以下问题:

  • 错误的强制类型转换
  • 数组访问越界
  • 访问null指针
    其他异常包括:
  • 试图超越文件末尾继续读取数据
  • 试图打开一个不存在的文件
  • 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在

Java将派生于Error类或RuntimeException类的所有异常称为非检查型(unchecked)异常,所有其他的异常称为检查型(checked)异常。

(二)声明检查型异常

声明异常时,不必声明这个方法可能抛出的所有异常。以下4种情况会抛出异常:

  • 调用了一个抛出检查型异常的方法。如,FIleInputStream构造器:public FileInputStream(String name) throws FileNotFoundExxception
  • 检测到一个错误,并利用throw语句抛出一个检查型异常
  • 程序出现错误
  • java虚拟机或运行时库出现内部错误

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

有些Java方法包含在对外提供的类中,对于这些方法,应该通过方法首部的异常规范(exception specification)声明这个方法可能抛出异常。

class MyAnimation
{public Image loadImage(String s) throws IOException
	{}
}

如果一个方法有可能抛出多个检查型异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。如:

class MyAnimation
{public Image loadTmage(String s) throws FileNotFoundException, EOFExcepton
	{}
}

不需要声明Java的内部错误,即从Error继承的异常。任何程序代码都可能抛出那些异常,而我们对此完全无法控制。

同样,也不应该从Runtime Exception继承那些非检查型异常。

一个方法必须声明所有可能抛出的异常型异常,而非检查型异常要么在你的控制之外(Error),要么是由从一开始就应该避免的情况所导致的(RuntimeException)。

如果在子类中覆盖子超类的一个方法,子类方法中声明的检查类型异常不能比超类方法中声明的异常更加通用。如果超类方法没有抛出任何检查型异常,子类也不能超出任何检查型异常。

如果类中的一个方法声明它会抛出一个异常,而这个异常是某个特定类的实例,那么这个方法抛出的异常可能属于这个类,也可能属于这个类的任意一个。

(三)如何抛出异常

可以通过throw语句抛出异常:throw new EOFException

var e = new EOFException();
throw e;

(四)创建异常类

自定义异常需要包含两个构造器,一个是默认的构造器,一个是包含详细信息的构造器:

class FileFormatException extends IOException
{
	public FileFormatException()	{}
	public FileFormatException()
	{
		super(gripe);
	}
}

二、捕获异常

可以使用try/catch语句块捕获异常:

try
{
	code
}
catch (ExceptionType e)
{
	handler
}

可以使用多个catch语句捕获多个异常或一个catch捕获多种异常。只有当捕获的异常类型彼此之间不存在子类关系时才需要第二种。

可以使用catch子句抛出一个异常,把原始异常设为新异常的原因:

try
{
	access the database
}
catch (SQLException original)
{
	var e = new ServletException(“database error”);
	e.initCause(original);
	throw e;
}

捕获这个异常时,可以使用以下语句获取原始异常:
Throwable original = caughtException.getCause();

代码抛出异常时,会终止后续代码,需要清理已经获取的资源,可以使用finally语句

try
{
	code
}
catch (IOException e)
{
	show error message
}
finally
{
	in.close();
}

try语句可以只有finally语句,而没有catch子句。

相比于finally,现在更多使用try-with-Resources语句:

try (Resource res =)
{
	work with res
}

try块退出时,会自动调用res.close

堆栈轨迹(stack trace)是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息:

StackWalker walker = StackWalker.getInstance();
walker.forEach(frame -> analyze frame)

如果想要以懒方式处理Stream<StackWalker.StackFrame>,可以调用:
walker.walk(stream -> process stream)

三、异常的使用技巧

下面给出异常的一些技巧:

  1. 异常处理不能代替简单的测试
  2. 不要过分地细化异常
  3. 充分利用异常层次结构
  4. 不要压制异常
  5. 在检测错误时,“苛刻”要比放任好
  6. 不要羞于传递异常

四、断言

断言机制允许在测试期间向代码中插入一些检查,而在生产代码中自动删除这些检查:
assert condition;

assert condition : expression;
语句会计算条件,如果为false,抛出一个AssertionError异常。第二个语句,表达式将传入AssertError对象的构造器,并转换成一个消息字符串。

(一)启用或禁用断言

默认情况下断言是禁用的,可以使用 -enableassertions 或 -ea 选项启用断言:
java -enableassertions MyApp

五、日志

日志API具有以下优点:

  • 可以很容易地取消全部日志记录,或者仅仅取消某个级别以下的日志。可以很容易地再次打开日志开关。
  • 可以很简单地禁止日志记录
  • 日志记录可以被定向到不同的处理器
  • 日志记录器和处理器都可以对记录进行过滤
  • 日志记录可以采用不同的方式格式化
  • 应用程序可以使用多个日志记录器,它们使用与包命名类似的有层次结构的名字
  • 日志系统的配置由配置文件控制。

(一)基本日志

要生成简单的记录,可以使用全局日志记录器(global logger)并调用其info方法

Logger.getGlobal().info(File -> Open menu item selected”);

调用

Logger.getGlobal().setLevel(Level.OFF);

会取消所有日志

(二)高级日志

可以定义自己的日志记录器。可以调用getLogger方法创建或获取日志记录器:

private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”);

日志记录器也具有层次结构,与包不同,日志记录器的父与子之间将共享语义关系。子日志记录器会继承父日志记录器的日志级别。

通常,有以下7个日志级别:

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

默认情况下只记录前3个级别,可以设置不同的级别:

logger.setLevel(level.FINE);

FINE以及所有更高级别的日志都会记录。

也可以使用 Level.ALL 开启所有级别的记录或使用 Level.OFF 关闭所有级别的记录。

所有级别都有日志记录方法。如:logger.warning(message);

或者使用log方法制定级别,如:logger.log(Level.FINE, message);

默认日志记录将显示根据调用堆栈得出的包含日志调用的类名和方法名。如果虚拟机对执行过程进行了优化,就得不到准确的调用信息。可以使用logp方法获得调用类和方法的确切位置,这个方法的签名为:

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)

如:

int read(String file, String pattern)
{
	logger.entering(com.mycompany.mylib.Reader, “read”,
		new Object[] { file, pattern});
	…
	logger.exiting(com.mycompany.mylib.Reader, “read”, count);
	return count;
}

将生产FINER级别且以字符 ENTRY 和 RETURN 开头的日志记录。

日志记录的常见用途是记录那些预料之外的异常。可以使用下面两个便利方法在日记记录中包含异常的描述:
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);
}

throwing调用可以记录一条FINER级别的日志记录和一条以THROW开始的消息。

日志文件在虚拟机启动时初始化,也就是main方法执行前。


参考资料:

狂神说Java
Java核心技术 卷I(第11版)


上一章:Java从零开始系列04:接口、lambda表达式与内部类
下一章:Java从零开始系列06:泛型程序设计

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值