深入理解Java7:核心技术与最佳实践(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-26 13:33:37

点击下载

作者:成富

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

深入理解Java7:核心技术与最佳实践

深入理解Java7:核心技术与最佳实践试读:

前言

为什么要写这本书

我最早开始接触Java语言是在大学的时候。当时除了用Java开发一些小程序之外,就是用Struts框架开发Web应用。在后来的实习和工作中,我对Java的使用和理解更加深入,逐渐涉及Java相关的各种不同技术。使用Java语言的一个深刻体会是:Java语言虽然上手容易,但是要真正掌握并不容易。

Java语言对开发人员屏蔽了一些与底层实现相关的细节,但是仍然有很多内容对开发人员来说是很复杂的,这些内容恰好是容易出现错误的地方。我在工作中就经常遇到与类加载器和垃圾回收相关的问题。在解决这些问题的过程中,我积累了一些经验,遇到类似的问题可以很快地找到问题的根源。同时,在解决这些实际问题的过程中,我意识到虽然可以解决某些具体的问题,但是并没有真正理解这些问题的解决办法背后所蕴含的基本原理,仍然还只是处于一个“知其然,不知其所以然”的状态。于是我开始阅读Java相关的基础资料,包括Java语言规范、Java虚拟机规范、Java类库的源代码和其他在线资料等。在阅读的基础上,编写小程序进行测试和试验。通过阅读和实践,我对Java平台中的一些基本概念有了更加深入的理解。从2010年开始,我对积累的相关知识进行了整理,在InfoQ中文站的“Java深度历险”专栏上发表出来,受到了一定的关注。

2011年7月,在时隔数年之后,Java的一个重大版本Java SE 7发布了。在这个新的版本中,Java平台增加了很多新的特性。在Java虚拟机方面,invokedynamic指令的加入使虚拟机上的动态语言的性能得到很大的提升。这使得开发人员可以享受到动态语言带来的在提高生产效率方面的好处。在Java语言方面,语言本身的进一步简化,使开发人员编写代码的效率更高。在Java类库方面,新的IO库和同步实用工具类为开发人员提供了更多实用的功能。从另外一个角度来说,Java SE 7是Oracle公司收购Sun公司之后发布的第一个Java版本,从侧面反映出了Oracle公司对Java社区的领导力,可以继续推动Java平台向前发展。这可以打消企业和社区对于Oracle公司领导力的顾虑。Java SE 7的发布也证明了基于JCP和OpenJDK的社区驱动模式可以很好地推动Java向前发展。

随着新版本的发布,肯定会有越来越多的开发人员想尝试使用Java SE 7中的新特性,毕竟开发者社区对这个新版本期待了太长的时间。在Java程序中使用这些新特性,可以提高代码质量,提升工作效率。Java平台的每个版本都致力于提高Java程序的运行性能。随着新版本的发布,企业都应该考虑把Java程序的运行平台升级到最新的Java SE 7,这样可以享受到性能提升所带来的好处。对于新的Java程序开发,推荐使用Java SE 7作为标准的运行平台。本书将Java SE 7中的新特性介绍和对Java平台的深入探讨结合起来,让读者既可以了解最新版本的Java平台的新特性,又可以对Java平台的底层细节有更加深入的理解。

读者对象及如何阅读本书

本书面向的主要读者是具备一定Java基础的开发人员和在校学生。本书中不涉及Java的基本语法,因此不适合Java初学者阅读。如果只对Java SE 7中的新特性感兴趣,可以阅读第1章到第6章;如果对Java中的特定主题感兴趣,可以根据目录有选择地阅读。另外,第1章到第6章虽然以Java SE 7的新特性介绍为主,但是其中也穿插了对相关内容的深入探讨。

本书可分为三大部分:

第一部分为Java SE 7新特性介绍,从第1章到第6章。这部分详细地介绍了Java SE 7中新增的重要特性。在对新特性的介绍中,也包含了对Java平台相关内容的详细介绍。

第二部分为Java SE 7的深入探讨,从第7章到第13章。这部分着重讲解了Java平台上的底层实现,并对一些重要的特性进行了深入探讨。这个部分所涉及的内容包括Java虚拟机、Java源代码和字节代码操作、Java类加载器、对象生命周期、多线程与并发编程实践、Java泛型和Java安全。

第三部分为Java SE 8的内容展望,即第14章。这部分简要介绍了Java SE 8中将要增加的新特性。

本书还通过两个附录对OpenJDK(附录A)和Java语言的历史(附录B)进行了简要的介绍。

勘误和支持

由于作者的水平有限,编写时间仓促,书中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果您有更多的宝贵意见,欢迎发送邮件至邮箱alexcheng1982@gmail.com,也可以通过微博(http://weibo.com/alexcheng1982)与我取得联系。期待能够得到您的真挚反馈。

本书官方微群:http://q.weibo.com/943166。

本书中的源代码请登录华章公司的网站(http://www.hzbook.com)本书页面进行下载。

致谢

感谢InfoQ中文站和InfoQ编辑张凯峰先生。这本书能够面世,得益于我在InfoQ中文站的“Java深度历险”专栏上发表的文章。

感谢机械工业出版社华章公司的编辑杨福川和姜影的辛勤工作,使得这本书能够最终顺利完成。

感谢家人和朋友对我的支持与帮助!

Java的挑战与展望

从Java语言出现到现在的16年间,在Java语言本身发展演化的同时,整个软件开发行业也在发生着巨大的变化。新的软件开发思想和程序设计语言层出不穷。虽然Java语言一直是最流行的程序设计语言之一,但它也面临着来自其他编程语言的冲击。这其中主要是互联网应用发展所带来的动态语言的影响。

Java是静态强类型语言。这种特性使Java编译器在编译时就可以发现非常多的类型错误,而不会让这些错误在运行时才暴露出来。对于构建一个稳定而安全的应用来说,这是一个很大的优势,但是这种静态的类型检查也限制了开发人员编写代码时的创造性和灵活性。

Web 2. 0概念的出现和互联网应用的发展,为新语言的流行创造了契机。Ruby语言凭借着杀手级应用Ruby on Rails一举蹿红,而Google的Web应用开发平台Google App Engine最初也只支持Python一种语言,甚至流行的JavaScript语言也借助于node.js和Aptana Jaxer等平台在服务器端开发中占据了一席之地。这些语言的共同特征是动态类型与灵活自由的语法。开发人员一旦掌握了这些语言,开发效率会非常高。在这一点上,Java语言繁琐的语法就显得缺乏吸引力。Java语言也受到来自同样运行在Java虚拟机上的其他语言的挑战。这些语言包括Groovy、Scala、JRuby和Jython等。任何语言,只要它生成的字节代码符合Java字节代码规范,就可以在Java虚拟机上运行。前面提到的这些Java虚拟机上的语言既具有简洁优雅的语法,又能充分利用已有的Java虚拟机资源,相对于Java语言本身来说,非常具有竞争力。

基于前面的这些现状,在社区中有人悲观地预言:Java已死,COBOL式的死亡。COBOL这门诞生于20世纪50年代末的编程语言,已经被诸多机构和个人论证为已经死亡的语言。实际上,COBOL语言仍然在银行、金融和会计等商业应用领域占据着主导地位。只要这些应用存在,COBOL语言就不会消亡。Java语言也是如此。只要运行在Java平台上的应用还存在,Java语言就能一直生存下去。事实上,现在仍然有许多公司和个人在向Java平台投资。这些投资既包括投入资金和人力来开发基于Java平台的应用,也包括投入时间来学习Java平台的相关技术。

当然,Java平台也有不足之处,其中最明显的是整个Java平台的复杂性。最早在JDK 1.0发布的时候,只有几百个Java类,而现在的Java 6已经包括Java SE、Java EE和Java ME等多个版本,所包含的Java类多达数千个。对于普通开发者来说,完全理解和熟悉如此庞大的类库的难度非常大。在日常的开发过程中,经常可以看到开发者在重复实现某些功能,而这些功能在Java类库中已经存在,只是不被人知道而已。除了庞大的类库之外,Java语言的语法本身也缺乏足够的灵活性,实现某些功能所需的代码量可能是其他语言的几倍。另外一个复杂性体现在Web应用开发方面。一个完整的Java EE应用程序要求程序员掌握和理解的概念太多,要使用的库也非常多。这点从市面上到处可见的以Java Web应用开发和Struts、Spring及Hibernate等框架为内容的图书上就可以看出来。虽然新出现的Grails和Play框架等都试图降低这个复杂度,但是这些新的框架的流行仍然需要足够长的时间。

对于Java语言的未来,我们有理由相信Java平台会一直发展下去。其中很重要的依据是Java平台的开放性。依托JCP和OpenJDK项目,Java平台不仅在语言规范这个层次上有健康的开放管理流程,也有与之对应的参考实现。Java语言有着人数众多的开发者社区,每年有非常多新的开发者学习和使用Java。大量的开发者使用Java语言开发各种不同类型的应用。在社区中可以看到很多提供不同功能的类库和框架。Java虚拟机已经被安装到数以十亿计的不同类型的设备上,包括服务器、个人计算机、移动设备和智能卡等。依托庞大的社区和数量众多的运行平台,Java语言的发展前景是非常乐观的。

对于Java平台来说,未来的发展将侧重于以下几个重要的方面。第一个方面是提高开发人员的生产效率。由于Java语言的静态强类型特性,使用Java语言编写的程序代码一般比较繁琐,包含了过多不必要的语法元素,这在一定程度上降低了开发人员的生产效率。大量的时间被浪费在语言本身上,而不是真正需要的业务逻辑上。从另外一个角度来说,Java语言的这种严谨性,对于复杂应用的团队开发是大有好处的,有利于构建健壮的应用。Java语言需要在这两者之间达到一个平衡。Java语言的一个发展趋势是在可能的范围内降低语言本身的语法复杂度。从J2SE 5.0中增强的for循环,到JavaSE 7中的try-with-resources语句和<>操作符,再到Java SE 8中引入的lambda表达式,Java正在不断地简化自身的语法。

第二个方面是提高性能。Java平台的性能一直为开发人员所诟病,这主要是因为Java虚拟机这个中间层次的存在。随着硬件技术的发展,越来越多的硬件平台采用了多核CPU和多CPU的架构。应用程序应该充分利用这些资源来提高程序的运行性能。Java平台需要帮助开发人员更好地实现这个目标。Java SE 7中的fork/join框架是一个高效的任务执行框架。Java SE 8对集合类框架和相关API做了增强,以支持对批量数据进行自动的并行处理。

第三个方面是模块化。一直以来,Java平台所包含的各种功能不同的类库是一个统一的整体。在一个程序的运行过程中,很多类库其实是不需要的。比如对于一个服务器端运行的程序来说,Swing用户界面组件库通常是不需要的。模块化的含义是把Java平台提供的类库划分成不同的相互依赖的模块,程序可以根据需要选择运行时所依赖的模块,只有被选择的模块才会在运行时被加载。模块化的实现不仅可以应用到Java平台本身,也可以应用到Java应用程序的开发中,OpenJDK中的Jigsaw项目提供了这种模块化的支持。第1章 Java 7语法新特性

前面介绍Java所面临的挑战时就提到了Java语言的语法过于复杂的问题。与其他动态语言相比,利用Java语言所编写出来的代码不够简洁和直接。Java语言一直在不断改进自身的语法,以满足开发人员的需求。最大的改动发生在J2SE 5.0版本中。泛型、增强的for循环、基本类型的自动装箱和拆箱机制、枚举类型、参数长度可变的方法、静态引入(import static)和注解等都是在这个版本中添加的。随后的Java SE 6并没有增加新的语法特性,而Java SE 7又增加了一些语法新特性。本章将会着重介绍这些新特性。

OpenJDK中的Coin项目(Project Coin)的目的就是为了收集对Java语言的语法进行增强的建议。最终有6个语法新特性被加入到了Java 7中。这些语法新特性涉及switch语句、整数字面量、异常处理、泛型、资源处理和参数长度可变方法的调用等。

下面将对新特性进行具体的介绍。每节是独立的,读者可以有选择地阅读自己感兴趣的特性的相关章节。需要注意的是,Java 7中与泛型相关的语法新特性将在专门介绍泛型的第12章中介绍。1.1 Coin项目介绍

在介绍具体的新特性之前,有必要介绍一下Coin项目。OpenJDK中的Coin项目的目的是维护对Java语言所做的语法增强。在Coin项目开始之初,曾经广泛地向社区征求提议。在短短的一个月时间内就收到了近70条提议。最后有9条提议被列入考虑之中。在这9条提议中,有6条成为Java 7的一部分,剩下的2条提议会在Java 8中重新考虑,还有1条提议被移到其他项目中实现。这6条被接纳的提议除了本章会介绍的在switch语句中使用字符串、数值字面量的改进、优化的异常处理、try-with-resources语句和优化变长参数的方法调用之外,还包括第12章中会介绍的简化泛型类创建的“<>”操作符。在Java 8中考虑的2条提议则分别是集合类字面量和为List和Map提供类似数组的按序号的访问方式。

和其他对Java平台所做的修改一样,Coin项目所建议的修改也需要通过JCP来完成。这些改动以JSR 334(Small Enhancements to TMthe JavaProgramming Language)的形式提交到JCP。1.2 在switch语句中使用字符串

对于switch语句,开发人员应该都不陌生。大部分编程语言中都有类似的语法结构,用来根据某个表达式的值选择要执行的语句块。对于switch语句中的条件表达式类型,不同编程语言所提供的支持是不一样的。对于Java语言来说,在Java 7之前,switch语句中的条件表达式的类型只能是与整数类型兼容的类型,包括基本类型char、byte、short和int,与这些基本类型对应的封装类Character、Byte、Short和Integer,还有枚举类型。这样的限制降低了语言的灵活性,使开发人员在需要根据其他类型的表达式来进行条件选择时,不得不增加额外的代码来绕过这个限制。为此,Java 7放宽了这个限制,额外增加了一种可以在switch语句中使用的表达式类型,那就是很常见的字符串,即String类型。1.2.1 基本用法

在基于Java 7的代码中使用这个新特性非常简单,因为这个新特性并没有改变switch的语法含义,只是多了一种开发人员可以选择的条件判断的数据类型。但是这个简单的新特性却带来了重大的影响,因为根据字符串进行条件判断在开发中是很常见的。

考虑这样一个应用情景,在程序中需要根据用户的性别来生成合适的称谓,比如男性就使用“×××先生”,女性就使用“×××女士”。判断条件的类型可以是字符串,如“男”表示男性,“女”表示女性。不过这在Java 7之前的switch语句中是行不通的,之前只能添加额外的代码先将字符串转换成整数类型。而在Java 7中就可以根据字符串进行条件判断,如下面的代码清单1-1所示。

代码清单1-1 在switch语句中使用字符串的示例

public class Title{

public String generate(String name, String gender){

String title="";

switch(gender){

case"男":

title=name+"先生";

break;

case"女":

title=name+"女士";

break;

default:

title=name;

}

return title;

}

}

在上面的代码中,Title类的generate方法中的switch语句以传入的字符串参数gender作为判断条件,在对应的case子句中使用的是字符串常量。

注意 在switch语句中,表达式的值不能是null,否则会在运行时抛出NullPointerException。在case子句中也不能使用null,否则会出现编译错误。

根据switch语句的语法要求,其case子句的值是不能重复的。这个要求对字符串类型的条件表达式同样适用。不过对于字符串来说,这种重复值的检查还有一个特殊之处,那就是Java代码中的字符串可以包含Unicode转义字符。重复值的检查是在Java编译器对Java源代码进行相关的词法转换之后才进行的。这个词法转换过程中包括了对Unicode转义字符的处理。也就是说,有些case子句的值虽然在源代码中看起来是不同的,但是经词法转换后是一样的,这就会造成编译错误。代码清单1-2给出了一个例子。

代码清单1-2 switch语句的case子句包含重复值的示例

public class TitleDuplicate{

public String generate(String name, String gender){

String title="";

switch(gender){

case"男":

break;

case"\u7537":

break;

}

return title;

}

}

在上面的代码中,类TitleDuplicate是无法通过编译的。这是因为其中的switch语句中的两个case子句所使用的值“男”和“\u7537”在经过词法转换之后变成一样的。“\u7537”是“男”的Unicode转义字符形式。1.2.2 实现原理

在讨论了switch语句中字符串表达式的用法之后,下面来看看这个新特性是怎么实现的。实际上,这个新特性是在编译器这个层次上实现的。而在Java虚拟机和字节代码这个层次上,还是只支持在switch语句中使用与整数类型兼容的类型。这么做的目的是为了减少这个特性所影响的范围,以降低实现的代价。在编译器层次实现的含义是,虽然开发人员在Java源代码的switch语句中使用了字符串类型,但是在编译的过程中,编译器会根据源代码的含义来进行转换,将字符串类型转换成与整数类型兼容的格式。不同的Java编译器可能采用不同的方式来完成这个转换,并采用不同的优化策略。举例来说,如果switch语句中只包含一个case子句,那么可以简单地将其转换成一个if语句。如果switch语句中包含一个case子句和一个default子句,那么可以将其转换成if-else语句。而对于最复杂的情况,即switch语句中包含多个case子句的情况,也可以转换成Java 7之前的switch语句,只不过使用字符串的哈希值作为switch语句的表达式的值。

为了探究OpenJDK中的Java编译器使用的是什么样的转换方式,需要一个名为JAD的工具。这个工具可以把Java的类文件反编译成Java源代码。在对编译生成Title类的class文件使用了JAD之后,所得到的内容如代码清单1-3所示。

代码清单1-3 包含switch语句的Java类文件反编译之后的结果

public class Title

{

public String generate(String name, String gender)

{

String title="";

String s=gender;

byte byte0=-1;

switch(s.hashCode())

{

case 30007:

if(s.equals("\u7537"))

byte0=0;

break;

case 22899:

if(s.equals("\u5973"))

byte0=1;

break;

}

switch(byte0)

{

case 0://'\0'

title=(new StringBuilder()).append(name).append("\u5148\u751F").

toString();

break;

case 1://'\001'

title=(new StringBuilder()).append(name).append("\u5973\u58EB").

toString();

break;

default:

title=name;

break;

}

return title;

}

}

从上面的代码中可以看出,原来用在switch语句中的字符串被替换成了对应的哈希值,而case子句的值也被换成了原来字符串常量的哈希值。经过这样的转换,Java虚拟机所看到的仍然是与整数类型兼容的类型。在这里值得注意的是,在case子句对应的语句块中仍然需要使用String的equals方法来进行字符串比较。这是因为哈希函数在映射的时候可能存在冲突,多个字符串的哈希值可能是一样的。进行字符串比较是为了保证转换之后的代码逻辑与之前完全一样。1.2.3 枚举类型

以笔者的个人观点来看,Java 7引入的这个新特性虽然为开发人员提供了方便,但是比较容易被误用,造成代码的可维护性问题。提到这一点就必须要说一下Java SE 5.0中引入的枚举类型。switch语句的一个典型的应用就是在多个枚举值之间进行选择。比如代码清单1-1中的性别枚举值“男”和“女”,或者是一个星期中的每一天。在Java SE 5.0之前,一般的做法是使用一个整数来为这些枚举值编号,比如0表示“男”,1表示“女”。在switch语句中使用这个整数编码来进行判断。这种做法的弊端有很多,比如不是类型安全的、没有名称空间、可维护性差和不够直观等。Joshua Bloch最早在他的《Effective Java》一书中提出了一种类型安全的枚举类型的实现方式。这种方式在J2SE 5.0中被引入到标准库,就是现在的enum关键字。

Java语言中的枚举类型的最大优势在于它是一个完整的Java类,除了定义其中包含的枚举值之外,还可以包含任意的方法和域,以及实现任意的接口。这使得枚举类型可以很好地与其他Java类进行交互。在涉及多个枚举值的情况下,都应该优先使用枚举类型。

在Java 7之前,也就是switch语句还不支持使用字符串表达式类型时,如果要枚举的值本身都是字符串,使用枚举类型是唯一的选择。而在Java 7中,由于switch语句增加了对字符串条件表达式的支持,一些开发人员会选择放弃枚举类型而直接在case子句中用字符串常量来列出各个枚举值。这种方式虽然简单和直接,但是会带来维护上的麻烦,尤其是这样的switch语句在程序的多个地方出现的时候。在程序中多次出现字符串常量总是一个不好的现象,而使用枚举类型就可以避免这种情况。

对此,笔者的建议是,如果代码中有多个地方使用switch语句来枚举字符串,就考虑用枚举类型进行替换。1.3 数值字面量的改进

在编程语言中,字面量(literal)指的是在源代码中直接表示的一个固定的值。绝大部分编程语言都支持在源代码中使用基本类型字面量,包括整数、浮点数、字符串和布尔值等。少数编程语言支持复杂类型的字面量,如数组和对象等。Java语言只支持基本类型的字面量。Java 7中对数值类型字面量进行了增强,包括对整数和浮点数字面量的增强。1.3.1 二进制整数字面量

在Java源代码中使用整数字面量的时候,可以指定所使用的进制。在Java 7之前,所支持的进制包括十进制、八进制和十六进制。十进制是默认使用的进制。八进制是用在整数字面量之前添加“0”来表示的,而十六进制则是用在整数字面量之前添加“0x”或“0X”来表示的。Java 7中增加了一种可以在字面量中使用的进制,即二进制。二进制整数字面量是通过在数字前面添加“0b”或“0B”来表示的,如代码清单1-4所示。

代码清单1-4 二进制整数字面量的示例

import static java.lang.System.out;

public class BinaryIntegralLiteral{

public void display(){

out.println(0b001001);//输出9

out.println(0B001110);//输出14

}

}

这种新的二进制字面量的表示方式使得在源代码中使用二进制数据变得更加简单,不再需要先手动将数据转换成对应的八/十/十六进制的数值。1.3.2 在数值字面量中使用下划线

如果Java源代码中有一个很长的数值字面量,开发人员在阅读这段代码时需要很费力地去分辨数字的位数,以知道其所代表的数值大小。在现实生活中,当遇到很长的数字的时候,我们采取的是分段分隔的方式。比如数字500000,我们通常会写成500,000,即每三位数字用逗号分隔。利用这种方式就可以很快知道数值的大小。这种做法的理念被加入到了Java 7中,不过用的不是逗号,而是下划线“_”。

在Java 7中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,其目的主要是方便阅读。一些典型的用法包括每三位数字插入一个下划线来分隔,以及多行数值的对齐,如代码清单1-5所示。

代码清单1-5 在数值字面量中使用下划线的示例

import static java.lang.System.out;

public class Underscore{

public void display(){

out.println(1_500_000);//输出1500000

double value1=5_6.3_4;

int value2=89_3___1;

out.println(value1);//输出56.34

out.println(value2);//输出8931

}

}

虽然下划线在数值字面量中的应用非常灵活,但有些情况是不允许出现的。最基本的原则是下划线只能出现在数字中间,也就是说前后都必须是数字。所以“_100”、“120_”、“0b_101”、“0x_da0”这样的使用方式都是非法的,无法通过编译。这样限制的动机在于降低实现的复杂度。有了这个限制之后,Java编译器只需要在扫描源代码的时候,将所发现的数字中间的下划线直接删除就可以了。这样就和没有使用下划线的形式是相同的。如果不添加这个限制,那么编译器需要进行语法分析才能做出判断。比如“_100”可能是一个整数字面量100,也可能一个变量名称。这就要求编译器的实现做出更加复杂的改动。1.4 优化的异常处理

这一节将要介绍的是Java语言中的异常处理。相信大部分开发人员对于Java语言中使用try-catch-finally语句块进行异常处理的基本方式都有所了解。异常处理以一种简洁的方式表示了程序中可能出现的错误,以及应对这些错误的处理方式。适当地使用异常处理技术,可以提高代码的可靠性、可维护性和可读性。但是如果使用不当,就会产生相反的效果。比如虽然一个方法声明了会抛出某个异常,但是使用这个方法的代码在异常发生的时候,却只能捕获完异常之后就直接忽略它,无法做其他的处理。而为了能够通过编译,又不得不加上catch语句。这势必会造成冗余无用的代码,同时给出不适当的异常设计的一个信号。类似这种错误使用异常的例子在日常开发中还有很多。在Java标准库中同样也有设计失败的异常处理的例子。

Java 7对异常处理做了两个重要的改动:一个是支持在一个catch子句中同时捕获多个异常,另外一个是在捕获并重新抛出异常时的异常类型更加精确。本节的内容并不限于介绍Java 7中关于异常处理的这两个新特性,还会围绕整个异常处理进行展开。这样安排的目的是帮助读者深入理解与Java的异常处理相关的内容。1.4.1 异常的基础知识

Java语言中基本的异常处理是围绕try-catch-finally、throws和throw这几个关键词展开的。具体来说,throws用来声明一个方法可能抛出的异常,对方法体中可能抛出的异常都要进行声明;throw用来在遇到错误的时候抛出一个具体的异常;try-catch-finally则用来捕获异常并进行处理。Java中的异常有受检异常和非受检异常两类。1.受检异常和非受检异常

在异常处理的时候,都会接触到受检异常(checked exception)和非受检异常(unchecked exception)这两种异常类型。非受检异常指的是java.lang.RuntimeException和java.lang.Error类及其子类,所有其他的异常类都称为受检异常。两种类型的异常在作用上并没有差别,唯一的差别就在于使用受检异常时的合法性要在编译时刻由编译器来检查。正因为如此,受检异常在使用的时候需要比非受检异常更多的代码来避免编译错误。

一直以来,关于在程序中到底是该使用受检异常还是非受检异常,开发者之间一直存在着争议,毕竟两类异常都各有优缺点。受检异常的特点在于它强制要求开发人员在代码中进行显式的声明和捕获,否则就会产生编译错误。这种限制从好的方面来说,可以防止开发人员意外地忽略某些出错的情况,因为编译器不允许出现未被处理的受检异常;从不好的方面来说,受检异常对程序中的设计提出了更高的要求。不恰当地使用受检异常,会使代码中充斥着大量没有实际作用、只是为了通过编译而添加的代码。而非受检异常的特点是,如果不捕获异常,不会产生编译错误,异常会在运行时刻才被抛出。非受检异常的好处是可以去掉一些不需要的异常处理代码,而不好之处是开发人员可能忽略某些应该处理的异常。一个典型的例子是把字符串转换成数字时会发生java.lang.NumberFormatException异常,忽略该异常可能导致一个错误的输入就造成整个程序退出。

目前的主流意见是,最好优先使用非受检异常。2.异常声明是API的一部分

这一条提示主要是针对受检异常的。在一个公开方法的声明中使用throws关键词来声明其可能抛出的异常的时候,这些异常就成为这个公开方法的一部分,属于开放API。在维护这个公开API的时候,这些异常有可能会对API的演化造成阻碍,使得编写代码时不得不考虑向后兼容性的问题。

如果公开方法声明了会抛出一个受检异常,那么这个API的使用者肯定已经使用了try-catch-finally来处理这个异常。如果在后面的版本更新中,发现该API抛出这个异常是不合适的,也不能直接把这个异常的声明删除。因为这样会造成之前的API使用者的代码无法通过编译。

因此,对于API的设计者来说,谨慎考虑每个公开方法所声明的异常是很有必要的。因为一旦加了异常声明,在很长的一段时间内都无法甩掉它。这也是为什么推荐使用非受检异常的一个重要原因,非受检异常不需要声明就可以直接抛出。但是对于一个方法会抛出的非受检异常,也需要在文档中进行说明。1.4.2 创建自己的异常

和程序中的其他部分一样,异常部分也需要经过仔细的考虑和设计。开发人员一般会花费大量的精力对程序的主要功能部分进行设计,而忽略对于异常的设计。这会对程序的整体架构造成影响。在对异常部分进行设计的时候,考虑下面几个建议。1.精心设计异常的层次结构

一般来说,一个程序中应该要有自己的异常类的层次结构。如果只打算使用非受检异常,至少需要一个继承自RuntimeException的异常类。如果还需要使用受检异常,还要有另外一个继承自Exception的异常类。如果程序中可能出现的异常情况比较多,应该在不同的抽象层次上定义相关的异常,并形成一个完整的层次结构。这个异常的层次结构与程序本身的类层次结构是相对应的。不同抽象层次上的代码应该只声明抛出同一层次上的相关异常。

比如一个典型的Web应用按照自顶向下的顺序一般分成展现层、服务层和数据访问层。与之对应的异常也应该按照这个层次结构来进行划分。数据访问层的代码应该只声明抛出与访问数据相关的异常,如数据库连接和操作相关的异常。这么做的好处是工作于某个抽象层次上的开发人员不需要去了解其他层次上的细节。比如服务层开发人员会调用数据访问层的代码,他只需要关心数据访问可能出现异常即可,而并不需要去关心这是一个数据库访问异常,还是一个文件系统访问异常。这种抽象层次的划分对系统的演化是比较重要的。假如系统以后不再使用数据库作为数据访问的实现,服务层的异常处理逻辑也不会受到影响。

一般来说,对于程序中可能出现的各种错误,都需要声明一个异常类与之对应。有些开发人员会选择一个大而全的异常类来表示各种不同类型的错误,利用这个异常的消息来区分不同的错误。比如声明一个异常类BaseException,不管是数据访问错误还是用户输入的数据格式不对,都会抛出同一个异常,只是使用的消息内容不同。当采用这种异常设计方式的时候,异常的处理者只能根据异常消息字符串的内容来判断具体的错误类型。如果异常的处理者只是简单地进行日志记录或重新抛出此异常,这种方式并没有太大的问题。如果异常的处理者需要解析异常的消息格式来判断具体类型,那么这种方式就是不可取的,应该换成不同的异常类。

采用这种异常层次结构会遇到的一个常见的异常处理模式是包装异常。包装异常的目的在于使异常只出现在其所对应的抽象层次上。当一个异常抛出的时候,如果没有被捕获到,就会一直沿着调用栈往上传递,直到被上层方法捕获或是最终由Java虚拟机来处理。这种传递方式会使这个异常跨越多个抽象层次的边界,使得上层代码看到不需要关注的底层异常。为此,在一个异常要跨越抽象层次边界的时候,需要进行包装。包装之后的异常才是上层代码需要关注的。

对一个异常进行包装是一件非常简单的事情。从JDK 1.4开始,所有异常的基类java.lang.Throwable就支持在构造方法中传入另外一个异常作为参数。而这个参数所表示的异常被包装在新的异常中,可以通过getCause方法来获取。代码清单1-6给出了一个异常包装的示例,即把底层的IOException包装成更为抽象的DataAccessException。使用DataAccessGateway类的上层代码只需要知道DataAccessException即可,并不需要知道IOException的存在。

代码清单1-6 使用异常包装技术的示例

public class DataAccessGateway{

public void load()throws DataAccessException{

try{

FileInputStream input=new FileInputStream("data.txt");

}

catch(IOException e){

throw new DataAccessException(e);

}

}

}

在使用异常包装的时候,一个典型的做法就是为每个层次定义一个基本的异常类。这个层次的所有公开方法在声明异常的时候都使用这个异常类。所有这个层次中出现的底层异常都被包装成这个异常。2.异常类中包含足够的信息

异常存在的一个很重要的意义在于,当错误发生的时候,调用者可以对错误进行处理,从产生的错误中恢复。为了方便调用者处理这些异常,每个异常中都需要包含尽量丰富的信息。异常不应该只说明某个错误发生了,还应该给出相关的信息。异常类是完整的Java类,因此在其中添加所需的域和方法是一件很简单的事情。

考虑下面一个场景,当用户进行支付的时候,如果他的当前余额不足以完成支付,那么在所抛出的异常信息中,可以包含当前所需的金额、余额和其中的差额等信息。这样异常处理者就可以提供给用户更加具体的出错信息以及更加明确的解决方案。3.异常与错误提示

对于与用户进行交互的程序来说,需要适当区分异常与展示给用户的错误提示。通常来说,异常指的是程序的内部错误。与异常相关的信息,主要是供开发人员调试时使用的。这些信息对于最终用户来说是没有意义的。一般来说,普通用户除了重新执行出错的操作之外,没有其他应对办法。因此,程序需要保证在直接与用户交互的代码层次上,捕获所有的异常,并生成相应的错误提示。比如在一个servlet中,要确保在产生HTTP响应的时候捕获全部的异常,以避免用户看到一个包含异常堆栈信息的错误页面。

有些开发人员会直接将异常自带的消息作为给用户的错误提示。这个时候需要注意异常消息的国际化问题。只需要把异常与Java中的java.util.ResourceBundle结合起来,就可以很容易地实现异常消息的国际化。代码清单1-7给出了一个支持国际化异常消息的异常类的基类LocalizedException。

代码清单1-7 支持国际化异常消息的异常类的基类

public abstract class LocalizedException extends Exception{

private static final String DEFAULT_BASE_NAME="com/java7book/chapter1/

exception/java7/messages";

private String baseName=DEFAULT_BASE_NAME;

protected ResourceBundle resourceBundle;

private String messageKey;

public LocalizedException(String messageKey){

this.messageKey=messageKey;

initResourceBundle();

}

public LocalizedException(String messageKey, String baseName){

this.messageKey=messageKey;

this.baseName=baseName;

initResourceBundle();

}

private void initResourceBundle(){

resourceBundle=ResourceBundle.getBundle(baseName);

}

protected void setBaseName(String baseName){

this.baseName=baseName;

}

protected void setMessageKey(String key){

messageKey=key;

}

public abstract String getLocalizedMessage();

public String getMessage(){

return getLocalizedMessage();

}

protected String format(Object……args){

String message=resourceBundle.getString(messageKey);

return MessageFormat.format(message, args);

}

}

在使用的时候,每个需要国际化的异常类只需要继承LocalizedException,并实现getLocalizedMessage方法即可。代码清单1-8是之前提到的支付余额不足时抛出的异常类。在子类的构造方法中指定异常消息在消息资源文件中对应的名称。使用format方法可以对消息进行格式化。

代码清单1-8 继承自支持国际化异常消息的异常类的子类

public class InsufficientBalanceException extends LocalizedException{

private BigDecimal requested;

private BigDecimal balance;

private BigDecimal shortage;

public InsufficientBalanceException(BigDecimal requested, BigDecimal balance){

super("INSUFFICIENT_BALANCE_EXCEPTION");

this.requested=requested;

this.balance=balance;

this.shortage=requested.subtract(balance);

}

public String getLocalizedMessage(){

return format(balance, requested, shortage);

}

}1.4.3 处理异常

处理异常的基本思路也比较简单。一般来说就两种选择:处理或是不处理。如果某个异常在当前的调用栈层次上是可以处理和应该处理的,那么就应该直接处理掉;如果不能处理,或者不适合在这个层次上处理,就可以选择不理会该异常,而让它自行往更上层的调用栈上传递。如果当前的代码位于抽象层次的边界,就需要首先捕获该异常,重新包装之后,再往上传递。

决定是否在某个方法中处理一个异常需要判断从异常中恢复的方式是否合理。比如一个方法要从文件中读取配置信息,进行文件操作时可能抛出IOException。当出现异常的时候,如果可以采取的恢复措施是使用默认值,那么在这个方法中处理IOException就是合理的。而在同样的场景中,如果某些配置项没有合法的默认值,必须要手工设置一个值,那么读取文件时出现的IOException就不应该在这个方法中处理。

在确定了需要对异常进行处理之后,按照程序本身的逻辑来处理即可。下面将要介绍的是一个处理异常时容易忽略的问题——消失的异常。

开发人员对异常处理的try-catch-finally语句块都比较熟悉。如果在try语句块中抛出了异常,在控制权转移到调用栈上一层代码之前,finally语句块中的语句也会执行。但是finally语句块在执行的过程中,也可能会抛出异常。如果finally语句块也抛出了异常,那么这个异常会往上传递,而之前try语句块中的那个异常就丢失了。代码清单1-9给出了一个示例,try语句块会抛出NumberFormatException,而在finally语句块中会抛出ArithmeticException。对这个方法的使用者来说,他最终看到的只是finally语句块中抛出的ArithmeticException,而try语句中抛出的NumberFormatException消失不见了。

代码清单1-9 异常消失的示例

public class DisappearedException{

public void show()throws BaseException{

try{

Integer.parseInt("Hello");}

catch(NumberFormatException nfe){

throw new BaseException(nfe);}finally{

try{

int result=2/0;

}catch(ArithmeticException ae){

throw new BaseException(ae);}

}}

}

其实这样的例子在日常开发中也是比较常见的。比如在打开一个文件进行读取的时候,肯定需要用try-catch语句块来捕获其中的IOException,并且在finally语句块中关闭文件输入流。在关闭输入流的时候可能会抛出异常,造成之前在读取文件时产生的异常丢失。还有一个典型的情况发生在数据库操作的时候,在finally语句块中关闭数据库连接。由于之前产生的异常丢失,开发人员可能无法准确定位异常的发生位置,造成错误的判断。

对这种问题的解决办法一般有两种,一种是抛出try语句块中产生的原始异常,忽略在finally语句块中产生的异常。这么做的出发点是try语句块中的异常才是问题的根源。另外一种是把产生的异常都记录下来。这么做的好处是不会丢失任何异常。在Java 7之前,这种做法需要实现自己的异常类,而在Java 7中,已经对Throwable类进行了修改以支持这种情况。

第一种做法的实现方式如代码清单1-10所示。

代码清单1-10 抛出try语句块中产生的原始异常的示例

public class ReadFile{

public void read(String filename)throws BaseException{

FileInputStream input=null;

IOException readException=null;

try{

input=new FileInputStream(filename);

}catch(IOException ex){

readException=ex;

}finally{

if(input!=null){

try{

input.close();

}catch(IOException ex){

if(readException==null){

readException=ex;

}

}

}

if(readException!=null){

throw new BaseException(readException);

}

}

}

}

第二种做法需要利用Java 7中为Throwable类增加的addSuppressed方法。当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过addSuppressed方法把这些被抑制的方法记录下来。被抑制的异常会出现在抛出的异常的堆栈信息中,也可以通过getSuppressed方法来获取这些异常。这样做的好处是不会丢失任何异常,方便开发人员进行调试。代码清单1-11给出了使用addSuppressed方法记录异常的示例。

代码清单1-11 使用addSuppressed方法记录异常的示例

public class ReadFile{

public void read(String filename)throws IOException{

FileInputStream input=null;

IOException readException=null;

try{

input=new FileInputStream(filename);

}catch(IOException ex){

readException=ex;

}finally{

if(input!=null){

try{

input.close();

}catch(IOException ex){

if(readException!=null){

readException.addSuppressed(ex);

}

else{

readException=ex;

}

}

}

if(readException!=null){

throw readException;

}

}

}

}

这种做法的关键在于把finally语句中产生的异常通过addSuppressed方法加到try语句产生的异常中。1.4.4 Java 7的异常处理新特性

下面详细介绍Java 7中引入的与异常处理相关的新特性。1.一个catch子句捕获多个异常

在Java 7之前的异常处理语法中,一个catch子句只能捕获一类异常。在要处理的异常种类很多时这种限制会很麻烦。每一种异常都需要添加一个catch子句,而且这些catch子句中的处理逻辑可能都是相同的,从而会造成代码重复。虽然可以在catch子句中通过这些异常的基类来捕获所有的异常,比如使用Exception作为捕获的类型,但是这要求对这些不同的异常所做的处理是相同的。另外也可能会捕获到某些不应该被捕获的非受检异常。而在某些情况下,代码重复是不可避免的。比如某个方法可能抛出4种不同的异常,其中有2种异常使用相同的处理方式,另外2种异常的处理方式也相同,但是不同于前面的2种异常。这势必会在catch子句中包含重复的代码。

对于这种情况,Java 7改进了catch子句的语法,允许在其中指定多种异常,每个异常类型之间使用“|”来分隔,如代码清单1-12所示。ExceptionThrower类的manyExceptions方法会抛出ExceptionA、ExceptionB和ExceptionC三种异常,其中对ExceptionA和ExceptionB采用一种处理方式,对ExceptionC采用另外一种处理方式。

代码清单1-12 在catch子句中指定多种异常

public class ExceptionHandler{

public void handle(){

ExceptionThrower thrower=new ExceptionThrower();

try{

thrower.manyExceptions();

}catch(ExceptionA|ExceptionB ab){

}catch(ExceptionC c){

}

}

}

这种新的处理方式使上面提出的问题得到了很好的解决。需要注意的是,在catch子句中声明捕获的这些异常类中,不能出现重复的类型,也不允许其中的某个异常是另外一个异常的子类,否则会出现编译错误。如果在catch子句中声明了多个异常类,那么异常参数的具体类型是所有这些异常类型的最小上界。

关于一个catch子句中的异常类型不能出现其中一个是另外一个的子类的情况,实际上涉及捕获多个异常的内部实现方式。比如在代码清单1-13中,虽然NumberFormat-Exception是RuntimeException的

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载