throwable
本文是有关异常的教程。 但不是通常的一种。 其中有许多内容可以告诉您异常的含义,如何抛出异常,捕获异常,已检查异常和运行时异常之间的区别,等等。 没有必要了。 这对您来说也很无聊。 如果没有,那么请阅读其中的一本,并在您了解他们所教的内容后再回来。 本文从这些教程的结尾处开始。 我们将更深入地研究Java异常,您可以使用它们做什么,应该使用它们做什么以及它们可能没有听说的功能。 如果setStackTrace()
, getCause()
和getSuppressed()
是您早餐时使用的方法,则可以跳过本文。 但是,如果不是这样,并且您想对此有所了解,请继续。 这篇文章很长。 写作花了很长时间,而阅读花了很长时间。 这是必需的。
介绍
在本文中,我们将讨论异常以及Java异常可以做什么以及应该做什么。 最简单的情况是抛出一个然后捕获它,但是存在更复杂的情况,例如设置原因或抑制异常。 我们将探讨这些可能性,以及更多其他可能性。 为了发现可能性,我们将开发一个简单的应用程序,并逐步创建四个版本,进一步开发该应用程序,并使用越来越多的异常处理可能性。 源代码在存储库中可用:
https://github.com/verhas/BLOG/tree/master/exception_no_stack
不同的版本在不同的Java包中。 一些在不同版本中未更改的类要高一包,并且不会被版本化。
- 第一个版本
v1
只会引发en异常,并且应用程序不会对其进行处理。 测试代码期望测试设置抛出异常。 此版本是演示为什么我们需要更复杂的解决方案的基准。 我们将体验到,没有足够的信息来了解实际问题发生在哪里的异常。 - 第二个版本
v2
在更高级别上捕获了该异常,并引发了一个新异常,并提供了有关异常情况的更多信息,并且新异常中嵌入了原始异常作为原因。 这种方法可以提供足够的信息来跟踪问题的位置,但是甚至可以对其进行增强,以便于阅读和识别实际问题。 -
v3
将演示我们如何修改新异常的创建,以便更高级别的异常的堆栈跟踪不会指向捕获原始异常的位置,而是指向引发原始异常的位置。 - 最后,第四版
v4
将演示在异常情况下即使可能无法成功完成操作也可以继续处理时如何抑制表达式。 这种“更进一步”使最后可能有一个异常,该异常收集有关所有发现的特殊情况的信息,而不仅仅是第一次出现的信息。
如果您查看代码,还将在此找到本文的原始文本,以及有助于维护代码段的设置,这些代码段将其从源代码复制到文章中,从而使所有代码段都保持最新。 对我们有用的工具是Java :: Geci。
样品申请
我们使用异常来处理超出程序正常流程的范围。 引发异常时,程序的正常流程将中断,并且执行将停止将异常转储到某些输出。 也可以使用语言中内置的try
and catch
命令对来捕获这些异常。
try {
... some code ...
... even calling methods
several level deep ...
... where exception may be thrown ...
} catch (SomeException e){
... code having access to the exception object 'e'
and doing someting with it (handling) ....
}
异常本身是Java中的对象,并且可以包含很多信息。 当我们在代码中捕获异常时,我们可以访问异常对象,并且代码可以在特殊情况下也可以访问异常对象所携带的参数,从而采取行动。 可以实现我们自己的扩展Java的异常
java.lang.Throwable
类或直接或传递扩展Throwable
某些类。 (通常,我们扩展Exception
类。)我们自己的实现可以包含许多描述异常情况性质的参数。 我们为此目的使用对象字段。
尽管异常可以携带的数据没有限制,但通常所包含的信息和堆栈跟踪不超过一个。 在Throwable
类中定义了其他参数的空间,例如导致当前参数的异常( getCause()
)或一系列抑制异常( getSuppressed()
)。 很少使用它们,可能是因为开发人员不了解这些功能,并且因为大多数情况很简单,不需要这些可能性。 我们将在本文中介绍这些可能性,以使您不属于仅因为他们不了解这些方法而不使用这些方法的无知开发人员。
我们有一个示例应用程序。 它不仅仅是在catch
分支中引发,捕获和处理异常,该异常使代码得以继续。 这很简单,并且在您第一次学习Java编程时已阅读的教程中对此进行了解释。
我们的示例应用程序将更加复杂。 我们将在目录中列出文件,读取行,并计算wtf
字符串的数量。 通过这种方式,我们可以自动执行代码审查过程质量评估(开玩笑)。 可以说,代码质量与代码审查期间的WTF数量成反比。
解决方案包含
- 可以列出文件的
FileLister
, - 可以读取文件的
FileReader
, - 一个
LineWtfCounter
,它将在一行中计算wtf
, - 一个
FileWtfCounter
,它将使用上一个类对列出行的整个文件中的所有wtf
进行计数,最后, - 一个
ProjectWtfCounter
,它使用文件级计数器对整个项目中的wtf
进行计数,列出所有文件。
版本1,投掷
该应用程序的功能非常简单,并且因为我们专注于异常处理,所以实现也很简单。 例如,文件列表类很简单,如下所示:
package javax0.blog.demo.throwable; import java.util.List; public class FileLister {
public FileLister() {
}
public List<String> list() {
return List.of( "a.txt" , "b.txt" , "c.txt" );
} }
文件系统中有三个文件a.txt
, b.txt
和c.txt
。 当然,这是一个模拟,但是在这种情况下,我们不需要更复杂的方法来演示异常处理。 同样, FileReader
也是一种模拟实现,仅用于演示目的:
package javax0.blog.demo.throwable.v1; import java.util.List; public class FileReader {
final String fileName;
public FileReader(String fileName) {
this .fileName = fileName;
}
public List<String> list() {
if (fileName.equals( "a.txt" )) {
return List.of( "wtf wtf" , "wtf something" , "nothing" );
}
if (fileName.equals( "b.txt" )) {
return List.of( "wtf wtf wtf" , "wtf something wtf" , "nothing wtf" );
}
if (fileName.equals( "c.txt" )) {
return List.of( "wtf wtf wtf" , "wtf something wtf" , "nothing wtf" , "" );
}
throw new RuntimeException( "File is not found: " + fileName);
} }
计算一行中wtf
出现次数的计数器是
package javax0.blog.demo.throwable.v1; public class LineWtfCounter {
private final String line;
public LineWtfCounter(String line) {
this .line = line;
}
public static final String WTF = "wtf" ;
public static final int WTF_LEN = WTF.length();
public int count() {
if (line.length() == 0 ) {
throw new LineEmpty();
}
// the actual lines are removed from the documentation snippet
} }
为了节省空间并专注于我们的主题,代码段不显示实际的逻辑(由Java :: Geci自动删除)。 读者可以创建一个代码,该代码实际计算字符串中wtf
子字符串的数量,或者简单地计算“ wtf”。 即使读者不能编写这样的代码,也可以从本文开头提到的存储库中获得。
我们应用程序中的逻辑说,如果文件中的一行长度为零,则这是一种特殊情况。 在这种情况下,我们抛出异常。
通常,这种情况并不能证明是一个例外,我承认这是一个虚构的示例,但是我们需要一些简单的方法。 如果行的长度为零,则抛出LineEmpty
异常。 (我们没有列出LineEmpty
异常的代码。它在代码存储库中,它很简单,没什么特别的。它扩展了RuntimeException
,无需声明我们将其放在何处。)如果您查看FileReader
的模拟实现,则您会看到我们在文件c.txt
中插入了空行。
使用行级计数器的文件级计数器如下:
package javax0.blog.demo.throwable.v1; public class FileWtfCounter {
// fileReader injection is omitted for brevity
public int count() {
final var lines = fileReader.list();
int sum = 0 ;
for ( final var line : lines) {
sum += new LineWtfCounter(line).count();
}
return sum;
} }
(同样,从打印输出中跳过了一些琐碎的行。)
这是该应用程序的第一个版本。 它没有任何特殊的异常处理。 它只是对行计数器返回的值求和,如果较低级别有异常,则行wtf
计数器会自动向上传播。 在此级别上,我们不会以任何方式处理该异常。
项目级别计数器非常相似。 它使用文件计数器并对结果求和。
package javax0.blog.demo.throwable.v1; import javax0.blog.demo.throwable.FileLister; public class ProjectWftCounter {
// fileLister injection is omitted for brevity
public int count() {
final var fileNames = fileLister.list();
int sum = 0 ;
for ( final var fileName : fileNames) {
sum += new FileWtfCounter( new FileReader(fileName)).count();
}
return sum;
} }
我们使用简单的测试代码对其进行测试:
package javax0.blog.demo.throwable.v1; import javax0.blog.demo.throwable.FileLister; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; public class TestWtfCounter {
@Test
@DisplayName ( "Throws up for a zero length line" )
void testThrowing() {
Throwable thrown = catchThrowable(() ->
new ProjectWftCounter( new FileLister())
.count());
assertThat(thrown).isInstanceOf(LineEmpty. class );
thrown.printStackTrace();
} }
单元测试通常不应具有堆栈跟踪打印。 在这种情况下,我们可以演示所抛出的内容。 错误中的堆栈跟踪将向我们显示以下错误:
javax0.blog.demo.throwable.v1.LineEmpty: There is a zero length line
at javax0.blog.demo.throwable.v1.LineWtfCounter.count(LineWtfCounter.java:18)
at javax0.blog.demo.throwable.v1.FileWtfCounter.count(FileWtfCounter.java:19)
at javax0.blog.demo.throwable.v1.ProjectWftCounter.count(ProjectWftCounter.java:22)
at javax0.blog.demo.throwable.v1.TestWtfCounter.lambda$testThrowing$0(TestWtfCounter.java:18)
at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:62)
...
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
这个异常有点问题。 当我们使用此代码时,它不会告诉我们有关有问题的实际文件和行的任何信息。 如果有一个空文件,我们必须检查所有文件和所有行。 为此编写一个应用程序并不是太困难,但是我们不想代替创建该应用程序的程序员来工作。 如果有例外,我们希望该例外能够为我们提供足够的信息,以成功解决该情况。 应用程序必须告诉我哪个文件和哪一行有问题。
版本2,设置原因
为了在异常中提供信息,我们必须收集信息并将其插入异常中。 这是我们在第二版应用程序中所做的。
第一个版本中的异常不包含文件名或行号,因为代码未将其放在此处。 该代码有这样做的充分理由。 引发异常的位置的代码没有信息,因此无法将其没有的信息插入异常。
一种有利可图的方法是将这些信息与其他参数一起传递,以便在发生异常时代码可以将此信息插入到异常中。 我不推荐这种方法。 如果您查看我在GitHub上发布的源代码,则可能会找到这种做法的示例。 我不为他们感到骄傲,对不起。
通常,我建议异常处理不应干扰应用程序的主数据流。 必须将其分开,因为这是一个单独的问题。
解决方案是在多个级别上处理异常,在每个级别上添加实际可用的信息。 为此,我们修改了FileWtfCounter
和ProjectWftCounter
类。
ProjectWftCounter
的代码如下:
package javax0.blog.demo.throwable.v2; public class FileWtfCounter {
// some lines deleted ...
public int count() {
final var lines = fileReader.list();
int sum = 0 ;
int lineNr = 1 ;
for ( final var line : lines) {
try {
sum += new LineWtfCounter(line).count();
} catch (LineEmpty le){
throw new NumberedLineEmpty(lineNr,le);
}
lineNr ++;
}
return sum;
} }
该代码捕获了向空行发出信号的异常并引发了一个新的异常,该异常已经具有一个参数:该行的序列号。
此异常的代码LineEmpty
那样琐碎,因此在此处列出:
package javax0.blog.demo.throwable.v2; public class NumberedLineEmpty extends LineEmpty {
final protected int lineNr;
public NumberedLineEmpty( int lineNr, LineEmpty cause) {
super (cause);
this .lineNr = lineNr;
}
@Override
public String getMessage() {
return "line " + lineNr + ". has zero length" ;
} }
我们将行号存储在int
字段中,该字段为final
。 我们这样做是因为
- 尽可能使用
final
变量 - 如果可能,在对象上使用基元
- 尽可能长时间以原始形式存储信息,因此不限制其使用
前两个标准是通用的。 尽管不是特定于异常处理,但最后一种在这种情况下是特殊的。 但是,当我们处理异常时,仅生成包含行号的消息而不是使异常类的结构复杂化是非常有利可图的。 毕竟,我们永远不会的推理
除了将异常打印到屏幕上之外,将异常用于任何其他用途。 或不? 这取决于。 首先,永远不要说永远。 再三考虑:如果我们将行号编码到消息中,那么可以肯定的是,除了将其打印给用户之外,我们绝不会将其用于任何其他用途。 那是因为我们不能将它用于其他任何用途。 我们限制自己。 今天的程序员限制了将来的程序员对数据做有意义的事情。
您可能会争辩说这是YAGNI 。 当我们要使用它时,我们应该关心将行号存储为整数,并且在此刻关心它还为时过早,这只是浪费时间。 你是对的! 同时,创建额外字段和计算异常信息的文本版本的getMessage()
方法的人也是正确的。 有时,YAGNI与精心设计的良好风格之间的界限很细。 YAGNI是为了避免以后不再需要的复杂代码(除了在创建代码时,您认为自己会需要)。 在此示例中,我认为上述带有一个额外的int
字段的异常不是“复杂”的。
我们在“项目”级别有一个类似的代码,在这里我们处理所有文件。 ProjectWftCounter
的代码将是
package javax0.blog.demo.throwable.v2; import javax0.blog.demo.throwable.FileLister; public class ProjectWftCounter {
// some lines deleted ...
public int count() {
final var fileNames = fileLister.list();
int sum = 0 ;
for ( final var fileName : fileNames) {
try {
sum += new FileWtfCounter( new FileReader(fileName)).count();
} catch (NumberedLineEmpty nle) {
throw new FileNumberedLineEmpty(fileName, nle);
}
}
return sum;
} }
在这里,我们知道文件的名称,因此我们可以扩展信息,将其添加到异常中。
FileNumberedLineEmpty
异常也类似于NumberedLineEmpty
的代码。 这是FileNumberedLineEmpty
的代码:
package javax0.blog.demo.throwable.v2; public class FileNumberedLineEmpty extends NumberedLineEmpty {
final protected String fileName;
public FileNumberedLineEmpty(String fileName, NumberedLineEmpty cause) {
super (cause.lineNr, cause);
this .fileName = fileName;
}
@Override
public String getMessage() {
return fileName + ":" + lineNr + " is empty" ;
} }
现在,我将吸引您关注这样一个事实,即我们创建的异常也属于继承层次结构。 随着我们收集和存储的信息的扩展,它们扩展了另一个,因此:
FileNumberedLineEmpty - extends -> NumberedLineEmpty - extends -> LineEmpty
如果使用这些方法的代码期望并尝试处理LineEmpty
异常,那么即使我们抛出更详细和专门的异常,它也可以执行。 如果代码想要使用额外的信息,那么最终,它必须知道实际实例不是LineEmpty
而是更专业的NumberedLineEmpty
或FileNumberedLineEmpty
。 但是,如果只想将其打印出来,则获得消息,然后将异常作为LineEmpty
的实例来处理是绝对好的。 即使这样,由于OO编程多态性,消息仍将包含人类可读形式的额外信息。
吃的时候就是布丁的证明。 我们可以通过简单的测试运行代码。 测试代码与以前的版本相同,唯一的例外是预期的异常类型为FileNumberedLineEmpty
而不是LineEmpty
。 但是,打印输出很有趣:
javax0.blog.demo.throwable.v2.FileNumberedLineEmpty: c.txt:4 is empty
at javax0.blog.demo.throwable.v2.ProjectWftCounter.count(ProjectWftCounter.java:22)
at javax0.blog.demo.throwable.v2.TestWtfCounter.lambda$testThrowing$0(TestWtfCounter.java:17)
at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:62) ...
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58) Caused by: javax0.blog.demo.throwable.v2.NumberedLineEmpty: line 4. has zero length
at javax0.blog.demo.throwable.v2.FileWtfCounter.count(FileWtfCounter.java:21)
at javax0.blog.demo.throwable.v2.ProjectWftCounter.count(ProjectWftCounter.java:20)
... 68 more ... 68 Caused by: javax0.blog.demo.throwable.v2.LineEmpty: There is a zero length line
at javax0.blog.demo.throwable.v2.LineWtfCounter.count(LineWtfCounter.java:15)
at javax0.blog.demo.throwable.v2.FileWtfCounter.count(FileWtfCounter.java:19)
... 69 more ... 69
我们可以对这个结果感到满意,因为我们立即看到导致问题的文件是c.txt
,第四行是罪魁祸首。 另一方面,当我们想看看引发异常的代码时,我们不会感到高兴。 在将来的某个时候,我们可能不记得为什么一条线的长度不能为零。 在这种情况下,我们想看一下代码。 在那里,我们只会看到捕获并重新抛出异常。 幸运的是,这是有原因的,但是实际上直到到达LineWtfCounter.java:15
的真正问题的代码为止,这实际上是三个步骤。
有人会对捕获和抛出异常的代码感兴趣吗? 也许是的。 也许没有。 在我们的案例中,我们决定将不会有人对该代码感兴趣,而不是处理一长串列出有罪原因的异常链,而是将异常的堆栈跟踪更改为引发异常的堆栈跟踪
例外。
版本3,设置堆栈跟踪
在此版本中,我们仅更改以下两个异常的代码: NumberedLineEmpty
和FileNumberedLineEmpty
。 现在,他们不仅扩展了彼此,又扩展了另一个LineEmpty
而且还将自己的堆栈跟踪设置为引起异常的值。
这是NumberedLineEmpty
的新版本:
package javax0.blog.demo.throwable.v3; public class NumberedLineEmpty extends LineEmpty {
final protected int lineNr;
public NumberedLineEmpty( int lineNr, LineEmpty cause) {
super (cause);
this .setStackTrace(cause.getStackTrace());
this .lineNr = lineNr;
}
// getMessage() same as in v2
@Override
public Throwable fillInStackTrace() {
return this ;
} }
这是FileNumberedLineEmpty
的新版本:
package javax0.blog.demo.throwable.v3; public class FileNumberedLineEmpty extends NumberedLineEmpty {
final protected String fileName;
public FileNumberedLineEmpty(String fileName, NumberedLineEmpty cause) {
super (cause.lineNr, cause);
this .setStackTrace(cause.getStackTrace());
this .fileName = fileName;
}
// getMessage(), same as in v2
@Override
public Throwable fillInStackTrace() {
return this ;
} }
有一个公共的setStackTrace()
方法,可用于设置异常的堆栈跟踪。 有趣的是,此方法实际上是public
,不受保护。 该方法是public
这一事实意味着可以从外部设置任何异常的堆栈跟踪。 这样做(可能)违反了封装规则。
不过,它在那里,如果存在,那么我们可以使用它来将异常的堆栈跟踪设置为与引起异常的堆栈跟踪相同。
这些异常类中还有另一段有趣的代码。 这是公共的fillInStackTrace()
方法。 如果像上面那样实现这一点,那么我们可以节省异常在对象构造过程中花费的时间,以收集其自己的原始堆栈跟踪信息,无论如何我们将其替换并丢弃。
当我们创建一个新异常时,构造函数将调用本机方法来填充堆栈跟踪。 如果查看类java.lang.Throwable
的默认构造函数,您会发现实际上这就是它的全部功能(Java 14 OpenJDK):
public Throwable() {
fillInStackTrace(); }
方法fillInStackTrace()
不是本机的,但这是实际上调用完成工作的本机fillInStackTrace(int)
方法的方法。 这是完成的过程:
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
fillInStackTrace( 0 );
stackTrace = UNASSIGNED_STACK;
}
return this ; }
它里面有一些“魔术”,它如何设置字段stackTrace
但是到目前为止,这还不是很重要。 但是,请务必注意,方法fillInStackTrace()
是public
。 这意味着它可以被覆盖。 (为此, protected
就足够了,但public
更是允许。)
我们还设置了引起异常,在这种情况下,它将具有相同的堆栈跟踪。 运行测试(类似于我们先前仅列出其中一项的测试),我们将打印出堆栈:
javax0.blog.demo.throwable.v3.FileNumberedLineEmpty: c.txt:4 is empty
at javax0.blog.demo.throwable.v3.LineWtfCounter.count(LineWtfCounter.java:15)
at javax0.blog.demo.throwable.v3.FileWtfCounter.count(FileWtfCounter.java:16)
at javax0.blog.demo.throwable.v3.ProjectWftCounter.count(ProjectWftCounter.java:19)
at javax0.blog.demo.throwable.v3.TestWtfCounter.lambda$testThrowing$0(TestWtfCounter.java:17)
at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:62) ...
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58) Caused by: javax0.blog.demo.throwable.v3.NumberedLineEmpty: line 4. has zero length
... 71 more ... 71 Caused by: javax0.blog.demo.throwable.v3.LineEmpty: There is a zero length line
... 71 more ... 71
毫不奇怪,我们有一个FileNumberedLineEmpty
,它的堆栈跟踪从代码行LineWtfCounter.java:15
,不会引发该异常。 当我们看到这一点时,可能会有一些辩论:
- 当我们覆盖堆栈跟踪时,为什么我们需要在原始文件上附加引起异常的原因? (我们不。)
- 这是一个干净的解决方案吗? 堆栈跟踪源自没有引发该异常的行可能会造成混淆。
让我们回答这些问题,是的,出于演示目的,它们是必需的。在实际的应用程序中,每个程序员都可以决定是否要使用这样的解决方案。
这是我们可以获得的最佳解决方案吗? 可能不是,因为正如我所承诺的,我们拥有该应用程序的第四版。
版本4,抑制异常
当我们创建模拟FileReader
我们非常乐观。 我们假设只有一行的长度为零。 如果有不止一条这样的行怎么办? 在这种情况下,应用程序将从第一个停止。 用户修复了以下错误:要么在行中添加一些字符,以使该字符不再为空,要么完全删除该错误,以使该字符不再为行。 然后,用户再次运行该应用程序以获取异常中的第二个位置。 如果有很多这样的行要纠正,那么此过程可能很麻烦。 您还可以想象,实际应用程序中的代码可能会运行很长时间,更不用说要花几个小时了。 仅为了获得问题的下一个位置而执行该应用程序就是在浪费人力,浪费CPU时钟,能源,从而不必要地清洁产生氧气的CO2。
我们可以做的是,更改应用程序,以便在有空行的情况下继续进行处理,并且仅在处理完所有文件和所有行之后,它才会引发异常,列出所有在过程中发现并为空的行。 有两种方法。 一种是创建一些数据结构并将信息存储在其中,然后在处理结束时,应用程序可以查看该数据结构,并在其中存在有关某些空行的任何信息时引发异常。 另一种是使用异常类提供的结构来存储信息。
好处是使用异常类提供的结构是
- 结构已经在那里,不需要重新发明轮子,
- 它是由许多经验丰富的开发人员精心设计的,并且已经使用了数十年,可能是正确的结构,
- 该结构的通用性足以容纳其他类型的异常,不仅是我们当前拥有的异常,而且数据结构不需要任何更改。
让我们讨论最后一点。 稍后可能会发生的情况是,我们决定包含WTF
所有资本的行也是例外的,应该抛出异常。 在这种情况下,如果我们决定手工制作这些结构,则可能需要修改存储这些错误情况的数据结构。 如果我们使用Throwable类的受抑制异常,则没有其他事情要做。 有一个异常,我们将其捕获(如您将在示例中很快看到的那样),将其存储,然后将其作为抑制的异常附加到摘要异常的末尾。 当该演示应用程序极不可能扩展时,我们是否会考虑YAGNI? 是的,不是,通常没有关系。 当您花时间和精力过早开发某些东西时,YAGNI通常是一个问题。 在开发中以及随后的维护中,这是一笔额外的费用。 当我们只使用已经存在的更简单的东西时,不是YAGNI使用它。 它对我们使用的工具非常聪明并且知识渊博。
让我们看一下修改后的FileReader
,这次它已经在许多文件中返回许多空行:
package javax0.blog.demo.throwable.v4; import java.io.FileNotFoundException; import java.util.List; public class FileReader {
final String fileName;
public FileReader(String fileName) {
this .fileName = fileName;
}
public List<String> list() {
if (fileName.equals( "a.txt" )) {
return List.of( "wtf wtf" , "wtf something" , "" , "nothing" );
}
if (fileName.equals( "b.txt" )) {
return List.of( "wtf wtf wtf" , "" , "wtf something wtf" , "nothing wtf" , "" );
}
if (fileName.equals( "c.txt" )) {
return List.of( "wtf wtf wtf" , "" , "wtf something wtf" , "nothing wtf" , "" );
}
throw new RuntimeException( "File is not found: " + fileName);
} }
现在,所有三个文件都包含空行。 我们不需要修改LineWtfCounter
计数器。 空行时,我们抛出异常。 在此级别上,没有办法抑制此异常。 我们无法在此处收集任何例外列表。 我们只关注可能为空的一行。
FileWtfCounter
的情况不同:
package javax0.blog.demo.throwable.v4; public class FileWtfCounter {
private final FileReader fileReader;
public FileWtfCounter(FileReader fileReader) {
this .fileReader = fileReader;
}
public int count() {
final var lines = fileReader.list();
NumberedLinesAreEmpty exceptionCollector = null ;
int sum = 0 ;
int lineNr = 1 ;
for ( final var line : lines) {
try {
sum += new LineWtfCounter(line).count();
} catch (LineEmpty le){
final var nle = new NumberedLineEmpty(lineNr,le);
if ( exceptionCollector == null ){
exceptionCollector = new NumberedLinesAreEmpty();
}
exceptionCollector.addSuppressed(nle);
}
lineNr ++;
}
if ( exceptionCollector != null ){
throw exceptionCollector;
}
return sum;
} }
当我们捕获LineEmpty
异常时,我们将其存储在局部变量exceptionCollector
引用的聚合exceptionCollector
。 如果没有exceptionCollector
则在添加捕获到的异常之前先创建一个,以避免NPE。 在处理的最后,当我们处理完所有行时,我们可能会将许多异常添加到摘要异常exceptionCollector
。 如果存在,则将其抛出。
同样, ProjectWftCounter
收集由不同FileWtfCounter
实例引发的所有异常,并且在处理结束时,它将引发摘要异常,如以下代码行所示:
package javax0.blog.demo.throwable.v4; import javax0.blog.demo.throwable.FileLister; public class ProjectWftCounter {
private final FileLister fileLister;
public ProjectWftCounter(FileLister fileLister) {
this .fileLister = fileLister;
}
public int count() {
final var fileNames = fileLister.list();
FileNumberedLinesAreEmpty exceptionCollector = null ;
int sum = 0 ;
for ( final var fileName : fileNames) {
try {
sum += new FileWtfCounter( new FileReader(fileName)).count();
} catch (NumberedLinesAreEmpty nle) {
if ( exceptionCollector == null ){
exceptionCollector = new FileNumberedLinesAreEmpty();
}
exceptionCollector.addSuppressed(nle);
}
}
if ( exceptionCollector != null ){
throw exceptionCollector;
}
return sum;
} }
现在,我们已经将所有有问题的行收集到一个巨大的异常结构中,我们应该得到一个堆栈跟踪:
javax0.blog.demo.throwable.v4.FileNumberedLinesAreEmpty: There are empty lines
at javax0.blog.demo.throwable.v4.ProjectWftCounter.count(ProjectWftCounter.java:24)
at javax0.blog.demo.throwable.v4.TestWtfCounter.lambda$testThrowing$0(TestWtfCounter.java:17)
at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:62)
at org.assertj.core.api.AssertionsForClassTypes.catchThrowable(AssertionsForClassTypes.java:750)
at org.assertj.core.api.Assertions.catchThrowable(Assertions.java:1179)
at javax0.blog.demo.throwable.v4.TestWtfCounter.testThrowing(TestWtfCounter.java:15)
at java.base /jdk .internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base /jdk .internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base /jdk .internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base /java .lang.reflect.Method.invoke(Method.java:564)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:686)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:205)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:201)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:137)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:71)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.base /java .util.ArrayList.forEach(ArrayList.java:1510)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.base /java .util.ArrayList.forEach(ArrayList.java:1510)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:248)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$5(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:226)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:199)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Suppressed: javax0.blog.demo.throwable.v4.NumberedLinesAreEmpty
at javax0.blog.demo.throwable.v4.FileWtfCounter.count(FileWtfCounter.java:22)
at javax0.blog.demo.throwable.v4.ProjectWftCounter.count(ProjectWftCounter.java:21)
... 68 more ... 68
Suppressed: javax0.blog.demo.throwable.v4.NumberedLineEmpty: line 3.
at javax0.blog.demo.throwable.v4.LineWtfCounter.count(LineWtfCounter.java:15)
at javax0.blog.demo.throwable.v4.FileWtfCounter.count(FileWtfCounter.java:18)
... 69 more ... 69
Caused by: javax0.blog.demo.throwable.v4.LineEmpty: There is a zero length line
Suppressed: javax0.blog.demo.throwable.v4.NumberedLinesAreEmpty
at javax0.blog.demo.throwable.v4.FileWtfCounter.count(FileWtfCounter.java:22)
at javax0.blog.demo.throwable.v4.ProjectWftCounter.count(ProjectWftCounter.java:21)
... 68 more ... 68
Suppressed: javax0.blog.demo.throwable.v4.NumberedLineEmpty: line 2.
at javax0.blog.demo.throwable.v4.LineWtfCounter.count(LineWtfCounter.java:15)
at javax0.blog.demo.throwable.v4.FileWtfCounter.count(FileWtfCounter.java:18)
... 69 more ... 69
Caused by: javax0.blog.demo.throwable.v4.LineEmpty: There is a zero length line
Suppressed: javax0.blog.demo.throwable.v4.NumberedLineEmpty: line 5.
at javax0.blog.demo.throwable.v4.LineWtfCounter.count(LineWtfCounter.java:15)
at javax0.blog.demo.throwable.v4.FileWtfCounter.count(FileWtfCounter.java:18)
... 69 more ... 69
Caused by: javax0.blog.demo.throwable.v4.LineEmpty: There is a zero length line
Suppressed: javax0.blog.demo.throwable.v4.NumberedLinesAreEmpty
at javax0.blog.demo.throwable.v4.FileWtfCounter.count(FileWtfCounter.java:22)
at javax0.blog.demo.throwable.v4.ProjectWftCounter.count(ProjectWftCounter.java:21)
... 68 more ... 68
Suppressed: javax0.blog.demo.throwable.v4.NumberedLineEmpty: line 2.
at javax0.blog.demo.throwable.v4.LineWtfCounter.count(LineWtfCounter.java:15)
at javax0.blog.demo.throwable.v4.FileWtfCounter.count(FileWtfCounter.java:18)
... 69 more ... 69
Caused by: javax0.blog.demo.throwable.v4.LineEmpty: There is a zero length line
Suppressed: javax0.blog.demo.throwable.v4.NumberedLineEmpty: line 5.
at javax0.blog.demo.throwable.v4.LineWtfCounter.count(LineWtfCounter.java:15)
at javax0.blog.demo.throwable.v4.FileWtfCounter.count(FileWtfCounter.java:18)
... 69 more ... 69
Caused by: javax0.blog.demo.throwable.v4.LineEmpty: There is a zero length line
这次,我没有删除任何线条以使您感觉到它在肩上的重量。 现在,您可能会开始考虑使用异常结构而不是仅包含我们所需信息的整洁,苗条的专用数据结构是否值得。 如果您开始这样认为, 那就停止它 。 不要这样 问题(如果有的话)不是我们有太多信息。 问题是我们的表达方式。 为了克服它,解决方案不是将婴儿洗澡水倒掉……多余的信息,而是以更具可读性的方式表示出来。 如果应用程序很少遇到许多空行,那么对堆栈跟踪进行读取可能不会给用户带来难以承受的负担。 如果这是一个经常出现的问题,并且您希望对用户(客户,支付账单的用户)友好,那么,也许不错的异常结构打印机是一个不错的解决方案。
我们在项目中实际上有一个适合您
javax0.blog.demo.throwable.v4.ExceptionStructurePrettyPrinter
您可以随意使用甚至修改。 这样,先前“可怕”堆栈跟踪的打印输出将打印为:
FileNumberedLinesAreEmpty( "There are empty lines" )
Suppressed: NumberedLineEmpty( "line 3." )
Caused by:LineEmpty( "There is a zero length line" )
Suppressed: NumberedLineEmpty( "line 2." )
Caused by:LineEmpty( "There is a zero length line" )
Suppressed: NumberedLineEmpty( "line 5." )
Caused by:LineEmpty( "There is a zero length line" )
Suppressed: NumberedLineEmpty( "line 2." )
Caused by:LineEmpty( "There is a zero length line" )
Suppressed: NumberedLineEmpty( "line 5." )
Caused by:LineEmpty( "There is a zero length line" )
这样,我们就结束了练习。 我们逐步完成了以下步骤:从v1
简单地引发和捕获异常,到v2
设置导致异常的娃套风格, v3
更改嵌入异常的堆栈跟踪,最后v4
存储我们在处理过程中收集的所有抑制的异常。 您现在可以做的是下载项目,进行操作,检查堆栈跟踪,修改代码,等等。 或者继续阅读,我们有一些有关异常的额外信息,这些基本级教程很少讨论这些异常,也值得阅读最后的总结部分。
有关异常的其他注意事项
在本节中,我们将告诉您一些关于异常的基本Java教程中并不为人们所熟知的信息,而这些信息通常是缺失的。
JVM中没有检查异常的东西
除非方法声明明确指出可能会发生这种情况,否则无法从Java方法中引发已检查的异常。 有趣的是,JVM不了解检查异常的概念。 这是Java编译器处理的事情,但是当代码进入JVM时,不会对此进行检查。
Throwable (checked) <-- Exception (checked) <-- RuntimeException (unchecked)
<-- Other Exceptions (checked)
<-- Error (unchecked)
异常类的结构如上所述。 异常的根类是Throwable
。 可以抛出作为类实例的任何对象,直接或间接扩展Throwable
类。 将检查根类Throwable
,因此,如果从方法中抛出了它的实例,则必须对其进行声明。
如果任何类直接扩展该类并从方法中抛出,则必须再次声明它。 除非对象也是RuntimeException
或Error
的实例。 在这种情况下,不检查异常或错误,可以在不声明throwing方法的情况下抛出该异常或错误。
检查异常的想法是有争议的。 使用它有很多优点,但是有许多语言没有它的概念。 这就是JVM不强制执行检查异常的声明的原因。 如果这样做的话,就不可能从不需要声明的异常并且想要与Java异常互操作的语言中生成JVM代码。 当我们在Java中使用流时,检查异常也会引起很多麻烦。
可以克服检查的异常。 使用某种技巧创建的方法,或者仅使用Java以外的JVM语言创建的方法,即使该方法未声明要抛出的异常,也可以抛出已检查的异常。 hacky方式使用一种简单的static
实用程序方法,如以下代码片段所示:
package javax0.blog.demo.throwable.sneaky; public class SneakyThrower {
public static <E extends Throwable> E throwSneaky(Throwable e) throws E {
throw (E) e;
} }
当代码引发一个检查异常时,例如Exception
然后将其传递给throwSneaky()
将使编译器傻瓜。 编译器将查看静态方法的声明,并且无法决定是否检查其抛出的Throwable
。 这样,它将不需要在throwing方法中声明异常。
此方法的使用非常简单,并通过以下单元测试代码进行了演示:
package javax0.blog.demo.throwable.sneaky; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static javax0.blog.demo.throwable.sneaky.SneakyThrower.throwSneaky; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; public class TestSneaky {
@DisplayName ( "Can throw checked exception without declaring it" )
@Test
void canThrowChecked() {
class FlameThrower {
void throwExceptionDeclared() throws Exception {
throw new Exception();
}
void throwExceptionSecretly() {
throwSneaky( new Exception());
}
}
final var sut = new FlameThrower();
assertThat(catchThrowable(() -> sut.throwExceptionDeclared())).isInstanceOf(Exception. class );
assertThat(catchThrowable(() -> sut.throwExceptionSecretly())).isInstanceOf(Exception. class );
}
int doesNotReturn(){
throw throwSneaky( new Exception());
// no need for a return command
} }
两种方法throwExceptionDeclared()
和throwExceptionSecretly()
演示了正常抛出和偷偷摸摸的抛出之间的区别。
throwSneaky()
方法从不返回,并且仍然具有声明的返回值。 这样做的原因是允许在方法doesNotReturn()
可以看到的模式接近文本代码的结尾。 我们知道方法throwSneaky()
从不返回,但是编译器不知道。 如果我们简单地调用它,则编译器仍将在我们的方法中需要一些return语句。 在更复杂的代码流中,它可能会抱怨未初始化的变量。 另一方面,如果我们在代码中“抛出”返回值,那么它将为编译器提供有关执行流程的提示。 实际不会在此级别上进行实际投掷,但这无关紧要。
永远不要抓
当我们捕获异常时,我们可以捕获检查异常, RuntimeException
或任何Throwable
。 不过,也有其他的东西,是Throwable
,但都没有异常,也不会检查。 这些是错误。
故事:
我进行了很多技术面试,应聘者会来回答我的问题。 我对此有很多保留和不好的感觉。 我不喜欢玩“上帝”。 另一方面,当我遇到聪明的人时,即使他们不适合给定的工作职位,我也会很享受。 我通常尝试进行面试,从中获得的价值不仅是对候选人的评价,而且是候选人可以从中了解Java,专业或自己的东西。 有一个可以使用循环解决的编码任务,但是它诱使没有经验的开发人员拥有递归的解决方案。 许多创建递归解决方案的开发人员意识到,对于某些类型的输入参数,代码中没有退出条件。 (除非是因为他们以聪明的方式这样做。但是,当他们有足够的经验时,他们不会寻求递归解决方案而不是简单的循环。因此,当它是递归解决方案时,它们几乎永远不会有退出条件。 )如果我们使用永不结束递归循环的输入参数运行该代码,将会发生什么? 我们得到一个StackOverflowException
。 在面试的压力和压力下,他们中的许多人编写了一些捕获此异常的代码。 这是有问题的。 这是一个陷阱!
为什么是陷阱? 因为代码永远不会抛出StackOverflowException
。 JDK中没有StackOverflowException
这样的东西。 它是StackOverflowError
。 这也不例外,规则是
您的代码绝不能出错
StackOverflowError
(并非例外)扩展了VirtualMachineError
类,该类在JavaDoc中说:
抛出以表明Java虚拟机已损坏
发生故障时,您可以将其粘合在一起,进行修补,修复,但是您永远都不能使其断裂。 如果捕获的Throwable
也是Error
的实例,则在catch
部分执行的代码将在损坏的VM中运行。 那里会发生什么? 任何事情以及执行的继续可能都不可靠。
永远不要发现Error
!
摘要和总结
在本文中,我们讨论了异常,特别是:
- 如何通过在可用时添加信息来引发有意义的异常,
- 在
setTrackTrace()
的情况下如何使用setTrackTrace()
替换异常的堆栈跟踪, - 当您的应用程序可以多次抛出异常时,如何使用
addSuppressed()
收集异常我们还讨论了一些有趣的知识,关于JVM如何不了解已检查的异常以及为什么永远不应该捕获Error
。
不要只是在异常发生时(重新)抛出异常。 考虑一下它们为什么发生以及如何发生,并适当地处理它们。
使用本文中的信息使您的代码与众不同😉
(代码和文章由Mihaly Verhas进行了审核和校对。他还写了外卖部分,包括最后一篇
句子。)
翻译自: https://www.javacodegeeks.com/2020/05/all-you-wanted-to-know-about-throwable.html
throwable