我认为您可能遇到过这样的情况:您运行代码并以类似NullPointerException、ClassCastException或更糟的方式结束......接下来是调试、分析、谷歌搜索等的漫长过程。异常是美妙的:它们表明问题的性质和发生的地方。如果您想重温记忆并了解更多信息,请查看这篇文章:异常:已检查、未检查和自定义。
也就是说,在某些情况下您可能需要创建自己的异常。例如,假设您的代码需要从由于某种原因不可用的远程服务请求信息。或者假设有人填写银行卡申请表并提供了一个电话号码,无论是否偶然,该号码已经与系统中的另一个用户相关联。
当然,这里的正确行为仍然取决于客户的要求和系统的体系结构,但我们假设您的任务是检查电话号码是否已在使用中,如果是则抛出异常。
让我们创建一个例外:
public class PhoneNumberAlreadyExistsException extends Exception {
public PhoneNumberAlreadyExistsException(String message) {
super(message);
}
}
接下来我们在执行检查时使用它:
public class PhoneNumberRegisterService {
List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");
public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
if (registeredPhoneNumbers.contains(phoneNumber)) {
throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
}
}
}
为了简化我们的例子,我们将使用几个硬编码的电话号码来表示一个数据库。最后,让我们尝试使用我们的异常:
public class CreditCardIssue {
public static void main(String[] args) {
PhoneNumberRegisterService service = new PhoneNumberRegisterService();
try {
service.validatePhone("+1-111-111-11-14");
} catch (PhoneNumberAlreadyExistsException e) {
// Here we can write to logs or display the call stack
e.printStackTrace();
}
}
}
现在是时候按Shift+F10(如果您使用的是 IDEA),即运行项目。这是您将在控制台中看到的内容:
exception.PhoneNumberAlreadyExistsException:指定的电话号码已被其他客户使用!
在 exception.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)
看着你!您创建了自己的异常,甚至对其进行了一些测试。祝贺您取得这一成就!我建议稍微试验一下代码以更好地理解它是如何工作的。
添加另一个检查 - 例如,检查电话号码是否包含字母。您可能知道,在美国经常使用字母来使电话号码更容易记住,例如 1-800-MY-APPLE。您的支票可以确保电话号码只包含数字。
好的,所以我们已经创建了一个已检查的异常。一切都会好起来的,但是……
编程社区分为两个阵营——支持检查异常的阵营和反对检查异常的阵营。双方都提出了有力的论据。两者都包括一流的开发人员:Bruce Eckel 批评检查异常,而 James Gosling 则为它们辩护。看来这件事永远也解决不了了。也就是说,让我们看看使用检查异常的主要缺点。
检查异常的主要缺点是它们必须被处理。这里我们有两个选择:要么使用try-catch就地处理它,要么,如果我们在许多地方使用相同的异常,则使用throws抛出异常,并在顶级类中处理它们。
此外,我们最终可能会得到“样板”代码,即占用大量空间但不会做太多繁重工作的代码。
在处理大量异常的相当大的应用程序中会出现问题:顶级方法上的抛出列表很容易增长到包含十几个异常。
开发人员通常不喜欢这样,而是选择了一个技巧:他们让所有已检查的异常继承一个共同的祖先—— ApplicationNameException。现在他们还必须在处理程序中捕获该(已检查!)异常:
catch (FirstException e) {
// TODO
}
catch (SecondException e) {
// TODO
}
catch (ThirdException e) {
// TODO
}
catch (ApplicationNameException e) {
// TODO
}
这里我们面临另一个问题——我们应该在最后一个catch块中做什么?上面,我们已经处理了所有预期的情况,所以此时ApplicationNameException对我们来说无非是“异常:发生了一些无法理解的错误”。我们是这样处理的:
catch (ApplicationNameException e) {
LOGGER.error("Unknown error", e.getMessage());
}
最后,我们不知道发生了什么。
但是我们不能像这样一次抛出所有异常吗?
public void ourCoolMethod() throws Exception {
// Do some work
}
是的,我们可以。但是“throws Exception”告诉我们什么?那东西坏了。您必须从上到下调查所有内容,并长时间熟悉调试器才能理解原因。
您可能还会遇到一种有时称为“异常吞噬”的构造:
try {
// Some code
} catch(Exception e) {
throw new ApplicationNameException("Error");
}
这里没有太多要添加的解释 - 代码使一切变得清晰,或者更确切地说,它使一切变得不清楚。
当然,您可能会说您不会在实际代码中看到这一点。好吧,让我们深入了解java.net包中URL类的内部(代码)。想知道就关注我吧!
这是URL类中的构造之一:
public URL(String spec) throws MalformedURLException {
this(null, spec);
}
如您所见,我们有一个有趣的已检查异常 — MalformedURLException。这是它可能被抛出的时间(我引用):
“如果没有指定协议,或者找到未知协议,或者 spec 为空,或者解析的 URL 不符合相关协议的特定语法。”
那是:
- 如果没有指定协议。
- 发现未知协议。
- 规范为null。
- URL 不符合相关协议的特定语法。
让我们创建一个创建URL对象的方法:
public URL createURL() {
URL url = new URL("https://codegym.cc");
return url;
}
只要您在 IDE 中编写这些行(我在 IDEA 中编码,但即使在 Eclipse 和 NetBeans 中也可以),您将看到:
这意味着我们需要抛出异常,或者将代码包装在try-catch块中。现在,我建议选择第二个选项来可视化正在发生的事情:
public static URL createURL() {
URL url = null;
try {
url = new URL("https://codegym.cc");
} catch(MalformedURLException e) {
e.printStackTrace();
}
return url;
}
如您所见,代码已经相当冗长了。我们在上面提到了这一点。这是使用未经检查的异常的最明显原因之一。
我们可以通过扩展Java 中的RuntimeException创建一个未经检查的异常。
未经检查的异常是从Error类或RuntimeException类继承的。许多程序员认为这些异常可以在我们的程序中处理,因为它们表示我们无法期望在程序运行时从中恢复的错误。
当发生未经检查的异常时,通常是由于代码使用不当,传入的参数为 null 或其他无效参数所致。
好吧,让我们写代码:
public class OurCoolUncheckedException extends RuntimeException {
public OurCoolUncheckedException(String message) {
super(message);
}
public OurCoolUncheckedException(Throwable cause) {
super(cause);
}
public OurCoolUncheckedException(String message, Throwable throwable) {
super(message, throwable);
}
}
请注意,我们为不同的目的制作了多个构造函数。这让我们可以为异常提供更多功能。例如,我们可以让异常给我们一个错误代码。首先,让我们创建一个枚举来表示我们的错误代码:
public enum ErrorCodes {
FIRST_ERROR(1),
SECOND_ERROR(2),
THIRD_ERROR(3);
private int code;
ErrorCodes(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
现在让我们向异常类添加另一个构造函数:
public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
super(message, cause);
this.errorCode = errorCode.getCode();
}
别忘了添加一个字段(我们差点忘了):
private Integer errorCode;
当然,还有一种获取此代码的方法:
public Integer getErrorCode() {
return errorCode;
}
让我们看一下整个班级,以便我们检查和比较:
public class OurCoolUncheckedException extends RuntimeException {
private Integer errorCode;
public OurCoolUncheckedException(String message) {
super(message);
}
public OurCoolUncheckedException(Throwable cause) {
super(cause);
}
public OurCoolUncheckedException(String message, Throwable throwable) {
super(message, throwable);
}
public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
super(message, cause);
this.errorCode = errorCode.getCode();
}
public Integer getErrorCode() {
return errorCode;
}
}
哒哒!我们的异常完成了!如您所见,这里没有什么特别复杂的。让我们实际检查一下:
public static void main(String[] args) {
getException();
}
public static void getException() {
throw new OurCoolUncheckedException("Our cool exception!");
}
当我们运行我们的小应用程序时,我们会在控制台中看到如下内容:
现在让我们利用我们添加的额外功能。我们将在之前的代码中添加一些内容:
public static void main(String[] args) throws Exception {
OurCoolUncheckedException exception = getException(3);
System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
throw exception;
}
public static OurCoolUncheckedException getException(int errorCode) {
return switch (errorCode) {
case 1:
return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
case 2:
return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}
}
您可以像处理对象一样处理异常。当然,我相信您已经知道 Java 中的一切都是对象。
看看我们做了什么。首先,我们更改了方法,它现在不会抛出异常,而是根据输入参数简单地创建一个异常。接下来,使用switch-case语句,我们生成带有所需错误代码和消息的异常。在 main 方法中,我们获取创建的异常,获取错误代码,然后抛出它。
让我们运行它,看看我们在控制台上得到了什么:
看——我们打印了从异常中得到的错误代码,然后抛出异常本身。更重要的是,我们甚至可以准确地跟踪抛出异常的位置。根据需要,您可以将所有相关信息添加到消息中,创建额外的错误代码,并为您的异常添加新功能。
嗯,你怎么看?我希望一切顺利!
一般来说,例外是一个相当广泛的话题,而且不是很明确。还会有更多的争论。例如,只有 Java 有检查异常。在最流行的语言中,我还没有看到使用它们的语言。
Bruce Eckel 在他的“Thinking in Java”一书的第 12 章中对异常写得非常好 — 我建议您阅读它!还可以看看 Horstmann 的“Core Java”的第一卷——它在第 7 章中也有很多有趣的东西。
一个小总结
-
将所有内容写入日志!在抛出的异常中记录消息。这通常会对调试有很大帮助,并让您了解发生了什么。不要将catch块留空,否则它只会“吞下”异常,您将没有任何信息来帮助您查找问题。
-
当涉及到异常时,一次捕获所有异常是不好的做法(正如我的一位同事所说,“这不是口袋妖怪,它是 Java”),因此请避免 catch (Exception e) 或更糟的是 catch ( Throwable t )。
-
尽早抛出异常。这是良好的 Java 编程习惯。当您研究像 Spring 这样的框架时,您会发现它们遵循“快速失败”原则。也就是说,它们尽可能早地“失败”,以便能够快速找到错误。当然,这带来了一定的不便。但这种方法有助于创建更健壮的代码。
-
在调用代码的其他部分时,最好捕获某些异常。如果被调用的代码抛出多个异常,那么只捕获这些异常的父类是一种糟糕的编程习惯。例如,假设您调用抛出FileNotFoundException和IOException的代码。在调用此模块的代码中,最好编写两个 catch 块来捕获每个异常,而不是编写一个catch 块来捕获Exception。
-
仅当您可以为用户和调试有效地处理异常时才捕获异常。
-
不要犹豫,编写您自己的异常。当然,Java 有很多现成的,适用于各种场合,但有时您仍然需要发明自己的“轮子”。但是您应该清楚地了解为什么要这样做,并确保标准的异常集不具备您所需要的。
-
当您创建自己的异常类时,请注意命名!您可能已经知道正确命名类、变量、方法和包非常重要。例外也不例外!:) 总是以单词Exception结尾,异常的名称应该清楚地传达它所代表的错误类型。例如,FileNotFoundException。
-
记录您的例外情况。我们建议为异常编写一个@throws Javadoc 标记。当您的代码提供任何类型的接口时,这将特别有用。而且您还会发现以后更容易理解您自己的代码。您怎么看,如何确定MalformedURLException是关于什么的?来自 Javadoc!是的,编写文档的想法并不是很吸引人,但相信我,当您在六个月后返回自己的代码时,您会感谢自己的。
-
释放资源并且不要忽略try-with-resources结构。
-
以下是总体总结:明智地使用异常。就资源而言,抛出异常是一个相当“昂贵”的操作。在许多情况下,使用简单且“成本较低”的if-else可能更容易避免抛出异常,而是返回一个布尔变量来表示操作是否成功。
将应用程序逻辑与异常联系起来也很诱人,而您显然不应该这样做。正如我们在文章开头所说的那样,异常是针对异常情况的,而不是预期的情况,并且有多种工具可以防止它们发生。特别是,有Optional来防止NullPointerException,或Scanner.hasNext等来防止IOException,read()方法可能抛出。
GO TO FULL VERSION