java异常处理笔记

对于一个非常熟悉 C++ 异常处理模型的程序员来说,它几乎可以不经任何其它培训和学习,就可以完全接受和能够轻松地使用 Java 语言中的异常处理编程方法。这是因为 Java 语言中的异常处理模型几乎与 C++ 中异常处理模型有 99% 的相似度,无论是从语法规则,还是语义上来说,它们二者都几乎完全一致。

当然,如果你对 Java 语言中的异常处理模型有更多,或更深入的了解,你还是能够发现 Java 异常处理模型与 C++ 中异常处理模型还是存在不少差别的。是的,Java 语言本来就是 C++ 语言的完善改进版,所以,Java 语言中的异常处理模型也就必然会继承了 C++ 异常处理模型的风格和优点。但是,好的东西不仅仅是需要继承优点,更重要的是需要“去其糟粕,取其精华”,需要发展!!!毫无疑问,Java 语言中的异常处理模型完全达到了这一“发展”高度。它比 C++ 异常处理模型更安全,更可高,更强大和更丰富。

下面的内容中,阿愚不打算再向大家详细介绍一遍有关 Java 异常处理编程的具体语法和规则。因为这与 C++ 中的异常处理几乎完全一样,而且这些基础知识在太多太多的有关 java 编程的书籍中都有详细阐述。而阿愚认为: 这里更需要的是总结,需要的是比较,需要的是重点突出,需要的是关键之处 。所以,下面着重把 Java 语言中的异常处理模型与 C++ 异常处理模型展开比较,让我们透彻分析它到底有何发展?有何优势?与 C++ 异常处理模型到底有哪些细节上的不同?又为什么要这样做?

借鉴并引进了 SEH 异常模型中的 try-finally 语法

要说 Java 异常处理模型与 C++ 中异常处理模型的最大不同之处,那就是在 Java 异常处理模型中引入了 try-finally 语法,阿愚认为这是从微软的 SEH 借鉴而来。在前面的一些文章中,详细而深入阐述 SEH 异常处理模型的时候,我们从中获知,SEH 主要是为 C 语言而设计的,便于第三厂商开发的 Window 驱动程序有更好更高的安全保障。同时,SEH 异常处理模型中除了 try-except 来用于处理异常外,还有一个 try-finally 语法,它主要用来清除一些曾经分配的资源(由于异常出现,而导致这些资源不能够按正常的顺序被释放,还记得吗?这被称为“ UNWIND ”),try-finally 本质上有点类似于面向对象编程中的析构函数的作用,由于这项机制的存在,才导致 SEH 的强大和风光无比。

现在,Java 异常处理模型也吸收了这项设计。可我们知道,无论是 JAVA 中,还是 C++ 中,它们都有“析构函数”呀!它们完全可以利用面向的析构函数来自动释放资源呀!是的,没错!理论上是这样的。可是在实践中,我们也许会发现或经常碰到,仅仅利用析构函数来释放资源,并不是那么好使,例如,我们经常需要动态得从堆上分配的对象,这时,释放对象必须要显式地调用 delete 函数来触发该对象的析构函数的执行。如果这期间发生了异常,不仅该对象在堆中的内存不能达到被释放的结果,而且,该对象的析构函数中释放更多资源的一些代码也不能得以执行。因此这种后果非常严重,这也算 C++ 异常处理模型中一种比较大的缺陷吧!(虽然,C++ 有其它补救措施,那就是利用“智能指针”来保证一些对象的及时而有效地被释放)。另外,其实很有许多类似的例子,例如关闭一个内核句柄等操作( CloseHandle )。

在 Java 语言中,由于它由于采用了垃圾回收技术,所以它能有效避免上述所讲的类似于 C++ 语言中的由于异常出现所导致的那种资源不能得以正确释放的尴尬局面。但是,它仍然还是在它的异常处理模型中引入了 try-finally 语法,因为,无论如何,它至少会给程序员带来了极大的方便,例如如下的程序片断,就可以充分反映出 try-finally 对提高我们代码的质量和美观是多么的重要。

