如何在 Java 中抛出异常
“是的!我的应用程序终于成功了!”我听到你说。我知道你曾经说过这句话——伴随着大量的内啡肽、认可和解脱——因为我在 25 年的 Java 编程生涯中已经说过数百次了。事实上,如果这是一个特别棘手的问题,我通常会欢呼、欢呼,甚至站起来打几个响指来庆祝(我是远程工作的)。
但很多时候,当我们说“它有效!”时,我们实际上是在说我们已经完成了“快乐之路”。我们已经考虑到,只有当我们的最终用户不犯错误并且我们的应用程序拥有无限资源且没有错误时,事情才会有效。我们最终知道这不是真的,但暂时否认这一点也很好。
事实是,通常一半以上的战斗都是在处理错误。对于 Java 来说,这意味着异常。
快乐之路与正确之路
让我们考虑以下 Java 应用程序,它要求最终用户输入两个数字并将它们相除:
java
public class Divider {
public static void main(String… args) {
Console console = System.console();
Double numerator = Double.valueOf(console.readLine(“Enter the numerator: “));
Double denominator = Double.valueOf(console.readLine(“Enter the denominator: “));
Double quotient = numerator / denominator;
System.out.printf(“The quotient is %f%n”, quotient);
}
}
快乐之路运行得很好:
bash
Enter the numerator: 3.2
Enter the denominator: 1.6
The quotient is: 2.000000
您能想到这个应用程序可能出现的问题吗?暂停一下,看看您能想到多少问题。
你发现了什么吗?以下是一些:
用户可能输入非数字的内容,从而导致“NumberFormatException”
用户可能输入零作为分母,导致出现“ArithmeticException”,并且
如果没有可用的控制台,则 `System#console` 可能返回 `null`,从而导致 `NullPointerException`
在本文中,我们将探讨前两个问题。
Java 中错误处理的基础知识
如果您查看“Double#valueOf”的 JavaDoc,您将看到以下解释:
Throws:
NumberFormatException - if the string cannot be parsed as an integer.
记住这些信息后,你应该能够认识到这次“Divider”运行中发生了什么:
bash
Enter the numerator: the numerator
Exception in thread "main" java.lang.NumberFormatException: For input string: "the numerator"
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.base/java.lang.Double.parseDouble(Double.java:651)
at java.base/java.lang.Double.valueOf(Double.java:614)
at com.pluralsight.joshcummings.Divider.main(Divider.java:8)
当用户输入无效输入时,“Double#valueOf”会引发异常。
结果,程序无法继续运行。这是因为异常会中断应用程序的自然流程。程序无法继续运行,因为它现在处于无效状态。
您还可以看到,虽然异常可以保护应用程序免于处于无效状态,但不处理它们意味着用户会获得糟糕且令人困惑的体验。
您现在看到的是异常及其堆栈跟踪,或者抛出异常的确切行,以及导致该行的一系列方法调用。这对于调试目的很有帮助,但对您的最终用户可能没什么帮助。
如何处理 Java 中的异常
我们可以通过处理异常来改善这种情况。考虑改进版的 `readLine`,它会不断询问用户,直到他们提供有效的输入:
java
private static Double readDouble(Console console, String message) {
while (true) {
Double value = Double.valueOf(console.readLine(message));
if (value is valid) {
return value;
} else {
System.err.println(“Sorry, that input was invalid, please try again”);
}
}
}
上述伪代码通过检查然后循环再次询问来处理错误。在 Java 中,执行此操作的方法是捕获异常,如下所示:
java
private static Double readDouble(Console console, String message) {
while (true) {
try {
return Double.valueOf(console.readLine(message));
} catch (NumberFormatException ex) {
System.err.println(“Sorry, that input was invalid, please try again”);
}
}
}
然后,我们可以将原来的 `main` 方法改为如下所示:
java
public class Divider {
public static void main(String… args) {
Console console = System.console();
Double numerator = readDouble(console, “Enter the numerator: “);
Double denominator = readDouble(console, “Enter the denominator: “);
Double quotient = numerator / denominator;
System.out.printf(“The quotient is %f%n”, quotient);
}
}
并且随后的行为更加用户友好且具有弹性:
bash
Enter the numerator: the numerator
Sorry, that input was invalid, please try again.
Enter the numerator: 3.2
Enter the denominator: …
如何传达用户错误
当我们想要自己中断应用程序的流程时,我们可以利用同样的原则。
例如,现在让我们添加一个专用的“除法”方法来解决除以零的问题。它可能看起来像这样:
java
private static Double divide(Double numerator, Double denominator) {
if (denominator == 0) {
return null;
}
return numerator / denominator;
}
这是通过返回标记值来处理错误的示例。虽然这确实可以防止应用程序进入无效状态,但它具有不中断应用程序流程的潜在缺点。这样的应用程序的行为如下:
java
Enter the numerator: 12
Enter the denominator: 0
The quotient is null
在我们的例子中,由于输入无效,我们不想继续执行应用程序的其余部分。因此,我们不会返回标记值,而是抛出 Java 的“ArithmeticException”,在过程中提供有用的消息:
java
private static Double divide(Double numerator, Double denominator) {
if (denominator == 0) {
throw new ArithmeticException(“Sorry, you cannot divide by zero.”);
}
return numerator / denominator;
}
然后,你猜对了,我们需要处理它,以便最终用户看到有用的消息,而不是令人困惑的堆栈跟踪,如下所示:
java
public class Divider {
public static void main(String… args) {
Console console = System.console();
Double numerator = readDouble(console, “Enter the numerator: “);
Double denominator = readDouble(console, “Enter the denominator: “);
try {
Double quotient = divide(numerator, denominator);
System.out.printf(“The quotient is %f%n”, quotient);
} catch (ArithmeticException ex) {
System.err.println(ex.getMessage());
}
}
}
接下来,让我们通过以下两种方式增强`divide`方法:
使错误案例自我记录
添加自定义例外以进一步明确
如何记录异常
众所周知,方法签名说明了调用该方法时需要哪些输入以及预期输出。它还可以描述可能抛出的异常。
如果将“divide”方法签名更改为如下形式:
java
private Double divide(Double numerator, Double denominator) throws ArithmeticException
这有助于向程序员和 IDE 阐明您的方法的潜在危险,而无需检查方法主体(他们可能无法访问)。
现在,给定方法可能会抛出很多异常,而这些异常与您的业务逻辑完全无关。一个著名的异常是“NullPointerException”,这是 Java 在您尝试操作空引用时抛出的异常。其他类似的异常是“IllegalArgumentException”和“IllegalStateException”。在方法签名中声明这些异常并不能提供有关可能发生的错误类型的更多信息。因此,将它们添加到方法签名中是没有帮助的。
但是,每当异常是业务逻辑的重要表达时,就应该将其放在方法签名中以确保清晰。
如何抛出自定义异常
让我们通过创建自定义异常来进一步说明这一原则。异常只是一个 Java 类,这意味着它可以被扩展。通常,异常被扩展有两个原因:
不同的类型可以放在不同的 catch 块中
扩展的类名可以更清楚地说明哪里出了问题
要查看第二个函数的实际作用,请考虑以下扩展“ArithmeticException”的自定义异常:
java
public class DivideByZeroException extends ArithmeticException {
public DivideByZeroException() {
super(“Sorry, you cannot divide by zero.”);
}
}
完成此操作后,我们可以像这样改变除法:
java
private static Double divide(Double numerator, Double denominator) throws DivideByZeroException {
if (denominator == 0) {
throw new DivideByZeroException();
}
return numerator / denominator;
}
您会注意到两个变化。首先,我们抛出了自定义异常。其次,我们的方法签名对于可能出现的问题更具描述性。
现在您已经了解了足够的知识,可以做一些更高级的事情了!让我们对应用程序进行另一项更改,以说明抛出异常如何影响编程流程以及一些与已检查和未检查异常相关的最佳实践。
如何传播异常
假设我们引入了一个名为“run”的方法,它将运行我们的程序。如果我们想控制程序何时运行以及运行多少次,这可能会很有用。以下是我们新的“main”和“run”方法可能的样子:
java
public static void main(String… args) {
try {
run();
} catch (Exception ex) { // a trick to catch all exceptions!
System.err.printf(“An error occurred: %s%n“, ex.getMessage());
}
}
private static void run() {
Console console = System.console();
Double numerator = readDouble(console, “Enter the numerator: “);
Double denominator = readDouble(console, “Enter the denominator: “);
Double quotient = divide(numerator, denominator);
System.out.printf(“The quotient is %f%n”, quotient);
}
// …
请注意,在“divide”调用周围不再有“catch”块。这是因为 Java 会自动将异常传播到它找到的第一个处理该异常或该异常的超类的“catch”块。
换句话说,当我们的“DivideByZeroException”被抛出时,Java 会将其传递给“run”方法。如果“run”不处理它,它会转到堆栈跟踪中的下一个方法“main”。如果看到那里有一个处理“Exception”(“DivideByZeroException”的超类)的“catch”块,Java 会将程序流移到那里。
您可以通过分析堆栈跟踪的变化来看到这一点:
Exception in thread "main" com.pluralsight.joshcummings.DivideByZeroException: Sorry, you cannot divide by zero
at sample.config.Divider.divide(Divider.j
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~