目录
2.6通过在不同的平台上安装对应的Java 虚拟机,可以实现平台无关。
9.1.5 Java编程语言提供几种预定义的异常,最常见的异常描述:
本章内容:
l 程序设计语言的分类
l Java的发展过程
l Java程序的类型
l 编译和执行Java程序
程序设计语言是学习计算机技术的基础,它经历了较长的发展过程,有许多不同的分类方法。
1.按发展过程分类:
a.机器语言:是以二进制代码的形式组成的机器指令集合,不同的机器有不同的机器语言。这种语言编制的程序运行效率极高,但程序很不直观,编写很简单的功能就需要大量代码,重用性差,而且编写起来效率比较低,很容易出现错误。
b.汇编语言:比机器语言直观,它将机器指令进行了符号化,并增加了一些功能,如宏,符号地址等,存储空间的安排由机器完成,编程工作相对进行了极大的简化,使用起来方便了很多,错误也相对减少。但不同的指令集的机器仍有不同的汇编语言,程序重用性也很低。
c.高级语言:高级语言是与机器不相关的一类程序设计语言,读写起来更接近人类的自然语言,因此,用高级语言开发的程序可读性较好,便于维护。同时,由于高级语言并不直接和硬件相关,其编制出来的程序可移植性也要好得多。常见的高级语言Pascal,c,c++,Basic...Java就是高级语言的一种。
d.第四代语言(4GL)
具有一定的智能,更接近于日常语言,它对语言的概括更为抽象,从而使语言也更为简洁。
2.按执行方式分类
a.编译执行的语言
通过工具软件将源代码经过目标代码转换成机器代码,即可执行程序,然后直接交操作系统执行。这类程序语言的优点是执行速度比较快,缺点是编译器与机器之间存在一定的依赖性,不同操作系统需要的编译器可能不同,另外,在一个系统上编译的程序到另一系统上不一定正确运行。如C++,Pascal...
b.解释执行的语言
解释执行是程序读入一句执行一句,而不需要整体编译链接,这样的语言与操作系统的相关性相对较小,但运行效率低,而且需要一定的软件环境来做源代码的解释器。当然,有些解释执行的程序并不是使用源代码来执行的,而是需要预先编译成一种解释器能够识别的格式,再解释执行。如Java。
3.按思维模式分类
用计算机处理问题是个复杂的过程。需要许多设想,小心规划,逻辑的精度和细节的考虑。另一方面,他又具有挑战性,令人振奋,为个人的创造力和表达能力提供了令人满意的空间。
程序设计语言总是需要以某种思维方式进行设计和实现,因此不同的语言可能有不同的思维方式。目前存在两种思维方式。
a.面向过程的程序设计语言
过程化的解决问题的方法是把它分裂成几个小部分,然后再求解每个较小的问题。最后,汇总这些方案来求解整个问题。这种方法的缺点是模块间互相依赖,难以升级,而且互用性差,解决的方法是使用面向对象的程序设计模型。
b.面向对象的程序设计语言
OOP(面向对象程序设计)的设计思想:
现实世界有对象组成,这些对象被分成类,对象是类的一个特例。OOP根据现实世界的对象来描述软件系统。考察一个公司的职员,他是一个对象,它包含了某些属性:姓名,年龄,家庭住址,电话号码......他也包含了某些行为:允许用户输入或者现实职员的这些属性。OOP的好处是逼真地建模,并且对变动有弹性。
Java是一种纯面向对象的编程语言。
Java的诞生需追溯到1991年。当时,在Sun公司内,有一个称为Green的项目,这个项目的工程师受命设计一种小型的计算机语言,用于机顶盒、家电控制芯片等消费类设备。由于这些设备功能有限、内存较小,所以这种语言必须尽可能地小,而且应能生成非常紧凑的代码,最重要的一点是,这些设备所采用的处理芯片多种多样,要使这个语言有强大的生命力,就绝不能够受限于任何一种体系结构。
为了实现这一目标,项目组准备尝试一种古老的模型:设计出一种“可移植”的语言。在执行前,生成一个“中间码”,在任何一种机器上安装了特定的解释器,就可以运行这个“中间码”。
这样“中间码”一定会非常小,而解释器也不大,就可以达到这个目标。
这个小组大部分员工都熟悉C++语言,使得这个新语言走向了“面向对象”的道路。这个语言原来的名字是“Oak“(项目组领导办公室窗外的橡树名),但后来由于“Oak”这一名字己被占用,所以选用“Java”一种咖啡的名字作为新语言的名字,所以现在说到Java语言,大家都会想到那杯热气腾腾的咖啡!也许,Sun公司希望Java语言能够象咖啡一样被人们接受、喜爱吧!
然而Java语言并未被Sun公司和消费类家电公司所接受。接着该小组又开始了许多其它方面的尝试,均惨遭失败,不得不在1994年解散。
而此时,Internet上的www服务越来越普遍,人们需要一个好用的浏览器,以便更好地阅读Web页面。这时Green项目组的旧部意识到可用Java语言写一个浏览器。这个最终发展成目前的HotJava浏览器。
HotJava这个试验品在1995年5月召开的SunWorld大会上首次亮相,从而引起了遍及全球、至今未衰的Java热。
1995年秋NetScape决定让其浏览器支持Java,并在1996年初发布了支持Java的版本。这意味着Java语言开始大流行,后来,IBM等许多著名IT公司都注册了Java使用许可证,而且视Sun公司为敌的微软公司也在IE中提供了对Java的支持,并在Window中提供了Java虚拟机。
Java语言的初露锋芒,让Sun公司意识到它的价值,于是在1996年初发布了Java的第一个版本。几个月后,又发布了Java1.02版,但该版本的Java语言并不是十分完备,不适合用于正规的应用程序的开发。但仅过了一小段时间,Sun公司就发布了令人满意的Java1.1版,它实现了绝大部分目标。
在1998年召开的JavaOne大会上,Sun公司发布了Java1.2版,这就是Java 2.
[注:JavaScript是网景公司发明的,原名LiveScript]
1.3 Java程序的类型
Java程序有二类:应用程序和小应用程序(applications和applets)。
应用程序是可在任何操作系统提示下执行的程序。单独的应用程序可以是基于窗口的应用程序或是控制台应用程序。基于窗口的应用程序有图形用户界面,而控制台应用程序是基于字符的应用程序,没有图形用户界面。单机的应用程序使用单机的资源。网络应用程序可使用网上资源。分布式应用可访问网上跨许多计算机执行的对象。
允许应用在本地计算机上读入和写出文件。单独应用程序驻留在本地机器的硬盘上。当需要执行此应用程序时,把它从硬盘装入到内存,并执行之。
小应用程序是在Web页面内执行的Java程序。因此,不象应用程序,小应用程序需要Java使能的浏览器,如:Microsoft Internet Explorer4.0或以上版本、Netscape Navigator4.0或以上版本、或HotJava。小应用程序在用户通过Web浏览器装入Web页面时被装入和执行。当包含小应用程序的Web页面被显示时,用户与此小应用程序交互。
Applet使用简单,因为用户启动applet必须做的所有事是访问Web页面。另一方面,应用程序必须在执行它之前下载到本地计算机。Applets有图形用户界面。Applets的安全特权比应用程序差。Applet只可访问宿主机上资源,他不可访问被下载计算机上的文件。
与应用程序相反,小应用程序可以驻留在远程计算机上。当本地机器需要执行applet时,applet从远程系统被装入到本地机计算机的内存。一旦本地机计算机可用时,由浏览器解释此applet,并与本地提供的库资源链接起来,然后执行之。
1.4 编译和执行Java程序
有两种方法可以编译和执行Java程序:使用命令行或其他程序,如集成开发环境或者文本编辑器。
(1)使用命令行
(a)用记事本写一源文件并保存为".java"文件,例hello.java
(b)设置环境变量path和classpath(可设置系统变量,以便以任何用户身份登录都能使用
—我的电脑|属性|高级|环境变量)
例 path: d:\jdk1.3.1_07\bin;
classpath: d:\ jdk1.3.1_07\lib;
(c)javac hello.java //javac程序是Java的编译器。它把hello.java文件编译成hello.class文件
(d)java welcome //java程序是Java解释器,它负责解释执行编译器生成到class文件中的字节码。
(2)使用集成开发环境
如果有一个Java集成开发环境,就不需要做上面这些繁琐的事情了。集成开发环境不仅可以提供一个能够识别Java语法的编辑器,使读者简单地编译和执行Java程序,还可以建立应用程序或者小程序的框架。并调试程序,将做好的程序打包成可执行文件等等。即集成开发环境集成了编辑、编译、跟踪调试、执行及打包Java应用程序的一切工具,如Microsoft公司出品的Visual J++及Borland公司出品的JBuilder,它们对系统资源的侵占非常大,通常建议用户有256MB内存,目前流行的IDE开发工具为eclipse。
初学阶段,不需要编写很复杂的Java程序,对调试方面的需求也并不迫切,使用文本编辑器和JDK就足够了。
(3)从文本编辑器中编译和运行程序
JCreator,FreeJava是Java开发环境的一个图形用户界面。
第2章 JAVA2介绍
本章内容:
• 了解Java及其发展过程
• 了解 Java 的特点和优点
• 了解 Java 的运行机制
• 了解 Java 程序的基本结构
• 掌握如何编译和运行 Java 程序
2.1 什么是JAVA
• Java 完全面向对象的编程语言。
• Java 可用来生成两类程序:应用程序、小应用程序( Applet )。
• Java 既是一种解释执行的语言,也是一种编译执行的语言。
2.1.1 JAVA语言的版本
Java 1.0 小型版本
Java 1.1 1997年,改进了用户界面
Java 1.2 1998年,改进图形用户界面,数据库连接
Java 1.3 2000年,增加新的核心功能
Java 1.4 2002年,安全性的改进,对COBRA的支持等
• 面向对象
² 在 Java 中任何东西都是对象,因此,重点集中在数据以及应用程序中的数据操作方法。
• 易于学习
² Java的风格类似于C++,因而对C++程序员而言非常容易掌握Java编程技术;
² Java摒弃了C++中容易引发程序错误的地方,如指针操作和内存管理;
• 平台无关性
² Java 程序被编译成一种与体系结构无关的字节代码
² 只要安装了Java运行时系统Java程序可以在任意的处理器上运行
• 分布式
Java提供了包容广泛的例程库,可处理像HTTP和FTP这样的TCP/IP协议。Java应用程序可通过一个特定的URL来打开并访问对象,就像访问本地文件系统那样简单。
• 健壮性
² Java在编译和运行程序时都要对可能出现的问题进行检查
² 它提供自动垃圾收集来进行内存管理
² 面向对象的异常处理机制
• 安全
² Java语言提供的安全
² 编译器提供的安全
² 字节码校验器
² 类加载器
• 可移植性
² 源代码级
² 目标代码级
• 解释执行
² 编译器 javac 将处理.java源文件生成类文件
² 类文件可通过名称为 java 的加载实用程序解释执行,将Java 字节码转换为机器可执行代码。
• 高性能
通过JIT编译器首先将字节码编译成固有代码,将结果缓存下来,然后在需要时调用它们。
• 多线程
² Java语言内置支持多线程的功能
² Java 类库提供了Thread 类
• 动态
Java自身的设计使它适合于一个不断发展的环境。在Java类库中可以自由地加入新的方法和实例变量而不会影响用户程序的执行。
Java是解释执行的高级编程语言
编译型源程序 Java虚拟机 |
源程序 |
字节码程序 |
Java源程序 |
目标程序 |
编译 连接 |
编译 |
执行 |
解释 执行 |
.java |
.class |
q 一次编程到处执行,Write Once, Run AnyWhere
q 而传统的计算机应用程序需要针对不同的应用平台进行开发
2.6通过在不同的平台上安装对应的Java 虚拟机,可以实现平台无关。
所以… Write once , run anywhere!
• Java虚拟机是一种利用软件方法实现的抽象化的计算机,基于下层的操作系统和硬件平台,可以在上面执行Java的字节码程序。
• Java虚拟机将在内部创建一个运行时系统,帮助以下列方式执行代码:
– 加载 .class 文件
– 管理内存
– 执行垃圾收集
q J2ME(Java2 Micro Edition),针对嵌入式设备的编程技术。
q J2SE(Java2 Standard Edition),针对桌面计算机开发
q J2EE(Java2 Enterprise Edition),针对企业级的分布式应用,主要涉及JDBC、RMI、EJB、Servlet、JSP以及Web Service、XML等方面的技术
q 嵌入式技术(如嵌入式设备、移动通讯设备、手持式设备、测试仪器等);
q 基于Application/Applet、JavaBean的PC应用;
q 基于动态网站的Servlet、JSP 应用,实现Web应用程序等
q 基于EJB的J2EE企业级分布式应用等,主要是解决企业级异构环境(不同主机、不同的操作系统)下的编程。
• 使用“实时 (Just In Time)” 编译器,也称为JIT编译器。可以提高 Java 代码的执行速度。
• SUN公司在Java 2 发行版中提供了JIT编译器,JIT编译器是Java虚拟机的一部分。
• JIT 的主要用途是将字节码指令集转换为针对某一特定微处理器的机器代码指令。
编译时 |
.java |
(源代码) |
.class |
(字节码) |
运行时 |
类加载器 |
字节码校验器 |
解释器 |
JIT代码 生成器 |
硬件 |
网络 |
• JDK是有助于程序员开发Java 程序的 Java 开发工具包包括:
– 类库
– 编译器
– 调试器
– Java 运行时环境( JRE )
• JDK 提供的常用工具
Ø javac:Java编译器,将Java源代码换成字节代码
Ø java:Java解释器,直接从类文件执行Java应用程序代码
Ø appletviewer(小程序浏览器):一种执行HTML文件上的Java小程序类的Java浏览器
Ø javadoc:根据Java源代码及其说明语句生成的HTML文档
Ø jdb:Java调试器,可以逐行地执行程序、设置断点和检查变量
Ø javah:产生可以调用Java过程的C过程,或建立能被Java程序调用的C过程的头文件
Ø Javap:Java反汇编器,显示编译类文件中的可访问功能和数据,同时显示字节代码含义
|
import语句 |
主类名称 |
方法体 |
步骤:a.编写 b.编译和运行
Java API 文档是一种非常有用的文档,描述许多 java 的内置功能,包含类、包、接口等的帮助。
加载 API 文档的步骤:
- 打开 Internet Explorer。
- 键入下列 URL http://java.sun.com/j2se/1.4.1/docs/api/index.html
- 通过单击相关主题,可以浏览网页。
• Java 是一种面向对象的编程语言,主要用于 Internet 应用程序。
• Java 可用来生成两类程序:应用程序、小应用程序( Applet )。
• Java 既是一种解释执行的语言,也是一种编译执行的语言。
• Java 的特点包括:
² -面向对象
² -易于学习
² -平台无关性
• Java 虚拟机是 Java 编程语言的核心。
• Java 运行时环境 (JRE) 包含一方面与硬件交互、另一方面又与程序交互的 JVM。
• Java 开发工具包有助于开发 Java 程序。
• JDK 提供多种工具,这些工具位于 JDK 的 bin 目录下,具体如下:
– javac
– java
– appletviewer
本章内容:
l Java的基本语法
l 数据类型
l 运算符
l 流程控制语句
l 数组
l 注释
代码主要是放入类中
Java是严格区分大小写的
w Java是一种自由格式的语言
程序代码分为结构定义语句和功能执行语句。
功能执行语句的最后必须用(;)结束。
w Java中的标识符
java中的包、类、方法和变量的名字。可由任意顺序的大小写字母、数字、下划线(_)和美元符号($)组成,但标识符不能以数字开头,不能是关键字。
如:_userName、$username、username、user_name、class、100.1、Hello World
Java中的关键字
abcstract |
eo |
implement |
privste |
this |
boolean |
double |
import |
proterted |
throw |
break |
else |
instanceof |
public |
throws |
byte |
extend |
int |
return |
transient |
case |
false |
interface |
short |
true |
catch |
final |
long |
static |
try |
char |
finally |
native |
strictfp |
void |
f |
float |
new |
super |
volatile |
f |
for |
null |
seitch |
while |
default |
if |
package |
|
synchronized |
w 整型常量
十进制(18),十六进制(0x18),八进制(018)
w 长整型常量
13L
w 单精度浮点数
5.1f,4f,2e3f,0f
w 双精度浮点数
5.1,4,2e-3,0d
w 布尔常量
true和false
w 字符常量
‘a’, ‘ 6’, ‘\u0001’(Unicode值)
‘\r’表示接受键盘输入,相当于按下了回车键
‘\n’是换行
‘\t’是制表符,相当于table键
‘\b’是退格键,相当于Back Space;
‘\ ‘’是单引号
‘\”’是双引号
‘\\’反斜杠
w 字符串常量
“Hello World”, “7431”, “noise \nXXX”
w null常量
null常量只有一个值,用null表示,表示对象的引用为空
w 变量就是系统为程序分配的一块内存单元,用来存储各种类型的数据。根据所存储的数据类型的不同,有各种不同类型的变量。变量名代表这块内存中的数据。
w Int x=0,y;
y=x+3;
执行这两条语句的过程中,内存分配及变化情况。X在被取值前必须被初始化。
q 基本数据类型(int、float、char、boolean、byte等),Java中没有unsigned类型
q 引用数据类型(数组、类、接口等)。
基本数据类型 |
引用数据类型 |
数据类型 |
数值型 |
字符型(char) |
布尔型(boolean) ** if(x=0) |
类(class) |
接口(interface) |
数组 |
整数类型(byte,short,int,long) |
浮点类型(float,double) |
q 不同类型的数据混合运算时,系统自动将数据从低级转换到高级(域小的自动转化为域大的数据类型)
q 高级的数据类型转换到低级时,必须强制进行数据类型转换。
Char
Byte short int long
Float double
数字类型间的合法转换
q 不能对boolean类型进行类型转换。
q 不能把对象类型转换成不相关类的对象。
q 转换过程中可能导致溢出或损失精度
q int i = 8; byte b=(byte)i;
q (byte)255 == -1 (byte)0x5634 == 0x34
q 浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入
(int)23.7 == 23 (int)-45.89f == -45
q 算术运算符: +,―,*,/,%,++,――
q 关系运算符: >,<,>=,<=,==,!=
q 布尔逻辑运算符: !,&&,||
q 位运算符: >>,<<,>>>,&,|,^,~
q 赋值运算符: =,及其扩展赋值运算符如+=,―=,*=,/=等。
q 条件运算符: ? :
q 其它
ü 分量运算符 · ,
ü 下标运算符 [] ,
ü 实例运算符 instanceof,
ü 内存分配运算符new,
ü 强制类型转换运算符 (类型),
ü 方法调用运算符()
基本上与C/C++相同,但不同的是:
q 新增 instanceof(关系运算符):识别某对象是否为某类的对象,即测试它左边的对象是否是它右边的类的实例,返回boolean类型
void Cook(Person man)
{ if(man instanceof Chinese)
{//如果识别是中国人则按照中国人的习惯来Cook
}
else if(man instanceof American)
{//如果识别是美国人则按照美国人的习惯来Cook
}
}
q 改造“+”运算符:使其能够适应字符串运算:System.out.println("How Are"+"You !");
q “+”
“ABCD”+”EFGH”à“ABCDEFGH”
“ABCD”+‘E’+’F’ à“ABCDEF”
‘A’+’B’à两个字符的内码相加
q 字符串赋值
String X=“ABCD”;
String Y=X;
X=“EFGH”;
此时Y仍然为“ABCD”,因为每进行一次字符串赋值,系统将重新构造出一个新的字符串内存地址,然后将字符串内容拷贝
可以采用StringBuffer来代替,以免产生大量的字符串空间的垃圾
(1)相等判断(是否同一个对象的引用)
q 运算符== !=
q 示例
theObject == null
otherObject != theObject
(2)类型判断(是否是每个类的实例)
q 运算符instanceof
q 示例
theObject instanceof Object
“” instanceof String
1) . , [] , () 9) &
2) ++ , -- , ! , ~,instanceof 10) ^
3) new (type) 11) |
4) * , / , % 12) &&
5) + , - 13) ||
6) 14) ?:
7) > , < , >= , <= 15) = , += , -= , *= , /= , %= , ^=
8) == , != 16)&= , |= ,<<= , >>= , >>>=
可以使用语句System.out.print(x) 向控制台打印数字x。这条命令将按照x类型所允许的最多非零数字位数打印x。如:
x=10000.0/3.0;
System.out.println(x);
打印结果是:
3333.3333333333335
如果想显示的是美元、美分等内容,这么做有些问题,可以通过控制显示格式处理输出。Java.text包中的NumberFormat类有三个方法可以产生下列数据的标准格式化器:
l 数字
l 货币
l 百分数
值10000.0/3.0在这三种格式中打印如下:
3,333.333 NumberFormat.getNumberInstance()
$3,333.33 NumberFormat.getCurrencyInstance()
333,333% NumberFormat.getPercentInstance()
/*
double x=10000.0/3.0;
// NumberFormat formatter=NumberFormat.getNumberInstance();
NumberFormat formatter=NumberFormat.getCurrencyInstance(Locale.US);
// formatter.setMaximumFractionDigits(4);
// formatter.setMinimumIntegerDigits(6);
String s=formatter.format(x);
System.out.println(s);
*/
3.6.1主要的语句
q 分支语句:if-else, switch
q 循环语句:while, do-while, for
q 与程序转移有关的其它语句:break, continue, return
q 例外处理语句:try-catch-finally, throw
3.6.2特点
基本上类同于C/C++语言,但不同的是:
(1)无goto 语句。
(2)改造了continue语句,跳转到括号指明的外层循环中。
(3)改造了break语句,跳转到该语句标号处,从此再执行该语句。
注意:
break与continue的标准功能继续保留!
块作用域
块或复合语句是用一对花括号括起的任意数量的简单Java语句。块定义了变量的作用范围。块可以嵌套在其他块中。下面是一个块嵌套在main方法中的例子:
{
int n;
...
{
int k;
int n;
...
}
}
If语句
w 条件语句是程序设计语言中最基本的流程控制语句,几乎任何一门程序设计语言的条件语句都用到了if关键字,因而条件语句也被称为if语句。条件语句分简单语句和复合语句
语法:if(Boolean){statement1}
else{statement2}
public class ifElse
{
public static void main(String args[])
{
int month;
String season=" ";
for(month=1;month<=13;month++)
{
if(month==3||month==4||month==5)
season="春季(Spring)";
else if(month==6||month==7||month==8)
season="夏季(Summer)";
else if(month==9||month==10||month==11)
season="秋季(Autumn)";
else if(month==12||month==1||month==2)
season="冬季(Winter)";
else season="不合理月份(Bogus)";
System.out.println(month+"月是"+season);
}
}
}
public class ExampleIf
{
public static void main(String[] args)
{
boolean a=false;
boolean b=false;
if(a==b)
{
System.out.println("a等于b");
}
else
{
System.out.println("a不等于b");
}
}
}
switch
w switch是条件语句的一个变种 ,运用switch语句时,首先需要计算括号内表达式的值,然后把这个值与case后面的常量比较。执行第一个匹配的分支语句;若无匹配则执行最后一个default分支,如果同时缺省default项,则不执行任何语句。
switch(expression)
{
case 常量1:
statement 1;
break;
case 常量2:
statement 2;
break;
..........
default:
statement 3;
break;
}
public class SwitchCaseDefault
{
public static void main(String[] args)
{
int a=2;
switch(a)
{
case 1:
System.out.println("a="+a);
break;
case 2:
System.out.println("a="+a);
break;
default:
System.out.println("I DON'T KONW!");
}
}
}
for循环结构
w for语句是最标准的循环语句,也是功能最强的一种循环结构。for语句的功能是循环执行一段语句,直到某个条件为假时才结束。
public class sample2
{
public static void main(String args[])
{
int i;
for(i=0;i<=10;i++)
{
System.out.println("\nThe line "+i);
}
}
while循环结构
w while语句是Java最基本的循环语句。当它的控制表达式是真时,while语句重复执行一个语句或语句块;直到控制表达式为假时才结束。
public class WhileExample
{
public static void main(String args[])
{
int n = 10;
while(n > 0)
{
System.out.println("tick " + n);
n--;
}
}
}
do… while循环结构
w 如果while循环一开始条件表达式就是假的,那么循环体就根本不被执行。然而,有时需要在开始时条件表达式即使是假的情况下,while循环至少也要执行一次。Java就提供了这样的循环:do-while循环。do-while循环总是执行它的循环体至少一次,因为它的条件表达式在循环的结尾。
public static void main(String args[])
{
int n = 10;
do
{
System.out.println("tick " + n);
n--;
} while(n > 0);
}
}
break和continue
w 在循环语句的主体内(statement),可以使用break语句和continue来控制循环流程。
w break:跳出循环,不再执行剩余部分。
w continue:停止当次循环,回到循环起始处。continue语句之后的语句将不在执行。
w 无穷循环:
第一种形式:for(;;)
第二种形式:while(true)
3.7 大数字
编译运行下面这个程序:
public class Test{
public static void main(String args[]){
System.out.println(0.05+0.01);
System.out.println(1.0-0.42);
System.out.println(4.015*100);
System.out.println(123.3/100);
}
};
结果是:
0.060000000000000005
0.5800000000000001
401.49999999999994
1.2329999999999999
Java中的简单浮点数类型float和double不能够进行运算。不光是Java,在其它很多编程语言中也有这样的问题。在大多数情况下,计算的结果是准确的,但是多试几次(可以做一个循环)就可以试出类似上面的错误。
解决的方法是使用java.math包中的两个类,BigInteger和BigDecimal。这两个类可以操作任意长的数字。BigInteger类实现了任意精度的整数运算,而BigDecimal实现了任意精度的浮点运算。
BigDecimal一共有4个够造方法,我们不关心用BigInteger来够造的那两个,那么还有两个,它们是:
BigDecimal(double val)
BigDecimal(String val)
如果需要精确计算,非要用String来够造BigDecimal不可!
使用double构造BigDecimal将导致精度不准确,原因是double本身就不精确。
如BigDecimal bd=new BigDecimal(.1);
结果bd的值将会是0.1000000000000000055511151231257827021181583404541015625。必须用字符串来构造BigDecimal才能显示定义其精确度,
BigDecimal bd2=new BigDecimal(".1");
然而,不能使用熟悉的数学操作符,如+和*等来操作大数字。要实现这些功能,必须使用大数字类中add、subtract、multiply、divide、mod等方法。
l BigInteger add(BigInteger other)
l BigInteger subtract(BigInteger other)
l BigInteger multiply(BigInteger other)
l BigInteger divide(BigInteger other)
l BigInteger mod(BigInteger other)
[示例源文件Arith.java]
import java.math.BigDecimal;
/**
* 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精
* 确的浮点数运算,包括加减乘除和四舍五入。
*/
public class Arith{
//默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
//这个类不能实例化
private Arith(){
}
/**
* 提供精确的加法运算。
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
}
/**
* 提供精确的减法运算。
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的乘法运算。
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2).doubleValue();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入。
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double div(double v1,double v2){
return div(v1,v2,DEF_DIV_SCALE);
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入。
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1,double v2,int scale){
if(scale<0){
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 提供精确的小数位四舍五入处理。
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static double round(double v,int scale){
if(scale<0){
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(Double.toString(v));
BigDecimal one = new BigDecimal("1");
return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
};
假设在程序中需要定义100个整数变量,并且要求计算出它们相加的结果。这在程序中应该怎么去描述呢?至少要定义100个整数变量,一次定义100个整数变量,再把它们相加,这是一件很吃力的事情。
w 数组(array)是相同类型变量的集合,可以使用共同的名字引用它。数组可被定义为任何类型,可以是一维或多维,数组中的一个特别要素是通过下标来访问它。
w 数组一旦被创建,就不能再改变它的大小(尽管能够改变数组元素)。如果在程序运行过程中经常需要扩展数组,则应该使用另一种数据结构,数组列表。
w 定义(声明)数组时,既可使用语法int[] a; 也可使用语法int a[];
w 使用new操作符创建数组如:int[] a=new int[100];
w 给数组中的每个元素赋值,如:
int[] a=new int[100]
for(int i=0;i<100;i++) i<100也可写为i<a.length
a[i]=I;
Java中有个创建数组对象并同时赋与其初始值的简写形式,如:
Int[] smallPrimes={2,3,5,7,11,13}; 没有使用new操作符
在Java,还可以初始化一个匿名数组:new int[]{17,19,23,29,31,37};
可以把一个数组变量拷贝给另一个,这时,两个变量都指向相同的数组:
a.完全拷贝
int[] luckyNumbers=smallPrimes;
2 3 5 7 11 13 |
mallPrimes=
luckyNumbers= |
拷贝数组变量
luckyNumbers[5]=12;//现在smallPrimes[5]的值也是12
b.部分拷贝
如果实际上只是想一个数组中的值拷贝给另一个变量,那么需要使用System类中的arraycopy方法,其语法如下:
System.arraycopy(from, fromIndex, to, toIndex, count);//to数组必须有足够的空间来容纳拷贝的元素。
int[] smallPrimes={2,3,5,7,11,13};
int[] luckyNumbers={1001,1002,1003,1004,1005,1006,1007};
System.arraycopy(smallPrimes,2,luckyNumbers,3,4);
for(int i=0;i<luckyNumbers.length;i++)
System.out.println(i+": "+luckyNumbers[i]);)
2 3 5 7 11 13 |
mallPrimes=
|
1001 1002 1003 7 11 13 |
luckyNumbers= |
数组间拷贝值
每个Java程序都有一个main方法,它带有String[] args参数。这个参数表示main方法接收了一个字符串数组,也就是命令行参数。
对数组中的数字排序,可以Arrays类中的一种sort方法:
int[] a=new int[10000];
…
Arrays.sort(a);
[示例LotteryDrawing.java]
import java.util.*;
import javax.swing.*;
public class LotteryDrawing{
public static void main(String[] args){
String input=JOptionPane.showInputDialog("How many numbers do you need to draw?");
int k=Integer.parseInt(input);
input=JOptionPane.showInputDialog("What is the highest number you can draw?");
int n=Integer.parseInt(input);
int[] numbers=new int[n];
for(int i=0;i<numbers.length;i++)
numbers[i]=i+1;
int[] result=new int[k];
for(int i=0;i<result.length;i++) {
int r=(int)(Math.random()*n);
result[i]=numbers[r];
numbers[r]=numbers[n-1];
n--;
}
Arrays.sort(result);
System.out.println("Bet the following combination. It'll make you rich!");
for(int i=0;i<result.length;i++)
System.out.println(result[i]);
System.exit(0);
}
}
在Java中,多维数组(multidimensional arrays)实际上是数组的数组。定义多维数组变量要将每个维数放在它们各自的方括号中。例如,下面语句定义了一个名为more的二维数组变量。
int more[][] = new int[4][5];
public class MoreArray
{
public static void main(String args[])
{
int twoD[][]= new int[4][5];
int i, j, k = 0;
for(i=0; i<4; i++)
for(j=0; j<5; j++)
{
twoD[i][j] = k;
k++;
}
for(i=0; i<4; i++)
{
for(j=0; j<5; j++)
System.out.print(twoD[i][j] + " ");
System.out.println();
}
}
}
当给多维数组分配内存时,它只需指定第一个(最左边)维数的内存即可。也可以单独地给余下的维数分配内存。例如,下面的程序在数组more被定义时给它的第一个维数分配内存,对第二维则是手工分配地址。
int more[][] = new int[4][];
more [0] = new int[5];
more [1] = new int[5];
more [2] = new int[5];
more [3] = new int[5];
当手工分配内存时,不需要给每个维数相同数量的元素分配内存。多维数组实际上是数组的数组,每个数组的维数在它的控制之下。例如,下列程序定义了一个二维数组,它的第二维的大小是不相等的。
public class MoreArray2
{
public static void main(String args[])
{
int more[][] = new int[4][];
more[0] = new int[1];
more[1] = new int[2];
more[2] = new int[3];
more[3] = new int[4];
int i, j, k = 0;
for(i=0; i<4; i++)
for(j=0; j<i+1; j++)
{
more[i][j] = k;
k++;
}
for(i=0; i<4; i++)
{
for(j=0; j<i+1; j++)
System.out.print(more[i][j] + " ");
System.out.println();
}
}
}
3.9 Java中的注释方式
1、在Java程序中主要可以采用如下几种注释方式:
① C 语言的方式:
/* ..... */ 适用于多行注释文字
Java中的/* */注释并不嵌套使用。即,当一段代码本身含有*/符号时,无法简单地在这段代码的首尾分别加上/*和*/就把它注释掉。
② C++语言的方式:
// 适用于单行注释文字
③ Java语言的方式:
/**.....*/适用于多行文档注释文字
2、举例
(1)
// 单行注释,简单的解释语句含义.
(2)
/* 多行注释,用来说明更多的内容,包括算法等.
……
*/
(3)
/** Java文档注释,可以通过javadoc生
* 成类和接口的HTML格式的帮助文档.
* 这种注释有其特殊的格式(参见相关文档)
*/
3.10 JAVA的健壮的体现
w (1)JAVA是强类型语言———每种类型的数据必须赋与对应的数据值;
w (2)JAVA没有指针——— 不能强行访问指定的内存空间;
w (3)JAVA进行自动内存回收;
w (4)JAVA鼓励用接口而不要类。
3.11 JAVA与C++在语言上的差别
w (1)不再包括预处理器的功能
w (2)不再有structure,union,typedef
w (3)取消了函数而改称为方法
w (4)取消了多重继承
w (5)取消了goto
w (6)不再支持指针
本章重点:
l 认识类
l 方法的使用
l 参数的传递
l 方法的返回值
l 公有成员、私有成员及缺省访问修饰成员
随着商业逻辑的复杂化,外界对程序设计的要求日趋复杂,面向对象的概念也就跟着孕育而生。类为面向对象程序设计最重要的内容。
Java是纯面向对象的程序设计语言,每一个Java程序均少不了类,Java程序都至少存在一个或一个以上的类。Java中所有的变量和函数都必须写在类(class)里,
4.1.1类的基本概念
类的发展,是为了让程序语言更能清楚地描述出日常生活的事物。
例如,长方形(rectangle)是几何形状中常见的一种,它的边长有宽(width)和高(height)两个属性,根据这两个属性便可求出它的面积(area)和周长(perimeter)如何利用Java的类来描述长方形,使得它可以保存长方形的信息(宽与高),并能利用此类计算出面积与周长呢?作法是:定义一个长方形类。一般来说,类是由数据成员与函数成员封装而成的,它们的基本概念是:
数据成员
每一个长方形,不论尽寸的大小,均具有宽与高这两个属性,而这两个属性也就是长方形类的数据成员,当然,长方形类还可能有其他的数据,如颜色等。
函数成员
对于长方形类而言,除了宽与高这两个数据成员以外,还想计算其面积和周长。因此可以把计算面积与周长这两个函数纳入长方形类里,变成类的函数成员。Java称这种封装于类内的函数为“方法”。
因此,所谓的“类”是把事物的数据与相关功能封装在一起,形成一种特殊的结构,用以表达真实事物的一种抽象概念。
长width |
宽 height |
数据成员
函数成员 |
Area=width*height Perimeter=2(width+height) |
长方形类 |
Area() Perimeter() |
Width height |
数据成员,field |
函数成员,method |
4.1.2类的声明
使用类之前,必须先定义它,然后才可利用所定义的类
来声明变量,并创建对象。类定义的语法如下:
[格式4-1 类的定义]
class 类名称
{
数据类型 field 名称;
…
返回值的数据类型 方法名称(参数1,参数2,…)
{
程序语句;
Return 表达式;
}
…
}
以前述的范例为例,可定义如下的长方形类:
Class CRrectangle
{
int width;
int height;
int area()
{
return width*height;
}
int perimeter()
{
return 2*(width+height);
}
}
4.1.3 创建新的对象
“先打造一个长方形的模板(定义类),再以此模板制造长方形(创建对象)”
“由类所创建的对象,即为类的instance”
Width height |
Area() Perimeter() |
Width=12 Height=5 |
Width=6 Height=10 |
长方形类 |
图4-3 由类创建新的对象 |
长方形类所建立的对象(1) |
长方形类所建立的对象(2) |
§声明与创建对象
要创建属于某类的对象,可通过下面两个步骤来达成
(1)声明指向”由类所创建的对象“的变量。
(2)利用new创建新的对象,并指派给先前所创建的变量。
例如:要创建长方形类的对象,可用下列的语法来创建:
CRectangle rect1;
rect1=new CRectangle();
以上两句可缩减成一行:
CRectangle rect1=new CRectangle();
§指向对象的变量
变量rect1并不是基本类型的变量,它所存放的并非对象的实体,而是指向对象实体的一个参考地址。
Ox1000 |
图4-4 创建对象,并让变量rectl指向它 |
Ox1000 |
rect1 |
对象CRectangle |
参考地址 |
①先建立指向对象参考地址的变量 |
②配置存储CRectangle对象所需的内存空间 |
§访问对象的内容
如果要访问到对象里的某个变量(field)时,可以通过下面语法:
[格式3-2 访问对象中某个变量]
对象名称.特定的field
例如,对象rect1的宽和高可通过下列的语法来指定:
rect1.width;
rect1.height;
public static void main(String args[])
{
CRectangle rect1;
rect1=new Crectangle();
rect1.width=12;
rect1.height=5;
…
}
图3-5 对成员(或field)进行访问的操作 |
Width height |
Area() Perimeter() |
Width=12 Height=5 |
长方形类CRectangle |
由Crectangle类所建立的对象rectl |
|
Rectl.width=12 Rectl.height=5 |
4.1.4 使用类来设计完整的程序
4.1.5 同时创建多个对象
4.2 有关方法的使用
4.2.1 定义与使用方法
[格式3-3 声明方法,并定义其内容]
返回值类型 方法名称(类型 参数1,类型 参数2,…)
{
程序语句;
Return表达式;
}
对象要调用封装类里的方法时,只要用下列的语法即可:
对象名称.方法名称(参数1,参数2,…)
注意,若不需要传递参数到方法中,只要将左右括号写出,不必填入任何内容。
Class app4_3 { … rect1.area(); … rect1.perimeter(); … } |
Class Crectangle { … int area() { return width*height; } int perimeter() { return 2*(width+height); } } |
图4-6 方法的运行过程 |
4.2.2 再看一个简单的范例 App3_04
|
Pi radius |
Pi radius |
3.14 |
2.0 |
… |
3.14 |
2.0 |
图4-3 不同对象的数据成员在内存中的分配情形 |
4.2.2 数据成员的访问方式
在main()方法内如果需要访问field(如radius,pi)时,可通过:
指向对象的变量.field名称来进行。
如 cir1.radius=2.0;
cir1.pi=3.0;
然而,如果是在类声明内部使用这些field,则可直接取用field的名称,而
不需加上调用它的对象名称(事实上,在编写类的定义时,我们根本不知道
哪一个对象要调用它)
总而言之,在类声明之外需要用到field名称时,则必须指明是哪一个对象
变量,也就是用“指向对象的变量.field名称“的语法来访问。相反,若是在类声
明的内部使用这些field时,由于此时己是站在对象的角度来看待field,所以也
就不必指出field之前的对象名称了。
§ This 关键字
如果要刻意强调“对象本身的Field”的话,也可在field前面加上this这个关键字,即“this.field名称”。引时的this即代表取用此 field的对象。
4.2.3 在类定义的内部调用方法
到目前为止所学过的方法均是在类定义的外部被调用,其所采用的语法为:
对象名称.方法名称(参数1,参数2,…)
例:cir1.show_area();
事实上在类定义的内部,方法与方法之间也可以相互调用。
在同一类的定义里面,某一方法仍然可以直接调用其他方法,而不需加上 “指向对象的变量名称”。
同field一样,如果要刻意强调“对象本身的方法”的话,也可在方法前面加上this这个保留字,即“this.方法名”,此时的this即代表取用此一方法的对象。
4.3 参数的传递
当方法不需要传递任何参数时,方法的括号内什么也不用填,如 show_area()--没有传递任何参数的methods,事实上,方法也可加上各种数据类型的参数,以应付各种不同的计算需求。
4.3.1 调用方法并传递参数
调用方法并传递参数时,参数是置于方法的括号内来进行传递。括号内的参数可以是数值、字符串,甚至是对象。
4.3.2 传递多个参数
如果要传递两个以上的参数,只要将所有的参数置于方法
[格式4-4 方法的定义格式]
返回值类型 方法名称(类型 参数1,类型 参数2,…)
{
程序语句;
Return 表达式;
}
如果方法有返回值,则在方法一开始声明时便必须写上“返回值类型”,至于要返回的值,则把它置于return语句后面。通常return置于方法所有语句的最后面。
4.4 方法的返回值
使用方法时,不仅可以传递参数到方法内;也可以让方法返回特定的值到调用端的程序,这个返回来的值称为“返回值”。返回值可以是数值、字符串,或者是一个对象。值得注意的是,对任何一个方法而言,Java允许在程序中传递多个参数到方法内,但方法的返回值却只能有一个。
[格式4-4 方法的定义格式]
返回值类型 方法名称(类型 参数1,类型 参数2,…)
{
程序语句;
Return 表达式;
}
如果方法有返回值,则在方法一开始声明时便必须写上“返回值类型”,至于要返回的值,则把它置于return语句后面。通常Return置于方法所有语句的最后面。
4.4.1 没有返回值的方法
有些方法不必传递任何数据到调用端程序,因此没有返回值。若方法本身没有返回值,则必须在方法定义的前面加上关键字void。void的本意是“空无所有”之意。
当方法没有返回值,则方法最后的return语句可以省略,也可以加上,但不接任何的表达式。若方法本身没有返回值,则必须在前面加上void
Void show_area() { //show_area()方法,显示出圆面积
System.out.println(“area=“+pi*radius*radius);
return;
}
因没有返回值,故可在方法最后加上return语句,但不接任何的表达式,其执行结果与没有该语句的执行结果相同
4.4.2 有返回值的方法
double getRadius(){
return radius;
}
由于返回值的数据类型为double,因此getRadius()方法之前要冠上double
4.5 公有成员与私有成员
在类外部更改的数据成员,虽然对程序员来说非常方便,但从某个层面来说,却是隐藏着潜在的危险。
总之,从类外部访问数据成员时,如果没有一个机制来限定访问的方式,则很可能导致安全上的漏洞,而让bug进驻程序代码中。
4.5.1 创建私有成员
若数据成员没有一个机制来限定类中field的访问,则很可能会造成错误的输入。为了防止这种情况发生,Java提供了私有成员的赋值即在field声明的前面加上private,这样便无法从类以外的地方赋值或读取到类内部的成员,因此可达到数据保护的目的。
到目前为止,我们所编写的Java程序均是在同一个文件里中,事实上,如果同一个文件包含有多个类,则这些类将被视为在同一个package内。
4.5.2 创建公有成员
既然类外部无法访问到类内部的私有成员,则Java就必须提供另外的机制,使私有成员得以通过这个机制供外界访问。解决此问题的的方法是创建公有成员。即,在类的外部可对类内的公有成员做访问的操作,因此可通过公有成员的方法来对私有成员做处理。
4.5.3 数据的封装
在OOP的术语里,所谓的“封装”,就是把属性和方法依功能划分为“私有成员”与“公有成员”,并且包装在一个类内来保护私有成员,使得它不会直接受到外界的访问。
4.5.4 省略public与private
public与private是用来赋值公有与私有成员的标识符。标识符是可以略去不写的。
如果类的成员之前省略public与private标识符的话,则表示这个成员只能在同一个package(包)里被访问。这种访问方式被称为友好访问。
到目前为止,我们所编写的Java程序均是在同一个文件里中,事实上,如果同一个文件包含有多个类,则这些类将被视为在同一个package内
**每个类都可以有一个main方法。这在对类进行单元测试时是一个很方便的技巧。
本章内容:
l 函数成员(方法)的重载
Ø 重载
Ø 使用重载常犯的错误
l 构造函数
Ø 构造函数的基本认识
Ø 构造函数的调用时机
Ø 构造函数的重载
Ø 从某一构造函数调用另一构造函数
Ø 构造函数的公有与私有
Ø 构造函数的省略
l 类变量与类方法
Ø 实例变量与实例方法
Ø 类变量
Ø 类方法
Ø “类方法”使用的限制
l 类类型的变量
Ø 赋值给类类型的变量
Ø 以类类型的变量传递参数
Ø 由方法返回类类型的变量
Ø 释放内存
l 利用数组来保存对象
Ø 创建对象数组的范例
Ø 传递对象数组到方法里
l 内部类
Ø 内部类的编写
Ø 匿名内部类
所谓的“重载”是指相同的方法名称,如果参数个数不同,或者是参数个数相同、类型不同的话,方法便具有不同的功能
例
class CCircle
{
private String color;
private double pi=3.14;
private double radius;
public void setColor(String str)
{
color=str;
}
public void setRadius(double r)
{
radius=r;
}
public void setAll(String str,double r)
{
color=str;
radius=r;
}
public void show()
{
System.out.println("color="+color+",Radius="+radius);
System.out.println("area="+pi*radius*radius);
}
}
public class Test
{
public static void main(String[] ar)
{
CCircle cir1=new CCircle();
cir1.setColor("Red");
cir1.setRadius(2.0);
cir1.show();
cir1.setAll("Blue",4.0);
cir1.show();
}
}
其中的set Color()、setRadius()与setAll()均是在赋值对象的数据成员,如同是家里分别买了冷气机、暖气机和除湿机一样,不但占空间,且维护上也不怎么方便。Java的重载功能,恰可补足这方面的缺憾。利用重载编写的代码如
class CCircle
{
private String color;
private double pi=3.14;
private double radius;
public void setCircle(String str)
{
color=str;
}
public void setCircle(double r)
{
radius=r;
}
public void setCircle(String str,double r)
{
color=str;
radius=r;
}
public void show()
{
System.out.println("color="+color+",Radius="+radius);
System.out.println("area="+pi*radius*radius);
}
}
public class Test
{
public static void main(String ar[])
{
CCircle cir1=new CCircle();
cir1.setCircle("Red");
cir1.setCircle(2.0);
cir1.show();
cir1.setCircle("Blue",4.0);
cir1.show();
}
}
所谓重载是指相同的方法名称,可根据其参数的不同(可能是参数个数、类型或顺序不同)来设计不同的功能。
通过方法的重载,只需有一个方法名称,却可拥有不同的功用,使用起来相当的方便。
编译器通过匹配具体的方法调用中所使用的值的类型和多个方法头中的参数类型挑选出正确调用的方法。如果编译器找不到匹配的参数或者找到多个可能的匹配,则会产生一个编译时错误(这个过程称为重载解析。)
方法名相同,但方法的特征不同(包括方法参数的个数、类型或顺序),这样的一组方法称为方法重载,只有返回类型不同的一组方法不能被称为方法重载。例:
public void setCircle(double radius)
public int setCircle(double radius)
这两个方法的参数个数和类型完全相同,但返回类型不同,不能称为方法重载。如果尝试编译这样的方法,编译器会出现错误信息。
之前所创建的对象,其数据成员均是在对象创建之后,才由方法来赋值。实际上,Java也可以在创建对象的同时,一并赋值它的数据成员。方法是利用构造函数。
构造函数的功能是帮助新创建的对象赋初值。构造函数可视为一种特殊的方法,它的语法如下:
[格式4-1 构造函数的定义方式]
构造函数的名称必须和类名称相同
标识符 类名称(类型1 参数1,类型2 参数2,…)
{
程序语句;
… 构造函数没有返回值
}
构造函数包含初始化类的成员数据的代码。当类的对象创建时,它自动执行。因此,不管谁创建类对象,构造函数被激活及成员数据被初始化。
构造函数的规则:
(1)构造函数和声明它的类必须具有相同的名字。
(2)构造函数没有返回类型。
(3)不能显式地调用构造函数。
(4)一个类可以有多个构造函数。
静态构造函数和对象构造函数分别有不同的调用时机。如
class CCircle
{
private double pi=3.14;
private double radius;
static
{
System.out.println("Hello");
}
public CCircle(double r)
{
radius=r;
}
public void show()
{
System.out.println("radius="+radius+",area"+pi*radius*radius);
}
}
public class Test
{
public static void main(String[] args)
{
CCircle cir1=new CCircle(4.0);
cir1.show();
CCircle cir2=new CCircle(10);
cir2.show();
}
}
构造函数重载是方法重载的一个应用。
为了某些特定的运算,Java允许从某一构造函数内调用另一个构造函数。利用这个方法,可缩短程序代码,减少程序开发的时间。
从某一构造函数内调用另一构造函数,是通过this()这个关键字来调用的。
在某一个构造函数调用另一构造函数时的注意事项:
(1)必须使用this关键字来调用,不能以构造函数直接调用,否则编译器出现错误。
(2)this关键字必须写在构造函数内的第一行的位置,放错了地方也无法编译。
(3)可通过this有参数和无参数的构造函数
this()_调用无参数的构造函数
this(参数列表)_调用有参数的构造函数
构造函数也有public与private之分。Public构造函数可以在程序的任何地方被调用,在任何地方创建的对象均可自动调用它。如果构造函数被设成private,则无法在该构造函数所在的类以外的地方被调用。
如果构造函数省略,Java会自动调用默认的构造函数。默认的构造函数的格式如下:
[格式5-2 默认的构造函数。如果没有事先定义好构造函数,则Java会自动调用默认的构造函数]
Public Ccircle()
{
}
默认的构造函数并没有任何的参数,也不做任何事情。事实上,也就是因为有默认构造函数的这种设计,才使得即使在程序代码里没有编写任何的构造函数,也可以创建对象。
默认的构造函数是没有任何参数的构造函数。如果自行设计了一个没有参数的构造函数,则在创建对象时会调用自选设计的构造函数,而不会调用默认的构造函数。这是Java的覆写所致。
实际上,Java有三种初始化数据字段的方法:
l 在构造函数中设置值(默认构造函数和非默认构造函数)
l 在声明中赋值
l 通过初始化块设置值
5.2.7.1 在声明中赋值分为默认字段初始化和显式字段初始化
默认字段初始化:如果在构造函数中没有显式地给某个字段赋值,那么它会被自动赋为默认值:数字变量为0,布尔变量为false; 如果是对象引用,那么是null。但是靠这种方式给变量赋值不是一种好的编程习惯。
显式字段初始化:可以在类的定义中简单地把值赋给任何字段,如:
class Employee{ …private String DeptNo=”D002”;}
5.2.7.2初始化块
初始化块又分为初始化块和静态初始化块
由于数据字段存在多种初始化的手段,如果要列出构造过程的所有可能路径极易造成混淆。因此下面给出调用构造函数后详细的执行过程:
l 初始化所有数据字段为默认值(0、false或null)。
l 按照在类声明中出现的次序依次执行所有字段初始化语句和初始化块
l 如果构造函数的第一行代码调用了另一个构造函数,则执行被调用的构造函数主体。
l 执行构造函数主体。
[示例]
//ConstructorTest.java
import java.util.*;
public class ConstructorTest{
public static void main(String[] args){
Employee[] staff=new Employee[3];
staff[0]=new Employee("Harry",40000);
staff[1]=new Employee(60000);
staff[2]=new Employee();
for(int i=0;i<staff.length;i++){
Employee e=staff[i];
System.out.println("name="+e.getName()+",id=" +e.getId()+",salary="+e.getSalary());
}
}
}
class Employee{
public Employee(String n,double s){
name=n;
salary=s;
}
public Employee(double s){
this("Employee #"+nextId,s);
}
public Employee(){
}
public String getName(){
return name;
}
public double getSalary(){
return salary;
}
public int getId(){
return id;
}
{
id=nextId;
nextId++;
}
static{
Random generator=new Random();
nextId=generator.nextInt(10000);
}
private String name="";
private double salary;
private int id;
private static int nextId;
}
类变量、类方法与实例变量、实例方法意思相近,但功能却截然不同。
实例变量
每个对象都有自己保存属性的地方,而不与其他对象共享,如果改变了一个对象中某个变量(数据成员)的值,其他对象对应的数据成员并不受影响,因为这些变量各自独立,且存于不同的内存之内。具有特性的变量,Java称之为“实例变量”。
实例方法
类里有些方法必须通过对象来调用,即必须先创建对象再利用对象来调用它。无法直接调用方法而不通过对象。具有此特性的方法,Java称之为实例方法。
”实例变量”是属于个别对象所有,彼此之间不能共享。除了“实例变量”之外,Java也提供了另一种变量——“类变量”。与“实例变量”不同的是,它可由所有的对象来共享,也就是说每一个对象的“类变量”均相同,更改了某个对象的“类变量”,其他对象的“类变量”也将随之更改。
“类变量”和“实例变量”变量一样,都必须在类中声明。不同的是,如要把变量声明为“类变量”,必须在变量之前加上“static”这个标识符。
例如,假设Ccircle类里的变量pi,如要把它改为“类变量”,可将它声明成:
private static double pi=3.14;
这样声明的目的是把这个变量分享给每一个对象。所有对象的pi值相同,因此没有必要让每一对象都保有自己的pi值。
把pi声明成static,则由Ccircle类所创建的对象均可共享它
使用“类变量”可节省可观的内存空间,尤其是大量创建对象的时候。“类变量”使用的另一个时机是基于程序的需要。
前面所有的方法均是通过对象来调用。通过对象来调用方法,某些场合里不正确,如在没有对象产生的情况下无法通过对象调用方法。
解决的方法就是把方法声明成“类方法”。其作法是在方法之前加上static标识符:
public static void count();
在使用时,直接用类来调用它:
main()方法与static标识符
public static void main(String[ ] args)
main()之前加上了一个static标识符,使得main()变 成是一个“类方法”。原因main()方法是程序的入口函数,很显然,调用main()方法的是类而不是类的对象。因此在main()方法之前加上static是理所当然的。此外,main()方法均是在类之外被调用的,因此前面还得加上public标识符。
“类方法”的特性虽然可解决一些问题,但这些特性本身也带来了一些限制。
“类方法”访问的限制
“类方法”与任何特定的对象都没有特定的关系,因此在没有对象产生的情况下,“类方法”依然可以被调用。基于这个原故,“类方法”内部无法对“实例变量”与“实例方法”进行访问,
在“类方法”内部不能使用this关键字。因为this是代表调用该方法的对象,“类方法”既然己不需要对象来调用,this也自然不应存在于“类方法”内部。
变量可分为“基本类型的变量”与“非基本类型的变量”(“值类型变量”和“引用类型变量”)两种。所谓“基本类型的变量”是指由int、double等关键字所声明而得的变量;而由类声明而得的变量称之为“类类型的变量”,它是属于“非基本类型的变量”的一种。
例下面的语法声明了radius为基本类型的变量:
private double radius
下列则声明了cir1为CCircle类类型的变量:
CCircle cir1
cir1=new CCirlcle()
事实上,因cir1是指向“由CCircle类产生的对象“,所以称cir1为“指向对象的变量”,这只是从cir1变量功能的角度而称呼。
即使不用new产生新的对象,依然可对类类型的变量赋值。
在本例中,只产生了一个对象,但通过赋值,可将两个不同名称的变量cir1和cir2指向同一个对象,所以通过任一变量对对象做变更,另一变量所指向的对象内容也会随着更改。
如果想传递类类型的变量到方法里,只要在定义方法时,把类名称加到参数之前即可。传递类类型变量到方法的语法格式为:
[格式5-3 以类类型的变量传递参数]
void compare(CCircle obj){
…
}
在Java中,方法调用总是使用传值调用,这也意味着方法得到的只是所有参数值的拷贝。因此,方法不能修改传递给它的任何参数变量的内容。
如要由方法返回类类型的变量,只要在方法声明的前面加上欲返回的类即可,其语法如下:
[格式5-4 由方法返回类类型的变量]
返回类型为CCircle类的变量
CCircle compare(CCircle obj){
… 参数类型为CCircle
}
一些面向对象的程序设计语言,尤其是是C++,具有显式的析构方法,放置对象不再被使用时可能用到的清除代码。析构函数中最常见的活动就是回收分配给对象的内存。而由于Java能自动进行垃圾收集,不需要人工的内存回收,所以,Java并不支持析构函数 当然,有些对象除内存外,还使用了其他资源,如文件或是指向使用了系统资源的其他对象的句柄。这些情况下,在不再需要时,回收和再利用资源非常重要。
我们可以把finalize方法添加到任何类中。finalize方法会在垃圾收集器清除对象之前被调用。但在实际操作中,不要依赖finalize方法回收任何短缺资源--因为我们很难知道这个方法具体什么时候才被调用。
1:垃圾回收有两种启动方式:
a 当其感觉到内存不足时;
b 程序通过System.gc()强迫启动。
2:finalize()这个方法的名字是语言内定的,但是其内容可以在类的定义时由程序员给出。
它也有两种启动方式:
a 当垃圾回收动作启动时会自动调用这个函数;
b 程序通过System.runFinalizersOnExit(true)强迫启动,但此方法不安全,不鼓励。
垃圾回收是一个优先级非常低的线程,什么时候启动完全依赖JVM的实现算法.调用System.gc()是建议垃圾回收,但是不一定会回收,不过会加大回收的可能性.垃圾回收时会调用finalize方法,但是因为不知道什么时候垃圾回收,所以不要在finalize方法释放资源.垃圾回收随时都有可能运行,并不是说内存不足才启动的.
如果需要在资源使用完毕后立即关闭此资源,那就需要对它进行手工管理,对需要清除的资源使用dispose方法(java.awt.Window)。重要的是,如果你作用的类含有dispose方法,你需要在对象操作完成后调用这个方法
简单的做法是:
用new来创建新的对象后,Java便会分配内存空间给它。如果此对象不再使用了,可把指向该对象的变量设为null即可,例
CCircle cir1=new CCircle;
…
cir1=null;
一经赋值为null,该变量便不指向任何对象,Java便会释放原先被该对象所占据的内存块,但如果两个类类型的变量指向同一个对象,把其中一个变量设为null,因另一个变量还是指向它,因而Java的搜集残余内存机制并不会释放它。
对象可以用数组来存放,但必须有下面两个步骤:
1.声明类类型的数组变量,并用new分配内存空间给数组。
2.用new产生新的对象,并分配内存空间给它。
例如,欲创建三个CCircle类类型的数组元素,
CCircle cir[]; 声明CCirlce类类型的数组变量,
cir=new CCircle[3]; 并用new分配内存空间
创建好数组元素之后,便可把数组元素指向由CCircle类所产生的对象:
cir[0]=new CCircle();
cir[1]=new CCircle(); 用new产生新的对象,并分配内存空间给它
cir[2]=new CCircle();
即欲使用对象数组,至少需要用两次new运算符来分配内存空间,一次是分配内存空间给类类型的数组另一次是分配内存空间给数组中的对象元素。
class CCircle
{
private static double pi=3.14;
private double radius;
public CCircle(double r)
{
radius=r;
}
public void show()
{
System.out.println("area="+pi*radius*radius);
}
}
public class Test
{
public static void main(String args[])
{
CCircle cir[];
cir=new CCircle[3];
cir[0]=new CCircle(1.0);
cir[1]=new CCircle(4.0);
cir[2]=new CCircle(2.0);
cir[1].show();
cir[2].show();
}
}
传递数组时,在括号内填上数组名即可
class CCircle
{
private static double pi=3.14;
private double radius;
public CCircle(double r)
{
radius=r;
}
public static double compare(CCircle c[])
{
double max=0.0;
for(int i=0;i<c.length;i++)
if(c[i].radius>max)
max=c[i].radius;
return max;
}
}
public class Test
{
public static void main(String[] args)
{
CCircle cir[];
cir=new CCircle[3];
cir[0]=new CCircle(1.0);
cir[1]=new CCircle(4.0);
cir[2]=new CCircle(2.0);
System.out.println("Largest radius="+CCircle.compare(cir));
}
}
在类的内部也可以定义另一个类。如果在类A的内部再定义一个类B,此时类B称为内部类,而类A则称为外部类。内部类也可声明成public或private。当内部类声明成public或private时,其访问的限制与数据成员与方法完全相同。
定义内部类的语法格式:
[格式 5. 5 定义内部类别]
标识符 class 外部类的名称
{
//外部类的成员
标识符 class 内部类的名称
{
//内部类的成员
}
}
例1
public class Test
{
public static void main(String args[])
{
Caaa aa=new Caaa();
aa.set_num(5);
}
static class Caaa
{
int num;
void set_num(int n)
{
num=n;
System.out.println("num="+num);
}
}
}
例2
public class Test
{
public Test()
{
Caaa aa=new Caaa();
aa.set_num(5);
}
public static void main(String args[])
{
Test obj=new Test();
}
class Caaa
{
int num;
void set_num(int n)
{
num=n;
System.out.println("num="+num);
}
}
}
所谓的“匿名内部类”,是指可以利用内部类创建不具名称的对象,并利用它访问到类里的成员。创建匿名内部类并访问成员的语法如下:
[格式 5. 6 创建匿名内部类,并执行所定义的方法]
(
new 类名称()
{
方法名称(参数1,参数2,…,参数n)
{
语句;
}
}
).方法名称(参数1,参数2,…,参数n)
创建匿名内部类的用意,主要是用来补充内部类里没有定义到的方法,并可有效地简化程序代码。例
public class Test
{
public static void main(String[] args)
{
(
new Caaa()
{
void set_num(int n)
{
num=n;
System.out.println("num="+num);
}
}
).set_num(5);
}
static class Caaa
{
int num;
}
}
内部类的对象能访问外部类的成员。外部类通过内部类对象能访问内部类成员。你也能在方法内创建类。内部类方法能访问在方法(包含内部类)内规定的变量。内部类必须说明在方法变量后以致变量能进入内部类。
综合例题 TestInnerClass.java
class OuterClass
{
private int OutNumber;
private String OutString;
public OuterClass(int OutNumber,String OutString)
{
this.OutNumber=OutNumber;
this.OutString=OutString;
}
public class InnerClass
{
public int InnerNumber;
public String InnerString;
public InnerClass(int InnerNumber,String InnerString)
{
this.InnerNumber=InnerNumber;
this.InnerString=InnerString;
}
public void OutPutValue()
{
System.out.println(OutNumber);
System.out.println(OutString);
}
}
}
public class TestInnerClass
{
public static void main(String[] args)
{
OuterClass obj=new OuterClass(333333,"cccccc");
OuterClass.InnerClass obj1=obj.new InnerClass(222222,"bbbbbb");
System.out.println(obj1.InnerNumber);
System.out.println(obj1.InnerString);
System.out.println("");
System.out.println("");
obj1.OutPutValue();
}
}
本章内容:
• 继承的基本概念
Ø 简单的继承范例
Ø 调用父类中特定的构造函数
Ø 使用构造函数常见的错误
• 由子类访问父类的成员
• 覆盖
Ø 覆盖父类的方法
Ø 以父类的变量访问子类的成员
Ø 再谈super()与this()
Ø 终止继承
Ø 类之源----Object类
Ø 再谈super()与this()
Ø 终止继承
Ø 类之源----Object类
对OOP的程序而言,它的精华在于类的继承。继承可以使我们以既有的类为基础,进而派生出新的类通过这种方式,便能快速地开发新的类,而不需再编写相同程序代码。(就像不需要从头制造新型电视机)
类与类之间最常见的关系有:
l 依赖
l 聚合
l 继承
依赖关系(“use-a”):是最明显也最常见的关系。如果一个类的方法操作了另一个类中的对象,那么这个类就依赖于另一个类。
聚合关系(“has-a”):当类A的对象包含类B的对象时,我们称类A和类B为聚合关系。
继承关系(“is-a”):用来表示更特殊的和更一般的类之间的关系。
以设计一个硬币类CCoin类为例。可用来创建各种不同半径(radius)与不同币值(value)的硬币。由于CCircle类里己包含了pi、radius成员与setRadius()、show()等方法,所以在创建硬币类CCoin时,便可通过继承的方式来利用这些数据成员与方法。即只要针对硬币类Ccoin要新增的数据成员与方法编写程序代码即可,而不必在Ccoin里编写相同的程序代码,这就是继承的基本思想。
以既有类为基础,进而派生出另一类,Java称之为“类的扩展”。
Java类的继承,可用下面的语法来表示:
[格式 6. 1 类继承的格式]
Class CCircle
{
private double pi=3.14;
private double radius;
CCircle类里的方法…
}
Class CCoin extends CCircle
{
private int value;
CCoin类里的方法…
}
Ccoin是通过关键字extends,概括继承CCircle的所有非私有成员,并加入新的成员以符合类所需。即由Ccoin所创建的对象,除了本身的成员之外,同时也拥有CCircle类里所定义的各种非私有成员,包括数据成员、方法与构造函数,这种特性在Java里称为继承
此时原有的类称为父类(super class),因继承而产生的新类则称为子类(sub class)。父类与子类之间的成员继承关系可用图5-1来说明。
|
pi radius |
setRadius() show() |
pi radius value |
setRadius() show() setValue() |
CCircle类(父类) |
CCoin类(子类) |
继承 |
继承 |
子类本身的数据成员 |
继承自父类的method |
子类本身的method |
图6. 1 类的继承 |
继承自父类的数据成员 |
class CCircle
{
private double pi=3.14;
private double radius;
public CCircle(){
System.out.println("CCircle() constructor called");
}
public void setRadius(double r){
radius=r;
System.out.println("radius="+radius);
}
public void show(){
System.out.println("area="+pi*radius*radius);
}
}
class CCoin extends CCircle
{
private int value;
public CCoin(){
System.out.println("CCoin() constructor called");
}
public void setValue(int t){
value=t;
System.out.println("value="+value);
}
}
public class Test
{
public static void main(String[] args)
{
CCoin coin=new CCoin();
coin.setRadius(2.0);
coin.show();
coin.setValue(5);
}
}
上例说明了类继承的基本概念、运作模式以及使用方法等
继承中的几点重要概念:
1.通过extends关键字,可将父类的成员(非私有数据成员与方法)继承给子类。如要调用这些继承过来的成员时,使用过去惯用的语法即可。
2.在Java的继承中,执行子类的构造函数之前,会先调用父类中没有参数的构造函数,其目的是为了要帮助继承自父类的成员做初始化的操作。
即使没有明确地指定构造函数,子类还是会先调用父类中没有参数的构造函数,以便进行初始化的操作。问题是,如果父类有数个构造函数,要如何才能调用父类中特定的构造函数呢?其作法是,在子类的构造函数中,通过super()这个关键字来调用。
class CCircle
{
private double pi=3.14;
private double radius;
public CCircle(){
System.out.println("CCircle() constructor called");
}
public CCircle(double r)
{
System.out.println("CCircle(double r) constructor called");
radius=r;
}
public void show(){
System.out.println("area="+pi*radius*radius);
}
}
class CCoin extends CCircle
{
private int value;
public CCoin(){
System.out.println("CCoin() constructor called");
}
public CCoin(double r,int v){
super(r);
value=v;
System.out.println("CCoin(double r,int v) constructor called");
}
}
public class Test
{
public static void main(String args[])
{
CCoin coin1=new CCoin();
CCoin coin2=new CCoin(2.5,10);
coin1.show();
coin2.show();
}
}
这里有几点要提醒:
1.如果省略了super(r)语句,则父类中没有参数的构造函数还是会被调用的。
2.调用父类构造函数的super()语句必须写在子类构造函数的第一行,不能置于它处,否则编译时将出现错误信息。
3.super()可以重载,也就是说,super会根据参数数目和类型,执行相应的父类的构造函数。
Java在执行子类的构造函数之前,如果没有用super()来调用特定父类的构造函数,则会先调用父类中“没有参数的构造函数”。因此,如果父类中只定义了有参数的构造函数,而在子类的构造函数里又没有用
Super()来调用父类中特定的构造函数,则编译时将发生错误,因为Java在父类中找不到“没有参数的构造函数”可供执行。
This()与super()
构造函数可用this关键字来调用同一类内的其他构造函数。Super关键字也有类似的功能,但基本上两者的使用时机并不相同,this()是在同一类内调用其他的构造函数,而super()则是从子类的构造函数调用其父类的构造函数。
虽然使用的时机不同,但this()与super()还是有其相似之处:
1.当构造函数有重载时,this()与super()均会根据所给予的参数类型与个数,正确地执行相对应的构造函数
2.this()与super()均必须编写在构造函数内的第一行,也就是因为这个原因,this()与super()无法同时存在于同一个构造函数内。
父类的radius成员是声明成private,因此只能在CCircle类内访问,或都是通过构造函数或公有方法来达成访问的目的。若在子类内直接访问private的数据成员,会出现错误。
要想使父类开放权限,使得子类也能访问到父类的数据成员,可以在父类中把数据成员声明成protected(保护成员),而非private。
把成员声明成protected最大的好处是:可同时兼顾到成员的安全性与便利性,因它只能供父类与子类的内部来访问,而外界则无法更改或读取它。(一个包的除外)
“覆盖”的概念与“重载”相似,它们均是Java“多态”的技术之一。
Java既是以类为基础,而继承又是OOP语言里常用的把戏,因此在父类与子类里定义了名称、参数的数据类型与个数均完全相同的方法是很有可能的事,尤其是父类与子类是分别交由不同的人来编写时更容易发生。
当父类与子类均同时拥有名称、参数的个数与数据类型均相同的情形在OOP的技术里称为覆盖。通过“覆盖”技术,在子类中可定义和父类里的名称、参数个数与数据类型均完全相同的方法,用以取代父类中原有的方法。
“覆盖”与“重载”的比较
“覆盖”与“重载”均是Java“多态”的技巧之一。这两个技术对初学者而言很容易混淆,重载与覆盖的区别有5点:
1.方法的覆盖是子类和父类间的关系,而方法的重载是同一类内部多个方法间的关系。
2.方法的覆盖一般是两个方法间的,而重载时可能有多个重载方法。
3.覆盖的方法有相同的方法名和形参表,而重载的方法只能有相同的方法名,不能有相同的形参表。
4.覆盖时区分方法的是根据调用它的对象,而重载是根据形参表来决定调用的是哪个方法。
5.覆盖的英文名称为overriding,重载的英文名称为overloading。
通过父类的变量访问子类的成员,只限于“覆盖”的情况发生时。即父类与子类的方法名称、参数个数与类型必须完全相同,才可通过父类的变量调用子类的方法。
**一个对象变量可以指向多种实际类型的现象被称为“多态”。而在运行时自动选择正确的方法进行调用的现象被称为“动态绑定”。
在Java中,对象变量是多态的。一个类型为Employee的变量既可以指向类型为Employee的对象,又可以指向Employee类的任何子类的对象。
通过super关键字可用来调用父类的方法,事实上super后面也可加上数据成员(即变量)的名称。
this关键字用法与super类似。This除了可用来调用同一类内的其他构造函数之外,如果同一类内“实例变量”与“局部变量”的名称相同时,也可利用它来调用同一类内的“实例变量”。
覆盖有其便利性,但在设计类时,如果基于某些因素,父类的方法不希望子类的方法来覆盖它,便可在父类的方法之前加上“final“关键字。如此该方法便不会被覆盖。
final的另一个功用是把它加在数据成员变量前面,则该变量就变成一个常量,如此便无法在程序代码的任何地方再做修改。
例static final double pi=3.14;
前几节中的范例中,己学到类的基本继承方法,例如,Ccoin类继承自CCircle类,因而Ccoin称为子类,而CCircle则为父类,有趣的是,CCircle也有其父类虽然没有很明确地指定它。事实上在Java里,如果某一类没指定其父类的话,则Java会自动赋值该类继承自Object这个类,而成为它的子类(Object是置于java.lang类库里的一个类)。
Object类可说是类之源,所有的类均直接或间接继承它,如图6-2所示。
|
Object类 |
CCircle类 |
CCoin类 |
间接继承Object 的成员 |
如果没有指定父类,则新建的 类会以Object类做为它的父类 |
CCoin继承自的CCircle类,同 时它也会继承Object的成员 |
图6-2 所有的类均继承自Object这个类 |
直接继承Object 的成员
|
功能说明 |
方法名称 |
将调用toString()的对象转成字符串 |
String toString() |
两个类变量所指向的是否为同一个对象 |
Boolean equals(Object obj) |
取得调用getClass()的对象所属的类 |
Class getClass() |
equals()方法的使用
equals()方法可用来比较两个类变量是否指向同一个对象。如果是,则返回true,否则返回false。
所有的类均是Object类的子类,因此所有类都继承了Object类的equals()方法,因此都可以使用它。
toString()方法的使用
toString()方法的功能是将对象的内容转换成字符串,并返回其内容。例如,若变量a是指向由类Caaa所创建的对象,则下面的语句会调用toString()方法,并输出所指向对象的内容:
System.out.println(a);
上面的语句是以类类型的变量a当成println()的参数,此时Java会先用变量a来调用toString()方法,再把结果当成println()的参数。也可以用下面的语法来编写出相同功用,且更容易了解的语句:
System.out.println(a.toString());
因toString()的返回值不太具有意义,且很少有人看得懂它。因此如果要用toString()返回对象的内容,建议覆盖toString()方法,以符合需要。
本章内容:
• 抽象类
Ø 定义抽象类
Ø 抽象类的实现
Ø 用抽象类类型的变量来创建对象
Ø 使用抽象类的注意事项
• 接口的使用
• 多重继承
• 接口的扩展
• 包
“抽象类”与“接口”是类概念的扩展。通过继承扩展出子类,加上“覆盖”的应用,“抽象类”可以一次创建并控制多个子类。“接口”则是Java里实现多重继承的重要方法。灵活地运用“抽象类”与“接口”知识,将编写出更精巧的Java程序。
通过继承,可以从原有的类派生出新的类。原有的类称为基类(base class)或父类,而新的类则称为派生类(derived class)或子类(sub class)。通过这种机制派生出的新类不仅可以保持原有类的功能,同时也可以拥有新的功能。
Java也可以创建专门的类用来当作父类,这种类称为“抽象类”(abstract class)。抽象类有点类似“模板”的作用,其目的是要依据它的格式来修改并创建新的类。但是并不能直接由抽象类创建对象,只能通过抽象类派生出新的类,再由它来创建对象。
抽象类是以abstract关键字为开头的类。定义抽象类的语法如下:
[格式 7.1 抽象类的定义格式]
Abstract class 类名称 //定义抽象类
{
声明数据成员;
返回值的数据类型 方法名称(参数…)
{
…
}
abstract 返回值的数据类型 方法名称(参数…);
}
注意在抽象类定义的语法中,方法的定义可分为两种:一种是一般的方法,它和先前介绍过的方法没有什么区别;另一种是“抽象方法”,它是以abstract关键字为开头的方法,此方法只声明了返回值的数据类型、方法名称与所需的参数,但没有定义处理的方式,即没有方法体。
抽象类的目的是要依据它的格式来修改并创建新的类,因此抽象类里的“抽象方法”并没有定义具体的处理方式,而是要保留给从抽象类派生出的新类来定义。
假设想设计一个图形(shape)的父类Cshape,依据此类可用来派生出圆形(circle)、长方形(rectangle)与三角形(triangle)等具体图形的类。可以把父类与派生类之间的关系绘制成如图7-1所示。
|
Cshape类 |
CRectangle类 |
CCircle类 |
Ctriangle类 |
父类 |
衍生类 |
图7.1 父类与派生类之间的关系图 |
假设这些几何形状均具有“颜色”(color)这个属性,可以把color这个数据成员,以及赋值color的方法均设计在父类里,另外,如果想为每一个几何形状的类设计一个area()方法,用来显示几何形状的面积,因每种几何形状的面积计算方式并不相同,所以把area()方法的处理方式设计在父类里并不恰当,但每一个由CShape父类所派生出的子类又都需要用到这一个方法,因此可以在父类里只声明area()方法,而把area()方法处理的方法留在子类里来定义,也就是说,把area()声明成抽象方法,然后在各个子类里明确地定义他们即做覆盖的操作。
利用父类的变量也可以访问子类的内容,这种情况同样适用在抽象类与其派生类身上。也就是说,利用抽象类类型的变量也可访问派生类的对象。
使用抽象类时,需要注意:“抽象类不能用来直接产生对象”。其原因在于它的抽象方法只有声明,而没有明确地定义,因此如果用它来创建对象,则对象根本不知要如何使用这个抽象方法。
接口(interface)是Java所提供的另一种重要功能,它的结构和抽象类非常相似。接口本身也具有数据成员与抽象方法,但它与抽象类有下列两点不同:
1.接口的数据成员必须初始化。
2.接口里的方法必须全部都声明成abstract,也就是说,接口不能像抽象类一样保有一般的方法,而必须全部是“抽象方法”。
接口定义的语法如下:
[格式7-2 接口的定义格式]
Interface 接口名称
{
final 数据类型 成员名称=常量;//数据成员必须赋值初值
abstract 返回值的数据类型 方法名称(参数…);//抽象方法
}
接口与一般类一样,本身也具有数据成员与方法,数据成员必须是public static final修饰的,这是被系统默认的,所以写不写public static final修饰符效果都一样,但一般都写出final。方法系统默认为public abstract的,一般不写修饰符。
这些方法是公共的抽象方法,没有方法体,所以后面不接大括号,就以分号结束。
下例说明了接口定义的方式。
Interface iShape2D
{
final double pi=3.14;
abstract void area();
}
既然接口里只有抽象方法,它只要声明而不用定义处理方式,于是自然可以联想到接口也没有办法像抽象类一样,用new运算符直接产生对象。相反的,必须利用接口的特性来打造一个新的类,再用它来创建对象。利用接口打造新的类的过程,称之为接口的实现(implementation)。格式如下:
[格式 7-3 接口的实现]
Class 类名称 implements 接口名称 //接口的实现
{
… …
}
接口的运作方式和抽象类非常类似,不同的是,接口里的数据成员必须设为常量,且所有的方法必须声明成abstract。抽象类则限制较少,它的数据成员不必设初值,且允许一般的方法与“抽象方法”共存
不能直接由接口来创建对象,而必须通过由实现接口的类来创建。虽然如此,仍可以声明接口类型的变量(或数组),并用它来访问对象。
7.3 多重继承 有时候,我们会希望一个子类同时继承自两个以上的父类,以便使用每一个父类的功能,但很不幸的是,Java并不允许多个父类的继承,原因是多重继承容易引发歧义性。
通过接口的机制,多重继承的处理还是可以实现作法是将类实现两个或两个以上的接口,如此一来,类里的方法必须明确定义每一个接口里的方法,因而可达到多重继承的目的。 |
父类1 |
父类2 |
父类3 |
子类 |
类实现两个或两个以上的接口的语法如下:
[格式7-4 实现两个或两上以上的接口]
Class 类名称 implements 接口1,接口2,…{
… …
}
接口可通过扩展(extends)的技术来派生出新的接口。原来的接口称为父接口,派生出的接口称为子接口(sub interface)。这种机制,派生接口不仅可以保有父接口的成员,
同时也可以加入新的成员以满足实际问题的需要。 |
Circle类 |
Circle类 |
Circle类 |
Circle类 |
iShape2D接口 |
iShape3D接口 |
iShape2D接口 |
父接口 |
父接口 |
父接口 |
extends |
implements |
图7-5 接口的扩展 |
接口的扩展(或继承)也是通过关键字extends。值得注意的是,一个接口可以继承自多个接口,这与类的继承有所不同。下列为接口扩展的语法:
[格式 7-5 接口的扩展]
Interface 子接口名称 extends 父接口名称1,父接口名称2,…
{
… …
}
在Java里,可以将大型程序内的类独立出来,分门别类地存到不同文件中,再将这些文件一起编译执行,如此的程序代码将更具亲和性且易于维护。
在开发大型程序时,为了工作上的需要,程序代码的开发通常是由一些人,或者是几个小组同时进行。每个参与的小组或成员分别负责某些类,并将所编写的类分开保存在各自的文件中,直到所有的类均开发好,再分别编译与执行。这种概念是利用文件分割的方式,将大型程序分开成独立的类,以利于程序的开发与维护。
在Java里怎样实现文件分割和分别编译呢?示例如下:
//CCircle.java
class CCircle
{
public void show(){
System.out.println("show() method called");
}
}
//Test.java,
public class Test
{
public static void main(String args[])
{
CCircle cir=new CCircle();
cir.show();
}
}
两个文件分别创建好后,分别编译产生CCircle.class与Test.class文件,再运行Test文件即可。
使用package
当一个大型程序交由数个不同的组别或人员开发时,用到相同的类名称是很有可能的事。这时需要使用package关键字。
包(package)是类的容器,用来保存划分的类名空间以防类的名称冲突。包以分层方式保
存并被明确的引入新的类定义。前述每个例题类名从相同的名称空间获得。为了避免名称冲突,即为了确保你选用的类名不和其他程序员选择的类名相冲突,可以使用Java提供的把类名空间划分为更多易管理的块的机制,这种机制就是包。包既是命名机制也是可见度控制机制。我们可以在包内定义类,使在包外的代码能或不能访问该类。
package的使用方法是在类或接口的最上面一行加上package的声明(package必须是Java源文件的第一句)。该文件中定义的任何类将属于指定的包。
package语句定义了一个存储类的名字空间。如果省略package语句,类名被输入一个默认的没有名称的包。
package声明的通用形式为:
package MyPackage;
java 用文件系统目录来存储包。目录名必须和包名严格匹配。Package声明仅仅指定了文件中定义的文件属于哪一个包。它不拒绝其他文件的类成为相同包的一部分,即多个文件可以包含相同package声明。
我们还可以创建包层次。一个多级包的声明的通用形式为:
package pkg1[.pkg2][.pkg3]…;
Java编译器考虑的特定位置作为包层次的根被类路径控制。在命令行键入类名编译源文件和运行Java解释器解释类,并得到结果。这种情况下它能够正确地工作是因为默认的当前工作目录(.)通常在类路径环境变量中被设置。然而,当有包参与时,事情就不这么简单了。
假如在一个test包中创建了一个名为PackTest的类,因为目录结构必须与包相匹配,创建一个名为test的目录并把PackTest.class被存放在test目录下。此时PackTest类被保存在test包中,不再能简单用PackTest来引用。必须通过列举包层次来引用该类。引用包层次时用点号将包名隔开。该类现在必须叫做test.PackTest。
package MyPack;
class Balance
{
String name;
double bal;
Balance(String n,double b)
{
name=n;
bal=b;
}
void show()
{
if(bal<0)
System.out.println("-->");
System.out.println(name+":$"+bal);
}
}
class AccountBalance
{
public static void main(String args[])
{
Balance current[]=new Balance[3];
current[0]=new Balance("K.J.Fielding",123.23);
current[1]=new Balance("Will Tell",157.02);
current[2]=new Balance("Tom Jackson",-11.33);
for(int i=0;i<3;i++)
current[i].show();
}
}
该文件名为AccountBalance.java,把它存放在MyPack目录中。接着,编译文件。确信结果文件.class同样在MyPack目录中。然后用下面的命令行执行AccountBalance类:
java MyPack.AccountBalance
注释:此例中AccountBalance是MyPack包的一部分。这意味着它不能自己执行。即不能用下面的命令行:
java AccountBalance;(因为AccountBalance必须和它的包名一起使用。)
Java提供很多级别的保护以使在类、子类和包中有完善的访问控制。包是封装类和下级包的容器,而类就像是数据和代码的容器。
一个类(或接口)只可能有两个访问级别:默认的或是公共的。如果一个类声明成public,它可以被任何其他代码访问。如果该类是默认访问控制符,它仅可以被相同包中的其他代码访问。
由于类和包的相互影响,Java将类成员的可见度分为四种:。
l 相同包有继承关系的类
l 相同包无继承关系的类
l 不同包有继承关系的类
l 不同包无继承关系的类
类成员的访问控制符有四类:三个访问控制符:public、private、protected和缺省,
它们之间的相互作用如下表:
|
Private成员 |
默认的成员 |
Protected成员 |
Public成员 |
|||||||
同一类中可见 |
是 |
是 |
是 |
是 |
|||||||
同一个包中对子类可见 |
否 |
是 |
是 |
是 |
|||||||
同一个包中对非子类可见 |
否 |
是 |
是 |
是 |
|||||||
不同包中对子类可见 |
否 |
否 |
是 |
是 |
|||||||
不同的包中对非子类可见 |
否 |
否 |
否 |
是 |
说明:任何声明为public的内容可以被从任何地方访问。被声明成private的成员不能被该类外看到。如果一个成员不含有一个明确的访问说明,它对于该包中的子类或其他类是可见的。这是默认访问。如果希望一个元素在当前包外可见,但仅仅是元素所在类的子类直接可见,把元素定义成protected。
成员与构造函数所使用的修饰符
修饰符 |
说明 |
没有修饰符 |
成员或构造函数只能被同一个package内的程序所访问 |
public |
如果所属的类也声明成public,则成员或构造函数可被不同package内所有的类所访问。若所属类不是声明成public,则成员或构造函数只能被同一个package内的程序所访问。 |
private |
成员或构造函数只能被位于同一个类内的程序所访问 |
protected |
成员或构造函数只能被位于同一package内的类以及它的子类为访问 |
//Protection.java
package p1;
public class Protection
{
int n=1;
private int n_pri=2;
protected int n_pro=3;
public int n_pub=4;
public Protection()
{
System.out.println("base constructor");
System.out.println("n="+n);
System.out.println("n_pri="+n_pri);
System.out.println("n_pro="+n_pro);
System.out.println("n_pub="+n_pub);
}
}
//Derived.java
package p1;
class Derived extends Protection
{
Derived()
{
System.out.println("derived constructor");
System.out.println("n="+n);
//System.out.println("n_pri="+n_pri);
System.out.println("n_pro="+n_pro);
System.out.println("n_pub="+n_pub);
}
}
//SamePackage.java
package p1;
class SamePackage
{
SamePackage()
{
Protection p=new Protection();
System.out.println("same package constructor");
System.out.println("n="+p.n);
// System.out.println("n_pri="+p.n_pri);
System.out.println("n_pro="+p.n_pro);
System.out.println("n_pub="+p.n_pub);
}
}
//Protection2.java
package p2;
class Protection2 extends p1.Protection
{
Protection2()
{
System.out.println("derived other package constructor");
//System.out.println("n="+n);
//System.out.println("n_pri="+n_pri);
System.out.println("n_pro="+n_pro);
System.out.println("n_pub="+n_pub);
}
}
//OtherPackage.java
package p2;
class OtherPackage
{
OtherPackage()
{
p1.Protection p=new p1.Protection();
System.out.println("other package constructor");
System.out.println("n="+p.n);
System.out.println("n_pri="+p.n_pri);
System.out.println("n_pro="+p.n_pro);
System.out.println("n_pub="+p.n_pub);
}
}
//Demo.java
package p1;
public class Demo
{
public static void main(String args[])
{
Protection ob1=new Protection();
Derived ob2=new Derived();
SamePackage ob3=new SamePackage();
}
}
package p2;
public class Demo
{
public static void main(String args[])
{
Protection2 ob1=new Protection2();
OtherPackage ob2=new OtherPackage();
}
}
包的存在是划分不同类的好的机制。所有的标准类都存储在不同的包中,可以通过“被访问的package名称.类名称”的语法来访问位于不同package里的类。
利用这种方法有些麻烦,因为每次都要指明被访问的package名称。简单的方法是直接把被访问的package导入程序代码中,这样它们就相当于位于同一个文件内,于是“被访问的package名称.”就可以省略了,即一旦类所属的包被引入,类就可以被直呼其名地引用。
import语句对于程序员是很方便的而且在技术上并不需要编写完整的Java程序。如果在程序中将要引用若干个类,那么用import语句将会节省很多打字时间。
import声明的通用形式:
import pkg1[.pkg2][.pkg3].(classname|*);
说明:
l 除非是文件系统的限制,不存在对于包层次尝试的实际限制。
l Import java.io.*;可能会增加编译时间—特别是在引入多个大包时。因此,明确的命名想要用到的类而不是引入整个包是一个好的方法。然而,星号形式对运行时间、性能和类的大小绝对没有影响。
l 编译器为所有程序隐式引入java.lang.*
l 如果在用星号形式引用的两个不同包中存在具有相同类名的类,编译器将保持沉默,除非试图运用其中的一个。此时,将产生一个编译错误要求用明确的命名指定包中的类。
l 任何用到类名的地方,可以使用它的全名,全名包括它所有的包层次。
l 当一个包被引入,仅仅是该包中声明成public的项目可以在引入代码中对非子类可用。
本章内容:
• Java常用的类库
Ø 有关字符串的类库
Ø StringBuffer类库
Ø Wrapper class
Ø 使用math类
Ø 日期类
Ø 随机数类Random
Ø 向量类Vector
Ø Class类与Runtime类
Ø 常用集合类
Java己经把功能相近的类分门别类到不同的类库中,例常用的String类是置于java.lang类库里,而BufferedReader则是置于java.io类库内。可通过帮助文档查看Java所提供的类库的全貌。
下图演示了类库、子类库、类与接口之间的层次关系。
其中实线是类库与子类库之间的连接,而虚线则连接类库或子类库里所提供的类或接口。
java.applet |
java.awt |
java.beans |
… … |
java.awt.color |
java.awt.event |
java.awt.font |
java.awt. … |
ColorSpace |
… … |
ActionEvent |
ContainerEvent |
FocusEvent |
MouseListener |
… … |
Button |
Canvas |
CheckBox |
… … |
类库的层次关系图 |
Java提供的类库很多,但常用的只有几个,下表列出了Java常用的类库以及它们所包含类的主要功能,这些类库在稍后的章节里将会一一介绍。
Java常用的类库
类库名称 |
所包含类的主要功能 |
java.applet |
与applet相关的类,applet是嵌在网页里的小程序,用来执行特定的功能 |
java.awt |
与java早期窗口组件设计有关的类 |
java.awt.event |
与事件(event)触发相关的类 |
java.lang |
Java最基本的类,此类会自动加载 |
java.io |
与输入/输出相关的类 |
java.net |
与网络组件相关的类 |
java.util |
与java utility相关的类 |
如果想使用某个类库里的某个类,可用“类库名称.类名称“,但如果想省略类库名称,只使用类名称,则必须将整个类库导入。
例要导入java.awt类库里的多个类,可用以下语法
import java.awt.Button;
import java.awt.Canvas;
如果要导入类库里的所有类时,可以通过通配符 “*”来导入:
import java.awt.*;
需要注意的是,当用通配符导入类库里的所有类时,该类库底下的子类库里的类并不会自动导入。也就是说,如果导入java.awt.*,则ActionEvent、ColorSpace等类并不会被导入。因此如果要导入Java.awt.event下的所有类可用下列的语法:
import java.awt.event.*;
String类是放置在java.lang类库内。Java.lang类库里所有的类均会自动加载,因此当使用到String类时,不需利用import命令来加载它。
创建字符串对象(String object)
1.可以利用String这个标识符来声明属于字符串的变量,然后再赋值给它,如:
String str=“abc”;
2.字符串可视为由字符数组组成,因此也可利用字符数组来产生字符串,如:
char data[ ]={‘a’,’b’,’c’};
String str=new String(data);
3.直接利用String构造函数来创建:
String str=new String(“abc”);
String类构造函数的格式
构造函数格式 |
主要功能 |
String() |
没有参数的String()构造函数 |
String(byte[ ] bytes |
以byte数组创建字符串 |
String(byte[] bytes,int offset,int length) |
取出byte数组,从数组的第offset位置开始,长度为length来创建字符串 |
String(char[] value) |
利用字符数组来产生字符串对象 |
String(char[] value,int offset,int count) |
取出字符数组,从数组的第offset位置开始,长度为count来创建字符串 |
String(String original) |
利用初始字符串(original string)来产生字符串对象 |
字符串类所提供的方法
String类提供了相当多的方法来做字符串的处理,如字符串的查找、转换大小写等。下表列出常用方法
方法 |
主要功能 |
char charAt(int index) |
取得index位置的字符 |
boolean equals(String str) |
测试字符串是否与str相同 |
int indexOf(char ch) |
根据字符ch找出第一个在字符串出现的位置 |
int length() |
取得字符串的长度 |
String substring(int index) |
取出index之后的子字符串 |
String substring(int ind1,int ind2) |
取出位于ind1和ind2之间的字符串 |
Boolean starsWidth(String prefix) |
测试字符串是否以prefix字符串为开头 |
String toLowerCase() |
将字符串转换成小写 |
String toUpperCase() |
将字符串转换成大定 |
由于String类只提供了一些查找与测试的方法,如果要用字符串做连接或修改,String类就没有提供,因此如果要修改字符串,则必须使用StringBuffer类来声明字符串,并用此类所提供的方法来进行字符串的修改。
与String类一样,StringBuffer类也是置于java.lang类库里,这个类库里的类会自动加载,因此不用特别去加载它。
下表列出了StringBuffer类常用的一些方法。
StringBuffer类常用的方法
方法 |
主要功能 |
StringBuffer append(char c) |
将字符c放到字符串缓冲区之后 |
StringBuffer append(String str) |
将字符串str放到字符串缓冲区之后 |
StringBuffer deleteCharAt(int index) |
删除字符串缓冲区里第index位置的字符 |
StringBuffer insert(int k,char c) |
在字符串缓冲区的第k个位置插入字符c |
StringBuffer insert(int k,String str) |
在字符串缓冲区的第k 个位置插入字符串str |
int length() |
取得缓冲区字符串的长度 |
StringBuffer replace(int m,int n,String str) |
将字符串缓冲区里第m到n之间以字符串str取代 |
StringBuffer reverse() |
将字符串缓冲区里的字符串反向排列 |
String toString() |
将字符串缓冲区里的字符串转换成String |
原始数据类型(primitive)如byte、short、char、int、long、float、float、double等均不被看成是对象。虽然如此,Java还是提供了一些特殊的类,让原始数据类型在使用上尤如对象一般,这些特殊的类称之为 wrapper class。
所谓的wrap就是把东西包装起来之意。因此 wrapper class便是把原始数据类型包装起来,并且额外提供相关的功能。Wrapper class所提供的变量均属 “类变量”,所提供的方法均是“类方法”。例如前面提到的各种类型的最大值与最小值代码均属于“类变量”,且定义在它们相对应的wrapper class里。
下表列出了原始数据类型与相对应的 wrapper class。
下表原始数据类型与相对应的wrapper class
原始数据类型 |
wrapper class |
boolean |
Boolean |
byte |
Byte |
char |
Character |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
wrapper class所提供的方法常被用在数据类型的转换上。下表列出了各种类常用的转换方法
各种类常用的转换函数
类 |
方法 |
主要功能 |
Byte |
static byte parseByte(String s) |
将字符串s转换成byte类型的值 |
Byte |
static String toString(byte b) |
将byte类型的数值b转换成字符串 |
Character |
static String toString(char c) |
将字符c转换成字符串 |
Short |
static short parseShort(String s) |
将字符串s转换成短整数 |
Short |
staic String toString(short s) |
将短整数s转换成字符串 |
Integer |
static int parseInt(String s) |
将字符串s转换成整数 |
Integer |
static String toString(int I) |
将整数I转换成字符串 |
Long |
static long parseLong(String s) |
将字符串s转换成长整数 |
Long |
static String toString(Long i) |
将长整数i转换成字符串 |
Float |
static float parseFloat(String s) |
将字符串s转换成浮点数 |
Float |
static String toString(float f) |
将浮点数f转换成字符串 |
Double |
static double parseDouble(String s) |
将字符串s转换成双精度浮点数 |
Double |
static String toString(double d) |
将双精度浮点数d转换成字符串 |
Math类是置于java.lang类库里的一个类,它所提供的方法可用来计算相关的数学函数。与wrapper class一样,Math类所提供的变量也是属于“类变量”,所提供的方法则是属于“类方法”。即不需要产生对象,即可通过Math类来调用它所提供的变量与方法。
下表列出了Math类所提供的“类变量”。
Math类所提供的类变量
方法 |
主要功能 |
public static final double E |
尤拉常量 |
public static final double PI |
圆周率,∏ |
Math类里的“类方法”多达20余个,下表例举了常用的几个
方法 |
主要功能 |
public static souble sin(double a) |
正弦函数 sin(a) |
public static double cos(double a) |
余弦函数 cos(a) |
public static double tan(double a) |
正切函数 tan(a) |
public static double asin(double a) |
反正弦函数 sin(a) |
public static double acos(double a) |
反余弦函数 cos(a) |
public static double atan(double a) |
反正切函数 tan(a) |
public static double exp(double a) |
自然指数函数 exp(a) |
public static double log(double a) |
自然对数函数 log(a) |
public static double sqrt(double a) |
开根号函数 sqrt(a) |
public static double ceil(double a) |
产生一个大于a的最小整数 |
public static double floor(double a) |
产生一个小于a的最大整数 |
public static souble pow(double a,double b) |
计算a的b次方 |
public static int round(float a) |
返回最接近a的整数 |
public static double random() |
返回0.0-1.0之间的随机数 |
public static type abs(type a) |
计算a的绝对值,其中type可为int,long,float或是double |
public static int max(int a,int b) |
找出a与b中较大者 |
public static int min(int a,int b) |
找出a与b中较小者 |
Java提供了4个常用的日期类:Date,Calendar,DateFormat和GregorianCalendar.在程序中,对日期的处理主要是如何获取、设置和格式化,Java的日期类提供了很多方法以满足程序员的各种需要.其中,Date主要用于创建日期对象并获取日期,Calendar可获取和设置日期,DateFormat主要用来创建日期格式化器,由格式化器将日期转换为各种日期格式串输出,GregorianCalendar类用我们熟悉的日历符号表示日期,它是对更一般的Canlendar类的扩展. Java是网络语言,程序必须注意到各个国家对日期格式的不同理解.
例DateTest.java演示了日期的获取、设置和格式化.
用GregorianCalendar类可使得编写日历程序十分简单,如下例:
//CalendarTest.java
import java.util.*;
public class CalendarTest{
public static void main(String[] args){
GregorianCalendar d=new GregorianCalendar();
int today=d.get(Calendar.DAY_OF_MONTH);
int month=d.get(Calendar.MONTH);
d.set(Calendar.DAY_OF_MONTH,1);
int weekday=d.get(Calendar.DAY_OF_WEEK);
System.out.println("Sun Mon Tue Wed Thu Fri Sat");
for(int i=Calendar.SUNDAY;i<weekday;i++)
System.out.print(" ");
do{
int day=d.get(Calendar.DAY_OF_MONTH);
if(day<=10)
System.out.print(" ");
System.out.print(day);
if(day==today)
System.out.print("*");
else
System.out.print(" ");
if(weekday==Calendar.SATURDAY)
System.out.println();
d.add(Calendar.DAY_OF_MONTH,1);
weekday=d.get(Calendar.DAY_OF_WEEK);
}while(d.get(Calendar.MONTH)==month);
if(weekday!=Calendar.SUNDAY)
System.out.println(); }
}
使用Math中的Random类可产生一个0到1之间的伪随机数,为了适应网络时代编程
对随机数的需要,Java在Random类中提供了更多的功能,Random的实例化对象可使用一个48位长的种子数来生成随机数.如果两个Random对象使用同样的种子数,并以同样的顺序调用生成方法,仍然可以保证得到两个不同的32位伪随机数.为了使Java程序有良好的可移植性,应该尽可能使用Random类来生成随机数.
Random有两个构造方法:Random()、Random(long seed).前者使用系统时间作为种子数,后者使用指定的种子数.构造方法只是创建了随机数生成器,必须调用生成器的方法才能产生随机数.Random具有nextBoolean、nextDouble、nextInt等方法。
RandomTest.java演示了用Random生成各种类型的随机数。
大多数编程语言中的数组是固定长度的,即数组一经建立就不能在使用过程中改变其长度.有些程序可能要解决数组长度不断改变的问题,这对于无法预见使用多长的数组更合适的程序员来说是一件头痛的事情,Java引入的Vector类较好地解决了这个问题.
Vector被设计成一个能不断增长的序列,用来保存对象引用.在创建Vector对象时可以指定初始容量和增量,每次添加元素都将使向量长度按增量自动增长.
Vector类似于可变长数组,但功能更加强大,任何类型的对象都可以放入Vector.通过调用Vector封装的方法,可以随时添加或删除向量元素,以及增加或缩短向量序列的长度.
Vector的常用方法如下所示.
Vector(int initcapacity,int increment)//构造方法,指定初始容量和增量
Void addElement(Object obj)//在尾部添加元素,元素个数自动增1
Boolean removeElement(Object obj)//删除第一个与obj相同的元素
Void setElementAt(Object obj,int index)//替换指定位置上的元素
Object elementAt(int index)//返回指定位置的元素
Int indexOf(Object obj)//返回指定元素obj在向量中的位置
Int size()//返回元素个数
Int capacity()//返回向量的长度
Void trimToSize()//按当前元素个数缩减向量长度
Enumeration elements()//生成一个向量元素的枚举
Vector包含在java.util包中,例子VectorTest.java演示了Vector的应用
/**
Enumeration接口定义了可以对一个对象的类集中的元素进行枚举(一次获得一个)的方法。这个接口尽管没有被摈弃,但已经被Iterator所替代。Enumeration对新程序来说是过时的。然而它仍被几种从以前版本遗留下来的类(例如Vector和Properties)所定义的方法使用,被几种其他的API类所使用以及被目前广泛使用的应用程序所使用。
Enumeration指定下面的两个方法:
boolean hasMoreElements()
Object nextElement()
执行后,当仍有更多的元素可提取时,hasMoreElements()方法一定返回true。当所有元素都被枚举了,则返回false。nextElement()方法将枚举中的下一个对象做为一个类属 Object的引用而返回。也就是每次调用nextElement()方法获得枚举中的下一个对象。调用例程必须将那个对象置为包含在枚举内的对象类型。
对于Enumeration可以以Vector为例
Vector里有很多对象,如果你要查看其中的所有对象,一个办法是用Vector的get(int index)方法,不过这样效率比较低,另外一个方法是用Vector的elements()方法返回一个Enumeration对象,用Enumeration的hasMoreElements()方法向下移动并判断当前位置是否有对象,有则用nextElement()方法返回这个对象
例如, 打印 vector v中的所有对象:
Enumeration e = v.elements()
while(e.hasMoreElements() )
{
System.out.println(e.nextElement());
}
另外还有个Iterator接口,和Enumeration是差不多的,不过名称比较短,通常推荐用Iterator
Iterator
对集合进行迭代的迭代器。迭代器代替了 Java Collections Framework 中的 Enumeration。Collections 接口中定义了 iterator 方法,用于返回在此 collection 的元素上进行迭代的迭代器。
*/
通过Class类与Runtime类中的方法可以获得运行时的信息,如当前类名、超类名、包名、内存空间以及操作系统名称等.
以下程序演示了如何获取运行时信息.
//ClassTest.java
public class ClassTest
{
public static void main(String args[])
{
String str=new String("");
System.out.println("本类名 ="+str.getClass().getName());
System.out.println("超类名 ="+str.getClass().getSuperclass().getName());
System.out.println("包名 ="+str.getClass().getPackage().getName());
System.out.println("操作系统 ="+System.getProperty("os.name"));
System.out.println("Java版本 ="+System.getProperty("java.vm.version"));
System.out.println("内存总量 ="+Runtime.getRuntime().totalMemory());
System.out.println("剩余空间 ="+Runtime.getRuntime().freeMemory());
}
}
System.getProperty(String name)方法用于得到系统的属性.下面是该方法的常用参数。-------------------------------------------------
java.versionjava.version Java运行环境版本
java.vendorjava.vendor Java运行环境卖主
java.vendor.urljava.vendor.url Java卖主的URL
java.homejava.home Java的安装目录
java.vm.specification.version Java虚拟机规范版本
java.vm.specification.vendor Java虚拟机规范供应商
java.vm.specification.name Java虚拟机规范名称
java.vm.versionjava.vm.version Java虚拟机执行版本
java.vm.vendorjava.vm.vendor Java虚拟机实现供应商
java.vm.namejava.vm.name Java虚拟机实现名称
java.specification.version Java运行时环境规范版本
java.specification.vendor Java运行时环境规范供应商
java.specification.name Java运行时环境规范名称
java.class.versionjava.class.version Java类格式版本号
java.class.pathjava.class.path Java类路径
java.library.pathjava.library.path 名单上的路径搜索时,装载图书馆
java.io.tmpdirjava.io.tmpdir 默认temp文件路径
java.compilerjava.compiler 名称JIT编译器使用
java.ext.dirsjava.ext.dirs 道路扩建目录或目录
os.name 操作系统名称
os.archos.arch 操作系统体系结构
os.versionos.version 操作系统版本
file.separatorfile.separator 文件分隔符("/"在UNIX)
path.separatorpath.separator 路径分隔符(":"在UNIX)
line.separatorline.separator 线分离("\氮",在Unix)
user.nameuser.name 用户的帐号名称
user.homeuser.home 用户的home目录
user.diruser.dir 用户的当前工作目录
Java的工具包中提供了大量的类,我们可以根据需要来了解它们的使用方法.
Java中的集合框架可理解为一个容器,该容器主要指映射(map)、集合(set)、列表(list)、数组(array)和散列表(hashtable)等抽象数据结构。容器包含有多个元素,这些元素通常是一些java对象。而集合框架则是针对上述抽象数据结构所定义的一些标准编程接口,主要是由一组精心设计的接口、类和隐含在其中的算法所组成,通过它们可以采用集合的方式完成对java对象的存储、获取、操作以及转换等功能。
1. 数组(Arrays)类
是集合框架的一个成员类,功能主要是对数组对象进行排序、搜索和填充操作。它所提供的方法都是静态方法
常用方法如下:
binarySearch(int[] a,int key):二分法返回值所在位置(要求该数组己排好序)
public static boolean equals(int[] a,int[] b):判两个数组是否相等
public static void fill(int[] a,int val):用指定值填充数组中的每个元素
public static void sort(int[] a):按升序排序
例:
int[] a1=newint[10];
int[] a2=newint[10];
Arrays.fill(a1,47);
Arrays.fill(a2,47);
System.out.println(Arrays.equals(a1,a2));
a2[3]=11;
a2[2]=9;
System.out.println(Arrays.equals(a1, a2));
Arrays.sort(a2);
System.out.println(Arrays.binarySearch(a2,11));
2. 基本的集合接口
Collection接口 Map接口
Set接口 List接口
Map接口号Collection接口没有任关系
Map的典型应用是访问按关键字存储的值,所包含的是一个个键-值对,而不是单个独立的元素。
Collection指的是一组元素,这四个集合的描述如下:
(1) Collection接口是一组允许重复的对象
(2) Set接口继承Collection,但不允许集合中出现重复元素
(3) List接口允许集合中有重复,并引入位置索引
(4) Map接口包含的是键-值对
Set接口的实现类有HashSet和TreeSet,List接口的实现类有ArrayList和LinkedList(历史集合类为Vector-ArrayList和Stack-LinkedList类),Map接口听实现类有HashMap和TreeMap(历史集合类为HashTable-HashMap)
是指一个不包含重复的对象集合,其定义方法分为两类(集合和元素之间的方法,集合之间的方法)
l public boolean add(Object o)
l public boolean remove(Object o)
l public boolean contains(Object o)
l public int size()
n public boolean containsAll(Collection c)
n public boolean addAll(Collection c)
n public boolean removeAll(Collection c)
n public boolean retainAll(Collection c)
一般采用HashSet类创建一个无序的集合对象,而用TreeSet创建一个有序的集合对象
例1-1:
publicstaticvoid main(String[] args) {
HashSet s=new HashSet();
for(int i=0;i<args.length;i++)
if(!s.add(args[i]))
System.out.println("重复的单词"+args[i]);
System.out.println(s.size()+"不同的单词:"+s);
}
例1-2
HashSet uniques=new HashSet();
HashSet dups=new HashSet();
for(int i=0;i<args.length;i++)
if(!uniques.add(args[i]))
dups.add(args[i]);
uniques.removeAll(dups);
System.out.println("不同的单词:"+uniques);
System.out.println("重复的单词:"+dups);:
例2:
boolean b;
TreeSet s=new TreeSet();
b=s.add("to");
System.out.println("添加单词 to,返回为 "+b);
b=s.add("be");
b=s.add("or");
b=s.add("not");
b=s.add("to");
b=s.add("be");
Iterator i=s.iterator();
while(i.hasNext())
System.out.println(i.next());
例3:[HashSet与TreeSet区别]
HashSet s=new HashSet();
s.add("40");
s.add(new Integer(40));
Iterator i=s.iterator();
while(i.hasNext())
System.out.println(i.next());//该程序输出为40 40,数据类型不匹配没关系
若该程序中的HashSet变为TreeSet,则程序抛出Exception in thread "main" java.lang.ClassCastException
是指一个有序的对象集合,也称为对象序列
通过列表接口,可以利用整数索引(即元素在列表中的位置)对列表中的每个元素有准确的控制,包括访问特定位置的元素对象和查询特定的元素。与集合最大的不同是,列表中允许出现重复的元素
a. 针对特定位置的方法:
n public void add(int index,Object element)
n public boolean addAll(int index,Collection c)
n public Object get(int index)
n public int indexOf(Object o)
n public int lastIndexOf(Object o)
n public Object remove(int index)
n public Object set(int index,Object element)
b. 针对集合类的方法描述:
n public boolean containsAll(Collection c)
n public boolean addAll(Collection c)
n public boolean removeAll(Collection c)
n public boolean retainAll(Collection c)
ArrayList类是通过数组方式来实现的,而LinkedList类是通过链表结构来实现的,若对一个列表结构的开始和结束处有频繁地添加和删除操作时,较为理想地是选用LinkedList,通常用LinkedList对象来表示一个堆栈(Stack)或队列(Queue)。
LinkedList特有的方法:
n public void addFirst(Object o)
n public void addLast(Object o)
n public Object removeFirst()
n public Object removeLast()
ArrayList的例子:
ArrayList list=new ArrayList();
list.add(new Byte((byte)1));
list.add(new Short((short)2));
list.add(new Integer(3));
list.add(new Long(60L));
list.add(new Float(5.0F));
list.add(new Double(60.00));
Iterator i=list.iterator();
while(i.hasNext())
System.out.println(i.next());
int len=list.size();
for(int n=0;n<len;n++)
System.out.println(list.get(n));
LinkedList的例子:
LinkedList queue=new LinkedList();
queue.addFirst("Benz");
queue.addFirst("Buick");
queue.addFirst("Honda");
System.out.println(queue);
queue.removeLast();
queue.removeLast();
System.out.println(queue);
该接口描述了不重复的键到值的映射,一个映射对象中不能包含重复的键,且每个键只能映射到一个值,类似于数学中的“一一映射”。
键1 键2 键3 |
值1 值2 值3 |
键n |
值n |
Map接口中主要定义三类操作方法:修改、查询和集合视图
修改:
u public Object put(Object key,Object value)
u public Object remove(Object key)
u public void putAll(Map t)
u public void clear()
查询:
u public Object get(Object key)
u public boolean containsKey(Objec key)
u public boolean containsValue(Object value)
u public boolean isEmpty()
u public int size()
集合视图(允许将键、值或条目“键-值”对)作为集合来处理
u public Collection values()
u public Set keySet()
u public Set entrySet()
另外Map接口下还有一接口MapEntry,方法如下:
u public Object setValue(Object value)
u public Object getValue()
u public Object getKey()
u public boolean equals(Object o)
在Map中插入、删除和定位元素,最好用HashMap,但如果需要按顺序遍历键,那么需要选择TreeMap。
例:HashMap用法,判断命令行参数中各个字符串所出现的次数
private static final Integer ONE=new Integer(1);
public static void main(String args[])
{
HashMap m=new HashMap();
Integer freq=null;
for(int i=0;i<args.length;i++)
{
m.put(args[i],(freq==null?ONE:new Integer(freq.intValue()+1)));
}
System.out.println("不同的单词有:"+m.size()+"个");
System.out.println(m);
}
java.lang.Object
java.util.regex.Pattern
1.使用Pattern类定义规则表达式,使用Matcher类来匹配其他序列中的模式。
2.Pattern 没有构造函数,使用函数 static Pattern compile(String pattern) 来获得。Matcher 对象也没有构造函数,需要使用Pattern对象的matcher方法获得。Matcher 有方法:matches(判断是否匹配), find (判断是否有子串匹配),start 和 end 分别获得当前匹配的输入序列的索引和超出匹配项末尾(第一个)的索引,replaceAll替换所有匹配的序列 等。
3.规则表达式:+ 匹配一次或多次; * 匹配0次或多次; ? 匹配0或1次(详细参见帮助文档)
示例//PatternTest.java:
import java.util.regex.*;
public class PatternTest {
public static void main(String[] args) {
Pattern p = Pattern.compile("java");
Matcher m = p.matcher("hellojavaiswhatIlovejava,hah");
System.out.println("matches:" + m.matches());
System.out.println("find:" + m.find());
System.out.println("groupcount:" + m.groupCount());
System.out.println("group:" + m.group());
System.out.println("start:" + m.start());
System.out.println("end:" + m.end());
}
}
正则表达式在字符串处理上有着强大的功能,sun在jdk1.4加入了对它的支持 下面简单的说下它的4种常用功能:
查询:
以下是代码片段:
String str="abc efg ABC";
String regEx="a|f"; //表示a或f
Pattern p=Pattern.compile(regEx);
Matcher m=p.matcher(str);
boolean rs=m.find();
如果str中有regEx,那么rs为true,否则为flase。如果想在查找时忽略大小写,则可以写成Pattern p=Pattern.compile(regEx,Pattern.CASE_INSENSITIVE);
提取:
以下是代码片段:
String regEx=".+\(.+)$";
String str="c:\dir1\dir2\name.txt";
Pattern p=Pattern.compile(regEx);
Matcher m=p.matcher(str);
boolean rs=m.find();
for(int i=1;i<=m.groupCount();i++){
System.out.println(m.group(i));
}
以上的执行结果为name.txt,提取的字符串储存在m.group(i)中,其中i最大值为m.groupCount();
分割:
以下是代码片段:
String regEx="::";
Pattern p=Pattern.compile(regEx);
String[] r=p.split("xd::abc::cde");
执行后,r就是{"xd","abc","cde"},其实分割时还有跟简单的方法:
String str="xd::abc::cde";
String[] r=str.split("::");
替换(删除):
以下是代码片段:
String regEx="a+"; //表示一个或多个a
Pattern p=Pattern.compile(regEx);
Matcher m=p.matcher("aaabbced a ccdeaa");
String s=m.replaceAll("A");
结果为"Abbced A ccdeA"
如果写成空串,既可达到删除的功能,比如:
String s=m.replaceAll("");
结果为"bbced ccde"
附:
\D 等於 [^0-9] 非数字
\s 等於 [ \t\n\x0B\f ] 空白字元
\S 等於 [^ \t\n\x0B\f ] 非空白字元
\w 等於 [a-zA-Z_0-9] 数字或是英文字
\W 等於 [^a-zA-Z_0-9] 非数字与英文字
^ 表示每行的开头
$ 表示每行的结尾
本章内容:
l 异常概念、引发原因、实例、分类及常用异常等
l 如何进行异常处理
l 异常类的继承架构
l 声明异常
l 抛出异常
l 自定义异常
l IOException类的使用
l 异常调用机制
Throwable异常,使用它们的子类
好处:因为'Exception'是所有异常的超类,如果方法中抛出'Exception'则会使得该方法的异常的接收者无法区分所需处理异常的类型。
引用:CS-> IllegalThrows
级别:强制
对于编程人员来说,程序的错误是不可避免的,一般来说,错误可分为语法错误、运行错误和逻辑错误。
其中编译器能检查出语法错误,如果程序中存在语法错误,程序将不能编译,更不能执行;运行错误是程序在执行过程中产生的错误,它很有可能产生不可意料的结果,有时可能对系统有严重的危害,如计算机的瘫痪。这种错误也被称为异常,可通过异常处理来解决;逻辑错误是编译没有问题,运行时也不会发生运行时错误,但运行结果和编程者的意图不符,这类错误可通过调试来解决。
在程序执行中,任何中断正常程序流程的异常条件就是错误或异常。例如,发生下列情况时,会出现异常:
1.要打开的文件并不存在。
2.网络连接中断
3.受控操作数超出预定范围
4.非常感兴趣地正在装载的类文件丢失
5.在访问数组时,数组的下标值超过了数组容许的范围。
6.原本预期用户由键盘输入的是整数,但用户输入的却是英文字母。
上述的状况均是在编译时期无法发现的,要等到程序真正运行时才会知道问题出在哪儿。Java把这类不寻常的状况称为异常。
异常是指程序在执行过程中出现程序本身没有预料到的情况,从而导致程序错误结束。
异常处理就是用来在发生运行异常时,告诉程序如何控制自身的运行。
异常是一个对象,它在程序运行异常的时候被创建出来,并被在发生错误的位置抛出(throw),由一定的接收机制来接收并处理,这就是异常处理的一个简单过程。这个异常对象必须是某个异常类的实例,这个异常类必须是己经定义好的,Java类库中己经存在了许多异常类,并提供一定的机制让程序员创建自己的异常类。
在Java编程语言中,错误类定义被认为是不能恢复的严重错误条件。在大多数情况下,当遇到这样的错误时,建议让程序中断。
(1)一个不正常的执行条件被Java虚拟机同步地检测到。这些异常不在程序的任意点抛出,而是在这些异常被规定为表达式求值或语句执行的可能结果的点被抛出,如:
l 违反Java语言的正常语义的操作。如:数组下标越界。
l 装载或连接Java程序的一部分中的错误。
l 超过了资源的某些限制。如:使用过多的存储器。
(2)在Java代码中执行一条throw语句。
(3)发生异步异常:类Thread的stop方法被调用或者是虚拟机发生内部错误。
如果和传统的方法比较,异常具备了很多优点,包括如下:
(1)把错误代码从常规代码中分离出来。
(2)把错误传播给调用堆栈。
(10)按错误类型和错误差别分组。
(4)系统提供了对于一些无法预测的错误的捕获和处理。
(5)克服了传统方法的错误信息有限的问题。
public class HelloWorld
{
public static void main(String args[])
{
int i=0;
String greetings[]={"Hello World!","No,I mean it!","Good bye!"};
while(i<4)
{
System.out.println(greetings[i]);
i++;
}
}
}
Object类是所有类的父类,Object类有一子类名为Throwable。从下图10-1中可以明显看出Throwable类是所有异常类的父类,也就是说Throwable类的任何子孙类所产生的对象都是异常。
图10-1 异常类层次关系
异常可分为以下三类:
(1)Error:很难恢复的严重错误,由Java虚拟机生成并抛出,Java程序不做处理。
(2)Exception(程序中的问题,可预知的):Java编译器要求Java程序必须捕获或声明所有的非运行时异常。
(10)可以根据需要创建自己的异常类。
9.1.5 Java编程语言提供几种预定义的异常,最常见的异常描述:
ArithmeticException:当出现异常算术条件时引发此异常。如除以零产生这种异常。
NullPointerException:当应用企图使用需要的对象处于null时引发此异常。没有分配内存的对象持有null值。引发此异常的情形包括:
使用没有为它分配内存的对象。
调用null对象的方法。
访问或修改null对象的属性。
ArrayIndexOutOfBoundsException:当企图访问超出数组下标的数组元素时引发此异常。例如,如果试图访问只有十个元素的数组的第十一个元素时,将引发此异常。
FileNotFoundException:试图访问不存在的文件时产生。
IOException:由于一般I-O故障而引起的,如读文件、网络等故障。
OutOfMemoryException:当没有足够的内存来分配新对象时产生。
SecurityException:当applet试图执行由于浏览器的安全设置而不允许的动作时产生。
在没有异常处理的语言中,必须使用if-else或switch等语句,配合所想得到的错误状况来捕捉(catch)程序里所有可能发生的错误。但为了捕捉这些错误,编写出来的程序代码经常是长串的if-else语句,也许还不能捕捉到所有的错误,因而导致运行效率降低。
Java的异常处理机制恰好弥补了这个不足。它具有易使用、可自行定义异常类、允许抛出异常,且不会拖慢运行速度等优点。因而在设计Java程序时,应充分地利用Java的异常处理机制,以增进程序的稳定性及效率。
Java本身己有相当好的机制来处理异常的发生。
下例说明了缺省情况下Java的异常处理
public class Test
{
public static void main(String args[])
{
int arr[]=new int[5];
arr[10]=7;
System.out.println("end of main() method!!");
}
}
如果没有编写处理异常的程序代码,则Java的默认异常处理机制会先(1)抛出异常,(2)停止程序运行。
程序的异常发生后,Java便把这个异常抛了出来可是抛出来之后没有程序代码去捕捉它,程序停止运行。
常规的异常处理是通过3个关键词来实现的:try-catch-finally。用try来执行一段程序,如果出现异常,系统抛出一个异常,可以通过它的类型来捕捉(catch)并处理它,或最后(finally)由缺省处理器来处理。
1.try语句
捕获异常的第一步就是用try{…}语句指定了一段代码,该段代码就是一次捕获并处理异常的范围。在执行过程中,该段代码可能会产生并抛弃一个或多个异常,因此,它后面的catch语句进行捕获时也要做相应的处理。
2.catch语句
每个try语句必须伴随一个或多个catch语句,用于捕获try代码块所产生的异常并做相应的处理。catch语句有一个形式参数,用于指明其所能捕获的异常类型,运行时系统通过参数值把被抛弃的异常对象传递给catch语句。
程序设计中要根据具体的情况来选择catch语句的异常处理类型,一般应该按照try代码块中异常可能产生的顺序及其真正类型进行捕获和处理,尽量避免选择最一般的类型作为catch语句中指定要捕获的类型。
当然也可以用一个catch语句处理多个异常类型,这时它的异常类型应该是这多个异常类型的父类,但这种方式使得在程序中不便于确切判断异常的具体类型。
catch中的参数应该是能够被处理的异常类名,而且必须是throwable类的子类。
3.finally语句
捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流程转到程序的其它部分以前,能够对程序的状态作统一的管理。
无论try所指定的程序块中抛出或不抛出异常,也无论catch语句的异常类型是否与所抛出的异常的类型一致,finally所指定的代码都要被执行,它提供了统一的出口。
通常在finally语句中可以进行资源的清除工作,如关闭打开的文件等。对于捕获异常有诸多语法格式,下面来探讨异常语法格式。
(1)捕获异常语法格式1:使用一个或多个catch匹配
try { //可能产生异常的代码; } catch(异常类型变量) { //调用者对异常的处理; } |
或:
try { //可能产生异常的代码; } catch(异常类型1变量) { //调用者对异常的处理; } catch(异常类型2变量) { //调用者对异常的处理; } …… |
下面分别对捕获异常语法格式进行举例。
例9-1:
public class Exception1 { public static void main(String[] args) { try { System.out.println(9/0); } catch (Java.lang.ArithmeticException e) { System.err.println("发生异常: " + e.toString()); e.printStackTrace(); } } } |
因为在除法中规定,除数不能为0。所以当上例中打印9/0时,会抛出一个异常。在异常类中,Java.lang.ArithmeticException类是用来捕捉运算时出现的异常。因此,在catch中是用Java.lang.ArithmeticException。捕捉到异常后,调用这个异常的toString()和printStackTrace()方法打印出这个异常的相关信息。
上面列举了使用一个catch匹配的语法,在下面的例子中对使用多个catch匹配进行举例。
public class Exception2 { public static void main(String[] args) { try { String num=args[0]; int numValue=Integer.parseInt(num); System.out.println("平方为 "+numValue*numValue); } catch(ArrayIndexOutOfBoundsException ne) { System.out.println("未提供任何参数!"); } catch(NumberFormatException nb) { System.out.println("不是数字!"); } } } |
上例中变量num的值是需要输入的,再用
int numValue=Integer.parseInt(num)语句进行类型转换,最后打印这个数的平方。如果当用户没有输入任何数,将产生ArrayIndexOutOfBoundsException异常。如果当用户输入的不是一个数字时,将产生NumberFormatException异常。
注意:异常类像其它类一样,但由于无论何时想要使用超类都必须定义一个相关的子类并使用该子类,因此,可以捕获异常“组”并以相同的捕获代码来处理它们。例如,尽管IOExceptions(输入/输出异常)有几种不同的类型,通过俘获IOException,也可以捕获IOException任何子类的实例。
(2)捕获异常语法格式2:定义总会被执行的代码
try { //可能产生异常的代码; } finally { //总是执行的代码; } |
或:
try { //可能出现异常的代码; } catch(异常类型1变量) { /调用者处理异常; } …… } finally { //总是执行的代码; } |
上面介绍了使用finally语句的一般格式,下面对其进行举例。
public class Exception10 { public static void main(String[] args) { try { System.out.println(9/0); } catch (Java.lang.ArithmeticException e) { System.err.println("发生异常: " + e.toString()); e.printStackTrace(); } finally { System.out.println("Finally 已执行"); } } } |
finally所指定的代码都是要被执行。
finally语句定义一个总是执行的代码块,而不考虑异常是否被捕获。
异常处理依据下列的顺序来处理异常:
1.try程序块若有异常发生时,程序的运行便中断并抛出“由异常类所产生的对象”。
2.抛出的对象如果属于catch()括号内欲捕捉的异常类,则catch会捕捉此异常,然后进到catch的块里继续运行。
3.无论try程序块是否捕捉到异常,或者捕捉到的异常是否与catch()括号里的异常类相同,最后一定会运行finally块里的程序代码。
另外,finally块是可以省略的。如果省略了finally块不写,则在catch()块运行结束后,程序跳到try-catch块之后的地方继续运行。
可以利用catch括号内的异常类变量e获取有关异常的信息。
[注意:]
a.不论有没有异常发生,finally语句都会执行,那何必还用finally语句呢?
finally还是有其特殊之处的,即使try代码块和catch代码块中使用了return语句退出当前方法或break跳出某个循环,相关的finally代码块都要执行。
b.finally中的代码块不能被执行的惟一情况是:在被保护代码块中执行了System.exit(0)。
c.try代码块与catch代码块及finally代码块之间不能有其他语句。
d.finally的块运行结束后,程序再回到try-catch-finally块之后的地方继续运行。
当异常发生时,通常有两种方法来处理,一种是交由Java默认的异常处理机制做处理。这种处理方式较无弹性,且Java通常只能输出异常信息,接着便终止程序的运行。
另一种是处理方式是自行编写try-catch-finally块来捕捉异常,自行编写程序代码来捕捉异常最大的好处是:可以灵活操控程序的流程,且可做出最适当的处理。
public class HelloWorld
{
public static void main(String args[])
{
int i=0;
String greetings[]={"Hello World!","No,I mean it!","Good bye!"};
while(i<4)
{
try
{
System.out.println(greetings[i]);
}
catch(ArrayIndexOutOfBoundsException e)
{
i+=1;
}
i++;
}
}
}
异常类使用中的两个问题
1. 异常匹配
异常匹配是指try块中抛出的对象如何找到对应的异常控制器。Java中规定,只要catch的括号内定义的对象名能指向try块中抛出的异常就可以匹配。即catch的括号内定义的对象名必须是try块中可能抛出的异常类的同一类或父类,甚至祖先类。因此,说用catch(Exception e)或catch(Throwable e)能捕获所有异常就并不奇怪了。由于以上的规则,如果用一个子类去捕获父类的异常将失败。
例MissCatch说明了子类不能和父类异常匹配
另外,由于抛出一个异常后,异常控制系统会按顺序搜索“最接近”的控制器。一旦找到相符的控制器,就认为异常己得到控制,不再进行更多的搜索工作。因此,控制器的排列应有一定的限制,就是从小往大排,否则,编译器会报错。
2.RuntimeException的特殊情况
if(t==null)
throw new NullPointerException();
上面的语句看起来很正常,如果传递的对象名t未指向任何实际地址,就抛出NullPointerException类的异常。但事实上Java中不需要这样做,Java中存在一个特殊的异常类:RuntimeException,这个类里含有一系列子类。这些类的异常是由系统自动抛出、自动捕获,并由系统自行处理。也就是说,任何一个Java程序事实上者被置入一个看不到的try块中,后面的控制器捕获的都是RuntimeException这一系列的异常类的对象,所以我们不必给这些类设立专门的try块和控制器,以上的语句就是不必要的。RuntimeException类的子类有很多,可通过文档获取。
如果在程序中不捕获RuntimeExcepiton类异常,系统又会如何处理呢?由于编译器并不强调我们自己捕获这些特殊的异常,能通过编译进而运行,但假如不捕获的话,一个RuntimeException可能滤过所有的方法,最后系统捕获这些异常,并输出有关信息。
异常类可分为两大类:java.lang.Exception与java.lang.Error类。这两个类均继承自java.lang.Throwable类。下图是Throwable类的继承关系图。
-------- VirtualMachineError-----
| |
------------ Error------| -------------------------------
| | | |
| |------ OutOfMemoryError StackOverflowError
| ------- AWTError
| ----- ArithmeticException
Throwable| ---------RuntimeException------|---- NullPointerException
| | |------
----------- Exception--|----- ----- IndexOutOfBoundsException
| ---- EOFException
---------- IOException-------------|-------
---- FileNotFoundException
习惯上将Error与Exception类统称为异常类,但这两者本质上还是有所不同的。Error类专门用来处理严重影响程序运行的错误(error),可是通常不会设计程序代码去捕捉这种错误,其原因在于即使捕捉到它,也无法给予适当的处理。比如说内存溢出。不可能指望程序能处理这样的情况。
相较于Error类,Exception类包含了一般性的异常这些异常通常在捕捉到之后便可做妥善的处理,以确保程序继续运行。
在异常类的继承架构图中可以看出:Exception类扩展出数个子类,其中IOException与RuntimeException是较常用的两种。RuntimeException可以不编写异常处理的程序代码,依然可以编译成功,它是在程序运行时才有可能发生,例如数组的下标超出了范围。与RuntimeException不同的是,IOException一定要编写异常处理的程序代码才行,它通常用来处理与输入/输出相关的工作,如文件的访问、网络的联机等。
事实上在catch()括号内,只接收由Throwable类的子类所产生的对象,除此之外,其他的类均不接受。
如果在一个方法中产生了异常,但是该方法并不处理它产生的异常,而是沿着调用层次向上传递,由调用它的方法来处理这些异常,叫声明异常。
通常的情况是在该方法中并不确切知道该如何对这些异常进行处理,比如FileNotFoundException类异常,它由FileInputStream的构造方法产生,但在其构造方法中并不清楚如何处理它,是终止程序的执行还是新生成一个文件,这需要由调用它的方法来处理。
声明异常的方法是在产生异常的方法名后面加上要抛出(throws)的异常的列表:
retType methodName([paramlist]) throws exceptionList |
如类FileInputStream中的read()方法是这样定义的:
public int read() throws IOException { … } |
throws子句中可以同时指明多个异常,说明该方法将不对这些异常进行处理,而且声明抛出它们。
需要强调的是,对于非运行时异常,程序中必须要作处理,或者捕获,或者声明抛弃;而对于运行时异常,程序中则可不处理。
通过用throws关键字声明异常之后,要求在程序中使用该方法时必须对异常进行处理,如果使用该方法的程序还不想处理异常,则可以继续抛出,即在定义方法的时候就用throws关键字声明异常,这样就可以通过编译,但在出错的时候,程序仍然会异常终止。如下图10-6:
图9-6 沿着调用层次向上传递
下例具体说明了怎样声明异常:
public class Exception4 { public void print() throws Exception { System.out.println(9/0); } public static void main(String[] args) { Exception4 e4=new Exception4(); try { e4.print(); } catch(Java.lang.ArithmeticException ae) { ae.printStackTrace(); } System.out.println("go on"); } } |
上例中,方法print()中会产生一个Java.lang.ArithmeticException异常。但并没有捕捉它,而是使用throws将它抛出。但在main()中调用print()方法时就必须捕捉它,否则无法通过编译。
声明抛出异常首先必须生成异常。前面所提到的异常或者是由Java虚拟机生成,或者是由Java类库中的某些类生成。事实上,在程序中也可以生成自己的异常对象,异常可以不是出错产生,而是人为地抛出。
不论那种方式,生成异常对象都是通过throw语句实现:
throw new ThrowableObject(); ArithmeticException e = new ArithmeticException(); throw e; |
注意:抛出的异常必须是Throwable或其子类的实例。
9.5.1在程序中抛出异常
在程序中抛出异常(运行时异常缺省地由系统抛出,不需要显式抛出)时,一定要用到throw这个关键字,其语法如下:
throw 由异常类所产生的对象
下例是在程序中抛出异常的范例
public class Test
{
public static void main(String args[])
{
int a=4,b=0;
try
{
if(b==0)
throw new ArithmeticException();
else
System.out.println(a+"/"+b+"="+a/b);
}
catch(ArithmeticException e){
System.out.println(e+" throwed");
}
}
}
Test1省略了抛出ArithmeticException的语句但程序依然得到与Test相同的结果,这是因为即使不抛出此异常,系统还是会自动抛出的原因。
public class App9_05
{
public static void main(String args[])
{
int a=4,b=0;
try
{
System.out.println(a+"/"+b+"="+a/b);
}
catch(ArithmeticException e){
System.out.println(e+" throwed");
}
}
}
9.5.2指定方法抛出异常
如果方法内的程序代码可能会发生异常,且方法内又没有使用任何的try-catch-finally块来捕捉这些异常时,则必须在声明方法时一并指明所有可能发生的异常,以便让调用此方法的程序得以做好准备来捕捉异常。即如果方法会抛出异常,则可将处理此异常的try-catch-finally块写在调用此方法的程序代码内。
由方法抛出异常,方法必须以下面的语法来声明
[格式2 由方法抛出异常]
方法名(参数…) throws 异常类1,异常类2,…
Test是指定由方法来抛出异常的范例。
public class Test
{
public static void aaa(int a,int b) throws ArithmeticException
{
int c;
c=a/b;
System.out.println(a+"/"+b+"="+c);
}
public static void main(String args[])
{
try
{
aaa(4,0);
}
catch(ArithmeticException e)
{
System.out.println(e+" throwed");
}
}
}
Test从不同类内的方法抛出异常
class Ctest
{
public static void aaa(int a,int b) throws ArithmeticException
{
int c;
c=a/b;
System.out.println(a+"/"+b+"="+c);
}
}
public class Test
{
public static void main(String args[])
{
try
{
Ctest.aaa(4,0);
}
catch(ArithmeticException e)
{
System.out.println(e+" throwed");
}
}
}
我们所创建的每个应用可能有特殊的约束.例如,在铁路预订应用中,乘客必须指出旅行的地方.还有,三岁以上的乘客必须买票.类似地,银行应用中,如果客户在18岁以下,只允许他开联合账户.开发考虑到这种约束的应用时错误处理是必要的.Java中异常类没有这种特定的应用约束.所以,我们可以创建自己的异常以处理这些约束并保证应用数据的完整性.
为了适应各种异常,Java可通过继承的方式编写自己的异常类。因为所有的异常类均继承自Throwable所以自己设计的类也必须继承这个类。
我们可通过扩充Exception类来创建异常类。
自定义异常类的语法如下:
[格式:编写自定义异常类的语法]
class 异常名称 extends Exception
{
… …
}
可以在自定义异常类里编写方法来处理相关的事情,甚至可以不编写任何语句也可正常地工作,这是因为父类Exception己提供相当丰富的方法,通过继承子类均可使用它们。
例1
import javax.swing.*;
class MyException extends Exception
{
public String getMessage()
{
return "Invalid Age. The customer should not be provided with a connection";
}
}
public class Customer
{
int custAge;
void setAge(int age) throws MyException
{
if((age<20)||(age>60))
throw new MyException();
custAge=age;
}
public static void main(String[] args)
{
while(true)
{
Customer obj=new Customer();
int age=Integer.parseInt(JOptionPane.showInputDialog("Enter the customer's age:"));
try
{
obj.setAge(age);
}
catch(MyException e)
{
System.out.println(e.getMessage());
}
}
}
}
例2:
这个实例主要用于验证天数有效性,在这个实例中还运用了自定义异常类的相关知识。在这个实例中定义了三个类,分别是:
类名 |
功能 |
MyDateException |
自定义异常类 |
MyDate |
验证天数的有效性 |
MyDateTest |
测试 |
代码如下:
class MyDate { int year,month,day; void setDate(int y,int m,int d) throws MyDateException { If(d>31) throw new MyDateException("Too Big Day Value!"); year=y; month=m; day=d; } } class MyDateException extends Exception { private String reason; public MyDateException(String r) { reason=r; } public String getReason() { return reason; } } public class MyDateTest { public static void main(String[] args) { MyDate md=new MyDate(); try { md.setDate(2001,1,35); } catch(MyDateException e) { System.out.println(e.getReason()); e.printStackTrace(); } } } |
如上面的代码所示:
类MyDateException是自定义异常类,它继承了Exception类,并定义了一个名为getResason()的方法。这个方法返回一个字符串。
类MyDate的主要功能是验证天数的有效性。在类MyDate中定义了一个名为void setDate(int y,int m,int d) throws MyDateException的方法。该方法接收3个参数,并声明MyDateException异常。在方法体中,当天数大于31时,则抛出MyDateException异常,并传递一个字符串给MyDateException。
类MyDateTest主要负责测试。在类的main()方法中生成一个MyDate类的对象。在try—catch块中使用MyDate类的对象调用setDate()方法,并传递三个参数。其中第三个参数的值为35,大于了31。所以当执行这条语句时将产生异常。
IOException是用来处理有关输入/输出的异常的针对IOException类的异常处理,编写的方式有两种:一种是直接由方法抛出异常,让Java默认的异常处理机制来处理。
例 App12_09
import java.io.*;
public class App12_09
{
public static void main(String[] args) throws IOException
{
BufferedReader buf;
String str;
buf=new BufferedReader(new InputStreamReader(System.in));
System.out.println("Input a string: ");
str=buf.readLine();
System.out.println("string= "+str);
}
}
另一种方式是在程序代码内编写try-catch块来捕捉由系统抛出的异常。例App12_10
import java.io.*;
public class App12_10
{
public static void main(String[] args)
{
BufferedReader buf;
String str;
buf=new BufferedReader(new InputStreamReader(System.in));
try
{
System.out.println("Input a string: ");
str=buf.readLine();
System.out.println("string= "+str);
}
catch(IOException e)
{
}
}
}
如果方法中的语句可能抛出异常,而方法中又没有提供相应的异常处理程序,那么这个异常就被抛出到调用方法中,如果异常也没有在调用方法中被处理,它就被抛出到该调用方法的调用程序,这个过程一直延续到异常被处理。如果异常到这时还没被处理,它便回到main(),而且,如果main()不处理它,那么,该异常就异常地中断程序。
本节内容 · 什么是数据流 · 数据流的分类 · 管道流 · 对象流
|
【讲解要点】
· 数据流的概念;
· 管道流和对象流在程序中的实现。
流的概念很形象,当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件、内存、或网络连接。当然,当程序需要写入数据的时候,就会开启一个通向目的地的流。如下图10-1:
图10-1 流的过程图
流可以看成是数据的导管。导管的两端一端是源端,一端是目的端。数据从源端输入,从目的端输出。流的使用者不用考虑流内部数据是如何传输的,只需要向源端输入数据和向目的端取出数据,即流的两端都有数据缓冲区可以暂存数据,这样接收端可以不必每时每刻都监视流是否有数据需要接收,数据来了之后放在缓冲区内,等需要的时候再去读取。而发送端也不必每一个字节都调用发送方法。而是等集中了—定数量的数据后再一起发送,这样可以大大提高发送效率。
Java中提供了两个基本的流操作类InputStream和OutputStream,同时在这两个类的基础又引出了如字符流、文件流等等。如下图10-2,10-3:
图10-2 文件流
图10-3 字符流
1.FileInputStream和FileOutputStream
这些类是结点流,而且正如这个名字所暗示的那样,它们使用磁盘文件。要构造一个FileInputStream,所关联的文件必须存在而且是可读的。如果要构造一个FileOutputStream而输出文件已经存在,则它将被覆盖。
FileInputStream infile =new FileInputStream("myfile.dat"); FileOutputStream outfile =new FileOutputStream("results.dat"); |
2.BufferInputStream和BufferOutputStream
BufferlnputStream和BufferOutputStream是过滤器流,它们可以提高I/O操作的效率。
3.DataInputStream和DataOutputStream
DataInputStream和DataOutputStream过滤器通过流来读写Java基本类。读写的过程如:
(1)DataInputStream方法
byte readByte()
long readLong()
double readDouble()
(2)DataOutputStream方法
void writeByte(byte)
void writeLong(long)
void writeDouble(double)
注意:DataInputStream和DataOutputStream的方法是成对的。
4.PipedInputStream和PipedOutputStream
PipedInputStream和PipedOutputStream管道流用来在线程间进行通信。
管道流用来在线程间进行通信。一个线程的PipedInputStream对象从另一个线程PipedOutputStream对象读取输入。要使管道流有用,必须有一个输入方和一个输出方。
创建管道流如下:
PipedInputStream pis=new PipedInputStream(); PipedOutputStream pos=new PipedOutputStream(pis); |
或:
PipedOutputStream pos=new PipedOutputStream(); PipedInputStream pis=new PipedInputStream(pos); |
管道流一定是输入输出并用。例如数据从输出管道进,从输入管道出,代码如下:
import Java.io.*; class pipedstream { public static void main(String args[]) throws IOException{ byte aByteData1=123, aByteData2=91; PipedInputStream pis= new PipedInputStream(); PipedOutputStream pos= new PipedOutputStream(pis); System.out.println("PipedInputStream"); try{ pos.write(aByteData); pos.write(aByteData2); System.out.println((byte)pis.read()); System.out.println((byte)pis.read()); } catch(Exception e){ e.printStackTrace (); } finally { pis.close(); pos.close(); } |
对一个文件流读写对象是一个简单的过程。读对象和写对象一样简单,只需要说明一点---readObject()方法将流作为一个Object类型返回,而且在使用那个类的方法之前,必须把它转换成合适的类名。
1.写一个对象流
对一个文件流读写对象是一个简单的过程。考虑如下代码段,它将一个Java.util.Data对象的实例发送到一个文件:
public class SerializeDate { SerializeDate(){ Date d = new Date (); try{ FileOutputStream f = new FileOutputStream("date.ser"); ObjectOutputStream s = new ObjectOutputStream(f); s.writeObject (d); f.close (); } catch (IOException e){ e.printStackTrace (); } } public static void main (String args[]){ new SerializeDate(); } |
2.读一个对象流
读对象和写对象一样简单,只需要说明一点readObject()方法将流作为一个Object类型返回,而且在使用那个类的方法之前,必须把它转换成合适的类名。
public class UnSerializeDate { UnSerializeDate (){ Date d = null; try{ FileInputStream f = new FileInputStream("date.ser"); ObjectInputStream s = new ObjectInputStream(f); d = (Date) s.readObject (); f.close (); } catch (Exception e){ e.printStackTrace (); } System.out.println("Unserialized Date object from date.ser"); System.out.println("Date: "+d); } public static void main (String args[]){ new UnSerializeDate(); } |
本节内容 · 文件I/O · 文件的输入/输出 · 随机存储文件 · 串行化
预计课时 3课时
|
【讲解要点】
~~~~~~~~~~~~~~~~~~~~~~~
· 对文件的认识和访问方法;
· 文件的输入/输出操作过程。
基于Java的平台无关性的特点,Java的文件访问机制也是独立于文件系统的。Java提供了一个类File,这个类提供了获取文件名和文件状态的机制。File类Java.io包中定义了一个文件类来专门处理文件,并获取文件的有关信息。
文件类的构造器有以下几个:
public File(String Path) public File (String Path,String name) public File (File Dir,String name) |
在第一个构造器中,可以通过指定一个目录结构来创建一个对象,这个目录结构既可以是个绝对目录,也可以是一个相对目录。例如可以通过下面的语句创建一个File对象:
File dir=new File(”C:\\Java\\projects”) |
如果当前目录已经是“c:\\”的话,也可以通过下面的语句创建—个和上面等价的File类:
File dir=new File(”\\Java\\projects”) |
第二个构造器则是为一个文件创建一个File类。在dir参数中指定了目录,在name参数中指定了文件名。下面是一个例子。
File dir=new File(”C:\\Java\\projects”,”File.Java”) |
第三个构造器和第二个构造器类似,只是dir参数通道一个File对象来指定路径,而不是通过字符串。下面是一个例子:
File dir=new File(”C:\\Java\\projects”) File file= new File(dir,”File.Java”) |
Java语言中通过File类来建立与磁盘文件的联系。File类用来获取或设置文件或目录的属性,但不支持从文件读取数据或者往文件里写数据。
例如判断是否有所要查找的文件:
import Java.io.*; class IOExample { public static void main(String[] args) { System.out.println("输入要查找的文件名或绝对路径: "); char c; StringBuffer buf = new StringBuffer(); try { while ((c = (char)System.in.read()) != '\n') buf.append(c); //将输入的字符加到buf中,直到出现回车符 } catch (IOException e) { System.out.println("Error:"+e.toString()); } File file = new File(buf.toString().trim()); //创建File 类的file对象 if (file.exists()) { //如果文件在当前目录存在 System.out.println("文件名:"+file.getName()); //显示文件的文件名 System.out.println("在当前目录下的路径:"+file.getPath());//显示文件的路径 System.out.println("绝对路径:"+file.getAbsolutePath()); //显示文件的绝对路径 System.out.println("是否可写:"+file.canWrite()); //显示文件是否可写 System.out.println("是否可读:"+file.canRead()); //显示文件是否可读 System.out.println("大小:"+(file.length()) +"B"); //显示文件的大小 } else //如果文件在当前目录不存在 System.out.println("Sorry,file not found."); //显示文件没有找到的提示信息 } } |
Java语言向文件中写数据或读出文件中的内容,是通过文件输入流、文件输出流来实现。FileInputStream类用来打开一个输入文件,FileOutputStream类用来打开一个输出文件。建立文件流格式如下:
FileInputStream in=new FileInputStream(fp); FileOutputStream out=new FileOutputStream(fp); |
输入流的参数是用于输入的文件名,输出流的参数是用于输出的文件名。
1.文件输入流(FileReader)
因为大多数程序会涉及文件读/写,FileReader类是一个经常用到的类,从FileReader类可以在一指定文件上实例化一个文件输入流,利用流提供的方法从文件中读取一个字符或者一组数据。
例:
import Java.io.FileReader; public class FileReaderExample { public static void main(String args[]) throws Exception { int size; char cbuf[]=new char[256]; FileReader f1 = new FileReader("abc.html"); //创建输入流 size=f1.read(cbuf,0,256); //从输入流读取数据到数组 System.out.println("Total Available Bytes: " + size); System.out.println("content of the file:"); System.out.println(cbuf); //打印数组 f1.close(); //关闭输入流 } } |
在使用此例时,要在和类文件同一目录下创建一个名为abc后缀为.html的文件。abc.html文件如图10-4:
图10-4测试Web
2.文件输出流(FileWriter)
由FileWriter 类可以实例化一个文件输出流,并提供向文件中写入一个字符或者一组数据的方法。
FileWriter也有两个和FileReader类似的构造方法。
如果用FileWriter来打开一个只读文件会产生IOException异常。
例:
import Java.io.*; class FileWriterExample { public static char getInput() [] throws Exception { char buffer[] = new char[12]; for (int i=0;i<12;i++) buffer[i] = (char) System.in.read(); //从键盘读入12个字符到缓冲区 return buffer; } public static void main(String args[]) throws Exception { char buf[] = getInput(); Writer f0 = new FileWriter("file1.txt"); Writer f1 = new FileWriter("file2.txt"); Writer f2 = new FileWriter("file3.txt"); for (int i=0; i < 12; i+=2) f0.write(buf[i]); //将缓冲区中偶数位置上的字符保存到文件file1.txt中 f0.close(); f1.write(buf); // 将缓冲区中的字符写入文件流f1 f1.close(); //关闭输出流f1 f2.write(buf,12/4,12/2); //将缓冲区中从位置3开始的连续6个字符写入文件流f2 f2.close(); //关闭文件输出流f2 } } |
如果键盘输入为“abcdefghijkl”,运行结束后查看三个文件的内容,将会看到如下所示的结果:
file1.txt的内容为:
acegik
file2.txt的内容为:
abcdefghijkl
file3.txt的内容为:
defghi
例:综合应用。
import Java.io.*; public class Example { public static void main(String[] args) throws IOException { File inputFile = new File("read.txt"); // 建立一个名字为inputFile的File类对象 File outputFile = new File("write.txt"); // 建立一个名字为outputFile的File类对象 FileReader in = new FileReader(inputFile); // 建立一个名字为in的字符流 FileWriter out = new FileWriter(outputFile); // 建立一个名字为out 的字符流 int c; while ((c = in.read()) != -1) //从文件read.txt中读数据 out.write(c); // 将读入的数据写到文件write.txt中 in.close(); // 关闭输入流in out.close(); // 关闭输出流out } } |
在使用此例时,要在当前目录下创建read.txt和write.txt两个文本文件。且read.txt中要有内容,write.txt为空。
平常所使用的文件中,有很多是二进制文件,它们以字节作为数据处理单位。对这些文件就要使用字节流来读写了,其实字符文件也可以用字节流来进行读写。字节流的使用和构造方法与其对应的字符流的使用和构造方法类似,在此不再赘述。
与文件有关的字节流有两个:FileInputStream和FileOutputStream,分别与字符流的FileReader和FileWriter相对应。
例:
import Java.io.*; import Java.util.*; class FileInputStreamExample { public static void main(String args[]) throws Exception { int size; FileInputStream f1 = new FileInputStream("abc.html"); size = f1.available(); //获取文件的可读字节数 System.out.println("Total Available Bytes: " + size); System.out.println("First 1/4 of the file: read()"); for (int i=0;i<size/4;i++) System.out.print((char) f1.read()); //获取文件的1/4的数据 System.out.println("Total Still Available: " + f1.available()); System.out.println("Reading the next 1/8: read(b[])"); byte b[] = new byte[size/8]; if (f1.read(b) != b.length) System.err.println("Something bad happened");//判断读入的字节长度是否跟预计长度一致 String tmpstr = new String(b,0,0,b.length); System.out.println(tmpstr); System.out.println("Still Available: " + f1.available()); System.out.println("Skipping another 1/4: skip()"); f1.skip(size/4); //跳过文件1/4的数据 System.out.println("Still Available: " + f1.available()); System.out.println("Reading 1/16 into the end of array"); if (f1.read(b,b.length-size/16,size/16) != size/16)//判断读入的字节长度是否跟预计长度一致 { System.err.println("Something bad happened"); } System.out.println("Still Available: " + f1.available()); f1.close(); //关闭流 } } |
注意:在使用上面的例子时,要在类文件相同目录下创建一个名为abc后缀为.html的文件。
目前,没有直接的方法可以将对象写入到随机存取文件中。但是可以使用ByteArray输入/输出流作为中介,来向随机存取文件中写入或从随机存取文件中读出字节,并且可以利用字节流来创建对象输入/输出流,以用于读写对象。需要注意的是在字节流中要包含一个完整的对象,否则读写对象时将发生错误。例如:Java.io.ByteArrayOutputStream可用于获取ObjectOutputStream的字节流,从中可得到byte数组并可将之写入到随机存取文件中。相反,可以从随机存取文件中读出字节数组,利用它可构造ByteArrayInputStream,进而构造出ObjectInputStream,以读取对象。
使用基本的文件读写方式存取数据,如果仅仅保存相同类型的数据,则可以用同一种格式保存,有时需要保存一个复合类型;相反,如果有多种不同类型的数据,要么把它分解成若干部分,以相同类型(如String)保存,需要在程序中添加解析不同类型数据格式的逻辑。串行化操作为大家提供花尽可能少的时间和代码对数据进行解析的一条途径。
串行化可以把对象以某种特定的编码格式写入或从外部字节流(即ObjectInputStream/ObjectOutputStream)中读取。串行化一个对象仅仅需要实现一下Serializable接口即可:
public class Example implements Java.io.Serializable { ... } |
但在要串行化的类中的每个属性都必须是可串行化的。其实所有基本类型都是可串行化的,可以看看JDK文档,会发现很多类其实已经实现了Serializable,于是这些类的对象以及基本数据类型都可以直接作为需要串行化的那个类的内部属性。如果在要串行化的类中的属性不是可串行化的,那么这个类还需要实现Serializable接口,把所有属性都变为可串行化的。
ObjectOutputStream.writeObject() 将一个对象序列化到外部字节流。
ObjectInputStream.readObject() 从外部字节流读取并重新构造对象。
实际应用中,Serializable接口并没有定义任何方法,仿佛它只是一个标记(或者说像是Java的关键字)而已,一旦虚拟机看到这个“标记”,就会尝试调用自身预定义的序列化机制,除非在实现Serializable接口的同时还定义了私有的readObject()或writeObject()方法。如果不愿意让系统使用缺省的方式进行序列化,那就必须定义上面提到的两个方法:
public class Example implements Java.io.Serializable { private void writeObject(Java.io.ObjectOutputStream out) throws IOException { ... } private void readObject(Java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { ... } ... } |
如可以在上面的writeObject()里调用默认的序列化方法:ObjectOutputStream.defaultWriteObject()。
如不愿意将某些敏感的属性和信息序列化,也可以调用ObjectOutputStream.writeObject()方法明确指定需要序列化那些属性。
小结 本章主要介绍流的概念,并提到数据流的分类,引申到管道流,对象流。本章的第二节中对文件的操作和对象的串行化做了较为详细的讲解。 |
Reader和InputStream定义了相同的方法,只是它们传输的数据类型不同.
Reader含有以下读取字符和字符数组的方法:
int read()
int read(char cbuf[])
int read(char cbuf[],int offset,int length)
InputStream定义了读取byte型数据的方法:
int read();
int read(byte cbuf[])
int read(byte cbuf[],int offset,int length)
同样,Writer和OutputStream定义了相同的方法来输出char型和byte型数据:
Writer方法如下:
int writer(int c)
int writer(char cbuf[])
int writer(char cbuf[],int offset,int length)
OutputStream方法如下:
int write(int c)
int write(byte cbuf[])
int write(byte cbuf[],int offset,int length)
以下方法的基本规律是:Reader和Writer的方法是相反的,一个是输入,一个是输出;InputStream和OutputStream的方法也是相反的;而Reader和InputStream有相同的方法名;处理不同的数据类型,Writer和OutputStream也一样。另外,所有的流包括readers,writers,inputStream和outputStream都在创建时(即创建某个流类的对象)自动打开,而关闭流可以有两种方式,一种是用close()方法,另一种由系统在程序退出之后自动关闭。
InputStreamReadert和OutputStreamWriter类用于在字节和Unicode字符流之间转换数据。InputStreamReader类转换InputStream子类对象为Unicode字符流。OutputStreamWriter类转换Unicode字符输出流为字节输出流。
缓冲字符的I/O由BufferedReader和BufferedWriter类支持。BufferedReader类的readLine()方法用于从控制台,文件,或其他输入流读文本行。
例:
import java.io.*;
public class test
{
public static void main(String args[])
{
InputStreamReader inputReader=new InputStreamReader(System.in);
BufferedReader bufferStream=new BufferedReader(inputReader);
String readString;
try
{
do
{
System.out.print("\nPlease enter something: ");
System.out.flush();
readString=bufferStream.readLine();
System.out.println("Hello User>This is what you wrote:");
System.out.println("> "+readString);
}while(readString.length()!=0);
}
catch(IOException e)
{
}
}
}
当你用文字编辑器创建文件,它允许你把文本存储到文件里。储存在文件的文本是封装在文件对象里的数据。一旦你把它存储在磁盘里,该对象就永久有效。在创建它的程序执行后对象依然存在的能力称为持久性。
当文字编辑器停止执行后,文件对象继续存在。为使文件对象持久,你必须实行重要的一步存储文件。存储文件对象被称为序列化。序列化是实现持久性的关键。它提供职能让你把对象写进流并能在以后读出来。
JDK1.2提供java.io包的Serializable接口以支持对象的序列化。读对象和把它写进文件流的过程是非常简单的。ObectInputStream类的readObject()方法用于从流读对象,同样地,ObjectOutputStream类的writeObject()方法把对象写进流。
本节内容 · 什么是进程与线程 · 线程与进程的区别 · 线程的优势 · 线程的模型 |
【讲解要点】
~~~~~~~~~~~~~~~~~~~~~~~
· 进程和线程的概念;
· 使用进程的原因;
· 通过和单进程的对比进一步了解线程。
进程(Process):简单来说就是一个程序在给定活动空间和初始条件下,在一个处理机上的执行过程。可以简单的理解为进程就是一个正在运行的程序。
线程(Thread):一个线程是一个程序内部的一个顺序控制流。即:一个或多个线程组成一个进程。
从下图12-1可以看出进程和线程的关系:
图12-1 进程与线程的关系
线程与进程的区别主要有以下两点:
(1)多个进程的内部数据和状态都是完全独立的,而多线程是共享一块内存空间和一组系统资源,有可能互相影响。
(2)线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担要小。
线程的优势在于以下五点:
(1)减轻编写交互频繁、涉及面多的程序的困难(如监听网络端口)。
(2)程序的吞吐量会得到改善(同时监听多种设备,如网络端口、串口、并口以及其它外设)。
(3)有多个处理器的系统,可以并发运行不同的线程。
(4)线程间的切换成本比进程间切换成本低。
(5)基于线程的开发所需的开销少。
Java运行系统在很多方面依赖于线程,所有的类库设计都考虑到多线程。实际上,Java使用线程来使整个环境异步。这有利于通过防止CPU循环的浪费来减少无效部分。
为更好的理解多线程环境的优势,可以将它与它的对照物(单线程环境)相比较。单线程系统的处理途径是使用一种叫做轮询的事件循环方法。在该模型中,单线程控制在无限循环中运行,轮询一个事件序列来决定下一步做什么。一旦轮询装置返回信号表明:已准备好读取网络文件,则事件循环调度控制管理到适当的事件处理程序。直到事件处理程序返回,系统中没有其它事件发生。这就没有有效的利用CPU,导致了程序的一部分独占了系统,阻止了其它事件的执行。总的来说,单线程环境,当一个线程因为等待资源时阻塞(block,挂起执行),整个程序停止运行。
Java多线程的优点在于取消了主循环/轮询机制。一个线程可以暂停而不影响程序的其它部分。例如,当一个线程从网络读取数据或等待用户输入时,所产生的空闲时间可以被利用到其它地方。在Java程序中出现线程阻塞,仅有一个线程暂停,其它线程继续运行。
线程存在于几种状态。线程可以正在运行(running)。只要获得CPU时间它就可以运行。运行的线程可以被挂起(suspend),并临时中断它的执行。一个挂起的线程可以被恢复(resume),允许它从停止的地方继续运行。一个线程可以在等待资源时被阻塞(block)。在任何时候,线程可以终止(terminate),使用该方法就立即中断了它的运行。一旦终止,线程不能被恢复。
本节内容 · 创建线程的方式 · 线程状态 · 线程的基本控制 · 线程的调度 · 同步和死锁 · Thread类的重要方法 · 优先级 · 线程组和线程池 · Wait-notify机制 |
【讲解要点】
~~~~~~~~~~~~~~~~~~~~~~~线程的创建;
· 同步和死锁产生的原因和解决的方法;
· 优先级的概念;
· Thread类中方法的使用;
· Wait-notify机制的原理及使用方法。
在Java中,创建线程有两种方式,分别是通过继承Java.lang.Thread类和实现Java.lang.Runnable接口。
1.继承Java.lang.Thread类
使用方式1创建一个线程的步骤为:
(1)继承Java.lang.Thread类(即,继承Java.lang.Thread类)。
(2)在继承了Java.lang.Thread的子类中覆盖Thread类的run()方法。
(3)创建这个新类的实例。
(4)在实例上调用start()方法。
下面的例子使用方式1来创建线程。
例12-1:
public class ThreadExample extends Thread//步骤1:继承Thread类。 { public void run() //步骤2:覆盖父类中的run()方法,让它打印10次new thread!!. { for(int i=0;i<10;i++) { System.out.println("new thread!!"); } } public static void main(String[] args) { ThreadExample example=new ThreadExample(); //步骤3:创建这个子类的一个实例 example.start(); //步骤4:调用start()方法 } } |
例12-1具体地演示了通过方式1创建线程的步骤。当启动一个线程时(即调用start()方法时),虚拟机会自动调用相对应的run()方法(注:程序的入口就是run()方法)。线程执行的每条语句都包含在run()方法中(无论是直接包含还是间接包含的代码)。当run()返回时,线程就消亡了,并且消亡的线程不能重新启动。
程序运行结果如下图12-2:
图12-2 程序运行结果
除了通过扩展Java.lang.Thread类来创建线程这种方式以外,还可以通过另一种方式创建线程。即:实现Java.lang.Runnable接口。
2.实现Java.lang.Runnable接口
使用方式2创建一个新的线程的步骤为:
(1)实现Java.lang.Runnable接口。
(2)在实现Java.lang.Runnable接口的新类中实现run()方法。
(3)创建这个新类的实例。
(4)利用新类的实例构造一个Thread类的实例。
(5)在Thread实例上调用start()方法。
下面是通过方式2创建线程的例子。
例12-2:
public class RunnableExample implements Runnable //步骤1:实现Runnable接口 { public void run() //步骤2:实现run()方法。让它打印10次new runnable!! { for(int i=0;i<10;i++) { System.out.println("new runnable!!"); } } public static void main(String[] args) { RunnableExample example=new RunnableExample(); //步骤3:创建这个类的实例 Thread t=new Thread(example); //步骤4:利用新类的实例构造一个Thread类的实例。 t.start();// 步骤5:调用start()方法 } } |
例12-2具体地演示了通过方式2创建线程的步骤。在实现了Runnable后,还需要实现Runnable中的run()方法。在类RunnableExample的main()方法中不仅要创建类RunnableExample的实例,还需利用这个实例构造一个Thread类的实例,最后利用Thread类的实例调用start()方法。总之,线程通过Thread对象的一个实例引用。线程从装入的Runnble实例的run()方法开始执行。线程操作的数据从传递给Thread构造函数的Runnable的特定实例处获得。
程序运行结果如下图12-3:
图12-3 程序运行结果
不论是以哪种方式创建线程,run()方法是运行线程的主体,启动线程时(即:调用start()方法时),由Java直接调用public void run()。
使用Runnable接口使得包含线程体的类还可以继承其它类,弥补了不能继承其它类的缺陷。
直接继承Thread类以后不能再继承其它类,但编写简单,并可直接操纵线程;使用Runnable接口时,若要在run()方法中操纵线程,必须使用Thread.currentThread()方法获得当前线程的引用后,才能对其进行操纵。
在具体应用中,采用哪种方法来构造线程体要根据具体情况而定。通常,当一个线程体所在的类已经继承了另一个类时,就应该用实现Runnable接口的方法。
线程是程序内部的一个顺序控制流,它总处于某一种状态中。线程的状态表示了线程正在进行的活动以及在这段时间内线程能完成的任务。
下图12-4清晰地说明了线程在调用特定方法时的特定状态。
图12-4 线程的状态
从上图中可以看出,线程的状态可分为以下四种:
1.创建状态(New Thread)
当创建了一个新的线程时(如,myThread thd=new myThread();),它就处于创建状态,此时它仅仅是一个空的线程对象,系统不为它分配资源。处于这种状态时只能启动或终止该线程,调用除这两种以外的其它方法都会失败并且会引起非法状态异常IllegalThreadStateException(对于其它状态,若所调用的方法与状态不符,都会引起非法状态异常)。
2.可运行状态(Runnable)
当线程处于创建状态时,可以调用start()方法来启动它,产生运行这个线程所需的系统资源,安排其运行,并调用线程体run()方法,这样就使得该线程处于可运行状态(Runnable)。
需要注意的是这一状态并不是运行中状态,因为线程也许实际上并未真正运行。由于很多计算机都是单处理器的,所以要在同一时刻运行所有的处于可运行状态的线程是不可能的,Java运行系统必须实现调度来保证这些线程共享处理器。
3.不可运行状态(Not Runnable)
线程处于可运行状态时,当下面四种情况发生,线程就进入不可运行状态:
(1)调用了sleep()方法;
(2)调用了suspend()方法;
(3)为等候一个条件变量,线程调用wait()方法;
(4)输入输出流中发生线程阻塞。
对于这四种使得线程处于不可运行状态的情况,都有特定的方法使线程返回可运行状态:
(1)如果线程处于睡眠状态中,sleep()方法中的参数为休息时间,当这个时间过去后,线程即为可运行的;
(2)如果一个线程被挂起,须调用resume()方法来返回;
(3)如果线程在等待条件变量,那么要停止等待的话,需要该条件变量所在的对象调用notify()或notifyAll()方法;
(4)如果在I/O流中发生线程阻塞,则特定的I/O指令将结束这种不可运行状态。
需要注意的是每种方法都仅仅对相应的情况才有作用,例如当一个线程睡眠并且睡眠时间还没有结束时,调用resume()方法是无效的,并且还会引起非法状态异常。
4.死亡状态(Dead):
线程的终止一般可通过两种方法实现:自然撤消或是被停止。自然撤消是指从线程的run()方法正常退出;而调用线程的实例方法stop()则可以强制停止当前线程。
在Java.lang.Thread类中定义了许多控制线程的方法,下面介绍一些常用的方法。
isAlive()方法:可以用来判断线程目前是否正在执行状态中。如果线程已被启动并且未被终止,那么isAlive()返回true,但该线程是可运行或是不可运行的,是不能作进一步的分辨。如果返回false,则该线程是新创建或是已被终止的(同样不能作进一步的分辨)。
下面是使用isAlive()方法的简单例子。
例12-3:
public class isAliveExample extends Thread { public void run() { for(int i=0;i<10;i++) { System.out.println("running!!!"); } } public static void main(String[] args) { isAliveExample t=new isAliveExample (); t.setPriority(1); System.out.println(t.isAlive()); //(1) t.start(); System.out.println(t.isAlive()); //(2) t.setPriority(Thread.MAX_PRIORITY); System.out.println(t.isAlive()); //(3) } } |
例12-3中,分别在三个地方判断线程目前是否正在执行状态中。为了更好的演示isAlive()方法,在线程进入可运行状态前设置它的优先级为最小优先级。在程序中标志为(1)的代码是在start()方法前作判断,所以它返回的结果是false。标志为(2)的代码是在线程进入可运行状态后作判断,此时返回的结果为true。接着为了让线程能更快的自然消亡,把它的优先级设置为最大优先级。标志为(3)的代码在线程自然消亡后作判断,其返回的是false。
程序运行结果如下图12-5:
图12-5 程序运行结果
Join()方法:等待线程执行完毕。
下面是使用join()方法的简单例子。
例12-4:
public class JoinExample extends Thread { public void run() { for(int i=0;i<10;i++) { System.out.println(Thread.currentThread().getName()); } } public static void main(String[] args) { JoinExample t1=new JoinExample (); JoinExample t2=new JoinExample (); t1.setName("t1"); t2.setName("t2"); t1.start(); try { t1.join(); } catch(Exception e) { e.printStackTrace(); } t2.start(); } } |
例12-4中,创建了两个线程,分别为t1和t2。首先让t1进入可运行状态,紧接着调用join()方法让t1等待消亡。然后,让t2进入可运行状态。
程序运行结果如下图12-6:
图12-6 程序运行结果
Yield()方法:将执行的权力交给其它优先级相同的线程,自己到可运行线程队列的最后等待,若队列空,该方法无效。
下面是使用yield()方法的简单例子。
例12-5:
class ThreadA extends Thread { public void run() { Thread.currentThread().yield(); for(int i=0;i<5;i++) { System.out.println("ThreadA!"); } } } class ThreadB extends Thread { public void run() { for(int i=0;i<5;i++) { System.out.println("ThreadB!"); } } } public class YieldExample { public static void main(String[] args) { ThreadA ta=new ThreadA(); ThreadB tb=new ThreadB(); ta.setPriority(3); tb.setPriority(3); ta.start(); tb.start(); } } |
在例12-5中,创建了两个线程,分别为ta和tb。首先把这两个线程的优先级都设置为3。注意:在类ThreadA的run()方法中调用了yield()方法。这时ta会暂停,并让其它线程执行。
程序运行结果如下图12-8:
图12-8 程序运行结果
sleep()方法可以暂停线程的执行,让其它线程得到机会。但sleep()要抛出InterruptedException异常,必须抓住。
下面是使用sleep()方法的简单例子。
例12-6:
class ThreadA extends Thread { public void run() { for(int i=0;i<5;i++) { System.out.println("ThreadA!"); try { Thread.sleep(1000); } catch(Exception e) { e.printStackTrace(); } } } } class ThreadB extends Thread { public void run() { for(int i=0;i<5;i++) { System.out.println("ThreadB!"); try { Thread.sleep(2000); } catch(Exception e) { e.printStackTrace(); } } } } public class test { public static void main(String[] args) { ThreadA ta=new ThreadA(); ThreadB tb=new ThreadB(); ta.start(); tb.start(); } } |
例12-6中,在类ThreadA和类ThreadB中都使用了sleep()方法。分别让ThreadA每休息1000毫秒,打印一次;让ThreadB每休息2000毫秒,打印一次。
程序运行结果如下图12-9:
图12-9 程序运行结果
Suspend()方法和resume()方法可以用来暂停线程或恢复线程。可以由线程自身在线程内部调用suspend()方法暂停自己,也可以在其它线程中通过线程实例调用suspend()方法暂停线程的执行。但是要恢复由suspend()方法暂停的线程,只能在其它线程中通过线程实例调用resume()方法。
下面是使用suspend()方法和resume()方法的简单例子。
例12-12:
public class test extends Thread { public void run() { for(int i=0;i<10;i++) { System.out.println("Run"); If(i==5) { System.out.println("此时i的值为:"+i); Thread.currentThread().suspend(); } } } public static void main(String[] args) { test t=new test(); t.setPriority(Thread.MAX_PRIORITY); t.start(); for(int i=0;i<5;i++) { System.out.println("Main"); } t.resume(); } } |
例12-12中,创建了一个线程t。为了更好的演示suspend()方法和resume()方法的使用,在这里把线程的优先级设置为最大。在线程的run()方法中当i=5时,调用suspend()方法将线程暂停。此时在main()函数中打印5次“main”后,调用resume()方法恢复被暂停的线程。恢复的线程接着刚刚执行到的位置,继续执行,直到消亡。注意:suspend()方法和resume()方法是已被淘汰的方法。但是理解这两个方法的工作原理,能更好的控制线程。
程序运行结果如下图12-10:
图12-10 程序运行结果
这里只对一些常用的方法作了讲解,了解更多控制线程的方法,请查阅相关资料。
Java提供一个线程调度器来监控程序中启动后进入可运行状态的所有线程。线程调度器按照线程的优先级决定调度哪些线程来执行,具有高优先级的线程会在较低优先级的线程之前得到执行。同时线程的调度是抢先式的,即如果当前线程在执行过程中,一个具有更高优先级的线程进入可执行状态,则该更高优先级的线程会被立即调度执行。
多个线程运行时,若线程的优先级相同,由操作系统按时间片轮转方式和独占方式来分配线程的执行时间。
1.同步
一个Java程序的多线程之间可以共享数据。当线程以异步方式访问共享数据时,有时候是不安全的或者不符合逻辑的。比如,同一时刻一个线程在读取数据,另外一个线程在处理数据,当处理数据的线程没有等到读取数据的线程读取完毕就去处理数据,可能得到错误的处理结果。如果采用多线程同步控制机制,等到第一个线程读取完数据,第二个线程才能处理该数据,就会避免错误。
Java中有一个同步模型监视器,负责管理线程对对象中的同步方法的访问,它的原理是:赋予该对象唯一的一把钥匙(钥匙可当作是否已有线程访问同步方法的一个标识),当多个线程进入对象,只有取得该对象钥匙的线程才可以访问同步方法,其它线程在该对象中等待,直到该线程放弃这把钥匙,其它等待的线程抢占该钥匙,抢占到钥匙的线程后才可得以执行,而没有取得钥匙的线程仍被阻塞在该对象中等待。
Java中使用synchronized关键字将对象或方法标志为同步,同步的含义就是在同一时刻只有一个进程可以访问此资源,也就是说,对被标记为同步的资源的访问必须是互斥的。
将对象标记为同步:
Synchronized(O){…}:检查对象O的标记,如果该标记存在,则表示该对象没有被别的线程访问,可以访问它,然后先将标记取走,并执行{}内的语句并在执行完后将标记返回给对象O;如果标记不存在,则将该线程放入等锁池中等待,直到其它线程将标记返回给对象O,才可以访问O。
将方法标记为同步:
public synchronized void print(){…}。 |
简单的说就是synchronized修饰符,确保在某一时刻,方法内只允许有一个线程。下面用一个警察捉贼例子来讲解synchronized修饰符的用法:
例11-8:
public class HotPursuit { public static void main(String[] args) { HotPursuit pursuit=new HotPursuit(); pursuit.go(); } public void go() { Police p=new Police(0); Thief t=new Thief(20); p.start(); t.start(); } } class MyPosition { static int x,y; public void setX(int xx) { this.x=xx; } public void setY(int yy) { this.y=yy; } public synchronized void show() { System.out.println("警察跑到:"+x); System.out.println("贼跑到 :"+y); System.out.println("-----------------------------"); } public boolean catched() { If(x>y) { return true; } else { return false; } } } class Police extends Thread { MyPosition position=new MyPosition(); public Police(int xx) { position.setX(xx); } public void run() { While(true) { position.x+=10; try { Thread.sleep(1000); } catch(InterruptedException ie) { ie.printStackTrace(); } System.out.println("一秒后警察和贼的位置:"); position.show(); if(position.catched()) { System.out.println("---------贼被捉住!---------"); System.exit(0); } } } } class Thief extends Thread { MyPosition position =new MyPosition(); public Thief(int yy) { position.setY(yy); } public void run() { while(true) { position.y+=5; try { Thread.sleep(1000); } catch(InterruptedException ie) { ie.printStackTrace(); }
position.show(); if(position.y>100) { System.out.println("---------贼逃掉了!----------"); System.exit(0); } } } } |
在例11-8中,“贼”是肯定会被捉住的。因为一开始“贼”和“警察”的距离只有20米。而“警察”一次跑10米,“贼”则一次只跑5米。因为只有在“贼”跑过100米时才算逃跑成功。所以按这样的跑法,“贼”是永远跑不掉的。用随机数产生它们的初始距离,可让此程序更真实。
这里主要是给出synchronized修饰符的用法。当Police进入show方法时,Thief是不能进入的。反之,当Thief进入show方法时,Police是不能进入的。(注意:此程序的运行结果可能和给出的运行结果不同,因为这里只把show方法做了同步)。
程序运行结果如下图11-12:
图11-12 程序运行结果
2.死锁
当两个线程竞争有限的资源的时候,如果二者获得资源的先后顺序不同,则有可能两个线程都得到部分资源且等待另一个线程将剩下的资源释放给它,但是如果两个线程都这样等待,程序就跟停止了一样,这种情况就是死锁。单看每个线程的代码是无法发现死锁的,只有线程之间发生交互,才会出现死锁。例如:当一个线程1持有一个synchronized块A时,此线程还希望持有另一个synchronized块B。但synchronized块B已经被线程2持有,而线程2却希望在持有synchronized块B的同时,再持有synchronized块A,则此时问题出现了。线程1和线程2将会永远等到对方把持有的synchronized块释放。
下面用兄弟两抢一套衣服的例子来说明死锁。
例12-9:
public class Deadlock extends Thread { static Object clothing=new Object(); static Object breeches=new Object(); boolean have; public Deadlock(boolean have) { this.have=have; }
public void run() { for(;;) { If(have) { synchronized(clothing) { System.out.println("弟弟抢到了衣服"); synchronized(breeches) { System.out.println("哥哥抢到了裤子"); } } } else { synchronized(breeches) { System.out.println("哥哥抢到了裤子"); synchronized(clothing) { System.out.println("弟弟抢到了裤子"); } } } } } public static void main(String[] args) { Deadlock d1=new Deadlock(true); d1.start(); Deadlock d2=new Deadlock(false); d2.start(); } } |
在例11-9中,当哥哥拿到衣服时,弟弟则把裤子拿到。反之,当弟弟拿到衣服时,哥哥则把裤子拿到。这样反反复复,弟弟和哥哥永远都在等到对方把东西交出来。这个程序是无法结束的。这样就形成了死锁。
程序运行结果如下图11-13:
图11-13 程序运行结果
Java技术不监测也不试图避免死锁。因而保证不发生死锁就成了程序员的责任。避免死锁的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序,按照与获取相反的次序释放锁。
下面列出Thread类的重要方法的详细说明。了解这些方法的功能和使用,能更灵活的开发线程程序。
currentThread:
public static native Thread currentThread() |
功能:返回指向当前执行线程对象的指针。
返回值:当前执行线程。
yield:
public static native void yield() |
功能:使当前执行线程暂停并允许其它线程执行。
sleep:
public static native void sleep(long millis) throws InterruptedException |
功能:使当前执行线程休眠(临时停止执行)指定的毫秒数。线程并不失去对任何监视程序的所有权。
参数:millis - 用毫秒为单位的睡眠时间。
异常:InterruptedException,表示另一个线程中断了该线程。
sleep :
public static void sleep(long millis, int nanos) throws InterruptedException |
功能:使当前执行线程休眠(临时停止执行)指定的毫秒数加纳秒数。线程并不失去对任何监视程序的所有权。
参数:millis - 用毫秒为单位的休眠时间。
nanos - 附加0-999999纳秒休眠时间。
异常:IllegalArgumentException若millis值为负或nanos的值不在范围0-999999中。
InterruptedException,表示另一个线程已中断了该线程。
start:
public native synchronized void start() |
功能:使该线程开始执行;Java虚拟机调用线程run方法。结果是当前线程(方法start返回的线程)和另一个线程(执行方法run的线程)同时运行。
异常:IllegalThreadStateException如果该线程已经启动。
run :
public void run() |
功能:如果该线程是用不同的Runnable运行对象构造的,则该Runnable对象的run方法被调用;否则,该方法不做任何事就返回。Thread的子类必须覆盖此方法。
stop:
public final void stop() |
功能:强迫线程停止执行。首先,该线程的checkAccess方法被调用且无参数。这会导致抛出SecurityException(在当前线程中)。只要该线程发生任何异常并将新创建的ThreadDeath对象当作异常抛出,线程将被迫停止。允许终止还未启动的线程。若该线程最终启动了,它将立即终止。正常状态下应用程序不应试图捕捉ThreadDeath,除非它必须做一些特殊的清除操作(注意到抛出ThreadDeath会finally导致try语句在线程正式死亡之前被执行)。如果catch语句捕获到ThreadDeath对象,则为了让该线程真正死亡,应该将该对象重新抛出。响应其它未捕获异常的顶层错误处理程序既不打印消息也不通知应用程序,只要该未捕获异常是ThreadDeath的实例。
异常:SecurityException 若当前线程不能修改该线程。
stop:
public final synchronized void stop(Throwable o) |
功能:强迫线程停止执行。首先,该线程的方法checkAccess被调用且无参数。这会导致抛出SecurityException(在当前线程中)。如果参数obj为null,NullPointerException会被抛出(在当前线程中)。只要该线程发生任何异常并将Throwable对象obj当作异常抛出,线程将被迫结束。这是一个非常的行为;通常,应执行方法stop且不带参数。允许终止还未启动的线程。若该线程最终启动了,它将立即终止。
参数:obj - 被抛出的可抛出对象。
异常:SecurityException 若当前线程不能修改该线程。
interrupt :
public void interrupt() |
功能:中断该线程。
interrupted :
public static boolean interrupted() |
功能:测试当前线程是否被中断。
返回值:true当前线程被中断;false当前线程没有被中断。
isInterrupted:
public boolean isInterrupted() |
功能:测试当前线程是否被中断。(应注意的是interrupted是一个静态方法,而isInterrupted会被当前Thread实例调用的)
返回值:true当前线程被中断;false当前线程没有被中断。
destroy:
public void destroy() |
功能:撤消该线程,不做任何清除。被加锁的监视程序仍被锁住。
isAlive:
public final native boolean isAlive() |
功能:测试该线程是否在运行状态。如果它已被启动且还未被中断则它是处于启动状态的。
返回值:true:当前线程是处于启动状态的;false:当前线程不处于启动状态。
suspend:
public final void suspend() |
功能:挂起该线程。
如果线程是处于运行状态,则它将被挂起并且在被继续执行前不会再有任何活动。
异常:SecurityException
若当前线程不能被修改则抛出该异常。
resume:
public final void resume() |
功能:继续执行被挂起的线程。
如果线程是处于运行状态时被挂起的,则继续执行。
异常:SecurityException
若当前线程不能被修改则抛出该异常。
setPriority:
public final void setPriority(int newPriority) |
功能:改变线程优先级。首先,该线程的checkAccess方法被调用且无参数。这会导致抛出SecurityException。否则,该线程的优先级被设置为指定的newPriority和线程组中线程所允许的最大优先级中的较小值。
异常:IllegalArgumentException
若优先级不在MIN_PRIORITY到MAX_PRIORITY的范围中,则抛出异常。
SecurityException若当前线程不能修改该线程。
getPriority:
public final int getPriority() |
功能:返回该线程的优先级。
返回值:该线程的名字。
setName:
public final void setName(String name) |
功能:把该线程的名字改为与参数名name相同。首先,该线程的checkAccess方法被调用且无参数。这会导致抛出SecurityException。
参数:name - 该线程的新名字。
异常:SecurityException 若当前线程不能修改该线程。
getName :
public final String getName() |
功能:返回该线程的名字。
返回值:该线程的名字。
getThreadGroup :
public final ThreadGroup getThreadGroup() |
功能:返回该线程的线程组。
返回值:该线程的线程组。
activeCount :
public static int activeCount() |
功能:返回该线程组当前活动的线程数。
返回值:该线程组当前活动的线程数。
enumerate :
public static int enumerate(Thread tarray[]) |
功能:把该线程组和其子组中所有运行的线程拷贝到指定数组中。此方法用数组参数简单调用该线程的线程组的方法enumerate。
返回值:放入数组中的线程数。
countStackFrames:
public native int countStackFrames() |
功能:计算该线程中堆栈帧的数目。线程必须被挂起。
返回值:该线程中堆栈帧的数目。
异常:IllegalThreadStateException 若该线程未被挂起。
join :
public final synchronized void join(long millis) throws InterruptedException |
功能:等待最多millis毫秒等该线程死亡。定时0表示永远等待。
参数:millis - 用毫秒计算的等待时间。
异常:InterruptedException 如果另一个线程已中断了当前线程。
join:
public final synchronized void join(long millis, int nanos) throws InterruptedException |
功能:等待最多millis毫秒加nanos纳秒等该线程死亡。
参数:
millis - 用毫秒计算的等待时间。
nanos - 附加0-999999纳秒等待时间。
异常:IllegalArgumentException 若millis值为负或nanos的值不在范围0-999999中。
InterruptedException 如果另一个线程已中断了当前线程。
join:
public final void join() throws InterruptedException |
功能:等待线程死亡。
异常:InterruptedException 如果另一个线程已中断了当前线程。 dumpStack :
public static void dumpStack() |
功能:打印当前线程的堆栈状态。此方法用于调试。
setDaemon:
public final void setDaemon(boolean on) |
功能:标记该线程是后台驻留线程还是用户线程。Java虚拟机当所有运行线程都为后台驻留线程时退出。此方法必须在线程启动前调用。
参数:on - 若 true, 将该线程为后台驻留线程。
异常:IllegalThreadStateException 该线程是否活着。
isDaemon :
public final boolean isDaemon() |
功能:测试该线程是否是后台驻留线程。
返回值:true 若当前线程是后台驻留线程;false否则。checkAccess:
public void checkAccess() |
功能:确定当前运行的线程是否有权修改该线程。
如果存在安全管理程序,则用该线程为参数调用它的checkAccess方法。这会导致抛出SecurityException。
异常:SecurityException 若当前线程不允许访问该线程。
toString:
public String to String() |
功能:返回表示该线程的字符串,并包含线程的名字,优先级和线程组。
返回值:该线程的字符串表示。
覆盖:类Object中的toString
虽然在上面列出的方法中,有些是已经被淘汰的方法。但是理解这些方法的用法和原理将会对灵活使用线程有很大帮助。
每个线程都有优先级,线程的优先级的高低决定线程的先后执行顺序。虽然CPU处理线程的顺序是非决定性的,但是如果有很多线程堵在那里等着启动,线程调度机制会倾向于首先启动优先级最高的线程。但这并不意味着低优先级的线程就没机会运行了(也就是说优先级不会造成死锁问题)。优先级低只表示运行的机会少而已。
在Java中线程的优先级是用数字1~10来表示的,分为三个优先级别:
低优先级:Thread.MIN_PRIORITY,数值为1(2~4)。
缺省优先级:Thread. NORM_PRIORITY,数值为5。
高优先级:Thread.MAX_PRIORITY,数值为10 (6~9)。
具有相同优先级的多个线程,若它们都为高优先级Thread.MAX_PRIORITY,则每个线程都是独占式的,也就是说这些线程将被顺序执行;若该优先级不为高优先级,则这些线程将同时执行,也就是说这些线程的执行是无序的。
线程被创建后,其缺省的优先级是缺省优先级Thread. NORM_PRIORITY。
不使用缺省优先级时,可调用Thread类的setPriority(int newPriority)方法改变线程的优先级。与setPriority(int newPriority)方法对应的是getPriority(),这个方法用来得到线程的优先级。
下面的例子中使用setPriority(int newPriority)方法类改变线程的优先级。
例11-10:
public class ThreadExample1 extends Thread { public void run() { for(int i=1;i<=10;i++) { try { Thread.currentThread().setPriority(i); } catch(IllegalArgumentException iae) {} System.out.println("当前优先级为:"+Thread.currentThread().getPriority()); } } public static void main(String[] args) { ThreadExample1 example=new ThreadExample1(); example.start(); } } |
例11-10中通过继承Thread类创建了一个线程。在run()方法中改变线程的优先级。在改变优先级时会抛出IllegalArgumentException异常。
程序运行结果如下图11-14:
图11-14 程序运行结果
当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用.达到此目的的过程叫做同步(synchronization).Java为此提供了独特的,语言水平上的支持.
同步的关键是管程的概念.管程是一个互斥独占锁定的对象,或称互斥体.在给定的时间,仅有一个线程可以获得管程.当一个线程需要锁定,它必须时入管程.所有其他的试图进入己经锁定的管程的线程必须挂起直到第一个线程退出管程.这些其他的线程被称为等待管程.一个拥有管程的线程如果愿意的话可以再次进入相同的管程.
可以用两种方法同步化代码.两者都包括synchronized关键字的运用.
Java中同步是简单的,因为所有对象都有它们与之对应的隐式管程.进入某一对象的管程,就是调用被synchronized关键字修饰的方法.当一个线程在一个同步方法内部,所有试图调用该方法(或其他同步方法)的同实例的其他线程必须等待.为了退出管程,并放弃对对象的控制权给其他等待的线程,拥有管程的线程仅需从同步方法中返回.
//SynchTest1.java(未同步)和//SynchTest2.java(同步后)
尽管在创建的类的内部创建同步步是获得同步的简单和有效方法,但它并非在任何时候都有效.原因是:假设想获得不为多线程访问设计的类对象的同步访问,即该类没有用到synchronized方法.而且,该类不是我们自己,而是第三方创建的,我们不能获得它的源代码.这样,我们不能在相关方法前加synchronized修饰符.怎样才能使该类的一个对象同步化呢?解决方法很简单:只需将对这个类定义的方法的调用放入一个synchronized块内就可以了.
//statements to be synchronized
上述例题无条件的阻塞了其他线程异步访问某个方法.Java对象中隐式管程的应用是很强大的,但是我们可以通过进程间通信达到更微妙的境界.
多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序.线程还有第二优点:它远离了轮询.轮询通常由重复监测条件的循环实现.一旦条件成立,就要采取适当的行动.这浪费了CPU时间,这程情形不受欢迎.
为避免轮询,Java包含了通过wait(),notify()和notifyAll()方法实现的一个进程间通信机制.这些方法在对象中是用final方法实现的,所以所有的类都包含它们.这三个方法仅在synchronized方法中才能被调用.尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来是很简单的:
l Wait() 告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify()
l Notify() 恢复相同对象中第一个调用 wait() 的线程
l notifyAll() 恢复相同对象中所有调用wait()的线程.具有最高优先级的线程最先运行.
例子WaitNotifyNotifyAll.java和WaitNotifyNotifyAll1.java显示wait(),notify()及notifyAll()方法的使用:
实例:
这个实例主要是利用管道流,在两个线程间传递基于字符的数据。
代码如下:
import Java.io.*; public class PipedCharacters { public static void writeStuff(Writer rawOut) { try { BufferedWriter out=new BufferedWriter(rawOut); String[][] line={ {"if","you","really","loves","someone,"}, {"just","speak","it","out,"},{"speak","it","out","loud",}, {"otherwise,","the","moment","will"},{"pass","you","by."}, }; for(int i=0;i<line.length;i++) { String[] word=line[i]; for(int j=0;j<word.length;j++) { If(j>0) { out.write(" "); } out.write(word[j]); } out.newLine(); } out.flush(); out.close(); } catch(IOException x) { x.printStackTrace(); } } public static void readStuff(Reader rawIn) { try { BufferedReader in=new BufferedReader(rawIn); String line; While((line=in.readLine())!=null) { System.out.println("read line:"+line); } System.out.println("Read all data from the pipe"); } catch(IOException x) { x.printStackTrace(); } } public static void main(String[] args) { try { final PipedWriter out=new PipedWriter(); final PipedReader in=new PipedReader(out);
Runnable runA=new Runnable() { public void run() { writeStuff(out); } }; Thread threadA=new Thread(runA,"threadA"); threadA.start(); Runnable runB=new Runnable() { public void run() { readStuff(in); } }; Thread threadB=new Thread(runB,"threadB"); threadB.start(); } catch(IOException x) { x.printStackTrace(); } } } |
|
本实例结合IO输入/输出,利用PipedWriter和PipedReader构造一个基于字符的数据管道,并创建了两个线程threadA和threadB。第一个线程(threadA)写入管道,第二个线程(threadB)从管道中读取。
在writeStuff()方法中利用for循环扫描行中的每个单词。写入最后一个单词后,使用newLine()方法来标记行尾,返回之前刷新BufferedWriter并关闭。
在readStuff()方法中从BufferedReader读取每行,直到从readLine()方法返回null(即,检测到文件末尾)为止。
需要避免的与多任务任务有关的特殊错误类型是死锁(deadlock).死锁发生在当两个线程对一对同步对象有循环依赖关系时.例如,假定一个线程进入了对象X的管程而另一个线程进入了对象Y的管程.如果X的线程试图调用Y的同步方法,它将像预料的一样被锁定.而Y的线程同样希望调用X的一些同步方法线程永远等待,因为为到达X,必须释放自己的Y的锁定以使第一个线程可以完成.死锁是很难调试的错误.
本章主要讲解了线程的概念及其实现。在第二节中详细地说明了创建线程的方式、线程的状态、线程的基本控制、线程的调度、同步和死锁、Thread类的重要方法、优先级、线程组和线程池以及wait-notify机制等。 |
有时,在运行时间内知道对象类型是很有用的.例如,你有一个执行线程生成各种类型的对象,其他线程处理这些对象.这种情况下,让处理线程在接受对象时知道每一个对象的类型是大有益处的.另一种在运行时间内知道对象的类型是很有用的情形是强制类型转换.例如,一个名为A的父类能生成两个子类B和C.这样,在强制B对象转换为类型A或强制C对象转换为类型A都是合法的,但强制B对象转换为C对象(或相反)都是不合法的.因为类型A的一个对象可以引用B或C.但是如何知道,在运行时,在强制转换为C之前哪类对象被引用?它可能是A,B或C的一个对象.如果它是B的对象,一个运行时异常被引发.Java提供运行时运算符instanceof来解决这个问题.
instanceof运算符具有下面的一般形式:
object instanceof type
这里,object是类的实例,而type是类的类型.如果object是指定的类型或者可以被强制转换成指定类型,instanceof将它评估成true,若不是,则结果为false.
transient用来处理特殊的情况.如果用transient声明一个实例变量,
当对象存储时,它的值不需要维持.
strictfp的意思是FP-strict,也就是说精确浮点的意思。在Java虚拟机进行浮点运算时,如果没有指定strictfp关键字时,Java的编译器以及运行环境在对浮点运算的表达式是采取一种近似于我行我素的行为来完成这些操作,以致于得到的结果往往无法令你满意。而一旦使用了strictfp来声明一个类、接口或者方法时,那么所声明的范围内Java的编译器以及运行环境会完全依照浮点规范IEEE-754来执行。因此如果你想让你的浮点运算更加精确,而且不会因为不同的硬件平台所执行的结果不一致的话,那就要用关键字strictfp。
可以将一个类、接口以及方法声明为strictfp,但是不允许对接口中的方法以及构造函数声明strictfp关键字,例如下面的代码:
a. 合法的使用关键字strictfp
strictfp interface A {}
public strictfp class FpDemo1 {
strictfp void f() {}
}
b. 错误的使用方法
interface A {
strictfp void f();
}
public class FpDemo2 {
strictfp FpDemo2() {}
}
一旦使用了关键字strictfp来声明某个类、接口或者方法时,那么在这个关键字所声明的范围内所有浮点运算都是精确的。当一个类被strictfp修饰,所有该类的方法都自动被strictfp修饰。
也许希望调用不是用Java语言写的子程序.通常,这样的子程序是
CPU的或是工作环境的执行代码,即本地代码.例如,希望调用本机代码子程序来获得较快的执行时间,或者,希望用一个专用的第三方的库,
它告诉编译器被volatile修饰的变量可以被程序的其他部分改变.一
种这样的情形是多线程程序.在多线程程序里,有时两个或更多的线程共享一个相同的实例变量.考虑效率的问题,每个线程可以自己保存该共享变量的私有拷贝.实际的(或主要的)变量副本在不同的时候更新,例如当进入synchronized方法时.当这种方式运行良好时,它在时间上会是低效的.在某些情况,真正要紧的是变量主副本的值会体现当前的状态.为保证这点,仅需把变量定义成volatile型,它告诉编译器它必须总是使用volatile变量的主副本(或者至少总是保持一些私有的最新的主副本的拷贝,反之亦然),同时,对主变量的获取必须以简洁次序执行,就像执行私有拷贝一样.
Java SDK中包含一个非常有用的工具-javadoc,它可以从源文件产生HTML文档。前面看到的在线API文档就是对标准库的源代码运行javadoc所生成的结果。
如何向源代码中添加以专用定界符/**开始的注释,可以很容易地生成专业风格的文档。
Javadoc工具从以下几项内容中提取信息:
l 包;
l 公有类与接口;
l 公有方法和受保护方法;
l 公有字段和受保护字段。
我们应该为上述每个部件提供注释。注释应紧靠着放在它所描述的部件上面,以/**开始,以*/结束。
第条/**…*/文档注释在标记后都包含着自由格式文本。标记以@开始,比如@author或@param。
13.2类注释
类注释必须放在所有的import语句之后,直接放在class定义之前。如:
/**
A <code>Card</code> object represents a playing card, such as “Queen of Hearts”. A card has a suit (Diamond, Heart, Spade or Club) and a value (1=Ace,2…10,11=Jack,12=Queen,13=King).
*/
Public class Card
{
…
}
很多程序员习惯于在每行注释前都加上一个星号,如下所示:(不推荐这样做)
/**
*A <code>Card</code> object represents a playing card, such as “Queen of Hearts”. A *card has a suit (Diamond, Heart, Spade or Club) and a value *(1=Ace,2…10,11=Jack,12=Queen,13=King).
*/
方法注释必须紧靠着放在它所描述的方法前面。除了通用标记外,我们可以使用以下几种标记:
@param variable description:将向当前方法的“参数”部分添加条目。Description可以占多行,其中还可以使用HTML标记。同一个方法的所有@param标记必须放在一起。
@return description:向当前方法添加“返回”部分。Description可以占多行,其中也可以使用HTML标记。
@throws class desciption:添加说明该方法可能抛出异常的注解。
方法注释实例:
/**
*Raises the salary of an employee.
*@param byPercent the percentage by which to raise the salary(e.g.10=10%)
*@return the amount of the raise
*/
public double raiseSalary(double byPercent)
{
double railse=salary*byPercent/100;
salary+=raise;
return raise;
}
我们只用对公有字段产生文档-通常指的是静态常量。如:
/**
*The "Hearts" card suit
*/
public static final int HEARTS=1;
下列标记可以用于类文档注释。
@author name:产生一“作者”条目,每个标记对应一个作者,可以包含多个@author标记。
@version text:产生一“版本”条目,text可以是对当前版本的任何说明。
下列标记可以用于所有的文档注释:
@since text:产生一个“始自”条目。Text可以是对引入此特性的版本的任何说明。如:@since version1.7.1
@deprecated text:说明此类、方法或变量不应再被使用。Text应该给出替代建议。如:@deprecated Use <code>setVisible(true)</code> instead
通过@see和@link标记,可以利用超链接指向javadoc文档的其他相应部分,或是链接到其他外部文档。
@see link:添加一超级链接,指向“参见”部分。它既可用于类,也可用于方法。在此处,link可以是下列情形之一:
l Package.class#feature label
l <a href=”…”>label</a>
l “text”
第一种情形最为有用。只要提供了类、演绎法中是变量的名字,javadoc会向文档插入一个超链接。如:
@see com.horstmann.corejava.Employee#raiseSalary(double)
将产生一个指向com.horstmann.corejava.Employee类中raiseSalary(double)方法的链接。我们可以省略包名,甚至是把包名和类名都省掉。这时,链接目标将位于当前包或当前类中。
不过要注意,我们必须使用#,而不是.,来分隔类与方法中变量名。因为Java编译器十分灵巧,可以分辨出句点用作包、子包、类、内部类、变量之间的分隔符时的不同含义。但javadoc工具不能区分其不同的含义,所以只能使用符号#对它进行帮助。
如果@see标记后跟随的是字符<,那么则需要指定超链接,超链接可以是任意的URL,如:
@see <a href=www.horstmann.com/corejava.html>The Core Java home page</a>
在各种情形下,都可以指定label,它是个可选项,作为链接锚显示。如果省略了这个标记,那么链接锚将是目标代码名称或URL。
如果@see标记后跟随的是字符”,那么将会在“参见”部分显示文本。比如:
@see “Core Java 2 volume 2”
可以为一项特性添加多个@see标记,不过,它们必须放在一起。
还可以在注释的任何地方插入专用标记,放置指向任意类或方法的超级链接,它的形式为{@link package.class#feature label}。对feature的描述同@see标记一样。
类、方法和变量注释直接放到Java源文件中,但要生成包注释就需要在第个包目录中添加一个名为package.html的文件。所有在<body>…</body>标记之间的文本者会被提取。
还可以为所有的源文件提供概述注释。这些注释放在名为overview.html的文件中,该文件位于包含所有源文件的父目录下。所有在<body>…</body>标记之间的文本者会被提取。用户从导航栏中选择“Overview”,这些注释就会显示。
假设要存放HTML文件的目录为docDirectory。执行步骤如下:
(1) 切换到包含所有要生成文档的源文件的目录。如果有嵌套包要生成文档,比如com.horstmann.corejava,那么必须转到包含子目录com的目录下(它也是overview.html所在的目录,如果overview.html存在的话)。
(2) 对于单个包,运行命令:
Javadoc –d docDirectory nameOfPackage
如果要多个包生成文档,则运行命令:
Javadoc –d docDirecotry nameOfPackage1 nameOfPackage2…
如果文件放在默认包中,那么运行命令:
Javadoc –d docDirectory *.java
如果省略了-d docDirectory选项,那么会为当前目录提取HTML文件。这么做,显得很混乱,不提倡这种做法。
还可以通过多种命令行选项对javadoc程序进行调整。比如:可以使用-author和-version选项把@author和@version标记加入到文档中(默认情况下,这些标记会被省略)。
相关推荐
Java程序设计基础是编程学习的重要领域,主要涵盖了Java语言的基础概念、语法结构和特性。Java是一种全面面向对象的编程语言,由Sun公司的James Gosling等人于1990年开发,最初命名为Oak,后来因互联网的兴起而更名...
Java程序设计基础是计算机科学领域一个至关重要的课程,主要针对初学者,旨在教授如何使用Java语言进行编程。这个课程涵盖了一系列的关键概念和技术,包括语法、数据类型、控制结构、类与对象、异常处理、集合框架等...
Java程序设计基础是一本旨在帮助初学者快速入门Java编程语言的教材。Java作为一种广泛使用的高级编程语言,具备面向对象、跨平台执行等特性。本书从程序设计语言的发展历程讲起,介绍了从机器语言、汇编语言、高级...
Java程序设计基础教程是入门Java编程的必备指南,它涵盖了从环境配置到基本语法,再到面向对象编程等核心概念。本教程旨在帮助初学者建立起坚实的Java编程基础,并逐步熟悉整个开发流程。 首先,我们从环境配置开始...
【Java程序设计基础教程】是入门Java编程领域的重要学习资料,尤其适合初学者。Java是一种广泛应用的面向对象的编程语言,由Sun Microsystems(现已被Oracle公司收购)在1995年发布。它以其“一次编写,到处运行”的...
Java程序设计基础是计算机科学领域中的重要组成部分,尤其对于初学者而言,它是进入IT行业的敲门砖。"1+X 初级 Java程序设计基础 1-9 章测试题汇总"是一个针对初级Java程序员的全面学习资源,旨在帮助学习者巩固和...
Java程序设计基础是计算机科学领域中的重要课程,主要教授如何使用Java语言进行程序开发。这份教案将深入浅出地引导初学者掌握Java编程的核心概念、语法结构和编程思想。 一、Java简介 Java是由Sun Microsystems...
【Java程序设计基础教案】 Java是一种广泛使用的高级编程语言,尤其以其面向对象的特性而闻名。面向对象编程(OOP)是一种将现实世界中的问题抽象为类和对象的方法,使得程序设计更加模块化和易于维护。Java语言由...
本书是java程序设计的基础篇。是英文版的。适合英语水平较好的java初学者观看。也适合开发人员的参考用书。中文版的可以进一步咨询QQ945745429
Java程序设计基础是计算机科学领域中的重要组成部分,尤其对于初学者而言,它是进入软件开发世界的敲门砖。这份“Java程序设计基础课件超实用”资料,无疑为想要快速掌握Java编程技能的学习者提供了一个宝贵的资源。...
### Java程序设计基础与实验 #### 一、Java概述与编程环境 - **Java Application**:Java Application 是一种独立运行的Java程序,它可以是任何类型的程序,如桌面应用程序或者简单的命令行工具。这类程序通常从`...
主要讲解了 java基本语法(java语言组成,变量和直接变量,数据类型,语句等);面向对象的程序设计(类,对象等);异常处理和常用系统;图形用户界面;...(配合java程序设计基础教程一起学习效果更佳)