您的位置 首页 java

“全栈2019”Java异常第十三章:看懂异常信息printStackTrace

难度

初级

学习时间

30分钟

适合人群

零基础

开发语言

Java

开发环境

  • JDK v11
  • IntelliJ IDEA v2018.3

友情提示

  • 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
  • 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!

1.堆栈

什么是堆?什么是栈?什么是堆栈?下面结合例子来讲解这三个知识点, 其实这三个知识点需要长时间的开发经验积累去理解的,一个例子不足以理解全部意思,所以希望大家以后在开发过程中能够慢慢地消化和理解它们,同时呢我有讲得不好的地方希望大家能够谅解!

首先,我们先来一段程序代码:

这段程序非常简单,一个Main类,一个main()方法。

程序运行起来以后,它们到底被储存在哪?

存在我们电脑 内存 中。

比如这就是内存空间:

现在还没有存储任何东西。

JVM (Java虚拟机)呢,在内存中开辟了两块空间,一个叫“堆”,一个“栈”。也叫“ 堆内存 ”和“栈内存”。

堆内存和栈内存都是用来存放Java应用数据的(比如基本数据类型、数组、对象…等等)。

好,程序一旦运行起来,我们的main()方法就会被JVM调用,此时main()方法就被存放到栈内存中:

这里我们还得先简单地理解一下栈是什么。

栈的定义

栈(stack)又名堆栈,它是一种运算受限的 线性表 。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

定义太长,我们来分词解释+举例说明。

第一句: 栈(stack)又名堆栈

说明堆栈就是栈,它们是一个概念。

栈的数据结构是这样的:

它就像羽毛球筒一样(羽毛球就是一个一个的元素):

第二句: 它是一种运算受限的线性表

线性表主要由顺序表示或链式表示。在实际应用中,常以栈、队列、字符串等特殊形式使用。具体的线性表的学习在后面我们也会有相应的教学。

第三句: 其限制是仅允许在表的一端进行插入和删除运算

也就是说它只能在一端添加元素和删除元素。就像我们存取羽毛球一样,只能从一个口里面拿出羽毛球和放入羽毛球。

第四句: 这一端被称为栈顶,相对地,把另一端称为栈底

栈的一端叫栈顶:

栈的另一端叫栈底:

第五句: 向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素

给栈添加数据的操作叫入栈(或者压栈、进栈):

存入一次数据

再存入一次数据

第六句: 从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

从栈取出数据的操作叫出栈:

取一次数据

再取一次数据

栈和羽毛球筒的原理是一样的,当我们放一个羽毛球进去的时候,羽毛球自然而然落入羽毛球筒的底部;当我们从羽毛球筒里面取一个羽毛球出来的时候,自然是从羽毛球筒的顶部开始取。

好了,暂时我们对栈就先了解这么多,更多知识点放在后面单独讲解栈的时候再好好聊聊。

为什么要讲栈?我们本章内容不是异常堆栈跟踪信息吗?我到现在都没看见异常呢?

我们讲到现在都好像和异常没什么关系,其实不然,这恰恰和我们接下来要讲的东西密不可分,如果你不理解栈的基本运作方式,那么遇到异常的栈轨迹你就会很迷糊。

我们再来看看刚刚的图:

之前讲到我们的main()方法入栈了,此时main()方法在栈底。

在异常体系中,Throwable有这样一个方法,叫做“printStackTrace”,翻译过来就是“打印堆栈跟踪信息”,它能输出从异常发生的地方开始的整个方法调用链。

多说无益,我们来试试,看看就知道了。

演示:

请输出异常的堆栈跟踪信息。

请观察程序代码及结果。

代码:

Main类:

结果:

从运行结果来看,我们输出的 异常堆栈跟踪信息包括异常的位置以及错误信息。

java.lang.Exception 告诉我们错误信息是什么。

at main.Main.main(Main.java:13) 告诉我们异常出现在什么位置。

此时小伙伴有疑问了,这和栈有什么关系?上面讲那么多,岂不是没用?

各位小伙伴,这里它之所以能输出异常的位置全靠栈。

为什么说全靠栈?

先问一个问题,这里只有一个方法被调用,是不是?

是的,只有main()被调用。

假如main()调用了a()方法,a()方法调用了b()方法,b()方法调用了c()方法,c()方法此时出异常了,请问你除了知道在哪出的错,你知道谁调用它导致出的错吗?

不知道。因为没有记录方法的调用顺序,所以不知道谁调用的它才导致出错的。

如果有一个地方讲方法调用链记录起来的话,这事是不是就解决了?

是的。那这个地方是?

这个地方就是栈。栈里面就存放的是方法调用链。

2.方法链

上面都演示的是调用单个方法main()。下面我们来演示多个方法调用链。

演示:

请在程序中体现方法调用链。

请观察程序代码及结果。

代码:

Main类:

结果:

我们先不要去看这个结果,先来看看这个栈内存的图。

根据程序代码来一步一步画给大家看。首先我们的main()方法入栈:

JVM调用main()方法

main()方法入栈

然后,main()方法里面调用了a()方法:

main()方法调用a()方法

a()方法入栈

接着,a()方法调用了b()方法:

a()方法调用b()方法

b()方法入栈

最后,b()方法调用了c()方法:

b()方法调用c()方法

c()方法入栈

我们可以中看到,c()方法里面出了异常:

java.lang.ArithmeticException: / by zero

at main.Main.c(Main.java:34)

at main.Main.b(Main.java:25)

at main.Main.a(Main.java:18)

at main.Main.main(Main.java:11)

大家有没有观察到这四个at…顺序和我们栈里面的顺序是一样的?

对的,没错,就是我们 栈内存中的方法调用顺序。而且这个顺序就是方法出栈的顺序。

以后大家再遇到错误的时候,千万不要慌,也不要着急,因为我们现在可以看懂printStackTrace()方法输出的是什么了,而且它输出的结构是怎么样的我们也弄明白了。

异常输出信息解析

java.lang.ArithmeticException: / by zero 是错误信息。其中 java.lang.ArithmeticException 是异常类型。 / by zero 是错误原因。

at…开头的行都是方法调用顺序。

最上面的是最后一个被调用的方法:例如c()方法是最后一个被调用的方法。

最下面的是最开始被调用的方法:例如main()方法是最开始被调用的方法。

3.printStackTrace()的 重载 方法

printStackTrace()有两个重载方法,所以加在一起是有三个方法:

  • voidprintStackTrace()
  • voidprintStackTrace​(PrintStream s)
  • voidprintStackTrace​(PrintWriter s)

printStackTrace()源码:

printStackTrace​(PrintStream s)源码:

printStackTrace​(PrintWriter s)源码:

printStackTrace​(PrintStream s)、printStackTrace​(PrintWriter s)有什么用?

printStackTrace​(PrintStream s)的作用是我们可以指定打印流。

printStackTrace​(PrintWriter s)的作用是我们可以指定打印输出流,将异常结果写到日志中或者文本中。(下一章讲解它的用法)

4.printStackTrace()源码【选读】

温馨提示:

本小节内容为选读内容,零基础的小伙伴可以跳过本节内容,对本章和今后的学习没有任何影响。基础较好的小伙伴或者是感兴趣的小伙伴可以试读。有讲解不清楚的地方还请大家指出和谅解,谢谢!

printStackTrace()方法里面究竟是这么实现的?

先看看printStackTrace()方法源码:

printStackTrace()方法里面调用了printStackTrace()方法,还传入了一个System.err的参数,而System.err是一个打印流,从源码中也可以看出:

既然System.err是一个打印流,那么调用的重载方法一定是printStackTrace​(PrintStream s):

printStackTrace​(PrintStream s)内部调用了printStackTrace​(PrintStreamOrWriter s),而WrappedPrintStream正好继承自PrintStreamOrWriter:

printStackTrace​(PrintStreamOrWriter s)方法内部首先创建了一个Set集合:

其次是开启同步锁:

在同步代码块中首先打印了this:

这一行结果在哪可以看见?

其实就是上例中的这个:

继续往下:

这一行代码在做什么?

它实际上就在获取方法的调用顺序集合。

getOurStackTrace()方法源码:

getOurStackTrace()方法内部做了什么呢?

它里面返回了一个StackTraceElement[]数组,我们看看StackTraceElement是什么。

StackTraceElement类源码:

从StackTraceElement类里面的字段我们可以看出异常信息是出自这个类的,请看:

方法名和文件名还有行号都有记录。

好我们回到printStackTrace​(PrintStreamOrWriter s)方法的getOurStackTrace()处:

继续往下执行:

这是在遍历StackTraceElement[]数组,for语句循环体里面是在打印StackTraceElement对象。我们知道打印一个对象就是在调用它的toString()方法,于是去看看StackTraceElement的toString()方法:

前面不用看,直接看重点部分,也就是return部分:

前面部分执行下来变量s就是类名,也就是上例中的“main.Main”。

因为s为“main.Main”,而methodName第一个就是c,所以这段结果就是“main.Main.c”,对应着输出结果:

然后就是后面的小括号里面的,先判断这个方法是不是本地方法(基本可以理解为用C语言写的方法):

显然不是本地方法,结果为false,所以执行问号后面的:

又判断 文件名 行号 是不是空的:

不是空的,结果为true,执行问号前面的:

那就是fileName + “:” + lineNumber,对应输出结果中的:

Main.java是文件名。

36是行号。

这里基本上就全解读完毕,有讲的不好的地方还请大家谅解。

总结

  • 堆内存和栈内存都是用来存放Java应用数据的(比如基本数据类型、数组、对象…等等)。
  • 栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
  • printStackTrace()方法是将栈轨迹给打印出来,实际上记录方法的调用链。
  • StackTraceElement类里面记录了栈轨迹中元素的信息,包括文件名、方法名和行号(在哪一行调用的方法)。

至此,Java中printStackTrace()方法相关内容讲解先告一段落,更多内容请持续关注。

答疑

如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。

上一章

下一章

“全栈2019”Java异常第十四章:将异常信息输出到文本文件中

学习小组

加入同步学习小组,共同交流与进步。

  • 方式一:关注头条号Gorhaf,私信“Java学习小组”。
  • 方式二:关注公众号Gorhaf,回复“Java学习小组”。

全栈工程师学习计划

关注我们,加入“全栈工程师学习计划”。

版权声明

原创不易,未经允许不得转载!

文章来源:智云一二三科技

文章标题:“全栈2019”Java异常第十三章:看懂异常信息printStackTrace

文章地址:https://www.zhihuclub.com/176125.shtml

关于作者: 智云科技

热门文章

网站地图