﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>BlogJava-享受JAVA-文章分类-编程基础</title><link>http://www.blogjava.net/nighthawk/category/2524.html</link><description>享受JAVA，享受人生</description><language>zh-cn</language><lastBuildDate>Wed, 28 Feb 2007 20:30:02 GMT</lastBuildDate><pubDate>Wed, 28 Feb 2007 20:30:02 GMT</pubDate><ttl>60</ttl><item><title>设计模式之Adapter(适配器)</title><link>http://www.blogjava.net/nighthawk/articles/10432.html</link><dc:creator>nighthawk</dc:creator><author>nighthawk</author><pubDate>Thu, 18 Aug 2005 06:41:00 GMT</pubDate><guid>http://www.blogjava.net/nighthawk/articles/10432.html</guid><wfw:comment>http://www.blogjava.net/nighthawk/comments/10432.html</wfw:comment><comments>http://www.blogjava.net/nighthawk/articles/10432.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/nighthawk/comments/commentRss/10432.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/nighthawk/services/trackbacks/10432.html</trackback:ping><description><![CDATA[<P><B>适配器模式定义:</B><BR>将两个不兼容的类纠合在一起使用，属于结构型模式,需要有Adaptee(被适配者)和Adaptor(适配器)两个身份.</P>
<P><B>为何使用?</B><BR>我们经常碰到要将两个没有关系的类组合在一起使用,第一解决方案是：修改各自类的接口，但是如果我们没有源代码，或者，我们不愿意为了一个应用而修改各自的接口。 怎么办? </P>
<P>使用Adapter，在这两种接口之间创建一个混合接口(混血儿).</P>
<P><B>如何使用?</B><BR>实现Adapter方式,其实"think in Java"的"类再生"一节中已经提到,有两种方式：组合(composition)和继承(inheritance).</P>
<P><BR>假设我们要打桩，有两种类：方形桩 圆形桩.<BR>public class SquarePeg{<BR>　　public void insert(String str){<BR>　　　　System.out.println("SquarePeg insert():"+str);<BR>　　}</P>
<P>}</P>
<P>public class RoundPeg{<BR>　　public void insertIntohole(String msg){<BR>　　　　System.out.println("RoundPeg insertIntoHole():"+msg);<BR>}<BR>}</P>
<P>现在有一个应用,需要既打方形桩,又打圆形桩.那么我们需要将这两个没有关系的类综合应用.假设RoundPeg我们没有源代码,或源代码我们不想修改,那么我们使用Adapter来实现这个应用:</P>
<P>public class PegAdapter extends SquarePeg{<BR><BR>　　private RoundPeg roundPeg;</P>
<P>　　public PegAdapter(RoundPeg peg)(this.roundPeg=peg;)</P>
<P>　　public void insert(String str){ roundPeg.insertIntoHole(str);}</P>
<P>}</P>
<P>在上面代码中,RoundPeg属于Adaptee,是被适配者.PegAdapter是Adapter,将Adaptee(被适配者RoundPeg)和Target(目标SquarePeg)进行适配.实际上这是将组合方法(composition)和继承(inheritance)方法综合运用.</P>
<P>PegAdapter首先继承SquarePeg，然后使用new的组合生成对象方式，生成RoundPeg的对象roundPeg，再重载父类insert()方法。从这里,你也了解使用new生成对象和使用extends继承生成对象的不同,前者无需对原来的类修改,甚至无需要知道其内部结构和源代码.</P>
<P>如果你有些Java使用的经验，已经发现，这种模式经常使用。</P>
<P><B>进一步使用</B><BR>上面的PegAdapter是继承了SquarePeg,如果我们需要两边继承，即继承SquarePeg 又继承RoundPeg,因为Java中不允许多继承，但是我们可以实现(implements)两个接口(interface)</P>
<P>public interface IRoundPeg{<BR>　　public void insertIntoHole(String msg);<BR><BR>}</P>
<P>public interface ISquarePeg{<BR>　　public void insert(String str);</P>
<P>}</P>
<P>下面是新的RoundPeg 和SquarePeg, 除了实现接口这一区别，和上面的没什么区别。<BR>public class SquarePeg implements ISquarePeg{<BR>　　public void insert(String str){<BR>　　　　System.out.println("SquarePeg insert():"+str);<BR>　　}</P>
<P>}</P>
<P>public class RoundPeg implements IRoundPeg{<BR>　　public void insertIntohole(String msg){<BR>　　　　System.out.println("RoundPeg insertIntoHole():"+msg);<BR>　　}<BR>}</P>
<P>下面是新的PegAdapter,叫做two-way adapter:</P>
<P>public class PegAdapter implements IRoundPeg,ISquarePeg{<BR><BR>　　private RoundPeg roundPeg;<BR>　　private SquarePeg squarePeg;<BR><BR>　　// 构造方法<BR>　　public PegAdapter(RoundPeg peg){this.roundPeg=peg;}<BR>　　// 构造方法<BR>　　public PegAdapter(SquarePeg peg)(this.squarePeg=peg;)</P>
<P>　　public void insert(String str){ roundPeg.insertIntoHole(str);}</P>
<P>}</P>
<P>还有一种叫Pluggable Adapters,可以动态的获取几个adapters中一个。使用Reflection技术，可以动态的发现类中的Public方法。<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 自 JDON</P><img src ="http://www.blogjava.net/nighthawk/aggbug/10432.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/nighthawk/" target="_blank">nighthawk</a> 2005-08-18 14:41 <a href="http://www.blogjava.net/nighthawk/articles/10432.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>JSP/Servlet 中的汉字编码问题 </title><link>http://www.blogjava.net/nighthawk/articles/9222.html</link><dc:creator>nighthawk</dc:creator><author>nighthawk</author><pubDate>Thu, 04 Aug 2005 07:02:00 GMT</pubDate><guid>http://www.blogjava.net/nighthawk/articles/9222.html</guid><wfw:comment>http://www.blogjava.net/nighthawk/comments/9222.html</wfw:comment><comments>http://www.blogjava.net/nighthawk/articles/9222.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/nighthawk/comments/commentRss/9222.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/nighthawk/services/trackbacks/9222.html</trackback:ping><description><![CDATA[问题的起源 <BR><BR>每个国家（或区域）都规定了计算机信息交换用的字符编码集，如美国的扩展 ASCII码, 中国的 GB2312-80，日本的 JIS 等，作为该国家/区域内信息处理的基础，有着统一编码的重要作用。字符编码集按长度分为 SBCS（单字节字符集），DBCS（双字节字符集）两大类。早期的软件（尤其是操作系统），为了解决本地字符信息的计算机处理，出现了各种本地化版本（L10N），为了区分，引进了 LANG, Codepage 等概念。但是由于各个本地字符集代码范围重叠，相互间信息交换困难；软件各个本地化版本独立维护成本较高。因此有必要将本地化工作中的共性抽取出来，作一致处理，将特别的本地化处理内容降低到最少。这也就是所谓的国际化（I18N）。各种语言信息被进一步规范为 Locale 信息。处理的底层字符集变成了几乎包含了所有字形的 Unicode。 <BR><BR>现在大部分具有国际化特征的软件核心字符处理都是以 Unicode 为基础的，在软件运行时根据当时的 Locale/Lang/Codepage 设置确定相应的本地字符编码设置，并依此处理本地字符。在处理过程中需要实现 Unicode 和本地字符集的相互转换，甚或以 Unicode 为中间的两个不同本地字符集的相互转换。这种方式在网络环境下被进一步延伸，任何网络两端的字符信息也需要根据字符集的设置转换成可接受的内容。 <BR><BR>Java 语言内部是用 Unicode 表示字符的，遵守 Unicode V2.0。Java 程序无论是从/往文件系统以字符流读/写文件，还是往 URL 连接写 HTML 信息，或从 URL 连接读取参数值，都会有字符编码的转换。这样做虽然增加了编程的复杂度，容易引起混淆，但却是符合国际化的思想的。 <BR><BR>从理论上来说，这些根据字符集设置而进行的字符转换不应该产生太多问题。而事实是由于应用程序的实际运行环境不同，Unicode 和各个本地字符集的补充、完善，以及系统或应用程序实现的不规范，转码时出现的问题时时困扰着程序员和用户。 <BR><BR>2. GB2312-80，GBK，GB18030-2000 汉字字符集及 Encoding <BR><BR>其实解决 JAVA 程序中的汉字编码问题的方法往往很简单，但理解其背后的原因，定位问题，还需要了解现有的汉字编码和编码转换。 <BR><BR>GB2312-80 是在国内计算机汉字信息技术发展初始阶段制定的，其中包含了大部分常用的一、二级汉字，和 9 区的符号。该字符集是几乎所有的中文系统和国际化的软件都支持的中文字符集，这也是最基本的中文字符集。其编码范围是高位0xa1－0xfe，低位也是 0xa1-0xfe；汉字从 0xb0a1 开始，结束于 0xf7fe； <BR><BR>GBK 是 GB2312-80 的扩展，是向上兼容的。它包含了 20902 个汉字，其编码范围是 0x8140-0xfefe，剔除高位 0x80 的字位。其所有字符都可以一对一映射到 Unicode 2.0，也就是说 JAVA 实际上提供了 GBK 字符集的支持。这是现阶段 Windows 和其它一些中文操作系统的缺省字符集，但并不是所有的国际化软件都支持该字符集，感觉是他们并不完全知道 GBK 是怎么回事。值得注意的是它不是国家标准，而只是规范。随着 GB18030-2000国标的发布，它将在不久的将来完成它的历史使命。 <BR><BR>GB18030-2000(GBK2K) 在 GBK 的基础上进一步扩展了汉字，增加了藏、蒙等少数民族的字形。GBK2K 从根本上解决了字位不够，字形不足的问题。它有几个特点， <BR><BR>它并没有确定所有的字形，只是规定了编码范围，留待以后扩充。 <BR>编码是变长的，其二字节部分与 GBK 兼容；四字节部分是扩充的字形、字位，其编码范围是首字节 0x81-0xfe、二字节0x30-0x39、三字节 0x81-0xfe、四字节0x30-0x39。 <BR>它的推广是分阶段的，首先要求实现的是能够完全映射到 Unicode 3.0 标准的所有字形。 <BR>它是国家标准，是强制性的。<BR>现在还没有任何一个操作系统或软件实现了 GBK2K 的支持，这是现阶段和将来汉化的工作内容。 <BR>Unicode 的介绍......就免了吧。 <BR><BR>JAVA 支持的encoding中与中文编程相关的有：(有几个在JDK文档中未列出) ASCII 7-bit, 同 ascii7 <BR>ISO8859-1 8-bit, 同 8859_1,ISO-8859-1,ISO_8859-1,latin1... <BR>GB2312-80 同gb2312,gb2312-1980,EUC_CN,euccn,1381,Cp1381, 1383, Cp1383, ISO2022CN,ISO2022CN_GB...... <BR>GBK (注意大小写),同MS936 <BR>UTF8 UTF-8 <BR>GB18030 (现在只有IBM JDK1.3.?有支持), 同Cp1392,1392 <BR><BR><BR>JAVA 语言采用Unicode处理字符. 但从另一个角度来说，在java程序中也可以采用非Unicode的转码，重要的是保证程序入口和出口的汉字信息不失真。如完全采用ISO-8859-1来处理汉字也能达到正确的结果。网络上流行的许多解决方法，都属于这种类型。为了不致引起混淆，本文不对这种方法作讨论。 <BR><BR>3. 中文转码时'?'、乱码的由来 <BR><BR>两个方向转换都有可能得到错误的结果： <BR><BR>Unicode--&gt;Byte, 如果目标代码集不存在对应的代码，则得到的结果是0x3f. <BR>如：<BR>"\u00d6\u00ec\u00e9\u0046\u00bb\u00f9".getBytes("GBK") 的结果是 "?ìéF?ù", Hex 值是3fa8aca8a6463fa8b4. <BR>仔细看一下上面的结果，你会发现\u00ec被转换为0xa8ac, \u00e9被转换为\xa8a6... 它的实际有效位变长了！ 这是因为GB2312符号区中的一些符号被映射到一些公共的符号编码，由于这些符号出现在ISO-8859-1或其它一些SBCS字符集中，故它们在Unicode中编码比较靠前，有一些其有效位只有8位，和汉字的编码重叠(其实这种映射只是编码的映射，在显示时仔细不是一样的。Unicode 中的符号是单字节宽，汉字中的符号是双字节宽) . 在Unicode\u00a0--\u00ff 之间这样的符号有20个。了解这个特征非常重要！由此就不难理解为什么JAVA编程中，汉字编码的错误结果中常常会出现一些乱码(其实是符号字符), 而不全是'?'字符, 就比如上面的例子。<BR><BR>Byte--&gt;Unicode, 如果Byte标识的字符在源代码集不存在，则得到的结果是0xfffd. <BR>如：<BR>Byte ba[] = {(byte)0x81,(byte)0x40,(byte)0xb0,(byte)0xa1}; new String(ba,"gb2312"); <BR>结果是"?啊", hex 值是"\ufffd\u554a". 0x8140 是GBK字符，按GB2312转换表没有对应的值，取\ufffd. (请注意：在显示该uniCode时，因为没有对应的本地字符，所以也适用上一种情况，显示为一个"?".)<BR><BR>实际编程中，JSP/Servlet 程序得到错误的汉字信息，往往是这两个过程的叠加，有时甚至是两个过程叠加后反复作用的结果. <BR><BR>4. JSP/Servlet 汉字编码问题及在 WAS 中的解决办法 <BR><BR>4.1 常见的 encoding 问题的现象<BR>网上常出现的 JSP/Servlet encoding 问题一般都表现在 browser 或应用程序端，如: <BR>浏览器中看到的 Jsp/Servlet 页面中的汉字怎么都成了 ’?’ ? <BR>浏览器中看到的 Servlet 页面中的汉字怎么都成了乱码？ <BR>JAVA 应用程序界面中的汉字怎么都成了方块？ <BR>Jsp/Servlet 页面无法显示 GBK 汉字。 <BR>JSP 页面中内嵌在<%...%>,<%=...%>等Tag包含的 JAVA code 中的中文成了乱码，但页面的其它汉字是对的。 <BR>Jsp/Servlet 不能接收 form 提交的汉字。 <BR>JSP/Servlet 数据库读写无法获得正确的内容。<BR>隐藏在这些问题后面的是各种错误的字符转换和处理（除第3个外，是因为 Java font 设置错误引起的）。解决类似的字符 encoding 问题，需要了解 Jsp/Servlet 的运行过程，检查可能出现问题的各个点。 <BR><BR>4.2 JSP/Servlet web 编程时的 encoding 问题<BR>运行于Java 应用服务器的 JSP/Servlet 为 Browser 提供 HTML 内容 <BR>其中有字符编码转换的地方有:<BR><BR>JSP 编译。Java 应用服务器将根据 JVM 的 file.encoding 值读取 JSP 源文件，编译生成 JAVA 源文件，再根据 file.encoding 值写回文件系统。如果当前系统语言支持 GBK，那么这时候不会出现 encoding 问题。如果是英文的系统，如 LANG 是 en_US 的 Linux, AIX 或 Solaris，则要将 JVM 的 file.encoding 值置成 GBK 。系统语言如果是 GB2312，则根据需要，确定要不要设置 file.encoding，将 file.encoding 设为 GBK 可以解决潜在的 GBK 字符乱码问题 <BR><BR><BR>Java 需要被编译为 .class 才能在 JVM 中执行，这个过程存在与a.同样的 file.encoding 问题。从这里开始 servlet 和 jsp 的运行就类似了，只不过 Servlet 的编译不是自动进行的。对于JSP程序, 对产生的JAVA 中间文件的编译是自动进行的(在程序中直接调用sun.tools.javac.Main类). 因此如果在这一步出现问题的话, 也要检查encoding和OS的语言环境，或者将内嵌在JSP JAVA Code 中的静态汉字转为 Unicode, 要么静态文本输出不要放在 JAVA code 中。 对于Servlet, javac 编译时手工指定-encoding 参数就可以了。 <BR><BR><BR>Servlet 需要将 HTML 页面内容转换为 browser 可接受的 encoding 内容发送出去。依赖于各 JAVA App Server 的实现方式，有的将查询 Browser 的 accept-charset 和 accept-language 参数或以其它猜的方式确定 encoding 值，有的则不管。因此采用固定encoding 也许是最好的解决方法。对于中文网页，可在 JSP 或 Servlet 中设置 contentType="text/html; charset=GB2312"；如果页面中有GBK字符，则设置为contentType="text/html; charset=GBK"，由于IE 和 Netscape对GBK的支持程度不一样，作这种设置时需要测试一下。<BR>因为16位 JAVA char在网络传送时高8位会被丢弃，也为了确保Servlet页面中的汉字（包括内嵌的和servlet运行过程中得到的）是期望的内码，可以用 PrintWriter out=res.getWriter() 取代 ServletOutputStream out=res.getOutputStream(). PrinterWriter 将根据contentType中指定的charset作转换 (ContentType需在此之前指定！); 也可以用OutputStreamWriter封装 ServletOutputStream 类并用write(String)输出汉字字符串。<BR>对于 JSP，JAVA Application Server 应当能够确保在这个阶段将嵌入的汉字正确传送出去。 <BR><BR><BR>这是解释 URL 字符 encoding 问题。如果通过 get/post 方式从 browser 返回的参数值中包含汉字信息， servlet 将无法得到正确的值。SUN的 J2SDK 中，HttpUtils.parseName 在解析参数时根本没有考虑 browser 的语言设置，而是将得到的值按 byte 方式解析。这是网上讨论得最多的 encoding 问题。因为这是设计缺陷，只能以 bin 方式重新解析得到的字符串；或者以 hack HttpUtils 类的方式解决。参考文章 2 均有介绍，不过最好将其中的中文 encoding GB2312、 CP1381 都改为 GBK，否则遇到 GBK 汉字时，还是会有问题。 <BR>Servlet API 2.3 提供一个新的函数 HttpServeletRequest.setCharacterEncoding 用于在调用 request.getParameter(“param_name”) 前指定应用程序希望的 encoding，这将有助于彻底解决这个问题。<BR>4.3 IBM Websphere Application Server 中的解决方法 <BR><BR>WebSphere Application Server 对标准的 Servlet API 2.x 作了扩展，提供较好的多语言支持。运行在中文的操作系统中，可以不作任何设置就可以很好地处理汉字。下面的说明只是对WAS是运行在英文的系统中，或者需要有GBK支持时有效。 <BR><BR>上述c,d情况，WAS 都要查询 Browser 的语言设置，在缺省状况下， zh, zh-cn 等均被映射为 JAVA encoding CP1381（注意： CP1381 只是等同于 GB2312 的一个 codepage，没有 GBK 支持）。这样做我想是因为无法确认 Browser 运行的操作系统是支持GB2312, 还是 GBK，所以取其小。但是实际的应用系统还是要求页面中出现 GBK 汉字，最著名的是朱总理名字中的“镕"(rong2 ，0xe946，\u9555)，所以有时还是需要将 Encoding/Charset 指定为 GBK。当然 WAS 中变更缺省的 encoding 没有上面说的那么麻烦，针对 a,b，参考文章 5，在 Application Server 的命令行参数中指定 -Dfile.encoding=GBK 即可； 针对 d，在 Application Server 的命令行参数中指定-Ddefault.client.encoding=GBK。如果指定了-Ddefault.client.encoding=GBK，那么c情况下可以不再指定charset。 <BR><BR>上面列出的问题中还有一个关于Tag<%...%>,<%=...%>中的 JAVA 代码里包含的静态文本未能正确显示的问题，在WAS中的解决方法是除了设置正确的file.encoding, 还需要以相同方法设置-Duser.language=zh -Duser.region=CN。这与JAVA locale的设置有关。 <BR><BR>4.4 数据库读写时的 encoding 问题 <BR><BR>JSP/Servlet 编程中经常出现 encoding 问题的另一个地方是读写数据库中的数据。 <BR><BR>流行的关系数据库系统都支持数据库 encoding，也就是说在创建数据库时可以指定它自己的字符集设置，数据库的数据以指定的编码形式存储。当应用程序访问数据时，在入口和出口处都会有 encoding 转换。 对于中文数据，数据库字符编码的设置应当保证数据的完整性. GB2312，GBK，UTF-8 等都是可选的数据库 encoding；也可以选择 ISO8859-1 (8-bit)，那么应用程序在写数据之前须将 16Bit 的一个汉字或 Unicode 拆分成两个 8-bit 的字符，读数据之后则需将两个字节合并起来，同时还要判别其中的 SBCS 字符。没有充分利用数据库 encoding 的作用，反而增加了编程的复杂度，ISO8859-1不是推荐的数据库 encoding。JSP/Servlet编程时，可以先用数据库管理系统提供的管理功能检查其中的中文数据是否正确。 <BR><BR>然后应当注意的是读出来的数据的 encoding，JAVA 程序中一般得到的是 Unicode。写数据时则相反。 <BR><BR>4.5 定位问题时常用的技巧 <BR><BR>定位中文encoding问题通常采用最笨的也是最有效的办法——在你认为有嫌疑的程序处理后打印字符串的内码。通过打印字符串的内码，你可以发现什么时候中文字符被转换成Unicode，什么时候Unicode被转回中文内码，什么时候一个中文字成了两个 Unicode 字符，什么时候中文字符串被转成了一串问号，什么时候中文字符串的高位被截掉了…… <BR><BR>取用合适的样本字符串也有助于区分问题的类型。如：”aa啊aa丂aa” 等中英相间、GB、GBK特征字符均有的字符串。一般来说，英文字符无论怎么转换或处理，都不会失真（如果遇到了，可以尝试着增加连续的英文字母长度）。<BR><BR>5. 结束语 <BR><BR>其实 JSP/Servlet 的中文encoding 并没有想像的那么复杂，虽然定位和解决问题没有定规，各种运行环境也各不尽然，但后面的原理是一样的。了解字符集的知识是解决字符问题的基础。不过，随着中文字符集的变化，不仅仅是 java 编程，中文信息处理中的问题还是会存在一段时间的。 <BR><BR>6. 参考文章 <BR><BR>Character Problem Review <BR>Java 编程技术中汉字问题的分析及解决 <BR>GB18030 <BR>Setting language encoding in web applications: Websphere applications Server<BR><img src ="http://www.blogjava.net/nighthawk/aggbug/9222.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/nighthawk/" target="_blank">nighthawk</a> 2005-08-04 15:02 <a href="http://www.blogjava.net/nighthawk/articles/9222.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>介绍线程、线程类及Runnable。（转） </title><link>http://www.blogjava.net/nighthawk/articles/9219.html</link><dc:creator>nighthawk</dc:creator><author>nighthawk</author><pubDate>Thu, 04 Aug 2005 06:59:00 GMT</pubDate><guid>http://www.blogjava.net/nighthawk/articles/9219.html</guid><wfw:comment>http://www.blogjava.net/nighthawk/comments/9219.html</wfw:comment><comments>http://www.blogjava.net/nighthawk/articles/9219.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.blogjava.net/nighthawk/comments/commentRss/9219.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/nighthawk/services/trackbacks/9219.html</trackback:ping><description><![CDATA[摘要 <BR>用户期望程序能展现优异的性能。为了满足这个期望，你的程序常常使用到线程。在这篇文章中我们开始练习使用线程。你将学习到线程、线程类及Runnable。 <BR><BR>用户不喜欢反应迟钝的软件。当用户单击一个鼠标时，他们希望程序立即回应他们的请求，即使程序正处于费时的运行之中，比如为一篇很长的文档重编页码或等待一个网络操作的完成。对用户响应很慢的程序其性能拙劣。为提高程序性能，开发者一般使用线程。 <BR>这篇文章是探索线程的第一部份。虽然你可能认为线程是一种难于掌握的事物，但我打算向你显示线程是易于理解的。在这篇文章中，我将向你介绍线程和线程类，以及讨论Runnable。此外，在后面的文章中，我将探索同步（通过锁），同步的问题（比如死锁），等待/通知机制，时序安排（有优先权和没有优先权），线程中断，计时器，挥发性，线程组和线程本地变量。 <BR>阅读关于线程设计的整个系列： <BR>·第1部份：介绍线程和线程类，以及Runnable <BR>·第2部份：使用同步使线程串行化访问关键代码部份 <BR>注意 <BR>这篇文章及其应用程序的三个相关线程练习与applets不同。然而，我在应用程序中介绍的多数应用到applets。主要不同的是：为了安全的原因，不是所有的线程操作都可以放到一个applet中（我将在以后的文章中讨论applets）。 <BR>什么是线程？ <BR>线程的概念并不难于掌握：它是程序代码的一个独立的执行通道。当多个线程执行时，经由相同代码的一个线程的通道通常与其它的不同。例如，假设一个线程执行一段相当于一个if-else语句的if部分的字节代码时，而另一个线程正执行相当于else部分的字节代码。JVM怎样保持对于每一个线程执行的跟踪呢？JVM给每一个线程它自己的方法调用堆栈。另外跟踪当前指令字节代码，方法堆栈跟踪本地变量，JVM传递给一个方法的参数，以及方法的返回值。 <BR>当多个线程在同一个程序中执行字节代码序列时，这种行为叫作多线程。多线程在多方面有利于程序： <BR>·当执行其它任务时多线程GUI（图形用户界面）程序仍能保持对用户的响应，比如重编页码或打印一个文档。 <BR>·带线程的程序一般比它们没有带线程的副本程序完成得快。这尤其表现在线程运行在一个多处理器机器上，在这里每一个线程都有它自己的处理器。 <BR>Java通过java.lang.Thread类完成多线程。每一个线程对象描述一个单独的执行线程。那些运行发生在线程的run()方法中。因为缺省的run()方法什么都不做，你必须创建Thread子类并重载run()以完成有用的工作。练习列表1中领略一个在Thread中的线程及多线程： <BR>列表1. ThreadDemo.java <BR>// ThreadDemo.java <BR>class ThreadDemo <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt = new MyThread (); <BR>mt.start (); <BR>for (int i = 0; i &lt; 50; i++) <BR>System.out.println ("i = " + i + ", i * i = " + i * i); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>public void run () <BR>{ <BR>for (int count = 1, row = 1; row &lt; 20; row++, count++) <BR>{ <BR>for (int i = 0; i &lt; count; i++) <BR>System.out.print ('*'); <BR>System.out.print ('\n'); <BR>} <BR>} <BR>} <BR>列表1显示了一个由类ThreadDemo和MyThread组成的应用程序的源代码。类ThreadDemo通过创建一个MyThread对象驱动应用程序，开始一个与其对象相关的线程并执行一段打印一个正方形表的代码。相反， MyThread重载Thread的run()方法打印（通过标准输入流）一个由星形符号组成的直角三角形。 <BR>当你键入java ThreadDemo运行应用程序时， JVM创建一个运行main()方法的开始线程。通过执行mt.start ()，开始线程告诉JVM创建一个执行包含MyThread对象的run()方法的字节代码指令的第二个线程。当start()方法返回时，开始线程循环执行打印一个正方形表，此时另一个新线程执行run()方法打印直角三角形。 <BR>输出会象什么样呢？运行ThreadDemo就可以看到。你将注意到每一个线程的输出与其它线程的输出相互交替。这样的结果是因为两个线程将它们的输出都发送到了同样的标准输出流。 <BR>注意 <BR>多数（不是所有）JVM设备使用下层平台的线程性能。因为那些性能是平台特有的，你的多线程程序的输出顺序可能与一些人的其他输出的顺序不一样。这种不同是由于时序的安排，我将在这一系列的稍后探讨这一话题。 <BR>线程类 <BR>要精通写多线程代码，你必须首先理解创建Thread类的多种方法。这部份将探讨这些方法。明确地说，你将学到开始线程的方法，命名线程，使线程休眠，决定一个线程是否激活，将一个线程与另一个线程相联，和在当前线程的线程组及子组中列举所有激活的线程。我也会讨论线程调试辅助程序及用户线程与监督线程的对比。 <BR>我将在以后的文章中介绍线程方法的余下部份，Sun不赞成的方法除外。 <BR>警告 <BR>Sun有一些不赞成的线程方法种类，比如suspend()和resume()，因为它们能锁住你的程序或破坏对象。所以，你不必在你的代码中调用它们。考虑到针对这些方法工作区的SDK文件，在这篇文章中我没有包含这些方法。 <BR>构造线程 <BR>Thread有八个构造器。最简单的是： <BR>·Thread()，用缺省名称创建一个Thread对象 <BR>·Thread(String name)，用指定的name参数的名称创建一个Thread对象 <BR>下一个最简单的构造器是Thread(Runnable target)和Thread(Runnable target, String name)。 除Runnable参数之外，这些构造器与前述的构造器一样。不同的是：Runnable参数识别提供run()方法的线程之外的对象。（你将在这篇文章稍后学到Runnable。）最后几个构造器是Thread(String name)，Thread(Runnable target)，和Thread(Runnable target, String name)。然而，最后的构造器包含了一个为了组织意图的ThreadGroup参数。 <BR>最后四个构造器之一，Thread(ThreadGroup group, Runnable target, String name, long stackSize)，令人感兴趣的是它能够让你指定想要的线程方法调用堆栈的大小。能够指定大小将证明在使用递归方法（一种为何一个方法不断重复调用自身的技术）优美地解决一些问题的程序中是十分有帮助的。通过明确地设置堆栈大小，你有时能够预防StackOverflowErrors。然而，太大将导致OutOfMemoryErrors。同样，Sun将方法调用堆栈的大小看作平台依赖。依赖平台，方法调用堆栈的大小可能改变。因此，在写调用Thread(ThreadGroup group, Runnable target, String name, long stackSize)代码前仔细考虑你的程序分枝。 <BR>开始你的运载工具 <BR>线程类似于运载工具：它们将程序从开始移动到结束。Thread 和Thread子类对象不是线程。它们描述一个线程的属性，比如名称和包含线程执行的代码（经由一个run（）方法）。当一个新线程执行run（）时，另一个线程正调用Thread或其子类对象的start()方法。例如，要开始第二个线程，应用程序的开始线程—它执行main（）—调用start（）。作为响应，JVM和平台一起工作的线程操作代码确保线程正确地初始化并调用Thread或其子类对象的run()方法。 <BR>一旦start()完成，多重线程便运行。因为我们趋向于在一种线性的方式中思维，我们常发现当两个或更多线程正运行时理解并发（同时）行为是困难的。因此，你应该看看显示与时间对比一个线程正在哪里执行（它的位置）的图表。下图就是这样一个图表。 <BR><BR><BR>与时间对比一个开始线程和一个新建线程执行位置的行为<BR><BR>图表显示了几个重要的时间段： <BR>·开始线程的初始化 <BR>·线程开始执行main（）瞬间 <BR>·线程开始执行start（）的瞬间 <BR>·start()创建一个新线程并返回main（）的瞬间 <BR>·新线程的初始化 <BR>·新线程开始执行run（）的瞬间 <BR>·每个线程结束的不同瞬间 <BR>注意新线程的初始化，它对run（）的执行，和它的结束都与开始线程的执行同时发生。 <BR>警告 <BR>一个线程调用start()后，在run（）方法退出前并发调用那方法将导致start()掷出一个java.lang.IllegalThreadStateException对象。 <BR><BR><BR><BR>怎样使用名称 <BR>在一个调试会话期间，使用用户友好方式从另一个线程区别其中一个线程证明是有帮助的。要区分其中一个线程，Java给一个线程取一个名称。Thread缺省的名称是一个短线连字符和一个零开始的数字符号。你可以接受Java的缺省线程名称或选择使用你自己的。为了能够自定义名称，Thread提供带有name参数和一个setName(String name)方法的构造器。Thread也提供一个getName()方法返回当前名称。表2显示了怎样通过Thread(String name)创建一个自定义名称和通过在run()方法中调用getName()检索当前名称： <BR>表2.NameThatThread.java <BR>// NameThatThread.java <BR>class NameThatThread <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt; <BR>if (args.length == 0) <BR>mt = new MyThread (); <BR>else <BR>mt = new MyThread (args [0]); <BR>mt.start (); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>MyThread () <BR>{ <BR>//编译器创建等价于super()的字节代码 <BR>} <BR>MyThread (String name) <BR>{ <BR>super (name); //将名称传递给Thread超类 <BR>} <BR>public void run () <BR>{ <BR>System.out.println ("My name is: " + getName ()); <BR>} <BR>} <BR>你能够在命令行向MyThread传递一个可选的name参数。例如，java NameThatThread X 建立X作为线程的名称。如果你指定一个名称失败，你将看到下面的输出： <BR>My name is: Thread-1 <BR>如果你喜欢，你能够在MyThread(String name)构造器中将super(name)调用改变成setName(String name)调用——作为setName(name)后一种方法调用达到同样建立线程名称的目的——作为super(name)我作为练习保留给你们。 <BR>注意 <BR>Java主要将名称指派给运行main() 方法的线程，开始线程。你特别要看看当开始线程掷出一个例外对象时在线程“main”的例外显示的JVM的缺省例外处理打印消息。 <BR>休眠或停止休眠 <BR>在这一栏后面，我将向你介绍动画——在一个表面上重复画图形，这稍微不同于完成一个运动画面。要完成动画，一个线程必须在它显示两个连续画面时中止。调用Thread的静态sleep(long millis)方法强迫一个线程中止millis毫秒。另一个线程可能中断正在休眠的线程。如果这种事发生，正在休眠的线程将醒来并从sleep(long millis)方法掷出一个InterruptedException对象。结果，调用sleep(long millis)的代码必须在一个try代码块中出现——或代码方法必须在自己的throws子句中包括InterruptedException。 <BR>为了示范sleep(long millis)，我写了一个CalcPI1应用程序。这个应用程序开始了一个新线程便于用一个数学运算法则计算数学常量pi的值。当新线程计算时，开始线程通过调用sleep(long millis)中止10毫秒。在开始线程醒后，它将打印pi的值，其中新线程存贮在变量pi中。表3给出了CalcPI1的源代码： <BR>表3. CalcPI1.java <BR>// CalcPI1.java <BR>class CalcPI1 <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt = new MyThread (); <BR>mt.start (); <BR>try <BR>{ <BR>Thread.sleep (10); //休眠10毫秒 <BR>} <BR>catch (InterruptedException e) <BR>{ <BR>} <BR>System.out.println ("pi = " + mt.pi); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>boolean negative = true; <BR>double pi; //缺省初始化为0.0 <BR>public void run () <BR>{ <BR>for (int i = 3; i &lt; 100000; i += 2) <BR>{ <BR>if (negative) <BR>pi -= (1.0 / i); <BR>else <BR>pi += (1.0 / i); <BR>negative = !negative; <BR>} <BR>pi += 1.0; <BR>pi *= 4.0; <BR>System.out.println ("Finished calculating PI"); <BR>} <BR>} <BR>如果你运行这个程序，你将看到输出如下（但也可能不一样）： <BR>pi = -0.2146197014017295 <BR>完成计算PI <BR>为什么输出不正确呢？毕竟，pi的值应近似等于3.14159。回答是：开始线程醒得太快了。在新线程刚开始计算pi时，开始线程就醒过来读取pi的当前值并打印其值。我们可以通过将10毫秒延迟增加为更长的值来进行补偿。这一更长的值（不幸的是它是依赖于平台的）将给新线程一个机会在开始线程醒过来之前完成计算。(后面，你将学到一种不依赖平台的技术，它将防止开始线程醒来直到新线程完成。) <BR>注意 <BR>线程同时提供一个sleep(long millis, int nanos)方法，它将线程休眠millis 毫秒和nanos 纳秒。因为多数基于JVM的平台都不支持纳秒级的分解度，JVM 线程处理代码将纳秒数字四舍五入成毫秒数字的近似值。如果一个平台不支持毫秒级的分解度，JVM 线程处理代码将毫秒数字四舍五入成平台支持的最小级分解度的近似倍数。 <BR>它是死的还是活的？ <BR>当一个程序调用Thread的start()方法时，在一个新线程调用run()之前有一个时间段（为了初始化）。run()返回后，在JVM清除线程之前有一段时间通过。JVM认为线程立即激活优先于线程调用run()，在线程执行run()期间和run()返回后。在这时间间隔期间，Thread的isAlive()方法返回一个布尔真值。否则，方法返回一个假值。 <BR>isAlive()在一个线程需要在第一个线程能够检查其它线程的结果之前等待另一个线程完成其run()方法的情形下证明是有帮助的。实质上，那些需要等待的线程输入一个while循环。当isAlive()为其它线程返回真值时，等待线程调用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪费更多的CPU循环)。一旦isAlive()返回假值，等待线程便检查其它线程的结果。 <BR>你将在哪里使用这样的技术呢？对于起动器，一个CalcPI1的修改版本怎么样，在打印pi的值前开始线程在哪里等待新线程的完成？表4的CalcPI2源代码示范了这一技术： <BR>表4. CalcPI2.java <BR>// CalcPI2.java <BR>class CalcPI2 <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt = new MyThread (); <BR>mt.start (); <BR>while (mt.isAlive ()) <BR>try <BR>{ <BR>Thread.sleep (10); //休眠10毫秒 <BR>} <BR>catch (InterruptedException e) <BR>{ <BR>} <BR>System.out.println ("pi = " + mt.pi); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>boolean negative = true; <BR>double pi; //缺省初始化成0.0 <BR>public void run () <BR>{ <BR>for (int i = 3; i &lt; 100000; i += 2) <BR>{ <BR>if (negative) <BR>pi -= (1.0 / i); <BR>else <BR>pi += (1.0 / i); <BR>negative = !negative; <BR>} <BR>pi += 1.0; <BR>pi *= 4.0; <BR>System.out.println ("Finished calculating PI"); <BR>} <BR>} <BR>CalcPI2的开始线程在10毫秒时间间隔休眠，直到mt.isAlive ()返回假值。当那些发生时，开始线程从它的while循环中退出并打印pi的内容。如果你运行这个程序，你将看到如下的输出（但不一定一样）： <BR>完成计算PI <BR>pi = 3.1415726535897894 <BR>这不，现在看上去更精确了？ <BR>注意 <BR>一个线程可能对它自己调用isAlive() 方法。然而，这毫无意义，因为isAlive()将一直返回真值。 <BR>合力 <BR>因为while循环/isAlive()方法/sleep()方法技术证明是有用的，Sun将其打包进三个方法组成的一个组合里：join()，join(long millis)和join(long millis, int nanos)。当当前线程想等待其它线程结束时，经由另一个线程的线程对象引用调用join()。相反，当它想其中任意线程等待其它线程结束或等待直到millis毫秒和nanos纳秒组合通过时，当前线程调用join(long millis)或join(long millis, int nanos)。(作为sleep()方法，JVM 线程处理代码将对join(long millis)和join(long millis,int nanos)方法的参数值四舍五入。)表5的CalcPI3源代码示范了一个对join()的调用： <BR>表5. CalcPI3.java <BR>// CalcPI3.java <BR>class CalcPI3 <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt = new MyThread (); <BR>mt.start (); <BR>try <BR>{ <BR>mt.join (); <BR>} <BR>catch (InterruptedException e) <BR>{ <BR>} <BR>System.out.println ("pi = " + mt.pi); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>boolean negative = true; <BR>double pi; //缺省初始化成0.0 <BR>public void run () <BR>{ <BR>for (int i = 3; i &lt; 100000; i += 2) <BR>{ <BR>if (negative) <BR>pi -= (1.0 / i); <BR>else <BR>pi += (1.0 / i); <BR>negative = !negative; <BR>} <BR>pi += 1.0; <BR>pi *= 4.0; <BR>System.out.println ("Finished calculating PI"); <BR>} <BR>} <BR>CalcPI3的开始线程等待与MyThread对象有关被mt引用的线程结束。接着开始线程打印pi的值，其值与CalcPI2的输出一样。 <BR>警告 <BR>不要试图将当前线程与其自身连接，因为这样当前线程将要永远等待。 <BR><BR><BR>怎样使用名称 <BR>在一个调试会话期间，使用用户友好方式从另一个线程区别其中一个线程证明是有帮助的。要区分其中一个线程，Java给一个线程取一个名称。Thread缺省的名称是一个短线连字符和一个零开始的数字符号。你可以接受Java的缺省线程名称或选择使用你自己的。为了能够自定义名称，Thread提供带有name参数和一个setName(String name)方法的构造器。Thread也提供一个getName()方法返回当前名称。表2显示了怎样通过Thread(String name)创建一个自定义名称和通过在run()方法中调用getName()检索当前名称： <BR>表2.NameThatThread.java <BR>// NameThatThread.java <BR>class NameThatThread <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt; <BR>if (args.length == 0) <BR>mt = new MyThread (); <BR>else <BR>mt = new MyThread (args [0]); <BR>mt.start (); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>MyThread () <BR>{ <BR>//编译器创建等价于super()的字节代码 <BR>} <BR>MyThread (String name) <BR>{ <BR>super (name); //将名称传递给Thread超类 <BR>} <BR>public void run () <BR>{ <BR>System.out.println ("My name is: " + getName ()); <BR>} <BR>} <BR>你能够在命令行向MyThread传递一个可选的name参数。例如，java NameThatThread X 建立X作为线程的名称。如果你指定一个名称失败，你将看到下面的输出： <BR>My name is: Thread-1 <BR>如果你喜欢，你能够在MyThread(String name)构造器中将super(name)调用改变成setName(String name)调用——作为setName(name)后一种方法调用达到同样建立线程名称的目的——作为super(name)我作为练习保留给你们。 <BR>注意 <BR>Java主要将名称指派给运行main() 方法的线程，开始线程。你特别要看看当开始线程掷出一个例外对象时在线程“main”的例外显示的JVM的缺省例外处理打印消息。 <BR>休眠或停止休眠 <BR>在这一栏后面，我将向你介绍动画——在一个表面上重复画图形，这稍微不同于完成一个运动画面。要完成动画，一个线程必须在它显示两个连续画面时中止。调用Thread的静态sleep(long millis)方法强迫一个线程中止millis毫秒。另一个线程可能中断正在休眠的线程。如果这种事发生，正在休眠的线程将醒来并从sleep(long millis)方法掷出一个InterruptedException对象。结果，调用sleep(long millis)的代码必须在一个try代码块中出现——或代码方法必须在自己的throws子句中包括InterruptedException。 <BR>为了示范sleep(long millis)，我写了一个CalcPI1应用程序。这个应用程序开始了一个新线程便于用一个数学运算法则计算数学常量pi的值。当新线程计算时，开始线程通过调用sleep(long millis)中止10毫秒。在开始线程醒后，它将打印pi的值，其中新线程存贮在变量pi中。表3给出了CalcPI1的源代码： <BR>表3. CalcPI1.java <BR>// CalcPI1.java <BR>class CalcPI1 <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt = new MyThread (); <BR>mt.start (); <BR>try <BR>{ <BR>Thread.sleep (10); //休眠10毫秒 <BR>} <BR>catch (InterruptedException e) <BR>{ <BR>} <BR>System.out.println ("pi = " + mt.pi); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>boolean negative = true; <BR>double pi; //缺省初始化为0.0 <BR>public void run () <BR>{ <BR>for (int i = 3; i &lt; 100000; i += 2) <BR>{ <BR>if (negative) <BR>pi -= (1.0 / i); <BR>else <BR>pi += (1.0 / i); <BR>negative = !negative; <BR>} <BR>pi += 1.0; <BR>pi *= 4.0; <BR>System.out.println ("Finished calculating PI"); <BR>} <BR>} <BR>如果你运行这个程序，你将看到输出如下（但也可能不一样）： <BR>pi = -0.2146197014017295 <BR>完成计算PI <BR>为什么输出不正确呢？毕竟，pi的值应近似等于3.14159。回答是：开始线程醒得太快了。在新线程刚开始计算pi时，开始线程就醒过来读取pi的当前值并打印其值。我们可以通过将10毫秒延迟增加为更长的值来进行补偿。这一更长的值（不幸的是它是依赖于平台的）将给新线程一个机会在开始线程醒过来之前完成计算。(后面，你将学到一种不依赖平台的技术，它将防止开始线程醒来直到新线程完成。) <BR>注意 <BR>线程同时提供一个sleep(long millis, int nanos)方法，它将线程休眠millis 毫秒和nanos 纳秒。因为多数基于JVM的平台都不支持纳秒级的分解度，JVM 线程处理代码将纳秒数字四舍五入成毫秒数字的近似值。如果一个平台不支持毫秒级的分解度，JVM 线程处理代码将毫秒数字四舍五入成平台支持的最小级分解度的近似倍数。 <BR>它是死的还是活的？ <BR>当一个程序调用Thread的start()方法时，在一个新线程调用run()之前有一个时间段（为了初始化）。run()返回后，在JVM清除线程之前有一段时间通过。JVM认为线程立即激活优先于线程调用run()，在线程执行run()期间和run()返回后。在这时间间隔期间，Thread的isAlive()方法返回一个布尔真值。否则，方法返回一个假值。 <BR>isAlive()在一个线程需要在第一个线程能够检查其它线程的结果之前等待另一个线程完成其run()方法的情形下证明是有帮助的。实质上，那些需要等待的线程输入一个while循环。当isAlive()为其它线程返回真值时，等待线程调用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪费更多的CPU循环)。一旦isAlive()返回假值，等待线程便检查其它线程的结果。 <BR>你将在哪里使用这样的技术呢？对于起动器，一个CalcPI1的修改版本怎么样，在打印pi的值前开始线程在哪里等待新线程的完成？表4的CalcPI2源代码示范了这一技术： <BR>表4. CalcPI2.java <BR>// CalcPI2.java <BR>class CalcPI2 <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt = new MyThread (); <BR>mt.start (); <BR>while (mt.isAlive ()) <BR>try <BR>{ <BR>Thread.sleep (10); //休眠10毫秒 <BR>} <BR>catch (InterruptedException e) <BR>{ <BR>} <BR>System.out.println ("pi = " + mt.pi); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>boolean negative = true; <BR>double pi; //缺省初始化成0.0 <BR>public void run () <BR>{ <BR>for (int i = 3; i &lt; 100000; i += 2) <BR>{ <BR>if (negative) <BR>pi -= (1.0 / i); <BR>else <BR>pi += (1.0 / i); <BR>negative = !negative; <BR>} <BR>pi += 1.0; <BR>pi *= 4.0; <BR>System.out.println ("Finished calculating PI"); <BR>} <BR>} <BR>CalcPI2的开始线程在10毫秒时间间隔休眠，直到mt.isAlive ()返回假值。当那些发生时，开始线程从它的while循环中退出并打印pi的内容。如果你运行这个程序，你将看到如下的输出（但不一定一样）： <BR>完成计算PI <BR>pi = 3.1415726535897894 <BR>这不，现在看上去更精确了？ <BR>注意 <BR>一个线程可能对它自己调用isAlive() 方法。然而，这毫无意义，因为isAlive()将一直返回真值。 <BR>合力 <BR>因为while循环/isAlive()方法/sleep()方法技术证明是有用的，Sun将其打包进三个方法组成的一个组合里：join()，join(long millis)和join(long millis, int nanos)。当当前线程想等待其它线程结束时，经由另一个线程的线程对象引用调用join()。相反，当它想其中任意线程等待其它线程结束或等待直到millis毫秒和nanos纳秒组合通过时，当前线程调用join(long millis)或join(long millis, int nanos)。(作为sleep()方法，JVM 线程处理代码将对join(long millis)和join(long millis,int nanos)方法的参数值四舍五入。)表5的CalcPI3源代码示范了一个对join()的调用： <BR>表5. CalcPI3.java <BR>// CalcPI3.java <BR>class CalcPI3 <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>MyThread mt = new MyThread (); <BR>mt.start (); <BR>try <BR>{ <BR>mt.join (); <BR>} <BR>catch (InterruptedException e) <BR>{ <BR>} <BR>System.out.println ("pi = " + mt.pi); <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>boolean negative = true; <BR>double pi; //缺省初始化成0.0 <BR>public void run () <BR>{ <BR>for (int i = 3; i &lt; 100000; i += 2) <BR>{ <BR>if (negative) <BR>pi -= (1.0 / i); <BR>else <BR>pi += (1.0 / i); <BR>negative = !negative; <BR>} <BR>pi += 1.0; <BR>pi *= 4.0; <BR>System.out.println ("Finished calculating PI"); <BR>} <BR>} <BR>CalcPI3的开始线程等待与MyThread对象有关被mt引用的线程结束。接着开始线程打印pi的值，其值与CalcPI2的输出一样。 <BR>警告 <BR>不要试图将当前线程与其自身连接，因为这样当前线程将要永远等待。 <BR><BR><BR>查询活跃线程 <BR>在有些情形下，你可能想了解在你的程序中哪些线程是激活的。Thread支持一对方法帮助你完成这个任务： activeCount()和 enumerate(Thread [] thdarray)。但那些方法只工作在当前线程的线程组中。换句话说，那些方法只识别属于当前线程的同一线程组的活跃线程。 (我将在以后的系列文章中讨论线程组——一种组织机制。) <BR>静态activeCount()方法返回在当前线程的线程组中正在活跃运行的线程数量。一个程序利用这个方法的整数返回值设定一个Thread引用数组的大小。检索那些引用，程序必须调用静态enumerate(Thread [] thdarray)方法。这个方法的整数返回值确定Thread引用存贮在数组中的enumerate(Thread []thdarray)的总数。要看这些方法如何一起工作，请查看表6： <BR>表6. Census.java <BR>// Census.java <BR>class Census <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>Thread [] threads = new Thread [Thread.activeCount ()]; <BR>int n = Thread.enumerate (threads); <BR>for (int i = 0; i &lt; n; i++) <BR>System.out.println (threads [i].toString ()); <BR>} <BR>} <BR>在运行时，这个程序会产生如下的输出： <BR>Thread[main,5,main] <BR>输出显示一个线程，开始线程正在运行。左边的main表示线程的名称。5显示线程的优先权，右边的main表示线程的线程组。你也许很失望不能在输出中看到任何系统线程，比如垃圾收集器线程。那种限制由Thread的enumerate(Thread [] thdarray) 方法产生，它仅询问当前线程线程组的活跃线程。然而， ThreadGroup类包含多种enumerate()方法允许你捕获对所有活跃线程的引用而不管线程组。在稍后的系列中，探讨ThreadGroup时我将向你显示如何列举所有的引用。 <BR>警告 <BR>当重申一个数组时不要依靠activeCount()的返回值。如果你这样做了，你的程序将冒掷出一个NullPointerException对象的风险。为什么呢？在调用activeCount()和enumerate(Thread [] thdarray)之间，一个或更多线程可能结束。结果， enumerate(Thread [] thdarray)能够复制少数线程引用进它的数组。因此，仅考虑将activeCount()的返回值作为数组可能大小的最大值。同样，考虑将enumerate(Thread [] thdarray)的返回值作为在一个程序对那种方法调用时活跃线程的数目。 <BR>反臭虫 <BR>如果你的程序出现故障并且你怀疑问题出在线程，通过调用Thread的dumpStack()和toString()方法你能够了解到线程的更多细节。静态dumpStack()方法提供一个new Exception ("Stack trace").printStackTrace ()的封装，打印一个追踪当前线程的堆栈。toString()依据下面格式返回一个描述线程的名称、优先权和线程组的字符串： Thread[thread-name,priority,thread-group]. (在稍后的系列中你将学到更多关于优先权的知识。) <BR>技巧 <BR>在一些地方，这篇文章提到了当前线程的概念。如果你需要访问描述当前线程的Thread对象，则调用Thread的静态currentThread()方法。例：Thread current = Thread.currentThread ()。 <BR>等级系统 <BR>不是所有线程都被平等创建。它们被分成两类：用户和监督。一个用户线程执行着对于程序用户十分重要的工作，工作必须在程序结束前完成。相反，一个监督线程执行着后勤事务（比如垃圾收集）和其它可能不会对应用程序的主要工作作出贡献但对于应用程序继续它的主要工作却非常必要的后台任务。和用户线程不一样，监督线程不需要在应用程序结束前完成。当一个应用程序的开始线程（它是一个用户线程）结束时，JVM检查是否还有其它用户线程正在运行。如果有，JVM就会阻止应用程序结束。否则，JVM就会结束应用程序而不管监督线程是否正在运行。 <BR>当一个线程调用一个线程对象的start()方法时，新的已经开始的线程就是一个用户线程。那是缺省的。要建立一个线程作为监督线程，程序必须在调用start()前调用Thread的一个带布尔真值参数的setDaemon(boolean isDaemon)方法。稍后，你可以通过调用Thread的isDaemon()方法检查一个线程是否是监督线程。如果是监督线程那个方法返回一个布尔真值。 <BR>为了让你试试用户和监督线程，我写了一个UserDaemonThreadDemo： <BR>表7. UserDaemonThreadDemo.java <BR>// UserDaemonThreadDemo.java <BR>class UserDaemonThreadDemo <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>if (args.length == 0) <BR>new MyThread ().start (); <BR>else <BR>{ <BR>MyThread mt = new MyThread (); <BR>mt.setDaemon (true); <BR>mt.start (); <BR>} <BR>try <BR>{ <BR>Thread.sleep (100); <BR>} <BR>catch (InterruptedException e) <BR>{ <BR>} <BR>} <BR>} <BR>class MyThread extends Thread <BR>{ <BR>public void run () <BR>{ <BR>System.out.println ("Daemon is " + isDaemon ()); <BR>while (true); <BR>} <BR>} <BR>编译了代码后，通过Java2 SDK的java命令运行UserDaemonThreadDemo。如果你没有使用命令行参数运行程序，例如java UserDaemonThreadDemo， new MyThread ().start ()执行。这段代码片断开始一个在进入一个无限循环前打印Daemon is false的用户线程。(你必须按Ctrl-C或一个等价于结束一个无限循环的组合按键。)因为新线程是一个用户线程，应用程序在开始线程结束后仍保持运行。然而，如果你指定了至少一个命令行参数，例如java UserDaemonThreadDemo x，mt.setDaemon (true)执行并且新线程将是一个监督线程。结果，一旦开始线程从100毫秒休眠中醒来并结束，新的监督线程也将结束。 <BR>警告 <BR>如果线程开始执行后调用setDaemon(boolean isDaemon)方法，setDaemon(boolean isDaemon)方法将掷出一个IllegalThreadStateException对象。 <BR>Runnable <BR>学习前面部份的例子后，你可能认为引入多线程进入一个类总是要求你去扩展Thread并将你的子类重载Thread's run()方法。然而那并不总是一种选择。Java对继承的强制执行禁止一个类扩展两个或更多个超类。结果，如果一个类扩展了一个无线程类，那个类就不能扩展Thread. 假使限制，怎样才可能将多线程引入一个已经扩展了其它类的类？幸运的是， Java的设计者已经意识到不可能创建Thread子类的情形总会发生的。这导致产生java.lang.Runnable接口和带Runnable参数的Thread构造器，如Thread(Runnable target)。 <BR>Runnable接口声明了一个单独方法署名：void run()。这个署名和Thread的run()方法署名一样并作为线程的执行入口服务。因为Runnable是一个接口，任何类都能通过将一个implements子句包含进类头和提供一个适当的run()方法实现接口。在执行时间，程序代码能从那个类创建一个对象或runnable并将runnable的引用传递给一个适当的Thread构造器。构造器和Thread对象一起存贮这个引用并确保一个新线程在调用Thread对象的start()方法后调用runnable的run()方法。示范如表8： <BR>表8.RunnableDemo.java <BR>// RunnableDemo.java <BR>class RunnableDemo <BR>{ <BR>public static void main (String [] args) <BR>{ <BR>Rectangle r = new Rectangle (5, 6); <BR>r.draw (); <BR>//用随机选择的宽度和高度画不同的长方形 <BR>new Rectangle (); <BR>} <BR>} <BR>abstract class Shape <BR>{ <BR>abstract void draw (); <BR>} <BR>class Rectangle extends Shape implements Runnable <BR>{ <BR>private int w, h; <BR>Rectangle () <BR>{ <BR>//创建一个绑定这个runnable的新Thread对象并开始一个将调用这个runnable的 <BR>//run()方法的线程 <BR>new Thread (this).start (); <BR>} <BR>Rectangle (int w, int h) <BR>{ <BR>if (w &lt; 2) <BR>throw new IllegalArgumentException ("w value " + w + " &lt; 2"); <BR>if (h &lt; 2) <BR>throw new IllegalArgumentException ("h value " + h + " &lt; 2"); <BR>this.w = w; <BR>this.h = h; <BR>} <BR>void draw () <BR>{ <BR>for (int c = 0; c &lt; w; c++) <BR>System.out.print ('*'); <BR>System.out.print ('\n'); <BR>for (int r = 0; r &lt; h - 2; r++) <BR>{ <BR>System.out.print ('*'); <BR>for (int c = 0; c &lt; w - 2; c++) <BR>System.out.print (' '); <BR>System.out.print ('*'); <BR>System.out.print ('\n'); <BR>} <BR>for (int c = 0; c &lt; w; c++) <BR>System.out.print ('*'); <BR>System.out.print ('\n'); <BR>} <BR>public void run () <BR>{ <BR>for (int i = 0; i &lt; 20; i++) <BR>{ <BR>w = rnd (30); <BR>if (w &lt; 2) <BR>w += 2; <BR>h = rnd (10); <BR>if (h &lt; 2) <BR>h += 2; <BR>draw (); <BR>} <BR>} <BR>int rnd (int limit) <BR>{ <BR>//在0&lt;=x&lt;界限范围内返回一个随机数字x <BR>return (int) (Math.random () * limit); <BR>} <BR>} <BR>RunnableDemo由类RunnableDemo，Shape和Rectangle组成。类RunnableDemo通过创建一个Rectangle对象驱动应用程序—通过调用对象的draw()方法—和通过创建第二个什么都不做的Rectangle类。相反，Shape和Rectangle组成了一个基于shape层次的类。Shape是抽象的因为它提供一个抽象的draw()方法。各种shape类，比如Rectangle，扩展Shape和描述它们如何画它们自己的重载draw()。以后，我可能决定引入一些另外的shape类，创建一个Shape数组，通过调用Shape的draw()方法要求每一个Shape元素画它自己。 <BR>RunnableDemo 作为一个不带多线程的简单程序产生。后面我决定引入多线程到Rectangle，这样我能够用各种宽度和高度画种种矩形。因为Rectangle扩展Shape (为了以后的多态性原因)，我没有其它选择只有让Rectangle实现Runnable。同样，在Rectangle()构造器内，我不得不将一个Rectangle runnable绑定到一个新的Thread对象并调用Thread的start()方法开始一个新的线程调用Rectangle的run()方法画矩形。 <BR>因为包括在这篇文章中的RunnableDemo的新输出太长了，我建议你自己编译并运行程序。 <BR>技巧 <BR>当你面对一个类不是能扩展Thread就是能实现Runnable的情形时，你将选择哪种方法？如果这个类已经扩展了其它类，你必须实现Runnable。然而，如果这个类没有扩展其它类，考虑一下类的名称。名称将暗示这个类的对象不是积极的就是消极的。例如，名称Ticker暗示它的对象是积极的。因此，Ticker类将扩展Thread，并且Ticker对象将被作为专门的Thread对象。相反，Rectangle暗示消极对象—Rectangle对象对于它们自己什么也不做。因此，Rectangle类将实现Runnable,并且Rectangle 对象将使用Thread对象(为了测试或其它意图)代替成为专门的Thread对象。 <BR>回顾 <BR>用户期望程序达到优异的性能。一种办法是用线程完成那些任务。一个线程是一条程序代码的独立执行通道。线程有益于基于GUI的程序，因为它们允许那些程序当执行其它任务时仍对用户保持响应。另外，带线程的程序比它们没带线程的副本程序完成的快。这对于运行在多处理器机器上的情形尤其明显，在这里每一个线程有它自己的处理器。Thread和Thread子类对象描述了线程并与那些实体相关。对于那些不能扩展Thread的类，你必须创建一个runnable以利用多线程的优势。<img src ="http://www.blogjava.net/nighthawk/aggbug/9219.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/nighthawk/" target="_blank">nighthawk</a> 2005-08-04 14:59 <a href="http://www.blogjava.net/nighthawk/articles/9219.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>浅谈Java的输入输出流</title><link>http://www.blogjava.net/nighthawk/articles/9218.html</link><dc:creator>nighthawk</dc:creator><author>nighthawk</author><pubDate>Thu, 04 Aug 2005 06:57:00 GMT</pubDate><guid>http://www.blogjava.net/nighthawk/articles/9218.html</guid><wfw:comment>http://www.blogjava.net/nighthawk/comments/9218.html</wfw:comment><comments>http://www.blogjava.net/nighthawk/articles/9218.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/nighthawk/comments/commentRss/9218.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/nighthawk/services/trackbacks/9218.html</trackback:ping><description><![CDATA[<DIV align=center>
<P align=left>Java语言的输入输出功能是十分强大而灵活的，美中不足的是看上去输入输出的代码并不是很简洁，因为你往往需要包装许多不同的对象。在Java类库中，IO部分的内容是很庞大的，因为它涉及的领域很广泛:标准输入输出，文件的操作，网络上的数据流，字符串流，对象流，zip文件流....本文的目的是为大家做一个简要的介绍。</P>
<P align=left>流是一个很形象的概念，当程序需要读取数据的时候，就会开启一个通向数据源的流，这个数据源可以是文件，内存，或是网络连接。类似的，当程序需要写入数据的时候，就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样，如下图：<IMG height=108 src="http://www.frontfree.net/articles/pages/0000000728/19stream.gif" width=438></P>
<P align=left><IMG height=108 src="http://www.frontfree.net/articles/pages/0000000728/20stream2.gif" width=429></P>
<P align=left>Java中的流分为两种，一种是字节流，另一种是字符流，分别由四个抽象类来表示（每种流包括输入和输出两种所以一共四个）:InputStream，OutputStream，Reader，Writer。Java中其他多种多样变化的流均是由它们派生出来的:</P>
<P align=left><IMG height=199 src="http://www.frontfree.net/articles/pages/0000000728/25inputs.gif" width=462></P>
<P align=left><IMG height=142 src="http://www.frontfree.net/articles/pages/0000000728/26outputs.gif" width=458></P>
<P align=left>&nbsp;</P>
<P align=left><IMG height=169 src="http://www.frontfree.net/articles/pages/0000000728/23reader.gif" width=462></P>
<P align=left><IMG height=222 src="http://www.frontfree.net/articles/pages/0000000728/24writer.gif" width=465></P>
<P align=left>在这其中InputStream和OutputStream在早期的Java版本中就已经存在了，它们是基于字节流的，而基于字符流的Reader和Writer是后来加入作为补充的。以上的层次图是Java类库中的一个基本的层次体系，如果你感兴趣想了解更多内容的话，可以到<A href="http://java.sun.com/j2se/1.4.1/docs/api/java/io/package-summary.html">Sun公司主页</A>获取更多信息。</P>
<P align=left>在这四个抽象类中，InputStream和Reader定义了完全相同的接口：</P>
<TABLE class=code cellSpacing=2 cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD bgColor=#e0e0e0><FONT color=#0000ff><SPAN class=unnamed1>int</SPAN></FONT><SPAN class=unnamed1> read()<BR><FONT color=#0000ff>int</FONT> read(<FONT color=#0000ff>char</FONT> cbuf[])<BR><FONT color=#0000ff>int</FONT> read(<FONT color=#0000ff>char</FONT> cbuf[], <FONT color=#0000ff>int </FONT>offset, <FONT color=#0000ff>int</FONT> length)</SPAN></TD></TR></TBODY></TABLE>
<P align=left>而OutputStream和Writer也是如此：</P>
<TABLE class=code cellSpacing=2 cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD bgColor=#e0e0e0><FONT color=#0000ff><SPAN class=unnamed1>int</SPAN></FONT><SPAN class=unnamed1> write(<FONT color=#0000ff>int</FONT> c)<BR><FONT color=#0000ff>int</FONT> write(<FONT color=#0000ff>char</FONT> cbuf[])<BR><FONT color=#0000ff>int</FONT> write(<FONT color=#0000ff>char</FONT> cbuf[], <FONT color=#0000ff>int</FONT> offset, <FONT color=#0000ff>int</FONT> length)</SPAN></TD></TR></TBODY></TABLE>
<P align=left>这六个方法都是最基本的，read()和write()通过方法的重载来读写一个字节，或者一个字节数组。</P>
<P align=left>更多灵活多变的功能是由它们的子类来扩充完成的。知道了Java输入输出的基本层次结构以后，本文在这里想给大家一些以后可以反复应用例子，对于所有子类的细节及其功能并不详细讨论。</P>
<TABLE class=code cellSpacing=2 cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD bgColor=#e0e0e0>
<DIV align=center>
<P align=left><FONT color=#0000ff><SPAN class=unnamed1>import</SPAN></FONT><SPAN class=unnamed1> java.io.*;</SPAN></P></DIV>
<OL>
<DIV align=left>
<P class=unnamed1 align=left><FONT color=#0000ff>public class</FONT> IOStreamDemo {<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>public void</FONT> samples() throws IOException {</P>
<P align=left><SPAN class=unnamed1>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#006600>//1. 这是从键盘读入一行数据,返回的是一个字符串</FONT><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>BufferedReader</FONT> stdin =<FONT color=#0000ff>new</FONT> BufferedReader(new InputStreamReader(System.in)); <BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>System</FONT>.out.print(<FONT color=#ff00ff>"Enter a line:"</FONT>);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>System</FONT>.out.println(stdin.readLine());</SPAN></P>
<P class=unnamed1 align=left>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#006600>//2. 这是从文件中逐行读入数据 </FONT></P>
<P class=unnamed1 align=left>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>BufferedReader</FONT> in = <FONT color=#0000ff>new</FONT> BufferedReader(<FONT color=#0000ff>new</FONT> FileReader(<FONT color=#ff00ff>"IOStreamDemo.java"</FONT>));<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>String</FONT> s, s2 = <FONT color=#0000ff>new</FONT> String();<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>while</FONT>((s = in.readLine())!= <FONT color=#0000ff>null</FONT>)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;s2 += s + <FONT color=#ff00ff>"\n"</FONT>;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;in.close();<BR></P>
<P class=unnamed1 align=left>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#006600>//3. 这是从一个字符串中逐个读入字节 </FONT><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>StringReader</FONT> in1 = <FONT color=#0000ff>new</FONT> StringReader(s2);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>int</FONT> c;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>while</FONT>((c = in1.read()) != -1)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>System</FONT>.out.print((char)c);<BR></P>
<P align=left><SPAN class=unnamed1>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#006600>//4. 这是将一个字符串写入文件 </FONT><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>try</FONT> {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>BufferedReader</FONT> in2 = <FONT color=#0000ff>new</FONT> BufferedReader(new StringReader(s2));<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>PrintWriter</FONT> out1 = <FONT color=#000000>new</FONT> PrintWriter(new BufferedWriter(new FileWriter(<FONT color=#ff00ff>"IODemo.out"</FONT>)));<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>int</FONT> lineCount = 1;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>while</FONT>((s = in2.readLine()) != <FONT color=#0000ff>null</FONT> )<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;out1.println(lineCount++ + <FONT color=#ff00ff>": "</FONT> + s);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;out1.close();<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} <FONT color=#0000ff>catch</FONT>(EOFException e) {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>System</FONT>.err.println(<FONT color=#ff00ff>"End of stream"</FONT>);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} </SPAN></P>
<P class=unnamed1 align=left>}</P></DIV></OL></TD></TR></TBODY></TABLE>
<P align=left>对于上面的例子，需要说明的有以下几点：</P></DIV>
<DIV align=center>
<P align=left>1.<FONT color=#ff0000> BufferedReader</FONT>是<FONT color=#ff0000>Reader</FONT>的一个子类，它具有缓冲的作用，避免了频繁的从物理设备中读取信息。它有以下两个构造函数：</P>
<TABLE class=code cellSpacing=2 cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD bgColor=#e0e0e0><FONT color=#ff0000><SPAN class=unnamed1>BufferedReader</SPAN></FONT><SPAN class=unnamed1>(<FONT color=#0000ff>Reader</FONT> in) <BR><FONT color=#ff0000>BufferedReader</FONT>(<FONT color=#0000ff>Reader</FONT> in, <FONT color=#0000ff>int</FONT> sz) </SPAN></TD></TR></TBODY></TABLE>
<P align=left>这里的sz是指定缓冲区的大小。</P>
<P align=left>它的基本方法：</P>
<TABLE class=code cellSpacing=2 cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD bgColor=#e0e0e0><FONT color=#0000ff><SPAN class=unnamed1>void</SPAN></FONT><SPAN class=unnamed1><FONT color=#000000> close(</FONT>) <FONT color=#006600>//关闭流 </FONT><BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>void </FONT>mark(int readAheadLimit)<FONT color=#006600> //标记当前位置</FONT><BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>boolean</FONT> markSupported() <FONT color=#006600>//是否支持标记</FONT><BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>int</FONT> read() <FONT color=#006600>//继承自Reader的基本方法</FONT><BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>int</FONT> read(<FONT color=#0000ff>char</FONT>[] cbuf, <FONT color=#0000ff>int</FONT> off,<FONT color=#000000> <FONT color=#0000ff>int</FONT></FONT> len) <FONT color=#006600>//继承自Reader的基本方法</FONT><BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>String</FONT> readLine() <FONT color=#006600>//读取一行内容并以字符串形式返回</FONT><BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>boolean</FONT> ready() <FONT color=#006600>//判断流是否已经做好读入的准备</FONT><BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>void</FONT> reset() <FONT color=#006600>//重设到最近的一个标记</FONT><BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>long</FONT> skip(<FONT color=#0000ff>long</FONT> n) <FONT color=#006600>//跳过指定个数的字符读取</FONT></SPAN></TD></TR></TBODY></TABLE>
<P align=left>2. <FONT color=#000000>InputStreamReader</FONT>是<FONT color=#000000>InputStream</FONT>和<FONT color=#000000>Reader</FONT>之间的桥梁，由于System.in是字节流，需要用它来包装之后变为字符流供给 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>BufferedReader</FONT>使用。<BR></P>
<P align=left>3. <FONT color=#0000ff>PrintWriter</FONT> out1 = <FONT color=#0000ff>new</FONT> PrintWriter(<FONT color=#0000ff>new</FONT> BufferedWriter(<FONT color=#0000ff>new</FONT> FileWriter(<FONT color=#ff00ff>"IODemo.out"</FONT>)));</P>
<P align=left>这句话体现了Java输入输出系统的一个特点，为了达到某个目的，需要包装好几层。首先，输出目的地是文件IODemo.out，所以最内层包装的是FileWriter，建立一个输出文件流，接下来，我们希望这个流是缓冲的，所以用<FONT color=#ff0000>BufferedWriter</FONT>来包装它以达到目的，最后，我们需要格式化输出结果，于是将<FONT color=#ff0000>PrintWriter</FONT>包在最外层。<BR></P>
<P align=left>Java提供了这样一个功能，将标准的输入输出流转向，也就是说，我们可以将某个其他的流设为标准输入或输出流，看下面这个例子：</P>
<TABLE class=code cellSpacing=2 cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD bgColor=#e0e0e0>
<P align=left><FONT color=#0000ff><SPAN class=unnamed1>import</SPAN></FONT><SPAN class=unnamed1> java.io.*;</SPAN></P>
<P class=unnamed1 align=left><FONT color=#0000ff>public class</FONT> Redirecting {<BR><FONT color=#0000ff><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public static void </FONT>main(<FONT color=#0000ff>String</FONT>[] args) throws <FONT color=#ff0000>IOException</FONT> {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>PrintStream</FONT> console = System.out;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>BufferedInputStream</FONT> in = <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>BufferedInputStream</FONT>( <FONT color=#0000ff>new</FONT><FONT color=#ff0000> FileInputStream</FONT>( <FONT color=#ff00ff>"Redirecting.java"</FONT>));<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>PrintStream</FONT> out =<FONT color=#0000ff> new</FONT> <FONT color=#ff0000>PrintStream</FONT>( new <FONT color=#ff0000>BufferedOutputStream</FONT>(<FONT color=#0000ff> new FileOutputStream</FONT>(<FONT color=#ff00ff>"test.out"</FONT>)));<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.setIn(in);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.setOut(out);</P>
<P align=left><SPAN class=unnamed1>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>BufferedReader</FONT> br = <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>BufferedReader</FONT>( <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>InputStreamReader</FONT>(System.in));<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>String</FONT> s;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>while</FONT>((s = br.readLine()) !=<FONT color=#0000ff> null</FONT>)<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(s);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;out.close(); <BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.setOut(console);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} <BR>}</SPAN></P></TD></TR></TBODY></TABLE>
<P align=left>在这里java.lang.System的静态方法</P>
<TABLE class=code cellSpacing=2 cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD bgColor=#e0e0e0><FONT color=#0000ff><SPAN class=unnamed1>static void</SPAN></FONT><SPAN class=unnamed1> setIn(InputStream in) <BR><FONT color=#0000ff>static void</FONT> setOut(PrintStream out) </SPAN></TD></TR></TBODY></TABLE>
<P align=left>提供了重新定义标准输入输出流的方法，这样做是很方便的，比如一个程序的结果有很多，有时候甚至要翻页显示，这样不便于观看结果，这是你就可以将标准输出流定义为一个文件流，程序运行完之后打开相应的文件观看结果，就直观了许多。</P>
<P align=left>Java流有着另一个重要的用途，那就是利用对象流对对象进行序列化。下面将开始介绍这方面的问题。</P>
<P align=left>在一个程序运行的时候，其中的变量数据是保存在内存中的，一旦程序结束这些数据将不会被保存，一种解决的办法是将数据写入文件，而Java中提供了一种机制，它可以将程序中的对象写入文件，之后再从文件中把对象读出来重新建立。这就是所谓的对象序列化Java中引入它主要是为了RMI（Remote Method Invocation）和Java Bean所用，不过在平时应用中，它也是很有用的一种技术。</P>
<P align=left>所有需要实现对象序列化的对象必须首先实现Serializable接口。下面看一个例子：</P>
<TABLE class=code cellSpacing=2 cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD bgColor=#e0e0e0>
<P align=left><FONT color=#0000ff><SPAN class=unnamed1>import</SPAN></FONT><SPAN class=unnamed1> java.io.*;<BR><FONT color=#0000ff>import</FONT> java.util.*;</SPAN></P>
<P class=unnamed1 align=left><FONT color=#0000ff>public class</FONT> Logon implements Serializable {<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>private Date</FONT> date = <FONT color=#0000ff>new</FONT> Date();<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>private String</FONT> username;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>private transient String</FONT> password;</P>
<P class=unnamed1 align=left><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Logon(<FONT color=#0000ff>String</FONT> name, <FONT color=#0000ff>String</FONT> pwd) {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;username = name;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;password = pwd;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}</P>
<P class=unnamed1 align=left><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>public String</FONT> toString() {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>String</FONT> pwd = (password == <FONT color=#0000ff>null</FONT>) ? <FONT color=#ff00ff>"(n/a)"</FONT> : password;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return <FONT color=#ff00ff>"logon info: \n "</FONT> + <FONT color=#ff00ff>"username: "</FONT> + username + <FONT color=#ff00ff>"\n date: "</FONT> + date +<FONT color=#ff00ff> "\n password: "</FONT> + pwd;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}</P>
<P class=unnamed1 align=left><FONT color=#0000ff><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public static void</FONT> main(<FONT color=#ff0000>String[]</FONT> args) throws <FONT color=#ff0000>IOException</FONT>, ClassNotFoundException {<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>Logon</FONT> a = <FONT color=#0000ff>new</FONT> Logon(<FONT color=#ff00ff>"Morgan"</FONT>, <FONT color=#ff00ff>"morgan83"</FONT>);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>System</FONT>.out.println( <FONT color=#ff00ff>"logon a = "</FONT> + a);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>ObjectOutputStream</FONT> o = <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>ObjectOutputStream</FONT>( <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>FileOutputStream</FONT>(<FONT color=#ff00ff>"Logon.out"</FONT>));<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;o.writeObject(a);<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;o.close();</P>
<P align=left><SPAN class=unnamed1>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>int</FONT> seconds = 5;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>long</FONT> t = <FONT color=#ff0000>System</FONT>.currentTimeMillis()&nbsp;+ seconds * 1000;<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#0000ff>while</FONT>(<FONT color=#ff0000>System</FONT>.currentTimeMillis() &lt; t) ;<BR><BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>ObjectInputStream</FONT> in = <FONT color=#0000ff>new</FONT><FONT color=#ff0000> ObjectInputStream</FONT>( <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>FileInputStream</FONT>(<FONT color=#ff00ff>"Logon.out"</FONT>));<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>System</FONT>.out.println( <FONT color=#ff00ff>"Recovering object at "</FONT> + <FONT color=#0000ff>new</FONT> Date());<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a = (Logon)in.readObject();<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<FONT color=#ff0000>System</FONT>.out.println(<FONT color=#ff00ff>"logon a = "</FONT> + a); <BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<BR>}</SPAN></P></TD></TR></TBODY></TABLE>
<P align=left>类Logon是一个记录登录信息的类，包括用户名和密码。首先它实现了接口Serializable，这就标志着它可以被序列化。之后再main方法里<FONT color=#ff0000>ObjectOutputStream</FONT> o = <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>ObjectOutputStream</FONT>( <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>FileOutputStream</FONT>(<FONT color=#ff00ff>"Logon.out"</FONT>));新建一个对象输出流包装一个文件流，表示对象序列化的目的地是文件Logon.out。然后用方法<FONT color=#ff0000>writeObject</FONT>开始写入。想要还原的时候也很简单<FONT color=#ff0000>ObjectInputStream </FONT>in = <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>ObjectInputStream</FONT>( <FONT color=#0000ff>new</FONT> <FONT color=#ff0000>FileInputStream</FONT>(<FONT color=#ff00ff>"Logon.out"</FONT>));新建一个对象输入流以文件流Logon.out为参数，之后调用readObject方法就可以了。</P>
<P align=left>需要说明一点，对象序列化有一个神奇之处就是，它建立了一张对象网，将当前要序列化的对象中所持有的引用指向的对象都包含起来一起写入到文件，更为奇妙的是，如果你一次序列化了好几个对象，它们中相同的内容将会被共享写入。这的确是一个非常好的机制。它可以用来实现深层拷贝，有关深层拷贝的问题在<A href="http://www.javaworld.com/javaworld/javaqa/2003-01/02-qa-0124-clone.html">JavaWorld</A>上有一篇文章做了几种实现方法的介绍和比较，有兴趣者可以去看看。</P>
<P align=left>关键字transient在这里表示当前内容将不被序列化，比如例子中的密码，需要保密，所以没有被写入文件。</P>
<P align=left>对Java的输入输出功能，就浅浅的介绍到这里，本文的目的只是开一个好头，希望能让大家对Java输入输出流有个基本的认识，更多更为全面的信息在http://java.sun.com有权威的说明。</P></DIV><img src ="http://www.blogjava.net/nighthawk/aggbug/9218.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/nighthawk/" target="_blank">nighthawk</a> 2005-08-04 14:57 <a href="http://www.blogjava.net/nighthawk/articles/9218.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>