import java.io.*;/** *//**author by http://www.bt285.cn http://www.5a520.cn*/public class Trans{public static void main(String[] args){try{BufferedReader rd=null;Writer wr=null;try{File srcFile = new File((args[0]));File dstFile = new File((args[1]));rd = new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), args[2]));wr = new OutputStreamWriter(new FileOutputStream(dstFile), args[3]);while(true){String sLine = rd.readLine();if(sLine == null) break;wr.write(sLine);wr.write("/r/n");}}finally{// 这里能保证在何种情况下,文件流的句柄都得以被正确关闭// 该方法主要用于清理非内存性质的资源(垃圾回收机制无法// 处理的资源,如数据库连接、 Socket 关闭、文件关闭等等)。wr.flush();wr.close();rd.close();}}catch(Exception ex){ex.printStackTrace();}}}

所有的异常都必须从 Throwable 继承而来

在 C++ 异常处理模型中,它给予程序员最大的自由度和发挥空间(这与 C++ 中所主导的设计思想是一致的),例如它允许程序员可以抛出任何它想要的异常对象,它可以是语言系统中原本所提供的各种简单数据类型(如 int 、 float 、 double 等),也可以是用户自定义的抽象数据对象(如 class 的 object 实例)。虽然说,无论从何个角度上考量,我们都应该把异常处理与面向对象紧密结合起来(采用带有继承特点的层次化的类对象结构来描述系统中的各种异常),但是 C++ 语言规范中并无此约束;况且,即便大家都不约而同地采用面向对象的方法来描述“异常”,但也会由于各个子系统(基础运行库)不是一个厂商(某一个人)所统一设计,所以导致每个子系统所设计出的异常对象系统彼此相差甚远。这给最终使用(重用)这些库的程序员带来了很大的不一致性,甚至是很大的麻烦,我们不仅需要花费很多很宝贵的时间来学习和熟悉这些不同的异常对象子系统;而且更大的问题是,这些不同子系统之间语义上的不一致,而造成程序员在最终处理这些异常时,将很难把它们统一起来,例如,MFC 库系统中,采用 CMemoryException 来表示一个与内存操作相关的异常;而其它的库系统中很可能就会采用另外一个 class 来表示内存操作的异常错误。本质上说,这是由于缺乏规范和统一所造成的恶劣后果,所以说,如果在语言设计的时候,就充分考虑到这些问题,把它们纳入语言的统一规范之中,这对广大的程序员来说,无疑是个天大的好事情。

Java 语言毫无疑问很好地做到了这一点,它要求 java 程序中(无论是谁写的代码),所有抛出( throw )的异常都必须是从 Throwable 派生而来,例如,下面的代码编译时就无法通过。

import java.io.*;/** *//***author by http://www.bt285.cn http://www.5a520.cn */public class Trans{public static void main(String[] args){try{BufferedReader rd=null;Writer wr=null;try{File srcFile = new File((args[0]));File dstFile = new File((args[1]));rd = new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), args[2]));wr = new OutputStreamWriter(new FileOutputStream(dstFile), args[3]);// 编译时,这里将被报错// 而正确的做法可以是: throw new Exception("error! test!");if (rd == null || wr == null) throw new String("error! test!");while(true){String sLine = rd.readLine();if(sLine == null) break;wr.write(sLine);wr.write("/r/n");}}finally{wr.flush();wr.close();rd.close();}}catch(Exception ex){ex.printStackTrace();}}}

编译时,输出的错误信息如下:

E:/work//Trans.java:20: incompatible typesfound : java.lang.Stringrequired: java.lang.Throwableif (rd == null || wr == null) throw new String("error! test!");

1 error

当然,实际的 Java 编程中,由于 JDK 平台已经为我们设计好了非常丰富和完整的异常对象分类模型。因此,java 程序员一般是不需要再重新定义自己的异常对象。而且即便是需要扩展自定义的异常对象,也往往会从 Exception 派生而来。所以,对于 java 程序员而言,它一般只需要在它的顶级函数中 catch(Exception ex) 就可以捕获出所有的异常对象,而不必像 C++ 中采用 catch(…) 那样不伦不类,但又无可奈何的语法。因此,在 java 中也就不需要(也没有了) catch(…) 这样的语法。

至于 JDK 平台中的具体的异常对象分类模型,主人公阿愚打算放在另外单独的一篇文章中详细讨论,这里只简单的概括一下: 所有异常对象的根基类是 Throwable ,Throwable 从 Object 直接继承而来(这是 java 系统所强制要求的),并且它实现了 Serializable 接口(这为所有的异常对象都能够轻松跨越 Java 组件系统做好了最充分的物质准备)。从 Throwable 直接派生出的异常类有 Exception 和 Error 。 Exception 是 java 程序员所最熟悉的,它一般代表了真正实际意义上的异常对象的根基类。也即是说,Exception 和从它派生而来的所有异常都是应用程序能够 catch 到的,并且可以进行异常错误恢复处理的异常类型。而 Error 则表示 Java 系统中出现了一个非常严重的异常错误,并且这个错误可能是应用程序所不能恢复的,例如 LinkageError ,或 ThreadDeath 等。

对异常处理的管理更严格,也更严谨!

同样还是与 C++ 异常处理模型作比较,在 Java 系统中,它对异常处理的管理更严格,也更严谨!为什么这么说呢?下面请听阿愚娓娓道来!

首先还是看一个例子吧!代码如下:

import java.io.*;public class Trans{public static void main(String[] args){try{BufferedReader rd=null;Writer wr=null;try{File srcFile = new File((args[0]));File dstFile = new File((args[1]));rd = new BufferedReader(new InputStreamReader(new FileInputStream(srcFile), args[2]));wr = new OutputStreamWriter(new FileOutputStream(dstFile), args[3]);// 注意下面这条语句,它有什么问题吗?if (rd == null || wr == null) throw new Exception("error! test!");while(true){String sLine = rd.readLine();if(sLine == null) break;wr.write(sLine);wr.write("/r/n");}}finally{wr.flush();wr.close();rd.close();}}catch(IOException ex){ex.printStackTrace();}}}

熟悉 java 语言的程序员朋友们,你们认为上面的程序有什么问题吗?编译能通过吗?如果不能,那么原因又是为何呢?好了,有了自己的分析和预期之后,不妨亲自动手编译一下上面的小程序,呵呵!结果确实如您所料?是的,的确是编译时报错了,错误信息如下:

E:/Trans.java:20: unreported exception java.lang.Exception; must be caught or declared to be thrownif (rd == null || wr == null) throw new Exception("error! test!");

1 error

上面这种编译错误信息,相信 Java 程序员肯定见过(可能还是屡见不鲜!),而且,这也是许多从 C++ 程序员刚刚转来写 Java 程序不久(还没有真正入行)时,最感到迷糊和不解的地方,“俺代码明明没什么问题吗?可为什么编译有问题呢”!

呵呵!阿愚相信老练一些的 Java 程序员一定非常清楚上述编译出错的原因。那就是如错误信息中(“ must be caught ”)描述的那样,在 Java 的异常处理模型中,要求所有被抛出的异常都必须要有对应的“异常处理模块” 。也即是说,如果你在程序中 throw 出一个异常,那么在你的程序中(函数中)就必须要 catch 这个异常(处理这个异常)。例如上面的例子中,你在第 20 行代码处,抛出了一个 Exception 类型的异常,但是在该函数中,却没有 catch 并处理掉此异常的地方。因此,这样的程序即便是能够编译通过,那么运行时也是致命的(可能导致程序的崩溃),所以,Java 语言干脆在编译时就尽可能地检查(并卡住)这种本不应该出现的错误,这无疑对提高程序的可靠性大有帮助。相反,C++ 中则不是这样,它把更多的事情和责任交给 C++ 程序员去完成,它总相信 C++ 程序员是最优秀的,会自己处理好这一切的所有事情。如果程序中真的出现了未被捕获的异常,系统它(实际上是 C++ 运行库中)就认为这是出现了一个致命的、不可恢复的错误,接着调用 terminate 函数终止该进程( NT 系统中稍微负责任一些,还弹出个“系统错误”对华框,并且 report 出一些与异常相关的错误信息)。

从上面的分析可以看出,Java 异常处理模型的确比 C++ 异常处理模型更严谨和更安全。实际上,这还体现在更多的方面,例如,在 C++ 异常处理模型中,异常的声明已经可以成为声明函数接口的一部分,但这并非语言所强求的。虽然许多优秀的库系统在设计时,都把这当成了一种必须的约定,甚至是编程规范,例如 MFC 中,就有许多可能抛出异常的函数,都显式地做了声明,如 CFile::Read 函数,声明如下:

virtual UINT Read( void* lpBuf , UINT nCount );

throw( CFileException );

但是,在 Java 语言中,这就是必须的。 如果一个函数中,它运行时可能会向上层调用者函数抛出一个异常,那么,它就必须在该函数的声明中显式的注明(采用 throws 关键字,语法与 C++ 类似) 。还记得刚才那条编译错误信息吗?“ must be caught or declared to be thrown ”,其中“ must be caught ”上面已经解释了,而后半部分呢?“ declared to be thrown ”是指何意呢?其实指的就是“必须显式地声明某个函数可能会向外部抛出一个异常”,也即是说,如果一个函数内部,它可能抛出了一种类型的异常,但该函数内部并不想(或不宜) catch 并处理这种类型的异常,此时,它就必须( 注意,这是必须的!强求的!而 C++ 中则不是必须的 )使用 throws 关键字来声明该函数可能会向外部抛出一个异常,以便于该函数的调用者知晓并能够及时处理这种类型的异常。下面列出了这几种情况的比较,代码如下:

// 示例程序 1 ,这种写法能够编译通过import java.io.*;public class Trans{public static void main(String[] args){try{test();}catch(Exception ex){ex.printStackTrace();}}static void test(){try{throw new Exception("test");}catch(Exception ex){ex.printStackTrace();}}}// 示例程序 2 ,这种写法就不能够编译通过import java.io.*;public class Trans{public static void main(String[] args){try{test();}// 虽然这里能够捕获到 Exception 类型的异常catch(Exception ex){ex.printStackTrace();}}static void test(){throw new Exception("test");}}// 示例程序 3 ,这种写法又能够被编译通过import java.io.*;public class Trans{public static void main(String[] args){try{test();}catch(Exception ex){ex.printStackTrace();}}// 由于函数声明了可能抛出 Exception 类型的异常static void test() throws Exception{throw new Exception("test");}}// 示例程序 4 ,它又不能够被编译通过了import java.io.*;public class Trans{public static void main(String[] args){try{// 虽然 test() 函数并没有真正抛出一个 Exception 类型的异常// 但是由于它函数声明时,表示它可能抛出一个 Exception 类型的异常// 所以,这里仍然不能被编译通过。// 呵呵!体会到了 Java 异常处理模型的严谨吧!test();}catch(IOException ex){ex.printStackTrace();}}static void test() throws Exception{}}

不知上面几个有联系的示例是否能够给大家带来“豁然开朗”的感觉,坦率的说,Java 提供的异常处理模型并不复杂,相信太多太多 Java 程序员有着比阿愚更深刻的认识。也许,阿愚对于这些简单而琐碎的事情如此婆婆妈妈絮叨个没完,有点招人笑话了!但是,阿愚还是坚持认为,非常有必要把这些简单但非常管用的准则阐述透彻。最后,补充一种例外情况,请看如下代码:

import java.io.*;public class Trans{public static void main(String[] args){try{test();}catch(Exception ex){ex.printStackTrace();}}static void test() throws Error{throw new Error(" 故意抛出一个 Error");}}

朋友们!上面的程序能被编译通过吗?注意,按照刚才上面所总结出的规律:在 Java 的异常处理模型中,要求所有被抛出的异常都必须要有对应的 catch 块!那么上面的程序肯定不能被编译通过,因为 Error 和 Exception 都是从 Throwable 直接派生而来,而 test 函数声明了它可能抛出 Error 类型的异常,但在 main 函数中却并没有 catch(Error) 或 catch(Throwable) 块,所以它理当是会编译出错的!真的吗?不妨试试!呵呵!结果并非我们之预料,而它恰恰是正确编译通过了。为何? WHY ? WHY ?

其实,原因很简单,那就是因为 Error 异常的特殊性。 Java 异常处理模型中规定: Error 和从它派生而来的所有异常,都表示系统中出现了一个非常严重的异常错误,并且这个错误可能是应用程序所不能恢复的 (其实这在前面的内容中已提到过)。因此,如果系统中真的出现了一个 Error 类型的异常,那么则表明,系统已处于崩溃不可恢复的状态中,此时,作为编写 Java 应用程序的你,已经是没有必要(也没能力)来处理此等异常错误。所以,javac 编译器就没有必要来保证:“在编译时,所有的 Error 异常都有其对应的错误处理模块”。当然,Error 类型的异常一般都是由系统遇到致命的错误时所抛出的,它最后也由 Java 虚拟机所处理。而作为 Java 程序员的你,可能永远也不会考虑抛出一个 Error 类型的异常。因此 Error 是一个特例情况!

特别关注一下 RuntimeException

上面刚刚讨论了一下 Error 类型的异常处理情况,Java 程序员一般无须关注它(处理这种异常)。另外,其实在 Exception 类型的异常对象中,也存在一种比较特别的“异常”类型,那就是 RuntimeException ,虽然它是直接从 Exception 派生而来,但是 Java 编译器( javac )对 RuntimeException 却是特殊待遇,而且是照顾有加。不信,看看下面的两个示例吧!代码如下:

// 示例程序 1// 它不能编译通过,我们可以理解import java.io.*;public class Trans{public static void main(String[] args){test();}static void test(){// 注意这条语句throw new Exception(" 故意抛出一个 Exception");}}// 示例程序 2// 可它却为什么能够编译通过呢?import java.io.*;public class Trans{public static void main(String[] args){test();}static void test(){// 注意这条语句throw new RuntimeException(" 故意抛出一个 RuntimeException");}}

对上面两个相当类似的程序,javac 编译时却遭遇了两种截然不同的处理,按理说,第 2 个示例程序也应该像第 1 个示例程序那样,编译时报错!但是 javac 编译它时,却例外地让它通过它,而且在运行时,java 虚拟机也捕获到了这个异常,并且会在 console 打印出详细的异常信息。运行结果如下:

java.lang.RuntimeException: 故意抛出一个 RuntimeException

at Trans.test(Trans.java:13)at Trans.main(Trans.java:8)Exception in thread "main"

为什么对于 RuntimeException 类型的异常(以及从它派生而出的异常类型),javac 和 java 虚拟机都特殊处理呢?要知道,这可是与“ Java 异常处理模型更严谨和更安全”的设计原则相抵触的呀!究竟是为何呢?这简直让人不法理解呀!主人公阿愚劝大家稍安勿躁,我们不妨换个角度,还是从 C++ 异常处理模型展开对比和分析,也许这能使我们茅塞顿开!

在 VC 实现的 C++ 异常处理中(基于 SEH 机制之上),一般有两类异常。其一,就是通过 throw 语句,程序员在代码中人为抛出的异常(由于运行时动态地监测到了一个错误);另外一个呢?就是系统异常,也即前面我们一直重点讨论的 SEH 异常,例如,“被 0 除”,“段存储保护异常”,“无效句柄” 等,这类异常实际上程序员完全可以做得到避免它(只要我们写代码时足够小心,足够严谨,使得写出的代码足够安全可靠),但实际上,这也许无法完全做到,太理想化了。因此,为了彻底解决这种隐患,提高程序整体可靠性(可以容忍程序员留下一些 BUG ,至少不至于因为编码时考虑不周,或一个小疏忽留下的一个小 BUG ,而导致整个应用系统在运行时崩溃!),VC 提供的 C++ 异常处理中,它就能够捕获到 SEH 类型的系统异常。 这类异常在被处理时,实际上它首先会被操作系统的中断系统所接管(也即应用程序出现这类异常时,会导致触发了一个系统“中断”事件),接着,操作系统会根据应用程序中(实际上是 VC 运行库)所设置的一系列“异常”回调函数和其它信息,来把处理该“系统异常”的控制权转交给应用程序层的“ VC 异常处理模型”中。 还记得上一篇文章中,《第 28 集 如何把 SEH 类型的系统异常转化为 C++ 类型的异常》中的方法和技术原理吗?我们不妨回顾一下,“把 SEH 类型的系统异常转化为 C++ 类型的异常”之后,它就提供了一种很好的技术方法,使得 VC 提供的 C++ 异常处理中,完全把“系统异常”的处理容纳到了按 C++ 方式的异常处理之中。

毫无疑问,这种技术大大的好!大大的妙!同样,现在 Java 的异常处理模型中,它理当应该也具备此项技术功能(谁让它是 C++ 的发展呢?),朋友们,阿愚现在是否已经把该问题解释明白了呢?是的,实际上,RuntimeException 异常的作用,就相当于上一篇文章中,阿愚为 C++ 所设计的 seh_exception_base 异常那样,它们在本质上完成同样类似的功能。只不过,Java 语言中,RuntimeException 被统一纳入到了 Java 语言和 JDK 的规范之中。请看如下代码,来验证一下我们的理解!

import java.io.*;public class Trans{public static void main(String[] args){test();}static void test(){int i = 4;int j = 0;// 运行时,这里将触发了一个 ArithmeticException// ArithmeticException 从 RuntimeException 派生而来System.out.println("i / j = " + i / j);}}运行结果如下:java.lang.ArithmeticException: / by zeroat Trans.test(Trans.java:16)at Trans.main(Trans.java:8)Exception in thread "main"又如下面的例子,也会产生一个 RuntimeException ,代码如下:import java.io.*;public class Trans{public static void main(String[] args){test();}static void test(){String str = null;// 运行时,这里将触发了一个 NullPointerException// NullPointerException 从 RuntimeException 派生而来str.compareTo("abc");}}

所以,针对 RuntimeException 类型的异常,javac 是无法通过编译时的静态语法检测来判断到底哪些函数(或哪些区域的代码)可能抛出这类异常(这完全取决于运行时状态,或者说运行态所决定的),也正因为如此,Java 异常处理模型中的“ must be caught or declared to be thrown ”规则也不适用于 RuntimeException (所以才有前面所提到过的奇怪编译现象,这也属于特殊规则吧)。但是,Java 虚拟机却需要有效地捕获并处理此类异常。当然,RuntimeException 也可以被程序员显式地抛出,而且为了程序的可靠性,对一些可能出现“运行时异常( RuntimeException )”的代码区域,程序员最好能够及时地处理这些意外的异常,也即通过 catch(RuntimeExcetion) 或 catch(Exception) 来捕获它们。如下面的示例程序,代码如下:

import java.io.*;public class Trans{public static void main(String[] args){try{test();}// 在上层的调用函数中,最好捕获所有的 Exception 异常!catch(Exception e){System.out.println("go here!");e.printStackTrace();}}// 这里最好显式地声明一下,表明该函数可能抛出 RuntimeExceptionstatic void test() throws RuntimeException{String str = null;// 运行时,这里将触发了一个 NullPointerException// NullPointerException 从 RuntimeException 派生而来str.compareTo("abc");}}

总结

•Java 异常处理模型与 C++ 中异常处理模型的最大不同之处,就是在 Java 异常处理模型中引入了 try-finally 语法,它主要用于清理非内存性质的一些资源(垃圾回收机制无法处理的资源),例如,数据库连接、 Socket 关闭、文件流的关闭等。

•所有的异常都必须从 Throwable 继承而来,不像 C++ 中那样,可以抛出任何类型的异常。因此,在 Java 的异常编程处理中,没有 C++ 中的 catch(…) 语法,而它的 catch(Throwable e) 完全可以替代 C++ 中的 catch(…) 的功能。

•在 Java 的异常处理模型中,要求所有被抛出的异常都必须要有对应的“异常处理模块”。也即是说,如果你在程序中 throw 出一个异常,那么在你的程序中(函数中)就必须要 catch 这个异常(处理这个异常)。但是,对于 RuntimeException 和 Error 这两种类型的异常(以及它们的子类异常),却是例外的。其中,Error 表示 Java 系统中出现了一个非常严重的异常错误;而 RuntimeException 虽然是 Exception 的子类,但是它却代表了运行时异常(这是 C++ 异常处理模型中最不足的,虽然 VC 实现的异常处理模型很好)

•如果一个函数中,它运行时可能会向上层调用者函数抛出一个异常,那么,它就必须在该函数的声明中显式的注明(采用 throws 关键字,语法与 C++ 类似)。

本篇文章虽然有点长,但是如果你能够坚持完整地看下来,相信你对 Java 的异常处理模型一定有了更深一步的了解(当然,由于阿愚水平有限,难免会有不少认识上或理解上的错误,欢迎大家不吝赐教!呵呵!讨论和交流也可以)。在下一篇文章中,阿愚将继续分析和阐述一些有关 Java 异常处理模型中更细节的语法特点和使用上的技巧,以及注意事项。感兴趣的朋友们,继续吧!GO !

就是去旅行。牵着彼此的手,

java异常处理笔记

相关文章:

你感兴趣的文章:

标签云: