java程序语言教案

文档属性

名称 java程序语言教案
格式 zip
文件大小 1.1MB
资源类型 教案
版本资源 通用版
科目 信息技术(信息科技)
更新时间 2010-05-08 17:14:00

文档简介

第十二章 Java数据库连接
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: JDBC是Sun提供的一套数据库编程接口API函数,由Java语言编写的类、界面组成。用JDBC写的程序能够自动地将SQL语句传送给相应的数据库管理系统。不但如此,使用Java编写的应用程序可以在任何支持Java的平台上运行,不必在不同的平台上编写不同的应用。Java和JDBC的结合可以让开发人员在开发数据库应用程序时真正实现“WriteOnce,RunEverywhere!”
下载一
【课前思考】
  1. 什么是JDBC—ODBC桥?
  2. 在Java中如何进行数据库的连接?
  3. 如何进行数据库的查询?
  4. 如何进进行数据库的修改?
【学习目标】
  理解Java中JDBC的概念,掌握如何使用JDBC—ODBC桥在Java程序和数据库之间进行通讯。会使用常用的查询、修改语句。
【学习指南】
  通过理解JDBC模型,以JDK提供的java.sql包为工具,勤加练习,掌握各种基于Java的连接的实现方法。
【难 重 点】
  1. JDBC模型
  2. 数据库的连接
  3. 对数据库的常用操作
【知 识 点】
12.1 慨述
12.1.1 什么是 JDBC?
12.1.2 JDBC 产品
12.2 与数据库的连接对象
12.2.1 打开连接
12.2.2 一般用法的
12.2.3 JDBC URL
12.2.4 "odbc" 子协议
12.2.5 注册子协议
12.2.6 发送
12.2.7 事务
12.2.8 事务隔离级别
12.3 DriverManager 类
12.3.1概述
12.3.2建立连接
12.4 Statement对象
12.4.1创建 Statement 对象
12.4.2使用 Statement 对象执行语句
12.4.3语句完成
12.4.4关闭 Statement 对象
12.4.5 Statement 对象中的 SQL 转义语法
12.4.6使用方法 execute
12.5 ResultSet对象
12.5.1 行和光标
12.5.2 列
12.5.3 数据类型和转换
12.5.4 对非常大的行值使用流
12.5.5 NULL 结果值
12.5.6 可选结果集或多结果集
12.6 PreparedStatement接口
12.6.1创建 PreparedStatement 对象
12.6.2 传递 IN 参数
12.6.3 IN 参数中数据类型的一致性
12.7 CallableStatement 对象
12.7.1 创建 CallableStatement 对象
12.7.2 IN和OUT参数
12.7.3、INOUT参数
12.7.4 先检索结果,再检索 OUT 参数
12.7.5 检索作为OUT参数的NULL值
第十二章 Java数据库连接
12.1 慨述
 JDBC是一种用于执行SQL语句的Java API,由一组用 Java 编程语言编写的类和接口组成。JDBC 为数据库开发人员提供了一组标准的API,使他们能够用纯Java API 来编写数据库应用程序。
12.1.1 什么是 JDBC?
  JDBC 是一种用于执行 SQL 语句的 Java API(有意思的是,JDBC 本身是个商标名而不是一个缩写字;然而,JDBC常被认为是代表 “Java 数据库连接 (Java Database Connectivity)”)。它由一组用 Java 编程语言编写的类和接口组成。JDBC 为工具/数据库开发人员提供了一个标准的 API,使他们能够用纯Java API 来编写数据库应用程序。
  有了 JDBC,向各种关系数据库发送 SQL 语句就是一件很容易的事。换言之,有了JDBC API,就不必为访问 Sybase 数据库专门写一个程序,为访问 Oracle 数据库又专门写一个程序,为访问Informix 数据库又写另一个程序,等等。您只需用 JDBC API 写一个程序就够了,它可向相应数据库发送 SQL 语句。而且,使用 Java 编程语言编写的应用程序,就无须去忧虑要为不同的平台编写不同的应用程序。将 Java 和 JDBC 结合起来将使程序员只须写一遍程序就可让它在任何平台上运行。
  Java 具有坚固、安全、易于使用、易于理解和可从网络上自动下载等特性,是编写数据库应用程序的杰出语言。所需要的只是 Java 应用程序与各种不同数据库之间进行对话的方法。而 JDBC 正是作为此种用途的机制。
  JDBC 扩展了 Java 的功能。例如,用 Java 和 JDBC API 可以发布含有 applet的网页,而该 applet 使用的信息可能来自远程数据库。企业也可以用 JDBC 通过Intranet 将所有职员连到一个或多个内部数据库中(即使这些职员所用的计算机有 Windows、 Macintosh 和 UNIX 等各种不同的操作系统)。随着越来越多的程序员开始使用 Java 编程语言,对从 Java中便捷地访问数据库的要求也在日益增加。
  MIS 管理员们都喜欢 Java 和 JDBC 的结合,因为它使信息传播变得容易和经济。企业可继续使用它们安装好的数据库,并能便捷地存取信息,即使这些信息是储存在不同数据库管理系统上。新程序的开发期很短。安装和版本控制将大为简化。程序员可只编写一遍应用程序或只更新一次,然后将它放到服务器上,随后任何人就都可得到最新版本的应用程序。对于商务上的销售信息服务, Java 和 JDBC 可为外部客户提供获取信息更新的更好方法。
  1 JDBC 的用途是什么?
  简单地说,JDBC 可做三件事:
  与数据库建立连接,发送 SQL 语句,处理结果。
  下列代码段给出了以上三步的基本示例:
Connection con = DriverManager.getConnection ("jdbc:odbc:wombat", "login", "password");Statement stmt = con.createStatement();ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM Table1");while (rs.next())System.out.println(rs.getString("a") + " " + rs.getString("b") + " " + rs.getString("c"));
  .2 JDBC 是一种低级 API ,是高级 API 的基础
  JDBC 是个“低级”接口,也就是说,它用于直接调用 SQL 命令。在这方面它的功能极佳,并比其它的数据库连接 API 易于使用,但它同时也被设计为一种基础接口,在它之上可以建立高级接口和工具。
  高级接口是“对用户友好的”接口,它使用的是一种更易理解和更为方便的 API,这种 API 在幕后被转换为诸如 JDBC 这样的低级接口。在编写本文时,正在开发两种基于 JDBC 的高级 API:
  一种用于 Java 的嵌入式 SQL。至少已经有一个提供者计划编写它。DBMS 实现SQL:一种专门设计来与数据库联合使用的语言。JDBC 要求 SQL 语句必须作为 String 传给 Java 方法。相反,嵌入式 SQL预处理器允许程序员将 SQL 语句直接与Java 混在一起使用。例如,可在 SQL 语句中使用 Java 变量,用以接受或提供SQL 值。然后,嵌入式 SQL 预处理器将通过 JDBC 调用把这种 Java/SQL 的混合物转换为Java。关系数据库表到 Java 类的直接映射。JavaSoft 和其它提供者都声称要实现该API。在这种“对象/关系”映射中,表中的每行对应于类的一个实例,而每列的值对应于该实例的一个属性。于是,程序员可直接对 Java 对象进行操作;存取数据所需的 SQL 调用将在“掩盖下”自动生成。此外还可提供更复杂的映射,例如将多个表中的行结合进一个 Java 类中。
  随着人们对 JDBC 的兴趣日益增涨,越来越多的开发人员一直在使用基于 JDBC 的工具,以使程序的编写更加容易。程序员也一直在编写力图使最终用户对数据库的访问变得更为简单的应用程序。例如,应用程序可提供一个选择数据库任务的菜单。任务被选定后,应用程序将给出提示及空白供填写执行选定任务所需的信息。所需信息输入后,应用程序将自动调用所需的SQL 命令。在这样一种程序的协助下,即使用户根本不懂 SQL 的语法,也可以执行数据库任务。
.3 JDBC 与 ODBC 和其它 API 的比较
  目前,Microsoft 的 ODBC(开放式数据库连接)API 可能是使用最广的、用于访问关系数据库的编程接口。它能在几乎所有平台上连接几乎所有的数据库。为什么Java 不使用 ODBC?
  对这个问题的回答是:Java 可以使用 ODBC,但最好是在 JDBC 的帮助下以JDBC-ODBC 桥的形式使用,这一点我们稍后再说。现在的问题已变成:“为什么需要 JDBC”? 回答如下:ODBC 不适合直接在 Java 中使用,因为它使用 C 语言接口。从 Java 调用本地 C 代码在安全性、实现、坚固性和程序的自动移植性方面都有许多缺点。
  从 ODBC API 到 Java API 的字面翻译是不可取的。例如,Java 没有指针,而 ODBC 却对指针用得很广泛(包括很容易出错的指针 "void *")。您可以将 JDBC 想象成被转换为面向对象接口的 ODBC,而面向对象的接口对 Java 程序员来说较易于接收。ODBC 很难学。它把简单和高级功能混在一起,而且即使对于简单的查询,其选项也极为复杂。相反,JDBC 尽量保证简单功能的简便性,而同时在必要时允许使用高级功能。启用“纯 Java ”机制需要象 JDBC 这样的 Java API。如果使用 ODBC,就必须手动地将 ODBC 驱动程序管理器和驱动程序安装在每台客户机上。如果完全用 Java 编写 JDBC 驱动程序则 JDBC 代码在所有 Java 平台上(从网络计算机到大型机)都可以自动安装、移植并保证安全性。
  总之,JDBC API 对于基本的 SQL 抽象和概念是一种自然的 Java 接口。它建立在 ODBC 上而不是从零开始。因此,熟悉ODBC 的程序员将发现 JDBC 很容易使用。JDBC 保留了 ODBC 的基本设计特征;事实上,两种接口都基于 X/Open SQL CLI(调用级接口)。它们之间最大的区别在于:JDBC 以 Java 风格与优点为基础并进行优化,因此更加易于使用。
  最近,Microsoft 又引进了 ODBC 之外的新 API: RDO、 ADO 和 OLE DB。这些设计在许多方面与 JDBC 是相同的,即它们都是面向对象的数据库接口且基于可在ODBC 上实现的类。但在这些接口中,我们未看见有特别的功能使我们要转而选择它们来替代 ODBC,尤其是在 ODBC 驱动程序已建立起较为完善的市场的情况下。它们最多也就是在 ODBC 上加了一种装饰而已。这并不是说 JDBC 不需要从其最初的版本再发展了;然而,我们觉得大部份的新功能应归入诸如前一节中所述的对象/关系映射和嵌入式 SQL 这样的高级 API。
  .4 两层模型和三层模型
  JDBC API 既支持数据库访问的两层模型,同时也支持三层模型。
  在两层模型中,Java applet 或应用程序将直接与数据库进行对话。这将需要一个 JDBC 驱动程序来与所访问的特定数据库管理系统进行通讯。用户的 SQL 语句被送往数据库中,而其结果将被送回给用户。数据库可以位于另一台计算机上,用户通过网络连接到上面。这就叫做客户机/服务器配置,其中用户的计算机为客户机,提供数据库的计算机为服务器。网络可以是 Intranet(它可将公司职员连接起来),也可以是 Internet。
  在三层模型中,命令先是被发送到服务的“中间层”,然后由它将 SQL 语句发送给数据库。数据库对 SQL 语句进行处理并将结果送回到中间层,中间层再将结果送回给用户。MIS 主管们都发现三层模型很吸引人,因为可用中间层来控制对公司数据的访问和可作的的更新的种类。中间层的另一个好处是,用户可以利用易于使用的高级API,而中间层将把它转换为相应的低级调用。最后,许多情况下三层结构可提供一些性能上的好处。
  到目前为止,中间层通常都用 C 或 C++ 这类语言来编写,这些语言执行速度较快。然而,随着最优化编译器(它把 Java字节代码转换为高效的特定于机器的代码)的引入,用 Java 来实现中间层将变得越来越实际。这将是一个很大的进步,它使人们可以充分利用 Java 的诸多优点(如坚固、多线程和安全等特征)。JDBC对于从 Java 的中间层来访问数据库非常重要。
  .5 SQL 的一致性
  结构化查询语言 (SQL) 是访问关系数据库的标准语言。困难之处在于:虽然大多数的 DBMS (数据库管理系统)对其基本功能都使用了标准形式的 SQL,但它们却不符合最近为更高级的功能定义的标准SQL 语法或语义。例如,并非所有的数据库都支持储存程序或外部连接,那些支持这一功能的数据库又相互不一致。人们希望 SQL 中真正标准的那部份能够进行扩展以包括越来越多的功能。但同时 JDBC API 又必须支持现有的 SQL。
  JDBC API 解决这个问题的一种方法是允许将任何查询字符串一直传到所涉及的DBMS 驱动程序上。这意味着应用程序可以使用任意多的 SQL 功能,但它必须冒这样的风险:有可能在某些 DBMS 上出错。事实上,应用程序查询甚至不一定要是SQL,或者说它可以是个为特定的 DBMS 设计的 SQL 的专用派生物(例如,文档或图象查)。
  JDBC 处理 SQL 一致性问题的第二种方法是提供 ODBC 风格的转义子句。这将在4.1.5 节“语句对象中的 SQL 转义语法”中讨论。
  转义语法为几个常见的 SQL 分歧提供了一种标准的 JDBC 语法。例如,对日期文字和已储存过程的调用都有转义语法。
  对于复杂的应用程序,JDBC 用第三种方法来处理 SQL 的一致性问题。它利用DatabaseMetaData 接口来提供关于 DBMS 的描述性信息,从而使应用程序能适应每个 DBMS 的要求和功能。
  由于 JDBC API 将用作开发高级数据库访问工具和 API 的基础 API,因此它还必须注意其所有上层建筑的一致性。“符合JDBC 标准TM" 代表用户可依赖的 JDBC 功能的标准级别。要使用这一说明,驱动程序至少必须支持 ANSI SQL-2 EntryLevel(ANSI SQL-2 代表美国国家标准局 1992 年所采用的标准。Entry Level 代表SQL 功能的特定清单)。驱动程序开发人员可用 JDBC API 所带的测试工具包来确定他们的驱动程序是否符合这些标准。
  “符合 JDBC 标准TM” 表示提供者的 JDBC 实现已经通过了 JavaSoft 提供的一致性测试。这些一致性测试将检查 JDBCAPI 中定义的所有类和方法是否都存在,并尽可能地检查程序是否具有 SQL Entry Level 功能。当然,这些测试并不完全,而且 JavaSoft 目前也无意对各提供者的实现进行标级。但这种一致性定义的确可对JDBC 实现提供一定的可信度。随着越来越多的数据库提供者、连接提供者、Internet 提供者和应用程序编程员对 JDBC API 的接受,JDBC 也正迅速成为 Java数据库访问的标准。
12.1.2 JDBC 产品
  在编写本文时,有几个基于 JDBC 的产品已开发完毕或正在开发中。当然,本节中的信息将很快成为过时信息。因此,有关最新的信息,请查阅 JDBC 的网站,可通过从以下 URL 开始浏览找到:
http://java.sun.com/products/jdbc
  1 JavaSoft 框架
  JavaSoft 提供三种 JDBC 产品组件,它们是 Java 开发工具包 (JDK) 的组成部份:JDBC 驱动程序管理器,JDBC 驱动程序测试工具包,和JDBC-ODBC 桥。
  JDBC 驱动程序管理器是 JDBC 体系结构的支柱。它实际上很小,也很简单;其主要作用是把 Java 应用程序连接到正确的JDBC 驱动程序上,然后即退出。
  JDBC 驱动程序测试工具包为使 JDBC 驱动程序运行您的程序提供一定的可信度。只有通过 JDBC 驱动程序测试包的驱动程序才被认为是符合 JDBC 标准TM 的。
  JDBC-ODBC 桥使 ODBC 驱动程序可被用作 JDBC 驱动程序。它的实现为 JDBC 的快速发展提供了一条途径,其长远目标提供一种访问某些不常见的 DBMS(如果对这些不常见的 DBMS 未实现 JDBC) 的方法。
  .2 JDBC 驱动程序的类型
  我们目前所知晓的 JDBC 驱动程序可分为以下四个种类:
  JDBC-ODBC 桥加 ODBC 驱动程序:JavaSoft 桥产品利用 ODBC 驱动程序提供 JDBC 访问。注意,必须将 ODBC 二进制代码(许多情况下还包括数据库客户机代码)加载到使用该驱动程序的每个客户机上。因此,这种类型的驱动程序最适合于企业网(这种网络上客户机的安装不是主要问题),或者是用 Java 编写的三层结构的应用程序服务器代码。
  本地 API - 部份用 Java 来编写的驱动程序: 这种类型的驱动程序把客户机 API 上的 JDBC 调用转换为 Oracle、Sybase、Informix、DB2 或其它 DBMS 的调用。注意,象桥驱动程序一样,这种类型的驱动程序要求将某些二进制代码加载到每台客户。
  JDBC 网络纯 Java 驱动程序:这种驱动程序将 JDBC 转换为与 DBMS 无关的网络协议,之后这种协议又被某个服务器转换为一种 DBMS 协议。这种网络服务器中间件能够将它的纯 Java 客户机连接到多种不同的数据库上。所用的具体协议取决于提供者。通常,这是最为灵活的 JDBC 驱动程序。有可能所有这种解决方案的提供者都提供适合于 Intranet 用的产品。为了使这些产品也支持 Internet 访问,它们必须处理 Web 所提出的安全性、通过防火墙的访问等方面的额外要求。几家提供者正将 JDBC 驱动程序加到他们现有的数据库中间件产品中。
  本地协议纯 Java 驱动程序:这种类型的驱动程序将 JDBC 调用直接转换为DBMS 所使用的网络协议。这将允许从客户机机器上直接调用 DBMS 服务器,是 Intranet 访问的一个很实用的解决方法。由于许多这样的协议都是专用的,因此数据库提供者自己将是主要来源,有几家提供者已在着手做这件事了。最后,我们预计第 3、4 类驱动程序将成为从 JDBC 访问数据库的首选方法。第1、2 类驱动程序在直接的纯 Java 驱动程序还没有上市前将会作为过渡方案来使用。对第 1、2 类驱动程序可能会有一些变种,这些变种要求有连接器,但通常这些是更加不可取的解决方案。第 3、4 类驱动程序提供了 Java 的所有优点,包括自动安装(例如,通过使用 JDBC 驱动程序的 applet applet来下载该驱动程序)。
12.2 与数据库的连接对象
Connection 对象代表与数据库的连接。连接过程包括所执行的 SQL 语句和在该连接上所返回的结果。一个应用程序可与单个数据库有一个或多个连接,或者可与许多数据库有连接。
Connection 对象代表与数据库的连接。连接过程包括所执行的 SQL 语句和在该连接上所返回的结果。一个应用程序可与单个数据库有一个或多个连接,或者可与许多数据库有连接。
12.2.1 打开连接
与数据库建立连接的标准方法是调用DriverManager.getConnection方法。该方法接受含有某个 URL 的字符串。DriverManager 类(即所谓的 JDBC管理层)将尝试找到可与那个 URL 所代表的数据库进行连接的驱动程序。DriverManager 类存有已注册的 Driver 类的清单。当调用方法 getConnection 时,它将检查清单中的每个驱动程序,直到找到可与URL 中指定的数据库进行连接的驱动程序为止。Driver 的方法connect 使用这个 URL来建立实际的连接。
  用户可绕过 JDBC 管理层直接调用 Driver 方法。这在以下特殊情况下将很有用:当两个驱动器可同时连接到数据库中,而用户需要明确地选用其中特定的驱动器。但一般情况下,让 DriverManager 类处理打开连接这种事将更为简单。
  下述代码显示如何打开一个与位于 URL "jdbc:odbc:wombat" 的数据库的连接。所用的用户标识符为 "oboy" ,口令为 "12Java":
String url = "jdbc:odbc:wombat";Connection con = DriverManager.getConnection(url, "oboy", "12Java");
12.2.2 一般用法的
URL由于 URL 常引起混淆,我们将先对一般 URL 作简单说明,然后再讨论 JDBC URL。
  URL(统一资源定位符)提供在 Internet 上定位资源所需的信息。可将它想象为一个地址。URL 的第一部份指定了访问信息所用的协议,后面总是跟着冒号。常用的协议有"ftp"(代表“文件传输协议”)和 "http" (代表“超文本传输协议”)。如果协议是 "file",表示资源是在某个本地文件系统上而非在 Internet 上(下例用于表示我们所描述的部分;它并非 URL 的组成部分)。
ftp:///docs/JDK-1_apidocs.ziphttp://java.sun.com/products/jdk/CurrentReleasefile:/home/haroldw/docs/books/tutorial/summary.html
  URL 的其余部份(冒号后面的)给出了数据资源所处位置的有关信息。如果协议是 file,则 URL 的其余部份是文件的路径。对于 ftp 和http 协议,URL 的其余部份标识了主机并可选地给出某个更详尽的地址路径。例如,以下是 JavaSoft 主页的URL。该 URL 只标识了主机:
  http://java.sun.com从该主页开始浏览,就可以进到许多其它的网页中,其中之一就是JDBC 主页。JDBC 主页的 URL 更为具体,它看起来类似: http://java.sun.com/products/jdbc
12.2.3 JDBC URL
  JDBC URL 提供了一种标识数据库的方法,可以使相应的驱动程序能识别该数据库并与之建立连接。实际上,驱动程序编程员将决定用什么 JDBC URL 来标识特定的驱动程序。用户不必关心如何来形成JDBC URL;他们只须使用与所用的驱动程序一起提供的 URL 即可。JDBC 的作用是提供某些约定,驱动程序编程员在构造他们的 JDBC URL 时应该遵循这些约定。
  由于 JDBC URL 要与各种不同的驱动程序一起使用,因此这些约定应非常灵活。首先,它们应允许不同的驱动程序使用不同的方案来命名数据库。例如, odbc 子协议允许(但并不是要求) URL 含有属性值。第二,JDBC URL 应允许驱动程序编程员将一切所需的信息编入其中。这样就可以让要与给定数据库对话的 applet 打开数据库连接,而无须要求用户去做任何系统管理工作。第三, JDBC URL 应允许某种程度的间接性。也就是说,JDBC URL 可指向逻辑主机或数据库名,而这种逻辑主机或数据库名将由网络命名系统动态地转换为实际的名称。这可以使系统管理员不必将特定主机声明为JDBC 名称的一部份。网络命名服务(例如 DNS、 NIS 和DCE )有多种,而对于使用哪种命名服务并无限制。JDBC URL 的标准语法如下所示。它由三部分组成,各部分间用冒号分隔:
jdbc:< 子协议 >:< 子名称 >
  JDBC URL 的三个部分可分解如下: jdbc ─ 协议。
  JDBC URL 中的协议总是 jdbc。
  <子协议> ─ 驱动程序名或数据库连接机制(这种机制可由一个或多个驱动程序支持)的名称。子协议名的典型示例是 "odbc",该名称是为用于指定 ODBC 风格的数据资源名称的 URL 专门保留的。例如,为了通过JDBC-ODBC 桥来访问某个数据库,可以用如下所示的 URL:
jdbc:odbc:fred
  本例中,子协议为 "odbc",子名称 "fred" 是本地ODBC 数据资源。
  如果要用网络命名服务(这样 JDBC URL 中的数据库名称不必是实际名称),则命名服务可以作为子协议。例如,可用如下所示的 URL :jdbc:dcenaming:accounts-payable本例中,该 URL 指定了本地 DCE 命名服务应该将数据库名称 "accounts-payable" 解析为更为具体的可用于连接真实数据库的名称。<子名称> ─ 一种标识数据库的方法。子名称可以依不同的子协议而变化。它还可以有子名称的子名称(含有驱动程序编程员所选的任何内部语法)。使用子名称的目的是为定位数据库提供足够的信息。前例中,因为 ODBC 将提供其余部份的信息,因此用 "fred" 就已足够。然而,位于远程服务器上的数据库需要更多的信息。例如,如果数据库是通过Internet 来访问的,则在 JDBC URL 中应将网络地址作为子名称的一部份包括进去,且必须遵循如下所示的标准 URL 命名约定://主机名:端口/子协议假设 "dbnet" 是个用于将某个主机连接到 Internet 上的协议,则 JDBC URL 类似:
jdbc:dbnet://wombat:356/fred
12.2.4 "odbc" 子协议
子协议 odbc 是一种特殊情况。它是为用于指定 ODBC 风格的数据资源名称的 URL 而保留的,并具有下列特性:允许在子名称(数据资源名称)后面指定任意多个属性值。odbc 子协议的完整语法为: jdbc:odbc:< 数据资源名称 >[;< 属性名 >=< 属性值 >]*
  因此,以下都是合法的 jdbc:odbc 名称:
jdbc:odbc:qeor7jdbc:odbc:wombatjdbc:odbc:wombat;CacheSize=20;ExtensionCase=LOWERjdbc:odbc:qeora;UID=kgh;PWD=fooey
12.2.5 注册子协议
驱动程序编程员可保留某个名称以将之用作 JDBC URL 的子协议名。
  当 DriverManager 类将此名称加到已注册的驱动程序清单中时,为之保留该名称的驱动程序应能识别该名称并与它所标识的数据库建立连接。例如,odbc 是为 JDBC- ODBC 桥而保留的。
  示例之二,假设有个 Miracle 公司,它可能会将 "miracle" 注册为连接到其Miracle DBMS 上的JDBC 驱动程序的子协议,从而使其他人都无法使用这个名称。JavaSoft 目前作为非正式代理负责注册 JDBC 子协议名称。要注册某个子协议名称,请发送电子邮件到下述地址:
jdbc@wombat.eng.sun.com
12.2.6 发送
  SQL 语句连接一旦建立,就可用来向它所涉及的数据库传送 SQL 语句。JDBC对可被发送的 SQL 语句类型不加任何限制。这就提供了很大的灵活性,即允许使用特定的数据库语句或甚至于非 SQL 语句。然而,它要求用户自己负责确保所涉及的数据库可以处理所发送的 SQL 语句,否则将自食其果。例如,如果某个应用程序试图向不支持储存程序的DBMS 发送储存程序调用,就会失败并将抛出异常。JDBC 要求驱动程序应至少能提供 ANSI SQL-2 Entry Level 功能才可算是符合 JDBC标准TM 的。这意味着用户至少可信赖这一标准级别的功能。JDBC 提供了三个类,用于向数据库发送 SQL 语句。Connection 接口中的三个方法可用于创建这些类的实例。下面列出这些类及其创建方法:
  Statement ─ 由方法 createStatement 所创建。Statement 对象用于发送简单的SQL 语句。
  PreparedStatement ─ 由方法 prepareStatement 所创建。
  PreparedStatement 对象用于发送带有一个或多个输入参数( IN 参数)的 SQL 语句。PreparedStatement 拥有一组方法,用于设置 IN 参数的值。
  执行语句时,这些 IN 参数将被送到数据库中。PreparedStatement 的实例扩展了 Statement ,因此它们都包括了 Statement 的方法。
  PreparedStatement 对象有可能比 Statement 对象的效率更高,因为它已被预编译过并存放在那以供将来使用。
  CallableStatement ─ 由方法 prepareCall 所创建。CallableStatement 对象用于执行 SQL 储存程序 ─ 一组可通过名称来调用(就象函数的调用那样)的SQL 语句。CallableStatement 对象从 PreparedStatement 中继承了用于处理 IN 参数的方法,而且还增加了用于处理 OUT 参数和 INOUT 参数的方法。
  以下所列提供的方法可以快速决定应用哪个 Connection 方法来创建不同类型的SQL 语句:
createStatement 方法用于:简单的 SQL 语句(不带参数) prepareStatement 方法用于: 带一个或多个IN 参数的 SQL 语句 经常被执行的简单 SQL 语句prepareCall 方法用于: 调用已储存过程
12.2.7 事务
事务由一个或多个这样的语句组成:这些语句已被执行、完成并被提交或还原。当调用方法 commit 或 rollback 时,当前事务即告就结束,另一个事务随即开始。
  缺省情况下,新连接将处于自动提交模式。也就是说,当执行完语句后,将自动对那个语句调用 commit 方法。这种情况下,由于每个语句都是被单独提交的,因此一个事务只由一个语句组成。如果禁用自动提交模式,事务将要等到 commit 或rollback 方法被显式调用时才结束,因此它将包括上一次调用 commit 或rollback 方法以来所有执行过的语句。对于第二种情况,事务中的所有语句将作为组来提交或还原。
  方法 commit 使 SQL 语句对数据库所做的任何更改成为永久性的,它还将释放事务持有的全部锁。而方法 rollback 将弃去那些更改。
  有时用户在另一个更改生效前不想让此更改生效。这可通过禁用自动提交并将两个更新组合在一个事务中来达到。如果两个更新都是成功,则调用 commit 方法,从而使两个更新结果成为永久性的;如果其中之一或两个更新都失败了,则调用 rollback 方法,以将值恢复为进行更新之前的值。
  大多数 JDBC 驱动程序都支持事务。事实上,符合 JDBC 的驱动程序必须支持事务。DatabaseMetaData 给出的信息描述 DBMS 所提供的事务支持水平。
12.2.8 事务隔离级别
  如果 DBMS 支持事务处理,它必须有某种途径来管理两个事务同时对一个数据库进行操作时可能发生的冲突。用户可指定事务隔离级别,以指明DBMS 应该花多大精力来解决潜在冲突。例如,当事务更改了某个值而第二个事务却在该更改被提交或还原前读取该值时该怎么办 假设第一个事务被还原后,第二个事务所读取的更改值将是无效的,那么是否可允许这种冲突? JDBC 用户可用以下代码来指示 DBMS 允许在值被提交前读取该值(“dirty 读取”),其中 con 是当前连接:
con.setTransactionIsolation(TRANSACTION_READ_UNCOMMITTED);
  事务隔离级别越高,为避免冲突所花的精力也就越多。Connection 接口定义了五级,其中最低级别指定了根本就不支持事务,而最高级别则指定当事务在对某个数据库进行操作时,任何其它事务不得对那个事务正在读取的数据进行任何更改。通常,隔离级别越高,应用程序执行的速度也就越慢(由于用于锁定的资源耗费增加了,而用户间的并发操作减少了)。在决定采用什么隔离级别时,开发人员必须在性能需求和数据一致性需求之间进行权衡。当然,实际所能支持的级别取决于所涉及的 DBMS 的功能。
  当创建 Connection 对象时,其事务隔离级别取决于驱动程序,但通常是所涉及的数据库的缺省值。用户可通过调用 setIsolationLevel方法来更改事务隔离级别。新的级别将在该连接过程的剩余时间内生效。要想只改变一个事务的事务隔离级别,必须在该事务开始前进行设置,并在该事务结束后进行复位。我们不提倡在事务的中途对事务隔离级别进行更改,因为这将立即触发 commit 方法的调用,使在此之前所作的任何更改变成永久性的。
12.3 DriverManager 类
DriverManager 类是 JDBC 的管理层,作用于用户和驱动程序之间。它跟踪可用的驱动程序,并在数据库和相应驱动程序之间建立连接。
12.3.1概述
  DriverManager 类是 JDBC 的管理层,作用于用户和驱动程序之间。它跟踪可用的驱动程序,并在数据库和相应驱动程序之间建立连接。另外,DriverManager 类也处理诸如驱动程序登录时间限制及登录和跟踪消息的显示等事务。
  对于简单的应用程序,一般程序员需要在此类中直接使用的唯一方法是 DriverManager.getConnection。正如名称所示,该方法将建立与数据库的连接。JDBC 允许用户调用 DriverManager 的方法 getDriver、getDrivers 和 registerDriver 及 Driver 的方法 connect。但多数情况下,让 DriverManager 类管理建立连接的细节为上策。
  1、跟踪可用驱动程序
  DriverManager 类包含一列 Driver 类,它们已通过调用方法 DriverManager.registerDriver 对自己进行了注册。所有 Driver 类都必须包含有一个静态部分。它创建该类的实例,然后在加载该实例时 DriverManager 类进行注册。这样,用户正常情况下将不会直接调用 DriverManager.registerDriver;而是在加载驱动程序时由驱动程序自动调用。加载 Driver 类,然后自动在 DriverManager 中注册的方式有两种:
  通过调用方法 Class.forName。这将显式地加载驱动程序类。由于这与外部设置无关,因此推荐使用这种加载驱动程序的方法。以下代码加载类 acme.db.Driver:
Class.forName("acme.db.Driver");
  如果将 acme.db.Driver 编写为加载时创建实例,并调用以该实例为参数的 DriverManager.registerDriver(本该如此),则它在 DriverManager 的驱动程序列表中,并可用于创建连接。
  通过将驱动程序添加到 java.lang.System 的属性 jdbc.drivers 中。这是一个由 DriverManager 类加载的驱动程序类名的列表,由冒号分隔:初始化 DriverManager 类时,它搜索系统属性 jdbc.drivers,如果用户已输入了一个或多个驱动程序,则 DriverManager 类将试图加载它们。以下代码说明程序员如何在 ~/.hotjava/properties 中输入三个驱动程序类(启动时,HotJava 将把它加载到系统属性列表中):
jdbc.drivers=foo.bah.Driver:wombat.sql.Driver:bad.test.ourDriver;
  对 DriverManager 方法的第一次调用将自动加载这些驱动程序类。
  注意:加载驱动程序的第二种方法需要持久的预设环境。如果对这一点不能保证,则调用方法 Class.forName 显式地加载每个驱动程序就显得更为安全。这也是引入特定驱动程序的方法,因为一旦 DriverManager 类被初始化,它将不再检查 jdbc.drivers 属性列表。
  在以上两种情况中,新加载的 Driver 类都要通过调用 DriverManager.registerDriver 类进行自我注册。如上所述,加载类时将自动执行这一过程。
  由于安全方面的原因,JDBC 管理层将跟踪哪个类加载器提供哪个驱动程序。这样,当 DriverManager 类打开连接时,它仅使用本地文件系统或与发出连接请求的代码相同的类加载器提供的驱动程序。
12.3.2建立连接
  加载 Driver 类并在 DriverManager 类中注册后,它们即可用来与数据库建立连接。当调用 DriverManager.getConnection 方法发出连接请求时,DriverManager 将检查每个驱动程序,查看它是否可以建立连接。
  有时可能有多个 JDBC 驱动程序可以与给定的 URL 连接。例如,与给定远程数据库连接时,可以使用 JDBC-ODBC 桥驱动程序、JDBC 到通用网络协议驱动程序或数据库厂商提供的驱动程序。在这种情况下,测试驱动程序的顺序至关重要,因为 DriverManager 将使用它所找到的第一个可以成功连接到给定 URL 的驱动程序。
  首先 DriverManager 试图按注册的顺序使用每个驱动程序(jdbc.drivers 中列出的驱动程序总是先注册)。它将跳过代码不可信任的驱动程序,除非加载它们的源与试图打开连接的代码的源相同。
  它通过轮流在每个驱动程序上调用方法 Driver.connect,并向它们传递用户开始传递给方法 DriverManager.getConnection 的 URL 来对驱动程序进行测试,然后连接第一个认出该 URL 的驱动程序。
  这种方法初看起来效率不高,但由于不可能同时加载数十个驱动程序,因此每次连接实际只需几个过程调用和字符串比较。
  以下代码是通常情况下用驱动程序(例如 JDBC-ODBC 桥驱动程序)建立连接所需所有步骤的示例:
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); //加载驱动程序 String url = "jdbc:odbc:fred"; DriverManager.getConnection(url, "userID", "passwd");
12.4 Statement对象
Statement 对象用于将 SQL 语句发送到数据库中。实际上有三种 Statement 对象,它们都作为在给定连接上执行 SQL 语句的包容器:Statement、PreparedStatement和 CallableStatement。
  Statement 对象用于将 SQL 语句发送到数据库中。实际上有三种 Statement 对象,它们都作为在给定连接上执行 SQL 语句的包容器:Statement、PreparedStatement(它从 Statement 继承而来)和 CallableStatement(它从 PreparedStatement 继承而来)。它们都专用于发送特定类型的 SQL 语句: Statement 对象用于执行不带参数的简单 SQL 语句;PreparedStatement 对象用于执行带或不带 IN 参数的预编译 SQL 语句;CallableStatement 对象用于执行对数据库已存储过程的调用。
Statement 接口提供了执行语句和获取结果的基本方法。PreparedStatement 接口添加了处理 IN 参数的方法;而 CallableStatement 添加了处理 OUT 参数的方法。
12.4.1创建 Statement 对象
  建立了到特定数据库的连接之后,就可用该连接发送 SQL 语句。Statement 对象用 Connection 的方法 createStatement 创建,如下列代码段中所示:
Connection con = DriverManager.getConnection(url, "sunny", "");
Statement stmt = con.createStatement();
  为了执行 Statement 对象,被发送到数据库的 SQL 语句将被作为参数提供给 Statement 的方法:
ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM Table2");
12.4.2使用 Statement 对象执行语句
Statement 接口提供了三种执行 SQL 语句的方法:executeQuery、executeUpdate 和 execute。使用哪一个方法由 SQL 语句所产生的内容决定。
方法 executeQuery 用于产生单个结果集的语句,例如 SELECT 语句。
方法 executeUpdate 用于执行 INSERT、UPDATE 或 DELETE 语句以及 SQL DDL(数据定义语言)语句,例如 CREATE TABLE 和 DROP TABLE。INSERT、UPDATE 或 DELETE 语句的效果是修改表中零行或多行中的一列或多列。executeUpdate 的返回值是一个整数,指示受影响的行数(即更新计数)。对于 CREATE TABLE 或 DROP TABLE 等不操作行的语句,executeUpdate 的返回值总为零。
方法 execute 用于执行返回多个结果集、多个更新计数或二者组合的语句。因为多数程序员不会需要该高级功能,所以本概述后面将在单独一节中对其进行介绍。
执行语句的所有方法都将关闭所调用的 Statement 对象的当前打开结果集(如果存在)。这意味着在重新执行 Statement 对象之前,需要完成对当前 ResultSet 对象的处理。
应注意,继承了 Statement 接口中所有方法的 PreparedStatement 接口都有自己的 executeQuery、executeUpdate 和 execute 方法。Statement 对象本身不包含 SQL 语句,因而必须给 Statement.execute 方法提供 SQL 语句作为参数。PreparedStatement 对象并不将 SQL 语句作为参数提供给这些方法,因为它们已经包含预编译 SQL 语句。CallableStatement 对象继承这些方法的 PreparedStatement 形式。对于这些方法的 PreparedStatement 或 CallableStatement 版本,使用查询参数将抛出 SQLException。
12.4.3语句完成
当连接处于自动提交模式时,其中所执行的语句在完成时将自动提交或还原。语句在已执行且所有结果返回时,即认为已完成。对于返回一个结果集的 executeQuery 方法,在检索完 ResultSet 对象的所有行时该语句完成。对于方法 executeUpdate,当它执行时语句即完成。但在少数调用方法 execute 的情况中,在检索所有结果集或它生成的更新计数之后语句才完成。
有些 DBMS 将已存储过程中的每条语句视为独立的语句;而另外一些则将整个过程视为一个复合语句。在启用自动提交时,这种差别就变得非常重要,因为它影响什么时候调用 commit 方法。在前一种情况中,每条语句单独提交;在后一种情况中,所有语句同时提交。
12.4.4关闭 Statement 对象
Statement 对象将由 Java 垃圾收集程序自动关闭。而作为一种好的编程风格,应在不需要 Statement 对象时显式地关闭它们。这将立即释放 DBMS 资源,有助于避免潜在的内存问题。
12.4.5 Statement 对象中的 SQL 转义语法
  Statement 可包含使用 SQL 转义语法的 SQL 语句。转义语法告诉驱动程序其中的代码应该以不同方式处理。驱动程序将扫描任何转义语法,并将它转换成特定数据库可理解的代码。这使得转义语法与 DBMS 无关,并允许程序员使用在没有转义语法时不可用的功能。
  转义子句由花括号和关键字界定:
{keyword . . . parameters . . . }
  该关键字指示转义子句的类型,如下所示。
  escape 表示 LIKE 转义字符
  字符“%”和“_”类似于 SQL LIKE 子句中的通配符(“%”匹配零个或多个字符,而“_”则匹配一个字符)。为了正确解释它们,应在其前面加上反斜杠(“\”),它是字符串中的特殊转义字符。在查询末尾包括如下语法即可指定用作转义字符的字符:
{escape 'escape-character'}
  例如,下列查询使用反斜杠字符作为转义字符,查找以下划线开头的标识符名:
stmt.executeQuery("SELECT name FROM Identifiers
WHERE Id LIKE `\_%' {escape `\'};
  fn 表示标量函数
  几乎所有 DBMS 都具有标量值的数值、字符串、时间、日期、系统和转换函数。要使用这些函数,可使用如下转义语法:关键字 fn 后跟所需的函数名及其参数。例如,下列代码调用函数 concat 将两个参数连接在一起:
{fn concat("Hot", "Java")};
  可用下列语法获得当前数据库用户名:
{fn user()};
  标量函数可能由语法稍有不同的 DBMS 支持,而它们可能不被所有驱动程序支持。各种 DatabaseMetaData 方法将列出所支持的函数。例如,方法 getNumericFunctions 返回用逗号分隔的数值函数列表,而方法 getStringFunctions 将返回字符串函数,等等。
  驱动程序将转义函数调用映射为相应的语法,或直接实现该函数。
  d、t 和 ts 表示日期和时间文字
  DBMS 用于日期、时间和时间标记文字的语法各不相同。JDBC 使用转义子句支持这些文字的语法的 ISO 标准格式。驱动程序必须将转义子句转换成 DBMS 表示。
  例如,可用下列语法在 JDBC SQL 语句中指定日期:
{d `yyyy-mm-dd'}
  在该语法中,yyyy 为年代,mm 为月份,而 dd 则为日期。驱动程序将用等价的特定于 DBMS 的表示替换这个转义子句。例如,如果 '28- FEB-99' 符合基本数据库的格式,则驱动程序将用它替换 {d 1999-02-28}。
  对于 TIME 和 TIMESTAMP 也有类似的转义子句:
{t `hh:mm:ss'}
{ts `yyyy-mm-dd hh:mm:ss.f . . .'}
  TIMESTAMP 中的小数点后的秒(.f . . .)部分可忽略。
  call 或 = call 表示已存储过程
  如果数据库支持已存储过程,则可从 JDBC 中调用它们,语法为:
{call procedure_name[( , , . . .)]}
  或(其中过程返回结果参数):
{ = call procedure_name[( , , . . .)]}
  方括号指示其中的内容是可选的。它们不是语法的必要部分。
  输入参数可以为文字或参数。有关详细信息,参见 JDBC 指南中第 7 节,“CallableStatement”。
  可通过调用方法 DatabaseMetaData.supportsStoredProcedures 检查数据库是否支持已存储过程。
  oj 表示外部连接
  外部连接的语法为
{oj outer-join}
  其中 outer-join 形式为
table LEFT OUTER JOIN {table / outer-join} ON search-condition
  外部连接属于高级功能。有关它们的解释可参见 SQL 语法。JDBC 提供了三种 DatabaseMetaData 方法用于确定驱动程序支持哪些外部连接类型:supportsOuterJoins、supportsFullOuterJoins 和 supportsLimitedOuterJoins。
  方法 Statement.setEscapeProcessing 可打开或关闭转义处理;缺省状态为打开。当性能极为重要时,程序员可能想关闭它以减少处理时间。但通常它将出于打开状态。应注意: setEscapeProcessing 不适用于 PreparedStatement 对象,因为在调用该语句前它就可能已被发送到数据库。有关预编译的信息,参见 PreparedStatement。
12.4.6使用方法 execute
  execute 方法应该仅在语句能返回多个 ResultSet 对象、多个更新计数或 ResultSet 对象与更新计数的组合时使用。当执行某个已存储过程或动态执行未知 SQL 字符串(即应用程序程序员在编译时未知)时,有可能出现多个结果的情况,尽管这种情况很少见。例如,用户可能执行一个已存储过程(使用 CallableStatement 对象 - 参见第 135 页的 CallableStatement),并且该已存储过程可执行更新,然后执行选择,再进行更新,再进行选择,等等。通常使用已存储过程的人应知道它所返回的内容。
  因为方法 execute 处理非常规情况,所以获取其结果需要一些特殊处理并不足为怪。例如,假定已知某个过程返回两个结果集,则在使用方法 execute 执行该过程后,必须调用方法 getResultSet 获得第一个结果集,然后调用适当的 getXXX 方法获取其中的值。要获得第二个结果集,需要先调用 getMoreResults 方法,然后再调用 getResultSet 方法。如果已知某个过程返回两个更新计数,则首先调用方法 getUpdateCount,然后调用 getMoreResults,并再次调用 getUpdateCount。
  对于不知道返回内容,则情况更为复杂。如果结果是 ResultSet 对象,则方法 execute 返回 true;如果结果是 Java int,则返回 false。如果返回 int,则意味着结果是更新计数或执行的语句是 DDL 命令。在调用方法 execute 之后要做的第一件事情是调用 getResultSet 或 getUpdateCount。调用方法 getResultSet 可以获得两个或多个 ResultSet 对象中第一个对象;或调用方法 getUpdateCount 可以获得两个或多个更新计数中第一个更新计数的内容。
  当 SQL 语句的结果不是结果集时,则方法 getResultSet 将返回 null。这可能意味着结果是一个更新计数或没有其它结果。在这种情况下,判断 null 真正含义的唯一方法是调用方法 getUpdateCount,它将返回一个整数。这个整数为调用语句所影响的行数;如果为 -1 则表示结果是结果集或没有结果。如果方法 getResultSet 已返回 null(表示结果不是 ResultSet 对象),则返回值 -1 表示没有其它结果。也就是说,当下列条件为真时表示没有结果(或没有其它结果):
((stmt.getResultSet() == null) && (stmt.getUpdateCount() == -1))
  如果已经调用方法 getResultSet 并处理了它返回的 ResultSet 对象,则有必要调用方法 getMoreResults 以确定是否有其它结果集或更新计数。如果 getMoreResults 返回 true,则需要再次调用 getResultSet 来检索下一个结果集。如上所述,如果 getResultSet 返回 null,则需要调用 getUpdateCount 来检查 null 是表示结果为更新计数还是表示没有其它结果。
  当 getMoreResults 返回 false 时,它表示该 SQL 语句返回一个更新计数或没有其它结果。因此需要调用方法 getUpdateCount 来检查它是哪一种情况。在这种情况下,当下列条件为真时表示没有其它结果:
((stmt.getMoreResults() == false) && (stmt.getUpdateCount() == -1))
  下面的代码演示了一种方法用来确认已访问调用方法 execute 所产生的全部结果集和更新计数:
stmt.execute(queryStringWithUnknownResults); while (true) { int rowCount = stmt.getUpdateCount(); if (rowCount > 0) { // 它是更新计数 System.out.println("Rows changed = " + count); stmt.getMoreResults(); continue; } if (rowCount == 0) { // DDL 命令或 0 个更新 System.out.println(" No rows changed or statement was DDL command"); stmt.getMoreResults(); continue; } // 执行到这里,证明有一个结果集 // 或没有其它结果 ResultSet rs = stmt.getResultSet; if (rs != null) {  . . . // 使用元数据获得关于结果集列的信息  while ( rs break; // 没有其它结果
12.5 ResultSet对象
ResultSet 包含符合 SQL 语句中条件的所有行,并且它通过一套 get 方法(这些 get 方法可以访问当前行中的不同列)提供了对这些行中数据的访问。
  ResultSet 包含符合 SQL 语句中条件的所有行,并且它通过一套 get 方法(这些 get 方法可以访问当前行中的不同列)提供了对这些行中数据的访问。ResultSet.next 方法用于移动到 ResultSet 中的下一行,使下一行成为当前行。
  结果集一般是一个表,其中有查询所返回的列标题及相应的值。例如,如果查询为 SELECT a, b, c FROM Table1,则结果集将具有如下形式:
a b c-------- --------- --------12345 Cupertino CA83472 Redmond WA83492 Boston MA
  下面的代码段是执行 SQL 语句的示例。该 SQL 语句将返回行集合,其中列 1 为 int,列 2 为 String,而列 3 则为字节数组:
java.sql.Statement stmt = conn.createStatement();ResultSet r = stmt.executeQuery("SELECT a, b, c FROM Table1");while (r.next()){ // 打印当前行的值。 int i = r.getInt("a"); String s = r.getString("b"); float f = r.getFloat("c"); System.out.println("ROW = " + i + " " + s + " " + f);}
12.5.1 行和光标
ResultSet 维护指向其当前数据行的光标。每调用一次 next 方法,光标向下移动一行。最初它位于第一行之前,因此第一次调用 next 将把光标置于第一行上,使它成为当前行。随着每次调用 next 导致光标向下移动一行,按照从上至下的次序获取ResultSet 行。
在 ResultSet 对象或其父辈 Statement 对象关闭之前,光标一直保持有效。
在 SQL 中,结果表的光标是有名字的。如果数据库允许定位更新或定位删除,则需要将光标的名字作为参数提供给更新或删除命令。可通过调用方法getCursorName 获得光标名。
注意:不是所有的 DBMS 都支持定位更新和删除。可使用 DatabaseMetaData.supportsPositionedDelete 和 supportsPositionedUpdate 方法来检查特定连接是否支持这些操作。当支持这些操作时,DBMS/驱动程序必须确保适当锁定选定行,以使定位更新不会导致更新异常或其它并发问题。
12.5.2 列
  方法 getXXX 提供了获取当前行中某列值的途径。在每一行内,可按任何次序获取列值。但为了保证可移植性,应该从左至右获取列值,并且一次性地读取列值。列名或列号可用于标识要从中获取数据的列。例如,如果 ResultSet 对象 rs 的第二列名为“title”,并将值存储为字符串,则下列任一代码将获取存储在该列中的值:
String s = rs.getString("title");String s = rs.getString(2);
  注意列是从左至右编号的,并且从列 1 开始。同时,用作 getXXX 方法的输入的列名不区分大小写。
  提供使用列名这个选项的目的是为了让在查询中指定列名的用户可使用相同的名字作为 getXXX 方法的参数。另一方面,如果 select 语句未指定列名(例如在“select * from table1”中或列是导出的时),则应该使用列号。这些情况下,
户将无法确切知道列名。
  有些情况下,SQL 查询返回的结果集中可能有多个列具有相同的名字。如果列名用作 getXXX 方法的参数,则 getXXX 将返回第一个匹配列名的值。因而,如果多个列具有相同的名字,则需要使用列索引来确保检索了正确的列值。这时,使用列号效率要稍微高一些。
  关于 ResultSet 中列的信息,可通过调用方法 ResultSet.getMetaData 得到。返回的 ResultSetMetaData 对象将给出其 ResultSet 对象各列的编号、类型和属性。
  如果列名已知,但不知其索引,则可用方法 findColumn 得到其列号。
12.5.3 数据类型和转换
  对于 getXXX 方法,JDBC 驱动程序试图将基本数据转换成指定 Java 类型,然后返回适合的 Java 值。例如,如果 getXXX 方法为 getString,而基本数据库中数据类型为 VARCHAR,则 JDBC 驱动程序将把 VARCHAR 转换成 Java String。getString 的返回值将为 Java String 对象。
  下表显示了允许用 getXXX 获取的 JDBC 类型及推荐用它获取的 JDBC 类型(通用SQL 类型)。小写的 x 表示允许 getXXX 方法获取该数据类型;大写的 X 表示对该数据类型推荐使用 getXXX 方法。例如,除了 getBytes 和 getBinaryStream 之外的任何 getXXX 方法都可用来获取 LONGVARCHAR 值,但是推荐根据返回的数据类型使用 getAsciiStream 或 getUnicodeStream 方法。方法 getObject 将任何数据类型返回为 Java Object。当基本数据类型是特定于数据库的抽象类型或当通用应用程序需要接受任何数据类型时,它是非常有用的。
  可使用 ResultSet.getXXX 方法获取常见的 JDBC 数据类型。
  “x”表示该 getXXX 方法可合法地用于获取给定 JDBC 类型。
  “X”表示推荐使用该 getXXX 方法来获取给定 JDBC 类型。
12.5.4 对非常大的行值使用流
  ResultSet 可以获取任意大的 LONGVARBINARY 或 LONGVARCHAR 数据。方法getBytes 和 getString 将数据返回为大的块(最大为 Statement.getMaxFieldSize 的返回值)。但是,以较小的固定块获取非常大的数据可能会更方便,而这可通过让 ResultSet 类返回 java.io.Input 流来完成。从该流中可分块读取数据。注意:必须立即访问这些流,因为在下一次对 ResultSet 调用getXXX 时它们将自动关闭(这是由于基本实现对大块数据访问有限制)。
  JDBC API 具有三个获取流的方法,分别具有不同的返回值:
  getBinaryStream 返回只提供数据库原字节而不进行任何转换的流。
  getAsciiStream 返回提供单字节 ASCII 字符的流。
  getUnicodeStream 返回提供双字节 Unicode 字符的流。
  注意:它不同于 Java 流,后者返回无类型字节并可(例如)通用于 ASCII 和Unicode 字符。
  下列代码演示了 getAsciiStream 的用法:
java.sql.Statement stmt = con.createStatement();ResultSet r = stmt.executeQuery("SELECT x FROM Table2");// 现在以 4K 块大小获取列 1 结果:byte buff = new byte[4096];while (r// 将新填充的缓冲区发送到 ASCII 输出流:output.write(buff, 0, size);}}
12.5.5 NULL 结果值
要确定给定结果值是否是 JDBC NULL,必须先读取该列,然后使用 ResultSet.wasNull 方法检查该次读取是否返回 JDBC NULL。
当使用 ResultSet.getXXX 方法读取 JDBC NULL 时,方法 wasNull 将返回下列值之一:
Java null 值:对于返回 Java 对象的 getXXX 方法(例如 getString、getBigDecimal、getBytes、getDate、getTime、getTimestamp、getAsciiStream、getUnicodeStream、getBinaryStream、getObject 等)。
  零值:对于 getByte、getShort、getInt、getLong、getFloat 和 getDouble。
  false 值:对于 getBoolean。
12.5.6 可选结果集或多结果集
通常使用 executeQuery(它返回单个 ResultSet)或 executeUpdate(它可用于任何数据库修改语句,并返回更新行数)可执行 SQL 语句。但有些情况下,应用程序在执行语句之前不知道该语句是否返回结果集。此外,有些已存储过程可能返回几个不同的结果集和/或更新计数。
为了适应这些情况,JDBC 提供了一种机制,允许应用程序执行语句,然后处理由结果集和更新计数组成的任意集合。这种机制的原理是首先调用一个完全通用的execute 方法,然后调用另外三个方法,getResultSet、getUpdateCount 和getMoreResults。这些方法允许应用程序一次一个地研究语句结果,并确定给定结果是 ResultSet 还是更新计数。
为了适应这些情况,JDBC 提供了一种机制,允许应用程序执行语句,然后处理由结果集和更新计数组成的任意集合。这种机制的原理是首先调用一个完全通用的execute 方法,然后调用另外三个方法,getResultSet、getUpdateCount 和getMoreResults。这些方法允许应用程序一次一个地研究语句结果,并确定给定结果是 ResultSet 还是更新计数。
用户不必关闭 ResultSet;当产生它的 Statement 关闭、重新执行或用于从多结果序列中获取下一个结果时,该 ResultSet 将被 Statement 自动关闭。
12.6 PreparedStatement接口
PreparedStatement 实例包含已编译的 SQL 语句。这就是使语句“准备好”。包含于 PreparedStatement 对象中的 SQL 语句可具有一个或多个 IN 参数。该 PreparedStatement 接口继承 Statement,并与之在两方面有所不同:
PreparedStatement 实例包含已编译的 SQL 语句。这就是使语句“准备好”。包含于 PreparedStatement 对象中的 SQL 语句可具有一个或多个 IN 参数。IN参数的值在 SQL 语句创建时未被指定。相反的,该语句为每个 IN 参数保留一个问号(“?”)作为占位符。每个问号的值必须在该语句执行之前,通过适当的setXXX 方法来提供。
由于 PreparedStatement 对象已预编译过,所以其执行速度要快于 Statement 对象。因此,多次执行的 SQL 语句经常创建为 PreparedStatement 对象,以提高效率。
  作为 Statement 的子类,PreparedStatement 继承了 Statement 的所有功能。另外它还添加了一整套方法,用于设置发送给数据库以取代 IN 参数占位符的值。同时,三种方法 execute、 executeQuery 和 executeUpdate 已被更改以使之不再需要参数。这些方法的 Statement 形式(接受 SQL 语句参数的形式)不应该用于 PreparedStatement 对象。
12.6.1创建 PreparedStatement 对象
  以下的代码段(其中 con 是 Connection 对象)创建包含带两个 IN 参数占位符的 SQL 语句的 PreparedStatement 对象:
PreparedStatement pstmt = con.prepareStatement("UPDATE table4 SET m = WHERE x = ");
  pstmt 对象包含语句 "UPDATE table4 SET m = WHERE x = ",它已发送给DBMS,并为执行作好了准备。
12.6.2 传递 IN 参数
  在执行 PreparedStatement 对象之前,必须设置每个 参数的值。这可通过调用 setXXX 方法来完成,其中 XXX 是与该参数相应的类型。例如,如果参数具有Java 类型 long,则使用的方法就是 setLong。setXXX 方法的第一个参数是要设置的参数的序数位置,第二个参数是设置给该参数的值。例如,以下代码将第一个参数设为 123456789,第二个参数设为 100000000:
pstmt.setLong(1, 123456789);pstmt.setLong(2, 100000000);
 一旦设置了给定语句的参数值,就可用它多次执行该语句,直到调用clearParameters 方法清除它为止。在连接的缺省模式下(启用自动提交),当语句完成时将自动提交或还原该语句。
  如果基本数据库和驱动程序在语句提交之后仍保持这些语句的打开状态,则同一个 PreparedStatement 可执行多次。如果这一点不成立,那么试图通过使用PreparedStatement 对象代替 Statement 对象来提高性能是没有意义的。
  利用 pstmt(前面创建的 PreparedStatement 对象),以下代码例示了如何设置两个参数占位符的值并执行 pstmt 10 次。如上所述,为做到这一点,数据库不能关闭 pstmt。在该示例中,第一个参数被设置为 "Hi"并保持为常数。在 for 循环中,每次都将第二个参数设置为不同的值:从 0 开始,到 9 结束。
pstmt.setString(1, "Hi");for (int i = 0; i < 10; i++) { pstmt.setInt(2, i); int rowCount = pstmt.executeUpdate();}
12.6.3 IN 参数中数据类型的一致性
  setXXX 方法中的 XXX 是 Java 类型。它是一种隐含的 JDBC 类型(一般 SQL 类型),因为驱动程序将把 Java 类型映射为相应的 JDBC 类型(遵循该 JDBCGuide中§8.6.2 “映射 Java 和 JDBC 类型”表中所指定的映射),并将该 JDBC 类型发送给数据库。例如,以下代码段将 PreparedStatement 对象 pstmt 的第二个参数设置为 44,Java 类型为 short:
pstmt.setShort(2, 44);
驱动程序将 44 作为 JDBC SMALLINT 发送给数据库,它是 Java short 类型的标准映射。
程序员的责任是确保将每个 IN 参数的 Java 类型映射为与数据库所需的 JDBC 数据类型兼容的 JDBC 类型。不妨考虑数据库需要 JDBC SMALLINT 的情况。如果使用方法 setByte ,则驱动程序将 JDBC TINYINT 发送给数据库。这是可行的,因为许多数据库可从一种相关的类型转换为另一种类型,并且通常 TINYINT 可用于SMALLINT 适用的任何地方
12.7 CallableStatement 对象
CallableStatement 对象为所有的 DBMS 提供了一种以标准形式调用已储存过程的方法。已储存过程储存在数据库中。对已储存过程的调用是 CallableStatement对象所含的内容。
CallableStatement 对象为所有的 DBMS 提供了一种以标准形式调用已储存过程的方法。已储存过程储存在数据库中。对已储存过程的调用是 CallableStatement对象所含的内容。这种调用是用一种换码语法来写的,有两种形式:一种形式带结果参,另一种形式不带结果参数。结果参数是一种输出 (OUT) 参数,是已储存过程的返回值。两种形式都可带有数量可变的输入(IN 参数)、输出(OUT 参数)或输入和输出(INOUT 参数)的参数。问号将用作参数的占位符。
  在 JDBC 中调用已储存过程的语法如下所示。注意,方括号表示其间的内容是可选项;方括号本身并不是语法的组成部份。
{call 过程名[( , , ...)]}
  返回结果参数的过程的语法为:
{ = call 过程名[( , , ...)]}
  不带参数的已储存过程的语法类似:
{call 过程名}
  通常,创建 CallableStatement 对象的人应当知道所用的 DBMS 是支持已储存过程的,并且知道这些过程都是些什么。然而,如果需要检查,多种DatabaseMetaData 方法都可以提供这样的信息。例如,如果 DBMS 支持已储存过程的调用,则supportsStoredProcedures 方法将返回 true,而getProcedures 方法将返回对已储存过程的描述。CallableStatement 继承 Statement 的方法(它们用于处理一般的 SQL 语句),还继承了 PreparedStatement 的方法(它们用于处理 IN 参)。
  CallableStatement 中定义的所有方法都用于处理 OUT 参数或 INOUT 参数的输出部分:注册 OUT 参数的 JDBC 类型(一般 SQL 类型)、从这些参数中检索结果,或者检查所返回的值是否为 JDBC NULL。
12.7.1 创建 CallableStatement 对象
  CallableStatement 对象是用 Connection 方法 prepareCall 创建的。下例创建 CallableStatement 的实例,其中含有对已储存过程 getTestData 调用。该过程有两个变量,但不含结果参数:
CallableStatement cstmt = con.prepareCall("{call getTestData( , )}");
  其中 占位符为IN、OUT还是INOUT参数,取决于已储存过程getTestData。
12.7.2 IN和OUT参数
  将IN参数传给 CallableStatement 对象是通过 setXXX 方法完成的。该方法继承自 PreparedStatement。所传入参数的类型决定了所用的setXXX方法(例如,用 setFloat 来传入 float 值等)。
  如果已储存过程返回 OUT 参数,则在执行 CallableStatement 对象以前必须先注册每个 OUT 参数的 JDBC 类型(这是必需的,因为某些 DBMS 要求 JDBC 类型)。注册 JDBC 类型是用 registerOutParameter 方法来完成的。语句执行完后,CallableStatement 的 getXXX 方法将取回参数值。正确的 getXXX 方法是为各参数所注册的 JDBC 类型所对应的 Java 类型。换言之, registerOutParameter 使用的是 JDBC 类型(因此它与数据库返回的 JDBC 类型匹配),而 getXXX 将之转换为 Java 类型。
  作为示例,下述代码先注册 OUT 参数,执行由 cstmt 所调用的已储存过程,然后检索在 OUT 参数中返回的值。方法 getByte 从第一个 OUT 参数中取出一个 Java 字节,而 getBigDecimal 从第二个 OUT 参数中取出一个 BigDecimal 对象(小数点后面带三位数):
CallableStatement cstmt = con.prepareCall("{call getTestData( , )}");cstmt.registerOutParameter(1, java.sql.Types.TINYINT);cstmt.registerOutParameter(2, java.sql.Types.DECIMAL, 3);cstmt.executeQuery();byte x = cstmt.getByte(1);java.math.BigDecimal n = cstmt.getBigDecimal(2, 3);
CallableStatement 与 ResultSet 不同,它不提供用增量方式检索大 OUT 值的特殊机制。
12.7.3、INOUT参数
  既支持输入又接受输出的参数(INOUT 参数)除了调用 registerOutParameter 方法外,还要求调用适当的 setXXX 方法(该方法是从 PreparedStatement 继承来的)。setXXX 方法将参数值设置为输入参数,而 registerOutParameter 方法将它的 JDBC 类型注册为输出参数。setXXX 方法提供一个 Java 值,而驱动程序先把这个值转换为 JDBC 值,然后将它送到数据库中。这种 IN 值的 JDBC 类型和提供给 registerOutParameter 方法的 JDBC 类型应该相同。然后,要检索输出值,就要用对应的 getXXX 方法。例如,Java 类型为byte 的参数应该使用方法 setByte 来赋输入值。应该给registerOutParameter 提供类型为 TINYINT 的 JDBC 类型,同时应使用 getByte 来检索输出值。
  下例假设有一个已储存过程 reviseTotal,其唯一参数是 INOUT 参数。方法setByte 把此参数设为 25,驱动程序将把它作为 JDBC TINYINT 类型送到数据库中。接着,registerOutParameter 将该参数注册为 JDBC TINYINT。执行完该已储存过程后,将返回一个新的 JDBC TINYINT 值。方法 getByte 将把这个新值作为 Java byte 类型检索。
CallableStatement cstmt = con.prepareCall("{call reviseTotal( )}");cstmt.setByte(1, 25);cstmt.registerOutParameter(1, java.sql.Types.TINYINT);cstmt.executeUpdate();byte x = cstmt.getByte(1);
12.7.4 先检索结果,再检索 OUT 参数
由于某些 DBMS 的限制,为了实现最大的可移植性,建议先检索由执行CallableStatement 对象所产生的结果,然后再用 CallableStatement.getXXX 方法来检索 OUT 参数。如果 CallableStatement 对象返回多个 ResultSet 对象(通过调用 execute 方法),在检索 OUT 参数前应先检索所有的结果。这种情况下,为确保对所有的结果都进行了访问,必须对 Statement 方法 getResultSet、getUpdateCount 和getMoreResults 进行调用,直到不再有结果为止。
检索完所有的结果后,就可用 CallableStatement.getXXX 方法来检索 OUT 参数中的值。
12.7.5 检索作为OUT参数的NULL值
返回到 OUT 参数中的值可能会是JDBC NULL。当出现这种情形时,将对 JDBC NULL 值进行转换以使 getXXX 方法所返回的值为 null、0 或 false,这取决于getXXX 方法类型。对于 ResultSet 对象,要知道0或false是否源于JDBCNULL的唯一方法,是用方法wasNull进行检测。如果 getXXX 方法读取的最后一个值是 JDBC NULL,则该方法返回 true,否则返回 flase。第五章 字符串类
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: Java中把字符串当作对象来处理, java.lang.String类提供了一系列操作字符串的方法,使得字符串的生成、访问和修改等操作容易和规范。
下载一
【课前思考】
  1. Java中的字符串有两种表示方法,这两种表示方法有什么不同?
  2. 如何访问字符串?如何修改字符串?如何对两个字符串进行比较?
【学习目标】
  本讲主要讲述java编程语言中的字符串及其处理。通过本讲的学习,同学们可以在java程序灵活的使用字符串。
【学习指南】
  同任何一种编程语言一样,应深刻理解字符串的概念,牢记字符串处理的相关语法,从而达到学习的目的。
【难 重 点】
 重点:
  1. 深刻理解字符串的概念。
  2. 能熟练处理字符串。
 难点:
  1. 使用StringBuffer类表示和操作字符串时,要注意它可以处理可变字符串,即当在StringBuffer类型的字符串中插入字符而超出已分配的缓冲区时,系统会自动地为它分配额外的空间。
【知 识 点】
   5 字符串的处理
    5.1 字符串的表示
    5.2 访问字符串
    5.3 修改字符串
    5.4 其它操作
第五章 字符串的处理
5.1 字符串的表示
5.1.1字符串常量
  字符串常量是用双引号括住的一串字符。
    "Hello World!"
5.1.2 String表示字符串常量
  用String表示字符串:
  String( char chars[ ] );
  String( char chars[ ], int startIndex, int numChars );
  String( byte ascii[ ], int hiByte );
  String( byte ascii[ ], int hiByte, int startIndex, int numChars );
  String使用示例:
  String s=new String() ; 生成一个空串
  下面用不同方法生成字符串"abc":
  char chars1[]={'a','b','c'};
  char chars2[]={'a','b','c','d','e'};
  String s1=new String(chars1);
  String s2=new String(chars2,0,3);
  byte ascii1[]={97,98,99};
  byte ascii2[]={97,98,99,100,101};
  String s3=new String(ascii1,0);
  String s4=new String(ascii2,0,0,3);
5.1.3 用StringBuffer表示字符串
  StringBuffer( );
  StringBuffer( int len );
  StringBuffer( String s );
5.2 访问字符串
1.类String中提供了length( )、charAt( )、indexOf( )、lastIndexOf( )、getChars( )、getBytes( )、toCharArray( )等方法。
  ◇ public int length() 此方法返回字符串的字符个数
  ◇ public char charAt(int index) 此方法返回字符串中index位置上的字符,其中index 值的 范围是0~length-1
  ◇ public int indexOf(int ch)
    public lastIndexOf(in ch)
  返回字符ch在字符串中出现的第一个和最后一个的位置
  ◇ public int indexOf(String str)
    public int lastIndexOf(String str)
  返回子串str中第一个字符在字符串中出现的第一个和最后一个的位置
  ◇ public int indexOf(int ch,int fromIndex)
    public lastIndexOf(in ch ,int fromIndex)
  返回字符ch在字符串中位置fromIndex以后出现的第一个和最后一个的位置
  ◇ public int indexOf(String str,int fromIndex)
    public int lastIndexOf(String str,int fromIndex)
  返回子串str中的第一个字符在字符串中位置fromIndex后出现的第一个和最后一个的位置。
  ◇ public void getchars(int srcbegin,int end ,char buf[],int dstbegin)
   srcbegin 为要提取的第一个字符在源串中的位置, end为要提取的最后一个字符在源串中的位置,字符数组buf[]存放目的字符串,    dstbegin 为提取的字符串在目的串中的起始位置。
  ◇public void getBytes(int srcBegin, int srcEnd,byte[] dst, int dstBegin)
  参数及用法同上,只是串中的字符均用8位表示。
 2.类StringBuffer提供了 length( )、charAt( )、getChars( )、capacity()等方法。
  方法capacity()用来得到字符串缓冲区的容量,它与方法length()所返回的值通常是不同的。
5.3 修改字符串
1.String类提供的方法:
   concat( )
   replace( )
   substring( )
   toLowerCase( )
   toUpperCase( )
  ◇ public String contat(String str);
  用来将当前字符串对象与给定字符串str连接起来。
  ◇ public String replace(char oldChar,char newChar);
  用来把串中出现的所有特定字符替换成指定字符以生成新串。
  ◇ public String substring(int beginIndex);
  public String substring(int beginIndex,int endIndex);
  用来得到字符串中指定范围内的子串。
  ◇ public String toLowerCase();
  把串中所有的字符变成小写。
  ◇ public String toUpperCase();
  把串中所有的字符变成大写。
 2.StringBuffer类提供的方法:
  append( )
  insert( )
  setCharAt( )
  如果操作后的字符超出已分配的缓冲区,则系统会自动为它分配额外的空间。
  ◇ public synchronized StringBuffer append(String str);
  用来在已有字符串末尾添加一个字符串str。
  ◇ public synchronized StringBuffer insert(int offset, String str);
  用来在字符串的索引offset位置处插入字符串str。
  ◇ public synchronized void setCharAt(int index,char ch);
  用来设置指定索引index位置的字符值。
  注意:String中对字符串的操作不是对源操作串对象本身进行的,而是对新生成的一个源操作串对象的拷贝进行的,其操作的结果不影响源串。
  相反,StringBuffer中对字符串的连接操作是对源串本身进行的,操作之后源串的值发生了变化,变成连接后的串。
5.4 其它操作
1.字符串的比较
  String中提供的方法:
  equals( )和equalsIgnoreCase( )
  它们与运算符'= ='实现的比较是不同的。运算符'= ='比较两个对象是否引用同一个实例,而equals( )和equalsIgnoreCase( )则比较  两个字符串中对应的每个字符值是否相同。
 2.字符串的转化
  java.lang.Object中提供了方法toString( )把对象转化为字符串。
 3.字符串"+"操作
  运算符'+'可用来实现字符串的连接:
  String s = "He is "+age+" years old.";
  其他类型的数据与字符串进行"+"运算时,将自动转换成字符串。具体过程如下:
  String s=new StringBuffer("he is").append(age).append("years old").toString();
  注意:除了对运算符"+"进行了重载外,java不支持其它运算符的重载。第一章:绪论
第二章:JAVA语言基础
第三章:面向对象技术
第四章:JAVA类和对象的高级特征
第五章:字符串类
第六章:异常处理
第七章:JAVA图形用户界面
第八章:JAVA Applet
第九章:输入输出系统
第十章:多线程
第十一章:网络编程
第十二章:JAVA数据库连接第七章 Java图形用户界面
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: 用AWT来生成图形化用户界面时,组件和容器的概念非常重要。组件是各种各样的类,封装了图形系统的许多最小单位,例如按钮、窗口等等;而容器也是组件,它的最主要的作用是装载其它组件,但是象Panel这样的容器也经常被当作组件添加到其它容器中,以便完成杂的界面设计。布局管理器是java语言与其它编程语言在图形系统方面较为显著的区别,容器中各个组件的位置是由布局管理器来决定的,共有5种布局管理器,每种布局管理器都有自己的放置规律。事件处理机制能够让图形界面响应用户的操作,主要涉及到事件源、事件、事件处理者等三方,事件源就是图形界面上的组件,事件就是对用户操作的描述,而事件处理者是处理事件的类。因此,对于AWT中所提供的各个组件,我们都需要了解该组件经常发生的事件以及处理该事件的相应的监听器接口。对于AWT而言,Java 1.1到Java 1.2最大的改变就是Java中所有的库。当Java 1.1版纳入新的事件模型和Java Beans时,平台被设置--现在它可以被拖放到可视化的应用程序构建工具中,创建GUI组件。另外,事件模型的设计和Bean无疑对轻松的编程和可维护的代码都非常有益。对于Swing组件而言,交叉平台GUI编程可以变成一种有意义的尝试。本章主要介绍了一些Swing的新特性,它和AWT相比有哪些不同的方法和应用,着重阐述了Swing的特色组件和容器,并以图形的形式给出具体描述,同时介绍了组件的分类,使用Swing的基本规则,各种容器面板以及布局管理器,由于Swing是Java2新增特性, 它对图形化用户界面提供了庞大而复杂的类库支持,要能做到开发和实用,还需做大量工作,利用API的帮助,逐步深入摸索其规律,从组件和容器入手,掌握其特色方法。从另一角度来看,Swing和AWT无论是布局管理器还是事件处理机制,以及对一些重量容器的保留和使用,都是我们非常熟悉的内容,其原理我们已在AWT一章做了详细介绍,因此,AWT作为Swing的基础,是需要很好掌握的,希望大家能在不断设计应用中摸索出新方法和新技巧。
下载一
 第七章 Java图形用户界面
【课前思考】
  1. java语言是跨平台的编程语言,那么图形用户界面如何做到跨平台?
  2. AWT有哪些组件和容器?它们各自的使用方法是什么?
3. AWT的事件处理模型是什么?原理又如何?
4. 什么是Swing?它和AWT比有什么优点?使用上有什么区别?
5. Swing的组件层次结构有什么特点?是如何实现的?
6. Swing有哪些常用组件?怎么用?
7. Swing有几种容器?其功能特性是什么?
8. Swing的布局管理器有哪些特点,与AWT有哪些区别?
【学习目标】
掌握用AWT来设计图形用户界面的方法,尤其是组件、容器、布局管理器等概念。学习AWT事件处理模型,掌握事件源、事件、事件处理者等概念,让程序能够响应用户的操作。最后了解AWT各个组件的用法及所采用的事件处理接口。
学习java中Swing的使用,掌握Swing的基本用法,了解其常用组件和容器的使用方法及功能,知道其布局管理器和事件处理与AWT处理上的区别,了解其辅助特性。
【学习指南】
理解概念,多实践,勤思考,举一反三。Swing和AWT边比较边学习,掌握Swing的新增特性、新方法、新容器。方法是:首先能透彻地掌握一个新增组件,由此扩展到其他新增组件上去,然后可以在API的帮助下,顺利把握其他新特性 。
【难 重 点】
 重点:  事件处理模型。Swing的新增特性。
 难点:  内部类匿名类在AWT中的应用。Swing新的容器模型以及众多的组件的使用方法。
【知 识 点】
 第七章 Java图形用户界面
 7.1 用AWT生成图形化用户界面
   7.1.1 java.awt包
   7.1.2 组件和容器
   7.1.3 常用容器
   7.1.4 布局管理器
  7.2 AWT事件处理模型
   7.2.1 事件类
   7.2.2 事件监听器
   7.2.3 AWT事件及其相应的监听器接口
   7.2.4 事件适配器
7.3 AWT组件库
7.4 Swing简介
   7.4.1 简介
   7.4.2 Swing的类层次结构
   7.4.3 Swing组件的多样化
   7.4.4 MVC(Model-View-Control)体系结构
   7.4.5 可存取性支持
   7.4.6 支持键盘操作
   7.4.7 设置边框
   7.4.8 使用图标(Icon)
   7.4.9 Swing程序结构简介
  7.5 Swing组件和容器
   7.5.1 组件的分类
   7.5.2 使用Swing的基本规则
   7.5.3 各种容器面板和组件
   7.5.4 布局管理器
【知识结构图】
7.1 用AWT生成图形化用户界面
  抽象窗口工具包AWT (Abstract Window Toolkit) 是 API为Java 程序提供的建立图形用户界面GUI (Graphics User Interface)工具集,AWT可用于Java的applet和applications中。它支持图形用户界面编程的功能包括: 用户界面组件;事件处理模型;图形和图像工具,包括形状、颜色和字体类;布局管理器,可以进行灵活的窗口布局而与特定窗口的尺寸和屏幕分辨率无关;数据传送类,可以通过本地平台的剪贴板来进行剪切和粘贴。
 7.1.1 java.awt包
  java.awt包中提供了GUI设计所使用的类和接口,可从图5.1中看到主要类之间的关系。
java.awt包提供了基本的java程序的GUI设计工具。主要包括下述三个概念:
  组件--Component
  容器--Container
  布局管理器—LayoutManager
7.1.2 组件和容器
Java的图形用户界面的最基本组成部分是组件(Component),组件是一个可以以图形化的方式显示在屏幕上并能与用户进行交互的对象,例如一个按钮,一个标签等。组件不能独立地显示出来,必须将组件放在一定的容器中才可以显示出来。
类java.ponent是许多组件类的父类,Component类中封装了组件通用的方法和属性,如图形的组件对象、大小、显示位置、前景色和背景色、边界、可见性等,因此许多组件类也就继承了Component类的成员方法和成员变量,相应的成员方法包括:
   getComponentAt(int x, int y)
   getFont()
   getForeground()
   getName()
   getSize()
   paint(Graphics g)
   repaint()
   update()
   setVisible(boolean b)
   setSize(Dimension d)
   setName(String name)等
容器(Container)也是一个类,实际上是Component的子类,因此容器本身也是一个组件,具有组件的所有性质,但是它的主要功能是容纳其它组件和容器。
布局管理器(LayoutManager):每个容器都有一个布局管理器,当容器需要对某个组件进行定位或判断其大小尺寸时,就会调用其对应的布局管理器。
为了使我们生成的图形用户界面具有良好的平台无关性,Java语言中,提供了布局管理器这个工具来管理组件在容器中的布局,而不使用直接设置组件位置和大小的方式。
在程序中安排组件的位置和大小时,应该注意以下两点:
1.容器中的布局管理器负责各个组件的大小和位置,因此用户无法在这种情况下设置组件的这些属性。如果试图使用Java 语言提供的setLocation(),setSize(),setBounds() 等方法,则都会被布局管理器覆盖。
2.如果用户确实需要亲自设置组件大小或位置,则应取消该容器的布局管理器,方法为:
   setLayout(null);
7.1.3 常用容器
容器java.awt.Container是Component的子类,一个容器可以容纳多个组件,并使它们成为一个整体。容器可以简化图形化界面的设计,以整体结构来布置界面。所有的容器都可以通过add()方法向容器中添加组件。
有三种类型的容器:Window、Panel、ScrollPane,常用的有Panel, Frame, Applet。
1.Frame
  
  以下是容器的例子:
 例7.1
  import java.awt.*;
  public class MyFrame extends Frame{
  public static void main(String args[ ]){
        MyFrame fr = new MyFrame("Hello Out There!");
                       //构造方法
        fr.setSize(200,200);
                //设置Frame的大小,缺省为(0,0)
        fr.setBackground(Color.red);
                //设置Frame的背景,缺省为红色
        fr.setVisible(true);
                //设置Frame为可见,缺省为不可见
  }
     public MyFrame (String str){
        super(str); //调用父类的构造方法
     }
  }
一般我们要生成一个窗口,通常是用Window的子类Frame来进行实例化,而不是直接用到Window类。Frame的外观就像我们平常在windows系统下见到的窗口,有标题、边框、菜单、大小等等。每个Frame的对象实例化以后,都是没有大小和不可见的,因此必须调用setSize( )来设置大小,调用setVisible(true)来设置该窗口为可见的。
另外,AWT在实际的运行过程中是调用所在平台的图形系统,因此同样一段AWT程序在不同的操作系统平台下运行所看到的图形系统是不一样的。例如在windows下运行,则显示的窗口是windows风格的窗口;而在UNIX下运行时,则显示的是UNIX风格的窗口。
 2. Panel
  
 例7.2
  import java.awt.*;
  public class FrameWithPanel extends Frame{
  public FrameWithPanel(String str){
        super(str);
      }
      public static void main(String args[]){
        FrameWithPanel fr = new FrameWithPanel("Frame with Panel");
        Panel pan=new Panel();
        fr.setSize(200,200);
        fr.setBackground(Color.red);
               //框架fr的背景颜色设置为红色
        fr.setLayout(null);
               //取消布局管理器
        pan.setSize(100,100);
        pan.setBackground(Color.yellow);
               //设置面板pan的背景颜色为黄色
        fr.add(pan); //用add方法把面板pan添加到框架fr中
        fr.setVisible(true);
        }
   }
一般我们要生成一个窗口,通常是用Window的子类Frame来进行实例化,而不是直接用到Window类。Frame的外观就像我们平常在windows系统下见到的窗口,有标题、边框、菜单、大小等等。每个Frame的对象实例化以后,都是没有大小和不可见的,因此必须调用setSize( )来设置大小,调用setVisible(true)来设置该窗口为可见的。
另外,AWT在实际的运行过程中是调用所在平台的图形系统,因此同样一段AWT程序在不同的操作系统平台下运行所看到的图形系统是不一样的。例如在windows下运行,则显示的窗口是windows风格的窗口;而在UNIX下运行时,则显示的是UNIX风格的窗口。
7.1.4 LayoutManager 布局管理器
java为了实现跨平台的特性并且获得动态的布局效果,java将容器内的所有组件安排给一个"布局管理器"负责管理,如:排列顺序,组件的大小、位置,当窗口移动或调整大小后组件如何变化等功能授权给对应的容器布局管理器来管理,不同的布局管理器使用不同算法和策略,容器可以通过选择不同的布局管理器来决定布局。
1. FlowLayout ( G:\study\java\java_qh\JAVA\text\ch05\se01\right5_1_4_1.htm" \l "01" \t "MyFrame )
  2. BorderLayout ( G:\study\java\java_qh\JAVA\text\ch05\se01\right5_1_4_1.htm" \l "02" \t "MyFrame )
  3. GridLayout ( G:\study\java\java_qh\JAVA\text\ch05\se01\right5_1_4_1.htm" \l "03" \t "MyFrame )
布局管理器主要包括:FlowLayout,BorderLayout,GridLayout,CardLayout,GridBagLayout
 例7.3
    import java.awt.*;
    public class ExGui{
        private Frame f;
        private Button b1;
        private Button b2;
        public static void main(String args[]){
            ExGui that = new ExGui();
            that.go();
    }
        public void go(){
            f = new Frame("GUI example");
            f.setLayout(new FlowLayout());
            //设置布局管理器为FlowLayout
            b1 = new Button("Press Me");
            //按钮上显示字符"Press Me"
            b2 = new Button("Don't Press Me");
            f.add(b1);
            f.add(b2);
            f.pack();
            //紧凑排列,其作用相当于setSize(),即让窗口
            尽量小,小到刚刚能够包容住b1、b2两个按钮
            f.setVisible(true);
        }
    }
 1. FlowLayout
  FlowLayout 是Panel,Applet的缺省布局管理器。其组件的放置规律是从上到下、从左到右进行放置,如果容器足够宽,第一个组件先添加到容器中第一行的最左边,后续的组件依次添加到上一个组件的右边,如果当前行已放置不下该组件,则放置到下一行的最左边。
  构造方法主要下面几种:
  FlowLayout(FlowLayout.RIGHT,20,40);
  
  FlowLayout(FlowLayout.LEFT);
  //居左对齐,横向间隔和纵向间隔都是缺省值5个象素
  FlowLayout();
  //缺省的对齐方式居中对齐,横向间隔和纵向间隔都是缺省值5个象素
 例7.4
    import java.awt.*;
    public class myButtons{
     public static void main(String args[])
     {
        Frame f = new Frame();
        f.setLayout(new FlowLayout());
        Button button1 = new Button("Ok");
        Button button2 = new Button("Open");
        Button button3 = new Button("Close");
        f.add(button1);
        f.add(button2);
        f.add(button3);
        f.setSize(300,100);
        f.setVisible(true);
     }
    }
  当容器的大小发生变化时,用FlowLayout管理的组件会发生变化,其变化规律是:组件的大小不变,但是相对位置会发生变化。例如上图中有三个按钮都处于同一行,但是如果把该窗口变窄,窄到刚好能够放下一个按钮,则第二个按钮将折到第二行,第三个按钮将折到第三行。按钮"Open"本来在按钮"OK"的右边,但是现在跑到了下面,所以说"组件的大小不变,但是相对位置会发生变化"。
 2. BorderLayout
  BorderLayout 是Window,Frame和Dialog的缺省布局管理器。BorderLayout布局管理器把容器分成5个区域:North,South,East,West和Center,每个区域只能放置一个组件。各个区域的位置及大小如下图所示:
    
 例7.5
    import java.awt.*;
    public class buttonDir{
     public static void main(String args[]){
      Frame f = new Frame("BorderLayout");
      f.setLayout(new BorderLayout());
      f.add("North", new Button("North"));
      //第一个参数表示把按钮添加到容器的North区域
      f.add("South", new Button("South"));
      //第一个参数表示把按钮添加到容器的South区域
      f.add("East", new Button("East"));
      //第一个参数表示把按钮添加到容器的East区域
      f.add("West", new Button("West"));
      //第一个参数表示把按钮添加到容器的West区域
      f.add("Center", new Button("Center"));
      //第一个参数表示把按钮添加到容器的Center区域
      f.setSize(200,200);
      f.setVisible(true);
     }
    }
  在使用BorderLayout的时候,如果容器的大小发生变化,其变化规律为:组件的相对位置不变,大小发生变化。例如容器变高了,则North、South区域不变,West、Center、East区域变高;如果容器变宽了,West、East区域不变,North、Center、South区域变宽。不一定所有的区域都有组件,如果四周的区域(West、East、North、South区域)没有组件,则由Center区域去补充,但是如果Center区域没有组件,则保持空白,其效果如下几幅图所示:
  
       North区域缺少组件         
  
      North和Center区域缺少组件
 3. GridLayout
  使容器中各个组件呈网格状布局,平均占据容器的空间。
 例7.6
    import java.awt.*;
    public class ButtonGrid {
    public static void main(String args[]) {
      Frame f = new Frame("GridLayout");
      f.setLayout(new GridLayout(3,2));
                 //容器平均分成3行2列共6格
      f.add(new Button("1")); //添加到第一行的第一格
      f.add(new Button("2")); //添加到第一行的下一格
      f.add(new Button("3")); //添加到第二行的第一格
      f.add(new Button("4")); //添加到第二行的下一格
      f.add(new Button("5")); //添加到第三行的第一格
      f.add(new Button("6")); //添加到第三行的下一格
      f.setSize(200,200);
      f.setVisible(true);
    }
    }
4. CardLayout
  CardLayout布局管理器能够帮助用户处理两个以至更多的成员共享同一显示空间,它把容器分成许多层,每层的显示空间占据整个容器的大小,但是每层只允许放置一个组件,当然每层都可以利用Panel来实现复杂的用户界面。牌布局管理器(CardLayout)就象一副叠得整整齐齐的扑克牌一样,有54张牌,但是你只能看见最上面的一张牌,每一张牌就相当于牌布局管理器中的每一层。
 例7.7
 import java.awt.*;
 import java.awt.event.*; //事件处理机制,下一节的内容
 public class ThreePages implements MousListener {
    CardLayout layout=new CardLayout(); //实例化一个牌布局管理器对象
    Frame f=new Frame("CardLayout");
    Button page1Button;
    Label page2Label; //Label是标签,实际上是一行字符串
    TextArea page3Text; //多行多列的文本区域
    Button page3Top;
    Button page3Bottom;
 public static void main(String args[])
 { new ThreePages().go(); }
 Public void go()
 {   f.setLayout(layout); //设置为牌布局管理器layout
    f.add(page1Button=new Button("Button page"),"page1Button");
    page1Button.addMouseListener(this); //注册监听器
    f.add(page2Label=new Label("Label page"),"page2Label");
    page2Label.addMouseLisener(this); //注册监听器
    Panel panel=new Panel();
    panel.setLayout(new BorderLayout());
    panel.add(page3Text=new TextArea("Composite page"),"Center");
    page3Text.addMouseListener(this);
    panel.add(page3Top=new Button("Top button") , "North");
    page3Top.addMouseListener(this);
    panel.add(page3Bottom=new Button("Bottom button") ,"South");
    page3Bottom.addMouseListener(this);
    f.add(panel,"panel");
    f.setSize(200,200);
    f.setVisible(true);
 }
 ……
 }
 5.容器的嵌套
  在复杂的图形用户界面设计中,为了使布局更加易于管理,具有简洁的整体风格,一个包含了多个组件的容器本身也可以作为一个组件加到另一个容器中去,容器中再添加容器,这样就形成了容器的嵌套。下面是一个容器嵌套的例子。
 例7.8
    import java.awt.*;
    public class ExGui3{
    private Frame f;
    private Panel p;
    private Button bw,bc;
    private Button bfile,bhelp;
       public static void main(String args[])
       {
         ExGui3 gui = new ExGui3();
         gui.go();
       }
    public void go(){
       f = new Frame("GUI example 3");
       bw=new Button("West");
       bc=new Button("Work space region");
       f.add(bw,"West");
       f.add(bc,"Center");
       p = new Panel();
       f.add(p,"North");
       bfile= new Button("File");
       bhelp= new Button("Help");
       p.add(bfile);
       p.add(bhelp);
       f.pack();
       f.setVisible(true);
    }
    }
  
7.2 AWT事件处理模型
  上一节中的主要内容是如何放置各种组件,使图形界面更加丰富多彩,但是还不能响应用户的任何操作,要能够让图形界面接收用户的操作,就必须给各个组件加上事件处理机制。在事件处理的过程中,主要涉及三类对象:
  ◇ Event-事件,用户对界面操作在java语言上的描述,以类的形式出现,例如键盘操作对应的事件类是KeyEvent。
  ◇ Event Source-事件源,事件发生的场所,通常就是各个组件,例如按钮Button。
  ◇ Event handler-事件处理者,接收事件对象并对其进行处理的对象。
例如,如果用户用鼠标单击了按钮对象button,则该按钮button就是事件源,而java运行时系统会生成ActionEvent类的对象actionE,该对象中描述了该单击事件发生时的一些信息,然后,事件处理者对象将接收由java运行时系统传递过来的事件对象actionE并进行相应的处理。
  由于同一个事件源上可能发生多种事件,因此java采取了授权处理机制(Delegation Model),事件源可以把在其自身所有可能发生的事件分别授权给不同的事件处理者来处理。比如在Canvas对象上既可能发生鼠标事件,也可能发生键盘事件,该Canvas对象就可以授权给事件处理者一来处理鼠标事件,同时授权给事件处理者二来处理键盘事件。有时也将事件处理者称为监听器,主要原因也在于监听器时刻监听着事件源上所有发生的事件类型,一旦该事件类型与自己所负责处理的事件类型一致,就马上进行处理。授权模型把事件的处理委托给外部的处理实体进行处理,实现了将事件源和监听器分开的机制。事件处理者(监听器)通常是一个类,该类如果要能够处理某种类型的事件,就必须实现与该事件类型相对的接口。例如例5.9中类ButtonHandler之所以能够处理ActionEvent事件,原因在于它实现了与ActionEvent事件对应的接口ActionListener。每个事件类都有一个与之相对应的接口。
  将事件源对象和事件处理器(事件监听器)分开。如图5.2所示
    
  打个不太恰当的比喻,比如说有一位李先生,李先生可能会发生很多法律纠纷,可能是民事法律纠纷,也可能是刑事法律纠纷,那么李先生可以请律师,他可以授权王律师负责帮他打民事法律的官司,同时也可以授权张律师帮他打刑事法律的官司。这个请律师的过程从李先生的角度来看,就是授权的过程,而从王律师和张律师的角度来看,一旦被授权,他们就得时刻对李先生负责,"监听"着李先生,一旦发生民事纠纷了,王律师就要马上去处理,而一旦发生刑事纠纷了,张律师就要马上进行处理。此时此刻,李先生就是事件源,王律师是一个事件处理者,张律师是另外一个事件处理者,民事纠纷和刑事纠纷就是不同类型的事件。
  
 例7.9
    import java.awt.*;
    import java.awt.event.*;
    public class TestButton {
    public static void main(String args[])
    {
      Frame f = new Frame("Test");
      Button b = new Button("Press Me!");
      b.addActionListener(new ButtonHandler());
      f.setLayout(new FlowLayout()); //设置布局管理器
      f.add(b);
      f.setSize(200,100);
      f.setVisible(true);
      }
    }
    class ButtonHandler implements ActionListener {
    //实现接口ActionListener才能做事件ActionEvent的处理者
    public void actionPerformed(ActionEvent e)
    //系统产生的ActionEvent事件对象被当作参数传递给该方法
    {
      System.out.println("Action occurred");
    //本接口只有一个方法,因此事件发生时,系统会自动调用本方法,需要做的操作就把代码写在则个方法里。
    }
    }
  使用授权处理模型进行事件处理的一般方法归纳如下:
  1.对于某种类型的事件XXXEvent, 要想接收并处理这类事件,必须定义相应的事件监听器类,该类需要实现与该事件相对应的接口XXXListener;
  2.事件源实例化以后,必须进行授权,注册该类事件的监听器,使用addXXXListener(XXXListener ) 方法来注册监听器。
7.2.1 事件类
  与AWT有关的所有事件类都由java.awt.AWTEvent类派生,它也是EventObject类的子类。AWT事件共有10类,可以归为两大类:低级事件和高级事件。
java.util.EventObject类是所有事件对象的基础父类,所有事件都是由它派生出来的。AWT的相关事件继承于java.awt.AWTEvent类,这些AWT事件分为两大类:低级事件和高级事件,低级事件是指基于组件和容器的事件,当一个组件上发生事件,如:鼠标的进入,点击,拖放等,或组件的窗口开关等,触发了组件事件。高级事件是基于语义的事件,它可以不和特定的动作相关联,而依赖于触发此事件的类,如在TextField中按Enter键会触发ActionEvent事件,滑动滚动条会触发AdjustmentEvent事件,或是选中项目列表的某一条就会触发ItemEvent事件。
  ◇ 低级事件
  ComponentEvent( 组件事件:组件尺寸的变化,移动)
  ContainerEvent( 容器事件:组件增加,移动)
  WindowEvent( 窗口事件:关闭窗口,窗口闭合,图标化)
  FocusEvent( 焦点事件:焦点的获得和丢失)
  KeyEvent( 键盘事件:键按下、释放)
  MouseEvent( 鼠标事件:鼠标单击,移动)
  ◇ 高级事件(语义事件)
  ActionEvent(动作事件:按钮按下,TextField中按Enter键)
  AdjustmentEvent(调节事件:在滚动条上移动滑块以调节数值)
  ItemEvent(项目事件:选择项目,不选择"项目改变")
  TextEvent(文本事件,文本对象改变)
7.2.2 事件监听器
  每类事件都有对应的事件监听器,监听器是接口,根据动作来定义方法。
例如,与键盘事件KeyEvent相对应的接口是:
  public interface KeyListener extends EventListener {
     public void keyPressed(KeyEvent ev);
     public void keyReleased(KeyEvent ev);
     public void keyTyped(KeyEvent ev);
  }
  注意到在本接口中有三个方法,那么java运行时系统何时调用哪个方法?其实根据这三个方法的方法名就能够知道应该是什么时候调用哪个方法执行了。当键盘刚按下去时,将调用keyPressed( )方法执行,当键盘抬起来时,将调用keyReleased( )方法执行,当键盘敲击一次时,将调用keyTyped( )方法执行。
  又例如窗口事件接口:
  public interface WindowListener extends EventListener{
     public void windowClosing(WindowEvent e);
     //把退出窗口的语句写在本方法中
     public void windowOpened(WindowEvent e);
     //窗口打开时调用
     public void windowIconified(WindowEvent e);
     //窗口图标化时调用
     public void windowDeiconified(WindowEvent e);
     //窗口非图标化时调用
     public void windowClosed(WindowEvent e);
     //窗口关闭时调用
     public void windowActivated(WindowEvent e);
     //窗口激活时调用
     public void windowDeactivated(WindowEvent e);
     //窗口非激活时调用
  }
  AWT的组件类中提供注册和注销监听器的方法:
  ◇ 注册监听器:
  public void add (listener);
  ◇ 注销监听器:
  public void remove (listener);
  例如Button类:(查API)
  public class Button extends Component {
     ……
     public synchronized void addActionListener(ActionListener l);
     public synchronized void removeActionListener(ActionListener l);
     ……}
7.2.3 AWT事件及其相应的监听器接口
  下表列出了所有AWT事件及其相应的监听器接口,一共10类事件,11个接口。下面这张表应能牢牢记住。
事件类别 描述信息 接口名 方法
 ActionEvent 激活组件   ActionListener  actionPerformed(ActionEvent)
 ItemEvent 选择了某些项目   ItemListener  itemStateChanged(ItemEvent)
 MouseEvent 鼠标移动   MouseMotionListener  mouseDragged(MouseEvent) mouseMoved(MouseEvent)
鼠标点击等   MouseListener  mousePressed(MouseEvent) mouseReleased(MouseEvent) mouseEntered(MouseEvent) mouseExited(MouseEvent)  mouseClicked(MouseEvent)
 KeyEvent 键盘输入   KeyListener  keyPressed(KeyEvent) keyReleased(KeyEvent) keyTyped(KeyEvent)
 FocusEvent 组件收到或失去焦点   FocusListener  focusGained(FocusEvent) focusLost(FocusEvent)
 AdjustmentEvent 移动了滚动条等组件   AdjustmentListener  adjustmentValueChanged(AdjustmentEvent)
 ComponentEvent 对象移动缩放显示隐藏等   ComponentListener  componentMoved(ComponentEvent) componentHidden(ComponentEvent) componentResized(ComponentEvent) componentShown(ComponentEvent)
 WindowEvent 窗口收到窗口级事件   WindowListener  windowClosing(WindowEvent) windowOpened(WindowEvent) windowIconified(WindowEvent) windowDeiconified(WindowEvent) windowClosed(WindowEvent) windowActivated(WindowEvent) windowDeactivated(WindowEvent)
 ContainerEvent 容器中增加删除了组件   ContainerListener  componentAdded(ContainerEvent) componentRemoved(ContainerEvent)
 TextEvent 文本字段或文本区发生改变   TextListener  textValueChanged(TextEvent)
例7.10说明事件处理模型的应用。
import java.awt.*;
  import java.awt.event.*;
    public class ThreeListener implements MouseMotionListener,MouseListener,WindowListener {
    //实现了三个接口
    private Frame f;
    private TextField tf;
    public static void main(String args[])
    {
     ThreeListener two = new ThreeListener();
     two.go(); }
    public void go() {
    f = new Frame("Three listeners example");
    f.add(new Label("Click and drag the mouse"),"North");
    tf = new TextField(30);
    f.add(tf,"South"); //使用缺省的布局管理器
    f.addMouseMotionListener(this); //注册监听器MouseMotionListener
    f.addMouseListener(this); //注册监听器MouseListener
    f.addWindowListener(this); //注册监听器WindowListener
    f.setSize(300,200);
    f.setVisible(true);
      }
    public void mouseDragged (MouseEvent e) {
    //实现mouseDragged方法
    String s = "Mouse dragging : X="+e.getX()+"Y = "+e.getY();
    tf.setText(s);
      }
    public void mouseMoved(MouseEvent e){}
    //对其不感兴趣的方法可以方法体为空
    public void mouseClicked(MouseEvent e){}
    public void mouseEntered(MouseEvent e){
      String s = "The mouse entered";
      tf.setText(s);
        }
    public void mouseExited(MouseEvent e){
      String s = "The mouse has left the building";
      tf.setText(s);
        }
    public void mousePressed(MouseEvent e){}
    public void mouseReleased(MouseEvent e){ }
    public void windowClosing(WindowEvent e) {
    //为了使窗口能正常关闭,程序正常退出,需要实现windowClosing方法
      System.exit(1);
        }
    public void windowOpened(WindowEvent e) {}
    //对其不感兴趣的方法可以方法体为空
    public void windowIconified(WindowEvent e) {}
    public void windowDeiconified(WindowEvent e) {}
    public void windowClosed(WindowEvent e) {}
    public void windowActivated(WindowEvent e) { }
    public void windowDeactivated(WindowEvent e) {}
    }
  上例中有如下几个特点:
  1.可以声明多个接口,接口之间用逗号隔开。
    ……implements MouseMotionListener, MouseListener, WindowListener;
  2.可以由同一个对象监听一个事件源上发生的多种事件:
  f.addMouseMotionListener(this);
  f.addMouseListener(this);
  f.addWindowListener(this);
  则对象f 上发生的多个事件都将被同一个监听器接收和处理。
  3.事件处理者和事件源处在同一个类中。本例中事件源是Frame f,事件处理者是类ThreeListener,其中事件源Frame f是类ThreeListener的成员变量。
  4.可以通过事件对象获得详细资料,比如本例中就通过事件对象获得了鼠标发生时的坐标值。
  public void mouseDragged(MouseEvent e) {
   String s="Mouse dragging :X="+e.getX()+"Y="+e.getY();
   tf.setText(s);
  }
  Java语言类的层次非常分明,因而只支持单继承,为了实现多重继承的能力,Java用接口来实现,一个类可以实现多个接口,这种机制比多重继承具有更简单、灵活、更强的功能。在AWT中就经常用到声明和实现多个接口。记住无论实现了几个接口,接口中已定义的方法必须一一实现,如果对某事件不感兴趣,可以不具体实现其方法,而用空的方法体来代替。但却必须所有方法都要写上。
7.2.4 事件适配器
  Java语言为一些Listener接口提供了适配器(Adapter)类。可以通过继承事件所对应的Adapter类,重写需要方法,无关方法不用实现。事件适配器为我们提供了一种简单的实现监听器的手段, 可以缩短程序代码。但是,由于java的单一继承机制,当需要多种监听器或此类已有父类时,就无法采用事件适配器了。
1.事件适配器--EventAdapter
  下例中采用了鼠标适配器:
  import java.awt.*;
  import java.awt.event.*;
  public class MouseClickHandler extends MouseAdaper{
    public void mouseClicked(MouseEvent e) //只实现需要的方法
       { ……}
  }
  java.awt.event包中定义的事件适配器类包括以下几个:
  1.ComponentAdapter( 组件适配器)
  2.ContainerAdapter( 容器适配器)
  3.FocusAdapter( 焦点适配器)
  4.KeyAdapter( 键盘适配器)
  5.MouseAdapter( 鼠标适配器)
  6.MouseMotionAdapter( 鼠标运动适配器)
  7.WindowAdapter( 窗口适配器)
 2. 用内部类实现事件处理
  内部类(inner class)是被定义于另一个类中的类,使用内部类的主要原因是由于:
  ◇ 一个内部类的对象可访问外部类的成员方法和变量,包括私有的成员。
  ◇ 实现事件监听器时,采用内部类、匿名类编程非常容易实现其功能。
  ◇ 编写事件驱动程序,内部类很方便。  
  因此内部类所能够应用的地方往往是在AWT的事件处理机制中。
例7.11
   import java.awt.* ;
   import java.awt.event.*;
     public class InnerClass{
       private Frame f;
       private TextField tf;
       public InnerClass(){
       f=new Frame("Inner classes example");
       tf=new TextField(30);
     }
     public voidi launchFrame(){
       Label label=new Label("Click and drag the mouse");
       f.add(label,BorderLayout.NORTH);
       f.add(tf,BorderLayout.SOUTH);
       f.addMouseMotionListener(new MyMouseMotionListener());
       f.setSize(300,200);
       f.setVisible(true);
     }
     class MyMouseMotionListener extends MouseMotionAdapter{
       public void mouseDragged(MouseEvent e) {
         String s="Mouse dragging: x="+e.getX()+"Y="+e.getY();
         tf.setText(s); }
       } ;
       public static void main(String args[]) {
         InnerClass obj=new InnerClass();
         obj.launchFrame();
       }
     }//内部类结束
    }
 3.匿名类(Anonymous Class)
  当一个内部类的类声名只是在创建此类对象时用了一次,而且要产生的新类需继承于一个已有的父类或实现一个接口,才能考虑用匿名类,由于匿名类本身无名,因此它也就不存在构造方法,它需要显示地调用一个无参的父类的构造方法,并且重写父类的方法。所谓的匿名就是该类连名字都没有,只是显示地调用一个无参的父类的构造方法。
例7.12
   import java.awt.* ;
   import java.awt.event.*;
    public class AnonymousClass{
     private Frame f;
     private TextField tf;
     public AnonymousClass(){
      f=new Frame("Inner classes example");
      tf=new TextField(30);
    }
    public void launchFrame(){
      Label label=new Label("Click and drag the mouse");
      f.add(label,BorderLayout.NORTH);
      f.add(tf,BorderLayout.SOUTH);
      f.addMouseMotionListener(new MouseMotionAdapter(){ //匿名类开始
       public void mouseDragged(MouseEvent e){
        String s="Mouse dragging: x="+e.getX()+"Y="+e.getY();
        tf.setText(s); }
      } ); //匿名类结束
      f.setSize(300,200);
      f.setVisible(true);
      }
       public static void main(String args[]) {
        AnonymousClass obj=new AnonymousClass();
        obj.launchFrame();
        }
      }
  其实大家仔细分析一下,例5.11和5.12实现的都是完全一样的功能,只不过采取的方式不同。5.11中的事件处理类是一个内部类,而5.12的事件处理类是匿名类,可以说从类的关系来说是越来越不清楚,但是程序也越来越简练。熟悉这两种方式也十分有助于大家编写图形界面的程序。
7.3 AWT组件库
  本节从应用的角度进一步介绍AWT的一些组件,目的使大家加深对AWT的理解,掌握如何用各种组件构造图形化用户界面,学会控制组件的颜色和字体。下面是一些常用的组件的介绍:
1. 按钮(Button)
  按钮是最常用的一个组件,其构造方法是:Button b = new Button("Quit");
  当按钮被点击后,会产生ActionEvent事件,需ActionListener接口进行监听和处理事件。
  ActionEvent的对象调用getActionCommand()方法可以得到按钮的标识名,缺省按钮名为label。
  用setActionCommand()可以为按钮设置组件标识符。
 2.复选框 (Checkbox)
  复选框提供简单的"on/off"开关,旁边显示文本标签。
  构造方法如下:
  setLayout(new GridLayout(3,1));
  add(new Checkbox("one",null,true));
  add(new Checkbox("two"));
  add(new Checkbox("three"));
  复选框用ItemListener 来监听ItemEvent事件,当复选框状态改变时用getStateChange()获取当前状态。使用getItem()获得被修改复选框的字符串对象。
例7.13
   class Handler implements ItemListener {
     public void itemStateChanged(ItemEvent ev){
       String state = "deselected";
       if (ev.getStateChange() = = ItemEvent.SELECTED){
         state = "selected"
       }
     System.out.println(ev.getItem()+" "+state);
     }
   }
 3.复选框组(CheckboxGroup)
  使用复选框组,可以实现单选框的功能。方法如下:
  setLayout(new GridLayout(3, 1));
  CheckboxGroup cbg = new CheckboxGroup();
  add(new Checkbox("one", cbg, true));
  add(new Checkbox("two", cbg, false));
  add(new Checkbox("three", cbg, false));
4. 下拉式菜单(Choice)
  下拉式菜单每次只能选择其中的一项,它能够节省显示空间,适用于大量选项。
  Choice Colorchooser=new Choice();
  Colorchooser.add("Green");
  Colorchooser.add("Red");
  Colorchooser.add("Blue");
  Choice 用ItemListener接口来进行监听
 5. Canvas
  一个应用程序必须继承Canvas类才能获得有用的功能,比如创建一个自定义组件。如果想在画布上完成一些图形处理,则Canvas类中的paint()方法必须被重写。
  Canvas组件监听各种鼠标,键盘事件。当在Canvas组件中输入字符时,必须先调用requestFocus()方法。
例7.14
   import java.awt.*;
   import java.awt.event.*;
   import java.util.*;
   public class MyCanvas implements KeyListener, MouseListener {
    Canvas c; //声明一个画布对象
    String s ="";
    public static void main(String args[]) {
     Frame f=new Frame("Canvas");
     MyCanvas mc=new MyCanvas();
     mc.c=new Canvas();
     f.add("Center",mc.c);
     f.setSize(150,150);
     mc.c.addMouseListener(mc); //注册监听器
     mc.c.addKeyListener(mc); //注册监听器
     f.setVisible(true);
    }
    public void mouseClicked(MouseEvent ev){
     System.out.println("MouseClicked");
     c.requestFocus();//获得焦点,表示该窗口将接收用户的键盘和鼠标输入
    }
    public void keyTyped(KeyEvent ev) {
     System.out.println("KeyTyped");
     s+=ev.getKeyChar(); //获取每个输入的字符,依次添加到字符串s中
     c.getGraphics().drawString(s,0,20); //显示字符串s
    }
    public void keyPressed(KeyEvent ev) { System.out.println("KeyPressed"); }
    public void keyReleased(KeyEvent ev) { System.out.println("KeyReleased"); }
    public void mousePressed(MouseEvent ev) {System.out.println("MousePressed"); }
    public void mouseReleased(MouseEvent ev) {System.out.println("MouseReleased"); }
    public void mouseEntered(MouseEvent ev) {System.out.println("MouseEntered"); }
    public void mouseExited(MouseEvent ev) {System.out.println("MouseExited"); }
    }
 6. 单行文本输入区(TextField)
  只能显示一行,当回车键被按下时,会发生ActionEvent事件,可以通过ActionListener中的actionPerformed()方法对事件进行相应处理。可以使用setEditable(boolean)方法设置为只读属性。
  单行文本输入区构造方法如下:
  TextField tf1,tf2,tf3,tf4:
  tf1=new TextField();
  tf2=new TextField("",20); //显示区域为20列
  tf3=new TextField("Hello!"); //按文本区域大小显示
  tf4=new TextField("Hello!",30); //初始文本为Hello!, 显示区域为30列
7. 文本输入区(TextArea)
  TextArea可以显示多行多列的文本。使用setEditable(boolean)方法,可以将其设置为只读的。在TextArea中可以显示水平或垂直的滚动条。
要判断文本是否输入完毕,可以在TextArea旁边设置一个按钮,通过按钮点击产生的ActionEvent对输入的文本进行处理。
 8. 列表(List)
  列表中提供了多个文本选项,列表支持滚动条,可以浏览多项
  List lst=new List(4,false); //两个参数分别表示显示的行数、是否允许多选
  lst.add("Venus");
  lst.add("Earth");
  lst.add("JavaSoft");
  lst.add("Mars");
  cnt.add(lst);
 9. 框架(Frame)
  Frame是顶级窗口,可以显示标题,重置大小。当Frame被关闭,将产生WindowEvent事件,Frame无法直接监听键盘输入事件。
 10. 对话框(Dialog)
  它是Window类的子类。对话框和一般窗口的区别在于它依赖于其它窗口。对话框分为非模式(non-modal)和模式(modal)两种。
 11. 文件对话框(Filedialog)
  当用户想打开或存储文件时,使用文件对话框进行操作。主要代码如下:
  FileDialog d=new FileDialog(ParentFr,"FileDialog");
  d.setVisible(true);
  String filename=d.getFile();
 12. 菜单(Menu)
  无法直接将菜单添加到容器的某一位置,也无法使用布局管理器对其加以控制。菜单只能被添加 quot;菜单容器"(MenuBar)中。
 13. MenuBar
  只能被添加到Frame对象中,作为整个菜单树的根基。
  Frame fr = new Frame("MenuBar");
  MenuBar mb = new MenuBar();
  fr.setMenuBar(mb);
  fr.setSize(150,100);
  fr.setVisible(true);
 14. Menu
  下拉菜单。它可以被添加到MenuBar中或其它Menu中。
  Frame fr = new Frame("MenuBar");
  MenuBar mb = new MenuBar();
  fr.setMenuBar(mb);
  Menu m1 = new Menu("File");
  Menu m2 = new Menu("Edit");
  Menu m3 = new Menu("Help");
  mb.add(m1);
  mb.add(m2);
  mb.setHelpMenu(m3);
  fr.setSize(200,200);
  fr.setVisible(true);
15. MenuItem
  MenuItem是菜单树中的"叶子节点"。MenuItem通常被添加到一个Menu中。对于MenuItem对象可以添加ActionListener,使其能够完成相应的操作。
  Menu m1 = new Menu("File");
  MenuItem mi1 = new MenuItem("Save");
  MenuItem mi2 = new MenuItem("Load");
  MenuItem mi3 = new MenuItem("Quit");
  m1.add(mi1);
  m1.add(mi2);
  m1.addSeparator();
  m1.add(mi3);
  MenuBar和Menu都没有必要注册监听器,只需要对MenuItem添加监听器ActionListener,完成相应操作。
 16. 组件与监听器的对应关系
  下表中列出了各个组件与所有的监听器的对应关系,打上""表明该组件可以注册此种监听器。
 表7.2
 监听器接口 Act Adj Cmp Cnt Foc Itm Key Mou MM Text Win
  Button
  Canvas
  Checkbox
CheckboxMenuItem
  Choice
  Component
  Container
  Dialog
  Frame
  Label
  List 
  MenuItem
  Panel
  Scrollbar
  ScrollPane
  TextArea
  TextField
  Window
  Act=ActionListener Adj=AdjustmentListener Cmp=ComponentListener
  Cnt=ConatainerListener Foc=FocusListener Itm=ItemListener
  Key=KeyListener Mou=MouseListener MM=MouseMotionListener
  Text=TextListener Win=WindowListener
7.4 Swing简介
7.4.1 简介
  第五讲中我们学习了AWT,AWT是Swing的基础。Swing的产生主要原因就是AWT不能满足图形化用户界面发展的需要。
AWT设计的初衷是支持开发小应用程序的简单用户界面。例如AWT缺少剪贴板、打印支持、键盘导航等特性,而且原来的AWT甚至不包括弹出式菜单或滚动窗格等基本元素。Swing是由100%纯Java实现的,Swing组件是用Java实现的轻量级( light-weight)组件,没有本地代码,不依赖操作系统的支持,这是它与AWT组件的最大区别。由于AWT组件通过与具体平台相关的对等类(Peer)实现,因此Swing比AWT组件具有更强的实用性。Swing在不同的平台上表现一致,并且有能力提供本地窗口系统不支持的其它特性。
  Swing采用了一种MVC的设计范式,即"模型-视图-控制"(Model-View-Controller),其中模型用来保存内容,视图用来显示内容,控制器用来控制用户输入。
  Swing外观感觉采用可插入的外观感觉(Pluggable Look and Feel,PL&F)
  在AWT组件中,由于控制组件外观的对等类与具体平台相关,使得AWT组件总是只有与本机相关的外观。Swing使得程序在一个平台上运行时能够有不同的外观。用户可以选择自己习惯的外观。以下三幅图是在同一个操作系统下得到不同的外观。
Metal风格
Motif风格
Windows风格
7.4.2 Swing的类层次结构
  在javax.swing包中,定义了两种类型的组件:顶层容器(JFrame,JApplet,JDialog和JWindow)和轻量级组件。Swing组件都是AWT的Container类的直接子类和间接子类。
java.ponent
    -java.awt.Container
       -java.awt.Window
          -java.awt.Frame-javax.swing.JFrame
          -javax.Dialog-javax.swing.JDialog
          -javax.swing.JWindow
       -java.awt.Applet-javax.swing.JApplet
       -javax.swing.Box
       -javax.swing.Jcomponet
  Swing包是JFC(Java Foundation Classes)的一部分,由许多包组成,如下表:
      包         描述
  Com.sum.swing.plaf.motif 用户界面代表类,它们实现Motif界面样式  
Com.sum.java.swing.plaf.windows 用户界面代表类,它们实现Windows界面样式
  Javax.swing   Swing组件和使用工具
  Javax.swing.border    Swing轻量组件的边框
  Javax.swing.colorchooser   JcolorChooser的支持类/接口
  Javax.swing.event   事件和侦听器类
  Javax.swing.filechooser   JFileChooser的支持类/接口
  Javax.swing.pending   未完全实现的Swing组件
  Javax.swing.plaf   抽象类,定义UI代表的行为
  Javax.swing.plaf.basic   实现所有标准界面样式公共功能的基类
  Javax.swing.plaf.metal 用户界面代表类,它们实现Metal界面样式
  Javax.swing.table   Jtable组件
  Javax.swing.text   支持文档的显示和编辑
  Javax.swing.text.html   支持显示和编辑HTML文档
  Javax.swing.text.html.parser   Html文档的分析器
  Javax.swing.text.rtf   支持显示和编辑RTF文件
  Javax.swing.tree   Jtree组件的支持类
  Javax.swing.undo   支持取消操作
  (在jdk1.3中,第一、第二和pending包没有了,增加了plaf.multi包,主要功能:给缺省的L&F加上附加的L&F,例如一个MultiButtonUI实例可以同时处理MotifButtonUI和AudioButtonUI.)
  swing包是Swing提供的最大包,它包含将近100个类和25个接口,几乎所有的Swing组件都在swing包中,只有JtableHeader和   JtextComponent是例外,它们分别在swing.table和swing.text中。
  swing.border包中定义了事件和事件监听器类,与AWT的event包类似。它们都包括事件类和监听器接口。
  swing.pending包包含了没有完全实现的Swing组件。
  swing.table包中主要包括了表格组建(JTable)的支持类。
  swing.tree同样是JTree的支持类。
  swing.text、swing.text.html、swing.text.html.parser和swing.text.rtf都是用于显示和编辑文档的包。
7.4.3 Swing组件的多样化
Swing是AWT的扩展,它提供了许多新的图形界面组件。Swing组件以"J"开头,除了有与AWT类似的按钮(JButton)、标签(JLabel)、复选框(JCheckBox)、菜单(JMenu)等基本组件外,还增加了一个丰富的高层组件集合,如表格(JTable)、树(JTree)。
7.4.4 MVC(Model-View-Control)体系结构
  Swing胜过AWT的主要优势在于MVC体系结构的普遍使用。在一个MVC用户界面中,存三个通讯对象:模型、视图和控件。模型是指定的逻辑表示法,视图是模型的可视化表示法,而控件则指定了如何处理用户输入。当模型发生改变时,它会通知所有依赖它的视图,视图使用控件指定其相应机制。
为了简化组件的设计工作,在Swing组件中视图和控件两部分合为一体。每个组件有一个相关的分离模型和它使用的界面(包括视图和控件)。比如,按钮JButton有一个存储其状态的分离模型ButtonModel对象。组件的模型是自动设置的,例如一般都使用JButton 而不是使用ButtonModel 对象。另外,通过Model类的子类或通过实现适当的接口,可以为组件建立自己的模型。把数据模型与组件联系起来用setModel( )方法。
  MVC是现有的编程语言中制作图形用户界面的一种通用的思想,其思路是把数据的内容本身和显示方式分离开,这样就使得数据的显示更加灵活多样。比如,某年级各个班级的学生人数是数据,则显示方式是多种多样的,可以采用柱状图显示,也可以采用饼图显示,也可以采用直接的数据输出。因此在设计的时候,就考虑把数据和显示方式分开,对于实现多种多样的显示是非常有帮助的。
7.4.5 可存取性支持
所有Swing组件都实现了Accessible接口,提供对可存取性的支持,使得辅助功能如屏幕阅读器能够十分方便的从Swing组件中得到信息。
7.4.6 支持键盘操作
在Swing组件中,使用JComponent类的registerKeyboardAction()方法,能使用户通过键盘操作来替代鼠标驱动GUI上Swing组件的相应动作。有些类还为键盘操作提供了更便利的方法。
  其实这就相当于热键,使得用户可以只用键盘进行操作。
7.4.7 设置边框
对Swing组件可以设置一个和多个边框。Swing中提供了各式各样的边框供用户选用,也能建立组合边框或自己设计边框。一种空白边框可以增大组件,协助布局管理器对容器中的组件进行合理的布局。
7.4.8 使用图标(Icon)
  与AWT的部件不同,许多Swing组件如按钮、标签,除了使用文字外,还可以使用图标修饰自己。
例7.15:
  import javax.swing.*; //引入Swing包名
             //import com.sun.java.swing.*;
             //使用JDK 1.2 Beta 4版和所有Swing 1.1 Beta 3
             //之前的版本,引入Swing包名用此方法。
  import java.awt.*;
  import java.awt.event.*;
  public class SwingApplication {
    private static String labelPrefix = "Number of button clicks: ";
    private int numClicks = 0; //计数器,计算点击次数
    public Component createComponents() {
      final JLabel label = new JLabel(labelPrefix + "0 ");
      JButton button = new JButton("I'm a Swing button!");
    button.setMnemonic(KeyEvent.VK_I); //设置按钮的热键为'I'
    button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        numClicks++;
        label.setText(labelPrefix + numClicks);
                 //显示按钮被点击的次数
      }
    });
    label.setLabelFor(button);
    
    JPanel pane = new JPanel();
    pane.setBorder(BorderFactory.createEmptyBorder(
              30, //top
              30, //left
              10, //bottom
              30) //right
              );
     pane.setLayout(new GridLayout(0, 1)); //单列多行
     pane.add(button);
     pane.add(label);
     return pane;
  }
  public static void main(String[] args) {
     try {
       UIManager.setLookAndFeel(
         UIManager.getCrossPlatformLookAndFeelClassName());
                          //设置窗口风格
     } catch (Exception e) { }
     //创建顶层容器并添加内容.
     JFrame frame = new JFrame("SwingApplication");
     SwingApplication app = new SwingApplication();
     Component contents = app.createComponents();
     frame.getContentPane().add(contents, BorderLayout.CENTER);
     //窗口设置结束,开始显示
     frame.addWindowListener(new WindowAdapter() {
                      //匿名类用于注册监听器
       public void windowClosing(WindowEvent e) {
         System.exit(0);
       }
     });
     frame.pack();
     frame.setVisible(true);
   }
  }
7.4.9 Swing程序结构简介
Swing的程序设计一般可按照下列流程进行:
  1. 引入Swing包
  2. 选择"外观和感觉"
  3. 设置顶层容器
  4. 设置按钮和标签
  5. 向容器中添加组件
  6. 在组件周围添加边界
  7. 进行事件处理
  例子7.1说明了Swing中程序设计的结构以及最基本的组件Button和Label的用法。在程序中,我们建立一个Swing风格的窗口,并在其中添加一个按钮,程序中保存一个计数器以计算按钮被点击的次数,并在每一次点击之后用一个Label显示。在这个程序中我们可以看到Swing组件的使用与AWT组件的使用基本方法一致,使用的事件处理机制也完全相同。这些在前面的AWT中已经讲过,不再赘述。
7.5 Swing组件和容器
在Swing中不但用轻量级的组件替代了AWT中的重量级的组件,而且Swing的替代组件中都包含有一些其他的特性。例如,Swing的按钮和标签可显示图标和文本,而AWT的按钮和标签只能显示文本。Swing中的大多数组件都是AWT组件名前面加了一个"J"。
7.5.1 组件的分类
Jcomponent是一个抽象类,用于定义所有子类组件的一般方法,其类层次结构如下所示:
   java.lang.Object
       |
       +--java.ponent
            |
            +--java.awt.Container
                |
                +--javax.swing.JComponent
  并不是所有的Swing组件都继承于JComponent类,JComponent类继承于Container类,所以凡是此类的组件都可作为容器使用。
  组件从功能上分可分为:
  1) 顶层容器:JFrame,JApplet,JDialog,JWindow共4个
  2) 中间容器:JPanel,JScrollPane,JSplitPane,JToolBar 
  3) 特殊容器:在GUI上起特殊作用的中间层,如JInternalFrame,JLayeredPane,JRootPane.
  4) 基本控件:实现人际交互的组件,如Jbutton, JComboBox, JList, JMenu, JSlider, JtextField。
  5) 不可编辑信息的显示:向用户显示不可编辑信息的组件,例如JLabel, JProgressBar, ToolTip。
  6) 可编辑信息的显示:向用户显示能被编辑的格式化信息的组件,如JColorChooser, JFileChoose, JFileChooser, Jtable, JtextArea。
  JComponent类的特殊功能又分为:
  1) 边框设置:使用setBorder()方法可以设置组件外围的边框,使用一个EmptyBorder对象能在组件周围留出空白。
2) 双缓冲区:使用双缓冲技术能改进频繁变化的组件的显示效果。与AWT组件不同,JComponent组件默认双缓冲区,不必自己重写代码。如果想关闭双缓冲区,可以在组件上施加setDoubleBuffered(false)方法。
3) 提示信息:使用setTooltipText()方法,为组件设置对用户有帮助的提示信息。
4) 键盘导航:使用registerKeyboardAction( ) 方法,能使用户用键盘代替鼠标来驱动组件。JComponent类的子类AbstractButton还提供了便利的方法--用setMnemonic( )方法指明一个字符,通过这个字符和一个当前L&F的特殊修饰共同激活按钮动作。
5) 可插入L&F:每个Jcomponent对象有一个相应的ComponentUI对象,为它完成所有的绘画、事件处理、决定尺寸大小等工作。 ComponentUI对象依赖当前使用的L&F,用UIManager.setLookAndFeel( )方法可以设置需要的L&F.
6) 支持布局:通过设置组件最大、最小、推荐尺寸的方法和设置X、Y对齐参数值的方法能指定布局管理器的约束条件,为布局提供支持。
7.5.2 使用Swing的基本规则
与AWT组件不同,Swing组件不能直接添加到顶层容器中,它必须添加到一个与Swing顶层容器相关联的内容面板(content pane)上。内容面板是顶层容器包含的一个普通容器,它是一个轻量级组件。基本规则如下:
  (1)把Swing组件放入一个顶层Swing容器的内容面板上
  (2)避免使用非Swing的重量级组件。
        
  对JFrame添加组件有两种方式:
  1) 用getContentPane( )方法获得JFrame的内容面板,再对其加入组件:frame.getContentPane().add(childComponent)
  2) 建立一个Jpanel或 JDesktopPane之类的中间容器,把组件添加到容器中,用setContentPane()方法把该容器置为JFrame的内容面板:
    Jpanel contentPane=new Jpanel( );
    ……//把其它组件添加到Jpanel中;
    frame.setContentPane(contentPane);
    //把contentPane对象设置成为frame的内容面板
7.5.3 各种容器面板和组件
 1. 根面板(JRootPane)
        
 根面板由一个玻璃面板(glassPane)、一个内容面板(contentPane)和一个可选择的菜单条(JMenuBar)组成,而内容面板和可选择的菜单条放在同一分层。玻璃面板是完全透明的,缺省值为不可见,为接收鼠标事件和在所有组件上绘图提供方便。
  根面板提供的方法:
  Container getContentPane(); //获得内容面板
  setContentPane(Container); //设置内容面
  JMenuBar getMenuBar( ); //活动菜单条
  setMenuBar(JMenuBar); //设置菜单条
  JLayeredPane getLayeredPane(); //获得分层面板
  setLayeredPane(JLayeredPane); //设置分层面板
  Component getGlassPane(); //获得玻璃面板
  setGlassPane(Component); //设置玻璃面板
2 分层面板(JLayeredPane)
 Swing提供两种分层面板:JlayeredPane和JDesktopPane。 JDesktopPane是JLayeredPane的子类,专门为容纳内部框架(JInternalFrame)而设置。
向一个分层面板种添加组件,需要说明将其加入哪一层,指明组件在该层中的位置:add(Component c, Integer Layer, int position)。
3 面板(JPanel)
面板(JPanel)是一个轻量容器组件,用法与Panel相同,用于容纳界面元素,以便在布局管理器的设置下可容纳更多的组件,实现容器的嵌套。Jpanel, JscrollPane, JsplitPane, JinteralFrame都属于常用的中间容器,是轻量组件。Jpanel的缺省布局管理器是FlowLayout。
  java.lang.Object
     |
     +--java.ponent
         |
         +--java.awt.Container
             |
             +--javax.swing.JComponent
                  |
                  +--javax.swing.Jpanel
4 滚动窗口(JScrollPane)
     
  JscrollPane是带滚动条的面板,主要是通过移动JViewport(视口)来实现的。JViewport是一种特殊的对象,用于查看基层组件,滚动条实际就是沿着组件移动视口,同时描绘出它在下面"看到"的内容。
5 分隔板(JSplitPane)
java.lang.Object
     |
     +--java.ponent
         |
         +--java.awt.Container
             |
             +--javax.swing.JComponent
                  |
                  +--javax.swing.JSplitPane
   
      
  JSplitPane提供可拆分窗口,支持水平拆分和垂直拆分并带有滑动条。常用方法有:
  addImpl(Component comp,Object constraints,int index)//增加指定的组件
  setTopComponent(Component comp) //设置顶部的组件
  setDividerSize(int newSize) //设置拆分的大小
  setUI(SplitPaneUI ui) //设置外观和感觉
6 选项板(JTabbedPane)
  JTabbedPane提供一组可供用户选择的带有标签或图标的开关键。常用方法:
  add(String title,Component component) //增加一个带特定标签的组件
  addChangeListener(ChangeListener l) //选项板注册一个变化监听器
7 工具栏(JToolBar)
                工具栏在左上角
    
                工具栏在右侧
  JtoolBar是用于显示常用工具控件的容器。用户可以拖拽出一个独立的可显示工具控件的窗口。
  常用方法有:
       JToolBar(String name) //构造方法
       getComponentIndex(Component c) //返回一个组件的序号
       getComponentAtIndex(int i) //得到一个指定序号的组件
.8 内部框架(JInternalFrame)
   
  内部框架JInternalFrame就如同一个窗口在另一个窗口内部,其特点如下:
  1) 必须把内部框架添加到一个容器中(通常为JDesktopPane),否则不显示;
  2) 不必调用show()或setVisible()方法,内部框架随所在的容器一起显示;
  3) 必须用setSize()或pack()或setBounds方法设置框架尺寸,否则尺寸为零,框架不能显示;
  4) 可以用setLocation()或setBounds( ) 方法设置内部框架在容器中的位置,缺省值为0,0,即容器的左上角;
  5) 象顶层JFrame一样,对内部框架添加组件也要加在它的内容面板上;
  6) 在内部框架中建立对话框,不能使用JDialog作为顶层窗口,必须用JOptionPane或JInternalFrame;
  7) 内部框架不能监听窗口事件,可以通过监听与窗口事件类似的内部框架(JInternalFrameEvent)处理内部框架窗口的操作。
  JFrame frame=new JFrame("InternalFrameDemo"); //实例化窗口
  JDesktopPane desktop=new JDesktopPane(); //实例化容器JDesktopPane
  MyInternalFrame myframe=new MyInternalFrame(); //实例化内部窗口
  desktop.add(myframe); //把内部窗口添加到容器中
  myframe.setSelected(true); //内部面板是可选择的
  frame.setContentPane(desktop); //把desktop设为frame的内容面板
9 按钮(JButton)
按钮是一个常用组件,按钮可以带标签或图象。     
  java.lang.Object
     |
     +--java.ponent
         |
         +--java.awt.Container
             |
             +--javax.swing.JComponent
                   |
                   +--javax.swing.AbstractButton
                        |
                        +--javax.swing.JButton
  常用的构造方法有:
  JButton(Icon icon) //按钮上显示图标
  JButton(String text) //按钮上显示字符
  JButton(String text, Icon icon) //按钮上既显示图标又显示字符
例.16
  public class ButtonDemo extends Jpanel implements ActionListener{
     JButton b1,b2,b3;
     public ButtonDemo() {
       super();
       ImageIcon leftButtonIcon=new ImageIcon("images/right.gif);
                    //显示在左按钮上的图标
       ImageIcon middleButtonIcon=new ImageIcon("images/middle.gif);
                    //显示在中间按钮上的图标
       ImageIcon middleButtonIcon=new ImageIcon("images/left.gif);
                    //显示在右按钮上的图标
       b1=new JButton("Disable middle button",leftButtonIcon);
                   //按钮b1上同时显示文字和图标
       b1.setVerticalTextPosition(AbstractButton.CENTER);
              //按钮b1上的文字在垂直方向上是居中对齐
       b1.setHorizontalTextPosition(AbstractButton.LEFT);
             //按钮b1上的文字在水平居方向上是居左对齐
       b1.setMnemonic('d');  //设置按钮b1的替代的键盘按键是'd'
       b1.setActionCommand("diaable");
       ……
    }
  }
   
10 复选框(JCheckBox)
  复选框提供简单的"on/off"开关,旁边显示文本标签。
11 单选框(JRadioButton)
   
  单选框JRadioButton与AWT中的复选框组功能类似。
12 选择框(JComboBox)
JComboBox每次只能选择其中的一项,但是可编辑每项的内容,而且每项的内容可以是任意类,而不再局限于String。    
       
13 文件选择器(JFileChooser)
 JFileChooser内建有"打开","存储"两种对话框,还可以自己定义其他种类的对话框。
14 标签(JLabel)
               提供可带图形的标签
15 列表(List)
  
  适用于数量较多的选项以列表形式显示,里面的项目可以由任意类型对象构成。支持单选和多选。
16 菜单(JMenu)
JMenu与AWT的菜单Menu的不同之处是它可以通过setJMenuBar(menubar)将菜单放置到容器中的任意地方。
17 进程条(JProgressBar)
  进程条是提供一个直观的图形化的进度描述,从"空"到"满"的过程。
18 滑动条(JSlider)
  滑动条使得用户能够通过一个滑块的来回移动来输入数据。
.19 表格(JTable)
表格是Swing新增加的组件,主要功能是把数据以二维表格的形式显示出来。使用表格,依据M-V-C的思想,最好先生成一个MyTableModel类型的对象来表示数据,这个类是从AbstractTableModel类中继承来的,其中有几个方法是一定要重写,例如getColumnCount,getRowCount,getColumnName,getValueAt。因为Jtable会从这个对象中自动获取表格显示所必需的数据,AbstractTableModel类的对象负责表格大小的确定(行、列)、内容的填写、赋值、表格单元更新的检测等等一切跟表格内容有关的属性及其操作。JTable类生成的对象以该TableModel为参数,并负责将TableModel对象中的数据以表格的形式显示出来。
  JTable类常用的方法有:
  getModel() //获得表格的数据来源对象
  JTable(TableModel dm) //dm对象中包含了表格要显示的数据
  //下列两个构造方法,第一个参数是数据,第二个参数是表格第一行中显示的内容
  JTable(object[][]rowData,object[]columnNams);
  JTable(Vector[][]rowData,Vector[]columnNams);
 例7.17 RecorderOfWorkers
  import javax.swing.JTable;
  import javax.swing.table.AbstractTableModel;
  import javax.swing.JScrollPane;
  import javax.swing.JFrame;
  import javax.swing.SwingUtilities;
  import javax.swing.JOptionPane;
  import java.awt.*;
  import java.awt.event.*;
  public class TableDemo extends JFrame {
    private boolean DEBUG = true;
    public TableDemo() { //实现构造方法
      super("RecorderOfWorkers"); //首先调用父类JFrame的构造方法生成一个窗口
      MyTableModel myModel = new MyTableModel();//myModel存放表格的数据
      JTable table = new JTable(myModel);//表格对象table的数据来源是myModel对象
      table.setPreferredScrollableViewportSize(new Dimension(500, 70));//表格的显示尺寸
      //产生一个带滚动条的面板
      JScrollPane scrollPane = new JScrollPane(table);
      //将带滚动条的面板添加入窗口中
      getContentPane().add(scrollPane, BorderLayout.CENTER);
      addWindowListener(new WindowAdapter() {//注册窗口监听器
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
  }
        //把要显示在表格中的数据存入字符串数组和Object数组中
  class MyTableModel extends AbstractTableModel {
     //表格中第一行所要显示的内容存放在字符串数组columnNames中
      final String[] columnNames = {"First Name",
                  "Position",
                  "Telephone",
                  "MonthlyPay",
                  "Married"};
     //表格中各行的内容保存在二维数组data中
      final Object[][] data = {
        {"Wangdong", "Executive",
        "01068790231", new Integer(5000), new Boolean(false)},
        {"LiHong", "Secretary",
        "01069785321", new Integer(3500), new Boolean(true)},
        {"LiRui", "Manager",
        "01065498732", new Integer(4500), new Boolean(false)},
        {"ZhaoXin", "Safeguard",
        "01062796879", new Integer(2000), new Boolean(true)},
        {"ChenLei", "Salesman",
        "01063541298", new Integer(4000), new Boolean(false)}
      };
      //下述方法是重写AbstractTableModel中的方法,其主要用途是被JTable对象调用,以便在表格中正确的显示出来。程序员必须根据采用的数据类型加以恰当实现。
      //获得列的数目
      public int getColumnCount() {
         return columnNames.length;
      }
      //获得行的数目
      public int getRowCount() {
         return data.length;
      }
      //获得某列的名字,而目前各列的名字保存在字符串数组columnNames中
      public String getColumnName(int col) {
         return columnNames[col];
      }
      //获得某行某列的数据,而数据保存在对象数组data中
      public Object getValueAt(int row, int col) {
         return data[row][col];
      }
      //判断每个单元格的类型
      public Class getColumnClass(int c) {
         return getValueAt(0, c).getClass();
      }
      //将表格声明为可编辑的
      public boolean isCellEditable(int row, int col) {
         if (col < 2) {
           return false;
         } else {
           return true;
         }
      }
      //改变某个数据的值
      public void setValueAt(Object value, int row, int col) {
         if (DEBUG) {
           System.out.println("Setting value at " + row + ",
                 " + col
                  + " to " + value
                  + " (an instance of "
                  + value.getClass() + ")");
         }
         if (data[0][col] instanceof Integer
             && !(value instanceof Integer)) {
          try {
             data[row][col] = new Integer(value.toString());
             fireTableCellUpdated(row, col);
          } catch (NumberFormatException e) {
             JOptionPane.showMessageDialog(TableDemo.this,
              "The \"" + getColumnName(col)
              + "\" column accepts only integer values.");
          }
      } else {
          data[row][col] = value;
          fireTableCellUpdated(row, col);
      }
      if (DEBUG) {
          System.out.println("New value of data:");
          printDebugData();
      }
   }
   private void printDebugData() {
     int numRows = getRowCount();
      int numCols = getColumnCount();
      for (int i=0; i < numRows; i++) {
        System.out.print(" row " + i + ":");
        for (int j=0; j < numCols; j++) {
          System.out.print(" " + data[i][j]);
        }
        System.out.println();
      }
      System.out.println("--------------------------");
   }
  }
  public static void main(String[] args) {
   TableDemo frame = new TableDemo();
   frame.pack();
   frame.setVisible(true);
  }
 }
20 树(JTree)
如果要显示一个层次关系分明的一组数据,用树状图表示能给用户一个直观而易用的感觉,JTree类如同Windows的资源管理器的左半部,通过点击可以"打开"、"关闭"文件夹,展开树状结构的图表数据。JTree也是依据M-V-C的思想来设计的,Jtree的主要功能是把数据按照树状进行显示,其数据来源于其它对象,其显示效果通常如下图所示:
              
  下面是一棵包含六个分枝点的树的例子,来演示JTree的实现过程。
  import java.awt.*;
  import java.awt.event.*;
  import javax.swing.*;
  import javax.swing.tree.*;
  class Branch{
     DefaultMutableTreeNode r;
//DefaultMutableTreeNode是树的数据结构中的通用节点,节点也可以有多个子节点。
    public Branch(String[] data){
       r=new DefaultMutableTreeNode(data[0]);
       for(int i=1;i       r.add(new DefaultMutableTreeNode(data[i]));
        //给节点r添加多个子节点
    }
    public DefaultMutableTreeNode node(){//返回节点
       return r;
    }
  }
  public class Trees extends JPanel{
    String [][]data={
            {"Colors","Red","Blue","Green"},
            {"Flavors","Tart","Sweet","Bland"},
            {"Length","Short","Medium","Long"},
            {"Volume","High","Medium","Low"},
            {"Temperature","High","Medium","Low"},
            {"Intensity","High","Medium","Low"}
            };
    static int i=0; //I用于统计按钮点击的次数
    DefaultMutableTreeNode root,child,chosen;
    JTree tree;
    DefaultTreeModel model;
    public Trees(){
       setLayout(new BorderLayout());
       root=new DefaultMutableTreeNode("root");
       //根节点进行初始化
       tree=new JTree(root);
       //树进行初始化,其数据来源是root对象
       add(new JScrollPane(tree));
       //把滚动面板添加到Trees中
       model=(DefaultTreeModel)tree.getModel();
       //获得数据对象DefaultTreeModel
       JButton test=new JButton("Press me");
       //按钮test进行初始化
       test.addActionListener(new ActionListener(){
       //按钮test注册监听器
          public void actionPerformed(ActionEvent e){
          if (i          //按钮test点击的次数小于data的长度
              child=new Branch(data[i++]).node();
              //生成子节点
     第二章:JAVA语言基础
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: java中的数据类型有简单数据类型和复合数据类型两种,其中简单数据类型包括整数类型、浮点类型、字符类型和布尔类型;复合数据类型包含类、接口和数组。表达式是由运算符和操作数组成的符号序列,对一个表达式进行运算时,要按运算符的优先顺序从高向低进行,同级的运算符则按从左到右的方向进行。条件语句、循环语句和跳转语句是java中常用的控制语句。  数组是最简单的复合数据类型,数组是有序数据的集合,数组中的每个元素具有相同的数据类型,可以用一个统一的数组名和下标来唯一地确定数组中的元素。Java中,对数组定义时并不为数组元素分配内存,只有初始化后,才为数组中的每一个元素分配空间。已定义的数组必须经过初始化后,才可以引用。数组的初始化分为静态初始化和动态初始化两种,其中对复合数据类型数组动态初始化时,必须经过两步空间分配:首先,为数组开辟每个元素的引用空间;然后,再为每个数组元素开辟空间。
下载一
第二章 JAVA语言基础
【课前思考】
  1. Java中的标识符是由哪些字符组成的?
  2. Java中有哪些保留字?简单数据类型包含哪几种?各种数据类型变量的定义方法和常量的表示方法及取值范围。
  3. Java 中各简单数据类型间的优先次序和自动转换规则是什么?
    各数据类型间在什么情况下,进行自动转换,在什么情况下使用强制转换?
  4. Java中有哪些运算符?这些运算符的优先关系是怎样的?
  5. Java 中有哪些控制语句?你了解每一种控制语句的语法规则吗?
  6. Java 中的一维数组和多维数组在数组动态初始化和静态初始化时有何不同?
  【学习目标】
  本讲主要讲述java编程语言的基本语法知识,如java 中的简单数据类型,运算符和表达式,控制语句及数组。通过本讲的学习,同学们可以编写简单的java程序。
【学习指南】
  同任何一种编程语言一样,应深刻理解各知识点的概念,牢记一些java的语法,从而达到学习的目的。
【难 重 点】
 重点:
  1. 深刻理解各知识点的概念,并熟记java 的语法规范。
  2. 熟练使用各种数据类型的定义,表示和引用。
  3. 能熟练使用各种控制语句。
 难点:
  1. 动态初始化复杂类型数组时,要先为数组中的元素开辟引用空间,再为每个元素开辟空间。
【知 识 点】
  2.1 简单数据类型
   2.1.1 标识符和保留字
   2.1.2 数据类型概述
   2.1.3 简单数据类型
   2.1.4 简单数据类型中各类型数据间的优先关系和相互转换
  2.2 运算符和表达式
   2.2.1 运算符
   2.2.2 表达式
  2.3 控制语句
   2.3.1 分支语句
   2.3.2 循环语句
   2.3.3 跳转语句
   2.3.4 例外处理语句
  2.4 数组
   2.4.1 一维数组
   2.4.2 多维数组
2.1简单数据类型
2.1.1 标识符和保留字
  1.标识符 ( G:\study\java\java_qh\JAVA\text\ch02\se01\right2_1_1.htm" \l "01" \t "MyFrame )
程序员对程序中的各个元素加以命名时使用的命名记号称为标识符(identifier)。Java语言中,标识符是以字母,下划线(_),美元符($)开始的一个字符序列,后面可以跟字母,下划线,美元符,数字。例如,identifier,userName,User_Name,_sys_val, $change为合法的标识符,而2mail room#,class 为非法的标识符。
  2.保留字 ( G:\study\java\java_qh\JAVA\text\ch02\se01\right2_1_1.htm" \l "02" \t "MyFrame )
具有专门的意义和用途,不能当作一般的标识符使用,这些标识符称为保留字(reserved word),也称为关键字,下面列出了java语言中的所有保留字:
bstract,break,byte,boolean,catch,case,class,char,continue,default,double,do,else,extends,false,final,float,for,finally,if,import,implements,int,interface,instanceof,long,length,native,new,null,package,private,protected,public,return,switch,synchronized,short,static,super,try,true,this,throw,throws,threadsafe,transient,void,while 。
  java语言中的保留字均用小写字母表示。
2.1.2 数据类型概述
1. java中的数据类型划分
  java语言的数据类型有简单类型和复合类型:
  简单数据类型包括:
     整数类型(Integer):byte, short, int, long
     浮点类型(Floating):float,double
     字符类型(Textual):char
     布尔类型(Logical):boolean
  复合数据类型包括:
     class
     interface
     数组
 2.常量和变量
  常量:用保留字final来实现
     final typeSpecifier varName=value[,varName[=value]…];
                     如:final int NUM=100;
  变量:是java 程序中的基本存储单元,它的定义包括变量名、变量类型和作用域几个部分。其定义格式如下:
     typeSpecifier varName[=value[,varName[=value]…];
                 如:int count; char c='a';
  变量的作用域指明可访问该变量的一段代码,声明一个变量的同时也就指明了变量的作用域。按作用域来分,变量可以有下面几种:局部变量、类变量、方法参数和例外处理参数。在一个确定的域中,变量名应该是唯一的。局部变量在方法或方法的一个块代码中声明,它的作用域为它所在的代码块(整个方法或方法中的某块代码)。类变量在类中声明,而不是在类的某个方法中声明,它的作用域是整个类。方法参数传递给方法,它的作用域就是这个方法。例外处理参数传递给例外处理代码,它的作用域就是例外处理部分。
2.1.3 简单数据类型
1.布尔类型--boolean
  布尔型数据只有两个值true和false,且它们不对应于任何整数值。布尔型变量的定义如:
   boolean b=true;
 2.字符类型--char
  字符常量:
  字符常量是用单引号括起来的一个字符,如'a','A';
  字符型变量:
  类型为char,它在机器中占16位,其范围为0~65535。字符型变量的定义如:
   char c='a';
 3.整型数据
  整型常量:
  ◇ 十进制整数
    如123,-456,0
  ◇ 八进制整数
    以0开头,如0123表示十进制数83,-011表示十进制数-9。
  ◇ 十六进制整数
    以0x或0X开头,如0x123表示十进制数291,-0X12表示十进制数-18。
  整型变量:
数据类型 所占位数 数的范围
byte 8 -27~27-1
bhort 16 -215~215-1
int 32 -231~231-1
long 64 -263~263-1
 4.浮点型(实型)数据
  实型常量:
  ◇ 十进制数形式
    由数字和小数点组成,且必须有小数点,如0.123, 1.23, 123.0
  ◇ 科学计数法形式
    如:123e3或123E3,其中e或E之前必须有数字,且e或E后面的指数必须为整数。
  ◇ float型的值,必须在数字后加f或F,如1.23f。
  实型变量:
数据类型 所占位数 数的范围
float 32 3.4e-038 ~3.4e+038
double 64 1.7e-038 ~1.7e+038
 5.简单数据类型的例子:
【例2.1】
  public class Assign {
   public static void main (String args [ ] ) {
   int x , y ; //定义x,y两个整型变量
   float z = 1.234f ; //指定变量z为float型,且赋初值为1.234
   double w = 1.234 ; //指定变量w为double型,且赋初值为1.234
   boolean flag = true ; //指定变量flag为boolean型,且赋初值为true
   char c ; //定义字符型变量c
   String str ; //定义字符串变量str
   String str1 = " Hi " ; //指定变量str1为String型,且赋初值为Hi
   c = ' A ' ; //给字符型变量c赋值'A'
   str = " bye " ; //给字符串变量str赋值"bye"
   x = 12 ; //给整型变量x赋值为12
   y = 300; //给整型变量y赋值为300
   }
  }
2.1.4 简单数据类型中各类型数据间的优先关系和相互转换
1.不同类型数据间的优先关系如下:
   低------------------------------------------->高
   byte,short,char-> int -> long -> float -> double
 2.自动类型转换规则
  整型,实型,字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型,然后进行运算,转换从低级到高级;
操作数1类型 操作数2类型 转换后的类型
byte、short、char    int     int
byte、short、char、int    long     long
byte、short、char、int、long    float     float
byte、short、char、int、long、float    double     double
 3.强制类型转换
  高级数据要转换成低级数据,需用到强制类型转换,如:
  int i;
  byte b=(byte)i;
2.2 运算符和表达式
2.1运算符
对各种类型的数据进行加工的过程成为运算,表示各种不同运算的符号称为运算符,参与运算的数据称为操作数,按操作数的数目来分,可有:
  ◇ 一元运算符:++,--,+,-
  ◇ 二元运算符:+,-,>
  ◇ 三元运算符:?:
  基本的运算符按功能划分,有下面几类:
  1 算术运算符: +,―,*,/,%,++,――。
   例如:
    3+2;
    a-b;
    i++;
    --i;
  2 关系运算符: >,<,>=,<=,==,!=。
   例如:
    count>3;
    I==0;
    n!=-1;
  3 布尔逻辑运算符: !,&&,|| 。
   例如:
   flag=true;
   !(flag);
   flag&&false;
  4 位运算符: >>,<<,>>>,&,|,^,~。
   例如:
   a=10011101; b=00111001;则有如下结果:
   a<<3 =11101000;
   a>>3 =11110011 a>>>3=00010011;
   a&b=00011001; a|b=10111101;
   ~a=01100010; a^b=10100100;
  5 赋值运算符 =,及其扩展赋值运算符如+=,―=,*=,/=等。
   例如:
   i=3;
   i+=3;     //等效于i=i+3;
  6 条件运算符 :
   例如:result=(sum= =0 1 : num/sum);
  7 其它:
   包括分量运算符· ,下标运算符 [],实例运算符instanceof,内存分配运算符new,强制类型转换运算符 (类型),方法调用运算符 () 等。例如:
  System.out.println("hello world");
  int array1[]=new int[4];
2.2.2 表达式
表达式是由操作数和运算符按一定的语法形式组成的符号序列。一个常量或一个变量名字是最简单的表达式,其值即该常量或变量的值;表达式的值还可以用作其他运算的操作数,形成更复杂的表达式。
  1.表达式的类型
  表达式的类型由运算以及参与运算的操作数的类型决定,可以是简单类型,也可以是复合类型:
  布尔型表达式: x&&y||z;
  整型表达式: num1+num2;
  2.运算符的优先次序
   表达式的运算按照运算符的优先顺序从高到低进行,同级运算符从左到右进行:
优先次序 运算符
1 . [] ()
2 ++ -- ! ~ instanceof
3 new (type)
4 * / %
5 + -
6 >> >>> <<
7 > < >= <=
8 = = !=
9 &
10 ^
11 |
12 &&
13 ||
14 :
15 = += -= *= /= %= ^=
16 &= |= <<= >>= >>>=
  例如,下述条件语句分四步完成:
  Result=sum==0 1:num/sum;
  第1步:result=sum==0 1:(num/sum)
  第2步:result=(sum==0) 1:(num/sum)
  第3步:result=((sum==0) 1:(num/sum))
  第4步:result=
2.3控制语句
2.3.1分支语句
分支语句提供了一种控制机制,使得程序的执行可以跳过某些语句不执行,而转去执行特定的语句。
  1.条件语句 if-else
   if(boolean-expression)
    statement1;
   [else statement2;]
  2.多分支语句 switch
   switch (expression){
    case value1 : statement1;
   break;
    case value2 : statement2;
   break;
   …………
    case valueN : statemendN;
   break;
    [default : defaultStatement; ]
   }
  ◇ 表达式expression的返回值类型必须是这几种类型之一:int,byte,char,short。
  ◇ case子句中的值valueN必须是常量,而且所有case子句中的值应是不同的。
  ◇ default子句是可选的。
  ◇break语句用来在执行完一个case分支后,使程序跳出switch语句,即终止switch语句的执行(在一些特殊情况下,多个不同的case值要执行一组相同的操作,这时可以不用break)。
2.3.2 循环语句
 1.while语句
  [initialization]
  while (termination){
    body;
  [iteration;]
  }
 2.do-while语句
  [initialization]
  do {
    body;
  [iteration;]
  } while (termination);
 3.for语句
  for (initialization; termination; iteration){
    body;
  }
  ◇ for语句执行时,首先执行初始化操作,然后判断终止条件是否满足,如果满足,则执行循环体中的语句,最后执行迭代部分。完成一次循环后,重新判断终止条件。
  ◇ 初始化、终止以及迭代部分都可以为空语句(但分号不能省),三者均为空的时候,相当于一个无限循环。
  ◇ 在初始化部分和迭代部分可以使用逗号语句,来进行多个操作。逗号语句是用逗号分隔的语句序列。
   for( i=0, j=10; i    ……
   }
2.3.3 跳转语句
1.break语句
  ◇ 在switch语中,break语句用来终止switch语句的执行。使程序从switch语句后的第一个语句开始执行。
  ◇ 在Java中,可以为每个代码块加一个括号,一个代码块通常是用大括号{}括起来的一段代码。加标号的格式如下:
  BlockLabel: { codeBlock }
  break语句的第二种使用情况就是跳出它所指定的块,并从紧跟该块的第一条语句处执行。例如:
  break BlockLabel;
  break语句
  a:{…… //标记代码块a
  b:{…… //标记代码块b
  c:{…… //标记代码块c
  break b;
   …… //此处的语句块不被执行
  }
   …… /此处的语句块不被执行
  }
   …… //从此处开始执行
  }
 2.continue语句
  continue语句用来结束本次循环,跳过循环体中下面尚未执行的语句,接着进行终止条件的判断,以决定是否继续循环。对于for语句,在进行终止条件的判断前,还要先执行迭代语句。它的格式为:
   continue;
  也可以用continue跳转到括号指明的外层循环中,这时的格式为
   continue outerLable;
  例如:
   outer: for( int i=0; i<10; i++ ){ //外层循环
   inner: for( int j=0; j<10; j++ ){ //内层循环
   if( i     ……
   continue outer;
}
     ……
   }
     ……
   }
 3.返回语句return
   
  return语句从当前方法中退出,返回到调用该方法的语句处,并从紧跟该语句的下一条语句继续程序的执行。返回语句有两种格式:
  return expression ;
  return;
  return语句通常用在一个方法体的最后,否则会产生编译错误,除非用在if-else语句中
2.4 数组
2.4.1 一维数组的定义
1. 一维数组的定义
  type arrayName[ ];
  类型(type)可以为Java中任意的数据类型,包括简单类型和复合类型。
  例如:
   int intArray[ ];
   Date dateArray[];
 2.一维数组的初始化
  ◇ 静态初始化
    int intArray[]={1,2,3,4};
    String stringArray[]={"abc", "How", "you"};
  ◇ 动态初始化
    1)简单类型的数组
    int intArray[];
    intArray = new int[5];
   2)复合类型的数组
    String stringArray[ ];
    String stringArray = new String[3];
    stringArray[0]= new String("How");//为第一个数组元素开辟空间
    stringArray[1]= new String("are");//为第二个数组元素开辟空间
    stringArray[2]= new String("you");// 为第三个数组元素开辟空间
 3.一维数组元素的引用
  数组元素的引用方式为:
     arrayName[index]
  index为数组下标,它可以为整型常数或表达式,下标从0开始。每个数组都有一个属性length指明它的长度,例如:intArray.length指明数组intArray的长度。
2.4.2 多维数组
1.二维数组的定义
  type arrayName[ ][ ];
  type [ ][ ]arrayName;
 2.二维数组的初始化
  ◇ 静态初始化
  int intArray[ ][ ]={{1,2},{2,3},{3,4,5}};
  Java语言中,由于把二维数组看作是数组的数组,数组空间不是连续分配的,所以不要求二维数组每一维的大小相同。
  ◇ 动态初始化
  1) 直接为每一维分配空间,格式如下:
  arrayName = new type[arrayLength1][arrayLength2];
  int a[ ][ ] = new int[2][3];
  2) 从最高维开始,分别为每一维分配空间:
  arrayName = new type[arrayLength1][ ];
  arrayName[0] = new type[arrayLength20];
  arrayName[1] = new type[arrayLength21];
  …
  arrayName[arrayLength1-1] = new type[arrayLength2n];
  3) 例:
  二维简单数据类型数组的动态初始化如下,
  int a[ ][ ] = new int[2][ ];
  a[0] = new int[3];
  a[1] = new int[5];
  对二维复合数据类型的数组,必须首先为最高维分配引用空间,然后再顺次为低维分配空间。
  而且,必须为每个数组元素单独分配空间。
  例如:
  String s[ ][ ] = new String[2][ ];
  s[0]= new String[2];//为最高维分配引用空间
  s[1]= new String[2]; //为最高维分配引用空间
  s[0][0]= new String("Good");// 为每个数组元素单独分配空间
  s[0][1]= new String("Luck");// 为每个数组元素单独分配空间
  s[1][0]= new String("to");// 为每个数组元素单独分配空间
  s[1][1]= new String("You");// 为每个数组元素单独分配空间
 3.二维数组元素的引用
  对二维数组中的每个元素,引用方式为:arrayName[index1][index2]
  例如: num[1][0];
 4.二维数组举例:
【例2.2】两个矩阵相乘
  public class MatrixMultiply{
   public static void main(String args[]){
   int i,j,k;
   int a[][]=new int [2][3]; //动态初始化一个二维数组
   int b[][]={{1,5,2,8},{5,9,10,-3},{2,7,-5,-18}};//静态初始化
                           一个二维数组
   int c[][]=new int[2][4]; //动态初始化一个二维数组
   for (i=0;i<2;i++)
     for (j=0; j<3 ;j++)
      a[i][j]=(i+1)*(j+2);
   for (i=0;i<2;i++){
     for (j=0;j<4;j++){
      c[i][j]=0;
   for(k=0;k<3;k++)
     c[i][j]+=a[i][k]*b[k][j];
      }
     }
   System.out.println("*******Matrix C********");//打印Matrix C标记
   for(i=0;i<2;i++){
     for (j=0;j<4;j++)
      System.out.println(c[i][j]+" ");
     System.out.println();
      }
     }
   }第六章 异常处理
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: 异常处理是java语言中一个独特之处,主要使用捕获异常和声明抛弃异常两种方法来处理程序中可能出现异常的语句块,其中捕获异常的方法是一种积极地处理异常的方法,而声明抛弃异常是一种消极的处理异常的方法。
下载一
【课前思考】
  1. 什么是异常?Java中有哪两种异常处理机制?
【学习目标】
  本讲主要讲述了java语言中的输入/输出的处理和独特的异常处理机制,通过本讲的学习,同学们可以编写更为完善的java程序。
【学习指南】
  仔细阅读本章各知识点的内容, 深刻理解 java 语言中独特的异常处理机制,掌握处理问题的方法,多练习,多上机。
【难 重 点】
 重点:
  1. 在编写程序时,要正确地使用捕获异常和声明抛弃异常的两种异常处理的方法。
 难点:
  1.如何使用Java中两种异常处理机制,抛弃异常和声明抛弃异常的区别与联系。
 【知 识 点】
  6.1 什么是异常
   6.1.1 异常示例
   6.1.2 异常处理机制
   6.1.3 异常类的层次
  6.2 异常的处理
   6.2.1 捕获异常
   6.2.2 声明抛弃异常
  6.3 自定义异常类的使用
 第六章 异常处理
6.1 什么是异常
异常就是在程序的运行过程中所发生的异常事件,它中断指令的正常执行。Java中提供了一种独特的处理异常的机制,通过异常来处理程序设计中出现的错误。
6.1.1 异常示例
【例6-1】
     import java.io.*;
     class ExceptionDemo1{
      public static void main( String args[ ] ){
       FileInputStream fis = new FileInputStream( "text" );
       int b;
       while( (b=fis.read())!=-1 ){
        System.out.print( b );
       }
       fis.close( );
      }
     }
   
 由于名为"text"的文件在本机硬盘上不存在,所以在编译时,程序出现了I/O异常事件。
 【例6-2】
     class ExceptionDemo2{
      public static void main( String args[ ] ){
       int a = 0;
       System.out.println( 5/a );
      }
     }
C:\>javac ExceptionDemo2.java
C:\>java ExceptionDemo2
  java.lang.ArithmeticException: / by zero at
  ExceptionDemo2.main(ExceptionDemo2.java:4)
因为除数不能为0,所以在程序运行的时候出现了除0溢出的异常事件。为什么有的异常在编译时出现,而有的异常是在运行时出现的?让我们继续学习java 的异常处理机制。
6.1.2 异常处理机制
  抛弃(throw)异常:
  在Java程序的执行过程中,如果出现了异常事件,就会生成一个异常对象。生成的异常对象将传递给Java运行时系统,这一异常的产生和提交过程称为抛弃(throw)异常。
两种处理异常的机制:
  ◇ 捕获异常:
  当Java运行时系统得到一个异常对象时,它将会沿着方法的调用栈逐层回溯,寻找处理这一异常的代码。找到能够处理这种类型的异常的方法后,运行时系统把当前异常对象交给这个方法进行处理,这一过程称为捕获(catch)异常。这是积极的异常处理机制。如果Java运行时系统找不到可以捕获异常的方法,则运行时系统将终止,相应的Java程序也将退出。
  ◇ 声明抛弃异常:
  如果一个方法并不知道如何处理所出现的异常,则可在方法声明时,声明抛弃(throws)异常。这是一种消极的异常处理机制。
6.1.3 异常类的层次
在jdk中,每个包中都定义了异常类,而所有的异常类都直接或间接地继承于Throwable类。图4-1 ( G:\study\java\java_qh\JAVA\text\ch04\se01\right4_1_3_1.htm" \t "MyFrame )为jdk中异常类的继承关系。
java中的异常类可分为两大类:
  ◇ Error ( G:\study\java\java_qh\JAVA\text\ch04\se01\right4_1_3_2.htm" \t "MyFrame )
  ◇ Exception ( G:\study\java\java_qh\JAVA\text\ch04\se01\right4_1_3_2.htm" \t "MyFrame )
异常类的层次
6.2 异常的处理
java语言中有两种异常处理机制:捕获异常和声明抛弃异常。下面我们做详细介绍。
6.2.1 捕获异常
捕获异常是通过try-catch-finally语句实现的。
try{
  ......
   }catch( ExceptionName1 e ){
   ......
   }catch( ExceptionName2 e ){
   ......
   }
   ......
   }finally{
   ......
  }
◇ try
捕获异常的第一步是用try{…}选定捕获异常的范围,由try所限定的代码块中的语句在执行过程中可能会生成异常对象并抛弃。
◇ catch
每个try代码块可以伴随一个或多个catch语句,用于处理try代码块中所生成的异常事件。catch语句只需要一个形式参数指明它所能够捕获的异常类型,这个类必须是Throwable的子类,运行时系统通过参数值把被抛弃的异常对象传递给catch块。
在catch块中是对异常对象进行处理的代码,与访问其它对象一样,可以访问一个异常对象的变量或调用它的方法。getMessage( )是类Throwable所提供的方法,用来得到有关异常事件的信息,类Throwable还提供了方法printStackTrace( )用来跟踪异常事件发生时执行堆栈的内容。例如:
 try{
    ......
   }catch( FileNotFoundException e ){
    System.out.println( e );
    System.out.println( "message: "+e.getMessage() );
    e.printStackTrace( System.out );
   }catch( IOException e ){
    System.out.println( e );
   }
  catch 语句的顺序:
捕获异常的顺序和catch语句的顺序有关,当捕获到一个异常时,剩下的catch语句就不再进行匹配。因此,在安排catch语句的顺序时,首先应该捕获最特殊的异常,然后再逐渐一般化。也就是一般先安排子类,再安排父类。
◇ finally
捕获异常的最后一步是通过finally语句为异常处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理。不论在try代码块中是否发生了异常事件,finally块中的语句都会被执行。
6.2.2 声明抛弃异常
1.声明抛弃异常
如果在一个方法中生成了一个异常,但是这一方法并不确切地知道该如何对这一异常事件进行处理,这时,一个方法就应该声明抛弃异常,使得异常对象可以从调用栈向后传播,直到有合适的方法捕获它为止。
声明抛弃异常是在一个方法声明中的throws子句中指明的。例如:
public int read () throws IOException{
        ......
}
throws子句中同时可以指明多个异常,之间由逗号隔开。例如:
public static void main(String args[]) throws
IOException,IndexOutOfBoundsException {…}
2.抛出异常
抛出异常就是产生异常对象的过程,首先要生成异常对象,异常或者由虚拟机生成,或者由某些类的实例生成,也可以在程序中生成。在方法中,抛出异常对象是通过throw语句实现的。
例如:
  IOException e=new IOException();
  throw e ;
可以抛出的异常必须是Throwable或其子类的实例。下面的语句在编译时将会产生语法错误:
  throw new String("want to throw");
6.3 自定义异常类的使用
自定义异常类必须是Throwable的直接或间接子类。
 注意:一个方法所声明抛弃的异常是作为这个方法与外界交互的一部分而存在的。所以,方法的调用者必须了解这些异常,并确定如何正确的处理他们。第三章:面向对象技术
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: 类是Java语言面向对象编程的基本元素,它定义了一个对象的结构和功能。 Java类中包含成员变量和成员方法。成员变量有两种,用static 关键字修饰的变量为类变量,无static 修饰的变量为实例变量。相应地,成员方法也有两种,用static 修饰的为类方法,无static修饰的为实例方法。实例方法不仅可以对当前对象的实例变量进行操作,也可以对类变量进行操作;但类方法只能访问类变量。实例变量和实例方法必须由实例对象来调用,而类变量和类方法不仅可由实例对象来调用,还可由类名直接调用。Java通过在类定义的大括号里声明变量来把数据封装在一个类里,这里的变量称为成员变量。为了解决类名可能相同的问题,java 中提供包来管理类名空间。  封装性、继承性和多态性是java语言中面向对象的三个特性。
下载一
第三章 面向对象技术
【课前思考】
  1. 什么是对象?什么是类?
  2. 面向对象编程的特性有哪三个?它们各自又有哪些特性?
  3. 你知道java语言在面向对象编程方面有何独特的特点吗?
【学习目标】
  本讲主要讲述了java语言的面向对象技术,包括面向对象的基本概念、面向对象的程序设计方法及java中的类、包、对象、的特性。通过本讲的学习,同学们可以使用面向对象技术编写java程序。
【学习指南】
  应深刻理解各知识点的概念,使用上一讲的编程基础知识及面向对象技术,编写简单的java类,由浅至深,养成风格良好的编程习惯。
【难 重 点】
 重点:
  1. 仔细体会面向对象编程的思想,熟练理解类和对象的概念,理解面向对象的特性,会编写简单的类,逐渐掌握面向对象编程的方法。
  2. 注意java语言中,不允许多重继承,以及类变量和类方法的使用。
 难点:
  1. 理解方法重载和方法重写,不要混淆了两者的使用。
  2. 类变量和类方法的使用。
【知 识 点】
  3.1 面向对象技术基础
    3.1.1 面向对象的基本概念
    3.1.2 面向对象的基本特征
    3.1.3 面向对象的程序设计方法
  3.2 Java语言的面向对象特性
    3.2.1 类
    3.2.2 对象
3.2.3面向对象特性
3.1 面向对象技术基础
3.1.1 面向对象的基本概念
  面向对象的基本思想
  面向对象是一种新兴的程序设计方法,或者是一种新的程序设计规范(paradigm),其基本思想是使用对象、类、继承、封装、消息等基本概念来进行程序设计。从现实世界中客观存在的事物(即对象)出发来构造软件系统,并且在系统构造中尽可能运用人类的自然思维方式。开发一个软件是为了解决某些问题,这些问题所涉及的业务范围称作该软件的问题域。其应用领域不仅仅是软件,还有计算机体系结构和人工智能等。
1. 对象的基本概念
  对象是系统中用来描述客观事物的一个实体,它是构成系统的一个基本单位。一个对象由一组属性和对这组属性进行操作的一组服务组成。从更抽象的角度来说,对象是问题域或实现域中某些事物的一个抽象,它反映该事物在系统中需要保存的信息和发挥的作用;它是一组属性和有权对这些属性进行操作的一组服务的封装体。客观世界是由对象和对象之间的联系组成的。
  主动对象是一组属性和一组服务的封装体,其中至少有一个服务不需要接收消息就能主动执行(称作主动服务)。
 2. 类的基本概念
  把众多的事物归纳、划分成一些类是人类在认识客观世界时经常采用的思维方法。分类的原则是抽象。类是具有相同属性和服务的一组对象的集合,它为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和服务两个主要部分。在面向对象的编程语言中,类是一个独立的程序单位,它应该有一个类名并包括属性说明和服务说明两个主要部分。类与对象的关系就如模具和铸件的关系,类的实例化结果就是对象,而对一类对象的抽象就是类。
3. 消息
  消息就是向对象发出的服务请求,它应该包含下述信息:提供服务的对象标识、服务标识、输入信息和回答信息。服务通常被称为方法或函数。
3.1.2 面向对象的基本特征
1.封装性
  封装性就是把对象的属性和服务结合成一个独立的相同单位,并尽可能隐蔽对象的内部细节,包含两个含义:
  ◇ 把对象的全部属性和全部服务结合在一起,形成一个不可分割的独立单位(即对象)。
  ◇ 信息隐蔽,即尽可能隐蔽对象的内部细节,对外形成一个边界〔或者说形成一道屏障〕,只保留有限的对外接口使之与外部发生联系。
  封装的原则在软件上的反映是:要求使对象以外的部分不能随意存取对象的内部数据(属性),从而有效的避免了外部错误对它的"交叉感染",使软件错误能够局部化,大大减少查错和排错的难度。
2.继承性
  特殊类的对象拥有其一般类的全部属性与服务,称作特殊类对一般类的继承。例如,轮船、客轮;人、大人。一个类可以是多个一般类的特殊类,它从多个一般类中继承了属性与服务,这称为多继承。例如,客轮是轮船和客运工具的特殊类。在java语言中,通常我们称一般类为父类(superclass,超类),特殊类为子类(subclass)。
 3.多态性
  对象的多态性是指在一般类中定义的属性或服务被特殊类继承之后,可以具有不同的数据类型或表现出不同的行为。这使得同一个属性或服务在一般类及其各个特殊类中具有不同的语义。例如:"几何图形"的"绘图"方法,"椭圆"和"多边形"都是"几何图"的子类,其"绘图"方法功能不同。
3.1.3 面向对象程序设计方法
OOA-Object Oriented Analysis     面向对象的分析
  
  OOD-Object Oriented Design      面向对象的设计
  
  OOI-Object Oriented Implementation  面向对象的实现
3.2 Java语言的面向对象特性
3.2.1 类
  类是java中的一种重要的复合数据类型,是组成java程序的基本要素。它封装了一类对象的状态和方法,是这一类对象的原形。一个类的实现包括两个部分:类声明和类体。
1.类声明:
  [public][abstract|final] class className [extends superclassName] [implements interfaceNameList]
  {……}
  其中,修饰符public,abstract,final 说明了类的属性,className为类名,superclassName为类的父类的名字,interfaceNameList为类所实现的接口列表。
 2.类体
  类体定义如下:
  class className
  {[public | protected | private ] [static]
  [final] [transient] [volatile] type
  variableName;                 //成员变量
  [public | protected | private ] [static]
  [final | abstract] [native] [synchronized]
  returnType methodName([paramList]) [throws exceptionList]
   {statements}                 //成员方法
  }
 3.成员变量
  成员变量的声明方式如下:
  [public | protected | private ] [static]
  [final] [transient] [volatile] type
  variableName;                 //成员变量
  其中,
  static: 静态变量(类变量);相对于实例变量
  final: 常量
  transient: 暂时性变量,用于对象存档
  volatile: 贡献变量,用于并发线程的共享
 4.成员方法
  方法的实现包括两部分内容:方法声明和方法体。
  [public | protected | private ] [static]
  [final | abstract] [native] [synchronized]
  returnType methodName([paramList])
  [throws exceptionList]            //方法声明
   {statements}                //方法体
  方法声明中的限定词的含义:
  static: 类方法,可通过类名直接调用
  abstract: 抽象方法,没有方法体
  final: 方法不能被重写
  native: 集成其它语言的代码
  synchronized: 控制多个并发线程的访问
  ◇ 方法声明
  方法声明包括方法名、返回类型和外部参数。其中参数的类型可以是简单数据类型,也可以是复合数据类型(又称引用数据类型)。
  对于简单数据类型来说,java实现的是值传递,方法接收参数的值,但不能改变这些参数的值。如果要改变参数的值,则用引用数据类型,因为引用数据类型传递给方法的是数据在内存中的地址,方法中对数据的操作可以改变数据的值。
  例3-1说明了简单数据类型与引用数据的区别。
【例3-1】
  import java.io.*;
  public class PassTest{
  float ptValue;
  public static void main(String args[]) {
  int val;
  PassTest pt=new PassTest();
  val=11;
  System.out.println("Original Int Value is:"+val);
  pt.changeInt(val);                   //值参数
  System.out.println("Int Value after Change is:" +val);
  pt.ptValue=101f;
  System.out.println("Original ptValue is:"+pt.ptValue);
  pt.changeObjValue(pt); //引用类型的参数
  System.out.println("ptValue after Change is:"+pt.ptValue);
  }
  public void changeInt(int value){
  value=55;            //在方法内部对值参数进行了修改
  }
  public void changeObjValue(PassTest ref){
  ref.ptValue=99f;        //在方法内部对引用参数进行了修改
    }
  }
   查看运行结果 ( G:\study\java\java_qh\JAVA\text\ch03\se02\" \l "07 )
  ◇ 方法体
  方法体是对方法的实现,它包括局部变量的声明以及所有合法的Java指令。方法体中声明的局部变量的作用域在该方法内部。若局部变量与类的成员变量同名,则类的成员变量被隐藏。
  例3-2 说明了局部变量z和类成员变量z的作用域是不同的。
【例3-2】
  import java.io.*;
  class Variable{
  int x=0,y=0,z=0;              //类的成员变量
  void init(int x,int y) {
  this.x=x; this.y=y;
  int z=5;                 //局部变量
  System.out.println("** in init**");
  System.out.println("x="+x+" y="+y+" z="+z);
    }
  }
  public class VariableTest{
  public static void main(String args[]){
  Variable v=new Variable();
  System.out.println("**before init**");
  System.out.println("x="+v.x+" y="+ v.y+" z="+v.z);
  v.init(20,30);
  System.out.println("**after init**");
  System.out.println("x="+v.x+ " y="+ v.y+" z="+v.z);
    }
  }
  上例中我们用到了this,这是因为init()方法的参数名与类的成员变量x,y的名字相同,而参数名会隐藏成员变量,所以在方法中,为了区别参数和类的成员变量,我们必须使用this。this-----用在一个方法中引用当前对象,它的值是调用该方法的对象。返回值须与返回类型一致,或者完全相同,或是其子类。当返回类型是接口时,返回值必须实现该接口。
 5.方法重载
  方法重载是指多个方法享有相同的名字,但是这些方法的参数必须不同,或者是参数的个数不同,或者是参数类型不同。返回类型不能用来区分重载的方法。
  参数类型的区分度一定要足够,例如不能是同一简单类型的参数,如int与long。
【例3-3】
  import java.io.*;
  class MethodOverloading{
  void receive(int i) {
  System.out.println("Receive one int data");
  System.out.println("i="+i);
  }
  void receive(int x, int y) {
  System.out.println("Receive two int datas");
  System.out.println("x="+x+" y="+y);
    } 
  }
  public class MethodOverloadingTest{
  public static void main(String args[]) {
  MethodOverloading mo=new MethodOverloading();
  mo.receive(1);
  mo.receive(2,3);
    } 
  }
 6. 构造方法
  ◇ 构造方法是一个特殊的方法。Java 中的每个类都有构造方法,用来初始化该类的一个对象。
  ◇ 构造方法具有和类名相同的名称,而且不返回任何数据类型。
  ◇ 重载经常用于构造方法。
  ◇ 构造方法只能由new运算符调用
【例3-4】
  class Point{
  int x,y;
  Point(){
  x=0; y=0;
  }
  Point(int x, int y){
  this.x=x;
  this.y=y;
    }
  }
3.2.2 对象
  类实例化可生成对象,对象通过消息传递来进行交互。消息传递即激活指定的某个对象的方法以改变其状态或让它产生一定的行为。一个对象的生命周期包括三个阶段:生成、使用和消除。
1. 对象的生成
  对象的生成包括声明、实例化和初始化。
  格式为:
  type objectName=new type([paramlist]);
  ◇ 声明:type objectName
  声明并不为对象分配内存空间,而只是分配一个引用空间;对象的引用类似于指针,是32位的地址空间,它的值指向一个中间的数据结构,它存储有关数据类型的信息以及当前对象所在的堆的地址,而对于对象所在的实际的内存地址是不可操作的,这就保证了安全性。
  ◇ 实例化:运算符new为对象分配内存空间,它调用对象的构造方法,返回引用;一个类的不同对象分别占据不同的内存空间。
  ◇ 生成:执行构造方法,进行初始化;根据参数不同调用相应的构造方法。
 2. 对象的使用
  通过运算符"."可以实现对变量的访问和方法的调用。变量和方法可以通过设定访问权限来限制其它对象对它的访问。
  ◇调用对象的变量
  格式:objectReference.variable
  objectReference是一个已生成的对象,也可以是能生成对象的表达式
  例: p.x= 10;
     tx=new Point( ).x;
  ◇调用对象的方法
  格式:objectReference.methodName([paramlist]);
  例如:p.move(30,20);
     new Point( ).move(30,20);
 3. 对象的清除
  当不存在对一个对象的引用时,该对象成为一个无用对象。Java的垃圾收集器自动扫描对象的动态内存区,把没有引用的对象作为垃圾收集起来并释放。
  System.gc( );  
  当系统内存用尽或调用System.gc( )要求垃圾回收时,垃圾回收线程与系统同步运行。
3.2.3 面向对象特性
  java语言中有三个典型的面向对象的特性:封装性、继承性和多态性,下面将详细阐述。
1. 封装性
  java语言中,对象就是对一组变量和相关方法的封装,其中变量表明了对象的状态,方法表明了对象具有的行为。通过对象的封装,实现了模块化和信息隐藏。通过对类的成员施以一定的访问权限,实现了类中成员的信息隐藏。
  ◇ 类体定义的一般格式:
  class className
  {   [public | protected | private ] [static]
     [final] [transient] [volatile] type
     variableName;            //成员变量
     [public | protected | private ] [static]
     [final | abstract] [native] [synchronized]
     returnType methodName([paramList])
     [throws exceptionList]
     {statements} //成员方法
  }
  ◇ java类中的限定词
  java语言中有四种不同的限定词,提供了四种不同的访问权限。
  1) private
  类中限定为private的成员,只能被这个类本身访问。
  如果一个类的构造方法声明为private,则其它类不能生成该类的一个实例。
  2) default
  类中不加任何访问权限限定的成员属于缺省的(default)访问状态,可以被这个类本身和同一个包中的类所访问。
  3) protected
  类中限定为protected的成员,可以被这个类本身、它的子类(包括同一个包中以及不同包中的子类)和同一个包中的所有其他的类访问。
  4) public
  类中限定为public的成员,可以被所有的类访问。
  表3-1列出了这些限定词的作用范围。
【表3-1】 java中类的限定词的作用范围比较
同一个类 同一个包 不同包的子类 不同包非子类
private *
default * *
protected * * *
public * * * *
  2. 继承性
  通过继承实现代码复用。Java中所有的类都是通过直接或间接地继承java.lang.Object类得到的。继承而得到的类称为子类,被继承的类称为父类。子类不能继承父类中访问权限为private的成员变量和方法。子类可以重写父类的方法,及命名与父类同名的成员变量。但Java不支持多重继承,即一个类从多个超类派生的能力。
  ◇ 创建子类
  格式:
  class SubClass extends SuperClass {
  …
  }
  ◇ 成员变量的隐藏和方法的重写
  子类通过隐藏父类的成员变量和重写父类的方法,可以把父类的状态和行为改变为自身的状态和行为。
  例如:
  class SuperClass{
    int x; …
    void setX( ){ x=0; } …
  }
  class SubClass extends SuperClass{
    int x;   //隐藏了父类的变量x
    …
    void setX( ) { //重写了父类的方法 setX()
    x=5; } ….
  }
  注意:子类中重写的方法和父类中被重写的方法要具有相同的名字,相同的参数表和相同的返回类型,只是函数体不同。
  ◇ super
  java中通过super来实现对父类成员的访问,super用来引用当前对象的父类。Super 的使用有三种情况:
  1)访问父类被隐藏的成员变量,如:
    super.variable;
  2)调用父类中被重写的方法,如:
    super.Method([paramlist]);
  3)调用父类的构造函数,如:
    super([paramlist]);
【例3-5】
  import java.io.*;
  class SuperClass{
    int x;
    SuperClass( ) {
     x=3;
     System.out.println("in SuperClass : x=" +x);
    }
     void doSomething( ) {
     System.out.println("in SuperClass.doSomething()");
    }
  }
  class SubClass extends SuperClass {
    int x;
    SubClass( ) {
     super( );    //调用父类的构造方法
     x=5;      //super( ) 要放在方法中的第一句
     System.out.println("in SubClass :x="+x);
    }
     void doSomething( ) {
     super.doSomething( ); //调用父类的方法
     System.out.println("in SubClass.doSomething()");
     System.out.println("super.x="+super.x+" sub.x="+x);
    }
  }
  public class Inheritance {
     public static void main(String args[]) {
     SubClass subC=new SubClass();
     subC.doSomething();
    }
  }
 3. 多态性
  在java语言中,多态性体现在两个方面:由方法重载实现的静态多态性(编译时多态)和方法重写实现的动态多态性(运行时多态)。
  1) 编译时多态
  在编译阶段,具体调用哪个被重载的方法,编译器会根据参数的不同来静态确定调用相应的方法。
  2) 运行时多态
  由于子类继承了父类所有的属性(私有的除外),所以子类对象可以作为父类对象使用。程序中凡是使用父类对象的地方,都可以用子类对象来代替。一个对象可以通过引用子类的实例来调用子类的方法。
  ◇ 重写方法的调用原则:java运行时系统根据调用该方法的实例,来决定调用哪个方法。对子类的一个实例,如果子类重写了父类的方法,则运行时系统调用子类的方法;如果子类继承了父类的方法(未重写),则运行时系统调用父类的方法。
  在例3-6中,父类对象a引用的是子类的实例,所以,java运行时调用子类B的callme方法。
【例3-6】
  import java.io.*;
  class A{
     void callme( ) {
      System.out.println("Inside A's callme()method");
     }
  }
  class B extends A{
     void callme( ) {
      System.out.println("Inside B's callme() Method");
     }
  }
  public class Dispatch{
     public static void main(String args[]) {
      A a=new B();
      a.callme( );
     }
  }
  ◇ 方法重写时应遵循的原则:
  1)改写后的方法不能比被重写的方法有更严格的访问权限(可以相同)。
  2)改写后的方法不能比重写的方法产生更多的例外。
 4. 其它
  ◇ final 关键字
  final 关键字可以修饰类、类的成员变量和成员方法,但final 的作用不同。
  1) final 修饰成员变量:
  final修饰变量,则成为常量,例如
  final type variableName;
  修饰成员变量时,定义时同时给出初始值,而修饰局部变量时不做要求。
  2) final 修饰成员方法:
  final修饰方法,则该方法不能被子类重写
  final returnType methodName(paramList){
  …
  }
  3) final 类:
  final修饰类,则类不能被继承
  final class finalClassName{
  …
  }
  ◇ 实例成员和类成员
  用static 关键字可以声明类变量和类方法,其格式如下:
  static type classVar;
  static returnType classMethod({paramlist}) {
  …
  }
  如果在声明时不用static 关键字修饰,则声明为实例变量和实例方法。
  1) 实例变量和类变量
  每个对象的实例变量都分配内存,通过该对象来访问这些实例变量,不同的实例变量是不同的。
  类变量仅在生成第一个对象时分配内存,所有实例对象共享同一个类变量,每个实例对象对类变量的改变都会影响到其它的实例对象。类变量可通过类名直接访问,无需先生成一个实例对象,也可以通过实例对象访问类变量。
  2) 实例方法和类方法
  实例方法可以对当前对象的实例变量进行操作,也可以对类变量进行操作,实例方法由实例对象调用。
  但类方法不能访问实例变量,只能访问类变量。类方法可以由类名直接调用,也可由实例对象进行调用。类方法中不能使用this或super关键字。
  例3-7 是关于实例成员和类成员的例子。
【例3-7】
  class Member {
    static int classVar;
    int instanceVar;
    static void setClassVar(int i) {
     classVar=i;
     // instanceVar=i; // 类方法不能访问实例变量
    }
    static int getClassVar()
     { return classVar; }
    void setInstanceVar(int i)
     { classVar=i; //实例方法不但可以访问类变量,也可以实例变量
     instanceVar=i; }
     int getInstanceVar( )
     { return instanceVar; }
    }
    public class MemberTest{
     public static void main(String args[]) {
         Member m1=new member();
         Member m2=new member();
         m1.setClassVar(1);
         m2.setClassVar(2);
         System.out.println("m1.classVar="+m1.getClassVar()+"
                   m2.ClassVar="+m2.getClassVar());
         m1.setInstanceVar(11);
         m2.setInstanceVar(22);
         System.out.println("m1.InstanceVar="+m1.getInstanceVar
              ()+" m2.InstanceVar="+m2.getInstanceVar());
     }
    }
  ◇ 类java.lang.Object
  类java.lang.Object处于java开发环境的类层次的根部,其它所有的类都是直接或间接地继承了此类。该类定义了一些最基本的状态和行为。下面,我们介绍一些常用的方法。
  equals() :比较两个对象(引用)是否相同。
  getClass():返回对象运行时所对应的类的表示,从而可得到相应的信息。
  toString():用来返回对象的字符串表示。
  finalize():用于在垃圾收集前清除对象。
  notify(),notifyAll(),wait():用于多线程处理中的同步。第八章 Java Applet
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: 本章对Java Applet做了介绍和一些基本的应用的讲解,例如Applet的创建,生命周期和Applet的主要方法以及Applet的AWT绘制,最后简单介绍了Applet和浏览器间的通信的方法。
下载一
【课前思考】
  1. 什么是Java Applet,它和Application的区别是什么?
  2. 如何创建Java Applet?
  3. Applet的生命周期及主要方法是什么?
  4. Applet的用途和用法是什么?
【学习目标】
  学习java中Applet的使用,掌握Applet的调度和控制方法,会用于设计程序。
【学习指南】
  掌握Java Applet的生命周期概念,尤其是通过HTML来调用Java Applet的实现机制。
【难 重 点】
  1. Java Applet的生命周期。
  2. Java Applet的通讯。
【知 识 点】
  8.1 Java Applet
   8.1.1 Applet 介绍
   8.1.2 Applet的AWT绘制
  8.2 Applet和浏览器间的通信
第八章 Java Applet
前面的章节我们阐述了Application的应用,这一讲我们将介绍java的另一类应用java Applet,即java小应用程序。
  在Java问世的头几年里,之所以如此热门,其根本原因还是在于Java具有"让Internet动起来"的能力。具体地说,就是Java能创建一种特殊类型的程序(通常称作"小应用程序"或者Applet),具备Java能力的Web浏览器可从网上下载这种程序,然后运行。
目前,几乎所有浏览器均支持动态HTML(DHTML)和脚本编制(支持XML的浏览器也有很多),所以比起Java刚刚问世的时候,浏览器能够做的事情要多得多。但尽管如此,由于小应用程序是用一种全功能的程序设计语言编制的,所以同HTML、XML和脚本语言的任何一种可能的组合相比,它仍然具有应用前景!
8.1 Java Applet 基础
8.1 .1 Applet 介绍
Applet就是使用Java语言编写的一段代码,它可以在浏览器环境中运行。它与Application的区别主要在于其执行方式的不同。application 是从其中的main() 方法开始运行的,而Applet 是在浏览器中运行的,必须创建一个HTML 文件,通过编写HTML 语言代码告诉浏览器载入何种Applet 以及如何运行。
1. Applet 举例
例8.1 HelloWorld.java 源程序:
  import java.awt.Graphics; //引入图形类Graphics
  import java.applet.Applet; //引入Applet类
  public class HelloWorld extends Applet {
      String hw_text ;
      public void init () { //init()方法是Applet首先执行的方法
      hw_text = "Hello World";
      }
      public void paint(Graphics g) {
      g.drawString (hw_text , 25, 25) ;
         //在坐标为(25,25)的地方显示字符串hw_text
      }
  }
  Applet程序编写完后,首先要用java编译器编译成为字节码文件,然后编写相应的HTML文件才能够正常执行,例如为运行上面的Applet程序所编写的HTML文件HelloWorld.html如下:
  
    
  
2.Applet的 安全性
"沙箱"机制:Java虚拟机为Applet提供能够良好运行的沙箱,一旦它们试图离开沙箱则会被禁止。
  由于小应用程序是通过网络传递的,这就不可避免地使人想到会发生安全问题。例如有人编写恶意程序通过小应用程序读取用户密码并散播到网络上,这将会是一件非常可怕的事情。所以,必须对小应用程序进行限制。
  浏览器禁止Applet执行下列操作:
  (1)在运行时调用其它程序。
  (2)文件读写操作。
  (3)装载动态连接库和调用任何本地方法。
  (4)试图打开一个socket进行网络通信,但是所连接的主机并不是提供Applet的主机。
 ◇ Applet的类层次:
 ◇ Applet的生命周期
  小应用程序的生命周期相对于Application而言较为复杂。在其生命周期中涉及到Applet类的四个方法(也被JApplet类继承):init()、start()、stop()和destroy()。下面首先用图来表示一个小应用程序的生命周期,然后再简要描述这四个方法。
 
  Applet的生命周期中有四个状态:初始态、运行态、停止态和消亡态。当程序执行完init()方法以后,Applet程序就进入了初始态;然后马上执行start()方法,Applet程序进入运行态;当Applet程序所在的浏览器图标化或者是转入其它页面时,该Applet程序马上执行stop()方法,Applet程序进入停止态;在停止态中,如果浏览器又重新装载该Applet程序所在的页面,或者是浏览器从图标中复原,则Applet程序马上调用start()方法,进入运行态;当然,在停止态时,如果浏览器关闭,则Applet程序调用destroy()方法,进入消亡态。
 ◇ Applet的主要方法:
  1. init( )
  创建Applet时执行,只执行一次
  当小应用程序第一次被支持Java的浏览器加载时,便执行该方法。在小应用程序的生命周期中,只执行一次该方法,因此可以在其中进行一些只执行一次的初始化操作,如处理由浏览器传递进来的参数、添加用户接口组件、加载图像和声音文件等。
小应用程序有默认的构造方法,但它习惯于在init()方法中执行所有的初始化,而不是在默认的构造方法内。
  2.start( )
  多次执行,当浏览器从图标恢复成窗口,或者是返回该主页时执行。
  系统在调用完init()方法之后,将自动调用start()方法。而且每当浏览器从图标恢复为窗口时,或者用户离开包含该小应用程序的主页后又再返回时,系统都会再执行一遍start()方法。start()方法在小应用程序的生命周期中被调用多次,以启动小应用程序的执行,这一点与init()方法不同。该方法是小应用程序的主体,在其中可以执行一些需要重复执行的任务或者重新激活一个线程,例如开始动画或播放声音等。
  3.stop( )
  多次执行,当浏览器变成图标时,或者是离开主页时执行,主要功能是停止一些耗用系统资源的工作,。
  与start()相反,当用户离开小应用程序所在页面或浏览器变成图标时,会自动调用stop()方法。因此,该方法在生命周期中也被多次调用。这样使得可以在用户并不注意小应用程序的时候,停止一些耗用系统资源的工作(如中断一个线程),以免影响系统的运行速度,且并不需要去人为地去调用该方法。如果你的小应用程序中不包含动画、声音等程序,通常也不必重载该方法。
  4.destroy( )
  用来释放资源,在stop( )之后执行
  浏览器正常关闭时,Java自动调用这个方法。destroy()方法用于回收任何一个与系统无关的内存资源。当然,如果这个小应用程序仍然处于活动状态,Java会在调用destroy()之前,调用stop()方法。
  下面的例子使用了小应用程序生命周期中的这几个方法。
 例8.2
  import java.awt.Graphics;
  import java.applet.Applet;
  public class HWloop extends Applet {
     AudioClip sound; //声音片断对象
     public void init( ){
        sound=getAudioClip("audio/hello.au"); //获得声音片断
     }
     public void paint(Graphics g) {
        g.drawString("hello Audio",25,25); //显示字符串
     }
  public void start( )
     {
        sound.loop( ); //声音片断开始播放
     }
  public void stop( )
     {
        sound.stop( ); //声音片断停止
        }
  }
  在本例子中,Applet开始执行后就不停的播放声音,如果浏览器图标化或者是转到其它页面,则声音停止播放;如果浏览器恢复正常或者是从其它页面跳回,则程序继续播放声音。
  由于Applet程序要嵌入到HTML文件中才能够正常执行,下面介绍与Applet程序有关的HTML文件标记。
  
  [archive=archiveList] //同一个codebase中需预先下载的文件
  code=appletFile.class //applet的名称
  width=pixels height=pixels //applet的初始显示空间
  [codebase=codebaseURL] //代码的地址
  [alt=alternateText] //如果浏览器不支持applet时,显示的替代文本
  [name=appletInstanceName]
           //给该applet取名,用于同页面applet之间的通信
  [align=alignment] //对齐方式,如left,right,top,baseline...
  [vspace=pixels] [hspace=pixels] //预留的边缘空白
  >
  []
  []
  …… //参数名称及其值
  [alternateHTML]
  

注意:
  1)不支持Java的浏览器会把之间的普通HTML文档显示出来;支持Java的浏览器,则把其中的普通HTML文档忽略。
  2)AppletViewer仅支持 标记,其它标记不会被显示出来。
  经过精心设计,可以使java程序同时是Applet与Application,如下例所示:
 例8.3
   import java.awt.*;
   import java.awt.event.*;
   public class AppletApp extends Applet {
      public void main(String args[]) {
        Frame frame=new Frame("Application"); //构造一个Frame
        AppletApp app=new AppletApp();
        frame.add("Center",app);
        frame.setSize(200,200); //改变Frame的尺寸
   frame.validate();
   frame.setVisible(true); //使Frame可见
   frame.addWindwoListener(new WindowControl(app));
                    //给Frame加入监听器
   app.init(); //初始化Applet
   app.start(); //运行该Applet
   }
      public void paint(Graphics g) { //重画方法
        g.drawString("hello world",25,25);
      }
      public void destroy(){
        System.exit(0);
      }
   }
   class WindowControl extends WindowAdapter { //监听器类
      Applet c;
      public WindowControl(Applet c) { //构造函数
        this.c=c;
      }
      public void windowClosing(WindowEvent e) {
                   //关闭窗口时调用的方法
        c.destroy( );
      }
   }
8.1..2 Applet的AWT绘制
  Applet程序中所采用的AWT的绘图机制主要涉及三个方法:paint()方法、update()方法和repaint()方法,update()方法和paint()方法都有一个Graphics类参数。Graphics是画图的关键,它可以支持两种绘图:一种是基本的绘图,如:画线、矩形、圆等;另一种是画图象,主要用于动画制作。
  要进行绘图,首先要找到一个Graphics类的对象。update()方法和paint()方法所传递的参数都是Graphics类的对象,因此主要是通过重载它们来进行绘图,这是在动画程序中经常使用的方法。我们还可以通过getGraphics()方法得到一个Graphics类的对象,这个对象和update()方法和paint()方法中所传递的对象一样,都是该成员所对应的Graphics类的对象。得到了Graphics类的对象,就可使用各种绘图方法。
  Graphics中提供的图形绘制方法有:
  paint( ) //进行绘图的具体操作,必须有程序员重写
  update( ) //用于更新图形,先清除背景、前景,再调用paint()
  repaint( )
  下面的方法支持基本的绘图和画图像:
  void drawLine( )
  void drawArc( )
  void drawPolygon( )
  void drawRect( )
  void drawRoundRect( )
  void fill3DRect( )
  void fillOval( )
  java.awt.Graphics类
  
输出文字:
  void drawBytes( )
  void drawChars( )
  void drawString( )
  Applet 的AWT绘制举例如下:
  
例8.4
  import java.awt.*;
  import java.awt.event.*;
  import java.applet.*;
  public class ArcTest extends Applet implements WindowListener {
     ArcControls controls;
     pulic void init(){ //Applet的入口方法
       setLayout(new BorderLayout());
       ArcCanvas c=new ArcCanvas();
  
     Add("Center",c);
     add("South",controls=new ArcControls(C));
  }
  public void start(){
     controls.setEnabled(true); //激活controls
  }
  public void stop(){
     controls.setEnabled(false);
  }
  public void windowActivated(WindowEvent e){ }
            //重写WindowListener的方法
  public void windowClosed(WindowEvent e){ }
            //重写WindowListener的方法
  public void windowClosing(WindowEvent e){
            //重写WindowListener的方法
     System.exit(0); }
  public void windowDeactivated(WindowEvent e){}
            //重写WindowListener的方法
  public void windowDeiconified(WindowEvent e){}
            //重写WindowListener的方法
  public void windowIconified(WindowEvent e){ }
            //重写WindowListener的方法
  public void windowOpend(WindowEvent e){ }
            //重写WindowListener的方法
  public static void main(String args[]) {
     Frame f=new Frame("ArcTest"); //构造Frame
     ArcTest arcTest=new ArcTest(); //构造arcTest
     atcTest.init();
     arcTest.start();
     f.add("Center",arcTest);
     f.setSize(300,300);
     f.show();
     f.addWindowListener(arcTest);
     }
  }
  class ArcCanvas extends Canvas{ //类ArcCanvas
     int startAngle=0;
     int endAngle=45;
     boolean filled=false;
     Font font;
     public void paint(Graphics g){
        //paint方法,该方法的作用是在Canvas上画图
      Rectangle r=getBounds();
      int hlines=r.height/10;
      int vlines=r.width/10;
      g.setColor(Color.pink);
  for(int i=1;i<=hlines;i++) {
      g.drawLine(0,i*10,r.width,i*10);
      }
  for(int i=1;i<=vlines;i++) {
      g.drawLine(i*10,0,i*10,r.height);
      }
  g.setColor(Color.red);
  if(filled) {
      g.fillArc(0,0,r.width-1,r.height-1,startAngle,endAngle); }
      else { g.drawArc(0,0,r.width-1,r.height-1,startAngle, endAngle);
  }
  g.setColor(Color.black);
  g.setFont(font);
  g.drawLine(0,r.height/2,r.width,r.height/2);
  g.drawLine(r.width/2,0,r.width/2,r.height);
  g.drawLine(0,,0,r.width,r.height);
  g.drawLine(r.width,0,0,r.height);
  int sx=10;
  int sy=r.height-28;
  g.drawString("S="+startAngle,sx,sy);
  g.drawString("E="+ednAngle,sx,sy+14);
  }
  public void redraw(boolean filled,int start,int end){ //重画方法
        this.filled=filled;
        this.startAngle=start;
        this.endAngle=end;
        repaint();
        //通过调用repaint()方法,从而最终调用paint方法完成重画
        }
  }
  class ArcControls extends Panel implements ActionListener { //ArcControls类
        TextFiled s;
        TextFiled e;
        ArcCanvas canvas;
  public ArcControls(ArcCanvas canvas) {
        Button b=null;
        this.canvas=canvas;
        add(s=new TextField("0",4));
        add(e=new TextField("45",4));
        b=new Button("Fill");
        b.addActionListener(this);
        add(b);
        b=new Button("Draw");
        b.addActionListener(this);
        add(b);
  }
  public void actionPerformed(ActionEvent ev) {
          //实现接口ActionListener的方法
     String label=ev.getActionCommand();
     canvas.redraw(label.equals("Fill"),
     Integer.parseInt(s.getText(),trim()),
     Integer.parserInt(e.getText().trim());
     )
  }
8.2 Applet和浏览器间的通信
在Applet类中提供了许多方法,使之可以与浏览器进行通信。下面对这些方法进行简要的介绍:
一个Web页可包含一个以上的小应用程序。一个Web页中的多个小应用程序可以直接通过java.applet包中提供的方法进行通信。
  getDocumentBase( ) //返回当前网页所在的URL
  getCodeBase( ) //返回当前applet所在的URL
  getImage(URL base,String target) //返回网址URL中名为target的图像
  getAudioClip(URL base,String target)
                 //返回网址URL中名为target的声音对象
  getParameter(String target ) //提取HTML文件中名为target的参数的值
  同页Applet间的通信
  在java.applet.Applet类中提供了如下方法得到当前运行页的环境上下文AppletContext对象。
  public AppletContext getAppletContext();
  通过AppletContext对象,可以得到当前小应用程序运行环境的信息。AppletContext是一个接口,其中定义了一些方法可以得到当前页的其它小应用程序,进而实现同页小应用程序之间的通信。
  (1)得到当前运行页的环境上下文AppletContext对象
     public AppletContext getAppletContext();
  (2)取得名为name的Applet对象
     public abstract Applet getApplet(String name);
  (3)得到当前页中所有Applet对象
     public abstract Enumeration getApplets();
例6.11
   import java.applet.*;
   import java.awt.*;
   import java.awt.event.*;
   import java.util.Enumeration;
   public class GetApplets extends Applet implemements ActionListener {
     private TextArea textArea; //声明一个TextArea
     private String newline;
     public void init() {
       Button b=new Button("Click to call getApplets()");
       b.addActionListener(this);
       setLayout(new BorderLayout());
     add("North",b);
     textArea=new TextAred(5,40);
     textArea.setEditable(false);
     add("Center",textArea);
     newline=System.getProperty("line,separator");
               //取得系统当前的换行符
   }
     public void actionPerformed(ActionEvent event) {
              
       printApplets();
     }
     public String getAppletInfo() {
       return "GetApplets by Dong.li";
   }
   public void printApplets()}
       Enumeration e=getAppletContext().getApplets();
             
       textArea.append("Results of getApplets():" + newline);
       while(e.hasMoreElements()) {
         Applet applet=(Applet) e.nextElement();
         String info=((Applet)applet).getAppletInfo();
            
         if (info!=null) {
           textArea.append("-"+info+newline);
            
         } else {
           textArea.append("-"+applet.getClass().getName()+newline) ;
         }
         }
           textArea.append("__________"+newline+newline);
       }
   }第十一章 网络编程
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: 本章主要讲解了Java环境下的网络编程。因为TCP/IP协议是Java网络编程的基础知识,本讲开篇重点介绍了TCP/IP协议中的一些概念,TCP/IP协议本身是一个十分庞大的系统,用几个小节是不可能讲清楚的。所以我们只是联系实际,讲解了一些最基本的概念,帮助学生理解后面的相关内容。重点有一下几个概念:主机名,IP,端口,服务类型,TCP,UDP。后续的内容分为两大块,一块是以URL为主线,讲解如何通过URL类和URLConnection类访问WWW网络资源,由于使用URL十分方便直观,尽管功能不是很强,还是值得推荐的一种网络编程方法,尤其是对于初学者特别容易接受。本质上讲,URL网络编程在传输层使用的还是TCP协议。另一块是以Socket接口和C/S网络编程模型为主线,依次讲解了如何用Java实现基于TCP的C/S结构,主要用到的类有Socket,ServerSocket。以及如何用Java实现基于UDP的C/S结构,还讨论了一种特殊的传输方式,广播方式,这种方式是UDP所特有的,主要用到的类有DatagramSocket , DatagramPacket, MulticastSocket。这一块在Java网络编程中相对而言是最难的(尽管Java在网络编程这方面已经做的够"傻瓜"了,但是网络编程在其他环境下的却是一件极为头痛的事情,再"傻瓜"还是有一定的难度),也是功能最为强大的一部分,读者应该好好研究,领悟其中的思想。
下载一
【课前思考】
  1. 什么是TCP/ IP协议?
  2. TCP/IP有哪两种传输协议,各有什么特点?
  3. 什么是URL?
  4. URL和IP地址有什么样的关系?
  5. 什么叫套接字(Socket)?
  6. 套接字(Socket)和TCP/IP协议的关系?
  7. URL和套接字(Socket)的关系?
【学习目标】
  理解计算机网络编程的概念,掌握如何使用Java在一台或多台计算机之间进行基于TCP/IP协议的网络通讯。
【学习指南】
  通过理解TCP/IP协议的通讯模型,以JDK提供的包为工具,勤加练习,掌握各种基于Java的网络通讯的实现方法。
【难 重 点】
  1. 基于URL的网络编程(主要针对WWW资源)
  2. 基于TCP的C/S网络编程(单客户,多客户)
  3. 基于UDP的C/S网络编程
【知 识 点】
  11.1 网络编程的基本概念,TCP/IP协议简介
   11.1.1 网络基础知识
   11.1.2 网络基本概念
   11.1.3 两类传输协议:TCP;UDP
  11.2 基于URL的高层次Java网络编程
   11.2.1 一致资源定位器URL
   11.2.2 URL的组成
   11.2.3 创建一个URL
   11.2.4 解析一个URL
   11.2.5 从URL读取WWW网络资源
   11.2.6 通过URLConnetction连接WWW
  11.3 基于Socket(套接字)的低层次Java网络编程
   11.3.1 Socket通讯
   11.3.2 Socket通讯的一般过程
   11.3.3 创建Socket
   11.3.4 客户端的Socket
   11.3.5 服务器端的ServerSocket
   11.3.6 打开输入/出流
   11.3.7 关闭Socket
   11.3.8 简单的Client/Server程序设计
   11.3.9 支持多客户的client/server程序设计
   11.3.10 据报Datagram通讯
   11.3.11 什么是Datagram
   11.3.12 Datagram通讯的表示方法:DatagramSocket;DatagramPacket
   11.3.13 基于UDP的简单的Client/Server程序设计
   11.3.14 用数据报进行广播通讯
【知识结构图】
第十一章 网络编程
11.1 网络编程的基本概念,TCP/IP协议简介
11.1.1 网络基础知识
  计算机网络形式多样,内容繁杂。网络上的计算机要互相通信,必须遵循一定的协议。目前使用最广泛的网络协议是Internet上所使用的TCP/IP协议。
网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。网络编程中有两个主要的问题,一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输。在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以唯一地确定Internet上的一台主机。而TCP层则提供面向应用的可靠的或非可靠的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据的。
  目前较为流行的网络编程模型是客户机/服务器(C/S)结构。即通信双方一方作为服务器等待客户提出请求并予以响应。客户则在需要服务时向服务器提出申请。服务器一般作为守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服务端口,使后来的客户也能及时得到服务。
11.1.2网络基本概念
IP地址:标识计算机等网络设备的网络地址,由四个8位的二进制数组成,中间以小数点分隔。
    如:166.111.136.3 , 166.111.52.80
  主机名(hostname):网络地址的助记名,按照域名进行分级管理。
    如:www.tsinghua.
      www.
  端口号(port number):网络通信时同一机器上的不同进程的标识。
    如:80,21,23,25,其中1~1024为系统保留的端口号
  服务类型(service):网络的各种服务。
    http, telnet, ftp, smtp
  我们可以用以下的一幅图来描述这里我们所提到的几个概念:
  在Internet上IP地址和主机名是一一对应的,通过域名解析可以由主机名得到机器的IP,由于机器名更接近自然语言,容易记忆,所以使用比IP地址广泛,但是对机器而言只有IP地址才是有效的标识符。
  通常一台主机上总是有很多个进程需要网络资源进行网络通讯。网络通讯的对象准确的讲不是主机,而应该是主机中运行的进程。这时候光有主机名或IP地址来标识这么多个进程显然是不够的。端口号就是为了在一台主机上提供更多的网络资源而采取得一种手段,也是TCP层提供的一种机制。只有通过主机名或IP地址和端口号的组合才能唯一的确定网络通讯中的对象:进程。
服务类型是在TCP层上面的应用层的概念。基于TCP/IP协议可以构建出各种复杂的应用,服务类型是那些已经被标准化了的应用,一般都是网络服务器(软件)。读者可以编写自己的基于网络的服务器,但都不能被称作标准的服务类型。
11.1.3两类传输协议:TCP;UDP
  尽管TCP/IP协议的名称中只有TCP这个协议名,但是在TCP/IP的传输层同时存在TCP和UDP两个协议。
TCP是Tranfer Control Protocol的简称,是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。
  UDP是User Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
  下面我们对这两种协议做简单比较:
  使用UDP时,每个数据报中都给出了完整的地址信息,因此无需要建立发送方和接收方的连接。对于TCP协议,由于它是一个面向连接的协议,在socket之间进行数据传输之前必然要建立连接,所以在TCP中多了一个连接建立的时间。
  使用UDP传输数据时是有大小限制的,每个被传输的数据报必须限定在64KB之内。而TCP没有这方面的限制,一旦连接建立起来,双方的socket就可以按统一的格式传输大量的数据。UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。而TCP是一个可靠的协议,它确保接收方完全正确地获取发送方所发送的全部数据。
  总之,TCP在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输。相比之下UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。
  读者可能要问,既然有了保证可靠传输的TCP协议,为什么还要非可靠传输的UDP协议呢?主要的原因有两个。一是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此TCP传输的效率不如UDP高。二是在许多应用中并不需要保证严格的传输可靠性,比如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。
11.2 基于URL的高层次Java网络编程
11.2.1一致资源定位器URL
URL(Uniform Resource Locator)是一致资源定位器的简称,它表示Internet上某一资源的地址。通过URL我们可以访问Internet上的各种网络资源,比如最常见的WWW,FTP站点。浏览器通过解析给定的URL可以在网络上查找相应的文件或其他资源。
  URL是最为直观的一种网络定位方法。使用URL符合人们的语言习惯,容易记忆,所以应用十分广泛。而且在目前使用最为广泛的TCP/IP中对于URL中主机名的解析也是协议的一个标准,即所谓的域名解析服务。使用URL进行网络编程,不需要对协议本身有太多的了解,功能也比较弱,相对而言是比较简单的,所以在这里我们先介绍在Java中如何使用URL进行网络编程来引导读者入门。
11.2.2 URL的组成
protocol://resourceName
  协议名(protocol)指明获取资源所使用的传输协议,如http、ftp、gopher、file等,资源名(resourceName)则应该是资源的完整地址,包括主机名、端口号、文件名或文件内部的一个引用。例如:
  http://www.sun.com/ 协议名://主机名
  http:///home/welcome.html 协议名://机器名+文件名
  http://www.gamelan.com:80/Gamelan/network.html#BOTTOM 协议名://机器名+端口号+文件名+内部引用
  端口号是和Socket编程相关的一个概念,初学者不必在此深究,在后面会有详细讲解。内部引用是HTML中的标记,有兴趣的读者可以参考有关HTML的书籍。
11.2.3 创建一个URL
为了表示URL, 中实现了类URL。我们可以通过下面的构造方法来初始化一个URL对象:
  (1) public URL (String spec);
     通过一个表示URL地址的字符串可以构造一个URL对象。
     URL urlBase=new URL("http://www. /")
  (2) public URL(URL context, String spec);
     通过基URL和相对URL构造一个URL对象。
     URL net263=new URL ("http://www./");
     URL index263=new URL(net263, "index.html")
  (3) public URL(String protocol, String host, String file);
     new URL("http", "www.gamelan.com", "/pages/Gamelan.net. html");
  (4) public URL(String protocol, String host, int port, String file);
     URL gamelan=new URL("http", "www.gamelan.com", 80, "Pages/Gamelan.network.html");
  注意:类URL的构造方法都声明抛弃非运行时异常(MalformedURLException),因此生成URL对象时,我们必须要对这一异常进行处理,通常是用try-catch语句进行捕获。格式如下:
try{
     URL myURL= new URL(…)
  }catch (MalformedURLException e){
  …
  //exception handler code here
  …
  }
11.2.4 解析一个URL
一个URL对象生成后,其属性是不能被改变的,但是我们可以通过类URL所提供的方法来获取这些属性:
   public String getProtocol() 获取该URL的协议名。
   public String getHost() 获取该URL的主机名。
   public int getPort() 获取该URL的端口号,如果没有设置端口,返回-1。
   public String getFile() 获取该URL的文件名。
   public String getRef() 获取该URL在文件中的相对位置。
   public String getQuery() 获取该URL的查询信息。
   public String getPath() 获取该URL的路径
   public String getAuthority() 获取该URL的权限信息
   public String getUserInfo() 获得使用者的信息
   public String getRef() 获得该URL的锚
  下面的例子中,我们生成一个URL对象,并获取它的各个属性。
  import .*;
  import java.io.*;
  public class ParseURL{
  public static void main (String [] args) throws Exception{
  URL Aurl=new URL("http://java.sun.com:80/docs/books/");
  URL tuto=new URL(Aurl,"tutorial.intro.html#DOWNLOADING");
  System.out.println("protocol="+ tuto.getProtocol());
  System.out.println("host ="+ tuto.getHost());
  System.out.println("filename="+ tuto.getFile());
  System.out.println("port="+ tuto.getPort());
  System.out.println("ref="+tuto.getRef());
  System.out.println("query="+tuto.getQuery());
  System.out.println("path="+tuto.getPath());
  System.out.println("UserInfo="+tuto.getUserInfo());
  System.out.println("Authority="+tuto.getAuthority());
  }
  }
  执行结果为:
   protocol=http host =java.sun.com filename=/docs/books/tutorial.intro.html
   port=80
   ref=DOWNLOADING
   query=null
   path=/docs/books/tutorial.intro.html
   UserInfo=null
   Authority=java.sun.com:80
11.2.5 从URL读取WWW网络资源
当我们得到一个URL对象后,就可以通过它读取指定的WWW资源。这时我们将使用URL的方法openStream(),其定义为:
         InputStream openStream();
  方法openSteam()与指定的URL建立连接并返回InputStream类的对象以从这一连接中读取数据。
  public class URLReader {
  public static void main(String[] args) throws Exception {
                      //声明抛出所有异常
    URL tirc = new URL("http://www.tirc1.cs.tsinghua./");
                      //构建一URL对象
    BufferedReader in = new BufferedReader(new InputStreamReader(tirc.openStream()));
    //使用openStream得到一输入流并由此构造一个BufferedReader对象
    String inputLine;
    while ((inputLine = in.readLine()) != null)
                 //从输入流不断的读数据,直到读完为止
       System.out.println(inputLine); //把读入的数据打印到屏幕上
    in.close(); //关闭输入流
  }
  }
11.2.6 通过URLConnetction连接WWW
通过URL的方法openStream(),我们只能从网络上读取数据,如果我们同时还想输出数据,例如向服务器端的CGI程序发送一些数据,我们必须先与URL建立连接,然后才能对其进行读写,这时就要用到类URLConnection了。CGI是公共网关接口(Common Gateway Interface)的简称,它是用户浏览器和服务器端的应用程序进行连接的接口,有关CGI程序设计,请读者参考有关书籍。
类URLConnection也在包中定义,它表示Java程序和URL在网络上的通信连接。当与一个URL建立连接时,首先要在一个URL对象上通过方法openConnection()生成对应的URLConnection对象。例如下面的程序段首先生成一个指向地址http://edu.chinaren.com/index.shtml的对象,然后用openConnection()打开该URL对象上的一个连接,返回一个URLConnection对象。如果连接过程失败,将产生IOException.
  Try{
    URL netchinaren = new URL ("http://edu.chinaren.com/index.shtml");
    URLConnectonn tc = netchinaren.openConnection();
  }catch(MalformedURLException e){ //创建URL()对象失败
  …
  }catch (IOException e){ //openConnection()失败
  …
  }
  类URLConnection提供了很多方法来设置或获取连接参数,程序设计时最常使用的是getInputStream()和getOurputStream(),其定义为:
     InputSteram getInputSteram();
     OutputSteram getOutputStream();
  通过返回的输入/输出流我们可以与远程对象进行通信。看下面的例子:
  URL url =new URL ("http://www./cgi-bin/backwards");
  //创建一URL对象
  URLConnectin con=url.openConnection();
  //由URL对象获取URLConnection对象
  DataInputStream dis=new DataInputStream (con.getInputSteam());
  //由URLConnection获取输入流,并构造DataInputStream对象
  PrintStream ps=new PrintSteam(con.getOutupSteam());
  //由URLConnection获取输出流,并构造PrintStream对象
  String line=dis.readLine(); //从服务器读入一行
  ps.println("client…"); //向服务器写出字符串 "client…"
  其中backwards为服务器端的CGI程序。实际上,类URL的方法openSteam()是通过URLConnection来实现的。它等价于
    openConnection().getInputStream();
基于URL的网络编程在底层其实还是基于下面要讲的Socket接口的。WWW,FTP等标准化的网络服务都是基于TCP协议的,所以本质上讲URL编程也是基于TCP的一种应用。
11.3 基于Socket(套接字)的低层次Java网络编程
11.3.1 Socket通讯
网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。
在传统的UNIX环境下可以操作TCP/IP协议的接口不止Socket一个,Socket所支持的协议种类也不光TCP/IP一种,因此两者之间是没有必然联系的。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。
说Socket编程是低层次网络编程并不等于它功能不强大,恰恰相反,正因为层次低,Socket编程比基于URL的网络编程提供了更强大的功能和更灵活的控制,但是却要更复杂一些。由于Java本身的特殊性,Socket编程在Java中可能已经是层次最低的网络编程接口,在Java中要直接操作协议中更低的层次,需要使用Java的本地方法调用(JNI),在这里就不予讨论了。
11.3.2 Socket通讯的一般过程
前面已经提到Socket通常用来实现C/S结构。
使用Socket进行Client/Server程序设计的一般连接过程是这样的:Server端Listen(监听)某个端口是否有连接请求,Client端向Server端发出Connect(连接)请求,Server端向Client端发回Accept(接受)消息。一个连接就建立起来了。Server端和Client端都可以通过Send,Write等方法与对方通信。
  
  对于一个功能齐全的Socket,都要包含以下基本结构,其工作过程包含以下四个基本的步骤:
  (1) 创建Socket;
  (2) 打开连接到Socket的输入/出流;
  (3) 按照一定的协议对Socket进行读/写操作;
  (4) 关闭Socket.
  第三步是程序员用来调用Socket和实现程序功能的关键步骤,其他三步在各种程序中基本相同。
  以上4个步骤是针对TCP传输而言的,使用UDP进行传输时略有不同,在后面会有具体讲解。
11.3.3 创建Socket
java在包中提供了两个类Socket和ServerSocket,分别用来表示双向连接的客户端和服务端。这是两个封装得非常好的类,使用很方便。其构造方法如下:
  Socket(InetAddress address, int port);
  Socket(InetAddress address, int port, boolean stream);
  Socket(String host, int prot);
  Socket(String host, int prot, boolean stream);
  Socket(SocketImpl impl)
  Socket(String host, int port, InetAddress localAddr, int localPort)
  Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
  ServerSocket(int port);
  ServerSocket(int port, int backlog);
  ServerSocket(int port, int backlog, InetAddress bindAddr)
其中address、host和port分别是双向连接中另一方的IP地址、主机名和端口号,stream指明socket是流socket还是数据报socket,localPort表示本地主机的端口号,localAddr和bindAddr是本地机器的地址(ServerSocket的主机地址),impl是socket的父类,既可以用来创建serverSocket又可以用来创建Socket。count则表示服务端所能支持的最大连接数。例如:
  Socket client = new Socket("127.0.01.", 80);
  ServerSocket server = new ServerSocket(80);
注意,在选择端口时,必须小心。每一个端口提供一种特定的服务,只有给出正确的端口,才能获得相应的服务。0~1023的端口号为系统所保留,例如http服务的端口号为80,telnet服务的端口号为21,ftp服务的端口号为23, 所以我们在选择端口号时,最好选择一个大于1023的数以防止发生冲突。
在创建socket时如果发生错误,将产生IOException,在程序中必须对之作出处理。所以在创建Socket或ServerSocket是必须捕获或抛出异常。
11.3.4 客户端的Socket
下面是一个典型的创建客户端Socket的过程。
   try{
     Socket socket=new Socket("127.0.0.1",4700);
     //127.0.0.1是TCP/IP协议中默认的本机地址
   }catch(IOException e){
     System.out.println("Error:"+e);
   }
  这是最简单的在客户端创建一个Socket的一个小程序段,也是使用Socket进行网络通讯的第一步,程序相当简单,在这里不作过多解释了。在后面的程序中会用到该小程序段。
11.3.5 服务器端的ServerSocket
下面是一个典型的创建Server端ServerSocket的过程。
  ServerSocket server=null;
  try {
     server=new ServerSocket(4700);
     //创建一个ServerSocket在端口4700监听客户请求
  }catch(IOException e){
     System.out.println("can not listen to :"+e);
  }
  Socket socket=null;
  try {
    socket=server.accept();
    //accept()是一个阻塞的方法,一旦有客户请求,它就会返回一个Socket对象用于同客户进行交互
  }catch(IOException e){
    System.out.println("Error:"+e);
  }
  以上的程序是Server的典型工作模式,只不过在这里Server只能接收一个请求,接受完后Server就退出了。实际的应用中总是让它不停的循环接收,一旦有客户请求,Server总是会创建一个服务线程来服务新来的客户,而自己继续监听。程序中accept()是一个阻塞函数,所谓阻塞性方法就是说该方法被调用后,将等待客户的请求,直到有一个客户启动并请求连接到相同的端口,然后accept()返回一个对应于客户的socket。这时,客户方和服务方都建立了用于通信的socket,接下来就是由各个socket分别打开各自的输入/输出流。
11.3.6 打开输入/出流
类Socket提供了方法getInputStream ()和getOutStream()来得到对应的输入/输出流以进行读/写操作,这两个方法分别返回InputStream和OutputSteam类对象。为了便于读/写数据,我们可以在返回的输入/输出流对象上建立过滤流,如DataInputStream、DataOutputStream或PrintStream类对象,对于文本方式流对象,可以采用InputStreamReader和OutputStreamWriter、PrintWirter等处理。
  例如:
  PrintStream os=new PrintStream(new BufferedOutputStreem(socket.getOutputStream()));
  DataInputStream is=new DataInputStream(socket.getInputStream());
  PrintWriter out=new PrintWriter(socket.getOutStream(),true);
  BufferedReader in=new ButfferedReader(new InputSteramReader(Socket.getInputStream()));
  输入输出流是网络编程的实质性部分,具体如何构造所需要的过滤流,要根据需要而定,能否运用自如主要看读者对Java中输入输出部分掌握如何。
11.3.7 关闭Socket
每一个Socket存在时,都将占用一定的资源,在Socket对象使用完毕时,要其关闭。关闭Socket可以调用Socket的Close()方法。在关闭Socket之前,应将与Socket相关的所有的输入/输出流全部关闭,以释放所有的资源。而且要注意关闭的顺序,与Socket相关的所有的输入/输出该首先关闭,然后再关闭Socket。
  os.close();
  is.close();
  socket.close();
尽管Java有自动回收机制,网络资源最终是会被释放的。但是为了有效的利用资源,建议读者按照合理的顺序主动释放资源。
11.3.8 简单的Client/Server程序设计
  下面我们给出一个用Socket实现的客户和服务器交互的典型的C/S结构的演示程序,读者通过仔细阅读该程序,会对前面所讨论的各个概念有更深刻的认识。程序的意义请参考注释。
1. 客户端程序
  import java.io.*;
  import .*;
  public class TalkClient {
    public static void main(String args[]) {
      try{
        Socket socket=new Socket("127.0.0.1",4700);
        //向本机的4700端口发出客户请求
        BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
        //由系统标准输入设备构造BufferedReader对象
        PrintWriter os=new PrintWriter(socket.getOutputStream());
        //由Socket对象得到输出流,并构造PrintWriter对象
        BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
        //由Socket对象得到输入流,并构造相应的BufferedReader对象
        String readline;
        readline=sin.readLine(); //从系统标准输入读入一字符串
        while(!readline.equals("bye")){
        //若从标准输入读入的字符串为 "bye"则停止循环
          os.println(readline);
          //将从系统标准输入读入的字符串输出到Server
          os.flush();
          //刷新输出流,使Server马上收到该字符串
          System.out.println("Client:"+readline);
          //在系统标准输出上打印读入的字符串
          System.out.println("Server:"+is.readLine());
          //从Server读入一字符串,并打印到标准输出上
          readline=sin.readLine(); //从系统标准输入读入一字符串
        } //继续循环
        os.close(); //关闭Socket输出流
        is.close(); //关闭Socket输入流
        socket.close(); //关闭Socket
      }catch(Exception e) {
        System.out.println("Error"+e); //出错,则打印出错信息
      }
  }
}
 2. 服务器端程序
  import java.io.*;
  import .*;
  import java.applet.Applet;
  public class TalkServer{
    public static void main(String args[]) {
      try{
        ServerSocket server=null;
        try{
          server=new ServerSocket(4700);
        //创建一个ServerSocket在端口4700监听客户请求
        }catch(Exception e) {
          System.out.println("can not listen to:"+e);
        //出错,打印出错信息
        }
        Socket socket=null;
        try{
          socket=server.accept();
          //使用accept()阻塞等待客户请求,有客户
          //请求到来则产生一个Socket对象,并继续执行
        }catch(Exception e) {
          System.out.println("Error."+e);
          //出错,打印出错信息
        }
        String line;
        BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
         //由Socket对象得到输入流,并构造相应的BufferedReader对象
        PrintWriter os=newPrintWriter(socket.getOutputStream());
         //由Socket对象得到输出流,并构造PrintWriter对象
        BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
         //由系统标准输入设备构造BufferedReader对象
        System.out.println("Client:"+is.readLine());
        //在标准输出上打印从客户端读入的字符串
        line=sin.readLine();
        //从标准输入读入一字符串
        while(!line.equals("bye")){
        //如果该字符串为 "bye",则停止循环
          os.println(line);
          //向客户端输出该字符串
          os.flush();
          //刷新输出流,使Client马上收到该字符串
          System.out.println("Server:"+line);
          //在系统标准输出上打印读入的字符串
          System.out.println("Client:"+is.readLine());
          //从Client读入一字符串,并打印到标准输出上
          line=sin.readLine();
          //从系统标准输入读入一字符串
        }  //继续循环
        os.close(); //关闭Socket输出流
        is.close(); //关闭Socket输入流
        socket.close(); //关闭Socket
        server.close(); //关闭ServerSocket
      }catch(Exception e){
        System.out.println("Error:"+e);
        //出错,打印出错信息
      }
    }
  }
从上面的两个程序中我们可以看到,socket四个步骤的使用过程。读者可以分别将Socket使用的四个步骤的对应程序段选择出来,这样便于读者对socket的使用有进一步的了解。
  读者可以在单机上试验该程序,最好是能在真正的网络环境下试验该程序,这样更容易分辨输出的内容和客户机,服务器的对应关系。同时也可以修改该程序,提供更为强大的功能,或更加满足读者的意图。
11.3.9 支持多客户的client/server程序设计
  前面提供的Client/Server程序只能实现Server和一个客户的对话。在实际应用中,往往是在服务器上运行一个永久的程序,它可以接收来自其他多个客户端的请求,提供相应的服务。为了实现在服务器方给多个客户提供服务的功能,需要对上面的程序进行改造,利用多线程实现多客户机制。服务器总是在指定的端口上监听是否有客户请求,一旦监听到客户请求,服务器就会启动一个专门的服务线程来响应该客户的请求,而服务器本身在启动完线程之后马上又进入监听状态,等待下一个客户的到来。
  客户端的程序和上面程序是完全一样的,读者如果仔细阅读过上面的程序,可以跳过不读,把主要精力集中在Server端的程序上。
  1. 客户端程序:MultiTalkClient.java
  import java.io.*;
  import .*;
  public class MultiTalkClient {
   public static void main(String args[]) {
    try{
      Socket socket=new Socket("127.0.0.1",4700);
      //向本机的4700端口发出客户请求
      BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
      //由系统标准输入设备构造BufferedReader对象
      PrintWriter os=new PrintWriter(socket.getOutputStream());
      //由Socket对象得到输出流,并构造PrintWriter对象
      BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
      //由Socket对象得到输入流,并构造相应的BufferedReader对象
      String readline;
      readline=sin.readLine(); //从系统标准输入读入一字符串
      while(!readline.equals("bye")){
      //若从标准输入读入的字符串为 "bye"则停止循环
        os.println(readline);
        //将从系统标准输入读入的字符串输出到Server
        os.flush();
        //刷新输出流,使Server马上收到该字符串
        System.out.println("Client:"+readline);
        //在系统标准输出上打印读入的字符串
        System.out.println("Server:"+is.readLine());
        //从Server读入一字符串,并打印到标准输出上
        readline=sin.readLine();
        //从系统标准输入读入一字符串
      } //继续循环
      os.close(); //关闭Socket输出流
      is.close(); //关闭Socket输入流
      socket.close(); //关闭Socket
    }catch(Exception e) {
      System.out.println("Error"+e); //出错,则打印出错信息
    }
  }
}
 2. 服务器端程序: MultiTalkServer.java
  import java.io.*;
  import .*;
  import ServerThread;
  public class MultiTalkServer{
   static int clientnum=0; //静态成员变量,记录当前客户的个数
   public static void main(String args[]) throws IOException {
    ServerSocket serverSocket=null;
    boolean listening=true;
    try{
      serverSocket=new ServerSocket(4700);
      //创建一个ServerSocket在端口4700监听客户请求
    }catch(IOException e) {
      System.out.println("Could not listen on port:4700.");
      //出错,打印出错信息
      System.exit(-1); //退出
    }
    while(listening){ //永远循环监听
      new ServerThread(serverSocket.accept(),clientnum).start();
      //监听到客户请求,根据得到的Socket对象和
       客户计数创建服务线程,并启动之
      clientnum++; //增加客户计数
    }
    serverSocket.close(); //关闭ServerSocket
  }
}
 3. 程序ServerThread.java
  import java.io.*;
  import .*;
  public class ServerThread extends Thread{
   Socket socket=null; //保存与本线程相关的Socket对象
   int clientnum; //保存本进程的客户计数
   public ServerThread(Socket socket,int num) { //构造函数
    this.socket=socket; //初始化socket变量
    clientnum=num+1; //初始化clientnum变量
   }
   public void run() { //线程主体
    try{
      String line;
      BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
  //由Socket对象得到输入流,并构造相应的BufferedReader对象
      PrintWriter os=newPrintWriter(socket.getOutputStream());
      //由Socket对象得到输出流,并构造PrintWriter对象
      BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
      //由系统标准输入设备构造BufferedReader对象
      System.out.println("Client:"+ clientnum +is.readLine());
      //在标准输出上打印从客户端读入的字符串
      line=sin.readLine();
      //从标准输入读入一字符串
      while(!line.equals("bye")){
      //如果该字符串为 "bye",则停止循环
        os.println(line);
        //向客户端输出该字符串
        os.flush();
        //刷新输出流,使Client马上收到该字符串
        System.out.println("Server:"+line);
        //在系统标准输出上打印该字符串
        System.out.println("Client:"+ clientnum +is.readLine());
        //从Client读入一字符串,并打印到标准输出上
        line=sin.readLine();
        //从系统标准输入读入一字符串
      } //继续循环
      os.close(); //关闭Socket输出流
      is.close(); //关闭Socket输入流
      socket.close(); //关闭Socket
      server.close(); //关闭ServerSocket
     }catch(Exception e){
      System.out.println("Error:"+e);
      //出错,打印出错信息
     }
   }
 }
  这个程序向读者展示了网络应用中最为典型的C/S结构,我们可以用下面的图来描述这样一种模型:
  通过以上的学习,读者应该对Java的面向流的网络编程有了一个比较全面的认识,这些都是基于TCP的应用,后面我们将介绍基于UDP的Socket编程。
11.3.10 据报Datagram通讯
前面在介绍TCP/IP协议的时候,我们已经提到,在TCP/IP协议的传输层除了TCP协议之外还有一个UDP协议,相比而言UDP的应用不如TCP广泛,几个标准的应用层协议HTTP,FTP,SMTP…使用的都是TCP协议。但是,随着计算机网络的发展,UDP协议正越来越来显示出其威力,尤其是在需要很强的实时交互性的场合,如网络游戏,视频会议等,UDP更是显示出极强的威力,下面我们就介绍一下Java环境下如何实现UDP网络传输。
11.3.11 什么是Datagram
所谓数据报(Datagram)就跟日常生活中的邮件系统一样,是不能保证可靠的寄到的,而面向链接的TCP就好比电话,双方能肯定对方接受到了信息。在本章前面,我们已经对UDP和TCP进行了比较,在这里再稍作小节:
  TCP,可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。
  UDP,不可靠,差错控制开销较小,传输大小限制在64K以下,不需要建立连接。
总之,这两种协议各有特点,应用的场合也不同,是完全互补的两个协议,在TCP/IP协议中占有同样重要的地位,要学好网络编程,两者缺一不可。
11.3.12 Datagram通讯的表示方法:DatagramSocket;DatagramPacket
包中提供了两个类DatagramSocket和DatagramPacket用来支持数据报通信,DatagramSocket用于在程序之间建立传送数据报的通信连接, DatagramPacket则用来表示一个数据报。先来看一下DatagramSocket的构造方法:
   DatagramSocket();
   DatagramSocket(int prot);
   DatagramSocket(int port, InetAddress laddr)
其中,port指明socket所使用的端口号,如果未指明端口号,则把socket连接到本地主机上一个可用的端口。laddr指明一个可用的本地地址。给出端口号时要保证不发生端口冲突,否则会生成SocketException类异常。注意:上述的两个构造方法都声明抛弃非运行时异常SocketException,程序中必须进行处理,或者捕获、或者声明抛弃。
用数据报方式编写client/server程序时,无论在客户方还是服务方,首先都要建立一个DatagramSocket对象,用来接收或发送数据报,然后使用DatagramPacket类对象作为传输数据的载体。下面看一下DatagramPacket的构造方法 :
   DatagramPacket(byte buf[],int length);
   DatagramPacket(byte buf[], int length, InetAddress addr, int port);
   DatagramPacket(byte[] buf, int offset, int length);
   DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port);
其中,buf中存放数据报数据,length为数据报中数据的长度,addr和port旨明目的地址,offset指明了数据报的位移量。
在接收数据前,应该采用上面的第一种方法生成一个DatagramPacket对象,给出接收数据的缓冲区及其长度。然后调用DatagramSocket 的方法receive()等待数据报的到来,receive()将一直等待,直到收到一个数据报为止。
  DatagramPacket packet=new DatagramPacket(buf, 256);
  Socket.receive (packet);
发送数据前,也要先生成一个新的DatagramPacket对象,这时要使用上面的第二种构造方法,在给出存放发送数据的缓冲区的同时,还要给出完整的目的地址,包括IP地址和端口号。发送数据是通过DatagramSocket的方法send()实现的,send()根据数据报的目的地址来寻径,以传递数据报。
  DatagramPacket packet=new DatagramPacket(buf, length, address, port);
  Socket.send(packet);
在构造数据报时,要给出InetAddress类参数。类InetAddress在包中定义,用来表示一个Internet地址,我们可以通过它提供的类方法getByName()从一个表示主机名的字符串获取该主机的IP地址,然后再获取相应的地址信息。
8.3.13 基于UDP的简单的Client/Server程序设计
  有了上面的知识,我们就可以来构件一个基于UDP的C/S 网络传输模型
1. 客户方程序 QuoteClient.java
  import java.io.*;
  import .*;
  import java.util.*;
  public class QuoteClient {
   public static void main(String[] args) throws IOException
   {
    if(args.length!=1) {
    //如果启动的时候没有给出Server的名字,那么出错退出
     System.out.println("Usage:java QuoteClient ");
     //打印出错信息
     return; //返回
    }
    DatagramSocket socket=new DatagramSocklet();
    //创建数据报套接字
    Byte[] buf=new byte[256]; //创建缓冲区
    InetAddress address=InetAddress.getByName(args [0]);
//由命令行给出的第一个参数默认为Server的名字,通过它得到Server的IP信息
    DatagramPacket packet=new DatagramPacket (buf, buf.length, address, 4445);
    //创建DatagramPacket对象
    socket.send(packet); //发送
    packet=new DatagramPacket(buf,buf.length);
    //创建新的DatagramPacket对象,用来接收数据报
    socket.receive(packet); //接收
    String received=new String(packet.getData());
    //根据接收到的字节数组生成相应的字符串
    System.out.println("Quote of the Moment:"+received );
    //打印生成的字符串
    socket.close(); //关闭套接口
   }
 }
 2. 服务器方程序:QuoteServer.java
  public class QuoteServer{
   public static void main(String args[]) throws java.io.IOException
   {
    new QuoteServerThread().start();
    //启动一个QuoteServerThread线程
   }
  }
 3. 程序QuoteServerThread.java
  import java.io.*;
  import .*;
  import java.util.*;
  //服务器线程
  public class QuoteServerThread extends Thread
  {
  protected DatagramSocket socket=null;
  //记录和本对象相关联的DatagramSocket对象
  protected BufferedReader in=null;
  //用来读文件的一个Reader
  protected boolean moreQuotes=true;
  //标志变量,是否继续操作
  public QuoteServerThread() throws IOException {
  //无参数的构造函数
    this("QuoteServerThread");
    //以QuoteServerThread为默认值调用带参数的构造函数
  }
  public QuoteServerThread(String name) throws IOException {
    super(name); //调用父类的构造函数
    socket=new DatagramSocket(4445);
    //在端口4445创建数据报套接字
    try{
      in= new BufferedReader(new FileReader(" one-liners.txt"));
      //打开一个文件,构造相应的BufferReader对象
    }catch(FileNotFoundException e) { //异常处理
      System.err.println("Could not open quote file. Serving time instead.");
       //打印出错信息
    }
  }
  public void run() //线程主体
  {
    while(moreQuotes) {
     try{
       byte[] buf=new byte[256]; //创建缓冲区
       DatagramPacket packet=new DatagramPacket(buf,buf.length);
       //由缓冲区构造DatagramPacket对象
       socket.receive(packet); //接收数据报
       String dString=null;
       if(in= =null) dString=new Date().toString();
       //如果初始化的时候打开文件失败了,
       //则使用日期作为要传送的字符串
       else dString=getNextQuote();
       //否则调用成员函数从文件中读出字符串
       buf=dString.getByte();
       //把String转换成字节数组,以便传送
       InetAddress address=packet.getAddress();
       //从Client端传来的Packet中得到Client地址
       int port=packet.getPort(); //和端口号
       packet=new DatagramPacket(buf,buf.length,address,port);
       //根据客户端信息构建DatagramPacket
       socket.send(packet); //发送数据报
      }catch(IOException e) { //异常处理
       e.printStackTrace(); //打印错误栈
       moreQuotes=false; //标志变量置false,以结束循环
      }
    }
    socket.close(); //关闭数据报套接字
  }
  protected String getNextQuotes(){
  //成员函数,从文件中读数据
    String returnValue=null;
    try {
       if((returnValue=in.readLine())= =null) {
       //从文件中读一行,如果读到了文件尾
       in.close( ); //关闭输入流
       moreQuotes=false;
       //标志变量置false,以结束循环
       returnValue="No more quotes. Goodbye.";
       //置返回值
       } //否则返回字符串即为从文件读出的字符串
    }catch(IOEception e) { //异常处理
       returnValue="IOException occurred in server";
       //置异常返回值
    }
    return returnValue; //返回字符串
  }
}
  可以看出使用UDP和使用TCP在程序上还是有很大的区别的。一个比较明显的区别是,UDP的Socket编程是不提供监听功能的,也就是说通信双方更为平等,面对的接口是完全一样的。但是为了用UDP实现C/S结构,在使用UDP时可以使用DatagramSocket.receive()来实现类似于监听的功能。因为receive()是阻塞的函数,当它返回时,缓冲区里已经填满了接受到的一个数据报,并且可以从该数据报得到发送方的各种信息,这一点跟accept()是很相象的,因而可以根据读入的数据报来决定下一步的动作,这就达到了跟网络监听相似的效果。
11.3.14 用数据报进行广播通讯
  DatagramSocket只允许数据报发送一个目的地址,包中提供了一个类MulticastSocket,允许数据报以广播方式发送到该端口的所有客户。MulticastSocket用在客户端,监听服务器广播来的数据。
我们对上面的程序作一些修改,利用MulticastSocket实现广播通信。新程序完成的功能是使同时运行的多个客户程序能够接收到服务器发送来的相同的信息,显示在各自的屏幕上。
1. 客户方程序:MulticastClient.java
  import java.io.*;
  import .*;
  import java.util.*;
  public class MulticastClient {
    public static void main(String args[]) throws IOException
    {
     MulticastSocket socket=new MulticastSocket(4446);
     //创建4446端口的广播套接字
     InetAddress address=InetAddress.getByName("230.0.0.1");
     //得到230.0.0.1的地址信息
     socket.joinGroup(address);
     //使用joinGroup()将广播套接字绑定到地址上
     DatagramPacket packet;
     for(int i=0;i<5;i++) {
       byte[] buf=new byte[256];
       //创建缓冲区
       packet=new DatagramPacket(buf,buf.length);
       //创建接收数据报
       socket.receive(packet); //接收
       String received=new String(packet.getData());
       //由接收到的数据报得到字节数组,
       //并由此构造一个String对象
       System.out.println("Quote of theMoment:"+received);
       //打印得到的字符串
     } //循环5次
     socket.leaveGroup(address);
     //把广播套接字从地址上解除绑定
     socket.close(); //关闭广播套接字
   }
 }
 2. 服务器方程序:MulticastServer.java
  public class MulticastServer{
    public static void main(String args[]) throws java.io.IOException
    {
      new MulticastServerThread().start();
      //启动一个服务器线程
    }
  }
 3. 程序MulticastServerThread.java
  import java.io.*;
  import .*;
  import java.util.*;
  public class MulticastServerThread extends QuoteServerThread
  //从QuoteServerThread继承得到新的服务器线程类MulticastServerThread
  {
    Private long FIVE_SECOND=5000; //定义常量,5秒钟
    public MulticastServerThread(String name) throws IOException
    {
      super("MulticastServerThread");
      //调用父类,也就是QuoteServerThread的构造函数
    }
    public void run() //重写父类的线程主体
    {
     while(moreQuotes) {
     //根据标志变量判断是否继续循环
      try{
        byte[] buf=new byte[256];
        //创建缓冲区
        String dString=null;
        if(in==null) dString=new Date().toString();
        //如果初始化的时候打开文件失败了,
        //则使用日期作为要传送的字符串
        else dString=getNextQuote();
        //否则调用成员函数从文件中读出字符串
        buf=dString.getByte();
        //把String转换成字节数组,以便传送send it
        InetAddress group=InetAddress.getByName("230.0.0.1");
        //得到230.0.0.1的地址信息
        DatagramPacket packet=new DatagramPacket(buf,buf.length,group,4446);
        //根据缓冲区,广播地址,和端口号创建DatagramPacket对象
        socket.send(packet); //发送该Packet
        try{
          sleep((long)(Math.random()*FIVE_SECONDS));
          //随机等待一段时间,0~5秒之间
        }catch(InterruptedException e) { } //异常处理
      }catch(IOException e){ //异常处理
        e.printStackTrace( ); //打印错误栈
        moreQuotes=false; //置结束循环标志
      }
    }
    socket.close( ); //关闭广播套接口
   }
 }
  至此,Java网络编程这一章已经讲解完毕。读者通过学习,应该对网络编程有了一个清晰的认识,可能对某些概念还不是十分的清楚,还是需要更多的实践来进一步掌握。编程语言的学习不同于一般的学习,及其强调实践的重要性。读者应该对URL网络编程,Socket中的TCP,UDP编程进行大量的练习才能更好的掌握本章中所提到的一些概念,才能真正学到Java网络编程的精髓!
  最后几个小节所举的例子,读者务必要亲自试验一下,如果遇到问题,想办法解决之。最好能根据自己的意图加以改进。这样才能更好的理解这几个程序,理解其中所包含的编程思想。第九章 输入输出系统
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: Java中的输入/输出处理是通过使用流技术,用统一的接口表示而实现的。输入/输出流中,最常见的是对文件的处理。Java语言中提供专门处理文件和目录的类,例如:java.io.File,java.io.FileInputStream,java.io.FileOutputStream,java.io.RandomAccessFile和接口java.io.FilenameFilter。输入/输出流根据处理的内容,分为字符流和字节流两种,其中字节流是以byte为基本处理单位的流;而字符流是以16位的Unicode码为处理单位的流。
下载一
【课前思考】
  1. 字节流和字符流的基类各是什么?
  2. 什么是对象的串行化?对象串行化的作用是什么?
【学习目标】
  本讲主要讲述了java语言中的输入/输出的处理,通过本讲的学习,同学们可以编写更为完善的java程序。
【学习指南】
  仔细阅读本章各知识点的内容, 深刻理解 java 语言中输入/输出流的概念,掌握处理问题的方法,多练习,多上机。
【难 重 点】
 重点:
  1.遇到实际问题时,要根据需要正确使用各种输入/输出流,特别是对中文使用适当的字符输入流。
  2.正确使用对象串行化的方法。
 难点:
  1.处理字符流时,其构造方法的参数是一个字节流。
  2.对象串行化的概念。
【知 识 点】
  9.1 I/O 流概述
   9.1.1 I/O流的层次
   9.1.2 InputStream 和OutputStream
   9.1.3 I/O中的异常
  9.2 文件处理
   9.2.1 文件描述
   9.2.2 文件的顺序处理
   9.2.3 随机访问文件
  9.3 过滤流
   9.3.1 几种常见的过滤流
  9.4 字符流的处理
   9.4.1 Reader和Writer
   9.4.2 InputStreamReader和OutputStreamWriter
   9.4.3 BufferedReader和BufferedWriter
  9.5 对象的串行化
   9.5.1 串行化的定义
   9.5.2 串行化方法
   9.5.3 串行化的注意事项
 
第九章 输入输出系统
9.1 I/O 流概述
输入/输出处理是程序设计中非常重要的一部分,比如从键盘读取数据、从文件中读取数据或向文件中写数据等等。
Java把这些不同类型的输入、输出源抽象为流(stream),用统一接口来表示,从而使程序简单明了。
Jdk 提供了包java.io,其中包括一系列的类来实现输入/输出处理。下面我们对java.io包的内容进行概要的介绍。
9.1.1 I/O流的层次
1.字节流 ( G:\study\java\java_qh\JAVA\text\ch04\se04\right4_4_1.htm" \l "01" \t "MyFrame )
2.字符流 ( G:\study\java\java_qh\JAVA\text\ch04\se04\right4_4_1.htm" \l "02" \t "MyFrame )
3.对象流 ( G:\study\java\java_qh\JAVA\text\ch04\se04\right4_4_1.htm" \l "03" \t "MyFrame )
4.其它 ( G:\study\java\java_qh\JAVA\text\ch04\se04\right4_4_1.htm" \l "04" \t "MyFrame )
1.字节流:
  从InputStream和OutputStream派生出来的一系列类。这类流以字节(byte)为基本处理单位。
  ◇ InputStream、OutputStream
  ◇ FileInputStream、FileOutputStream
  ◇ PipedInputStream、PipedOutputStream
  ◇ ByteArrayInputStream、ByteArrayOutputStream
  ◇ FilterInputStream、FilterOutputStream
  ◇ DataInputStream、DataOutputStream
  ◇ BufferedInputStream、BufferedOutputStream
 2.字符流:
  从Reader和Writer派生出的一系列类,这类流以16位的Unicode码表示的字符为基本处理单位。
  ◇ Reader、Writer
  ◇ InputStreamReader、OutputStreamWriter
  ◇ FileReader、FileWriter
  ◇ CharArrayReader、CharArrayWriter
  ◇ PipedReader、PipedWriter
  ◇ FilterReader、FilterWriter
  ◇ BufferedReader、BufferedWriter
  ◇ StringReader、StringWriter
 3.对象流
  ◇ ObjectInputStream、ObjectOutputStream
 4.其它
  ◇ 文件处理:
  File、RandomAccessFile;
  ◇ 接口
  DataInput、DataOutput、ObjectInput、ObjectOutput;
9.1.2 InputStream 和OutputStream
  1. InputStream ( G:\study\java\java_qh\JAVA\text\ch04\se04\right4_4_2.htm" \l "01" \t "MyFrame )
  2. OutputStream ( G:\study\java\java_qh\JAVA\text\ch04\se04\right4_4_2.htm" \l "02" \t "MyFrame )
1.InputStream
  ◇ 从流中读取数据:
  int read( ); //读取一个字节,返回值为所读的字节
  int read( byte b[ ] ); //读取多个字节,放置到字节数组b中,通常
              //读取的字节数量为b的长度,返回值为实际
              //读取的字节的数量
  int read( byte b[ ], int off, int len ); //读取len个字节,放置到以下标off开始
//数组b中,返回值为实际读取的字节的数量
  int available( );   //返回值为流中尚未读取的字节的数量
  long skip( long n ); //读指针跳过n个字节不读,返回值为实际
             //跳过的字节数量
  ◇ 关闭流:
  close( ); //流操作完毕后必须关闭
  ◇ 使用输入流中的标记:
  void mark( int readlimit ); //记录当前读指针所在位置,readlimit表示读指针
//读出readlimit个字节后所标记的指针位置才失效
  void reset( );     //把读指针重新指向用mark方法所记录的位置
  boolean markSupported( ); //当前的流是否支持读指针的记录功能
  有关每个方法的使用,详见java API。
 2.OutputStream
  ◇ 输出数据:
  void write( int b );   //往流中写一个字节b
  void write( byte b[ ] ); //往流中写一个字节数组b
  void write( byte b[ ], int off, int len ); //把字节数组b中从
              //下标off开始,长度为len的字节写入流中
  ◇ flush( )       //刷空输出流,并输出所有被缓存的字节
  由于某些流支持缓存功能,该方法将把缓存中所有内容强制输出到流中。
  ◇ 关闭流:
   close( );       //流操作完毕后必须关闭
9.1.3 I/O中的异常
进行I/O操作时可能会产生I/O异常,属于非运行时异常,应该在程序中处理。如:FileNotFoundException, EOFException, IOException
9.2 文件处理
I/O处理中,最常见的是对文件的操作,java.io包中有关文件处理的类有:File、FileInputStream、FileOutputStream、RamdomAccessFile和FileDescriptor;接口有:FilenameFilter。
9.2.1 文件描述
  类File提供了一种与机器无关的方式来描述一个文件对象的属性。下面我们介绍类File中提供的各种方法。
◇ 文件或目录的生成
  public File(String path);
  public File(String path,String name);//path是路径名,name是文件名
  public File(File dir,String name);//dir是路径名,name是文件名
文件名的处理
String getName( ); //得到一个文件的名称(不包括路径)
  String getPath( ); //得到一个文件的路径名
  String getAbsolutePath( );//得到一个文件的绝对路径名
  String getParent( ); //得到一个文件的上一级目录名
  String renameTo(File newName); //将当前文件名更名为给定文件的完整路径
 ◇ 文件属性测试
  boolean exists( ); //测试当前File对象所指示的文件是否存在
  boolean canWrite( );//测试当前文件是否可写
  boolean canRead( );//测试当前文件是否可读
  boolean isFile( ); //测试当前文件是否是文件(不是目录)
  boolean isDirectory( ); //测试当前文件是否是目录
 ◇ 普通文件信息和工具
  long lastModified( );//得到文件最近一次修改的时间
  long length( ); //得到文件的长度,以字节为单位
  boolean delete( ); //删除当前文件
 ◇ 目录操作
  boolean mkdir( ); //根据当前对象生成一个由该对象指定的路径
  String list( ); //列出当前目录下的文件
 【例9-1】
  import java.io.*; //引入java.io包中所有的类
  public class FileFilterTest{
    public static void main(String args[]){
     File dir=new File("d://ex"); //用File 对象表示一个目录
     Filter filter=new Filter("java"); //生成一个名为java的过滤器
     System.out.println("list java files in directory "+dir);
     String files[]=dir.list(filter); //列出目录dir下,文件后缀名
                       为java的所有文件
     for(int i=0;i      File f=new File(dir,files[i]); //为目录dir 下的文件或目录
                       创建一个File 对象
       if(f.isFile()) //如果该对象为后缀为java的文件,
                则打印文件名
        System.out.println("file "+f);
       else
        System.out.println("sub directory "+f ); //如果是目录
                             则打印目录名
     }
    }
   }
   class Filter implements FilenameFilter{
    String extent;
    Filter(String extent){
     this.extent=extent;
    }
    public boolean accept(File dir,String name){
     return name.endsWith("."+extent); //返回文件的后缀名
    }
   }
9.2.2 文件的顺序处理
类FileInputStream和FileOutputStream用来进行文件I/O处理,由它们所提供的方法可以打开本地主机上的文件,并进行顺序的读/写。例如,下列的语句段是顺序读取文件名为text的文件里的内容,并显示在控制台上面,直到文件结束为止。
FileInputStream fis;
   try{
    fis = new FileInputStream( "text" );
   System.out.print( "content of text is : ");
     int b;
     while( (b=fis.read())!=-1 ) //顺序读取文件text里的内容并赋值
                    给整型变量b,直到文件结束为止。
     {              
       System.out.print( (char)b );
     }
   }catch( FileNotFoundException e ){
   System.out.println( e );
   }catch( IOException e ){
   System.out.println( e );
   }
9.2.3 随机访问文件
对于InputStream 和OutputStream 来说,它们的实例都是顺序访问流,也就是说,只能对文件进行顺序地读/写。随机访问文件则允许对文件内容进行随机读/写。在java中,类RandomAccessFile 提供了随机访问文件的方法。类RandomAccessFile的声明为:
public class RandomAccessFile extends Object implements DataInput, DataOutput
接口DataInput 中定义的方法主要包括从流中读取基本类型的数据、读取一行数据、或者读取指定长度的字节数。如:readBoolean( )、readInt( )、readLine( )、readFully( ) 等。
接口DataOutput 中定义的方法主要是向流中写入基本类型的数据、或者写入一定长度的字节数组。如:writeChar( )、writeDouble( )、write( ) 等。 下面详细介绍RandomAccessFile类中的方法。
 ◇ 构造方法:
   RandomAccessFile(String name,String mode); //name是文件名,mode
          //是打开方式,例如"r"表示只读,"rw"表示可读写,"
  RandomAccessFile(File file,String mode); //file是文件对象
 ◇ 文件指针的操作
  long getFilePointer( ); //用于得到当前的文件指针
  void seek( long pos ); //用于移动文件指针到指定的位置
  int skipBytes( int n ); //使文件指针向前移动指定的n个字节
9.3 过滤流
过滤流在读/写数据的同时可以对数据进行处理,它提供了同步机制,使得某一时刻只有一个线程可以访问一个I/O流,以防止多个线程同时对一个I/O流进行操作所带来的意想不到的结果。类FilterInputStream和FilterOutputStream分别作为所有过滤输入流和输出流的父类。
过滤流类层次:
    
  为了使用一个过滤流,必须首先把过滤流连接到某个输入/出流上,通常通过在构造方法的参数中指定所要连接的输入/出流来实现。例如:
  FilterInputStream( InputStream in );
  FilterOutputStream( OutputStream out );
9.3.1 几种常见的过滤流
◇ BufferedInputStream和BufferedOutputStream
    缓冲流,用于提高输入/输出处理的效率。
◇ DataInputStream 和 DataOutputStream
 不仅能读/写数据流,而且能读/写各种的java语言的基本类型,如:boolean,int,float等。
◇ LineNumberInputStream
    除了提供对输入处理的支持外,LineNumberInputStream可以记录当前的行号。
◇ PushbackInputStream
    提供了一个方法可以把刚读过的字节退回到输入流中,以便重新再读一遍。
◇ PrintStream
    打印流的作用是把Java语言的内构类型以其字符表示形式送到相应的输出流。
9.4 字符流的处理
java中提供了处理以16位的Unicode码表示的字符流的类,即以Reader和Writer 为基类派生出的一系列类。
9.4.1 Reader和Writer
  1.Reader类是处理所有字符流输入类的父类。 ( G:\study\java\java_qh\JAVA\text\ch04\se07\right4_7_1.htm" \l "01" \t "MyFrame )
  2. Writer类是处理所有字符流输出类的父类。 ( G:\study\java\java_qh\JAVA\text\ch04\se07\right4_7_1.htm" \l "02" \t "MyFrame )
这两个类是抽象类,只是提供了一系列用于字符流处理的接口,不能生成这两个类的实例,只能通过使用由它们派生出来的子类对象来处理字符流。
 1.Reader类是处理所有字符流输入类的父类。
  ◇ 读取字符
  public int read() throws IOException; //读取一个字符,返回值为读取的字符
  public int read(char cbuf[]) throws IOException;
  public abstract int read(char cbuf[],int off,int len) throws IOException;
  
  ◇ 标记流
  public boolean markSupported(); //判断当前流是否支持做标记
  public void mark(int readAheadLimit) throws IOException;
   //给当前流作标记,最多支持readAheadLimit个字符的回溯。
  public void reset() throws IOException; //将当前流重置到做标记处
  ◇ 关闭流
  public abstract void close() throws IOException;
 2. Writer类是处理所有字符流输出类的父类。
  ◇ 向输出流写入字符
  public void write(int c) throws IOException;
  //将整型值c的低16位写入输出流
  public void write(char cbuf[]) throws IOException;
   //将字符数组cbuf[]写入输出流
  public abstract void write(char cbuf[],int off,int len) throws IOException;
  //将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流
  public void write(String str) throws IOException;
  //将字符串str中的字符写入输出流
  public void write(String str,int off,int len) throws IOException;
  //将字符串str 中从索引off开始处的len个字符写入输出流
  ◇ flush( )
  刷空输出流,并输出所有被缓存的字节。
  ◇ 关闭流
  public abstract void close() throws IOException;
9.4.2 InputStreamReader和OutputStreamWriter
  java.io包中用于处理字符流的最基本的类,用来在字节流和字符流之间作为中介。
◇ 生成流对象
  public InputStreamReader(InputStream in);
  
  public OutputStreamWriter(OutputStream out);
  
public OutputStreamWriter(OutputStream out,String enc) throws UnsupportedEncodingException; //enc是编码方式
  InputStreamReader和OutputStreamWriter的方法:
  ◇ 读入和写出字符
  基本同Reader和Writer。
  ◇ 获取当前编码方式
  public String getEncoding();
  ◇ 关闭流
  public void close() throws IOException;
9.4.3 BufferedReader和BufferedWriter
  生成流对象 ( G:\study\java\java_qh\JAVA\text\ch04\se07\right4_7_3.htm" \l "01" \t "MyFrame )
  读入/写出字符 ( G:\study\java\java_qh\JAVA\text\ch04\se07\right4_7_3.htm" \l "02" \t "MyFrame )
◇ 生成流对象
  public BufferedReader(Reader in); //使用缺省的缓冲区大小
  public BufferedReader(Reader in, int sz); //sz为缓冲区的大小
  public BufferedWriter(Writer out);
  public BufferedWriter(Writer out, int sz);
 ◇ 读入/写出字符
  除了Reader和Writer中提供的基本的读写方法外,增加对整行字符的处理。
  public String readLine() throws IOException; //读一行字符
  public void newLine() throws IOException; //写一行字符
【例9-2】
  import java.io.*;
  public class NumberInput{
   public static void main(String args[]){
    try{
      InputStreamReader ir;
      BufferedReader in;
      ir=new InputStreamReader(System.in);
      //从键盘接收了一个字符串的输入,并创建了一个字符输入流的对象
      in=new BufferedReader(ir);
      String s=in.readLine();
      //从输入流in中读入一行,并将读取的值赋值给字符串变量s
      System.out.println("Input value is: "+s);
      int i = Integer.parseInt(s);//转换成int型
      i*=2;
      System.out.println("Input value changed after doubled: "+i);
    }catch(IOException e)
    {System.out.println(e);}
   }
  }
    运行结果
D:\>java NumberInput
123
Input value is 123
Input value changed after doubled: 246
  注意:在读取字符流时,如果不是来自于本地的,比如说来自于网络上某处的与本地编码方式不同的机器,那么我们在构造输入流时就不能简单地使用本地缺省的编码方式,否则读出的字符就不正确;为了正确地读出异种机上的字符,我们应该使用下述方式构造输入流对象:
  ir = new InputStreamReader(is, "8859_1");
采用ISO 8859_1编码方式,这是一种映射到ASCII码的编码方式,可以在不同平台之间正确转换字符。
9.5 对象的串行化(Serialization)
9.5.1 串行化的定义
1. 什么是串行化
对象的寿命通常随着生成该对象的程序的终止而终止。有时候,可能需要将对象的状态保存下来,在需要时再将对象恢复。我们把对象的这种能记录自己的状态以便将来再生的能力,叫做对象的持续性(persistence)。对象通过写出描述自己状态的数值来记录自己,这个过程叫对象的串行化(Serialization)。
2. 串行化的目的
串行化的目的是为java的运行环境提供一组特性,其主要任务是写出对象实例变量的数值。
9.5.2 串行化方法
在java.io包中,接口Serializable用来作为实现对象串行化的工具,只有实现了Serializable的类的对象才可以被串行化。
1. 定义一个可串行化对象
 public class Student implements Serializable{
    int id; //学号
    String name; //姓名
    int age; //年龄
    String department //系别
    public Student(int id,String name,int age,String department){
     this.id = id;
     this.name = name;
     this.age = age;
     this.department = department;
    }
  }
 2. 构造对象的输入/输出流
  要串行化一个对象,必须与一定的对象输入/输出流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。
java.io包中,提供了ObjectInputStream和ObjectOutputStream将数据流功能扩展至可读写对象。在ObjectInputStream中用readObject()方法可以直接读取一个对象,ObjectOutputStream中用writeObject()方法可以直接将对象保存到输出流中。
  Student stu=new Student(981036,"Liu Ming",18, "CSD");
  FileOutputStream fo=new FileOutputStream("data.ser");
  //保存对象的状态
  ObjectOutputStream so=new ObjectOutputStream(fo);
  try{
    so.writeObject(stu);
    so.close();
    }catch(IOException e )
      {System.out.println(e);}
  FileInputStream fi=new FileInputStream("data.ser");
  ObjectInputStream si=new ObjectInputStream(fi);
  //恢复对象的状态
  try{
    stu=(Student)si.readObject();
    si.close();
    }catch(IOException e )
  {System.out.println(e);}
  在这个例子中,我们首先定义一个类Student,实现了 Serializable接口,然后通过对象输出流的writeObject()方法将Student对象保存到文件data.ser中。之后,通过对象输入流的readObject()方法从文件data.ser中读出保存下来的Student对象。
9.5.3 串行化的注意事项
1.串行化能保存的元素
只能保存对象的非静态成员变量,不能保存任何的成员方法和静态的成员变量,而且串行化保存的只是变量的值,对于变量的任何修饰符,都不能保存。
2.transient关键字
对于某些类型的对象,其状态是瞬时的,这样的对象是无法保存其状态的,例如一个Thread对象,或一个FileInputStream对象,对于这些字段,我们必须用transient关键字标明
3. 定制串行化
缺省的串行化机制,对象串行化首先写入类数据和类字段的信息,然后按照名称的上升排列顺序写入其数值。如果想自己明确地控制这些数值的写入顺序和写入种类,必须定义自己的读取数据流的方式。就是在类的定义中重写writeObject()和readObject()方法。
例如可在4.8.2的例子中,加入重写的writeObject()和readObject()方法,对Student 类定制其串行化。
 private void writeObject(ObjectOutputStream out)throws IOException
  {
    out.writeInt(id);
    out.writeInt(age);
    out.writeUTF(name);
    out.writeUTF(department);
  }
  private void readObject(ObjectInputStream in)throws IOException
  {
    id=in.readInt();
    age=in.readInt();
    name=in.readUTF();
    department=in.readUTF();
  }第一章:
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: java语言的产生起源于Sun Microsystems公司为消费电子产品上应用程序的开发寻找一门编程语言的过程中,而随着互联网时代的到来,原有的Oak软件就顺理成章的改造成java语言推向了市场,其跨平台、面相对象、安全等特点使其得到广泛的应用。通过在不同的软硬件上实现的java虚拟机,java的字节码文件就可以跨平台的进行运行,无用内存自动回收器也给程序员带来了极大的方便。java程序以两种方式进行运行,一种是通过java虚拟机进行直接运行的java application,另一种是通过浏览器进行运行的java applet,但是无论是何种方式,java都是一门纯粹的面向对象的编程语言。面向对象编程的思路认为程序都是对象的组合,因此要克服面向过程编程的思路,直接按照对象和类的思想去组织程序,面向对象所具有的封装性、继承性、多态性等特点使其具有强大的生命力。Sun公司为全世界java开发人员提供了一套免费的软件开发包Java2 SDK,也称为JDK,它不仅是java的开发平台,还是java的运行平台。java源程序存放在.java文件中,可以通过任意一个文本编辑器编辑产生,源程序经过"javac"命令编译过后,就生成了相应的.class文件,而用"java"命令就可以运行.class文件。作为面向对象编程人员来说,大体可以分为两种:类创建者和应用程序员,应用程序员是类的使用者。所以对程序的可读性和API帮助文档就有要求,java语言本身有一套约定成俗的编程规范,同时程序员首先要学会阅读系统API帮助文档,还要学会生成自己编写的程序的API帮助文档。
下载一
【课前思考】
  1. 一门新的语言的产生是否需要借鉴以前的编程语言?
  2. 在java语言出现之前是否存在其它跨平台的语言?
  3. 有哪些编程语言是面向对象的?而哪些编程语言是面向过程的?从编程思路上存在着哪些本质差别?C++语言是面向对象的还是面向过程的?
  4. 一段优秀的程序代码是否应该是可读性极强的?程序员之间是否应该遵循相同的编程规范?
  5. 一个程序员编好的代码如果需要让别的程序员使用,如何提供该代码的使用说明?
【学习目标】
  了解java语言产生的历史和工作原理,掌握java语言作为一门面向对象编程语言的基本编程思路,初步接触java程序的两种方式:java application和java applet,掌握java程序的结构、编程规范,学习阅读java帮助文档,并安装java开发环境和帮助文档,最后要学会编写小的java程序并生成相应的帮助文档。
【学习指南】
  跳过面向过程的编程思路,直接进入到面向对象的编程方式。
【难 重 点】
  java虚拟机
  无用内存自动回收器
  java语言的特点
  面向对象的编程思路
  java编程规范
  java类库帮助文档的浏览和生成
【知 识 点】
  1.1 java语言的发展史
   1.1.1 java语言在互联网时代获得巨大成功
   1.1.2 java语言的产生
  1.2 java的工作原理
   1.2.1 java虚拟机
   1.2.2 无用内存自动回收机制
   1.2.3 代码安全性检查机制
   1.2.4 java语言的特点
   1.2.5 java平台-不断扩展的计算平台
  1.3 一切都是对象
   1.3.1 面向过程
   1.3.2 面向对象
  1.4 构建java程序
   1.4.1 第一个java application
   1.4.2 java程序的编辑
   1.4.3 java程序的编译
   1.4.4 java application的执行
   1.4.5 第一个java applet
   1.4.6 java applet的执行
  1.5 java程序规范
   1.5.1 java源程序结构
   1.5.2 java编程规范
   1.5.3 java帮助文档
   1.5.4 java注释
  1.6 建立java开发环境
   1.6.1 安装java开发包JDK
   1.6.2 安装java帮助文档
   1.6.3 配置类路径
java语言的发展史
1.1.1java语言在互联网时代获得巨大成功
  大家想一想,在PC下用windows编写的程序能够不做修改就直接拿到UNIX系统上运行吗?显然是不可以的,因为程序的执行最终必须转换成为计算机硬件的机器指令来执行,专门为某种计算机硬件和操作系统编写的程序是不能够直接放到另外的计算机硬件上执行的,至少要做移植工作。要想让程序能够在不同的计算机上能够运行,就要求程序设计语言是能够跨越各种软件和硬件平台的,而java满足了这一需求。
 1995年,美国Sun Microsystems公司正式向IT业界推出了java语言,该语言具有安全、跨平台、面向对象、简单、适用于网络等显著特点,当时以web为主要形式的互联网正在迅猛发展,java语言的出现迅速引起所有程序员和软件公司的极大关注,程序员们纷纷尝试用java语言编写网络应用程序,并利用网络把程序发布到世界各地进行运行。包括IBM、Oracle、微软、Netscape、Apple、SGI等大公司纷纷与Sun Microsystems公司签订合同,授权使用java平台技术。微软公司总裁比尔盖茨先生在经过研究后认为"java语言是长时间以来最卓越的程序设计语言"。目前,java语言已经成为最流行的网络编程语言,截止到2001年中,全世界大约有310万java程序员,许多大学纷纷开设java课程,java正逐步成为世界上程序员最多的编程语言。
  在经历了以大型机为代表的集中计算模式和以PC机为代表的分散计算模式之后,互联网的出现使得计算模式进入了网络计算时代。网络计算模式的一个特点是计算机是异构的,即计算机的类型和操作系统是不一样的,例如SUN工作站的硬件是SPARC体系,软件是UNIX中的Solaris操作系统,而PC机的硬件是INTEL体系,操作系统是windows或者是Linux,因此相应的编程语言基本上只是适用于单机系统,例如COBOL、FORTRAN、C、C++等等;网络计算模式的另一个特点是代码可以通过网络在各种计算机上进行迁移,这就迫切需要一种跨平台的编程语言,使得用它编写的程序能够在网络中的各种计算机上能够正常运行,java就是在这种需求下应运而生的。正是因为java语言符合了互联网时代的发展要求,才使它获得了巨大的成功。
1.1.2 java语言的产生
  俗话说:"有心栽花花不成,无心插柳柳成荫"。Sun公司绝没想到本想用于消费电子产品开发的编程语言却率先在网络中得到了广泛应用,但是也可以说是"东方不亮西方亮",正是因为java语言在设计目标上的正确性使得java语言"是金字总会发光的"。C语言是面向过程的语言,也是使用率非常高的语言;而面向对象的思想引入到编程语言之后,C语言就被改造成为面向对象的C++语言,得到了广泛的应用。但是C++语言必须兼容C语言,因此C++语言是面向过程和面向对象混合的语言。
  
  java语言产生于C++语言之后,是完全的面向对象的编程语言,充分吸取了C++语言的优点,采用了程序员所熟悉的C和C++语言的许多语法,同时又去掉了C语言中指针、内存申请和释放等影响程序健壮性的部分,可以说java语言是站在C++语言这个"巨人的肩膀上"前进的。
  java语言的一个目标是跨平台,因此采用了解释执行而不是编译执行的运行环境,在执行过程中根据所在的不同的硬件平台把程序解释为当前的机器码,实现跨平台运行。而动态下载程序代码的机制完全是为了适应网络计算的特点,程序可以根据需要把代码实时的从服务器中下载过来执行,在此之前还没有任何一种语言能够支持这一点。
  java是印尼的一个小岛,盛产咖啡,而程序员往往喜欢喝咖啡,因此取名为java语言。看来,目前java这杯咖啡已经飘香在世界各地。 
任何事物的产生既有必然的原因也有偶然的因素,java语言的出现也验证了这一点。1991年,美国Sun Microsystems公司的某个研究小组为了能够在消费电子产品上开发应用程序,积极寻找合适的编程语言。消费电子产品种类繁多,包括PDA、机顶盒、手机等等,即使是同一类消费电子产品所采用的处理芯片和操作系统也不相同,也存在着跨平台的问题。当时最流行的编程语言是C和C++语言,Sun公司的研究人员就考虑是否可以采用C++语言来编写消费电子产品的应用程序,但是研究表明,对于消费电子产品而言C++语言过于复杂和庞大,并不适用,安全性也并不令人满意。于是,Bill Joy先生领导的研究小组就着手设计和开发出一种语言,称之为Oak。该语言采用了许多C语言的语法,提高了安全性,并且是面向对象的语言,但是Oak语言在商业上并未获得成功。时间转到了1995年,互联网在世界上蓬勃发展,Sun公司发现Oak语言所具有的跨平台、面向对象、安全性高等特点非常符合互联网的需要,于是改进了该语言的设计,要达到如下几个目标:
   ◇ 创建一种面向对象的程序设计语言,而不是面向过程的语言;
   ◇ 提供一个解释执行的程序运行环境,是程序代码独立于平台;
   ◇ 吸收C和C++的优点,使程序员容易掌握;
   ◇ 去掉C和C++中影响程序健壮性的部分,使程序更安全,例如指针、内存申请和释放;
   ◇ 实现多线程,使得程序能够同时执行多个任务;
   ◇ 提供动态下载程序代码的机制;
   ◇ 提供代码校验机制以保证安全性;
  
  最终,Sun公司给该语言取名为java语言,造就了一代成功的编程语言。
1.2 java的工作原理
1.2.1 java虚拟机
 java虚拟机是软件模拟的计算机,可以在任何处理器上(无论是在计算机中还是在其它电子设备中)安全并且兼容的执行保存在.class文件中的字节码。java虚拟机的"机器码"保存在.class文件中,有时也可以称之为字节码文件。java程序的跨平台主要是指字节码文件可以在任何具有java虚拟机的计算机或者电子设备上运行,java虚拟机中的java解释器负责将字节码文件解释成为特定的机器码进行运行。java源程序需要通过编译器编译成为.class文件(字节码文件),java程序的编译和执行过程如动画所示。
  但是,java虚拟机的建立需要针对不同的软硬件平台做专门的实现,既要考虑处理器的型号,也要考虑操作系统的种类。如下图所示,目前在SPARC结构、X86结构、MIPS和PPC等嵌入式处理芯片上、在UNIX、Linux、windows和部分实时操作系统上都有java虚拟机的实现。
              
1.2.2 无用内存自动回收机制
  在程序的执行过程中,部分内存在使用过后就处于废弃状态,如果不及时进行无用内存的回收,就会导致内存泄漏,进而导致系统崩溃。在C++语言中是由程序员进行内存回收的,程序员需要在编写程序的时候把不再使用的对象内存释放掉;但是这种人为的管理内存释放的方法却往往由于程序员的疏忽而致使内存无法回收,同时也增加了程序员的工作量。而在java运行环境中,始终存在着一个系统级的线程,专门跟踪内存的使用情况,定期检测出不再使用的内存,并进行自动回收,避免了内存的泄露,也减轻了程序员的工作量。
1.2.3 代码安全性检查机制
  安全和方便总是相对矛盾的。java编程语言的出现使得客户端机器可以方便的从网络上下载java程序到本机上运行,但是如何保证该java程序不携带病毒或者不怀有其它险恶目的呢?如果java语言不能保证执行的安全性,那么它就不可能存活到今天。虽然有时候少数程序员会抱怨说applet连文件系统也不能访问,但是正是各种安全措施的实行才确保了java语言的生存。
字节码的执行需要经过三个步骤,首先由类装载器(class loader)负责把类文件(.class文件)加载到java虚拟机中,在此过程需要检验该类文件是否符合类文件规范;其次字节码校验器(bytecode verifier)检查该类文件的代码中是否存在着某些非法操作,例如applet程序中写本机文件系统的操作;如果字节码校验器检验通过,由java解释器负责把该类文件解释成为机器码进行执行。java虚拟机采用的是"沙箱"运行模式,即把java程序的代码和数据都限制在一定内存空间里执行,不允许程序访问该内存空间外的内存,如果是applet程序,还不允许访问客户端机器的文件系统。
1.2.4 Java语言的特点
  1. 简单、面向对象和为人所熟悉
java的简单首先体现在精简的系统上,力图用最小的系统实现足够多的功能;对硬件的要求不高,在小型的计算机上便可以良好的运行。和所有的新一代的程序设计语言一样,java也采用了面向对象技术并更加彻底,所有的java程序和applet程序均是对象,封装性实现了模块化和信息隐藏,继承性实现了代码的复用,用户可以建立自己的类库。而且java采用的是相对简单的面向对象技术,去掉了运算符重载、多继承的复杂概念,而采用了单一继承、类强制转换、多线程、引用(非指针)等方式。无用内存自动回收机制也使得程序员不必费心管理内存,是程序设计更加简单,同时大大减少了出错的可能。java语言采用了C语言中的大部分语法,熟悉C语言的程序员会发现java语言在语法上与C语言极其相似。
  2. 鲁棒并且安全
java语言在编译及运行程序时,都要进行严格的检查。作为一种强制类型语言,java在编译和连接时都进行大量的类型检查,防止不匹配问题的发生。如果引用一个非法类型、或执行一个非法类型操作,java将在解释时指出该错误。在java程序中不能采用地址计算的方法通过指针访问内存单元,大大减少了错误发生的可能性;而且java的数组并非用指针实现,这样就可以在检查中避免数组越界的发生。无用内存自动回收机制也增加了java的鲁棒性。作为网络语言,java必须提供足够的安全保障,并且要防止病毒的侵袭。java在运行应用程序时,严格检查其访问数据的权限,比如不允许网络上的应用程序修改本地的数据。下载到用户计算机中的字节代码在其被执行前要经过一个核实工具,一旦字节代码被核实,便由java解释器来执行,该解释器通过阻止对内存的直接访问来进一步提高java的安全性。同时java极高的鲁棒性也增强了java的安全性。
  3. 结构中立并且可以移植
网络上充满了各种不同类型的机器和操作系统,为使java程序能在网络的任何地方运行,java编译器编译生成了与体系结构无关的字节码结构文件格式。任何种类的计算机,只有在其处理器和操作系统上有java运行时环境,字节码文件就可以在该计算机上运行。即使是在单一系统的计算机上,结构中立也有非常大的作用。随着处理器结构的不断发展变化,程序员不得不编写各种版本的程序以在不同的处理器上运行,这使得开发出能够在所有平台上工作的软件集合是不可能的。而使用java将使同一版本的应用程序可以运行在所有的平台上。
  体系结构的中立也使得java系统具有可移植性。java运行时系统可以移植到不同的处理器和操作系统上,java的编译器是由java语言实现的,解释器是由java语言和标准C语言实现的,因此可以较为方便的进行移植工作。
  4. 高性能
虽然java是解释执行的,但它仍然具有非常高的性能,在一些特定的CPU上,java字节码可以快速的转换成为机器码进行执行。而且java字节码格式的设计就是针对机器码的转换,实际转换时相当简便,自动的寄存器分配与编译器对字节码的一些优化可使之生成高质量的代码。随着java虚拟机的改进和"即时编译"(just in time)技术的出现使得java的执行速度有了更大的提高。
  5. 解释执行、多线程并且是动态的
为易于实现跨平台性,java设计成为解释执行,字节码本身包含了许多编译时生成的信息,使连接过程更加简单。而多线程使应用程序可以同时进行不同的操作,处理不同的事件。在多线程机制中,不同的线程处理不同的任务,互不干涉,不会由于某一任务处于等待状态而影响了其它任务的执行,这样就可以容易的实现网络上的实时交互操作。java在执行过程中,可以动态的加载各种类库,这一特点使之非常适合于网络运行,同时也非常有利于软件的开发,即使是更新类库也不必重新编译使用这一类库的应用程序。
  如果你了解C语言和C++语言,可以参考下列java与C/C++语言的比较,如果不了解C语言和C++语言,可以忽略本部分知识。
  a. 全局变量
  java程序不能定义程序的全局变量,而类中的公共、静态变量就相当于这个类的全局变量。这样就使全局变量封装在类中,保证了安全性,而在C/C++语言中,由于不加封装的全局变量往往会由于使用不当而造成系统的崩溃。
  b. 条件转移指令
  C/C++语言中用goto语句实现无条件跳转,而java语言没有goto语言,通过例外处理语句try、catch、finally来取代之,提高了程序的可读性,也增强了程序的鲁棒性。
  c. 指针
  指针是C/C++语言中最灵活,但也是最容易出错的数据类型。用指针进行内存操作往往造成不可预知的错误,而且,通过指针对内存地址进行显示类型转换后,可以类的私有成员,破坏了安全性。在java中,程序员不能进行任何指针操作,同时java中的数组是通过类来实现的,很好的解决了数组越界这一C/C++语言中不做检查的缺点。
  d. 内存管理
  在C语言中,程序员使用库函数malloc()和free()来分配和释放内存,C++语言中则是运算符new和delete。再次释放已经释放的内存块或者释放未被分配的内存块,会造成系统的崩溃,而忘记释放不再使用的内存块也会逐渐耗尽系统资源。在java中,所有的数据结构都是对象,通过运算符new分配内存并得到对象的使用权。无用内存回收机制保证了系统资源的完整,避免了内存管理不周而引起的系统崩溃。
  e. 数据类型的一致性
  在C/C++语言中,不同的平台上,编译器对简单的数据类型如int、float等分别分配不同的字节数。例如:int在IBM PC上为16位,在VAX-11上就为32位,导致了代码数据的不可移植。在java中,对数据类型的位数分配总是固定的,而不管是在任何的计算机平台上。因此就保证了java数据的平台无关性和可移植性。
  f. 类型转换
  在C/C++语言中,可以通过指针进行任意的类型转换,不安全因素大大增加。而在java语言中系统要对对象的处理进行严格的相容性检查,防止不安全的转换。
  g. 头文件
  在C/C++语言中使用头文件声明类的原型和全局变量及库函数等,在大的系统中,维护这些头文件是非常困难的。java不支持头文件,类成员的类型和访问权限都封装在一个类中,运行时系统对访问进行控制,防止非法的访问。同时,java中用import语句与其它类进行通信,以便访问其它类的对象。
  h. 结构和联合
  C/C++语言中用结构和联合来表示一定的数据结构,但是由于其成员均为公有的,安全性上存在问题。java不支持结构和联合,通过类把数据结构及对该数据的操作都封装在类里面。
  i. 预处理
  C/C++语言中有宏定义,而用宏定义实现的代码往往影响程序的可读性,而java不支持宏定义。
1.2.5 java平台-不断扩展的计算平台
  java不仅是编程语言,还是一个开发平台,java技术给程序员提供了许多工具:编译器、解释器、文档生成器和文件打包工具等等。同时java还是一个程序发布平台,有两种主要的"发布环境",首先java运行时环境(java runtime environment,简称JRE)包含了完整的类文件包,其次许多主要的浏览器都提供了java解释器和运行时环境。目前Sun公司把java平台划分成J2EE、J2SE、J2ME三个平台,针对不同的市场目标和设备进行定位。J2EE是Java2 Enterprise Edition,主要目的是为企业计算提供一个应用服务器的运行和开发平台。J2EE本身是一个开放的标准,任何软件厂商都可以推出自己的符合J2EE标准的产品,使用户可以有多种选择。IBM、Oracle、BEA、HP等29家已经推出了自己的产品,其中尤以BEA公司的weglogic产品和IBM公司的websphare最为著名。J2EE将逐步发展成为可以与微软的.NET战略相对抗的网络计算平台。J2SE是Java2 Standard Edition,主要目的是为台式机和工作站提供一个开发和运行的平台。我们在学习java的过程中,主要是采用J2SE来进行开发。J2ME是Java2 Micro Edition,主要是面向消费电子产品,为消费电子产品提供一个java的运行平台,使得java程序能够在手机、机顶盒、PDA等产品上运行。上述三个java平台的关系如右图所示。
1.3 一切都是对象
1.3.1 面向过程
  面向对象的第一个原则是把数据和对该数据的操作都封装在一个类中,在程序设计时要考虑多个对象及其相互间的关系。有些功能并不一定由一个程序段完全实现,可以让其它对象来实现,在本例中就由类Max完成求最大值的功能。而面向对象的另外一个好处是实现代码的重复使用,例如其它的程序中如果需要求最大值的功能,只需要通过类Max的对象就可以达到目的。但是如果象面向过程的代码段那样把求最大值的算法都实现在该代码段中,则无法复用。
1.3.2 面向对象
  1. 所有的东西都是对象。
 可以将对象想象成为一种新型变量,它保存着数据,而且还可以对自身数据进行操作。例如类Max中保留着数据的最大值,同时还有方法updateMax根据新加入的price值产生最新的最大值,还有getMax方法返回数据的最大值。
   2. 程序是一大堆对象的组合。
通过消息传递,各对象知道自己应该做些什么。如果需要让对象做些事情,则须向该对象"发送一条消息"。具体来说,可以将消息想象成为一个调用请求,它调用的是从属于目标对象的一个方法。例如上面面向对象的程序段应该是属于某个类的,比如说是属于类Shopping,则Shopping中就包含了类Max的对象max,调用方法updateMax就相当于Shopping对象对max对象发出一条指令"updateMax",要求对象max重新计算最大值。
   3. 每个对象都有自己的存储空间。
可容纳其它对象,或者说通过封装现有的对象,可以产生新型对象。因此,尽管对象的概念非常简单,但是经过封装以后却可以在程序中达到任意高的复杂程度。
   4. 每个对象都属于某个类。
根据语法,每个对象都是某个"类"的一个"实例"。一个类的最重要的的特征就是"能将什么消息发给它?",也就是类本身有哪些操作。例如max是类Max的实例。
1.4 构建java程序
1.4.1 第一个java application
  java程序分为java application(java 应用程序)和java applet(java小应用程序)两种。下面让我们编写一个java应用程序,它能够利用来自java标准库的System对象的多种方法,打印出与当前运行的系统有关的资料。其中"//"代表一种注释方式,表示从这个符号开始到这行结束的所有内容都是注释。在每个程序文件的开头,如果这个文件的代码中用到了系统所提供的额外的类,就必须放置一个import语句。说它是额外的是指一个特殊的类库"java.lang"会自动导入到每个java文件。
//这是我们的第一个java application,该程序保存在文件Property.java中
 import java.util.*;
            
 public class Property { //程序员给这个类取名为Property
  public static void main(String args[]){ //main是类的主方法
  System.out.println(new Date( )); //在命令行下面打印出日期
  Properties p=System.getProperties( ); //获得系统的Properties对象p
  p.list(System.out); //在命令行下打印出p中的各个系统变量的值
  System.out.println("--- Memory Usage:");
  Runtime rt=Runtime.getRuntime( ); //获得系统的Runtime对象rt
  System.out.println("Total Memory= "
          + rt.totalMemory( ) //打印总内存大小
          +" Free Memory = "
          +rt.freeMemory( )); //打印空闲内存大小
    }
  }  
  
  在java中,程序都是以类的方式组织的,java源文件都保存在以java为后缀的.java文件当中。每个可运行的程序都是一个类文件,或者称之为字节码文件,保存在.class文件中。而作为一个java application,类中必须包含主方法,程序的执行是从main方法开始的,方法头的格式是确定不变的:
   public static void main(String args[])
  其中关键字public意味着方法可以由外部世界调用。main方法的参数是一个字符串数组args,虽然在本程序中没有用到,但是必须列出来。
  程序的第一行非常有意思:
    System.out.println(new Date());
  
  打印语句的参数是一个日期对象Date,而创建Date对象的目的就是把它的值发给println()语句。一旦这个语句执行完毕,Date对象就没用了,而后"无用内存回收器"会将其收回。
  第二行中调用了System.getProperties( )。从帮助文档中可知,getProperties( )是System类的一个静态方法(static 方法),由于它是"静态"的,所以不必创建任何对象就可以调用该方法。在第三行,Properties对象有一个名为list( )的方法,它将自己的全部内容都发给一个PrintStream对象,该对象就是list()方法的参数。
  第四行和第六行是典型的打印语句,其中第六行通过运算符"+"的重载来连接多个字符串对象,在java中只有当"+"运算符作用于字符串时在能够进行重载。但是让我们仔细观察下述语句:
   System.out.println("Total Memory= "
           + rt.totalMemory( ) //打印总内存大小
           +" Free Memory = "
           +rt.freeMemory( )); //打印空闲内存大小
  其中,totalMemory( )和freeMemory( )返回的是数值,并非String对象。如果将一个字符串与一个数值相加,结果会如何?在这种情况下,编译器会自动调用一个toString()方法,将该数值(int型或者float型)转换成字符串。经过这样处理以后,就可以用"+"进行字符串连接了。
  main()的第五行通过调用Runtime的getRuntime()方法创建了一个Runtime对象,该对象中包含了内存等信息。
1.4.2 java程序的编辑
  java程序的编辑可以使用任何一种文本编辑器,例如UltraEdit、Notepad、Wordpad甚至word,然后只要把编辑好的文件存成.java文件。当然也可以用一些集成开发环境,例如Borland公司的JBuilder,IBM公司的Visualage for Java,此外还有cafe、kawa等其它集成开发环境。
1.4.3 java程序的编译
  Sun公司为全世界的java程序员提供了一个免费的java程序开发包(Java Develop Kit,简称JDK),其中包括了java编译器命令"javac",以及java执行命令"java",还有帮助文档生成器命令"javadoc"等等。所有这些命令都可以在命令行下运行,例如我们要编译上述java文件Property.java,如果是在windows中进行开发,就可以在"命令提示符"下进行编译,在命令行中敲入"javac Property.java",如图1_4_1所示:
1.4.4 java application的执行
  当编译结束以后,在java源文件中的每一个类都会生成相应的 .class 文件,例如上图中就会生成一个Property.class文件,而java程序在执行时调用的是.class 文件。Java application的执行是在命令行下进行的,如果是在windows系统中,就可以"命令提示符"下敲入"java Propery"进行执行,该"java"命令会启动java虚拟机,并读入Property.class文件进行执行。
  由于该程序的运行结果直接在命令行下进行输出
1.4.5 第一个java applet
  java程序的另一种形式是java applet,applet没有main()方法,它必须嵌在超文本文件中,在浏览器中进行运行。右面这个程序将在浏览器中显示一行字符串。
//这是我们的第一个java applet,该程序保存在文件HelloEducation.java中
 import java.awt.Graphics; //在进行显示输出时,需要用到类Graphics的对象;
 import java.applet.Applet; //Applet类是所有的java applet的父类;
 public class HelloEducation extends Applet {
          //程序员给这个类取名为HelloEducation
          //所有的applet程序都是Applet类的子类
   public String s;
   public void init() {        //
     s=new String("Welcome to Tongfang Education");
              //生成一个字符串对象
 }
 public void paint(Graphics g){
     g.drawString(s,25,25);
             //在浏览器中坐标为(25,25)的位置显示字符串s
   }
 }  
  applet程序是从方法init( )开始执行的,在该方法中完成了对字符串s的初始化工作,而显示功能是在方法paint( )中执行的。paint( )方法是类Applet的一个成员方法,其参数是图形对象Graphics g,通过调用对象g的drawString( )方法就可以显示输出。
1.4.6 java applet的执行
  java applet程序也是一个类,其编译方式与java application完全一样,HelloEducation.java程序经过编译以后就生成了HelloEducation.class文件。java applet的执行方式与java application完全不同,java applet程序必须嵌入到html文件中才能够执行,因此必须编写相应的html文件。下面为HelloEducaiton.html文件的内容:




  然后可以通过JDK所提供的命令"appletviewer",在命令行下面执行java applet程序。如果是在windows操作系统中,就可以在"命令提示符"下敲入"appletviewer HelloEducation.html",如图1_4_4 ( G:\study\java\java_qh\JAVA\text\ch01\se04\right1_4_6_1.htm" \t "MyFrame )所示。
  此时系统会弹出另外一个窗口运行该applet程序,结果如图1_4_5 ( G:\study\java\java_qh\JAVA\text\ch01\se04\right1_4_6_2.htm" \t "MyFrame )所示。
  applet还可以采用另外一种方式运行,那就是直接在浏览器中打开HelloEducation.html程序,结果如图1_4_6 ( G:\study\java\java_qh\JAVA\text\ch01\se05\right1_4_6_3.htm" \t "MyFrame )所示。在主流的浏览器如IE、Netscape中都包含有java虚拟机,负责解释执行java applet程序。
1.5 java程序规范
1.5.1 java源程序结构
  一个完整的java源程序应该包括下列部分:
  package语句; //该部分至多只有一句,必须放在源程序的第一句
  import语句;
  public classDefinition; //公共类定义部分,至多只有一个公共类的定义
       //java语言规定该java源程序的文件名必须与该公共类名完全一致
  classDefinition; //类定义部分,可以有0个或者多个类定义
  interfaceDefinition; //接口定义部分,可以有0个或者多个接口定义
  例如一个java源程序可以是如下结构,该源程序命名为HelloWorldApp.java:
   package javawork.helloworld;
   import java.awt.*;  //告诉编译器本程序中用到系统的AWT包
   import javawork.newcentury;
   public class HelloWorldApp{......}
   class TheFirstClass{......} //第一个普通类TheFirstClass的定义
   class TheSecondClass{......} //第二个普通类TheSecondClass的定义
              ...... //其它普通类的定义
   interface TheFirstInterface{......}
                 ...... //其它接口定义
  package语句:由于java编译器为每个类生成一个字节码文件,且文件名与类名相同,因此同名的类有可能发生冲突。为了解决这一问题,java提供包来管理类名空间,包实际提供了一种命名机制和可见性限制机制。而在java的系统类库中,把功能相似的类放到一个包(package)中,例如所有的图形界面的类都放在java.awt这个包中,与网络功能有关的类都放到这个包中。用户自己编写的类(指.class文件)也应该按照功能放在由程序员自己命名的相应的包中,例如上例中的javawork.helloworld就是一个包。包在实际的实现过程中是与文件系统相对应的,例如javawork.helloworld所对应的目录是path\javawork\helloworld,而path是在编译该源程序时指定的。比如在命令行中编译上述HelloWorldApp.java文件时,可以在命令行中敲入"javac -d f:\javaproject HelloWorldApp.java",则编译生成的HelloWorldApp.class文件将放在目录f:\javaproject\javawork\helloworld\目录下面,此时f:\javaprojcet相当于path。但是如果在编译时不指定path,则生成的.class文件将放在编译时命令行所在的当前目录下面。比如在命令行目录f:\javaproject下敲入编译命令"javac HelloWorldApp.java",则生成的HelloWorldApp.class文件将放在目录f:\javaproject下面,此时的package语句相当于没起作用。
  但是,如果程序中包含了package语句,则在运行时就必须包含包名。例如,HelloWorldApp.java程序的第一行语句是:package p1.p2;编译的时候在命令行下输入"javac -d path HelloWorldApp.java",则HelloWorldApp.class将放在目录path\p1\p2的下面,这时候运行该程序时有两种方式:
  第一种:在命令行下的path目录下输入字符"java p1.p2.HelloWorldApp"。
  第二种:在环境变量classpath中加入目录path,则运行时在任何目录下输入"java p1.p2.HelloWorldApp"即可。
  import语句:如果在源程序中用到了除java.lang这个包以外的类,无论是系统的类还是自己定义的包中的类,都必须用import语句标识,以通知编译器在编译时找到相应的类文件。例如上例中的java.awt是系统的包,而javawork.newcentury是用户自定义的包。比如程序中用到了类Button,而Button是属于包java.awt的,在编译时编译器将从目录classpath\java\awt中去寻找类Button,classpath是事先设定的环境变量,比如可以设为:classpath=.;d:\jdk1.3\lib\。 classpath也可以称为类路径,需要提醒大家注意的是,在classpath中往往包含多个路径,用分号隔开。例如classpath=.;d:\jdk1.3\lib\中的第一个分号之前的路径是一个点,表示当前目录,分号后面的路径是d:\jdk1.3\lib\,表示系统的标准类库目录。在编译过程中寻找类时,先从环境变量classpath的第一个目录开始往下找,比如先从当前目录往下找java.awt中的类Button时,编译器找不着,然后从环境变量classpath的第二个目录开始往下找,就是从系统的标准类库目录d:\jdk1.3\lib开始往下找java.awt的Button这个类,最后就找到了。如果要从一个包中引入多个类则在包名后加上".*"表示。
  如果程序中用到了用户自己定义的包中的类,假如在上面程序中要用到javawork.newcentury包中的类HelloWorldApp,而包javawork.newcentury所对应的目录是f:\javaproject\javawork\newcentury,classpath仍旧是classpath=.;d:\jdk1.3\lib\,则编译器在编译时将首先从当前目录寻找包javawork.newcentury,结果是没有找到;然后又从环境变量classpath的第二个目录d:\jdk1.3\lib\开始往下找,但是仍然没有找到。原因在于包javawork.newcentury是放在目录f:\javaproject下面。因此,需要重新设定环境变量classpath,设为classpath=.;d:\jdk1.3\lib\;f:\javaproject\ 。所以编译器从f:\javaproject开始找包javawork.newcentury就可以找到。
  源文件的命名规则:如果在源程序中包含有公共类的定义,则该源文件名必须与该公共类的名字完全一致,字母的大小写都必须一样。这是java语言的一个严格的规定,如果不遵守,在编译时就会出错。因此,在一个java源程序中至多只能有一个公共类的定义。如果源程序中不包含公共类的定义,则该文件名可以任意取名。如果在一个源程序中有多个类定义,则在编译时将为每个类生成一个.class文件。
1.5.2 java编程规范
  软件开发是一个集体协作的过程,程序员之间的代码是经常要进行交换阅读的,因此,java源程序有一些约定成俗的命名规定,主要目的是为了提高java程序的可读性。
包名:包名是全小写的名词,中间可以由点分隔开,例如:java.awt.event;
类名:首字母大写,通常由多个单词合成一个类名,要求每个单词的首字母也要大写,例如class HelloWorldApp;
接口名:命名规则与类名相同,例如interface Collection;
方法名:往往由多个单词合成,第一个单词通常为动词,首字母小写,中间的每个单词的首字母都要大写,例如:balanceAccount, isButtonPressed;
变量名:全小写,一般为名词,例如:length;
常量名:基本数据类型的常量名为全大写,如果是由多个单词构成,可以用下划线隔开,例如:int YEAR, int WEEK_OF_MONTH;如果是对象类型的常量,则是大小写混合,由大写字母把单词隔开。
1.5.3 java帮助文档
  java中所有类库的介绍都保存在java帮助文档中,程序员在编程过程中,必须查阅该帮助文档,了解系统提供的类的功能、成员方法、成员变量等等信息以后,才能够更好的编程。同时,java开发工具包(JDK)提供了
"java"、"javac"、"javadoc"、
"appletviewer"等命令,在java帮助文档中也对此进行了详细的介绍。java帮助文档是以HTML文件的形式组织,通常是安装在JDK目录下的docs子目录中的index.html文件,所以用浏览器就可以进行查阅。例如JDK是安装在D:\jdk1.3目录下面,则用浏览器打开D:\jdk1.3\docs\index.html文件,就可以看到图1_5_1 ( G:\study\java\java_qh\JAVA\text\ch01\se05\right1_5_3_1.htm" \t "MyFrame )所示的帮助文档。
  如果希望查阅JDK的命令,则可以选择"Tool Documentation",如图1_5_2 ( G:\study\java\java_qh\JAVA\text\ch01\se05\right1_5_3_2.htm" \t "MyFrame )红字所示。
  此时浏览器就会把java、javac、javadoc、appletviewer等命令列出来,如图1_5_3 ( G:\study\java\java_qh\JAVA\text\ch01\se05\right1_5_3_3.htm" \t "MyFrame )。
  但是大多时时候,我们需要查阅的是类库的文档,因此需要在"D:\jdk1.3\docs\index.html"文件中选择"Java 2 Platform API Specification",如图1_5_4 ( G:\study\java\java_qh\JAVA\text\ch01\se05\right1_5_3_4.htm" \t "MyFrame )中红字所示。
  然后就进入了详细的类库介绍,如图1_5_5 ( G:\study\java\java_qh\JAVA\text\ch01\se05\right1_5_3_5.htm" \t "MyFrame )所示。
图中左上部分列出了java标准类库中的各个包,例如第一个是java.applet包,与java applet有关的类都放在这个包中,第二个是java.awt包,有关图形界面的类都放在这个包中。如果选择了包java.awt,则图中左下部分就会列出java.awt包所包含的所有的类(class)、接口(interface)、例外(Exception)和错误(Error)。如果选择了类Button,则在图中右边空间内将显示该类的详细介绍,包括:该类的父类、实现的接口、功能介绍、构造方法、成员方法、成员变量、从父类继承的成员方法、从父类和接口继承的成员变量、每个成员方法的具体介绍。
  如果程序员知道某个成员方法或者成员变量的名字,但是并不知道它属于哪个类,则可以通过选择图中右上的"Index",就会按照字母表顺序列出所有的成员方法、成员变量、类和接口。"Index"旁边的"Tree"可以把所有的类按照继承关系列出来。
1.5.4 java注释
单行注释:从"//"开始到本行结束的内容都是注释,例如:
         //这是一行单行注释
         //则是另一行单行注释
多行注释:在""之间的所有内容都是注释,例如:
         
文档注释:在注释方面java提供一种C/C++所不具有的文档注释方式。其核心思想是当程序员编完程序以后,可以通过JDK提供的javadoc命令,生成所编程序的API文档,而该文档中的内容主要就是从文档注释中提取的。该API文档以HTML文件的形式出现,与java帮助文档的风格与形式完全一致。凡是在""之间的内容都是文档注释。例如下面的DocTest.java文件:
       
       public class DocTest{
          
         public int i;
          
         public void count( ) {}
       } 
1.6 建立java开发环境
1.6.1 安装Java Develop Kit(JDK)
  Sun公司为所有的java程序员提供了一套免费的java开发和运行环境,取名为Java2 SDK,可以从http://sun.com上进行下载,也可以从同方教育网站上下载。但是最新的消息和版本必须从Sun的网站上才能够得到。安装的时候可以选择安装到任意的硬盘驱动器上,例如安装到D:\jdk1.3目录下。通常在JDK目录下有bin、demo、lib、jre等子目录,其中bin目录保存了javac、java、appletviewer等命令文件,demo目录保存了许多java的例子,lib目录保存了java的类库文件,jre保存的是java的运行时环境。
1.6.2 安装java帮助文档
  由于JDK的安装程序中并不包含帮助文档,因此也必须从Sun的网站上下载进行安装。通常安装在JDK所在目录的docs子目录下面。用浏览器打开docs子目录下的index.html文件就可以阅读所有的帮助文档。
1.6.3 配置类路径
  在安装完JDK之后,必须配置类路径classpath和环境变量path,JDK才能够正常运行。如果是在windows98中运行,则在
  C:\autoexec.bat文件的末尾添加下列语句:
   classpath= .;d:\jdk1.3\lib;
   path=%path%;d:\jdk1.3\bin;
  如果是在windows2000中,则需要用右键单击桌面上"我的电脑",选择"属性",则弹出一个名为"系统特性"的窗口,选择"高级",然后选择"环境变量",在"环境变量"窗口中编辑classpath和path。
  
  
  第十章 多线程
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介: 本讲介绍了Java线程的一些基本知识和简单应用,通过对线程简介,阐明了线程与进程的区别,通过描述线程的概念模型的基本原理以及线程体的构造方法和应用实例,讲解了线程的基本特性和线程的不同状态的转换关系和调用方法,明确了线程的使用方法,然后,我们又进一步讲述了线程的几种调度策略,在不同的调度策略下优先级的作用。以及如何进行基本的线程的控制,线程的重点和难点在于多线程的互斥与同步,首先我们必须明白互斥锁的概念和作用,如何使用互斥锁来控制和处理多线程的同步问题。
下载一
【课前思考】
  1. 什么是线程?它和进程有什么区别?适用方向是什么?
  2. Java的线程是如何实现的?
  3. Java的线程是如何调度的?
  4. Java中的多线程有什么特点?同步和互斥的原理是如何实现的?
【学习目标】
学习java中线程的使用,掌握线程的调度和控制方法,清楚地理解多线程的互斥和同步的实现原理,以及多线程的应用。
【学习指南】
  掌握线程之间的相互调度关系,尤其是通过线程睡眠来使其它线程获得执行机会的机制,以及互斥和同步的实现机制。
【难 重 点】
  1. 多线程的调度和控制。
  2. 多线程的互斥和同步。
【知 识 点】
  10.1 线程简介
   10.1.1 线程的概念模型
   10.1.2 线程体
   10.1.3线程的调度
   10.1.4基本的线程控制
  10.2多线程的互斥与同步
   10.2.1互斥锁
   10.2.2多线程的同步
  
第十章 多线程
10.1 线程简介
随着计算机的飞速发展,个人计算机上的操作系统也纷纷采用多任务和分时设计,将早期只有大型计算机才具有的系统特性带到了个人计算机系统中。一般可以在同一时间内执行多个程序的操作系统都有进程的概念。一个进程就是一个执行中的程序,而每一个进程都有自己独立的一块内存空间、一组系统资源。在进程概念中,每一个进程的内部数据和状态都是完全独立的。Java程序通过流控制来执行程序流,程序中单个顺序的流控制称为线程,多线程则指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务。多线程意味着一个程序的多行语句可以看上去几乎在同一时间内同时运行。
线程与进程相似,是一段完成某个特定功能的代码,是程序中单个顺序的流控制;但与进程不同的是,同类的多个线程是共享一块内存空间和一组系统资源,而线程本身的数据通常只有微处理器的寄存器数据,以及一个供程序执行时使用的堆栈。所以系统在产生一个线程,或者在各个线程之间切换时,负担要比进程小的多,正因如此,线程被称为轻负荷进程(light-weight process)。一个进程中可以包含多个线程。
 一个线程是一个程序内部的顺序控制流。
  1. 进程:每个进程都有独立的代码和数据空间(进程上下文) ,进程切换的开销大。
  2. 线程:轻量的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
  3. 多进程:在操作系统中,能同时运行多个任务程序。
  4. 多线程:在同一应用程序中,有多个顺序流同时执行。
10.1.1 线程的概念模型
Java内在支持多线程,它的所有类都是在多线程下定义的,Java利用多线程使整个系统成为异步系统。Java中的线程由三部分组成,如图6.1所示。
  1. 虚拟的CPU,封装在java.lang.Thread类中。
  2. CPU所执行的代码,传递给Thread类。
  3. CPU所处理的数据,传递给Thread类。
图10.1线程
    
10.1.2 线程体
 Java的线程是通过java.lang.Thread类来实现的。当我们生成一个Thread类的对象之后,一个新的线程就产生了。
此线程实例表示Java解释器中的真正的线程,通过它可以启动线程、终止线程、线程挂起等,每个线程都是通过类Thread在Java的软件包Java.lang中定义,它的构造方法为:
   public Thread (ThreadGroup group,Runnable target,String name);
  其中,group 指明该线程所属的线程组;target实际执行线程体的目标对象,它必须实现接口Runnable; name为线程名。Java中的每个线程都有自己的名称,Java提供了不同Thread类构造器,允许给线程指定名称。如果name为null时,则Java自动提供唯一的名称。
当上述构造方法的某个参数为null时,我们可得到下面的几个构造方法:
  public Thread ();
  public Thread (Runnable target);
  public Thread (Runnable target,String name);
  public Thread (String name);
  public Thread (ThreadGroup group,Runnable target);
  public Thread (ThreadGroup group,String name);
  一个类声明实现Runnable接口就可以充当线程体,在接口Runnable中只定义了一个方法 run():
       public void run();
  任何实现接口Runnable的对象都可以作为一个线程的目标对象,类Thread本身也实现了接口Runnable,因此我们可以通过两种方法实现线程体。
  (一)定义一个线程类,它继承线程类Thread并重写其中的方法 run(),这时在初始化这个类的实例时,目标target可为null,表示由这个实例对来执行线程体。由于Java只支持单重继承,用这种方法定义的类不能再继承其它父类。
  (二)提供一个实现接口Runnable的类作为一个线程的目标对象,在初始化一个Thread类或者Thread子类的线程对象时,把目标对象传递给这个线程实例,由该目标对象提供线程体 run()。这时,实现接口Runnable的类仍然可以继承其它父类。
  每个线程都是通过某个特定Thread对象的方法run( )来完成其操作的,方法run( )称为线程体。图6.2表示了java线程的不同状态以及状态之间转换所调用的方法。
图10.2 线程的状态
    
  1. 创建状态(new Thread)
  执行下列语句时,线程就处于创建状态:
  Thread myThread = new MyThreadClass( );
  当一个线程处于创建状态时,它仅仅是一个空的线程对象,系统不为它分配资源。
  2. 可运行状态( Runnable )
  Thread myThread = new MyThreadClass( );
  myThread.start( );
  当一个线程处于可运行状态时,系统为这个线程分配了它需的系统资源,安排其运行并调用线程运行方法,这样就使得该线程处于可运行( Runnable )状态。需要注意的是这一状态并不是运行中状态(Running ),因为线程也许实际上并未真正运行。由于很多计算机都是单处理器的,所以要在同一时刻运行所有的处于可运行状态的线程是不可能的,Java的运行系统必须实现调度来保证这些线程共享处理器。
  3. 不可运行状态(Not Runnable)
  进入不可运行状态的原因有如下几条:
  1) 调用了sleep()方法;
  2) 调用了suspend()方法;
  3) 为等候一个条件变量,线程调用wait()方法;
  4) 输入输出流中发生线程阻塞;
  不可运行状态也称为阻塞状态(Blocked)。因为某种原因(输入/输出、等待消息或其它阻塞情况),系统不能执行线程的状态。这时即使处理器空闲,也不能执行该线程。
  4. 死亡状态(Dead)
  线程的终止一般可通过两种方法实现:自然撤消(线程执行完)或是被停止(调用stop()方法)。目前不推荐通过调用stop()来终止线程的执行,而是让线程执行完。
◇线程体的构造
  任何实现接口Runnable的对象都可以作为一个线程的目标对象,上面已讲过构造线程体有两种方法,下面通过实例来说明如何构造线程体的。
例10.1 通过继承类Thread构造线程体
  class SimpleThread extends Thread {
  public SimpleThread(String str) {
   super(str); //调用其父类的构造方法
  }
  public void run() { //重写run方法
   for (int i = 0; i < 10; i++) {
    System.out.println(i + " " + getName());  //打印次数和线程的名字
    try {
      sleep((int)(Math.random() * 1000));  //线程睡眠,把控制权交出去
    } catch (InterruptedException e) {}
  }
     System.out.println("DONE! " + getName()); //线程执行结束
    }
  }
  public class TwoThreadsTest {
   public static void main (String args[]) {
    new SimpleThread("First").start(); //第一个线程的名字为First
    new SimpleThread("Second").start();//第二个线程的名字为Second
}
}
   运行结果:
    0 First
    0 Second
    1 Second
    1 First
    2 First
    2 Second
    3 Second
    3 First
    4 First
    4 Second
    5 First
    5 Second
    6 Second
    6 First
    7 First
    7 Second
    8 Second
    9 Second
    8 First
    DONE! Second
    9 First
    DONE! First
  仔细分析一下运行结果,会发现两个线程是交错运行的,感觉就象是两个线程在同时运行。但是实际上一台计算机通常就只有一个CPU,在某个时刻只能是只有一个线程在运行,而java语言在设计时就充分考虑到线程的并发调度执行。对于程序员来说,在编程时要注意给每个线程执行的时间和机会,主要是通过让线程睡眠的办法(调用sleep()方法)来让当前线程暂停执行,然后由其它线程来争夺执行的机会。如果上面的程序中没有用到sleep()方法,则就是第一个线程先执行完毕,然后第二个线程再执行完毕。所以用活sleep()方法是学习线程的一个关键。
 例10.2 通过接口构造线程体
   public class Clock extends java.applet.Applet implements Runnable {//实现接口
      Thread clockThread;
      public void start() {
         //该方法是Applet的方法,不是线程的方法
      if (clockThread == null) {
         clockThread = new Thread(this, "Clock");
         
         clockThread.start(); //启动线程
         }
      }
      public void run() { //run()方法中是线程执行的内容
         while (clockThread != null) {
         repaint(); //刷新显示画面
         try {
           clockThread.sleep(1000);
           //睡眠1秒,即每隔1秒执行一次
          } catch (InterruptedException e){}
         }
      }
      public void paint(Graphics g) {
          Date now = new Date(); //获得当前的时间对象
          g.drawString(now.getHours() + ":" + now.getMinutes()+ ":" +now.getSeconds(), 5, 10);//显示当前时间
      }
      public void stop() {
        //该方法是Applet的方法,不是线程的方法
          clockThread.stop();
          clockThread = null;
      }
   }
  上面这个例子是通过每隔1秒种就执行线程的刷新画面功能,显示当前的时间;看起来的效果就是一个时钟,每隔1秒就变化一次。由于采用的是实现接口Runnable的方式,所以该类Clock还继承了Applet, Clock就可以Applet的方式运行。
  构造线程体的两种方法的比较:
  1. 使用Runnable接口
   1) 可以将CPU,代码和数据分开,形成清晰的模型;
   2) 还可以从其他类继承;
   3) 保持程序风格的一致性。
  2. 直接继承Thread类
   1) 不能再从其他类继承;
   2) 编写简单,可以直接操纵线程,无需使用Thread.currentThread()。
10.1.3 线程的调度
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪些线程来执行。
线程调度器按线程的优先级高低选择高优先级线程(进入运行中状态)执行,同时线程调度是抢先式调度,即如果在当前线程执行过程中,一个更高优先级的线程进入可运行状态,则这个线程立即被调度执行。
线程的优先级
  线程的优先级用数字来表示,范围从1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。一个线程的缺省优先级是5,即Thread.NORM_PRIORITY。下述方法可以对优先级进行操作:
  int getPriority(); //得到线程的优先级
  void setPriority(int newPriority);
  //当线程被创建后,可通过此方法改变线程的优先级
  例10.3中生成三个不同线程,其中一个线程在最低优先级下运行,而另两个线程在最高优先级下运行。
 例10.3
  class ThreadTest{
    public static void main( String args [] ) {
      Thread t1 = new MyThread("T1");
      t1.setPriority( Thread.MIN_PRIORITY ); //设置优先级为最小
      t1.start( );
      Thread t2 = new MyThread("T2");
      t2.setPriority( Thread.MAX_PRIORITY ); //设置优先级为最大
      t2.start( );
      Thread t3 = new MyThread("T3");
      t3.setPriority( Thread.MAX_PRIORITY ); //设置优先级为最大
      t3.start( );
    }
        }
   class MyThread extends Thread {
     String message;
     MyThread ( String message ) {
        this.message = message;
     }
     public void run() {
       for ( int i=0; i<3; i++ )
        System.out.println( message+" "+getPriority() ); //获得线程的优先级
     }
        }
  运行结果:
       T2 10
       T2 10
       T2 10
       T3 10
       T3 10
       T3 10
       T1 1
       T1 1
       T1 1
  注意:并不是在所有系统中运行Java程序时都采用时间片策略调度线程,所以一个线程在空闲时应该主动放弃CPU,以使其他同优先级和低优先级的线程得到执行。
10.1.4基本的线程控制
1.终止线程
线程终止后,其生命周期结束了,即进入死亡态,终止后的线程不能再被调度执行,以下几种情况,线程进入终止状态:
  1) 线程执行完其run()方法后,会自然终止。
  2) 通过调用线程的实例方法stop()来终止线程。
 2. 测试线程状态
可以通过Thread 中的isAlive() 方法来获取线程是否处于活动状态;线程由start() 方法启动后,直到其被终止之间的任何时刻,都处于'Alive'状态。
3. 线程的暂停和恢复
  有几种方法可以暂停一个线程的执行,在适当的时候再恢复其执行。
  1) sleep() 方法
  当前线程睡眠(停止执行)若干毫秒,线程由运行中状态进入不可运行状态,停止执行时间到后线程进入可运行状态。
  2) suspend()和resume()方法
  线程的暂停和恢复,通过调用线程的suspend()方法使线程暂时由可运行态切换到不可运行态,若此线程想再回到可运行态,必须由其他线程调用resume()方法来实现。
  注:从JDK1.2开始就不再使用suspend()和resume()。
  3) join()
  当前线程等待调用该方法的线程结束后, 再恢复执行.
  TimerThread tt=new TimerThread(100);
  tt.start();
  …
  public void timeout(){
  tt.join();// 当前线程等待线程tt 执行完后再继续往下执行
  … }
10.2多线程的互斥与同步
  临界资源问题
前面所提到的线程都是独立的,而且异步执行,也就是说每个线程都包含了运行时所需要的数据或方法,而不需要外部的资源或方法,也不必关心其它线程的状态或行为。但是经常有一些同时运行的线程需要共享数据,此时就需考虑其他线程的状态和行为,否则就不能保证程序的运行结果的正确性。例6.4说明了此问题。
 例10.4
  class stack{
   int idx=0; //堆栈指针的初始值为0
   char[ ] data = new char[6]; //堆栈有6个字符的空间
   public void push(char c){ //压栈操作
    data[idx] = c; //数据入栈
    idx + +; //指针向上移动一位
   }
     public char pop(){ //出栈操作
       idx - -; //指针向下移动一位
       return data[idx]; //数据出栈
     }
   }
  两个线程A和B在同时使用Stack的同一个实例对象,A正在往堆栈里push一个数据,B则要从堆栈中pop一个数据。如果由于线程A和B在对Stack对象的操作上的不完整性,会导致操作的失败,具体过程如下所示:
  1) 操作之前
     data = | p | q | | | | | idx=2
  2) A执行push中的第一个语句,将r推入堆栈;
     data = | p | q | r | | | | idx=2
  3) A还未执行idx++语句,A的执行被B中断,B执行pop方法,返回q:
     data = | p | q | r | | | | idx=1
  4〕A继续执行push的第二个语句:
     data = | p | q | r | | , | | idx=2
  最后的结果相当于r没有入栈。产生这种问题的原因在于对共享数据访问的操作的不完整性。
10.2.1 互斥锁
为解决操作的不完整性问题,在Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。
    public void push(char c){
    synchronized(this){ //this表示Stack的当前对象
       data[idx]=c;
       idx++;
    }
    }
    public char pop(){
       synchronized(this){ //this表示Stack的当前对象
       idx--;
       return data[idx];
       }
    }
  synchronized 除了象上面讲的放在对象前面限制一段代码的执行外,还可以放在方法声明中,表示整个方法为同步方法。
  public synchronized void push(char c){
  …
    }
  如果synchronized用在类声明中,则表明该类中的所有方法都是synchronized的。
10.2.2多线程的同步
本节将讨论如何控制互相交互的线程之间的运行进度,即多线程之间的同步问题,下面我们将通过多线程同步的模型: 生产者-消费者问题来说明怎样实现多线程的同步。
我们把系统中使用某类资源的线程称为消费者,产生或释放同类资源的线程称为生产者。
在下面的Java的应用程序中,生产者线程向文件中写数据,消费者从文件中读数据,这样,在这个程序中同时运行的两个线程共享同一个文件资源。通过这个例子我们来了解怎样使它们同步。
例10.5
   class SyncStack{ //同步堆栈类
   private int index = 0; //堆栈指针初始值为0
   private char []buffer = new char[6]; //堆栈有6个字符的空间
   public synchronized void push(char c){ //加上互斥锁
     while(index = = buffer.length){ //堆栈已满,不能压栈
     try{
        this.wait(); //等待,直到有数据出栈
       }catch(InterruptedException e){}
       }
   this.notify(); //通知其它线程把数据出栈
   buffer[index] = c; //数据入栈
   index++; //指针向上移动
   }
   public synchronized char pop(){ //加上互斥锁
       while(index ==0){ //堆栈无数据,不能出栈
        try{
           this.wait(); //等待其它线程把数据入栈
        }catch(InterruptedException e){}
          }
       this.notify(); //通知其它线程入栈
       index- -; //指针向下移动
       return buffer[index]; //数据出栈
    }
       }
    class Producer implements Runnable{ //生产者类
       SyncStack theStack;
        //生产者类生成的字母都保存到同步堆栈中
       public Producer(SyncStack s){
          theStack = s;
       }
       public void run(){
          char c;
          for(int i=0; i<20; i++){
            c =(char)(Math.random()*26+'A');
                          //随机产生20个字符
            theStack.push(c); //把字符入栈
            System.out.println("Produced: "+c); //打印字符
            try{
            Thread.sleep((int)(Math.random()*1000));
                     
            }catch(InterruptedException e){}
          }
       }
     }
     class Consumer implements Runnable{ //消费者类
         SyncStack theStack;
                  //消费者类获得的字符都来自同步堆栈
         public Consumer(SyncStack s){
             theStack = s;
         }
         public void run(){
             char c;
             for(int i=0;i<20;i++){
               c = theStack.pop(); //从堆栈中读取字符
             System.out.println("Consumed: "+c);
                             //打印字符
             try{
             Thread.sleep((int)(Math.random()*1000));
                    
             }catch(InterruptedException e){}
         }
       }
     }
     public class SyncTest{
       public static void main(String args[]){
         SyncStack stack = new SyncStack();
   //下面的消费者类对象和生产者类对象所操作的是同一个同步堆栈对象
         Runnable source=new Producer(stack);
         Runnable sink = new Consumer(stack);
         Thread t1 = new Thread(source); //线程实例化
         Thread t2 = new Thread(sink); //线程实例化
         t1.start(); //线程启动
         t2.start(); //线程启动
       }
     }
  类Producer是生产者模型,其中的 run()方法中定义了生产者线程所做的操作,循环调用push()方法,将生产的20个字母送入堆栈中,每次执行完push操作后,调用sleep()方法睡眠一段随机时间,以给其他线程执行的机会。类Consumer是消费者模型,循环调用pop()方法,从堆栈中取出一个数据,一共取20次,每次执行完pop操作后,调用sleep()方法睡眠一段随机时间,以给其他线程执行的机会。
  程序执行结果
        Produced:V
        Consumed:V
        Produced:E
        Consumed:E
        Produced:P
        Produced:L
        ...
        Consumed:L
        Consumed:P
  在上述的例子中,通过运用wait()和notify()方法来实现线程的同步,在同步中还会用到notifyAll()方法,一般来说,每个共享对象的互斥锁存在两个队列,一个是锁等待队列,另一个是锁申请队列,锁申请队列中的第一个线程可以对该共享对象进行操作,而锁等待队列中的线程在某些情况下将移入到锁申请队列。下面比较一下wait()、notify()和notifyAll()方法:
  (1) wait,nofity,notifyAll必须在已经持有锁的情况下执行,所以它们只能出现在synchronized作用的范围内,也就是出现在用       synchronized修饰的方法或类中。
  (2) wait的作用:释放已持有的锁,进入等待队列.
  (3) notify的作用:唤醒wait队列中的第一个线程并把它移入锁申请队列.
  (4) notifyAll的作用:唤醒wait队列中的所有的线程并把它们移入锁申请队列.
  注意:
  1) suspend()和resume()
    在JDK1.2中不再使用suspend()和resume(),其相应功能由wait()和notify()来实现。
  2) stop()
    在JDK1.2中不再使用stop(),而是通过标志位来使程序正常执行完毕。例6.6就是一个典型的例子。
 例10.6
   public class Xyz implements Runnable {
      private boolean timeToQuit=false; //标志位初始值为假
      public void run() {
         while(!timeToQuit) {//只要标志位为假,线程继续运行
             …
         }
      }
   public void stopRunning() {
         timeToQuit=true;} //标志位设为真,表示程序正常结束
      }
   public class ControlThread {
      private Runnable r=new Xyz();
      private Thread t=new Thread(r);
      public void startThread() {
         t.start();
      }
      public void stopThread() {
         r.stopRunning(); }
               //通过调用stopRunning方法来终止线程运行
      }第四章 JAVA类和对象的高级特征
教案名称: 教案大小:
教案类型: WORD文档 星级评定: ★★★★☆
教案简介:   本讲主要讲述了java语言中面向对象的高级特征,包括抽象类、接口和包的特性。通过本讲的学习,同学们可以使用java语言中较为深入的技术编写面向对象程序。
下载一
第四章 JAVA类和对象的高级特征
【课前思考】
  1. 什么是抽象类、接口?它们各自又有哪些特性?
  2. 你知道java语言在面向对象编程方面有何独特的特点吗?
【学习目标】
  本讲主要讲述了java语言中面向对象的高级特征,包括抽象类、接口和包的特性。通过本讲的学习,同学们可以使用java语言中较为深入的技术编写面向对象程序。
【学习指南】
  应深刻理解各知识点的概念,使用上一讲的编程基础知识及面向对象技术,编写各种java类,由浅至深,养成风格良好的编程习惯。
【难 重 点】
 重点:
  1. 仔细体会面向对象编程的思想,熟练理解类和对象的概念,理解面向对象的特性,会编写各种java类,逐渐掌握面向对象编程的方法。
  2. 注意java语言中,不允许多重继承,而使用接口的方法。
 难点:
  1. 理解方法抽象类和接口,不要混淆了两者的使用。
  2.接口的使用。
【知 识 点】
4.1 抽象类
4.2 接口
4.3包
4.4 JAVA应用程序编程接口
4.1 抽象类
java语言中,用abstract 关键字来修饰一个类时,这个类叫做抽象类,用abstract 关键字来修饰一个方法时,这个方法叫做抽象方法。格式如下:
  abstract class abstractClass{ …} //抽象类
  abstract returnType abstractMethod([paramlist]) //抽象方法
  抽象类必须被继承,抽象方法必须被重写。抽象方法只需声明,无需实现;抽象类不能被实例化,抽象类不一定要包含抽象方法。若类中包含了抽象方法,则该类必须被定义为抽象类。
4.2 接口
 接口是抽象类的一种,只包含常量和方法的定义,而没有变量和方法的实现,且其方法都是抽象方法。它的用处体现在下面几个方面:
  ◇ 通过接口实现不相关类的相同行为,而无需考虑这些类之间的关系。
  ◇ 通过接口指明多个类需要实现的方法。
  ◇ 通过接口了解对象的交互界面,而无需了解对象所对应的类。
  1)接口的定义
  接口的定义包括接口声明和接口体。
  接口声明的格式如下:
  [public] interface interfaceName[extends listOfSuperInterface] { … }
   extends 子句与类声明的extends子句基本相同,不同的是一个接口可有多个父接口,用逗号隔开,而一个类只能有一个父类。
  接口体包括常量定义和方法定义
  常量定义格式为:type NAME=value; 该常量被实现该接口的多个类共享; 具有public ,final, static的属性。
  方法体定义格式为:(具有 public和abstract属性)
  returnType methodName([paramlist]);
  2)接口的实现
  在类的声明中用implements子句来表示一个类使用某个接口,在类体中可以使用接口中定义的常量,而且必须实现接口中定义的所有方法。一个类可以实现多个接口,在implements子句中用逗号分开。
  3) 接口类型的使用
  接口作为一种引用类型来使用。任何实现该接口的类的实例都可以存储在该接口类型的变量中,通过这些变量可以访问类所实现的接口中的方法。
4.3包
“进行面向对象的设计时,一项基本的考虑是:如何将发生变化的东西与保持不变的东西分隔开。”这一点对于库来说是特别重要的。那个库的用户(客户程序员)必须能依赖自己使用的那一部分,并知道一旦新版本的库出台,自己不需要改写代码。而与此相反,库的创建者必须能自由地进行修改与改进,同时保证客户程序员代码不会受到那些变动的影响。
为达到这个目的,需遵守一定的约定或规则。例如,库程序员在修改库内的一个类时,必须保证不删除已有的方法,因为那样做会造成客户程序员代码出现断点。然而,相反的情况却是令人痛苦的。对于一个数据成员,库的创建者怎样才能知道哪些数据成员已受到客户程序员的访问呢?若方法属于某个类唯一的一部分,而且并不一定由客户程序员直接使用,那么这种痛苦的情况同样是真实的。如果库的创建者想删除一种旧有的实施方案,并置入新代码,此时又该怎么办呢?对那些成员进行的任何改动都可能中断客户程序员的代码。所以库创建者处在一个尴尬的境地,似乎根本动弹不得。
为解决这个问题,Java推出了“访问指示符”的概念,允许库创建者声明哪些东西是客户程序员可以使用的,哪些是不可使用的。这种访问控制的级别在“最大访问”和“最小访问”的范围之间,分别包括:public,“友好的”(无关键字),protected以及private。根据前一段的描述,大家或许已总结出作为一名库设计者,应将所有东西都尽可能保持为“private”(私有),并只展示出那些想让客户程序员使用的方法。这种思路是完全正确的,尽管它有点儿违背那些用其他语言(特别是C)编程的人的直觉,那些人习惯于在没有任何限制的情况下访问所有东西。到这一章结束时,大家应该可以深刻体会到Java访问控制的价值。
然而,组件库以及控制谁能访问那个库的组件的概念现在仍不是完整的。仍存在这样一个问题:如何将组件绑定到单独一个统一的库单元里。这是通过Java的package(打包)关键字来实现的,而且访问指示符要受到类在相同的包还是在不同的包里的影响。所以在本章的开头,大家首先要学习库组件如何置入包里。这样才能理解访问指示符的完整含义。
4.3.1 包:库单元
我们用import关键字导入一个完整的库时,就会获得“包”(Package)。例如:
import java.util.*;
它的作用是导入完整的实用工具(Utility)库,该库属于标准Java开发工具包的一部分。由于Vector位于java.util里,所以现在要么指定完整名称“java.util.Vector”(可省略import语句),要么简单地指定一个“Vector”(因为import是默认的)。
若想导入单独一个类,可在import语句里指定那个类的名字:
import java.util.Vector;
现在,我们可以自由地使用Vector。然而,java.util中的其他任何类仍是不可使用的。
之所以要进行这样的导入,是为了提供一种特殊的机制,以便管理“命名空间”(Name Space)。我们所有类成员的名字相互间都会隔离起来。位于类A内的一个方法f()不会与位于类B内的、拥有相同“签名”(自变量列表)的f()发生冲突。但类名会不会冲突呢?假设创建一个stack类,将它安装到已有一个stack类(由其他人编写)的机器上,这时会出现什么情况呢?对于因特网中的Java应用,这种情况会在用户毫不知晓的时候发生,因为类会在运行一个Java程序的时候自动下载。
正是由于存在名字潜在的冲突,所以特别有必要对Java中的命名空间进行完整的控制,而且需要创建一个完全独一无二的名字,无论因特网存在什么样的限制。
迄今为止,本书的大多数例子都仅存在于单个文件中,而且设计成局部(本地)使用,没有同包名发生冲突(在这种情况下,类名置于“默认包”内)。这是一种有效的做法,而且考虑到问题的简化,本书剩下的部分也将尽可能地采用它。然而,若计划创建一个“对因特网友好”或者说“适合在因特网使用”的程序,必须考虑如何防止类名的重复。
为Java创建一个源码文件的时候,它通常叫作一个“编辑单元”(有时也叫作“翻译单元”)。每个编译单元都必须有一个以.java结尾的名字。而且在编译单元的内部,可以有一个公共(public)类,它必须拥有与文件相同的名字(包括大小写形式,但排除.java文件扩展名)。如果不这样做,编译器就会报告出错。每个编译单元内都只能有一个public类(同样地,否则编译器会报告出错)。那个编译单元剩下的类(如果有的话)可在那个包外面的世界面前隐藏起来,因为它们并非“公共”的(非public),而且它们由用于主public类的“支撑”类组成。
编译一个.java文件时,我们会获得一个名字完全相同的输出文件;但对于.java文件中的每个类,它们都有一个.class扩展名。因此,我们最终从少量的.java文件里有可能获得数量众多的.class文件。如以前用一种汇编语言写过程序,那么可能已习惯编译器先分割出一种过渡形式(通常是一个.obj文件),再用一个链接器将其与其他东西封装到一起(生成一个可执行文件),或者与一个库封装到一起(生成一个库)。但那并不是Java的工作方式。一个有效的程序就是一系列.class文件,它们可以封装和压缩到一个JAR文件里(使用Java 1.1提供的jar工具)。Java解释器负责对这些文件的寻找、装载和解释(注释①)。
①:Java并没有强制一定要使用解释器。一些固有代码的Java编译器可生成单独的可执行文件。
“库”也由一系列类文件构成。每个文件都有一个public类(并没强迫使用一个public类,但这种情况最很典型的),所以每个文件都有一个组件。如果想将所有这些组件(它们在各自独立的.java和.class文件里)都归纳到一起,那么package关键字就可以发挥作用)。
若在一个文件的开头使用下述代码:
package mypackage;
那么package语句必须作为文件的第一个非注释语句出现。该语句的作用是指出这个编译单元属于名为mypackage的一个库的一部分。或者换句话说,它表明这个编译单元内的public类名位于mypackage这个名字的下面。如果其他人想使用这个名字,要么指出完整的名字,要么与mypackage联合使用import关键字(使用前面给出的选项)。注意根据Java包(封装)的约定,名字内的所有字母都应小写,甚至那些中间单词亦要如此。例如,假定文件名是MyClass.java。它意味着在那个文件有一个、而且只能有一个public类。而且那个类的名字必须是MyClass(包括大小写形式):
package mypackage;
public class MyClass {
// . . .
现在,如果有人想使用MyClass,或者想使用mypackage内的其他任何public类,他们必须用import关键字激活mypackage内的名字,使它们能够使用。另一个办法则是指定完整的名称:
mypackage.MyClass m = new mypackage.MyClass();
import关键字则可将其变得简洁得多:
import mypackage.*;
// . . .
MyClass m = new MyClass();
作为一名库设计者,一定要记住package和import关键字允许我们做的事情就是分割单个全局命名空间,保证我们不会遇到名字的冲突——无论有多少人使用因特网,也无论多少人用Java编写自己的类。
1 创建独一无二的包名
大家或许已注意到这样一个事实:由于一个包永远不会真的“封装”到单独一个文件里面,它可由多个.class文件构成,所以局面可能稍微有些混乱。为避免这个问题,最合理的一种做法就是将某个特定包使用的所有.class文件都置入单个目录里。也就是说,我们要利用操作系统的分级文件结构避免出现混乱局面。这正是Java所采取的方法。
它同时也解决了另两个问题:创建独一无二的包名以及找出那些可能深藏于目录结构某处的类。正如我们在第2章讲述的那样,为达到这个目的,需要将.class文件的位置路径编码到package的名字里。但根据约定,编译器强迫package名的第一部分是类创建者的因特网域名。由于因特网域名肯定是独一无二的(由InterNIC保证——注释②,它控制着域名的分配),所以假如按这一约定行事,package的名称就肯定不会重复,所以永远不会遇到名称冲突的问题。换句话说,除非将自己的域名转让给其他人,而且对方也按照相同的路径名编写Java代码,否则名字的冲突是永远不会出现的。当然,如果你没有自己的域名,那么必须创造一个非常生僻的包名(例如自己的英文姓名),以便尽最大可能创建一个独一无二的包名。如决定发行自己的Java代码,那么强烈推荐去申请自己的域名,它所需的费用是非常低廉的。
②:ftp://ftp.internic.net
这个技巧的另一部分是将package名解析成自己机器上的一个目录。这样一来,Java程序运行并需要装载.class文件的时候(这是动态进行的,在程序需要创建属于那个类的一个对象,或者首次访问那个类的一个static成员时),它就可以找到.class文件驻留的那个目录。
Java解释器的工作程序如下:首先,它找到环境变量CLASSPATH(将Java或者具有Java解释能力的工具——如浏览器——安装到机器中时,通过操作系统进行设定)。CLASSPATH包含了一个或多个目录,它们作为一种特殊的“根”使用,从这里展开对.class文件的搜索。从那个根开始,解释器会寻找包名,并将每个点号(句点)替换成一个斜杠,从而生成从CLASSPATH根开始的一个路径名(所以package foo.bar.baz会变成foo\bar\baz或者foo/bar/baz;具体是正斜杠还是反斜杠由操作系统决定)。随后将它们连接到一起,成为CLASSPATH内的各个条目(入口)。以后搜索.class文件时,就可从这些地方开始查找与准备创建的类名对应的名字。此外,它也会搜索一些标准目录——这些目录与Java解释器驻留的地方有关。
为进一步理解这个问题,下面以我自己的域名为例,它是。将其反转过来后,com.bruceeckel就为我的类创建了独一无二的全局名称(com,edu,org,net等扩展名以前在Java包中都是大写的,但自Java 1.2以来,这种情况已发生了变化。现在整个包名都是小写的)。由于决定创建一个名为util的库,我可以进一步地分割它,所以最后得到的包名如下:
package com.bruceeckel.util;
现在,可将这个包名作为下述两个文件的“命名空间”使用:
//: Vector.java
// Creating a package
package com.bruceeckel.util;
public class Vector {
public Vector() {
System.out.println(
"com.bruceeckel.util.Vector");
}
} ///:~
创建自己的包时,要求package语句必须是文件中的第一个“非注释”代码。第二个文件表面看起来是类似的:
//: List.java
// Creating a package
package com.bruceeckel.util;
public class List {
public List() {
System.out.println(
"com.bruceeckel.util.List");
}
} ///:~
这两个文件都置于我自己系统的一个子目录中:
C:\DOC\JavaT\com\bruceeckel\util
若通过它往回走,就会发现包名com.bruceeckel.util,但路径的第一部分又是什么呢?这是由CLASSPATH环境变量决定的。在我的机器上,它是:
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT
可以看出,CLASSPATH里能包含大量备用的搜索路径。然而,使用JAR文件时要注意一个问题:必须将JAR文件的名字置于类路径里,而不仅仅是它所在的路径。所以对一个名为grape.jar的JAR文件来说,我们的类路径需要包括:
CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar
正确设置好类路径后,可将下面这个文件置于任何目录里:
//: LibTest.java
// Uses the library
package c05;
import com.bruceeckel.util.*;
public class LibTest {
public static void main(String[] args) {
Vector v = new Vector();
List l = new List();
}
} ///:~
编译器遇到import语句后,它会搜索由CLASSPATH指定的目录,查找子目录com\bruceeckel\util,然后查找名称适当的已编译文件(对于Vector是Vector.class,对于List则是List.class)。注意Vector和List内无论类还是需要的方法都必须设为public。
1. 自动编译
为导入的类首次创建一个对象时(或者访问一个类的static成员时),编译器会在适当的目录里寻找同名的.class文件(所以如果创建类X的一个对象,就应该是X.class)。若只发现X.class,它就是必须使用的那一个类。然而,如果它在相同的目录中还发现了一个X.java,编译器就会比较两个文件的日期标记。如果X.java比X.class新,就会自动编译X.java,生成一个最新的X.class。
对于一个特定的类,或在与它同名的.java文件中没有找到它,就会对那个类采取上述的处理。
2. 冲突
若通过*导入了两个库,而且它们包括相同的名字,这时会出现什么情况呢?例如,假定一个程序使用了下述导入语句:
import com.bruceeckel.util.*;
import java.util.*;
由于java.util.*也包含了一个Vector类,所以这会造成潜在的冲突。然而,只要冲突并不真的发生,那么就不会产生任何问题——这当然是最理想的情况,因为否则的话,就需要进行大量编程工作,防范那些可能可能永远也不会发生的冲突。
如现在试着生成一个Vector,就肯定会发生冲突。如下所示:
Vector v = new Vector();
它引用的到底是哪个Vector类呢?编译器对这个问题没有答案,读者也不可能知道。所以编译器会报告一个错误,强迫我们进行明确的说明。例如,假设我想使用标准的Java Vector,那么必须象下面这样编程:
java.util.Vector v = new java.util.Vector();
由于它(与CLASSPATH一起)完整指定了那个Vector的位置,所以不再需要import java.util.*语句,除非还想使用来自java.util的其他东西。
4.3.2 自定义工具库
掌握前述的知识后,接下来就可以开始创建自己的工具库,以便减少或者完全消除重复的代码。例如,可为System.out.println()创建一个别名,减少重复键入的代码量。它可以是名为tools的一个包(package)的一部分:
//: P.java
// The P.rint & P.rintln shorthand
package com.bruceeckel.tools;
public class P {
public static void rint(Object obj) {
System.out.print(obj);
}
public static void rint(String s) {
System.out.print(s);
}
public static void rint(char[] s) {
System.out.print(s);
}
public static void rint(char c) {
System.out.print(c);
}
public static void rint(int i) {
System.out.print(i);
}
public static void rint(long l) {
System.out.print(l);
}
public static void rint(float f) {
System.out.print(f);
}
public static void rint(double d) {
System.out.print(d);
}
public static void rint(boolean b) {
System.out.print(b);
}
public static void rintln() {
System.out.println();
}
public static void rintln(Object obj) {
System.out.println(obj);
}
public static void rintln(String s) {
System.out.println(s);
}
public static void rintln(char[] s) {
System.out.println(s);
}
public static void rintln(char c) {
System.out.println(c);
}
public static void rintln(int i) {
System.out.println(i);
}
public static void rintln(long l) {
System.out.println(l);
}
public static void rintln(float f) {
System.out.println(f);
}
public static void rintln(double d) {
System.out.println(d);
}
public static void rintln(boolean b) {
System.out.println(b);
}
} ///:~
所有不同的数据类型现在都可以在一个新行输出(P.rintln()),或者不在一个新行输出(P.rint())。
大家可能会猜想这个文件所在的目录必须从某个CLASSPATH位置开始,然后继续com/bruceeckel/tools。编译完毕后,利用一个import语句,即可在自己系统的任何地方使用P.class文件。如下所示:
ToolTest.java
所以从现在开始,无论什么时候只要做出了一个有用的新工具,就可将其加入tools目录(或者自己的个人util或tools目录)。
1. CLASSPATH的陷阱
P.java文件存在一个非常有趣的陷阱。特别是对于早期的Java实现方案来说,类路径的正确设定通常都是很困难的一项工作。编写这本书的时候,我引入了P.java文件,它最初看起来似乎工作很正常。但在某些情况下,却开始出现中断。在很长的时间里,我都确信这是Java或其他什么在实现时一个错误。但最后,我终于发现在一个地方引入了一个程序(即第17章要说明的CodePackager.java),它使用了一个不同的类P。由于它作为一个工具使用,所以有时候会进入类路径里;另一些时候则不会这样。但只要它进入类路径,那么假若执行的程序需要寻找com.bruceeckel.tools中的类,Java首先发现的就是CodePackager.java中的P。此时,编译器会报告一个特定的方法没有找到。这当然是非常令人头疼的,因为我们在前面的类P里明明看到了这个方法,而且根本没有更多的诊断报告可为我们提供一条线索,让我们知道找到的是一个完全不同的类(那甚至不是public的)。
乍一看来,这似乎是编译器的一个错误,但假若考察import语句,就会发现它只是说:“在这里可能发现了P”。然而,我们假定的是编译器搜索自己类路径的任何地方,所以一旦它发现一个P,就会使用它;若在搜索过程中发现了“错误的”一个,它就会停止搜索。这与我们在前面表述的稍微有些区别,因为存在一些讨厌的类,它们都位于包内。而这里有一个不在包内的P,但仍可在常规的类路径搜索过程中找到。
如果您遇到象这样的情况,请务必保证对于类路径的每个地方,每个名字都仅存在一个类。
4.3.3 利用导入改变行为
Java已取消的一种特性是C的“条件编译”,它允许我们改变参数,获得不同的行为,同时不改变其他任何代码。Java之所以抛弃了这一特性,可能是由于该特性经常在C里用于解决跨平台问题:代码的不同部分根据具体的平台进行编译,否则不能在特定的平台上运行。由于Java的设计思想是成为一种自动跨平台的语言,所以这种特性是没有必要的。
然而,条件编译还有另一些非常有价值的用途。一种很常见的用途就是调试代码。调试特性可在开发过程中使用,但在发行的产品中却无此功能。Alen Holub(www.)提出了利用包(package)来模仿条件编译的概念。根据这一概念,它创建了C“断定机制”一个非常有用的Java版本。之所以叫作“断定机制”,是由于我们可以说“它应该为真”或者“它应该为假”。如果语句不同意你的断定,就可以发现相关的情况。这种工具在调试过程中是特别有用的。
可用下面这个类进行程序调试:
//: Assert.java
// Assertion tool for debugging
package com.bruceeckel.tools.debug;
public class Assert {
private static void perr(String msg) {
System.err.println(msg);
}
public final static void is_true(boolean exp) {
if(!exp) perr("Assertion failed");
}
public final static void is_false(boolean exp){
if(exp) perr("Assertion failed");
}
public final static void
is_true(boolean exp, String msg) {
if(!exp) perr("Assertion failed: " + msg);
}
public final static void
is_false(boolean exp, String msg) {
if(exp) perr("Assertion failed: " + msg);
}
} ///:~
这个类只是简单地封装了布尔测试。如果失败,就显示出出错消息。在第9章,大家还会学习一个更高级的错误控制工具,名为“违例控制”。但在目前这种情况下,perr()方法已经可以很好地工作。
如果想使用这个类,可在自己的程序中加入下面这一行:
import com.bruceeckel.tools.debug.*;
如欲清除断定机制,以便自己能发行最终的代码,我们创建了第二个Assert类,但却是在一个不同的包里:
//: Assert.java
// Turning off the assertion output
// so you can ship the program.
package com.bruceeckel.tools;
public class Assert {
public final static void is_true(boolean exp){}
public final static void is_false(boolean exp){}
public final static void
is_true(boolean exp, String msg) {}
public final static void
is_false(boolean exp, String msg) {}
} ///:~
现在,假如将前一个import语句变成下面这个样子:
import com.bruceeckel.tools.*;
程序便不再显示出断言。下面是个例子:
//: TestAssert.java
// Demonstrating the assertion tool
package c05;
// Comment the following, and uncomment the
// subsequent line to change assertion behavior:
import com.bruceeckel.tools.debug.*;
// import com.bruceeckel.tools.*;
public class TestAssert {
public static void main(String[] args) {
Assert.is_true((2 + 2) == 5);
Assert.is_false((1 + 1) == 2);
Assert.is_true((2 + 2) == 5, "2 + 2 == 5");
Assert.is_false((1 + 1) == 2, "1 +1 != 2");
}
} ///:~
通过改变导入的package,我们可将自己的代码从调试版本变成最终的发行版本。这种技术可应用于任何种类的条件代码。
4.3.4 包的停用
大家应注意这样一个问题:每次创建一个包后,都在为包取名时间接地指定了一个目录结构。这个包必须存在(驻留)于由它的名字规定的目录内。而且这个目录必须能从CLASSPATH开始搜索并发现。最开始的时候,package关键字的运用可能会令人迷惑,因为除非坚持遵守根据目录路径指定包名的规则,否则就会在运行期获得大量莫名其妙的消息,指出找不到一个特定的类——即使那个类明明就在相同的目录中。若得到象这样的一条消息,请试着将package语句作为注释标记出去。如果这样做行得通,就可知道问题到底出在哪儿。
4.4 Java 常用包
java中的包:
java.apple.Applet 小应用程序类
java.awt(java.swing) 用户界面类
java.awt.event 事件处理和监听器类
java.beans.JavaBeans 组件模型类
java.io 输入输出类
java.lang 核心类
java.lang.reflect 反射机制类
java.math 任意精度的算术类
网络类
java.rmi 远程方法调用类
java.security 安全性类
java.sql java数据库连接类
java.text 国际化文本处理类
java.util 各种实用工具类
同课章节目录