其他人已经很好地总结了为什么提早抛出。让我专注于为什么迟到的原因,对此我还没有满意的解释。
为何例外?
关于为什么首先存在异常的情况似乎存在很大的困惑。让我在这里分享一个大秘密:异常的原因和异常处理是... ABSTRACTION。
您是否看到过这样的代码:
static int divide(int dividend, int divisor) throws DivideByZeroException {
if (divisor == 0)
throw new DivideByZeroException(); // that's a checked exception indeed
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
try {
int res = divide(a, b);
System.out.println(res);
} catch (DivideByZeroException e) {
// checked exception... I'm forced to handle it!
System.out.println("Nah, can't divide by zero. Try again.");
}
}
那不是应该使用异常的方式。上面的代码确实存在于现实生活中,但是它们更多的是一种异常,并且确实是例外(双关语)。例如,即使在纯数学中,除法的定义也是有条件的:总是必须处理特殊的零以限制输入域的“调用者代码”。它很丑。呼叫者总是很痛苦。不过,在这种情况下,先检查再做模式是很自然的做法:
static int divide(int dividend, int divisor) {
// throws unchecked ArithmeticException for 0 divisor
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
if (b != 0) {
int res = divide(a, b);
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
另外,您可以像这样在OOP风格上进行全面突击:
static class Division {
final int dividend;
final int divisor;
private Division(int dividend, int divisor) {
this.dividend = dividend;
this.divisor = divisor;
}
public boolean check() {
return divisor != 0;
}
public int eval() {
return dividend / divisor;
}
public static Division with(int dividend, int divisor) {
return new Division(dividend, divisor);
}
}
static void doDivide() {
int a = readInt();
int b = readInt();
Division d = Division.with(a, b);
if (d.check()) {
int res = d.eval();
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
如您所见,调用者代码具有预检查的负担,但是此后不执行任何异常处理。如果ArithmeticException
来自divide
或的调用来自eval
,则您必须执行异常处理并修复代码,因为您忘记了check()
。出于类似的原因,捕获a NullPointerException
几乎总是错误的事情。
现在有些人说他们想查看方法/函数签名中的特殊情况,即显式扩展输出域。他们是赞成检查异常的人。当然,更改输出域应强制任何直接调用者代码进行调整,而这确实可以通过检查异常来实现。但是您不需要例外!这就是为什么要使用Nullable<T>
泛型类,案例类,代数数据类型和联合类型的原因。一些面向对象的人甚至可能更喜欢返回 null
简单的错误情况,例如:
static Integer divide(int dividend, int divisor) {
if (divisor == 0) return null;
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
Integer res = divide(a, b);
if (res != null) {
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
从技术上讲,可以将异常用于上述目的,但这是重点:此类用法不存在异常。异常是专业的抽象。异常与间接有关。例外允许扩展“结果”域而不破坏直接的客户合同,并将错误处理推迟到“其他地方”。如果你的代码抛出这是在相同的代码直接调用方处理的异常,中间没有任何抽象层,那么你正在做它错
如何赶上后期?
所以我们到了。我已经指出,在上述情况下使用异常并不是如何使用异常。但是,存在一个真正的用例,其中异常处理提供的抽象和间接是必不可少的。了解此类用法也将有助于理解后期建议。
该用例是:针对资源抽象编程 ...
是的,应该针对抽象而不是具体的实现对业务逻辑进行编程。顶级IOC “接线”代码将实例化资源抽象的具体实现,并将其传递给业务逻辑。这里没有新内容。但是这些资源抽象的具体实现可能会抛出它们自己的实现特定的异常,不是吗?
那么谁可以处理那些实现特定的异常呢?那么,是否有可能在业务逻辑中处理任何特定于资源的异常?不,不是。业务逻辑是针对抽象编程的,抽象确实排除了那些实现特定异常细节的知识。
“ Aha!”,您可能会说:“但这就是为什么我们可以对异常进行子类化并创建异常层次结构”(请查看Spring先生!)。我告诉你,这是一个谬论。首先,每本有关OOP的合理书籍都指出具体继承是不好的,但是以某种方式,JVM的核心组件(异常处理)与具体继承紧密相关。具有讽刺意味的是,约书亚·布洛赫(Joshua Bloch)在获得可以使用的JVM的经验之前还无法写出他的《有效Java》一书,是吗?它更多地是下一代的“经验教训”书。其次,更重要的是,如果您捕获了高级异常,那么您将如何处理它?PatientNeedsImmediateAttentionException
:我们要给她吃药还是要砍断她的腿!?关于所有可能的子类的switch语句怎么样?您的多态性就到了,抽象就到了。你明白了。
那么谁可以处理资源特定的异常呢?一定是知道凝结物的人!实例化资源的人!当然,“接线”代码!看一下这个:
针对抽象编码的业务逻辑...没有混凝土资源错误处理!
static interface InputResource {
String fetchData();
}
static interface OutputResource {
void writeData(String data);
}
static void doMyBusiness(InputResource in, OutputResource out, int times) {
for (int i = 0; i < times; i++) {
System.out.println("fetching data");
String data = in.fetchData();
System.out.println("outputting data");
out.writeData(data);
}
}
同时,其他地方的具体实现...
static class ConstantInputResource implements InputResource {
@Override
public String fetchData() {
return "Hello World!";
}
}
static class FailingInputResourceException extends RuntimeException {
public FailingInputResourceException(String message) {
super(message);
}
}
static class FailingInputResource implements InputResource {
@Override
public String fetchData() {
throw new FailingInputResourceException("I am a complete failure!");
}
}
static class StandardOutputResource implements OutputResource {
@Override
public void writeData(String data) {
System.out.println("DATA: " + data);
}
}
最后是接线代码...谁处理具体的资源异常?一个了解他们的人!
static void start() {
InputResource in1 = new FailingInputResource();
InputResource in2 = new ConstantInputResource();
OutputResource out = new StandardOutputResource();
try {
ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
}
catch (FailingInputResourceException e)
{
System.out.println(e.getMessage());
System.out.println("retrying...");
ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
}
}
现在忍受我。上面的代码很简单。您可能会说您拥有一个具有多个范围的IOC容器管理资源的企业应用程序/ Web容器,并且需要自动重试和会话或请求范围资源的重新初始化等。较低级别范围的接线逻辑可能会提供给抽象工厂以创建资源,因此不知道确切的实现。只有更高级别的范围才真正知道那些更低级别的资源会引发哪些异常。现在等等!
不幸的是,异常仅允许在调用堆栈上进行间接访问,并且具有不同基数的不同作用域通常在多个不同线程上运行。没有办法通过例外进行沟通。这里我们需要更强大的功能。答:异步消息传递。在较低级别作用域的根目录捕获每个异常。不要忽略任何东西,不要让任何东西漏掉。这将关闭并处置在当前作用域的调用堆栈上创建的所有资源。然后,通过在异常处理例程中使用消息队列/通道将错误消息传播到更高范围的示波器,直到达到已知凝固的级别。那是知道如何处理它的人。
SUMMA SUMMARUM
因此,根据我的解释,在任何您不破坏抽象的最方便的地方,捕获迟到的手段即可捕获异常。不要太早赶上!在创建具体异常并抛出资源抽象实例的层捕获异常,该层知道资源的抽象。“接线”层。
HTH。编码愉快!