Java教程 - 集合框架


http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 125 第九章 集合框架 教学目标: i掌握集合框架的基本概念 i掌握 Collection 接口 i掌握 Iterator 接口 i掌握 Set 接口 i掌握 List 接口 i掌握 Map 接口 i了解集合的排序 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 126 一:集合框架的基本概念 1:数学背景 在常见用法中,集合(collection)和数学上直观的集(set)的概念是相同的。集是一个唯一 项组,也就是说组中没有重复项。实际上,“集合框架”包含了一个 Set 接口和许多具体的 Set 类。 但正式的集概念却比 Java 技术提前了一个世纪,那时英国数学家 George Boole 按逻辑正式的定 义了集的概念。大部分人在小学时通过我们熟悉的维恩图引入的“集的交”和“集的并”学到过一 些集的理论。 集的基本属性如下: 集内只包含每项的一个实例 集可以是有限的,也可以是无限的 可以定义抽象概念 映射是一种特别的集。它是一种对(pair)集,每个对表示一个元素到另一元素的单向映射。一些 映射示例有: IP 地址到域名(DNS)的映射 关键字到数据库记录的映射 字典(词到含义的映射) 2 进制到 10 进制转换的映射 此外,因为映射也是集,所以它们可以是有限的,也 可以是无限的。无限映射的一个示例如 2 进 制到 10 进制的转换。不幸的是,“集合框架”不支持无限映射 — 有时用数学函数、公式或算法更 好。但在有限映射能解决问题时,“集合框架”会给 Java 程序员提供一个有用的 API。 因为“集合框架”有类 Set、Collection 和 Map 的正规定义,您会注意到小写的词 set、 collection 和 map 把实现和概念区分开来。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 127 2:基本概念 集合是包含多个对象的简单对象,所包含的对象称为元素。集合的典型应用是用来处理多种类 型的对象,这些类型必须有共同的父类。 既然您已经具备了一些集的理论,您应该能够更轻松的理解“集合框架”。 “集合框架”由一 组用来操作对象的接口组成。不同接口描述不同类型的组。在很大程度上,一旦您理解了接口,您 就理解了框架。虽然您总要创建接口特定的实现,但访问实际集合的方法应该限制在接口方法的使 用上;因此,允许您更改基本的数据结构而不必改变其它代码。框架接口层次结构如下图所示。 有的人可能会认为 Map 会继承 Collection。在数学中,映射只是对(pair)的集合。但是, 在“集合框架”中,接口 Map 和 Collection 在层次结构没有任何亲缘关系,它们是截然不同的。 这种差别的原因与 Set 和 Map 在 Java 库中使用的方法有关。Map 的典型应用是访问按关键字存 储的值。它支持一系列集合操作的全部,但 操作的是键-值对,而不是单个独立的元素。因此 Map 需 要支持 get() 和 put() 的基本操作,而 Set 不需要。此外,还有返回 Map 对象的 Set 视图的方 法: Set set = aMap.keySet(); 用“集合框架”设计软件时,记住该框架四个基本接口的下列层次结构关系会有用处: Collection 接口是一组允许重复的对象。 Set 接口继承 Collection,但不允许重复。 List 接口继承 Collection,允许重复,并引入位置下标。 Map 接口既不继承 Set 也不继承 Collection。 让我们转到对框架实现的研究,具体的集合类遵循命名约定,并将基本数据结构和框架接口相 结合。除了四个历史集合类外,Java 框架还引入了六个集合实现,如下表所示。关于历史集合类如 何转换、比如说,如何修改 Hashtable 并结合到框架中,请参阅历史集合类。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 128 接口 实现 历史集合类 Set HashSet TreeSet List ArrayList Vector LinkedList Stack Map HashMap Hashtable TreeMap Properties 这里没有 Collection 接口的实现。历史集合类,之所以这样命名是因为从 Java 类库 1.0 发 行版就开始沿用至今了。 如果从历史集合类转换到新的框架类,主要差异之一在于所有的操作都和新类不同步。您可以 往新类中添加同步的实现,但您不能把它从旧的类中除去。 二:Collection 接口 1:Collection 接口 Collection 接口用于表示任何对象或元素组。想要尽可能以常规方式处理一组元素时,就 使 用 这 一接口。这里是以统一建模语言(Unified Modeling Language(UML))表示法表示的 Collection 公有方法清单。 该接口支持如添加和除去等基本操作。设法除去一个元素时,如果这个元素存在,除去的仅仅 是集合中此元素的一个实例。 boolean add(Object element) boolean remove(Object element) Collection 接口还支持查询操作: int size() boolean isEmpty() boolean contains(Object element) Iterator iterator() http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 129 2:Iterator 接口 Collection 接口的 iterator() 方法返回一个 Iterator。Iterator 和您可能已经熟悉的 Enumeration 接口类似,我们将在 Enumeration 接口中对 Enumeration 进行讨论。使用 Iterator 接口方法,您可以从头至尾遍历集合,并安全的从底层 Collection 中除去元素: remove() 方法可由底层集合有选择的支持。当底层集合调用并支持该方法时,最近一次 next() 调用返回的元素就被除去。为演示这一点,用于常规 Collection 的 Iterator 接口代码如下: Collection collection = ...; Iterator iterator = collection.iterator(); while (iterator.hasNext()) { Object element = iterator.next(); if (removalCheck(element)) { iterator.remove(); } } 3:组操作 Collection 接口支持的其它操作,要么是作用于元素组的任务,要么是同时作用于整个集合的 任务。 boolean containsAll(Collection collection) boolean addAll(Collection collection) void clear() void removeAll(Collection collection) void retainAll(Collection collection) containsAll() 方法允许您查找当前集合是否包含了另一个集合的所有元素,即另一个集合是 否是当前集合的子集。其余方法是可选的,因为特定的集合可能不支持集合更改。 addAll() 方法 确保另一个集合中的所有元素都被添加到当前的集合中,通常称为并。 clear() 方法从当前集合中 除去所有元素。 removeAll() 方法类似于 clear() ,但 只 除去了元素的一个子集。 retainAll() 方 法类似于 removeAll() 方法,不过可能感到它所做的与前面正好相反:它从当前集合中除去不属于 另一个集合的元素,即交。 三:Set 接口 1:Set 接口 按照定义,Set 接口继承 Collection 接口,而且它不允许集合中存在重复项。所有原始方法 都是现成的,没有引入新方法。具体的 Set 实现类依赖添加的对象的 equals() 方法来检查等同性。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 130 2:HashSet 类和 TreeSet 类 “集合框架”支持 Set 接口两种普通的实现:HashSet 和 TreeSet。在更多情况下,您会使用 HashSet 存储重复自由的集合。考虑到效率,添加到 HashSet 的对象需要采用恰当分配散列码的方 式来实现 hashCode() 方法。虽然大多数系统类覆盖了 Object 中缺省的 hashCode() 实现,但创 建您自己的要添加到 HashSet 的类时,别忘了覆盖 hashCode()。当您要从集合中以有序的方式抽 取元素时,TreeSet 实现会有用处。为了能顺利进行,添加到 TreeSet 的元素必须是可排序的。 “集合框架”添加对 Comparable 元素的支持,在 排 序 的“ 可 比 较 的接口”部分中会详细介绍。 我们暂且假定一棵树知道如何保持 java.lang 包装程序器类元素的有序状态。一 般说来,先把元素 添加到 HashSet,再把集合转换为 TreeSet 来进行有序遍历会更快。 为优化 HashSet 空间的使用,您可以调优初始容量和负载因子。TreeSet 不包含调优选项,因 为树总是平衡的,保证了插入、删除、查询的性能为 log(n)。 HashSet 和 TreeSet 都实现 Cloneable 接口。 3:集的使用示例 为演示具体 Set 类的使用,下面的程序创建了一个 HashSet,并往里添加了一组名字,其中有 个名字添加了两次。接着,程序把集中名字的列表打印出来,演示了重复的名字没有出现。接着, 程序把集作为 TreeSet 来处理,并显示有序的列表。 import java.util.*; public class SetExample { public static void main(String args[]) { Set set = new HashSet(); set.add("Bernadine"); set.add("Elizabeth"); set.add("Gene"); set.add("Elizabeth"); set.add("Clara"); System.out.println(set); Set sortedSet = new TreeSet(set); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 131 System.out.println(sortedSet); } } 运行程序产生了以下输出。请注意重复的条目只出现了一次,列 表 的第二次输出已按字母顺序排序。 [Gene, Clara, Bernadine, Elizabeth] [Bernadine, Clara, Elizabeth, Gene] 四:List 接口 1:List 接口 List 接口继承了 Collection 接口以定义一个允许重复项的有序集合。该接口不但能够对列表 的一部分进行处理,还添加了面向位置的操作。 面向位置的操作包括插入某个元素或 Collection 的功能,还包括获取、除去或更改元素的功 能。在 List 中搜索元素可以从列表的头部或尾部开始,如果找到元素,还将报告元素所在的位置。 void add(int index, Object element) boolean addAll(int index, Collection collection) Object get(int index) int indexOf(Object element) int lastIndexOf(Object element) Object remove(int index) Object set(int index, Object element) http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 132 List 接口不但以位置友好的方式遍历整个列表,还能处理集合的子集: ListIterator listIterator() ListIterator listIterator(int startIndex) List subList(int fromIndex, int toIndex) 处理 subList() 时,位于 fromIndex 的元素在子列表中,而位于 toIndex 的元素则不是,提醒这 一点很重要。以下 for-loop 测试案例大致反映了这一点: for (int i=fromIndex; i(){…}。此时,匿名类必须实现 接口中所有的抽象方法。除此之外,匿名类具备内部类的全部特性,比如可以根据需要添加新的属 性和方法,或访问其封装类的成员。 示例 使用匿名内部类实现接口 程序 1:TestAnonymous2.java import java.awt.*; import java.awt.event.*; public class TestAnonymous2 { Frame f = new Frame("Test"); TextField tf = new TextField(10); Button b1 = new Button("Start"); public TestAnonymous2(){ f.add(b1,"North"); f.add(tf,"South"); b1.addActionListener(new ActionListener(){ http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 198 private int i; public void actionPerformed(ActionEvent e) { tf.setText(e.getActionCommand() + ++i); } }); f.pack(); f.setVisible(true); } public static void main(String args[]) { new TestAnonymous2(); } } 程序中的匿名内部类实现了 ActionListener 接口,其中添加了新的属性 i,用做计数器。需要注意 的是,匿名内部类无法同时实现多个接口。 练习实践课: l Swing 基础 l 事件处理 程序 1 鼠标移动 需求: 1、 不论鼠标何时移动都围绕它画一个小圆; 2、 每当按下鼠标时,屏幕显示文字。 目标: 1、 鼠标移动事件; 2、 绘图语句; 3、 色彩处理。 程序: //: BangBean.java package com.useful.java.part4; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; public class BangBean extends Canvas implements Serializable { protected int xm, ym; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 199 protected int cSize = 20; // Circle size protected String text = "Bang!"; protected int fontSize = 48; protected Color tColor = Color.red; protected ActionListener actionListener; public BangBean() { addMouseListener(new ML()); addMouseMotionListener(new MML()); } public int getCircleSize() { return cSize; } public void setCircleSize(int newSize) { cSize = newSize; } public String getBangText() { return text; } public void setBangText(String newText) { text = newText; } public int getFontSize() { return fontSize; } public void setFontSize(int newSize) { fontSize = newSize; } public Color getTextColor() { return tColor; } public void setTextColor(Color newColor) { tColor = newColor; } public void paint(Graphics g) { g.setColor(Color.black); g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize); } // This is a unicast listener, which is // the simplest form of listener management: public void addActionListener ( ActionListener l) throws TooManyListenersException { if(actionListener != null) throw new TooManyListenersException(); actionListener = l; } public void removeActionListener( ActionListener l) { actionListener = null; } class ML extends MouseAdapter { http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 200 public void mousePressed(MouseEvent e) { Graphics g = getGraphics(); g.setColor(tColor); g.setFont( new Font( "TimesRoman", Font.BOLD, fontSize)); int width = g.getFontMetrics().stringWidth(text); g.drawString(text, (getSize().width - width) /2, getSize().height/2); g.dispose(); // Call the listener's method: if(actionListener != null) actionListener.actionPerformed( new ActionEvent(BangBean.this, ActionEvent.ACTION_PERFORMED, null)); } } class MML extends MouseMotionAdapter { public void mouseMoved(MouseEvent e) { xm = e.getX(); ym = e.getY(); repaint(); } } public Dimension getPreferredSize() { return new Dimension(200, 200); } // Testing the BangBean: public static void main(String[] args) { BangBean bb = new BangBean(); try { bb.addActionListener(new BBL()); } catch(TooManyListenersException e) {} Frame aFrame = new Frame("BangBean Test"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(bb, BorderLayout.CENTER); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 201 aFrame.setSize(300,300); aFrame.setVisible(true); } // During testing, send action information // to the console: static class BBL implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("BangBean action"); } } } 说明: 1、 你可以尝试改变一下此程序,如圆的特征,色彩等; 2、 BangBean 同样拥有它自己的 addActionListener()和 removeActionListener()方法,因此我 们可以附上自己的当用户单击在 BangBean 上时会被激活的接收器。这样,我们将能够 确认可支持的属性和事件,最重要的是我们会注意到 BangBean 执行了这种串联化的接 口。这意味着应用程序构建工具可以在程序设计者调整完属性值后利用串联为 BangBean 贮藏所有的信息。当 Bean 作为运行的应用程序的一部分被创建时,那 些 被 贮 藏 的 属性被重新恢复,因此我们可以正确地得到我们的设计。 3、 程序运行如下: 作业 1、增加一个复选框到上一章练习 1 创建的程序中,捕捉事件,并插入不同的文字到文字字段中。 2、定义一个 Frame,要有菜单,然后在菜单上面有一个业务操作的项,下面有增加、查询的小 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 202 项,点击增加的时候出现新增界面,点击查询的时候出现查询界面,实现界面的切换。 第十二章 Applet 教学目标: i理解 Applet 基本概念 i掌握 Applet 的生命周期和方法 i掌握 Awt 的绘图模型 i掌握 Applet 的插件标记 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 203 一:Applet 的基本概念 Applet 是能够嵌入到一个 HTML 页面中,且可通过 Web 浏览器下载和执行的一种 Java 类。它是 Java 技术容器(container)的一种特定类型,其执行方式不同于应用程序。一个应用程序是从它的 main()方法被调用开始的,而一个 Applet 的生命周期在一定程度上则要复杂得多。本模块分析了 Applet 如何运行,如何被装载到浏览器中,以及它是如何编写的。 1:认识 Applet 类似于本书第一个的应用程序举例 HelloWorld,让我们先来感受一下 Java 小程序: 程序:HelloWorld.java import java.awt.Graphics; import java.applet.Applet; public class HelloWorld extends Applet { String text ; public void init () { 什么是 Applet? l 能嵌入到一个 HTML 页面中且可通过 Web 浏览器下载 和执行的一种 Java 类 l 可以以下方式装载: l 浏览器装载 URL l 浏览器装载 HTML 文档 l 浏览器装载 Applet 类 l 浏览器运行 Applet http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 204 text = "Hello World"; } public void paint(Graphics g) { g.drawString (text , 25, 25) ; } } 由于 Applet 在 Web 浏览器环境中运行,所以它并不直接由键入的一个命令启动。要运行此 Java 小程序,必须创建一个相应的 HTML 文件来告诉浏览器需装载什么以及如何运行它。例如可以创建如 下的 html 文件: 文件:test.html 将程序 HelloWorld.java 编译生成的字节码文件 HelloWorld.class 保存在 test.html 所在路径下, 用 IE 浏览器打开文件 test.html,Applet 程序即在浏览器中运行,如图 12-1 所示: 图 12-1 test.html 的显示效果 也可以使用 JDK 自带的 Applet 浏览器 appletviewer 运行上述 Java 小程序,在命令行中运行 下述命令:D:\ex\1401\>appletviewer test.html 运行效果如图 12-2 所示: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 205 其中,使用浏览器打开 test.html 文件时执行了下述操作: 浏览器获取要访问的 html 文件的 URL 浏览器装入 html 文件 浏览器载入 Applet 的类字节代码 启动 Java 虚拟机执行 Applet。 2: 装入 Applet 由于 Applet 在 Web 浏览器环境中运行,所以它并不直接由键入的一个命令启动。你必须要创建 一个 HTML 文件来告诉浏览器需装载什么以及如何运行它。 浏览器装入 URL 浏览器装入 HTML 文档 浏览器装入 Applet 类 浏览器运行 Applet 3: Applet 的安全限制 Applet 的安全限制 l 多数浏览器禁止以下操作: l 运行时执行另一程序 l 任何文件的输入/输出 图 12-2 test.html 的显示效果 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 206 由于通过网络装载,Applet 的代码具有一种内在的危险性。如果有人编写了一个恶意的类来读 取你的密码文件,并把它通过 Internet 传送,会产生怎样的后果呢? 所能够控制的安全程度是在浏览器层次上实现的。大多数浏览器(包括 Netscape Nevigator) 缺省地禁止以下操作: 运行时执行另一程序 任何文件的输入/输出 调用任何本地方法 尝试打开除提供 Applet 的主机之外的任何系统的 Socket 这些限制的关键在于,通过限制 Applet 对系统文件的存取来阻止它侵犯一个远程系统的隐私或 破坏该系统。禁止执行另一程序和不允许调用本地方法限制了 Applet 启动未经 JVM 检查的代码。对 Socket 的限制则禁止了与另一个可能有危害性的程序的通信。 JDK1.2 提供了一种方式,它指定了一个特殊的“保护域”或一个特殊 Applet 运行的安全性环 境。远程系统检查原始的 URL 以及它下载的 Applet 的签名,和一个含有从特殊的 Applet 到特殊保 护域的映射入口的本地文件进行比较。因此,来自特别位置的特殊 Applet 具有一些运行特权。 二:Applet 的生命周期和方法 Applet 的生命周期比所讨论的要稍微复杂一些。与其生命周期相关的有三个主要方法:init(), start()和 stop()。 Applet 类还提供了小应用程序及其运行环境间的标准接口,其中几个重要方法如下: init()方法: 方法格式:public void init(); 在 Applet 创建时被 IE 浏览器或 appletviewer 内嵌的 Java 虚拟机自动调用,用来通知 Applet 已经被载入到系统中。所谓创建 Applet 是指支持 Java 的浏览器在执行 applet 时,会首先自动创建 一个该类的对象,如示例 12-1 中的 HelloWorld 类型对象。 Applet 的方法和 Applet 的生命周期 l init() l 在 Applet 创建时被调用 l 可用于初始化数据值 l start() l 当 Applet 成为可见时运行 l stop() l 当 Applet 成为不可见时运行 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 207 Applet 类中的 init()方法不执行任何操作,可以在 Applet 的子类中通过重写此方法使 Applet 完成的初始化工作,例如示例 12-1 中对属性 text 赋初值“HelloWorld”。 start()方法 本方法用于通知 Applet 可以开始执行。浏览器在调用 init()方法后,会接着调用 start()方法; 以后每次 Applet 被激活时,都会调用 start()方法。 Applet 类中的 start()方法不执行任何操作,可以在 Applet 的子类中通过重写此方法使 Applet 每次被访问时执行特定的操作,例如示例 12-3 中的重新开始音乐播放。 stop()方法 本方法也是由浏览器调用,用来通知 Applet 停止执行。其被调用时机与 start()方法相反,每 次 Applet 由活动状态变为不活动状态时,例如用户离开 Web 主页或执行 destroy()方法前,都会自 动调用 stop()方法。 Applet 类中的 stop()方法不执行任何操作,可以在 Applet 的子类中通过重写此方法使 Applet 每次停止时执行特定的操作,例如示例 12-3 中的停止音乐播放。 destroy()方法 Applet 对象销毁时由浏览器虚拟机自动调用的方法,用来完成所有占用资源的释放。在调用本 方法前通常先调用 stop()方法。 Applet 类中的 destroy()方法不执行任何操作,可以在 Applet 的子类中通过重写此方法使 Applet 在被回收前执行特定的操作。例如使用了线程的 Applet,可以通过调用 init()方法创建线 程,再通过调用 destroy()方法杀死线程。 在示例 12-1 中还用到了方法 paint(),此方法并非 Applet 所特有,而是 Component 类中定义 的,在重画 AWT 组件时自动调用该组件的 paint()方法。可以在子类中重写此方法以实现在 AWT 组 件上绘图的功能。有关 AWT 绘图的内容见第 12.3 节。 和线程的情况类似,Applet 在其整个生命周期中处于四种不同的状态:初始态、运行态、停止 态和消亡态。其状态转换情况如图 12-3 所示: 图 12-3 Applet 生命周期 三:AWT 的绘图模型 除了基本的生命周期外,Applet 还有与其显示有关的一些重要的方法。这些方法的声明和文档 在 AWT 组件类中。使用 AWT 做显示处理时遵循正确的模型是非常重要的。 更新显示由一种被称为 AWT 线程的独立的线程来完成。这个线程可用来处理与显示更新相关的 两种情况。 第一种情况是显露(exposure),它或在首次显示时,或在部分显示已被破坏而必须刷新时出现。 初始态 停止态 消亡态 运行态 首次启动 构造->init() start() 从活动转入非活动状态 从非活动转入活动状态 start() stop() 关 闭 浏 览 器 destroy() http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 208 显示的破坏可能发生在任何时刻,因此,你的程序必须能在任意时刻更新显示。 第二种情况是在程序重画带有新内容的画面时。这种重画可能会要求首先擦除原来的图像。 1: Paint(Graphics g)方法 显露处理自动地发生,且导致对 paint()方法的一次调用。一种 Graphics 类的被称为裁剪矩形 的设备常用于对 paint()方法进行优化。除非必要,更新不会完全覆盖整个图形区域,而是严格限 制在被破坏的范围内。 2: repaint()方法 对 repaint()的调用可通知系统:你想改变显示,于是系统将调用 paint()。 3: update(Graphics g)方法 repaint()实际上产生了一个调用另一方法 update()的 AWT 线程。update 方法通常清除当前的 显示并调用 paint()。update()方法可以被修改,如:为了减少闪烁可不清除显示而直接调用 paint()。 4: 方法的交互 下面的框图描述了 paint(),update()和 repaint()方法间的内在关系。 5: Applet 的显示策略 Applet 的显示策略 l 维护一个显示模型 l 使 paint()提供仅仅基于这个模型的显示 l 更新这个模型并调用 repaint()来改变显示 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 209 Applet 模型要求你采取一种特定的策略来维护你的显示: 维护一个显示模型。这个模型是对为再次提供显示而所需做的事情的一个定义。关于如何去做 的指令在 paint()方法中被具体化;这些指令所用的数据通常是全局成员变量。 使 paint()提供仅仅基于该模型的显示。这使得无论 paint()何时被调用,它都能以一致的方法 再生该显示,并正确地处理显露问题。 使得程序对显示的改变,通过更新该模型而调用 repaint()方法来进行,以使 update()方法(最 终是 paint()方法)被 AWT 线程调用。 注-一个单一 AWT 线程处理所有的绘图组件和输入事件的分发。应保持 paint()和 update()的简单 性,以避免它们使 AWT 线程发生故障的可能性更大;在极端情况下,你将需要其他线程的帮助以达 到这一目的。 6:示例 让我们看一个 AWT 绘图的例子: 程序:AWTDrawing.java import java.awt.*; class SubPanel extends Panel{ public void paint(Graphics g){ g.drawString("this is a drawing test!",20,20); g.drawLine(30,60,100,120); g.draw3DRect(60,50,70,30,false); } } public class AWTDrawing { private Frame f = new Frame(" Hello Out There!"); private SubPanel p = new SubPanel(); public void launchFrame() { f.add(p); f.setSize(170,170); f.setBackground( new Color(189,245,245)); f.setVisible( true); } public static void main( String args[]) { AWTDrawing guiWindow = new AWTDrawing(); guiWindow.launchFrame(); } } 程序运行结果如图 12-4 所示。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 210 示例 12-5 Applet 绘图 程序:TestFont.java import java.awt.*; import java.applet.Applet; public class FontTest extends Applet { String fontList[]; public void init () { fontList = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); } public void paint(Graphics g) { Dimension d = this.getSize(); int x = d.width/4; int y = d.height/fontList.length+16; for(int i=0;i 程序运行结果如图 12-5 所示: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 211 图 12-5 Applet 绘图效果 Graphics 类中提供了绘制直线、折线、矩形、弧、椭圆、多边形、文本、图像等多种绘图方法, 还可以绘制无边线的填充矩形、弧、椭圆和多边形,可参照 JDK 文档使用。 四:Applet 的插件标记 在 HTML 文件中嵌入 Java Applet 时,可以采用插件标记的形式给出 Applet 属性或参数信息, 这些信息可分别用于下述用途: 帮助浏览定位 Applet 的代码 为不支持 Java 的浏览器提供说明性信息 从 html 页面向 Applet 中传递参数 控制 Applet 的显示效果,如尺寸、对齐方式等 Applet 插件标记的语法格式如下: [param name = var1 value = value1] [param name = var2 value = value2] http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 212 …… 各种插件标记的功能为: archive 可选属性,指明相关的 Java 档案文件(JAR, Java Archive),该档案文件中包含了当前 Applet 的类文件和其他资源文件。使用 JAR 文件可以提高 Applet 的下载效率,但不是必须的。 code 必需属性,给出 Applet 类文件(.class)名称。可以使用相对路径或 URL,但不能使用绝对路 径名。该文件的定位与所在的 html 页面保存位置有关,可能位于本地计算机,也可能位于网络上的 其他计算机。 width/height 必需属性,用于设定 Applet 显示区域的初始宽度/高度,单位为像素。 codebase 可选属性,给出 Applet 的代码基址,即包含当前 Applet 类文件的目录,可以与当前 html 文件 的 URL 地址不同。如缺省此属性,则默认为当前 html 文件的 URL 地址。 alt 不支持 Java 的浏览器中打开含有本 Applet 的 html 页面时,会显示此说明信息。 name 可选属性,为 Applet 实例命名,以实现同一 html 页面中的多个 Applet 间的识别和相互通信。 align 可选属性,设定 Applet 显示区域在 html 页面中的对齐方式,类似于图片(IMG)的布局方式, 可取属性值为:left, right, texttop, middle, absmiddle, baseline, bottom 及 absbottom。 vspace/hspace 可选属性,设定 Applet 显示区域上下/左右流出的空间大小,单位为像素。 param 可选标记,为 Applet 提供外部参数,在 Applet 中可以通过 getParameter()方法获取这些参数 的值。以实现从 html 页面到 Applet 程序的参数传递功能。 示例 12-2 使用 codebase 属性 程序:HelloWorld.java (同示例 12-1 中的 HelloWorld.java) 文件:hello.html 在此示例中,Applet 类文件存于网络上(发布目录 http://127.0.0.1:8080/javabook/1402/) http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 213 而 html 文件可保存于任何位置,如本机 E:\ex\hello.html,由于在 hello.html 中使用了插件标记 codebase 指明 Applet 类文件的确切位置,因此仍能成功定位该 Applet,其显示效果同示例 12-1。 示例 12-3 Applet 参数传递 程序:Test.java import java.awt.Graphics; import java.applet.Applet; public class Test extends Applet { String topic = "abcdefg" ; public void init () { topic = getParameter("topic"); } public void paint(Graphics g) { g.drawString (topic,25, 25) ; } } 文件:test1403.html 在 IE 浏览器中打开文件 test1403 时,显示效果如图 12-3 所示: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 214 图 12-3 页面 test1403.html 显示效果 文件 test1403.html 中,语句 codebase="."显示指明了 Applet 代码基址为当前 html 页面所在 的目录。使用 Applet 类中定义的 getParameter(string name)方法获取由 html 页面传递来的参数 topic 的值并显示。 五:多媒体支持 Applet 可以提供多媒体处理,即图像、动画、声音的显示或播放功能。正因如此,Java Applet 才得以在 Internet 上广为流行,嵌入 Applet 小程序的网页才变得丰富多彩。通常,有关的多媒体 资源文件以 URL 的形式给出。 1: 图像处理 目前的 JDK 版本支持的图像格式有 gif 和 jpeg。图像处理包括图像的获取和显示两个主要环节, 由于图像所涉及的信息量较大,在 Applet 中要显示图像,必须首先将其下载。 1.1:获取图像文件: Applet 类的 getImage()方法提供获取图像文件的功能,该方法可以获取并返回可以被显示 在屏幕上的图形对象。其中,java.awt.Image 类是表示各种图像的类的父类,本方法实际返回 的是其子类对象。getImage()方法格式如下: Image getImage(URL url); 其中,参数 url 必须是一个 URL 绝对地址。 Image getImage(URL url, String name); 其中,参数 url 是图像的 URL 基地址,而 name 则表示图像的相对地址,两者组合在一 起构成完整的 URL 地址。 如果不能确定图像文件的绝对 URL 地址,可以采取下述方法: 当图像文件与嵌入了 Applet 的 html 文档保存在同一位置时,可以使用 Applet 类的 getDocumentBase()方法获取该 URL 基地址; 当图像文件与 Applet 的字节码文件保存在同一位置时,可以使用 Applet 类的 getCodeBase() 方法获取该 URL 基地址。然后以之作为参数,调用 getImage()方法。 严格的说,调用 getImage()时并未立即下载图像,无论指定的图像是否存在,本方法通常立即 返回,这与 java.io.File 类的性质相似。只有当 Applet 试图显示该图像时,数据才被下载。 1.2:显示图像: java.awt.Graphics 类的 drawImage()方法提供显示图像的功能。 boolean drawImage(Image img, int x, int y, ImageObserver observer); 功能:在指定区域内绘制参数 img 指定的图像,该图像的左上角坐标为(x,y)。如果 绘制操作无法完成,则返回 false。其中的参数 observer 为 ImageObserver 接口类型对象, 它作为图像监听器,接 收在 图 像构 造 过程中通知给它的有关图像信息(如图像的尺寸缩放、 转换信息等)。 boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer); 功能与前者类似,但透明像素在绘制时采用参数 bgcolor 指定的背景颜色。此操作相 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 215 当于按照指定的图像的宽度、高度和颜色填充一个矩形,再将图像绘制在该矩形上,只是 效率更高一些。 boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer); 说明:所绘制的图像不会自动按比例调整自身大小以填充满目标区域 boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer); 示例 12-6 在 Applet 中显示图像 程序:ImageViewer.java import java.applet.Applet; import java.awt.*; public class ImageViewer extends Applet{ Image img; public void init() { img = getImage(getDocumentBase(),"t3.gif"); setBackground(Color.white); } public void paint(Graphics g) { g.drawImage(img,0,0,200,150,this); } } 文件:PictureSee.html 程序运行效果如图 12-9 所示。从示例 12-5 和 12-6 中可以看出,Applet 显示文本和图像都是 以绘图的方式实现的,这是为了保持程序风格的一直性,简化编程难度。 对于本程序稍加改造——采用循环的方式交替显示多幅图片,并使用线程休眠的方式实现时间 延迟,控制每幅图片的显示时间,就得到了一个简单的网络影集。见示例 12-7: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 216 图 12-6 页面 PictureSee.html 显示效果 示例 12-7 一个简单的 Applet 影集 程序:ImageTape.java import java.applet.Applet; import java.awt.*; public class ImageTape extends Applet{ int num = 5; Image imgs[]; public void init() { imgs = new Image[num]; for(int i = 0; i < num; i++){ imgs[i] = getImage(getDocumentBase(), "images/" +"t" + (i+1)+ ".gif"); } this.setBackground(Color.white); } public void paint(Graphics g){ while(true){ for(int i=0;i 程序运行界面与示例 12-6 相同,每两秒钟更换一次图片内容,并循环显示。程序中用到了 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 217 Graphics 类的 clearRect()方法,其功能是用背景色填充指定矩形区域,以达到清楚该矩形区域的 效果。 2: 声音处理 目前的 JDK 版本支持的音频文件格式有 au, aif, mid, wav, rfm 等。对于音频文件,可以和图 像文件类似,使用 URL 进行定位,但音频文件数据量较小,无须事先下载就可直接播放。在 Applet 中播放声音文件有两种不同的方式: 利用 Applet 类的 play()方法直接播放。play()方法格式为: public void play (URL url); public void play (URL url, String name); play()方法的功能是,根据参数提供的 URL 地址播放声音片段。如果无法找到该声音 片段,则本方法将不做任何事情。 示例 12-8 在 Applet 中播放音乐 程序:MyMusic.java import java.awt.*; import java.applet.Applet; public class MyMusic extends Applet{ public void paint(Graphics g){ g.drawString("请戴上耳机!",20,20); g.drawString("Enjoy it!",20,40); } public void start(){ play(getDocumentBase(),"passport.mid"); } } 文件:test.html 声音文件:passport.mid 程序运行界面如图 12-7 所示。 图 12-7 test.html 显示效果 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 218 使用 AudioClip 接口: AudioClip 接口中定义了常用的声音文件处理方法: public void play(); 开始播放一个声音文件,本方法每次调用,都从头开始重新播放。 public void loop() 循环播放当前声音文件 public void stop() 停止播放当前声音文件 可以使用 Applet 类的 getAudioClip()方法或 newAudioClip()方法获取 AudioClip 类型的对象, 后者为 static 方法,例如: AudioClip s1 = this.getAudioClip(aUrl,"music.au"); AudioClip s2 = Applet.newAudioClip(aUrl,"music.au"); 示例 12-9 在 Applet 中播放音乐 程序:ColorMusic.java import java.awt.*; import java.applet.Applet; import java.applet.AudioClip; public class ColorMusic extends Applet{ AudioClip sound; public void init(){ sound = getAudioClip(getDocumentBase(),"pt.mid"); } public void paint(Graphics g){ int x0,y0,r; x0 = 50; y0 = 50; for(r=51;r>=1;r=r-5){ g.setColor(new Color((r*20+100)%250, Math.abs(400-r*15)%250,(30+r*5)%255)); g.drawRect(x0-r,y0-r,x0+2*r,y0+2*r); g.fillRect(x0-r,y0-r,x0+2*r,y0+2*r); } } public void start(){ sound.loop(); } public void stop() { sound.stop(); } } 文件:ColorMusic.html http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 219 文件 ColorMusic.html 的显示界面如图 12-8 所示,显示此页面的同时播放音频文件 pt.mid。 当浏览器转到其他页面时,停止播放;再后退到本页面时,重新从头播放。 读者可在本示例基础上进行扩充,加入更多的控制组件和控制逻辑,实现一个简单的声音文件 播放器。 此外,对于本地文件系统的文件也可以 URL 的方式进行定位,进而采用 AudioClip 类的有关方 法进行处理。在 Java Application 中也可以使用类似的方式获取本地音频文件再进行处理。 3: Application 的多媒体支持 在 Java Application 中处理声音文件的方式与 Applet 完全相同,只是 Application 中不一定 用到 Applet 对象,可使用 static 方法 Applet.newAudioClip()直接获取 AudioClip 对象。 示例 12-10 在 Application 中播放音乐 程序:Test.java import java.net.URL; import java.applet.Applet; import java.applet.AudioClip; public class Test { public static void main(String[] arg){ URL u1 = null; try{ u1 = new URL( "file:///D://m1/sp.au"); }catch(java.net.MalformedURLException e){ e.printStackTrace(); } AudioClip sound = Applet.newAudioClip(u1); 图 12-8 ColorMusic.html 显示 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 220 sound.loop(); } } 程序运行时会循环播放指定的声音文件,直至退出。还可以采用下述便于记忆的方式获取本地 文件的 URL: File f = new File("D:\\m1\\sp.au"); URL u1 = f.toURL(); 在 Application 中处理图像时,需使用 Toolkit 类提供的 getImage()方法来获取 Image 对象, 而显示图像的操作与 Applet 完全相同。Toolkit 类的 getImage()方法有两种格式: public abstract Image getImage(String filename); public abstract Image getImage(URL url); 允许以 String 类型文件名的方式指定声音文件,也可以 URL 的方式指定。支持的声音文件类型 有 GIF、JPEG、PNG。 Toolkit 是一个抽象类,可以通过两种方法获取 Toolkit 对象: 调用 Toolkit 类的 static 方法 getDefaultToolkit()。 例如:Image img = Toolkit.getDefaultToolkit().getImage("m1.gif"); 调用 AWT 组件对象的实例方法 getToolkit(): 例如:Image img = this.getToolkit().getImage("m1.gif"); 示例 12-11 在 Application 中显示图片 程序:Test.java import java.awt.*; import java.awt.event.*; import java.awt.image.*; public class Test { public static void main(String[] arg){ Frame f = new Frame("My Picture"); Image im = Toolkit.getDefaultToolkit().getImage("m1.gif"); SubPanel p = new SubPanel(im); f.add(p); f.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent e){ System.exit(1); } }); f.setSize(200,180); f.setVisible(true); } } class SubPanel extends Panel{ Image img; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 221 public SubPanel(Image img){ this.img = img; } public void paint(Graphics g){ g.drawImage(img,0,0, (int)this.getBounds().getWidth(), (int)this.getBounds().getHeight(),this); } } 图片文件 m1.gif 保存在 Test.class 所在路径下,程序运行结果如图 12-9 所示: 练习实践课: 本章的内容为 Applet,实践重点: l Applet 开发及嵌入网页 程序 1: Applet 按钮与文本框 需求:写一个 Applet,需要按钮与文本框,每按一次,文本框内容改变。 目标: 1、 Applet 基础; 2、 Applet 与普通 GUI 应用程序的区别; 3、 Applet 的事件处理。 程序:(如下) //: TextField1.java package com.useful.java.part4; import java.awt.*; import java.applet.*; public class TextField1 extends Applet { Button b1 = new Button("Get Text"), b2 = new Button("Set Text"); 图 12-9 程序Test.java 显示 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 222 TextField t = new TextField("Starting text", 30); String s = new String(); public void init() { add(b1); add(b2); add(t); } public boolean action (Event evt, Object arg) { if(evt.target.equals(b1)) { getAppletContext().showStatus(t.getText()); s = t.getSelectedText(); if(s.length() == 0) s = t.getText(); t.setEditable(true); } else if(evt.target.equals(b2)) { t.setText("Inserted by Button 2: " + s); t.setEditable(false); } // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } } 说明: 1、 Applet 运行方式:在 d:\java 下建立一个 HTML 文件 applet.html,文件内容如下: applet
2、 Applet 与应用程序的最根本区别在于,Applet 需要继承 Applet; 3、 页面布局与事件处理与应用程序相同; 4、 Applet 不能操作本地数据; 5、 Applet 运行如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 223 作业 1:什么是 Applet?它和普通 Java 应用程序有什么区别? 2:简述 Applet 的生命周期 3:编写一个 Applet 在屏幕上画椭圆,椭圆的大小和位置由鼠标决定(按下鼠标左键,拖动画 出矩形,然后在此矩形内部画椭圆) 第十三章 I/O 流 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 224 一:Java 的 io 包 1:流的基本知识 一个流是字节的源或目的。次序是有意义的。例如,一个需要键盘输入的程序可以用流来做到 这一点。 两种基本的流是:输入流和输出流。你可以从输入流读,但你不能对它写。要从输入流读取字 节,必须有一个与这个流相关联的字符源。 在 java.io 包中,有一些流是结点流,即它们可以从一个特定的地方读写,例如磁盘或者一块 内存。其他流称作过滤器。一个过滤器输入流是用一个到已存在的输入流的连接创建的。此后,当 你试图从过滤输入流对象读时,它向你提供来自另一个输入流对象的字符。 教学目标: iI/O 包简介 i掌握文件和过滤器 i掌握 Reader 和 Writer i掌握二进制流的读写 i掌握文本流的读写 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 225 2:InputStream 与 OutputStream 可将 Java 库的 IO 类分割为输入与输出两个部分,这一点在用 Web 浏览器阅读联机 Java 类文档 时便可知道。通过继承,从 InputStream(输入流)衍生的所有类都拥有名为 read()的基本方法, 用于读取单个字节或者字节数组。类 似 地 ,从 OutputStream 衍生的所有类都拥有基本方法 write(), 用于写入单个字节或者字节数组。然而,我们通常不会用到这些方法;它们之所以存在,是因为更 复杂的类可以利用它们,以便提供一个更有用的接口。因此,我们很少用单个类创建自己的系统对 象。一般情况下,我们都是将多个对象重叠在一起,提供自己期望的功能。我们之所以感到 Java 的 流库(Stream Library)异常复杂,正是由于为了创建单独一个结果流,却需要创建多个对象的缘 故。 很有必要按照功能对类进行分类。库的设计者首先决定与输入有关的所有类都从 InputStream 继承,而与输出有关的所有类都从 OutputStream 继承。 2.1:InputStream InputStream 的作用是标志那些从不同起源地产生输入的类。这些起源地包括(每个都有一个 相关的 InputStream 子类): (1) 字节数组 (2) String 对象 (3) 文件 (4) “管道”, 它 的 工 作原理与现实生活中的管道类似:将一些东西置入一端,它们在另一端出 来。 (5) 一系列其他流,以便我们将其统一收集到单独一个流内。 (6) 其他起源地,如 Internet 连接等(将在本书后面的部分讲述)。 除此以外,FilterInputStream 也属于 InputStream 的一种类型,用它可为“破坏器”类提供 一个基础类,以便将属性或者有用的接口同输入流连接到一起。这将在以后讨论。 ByteArrayInputStream 允许内存中的一个缓冲区作为 InputStream 使用 从中提取字节的缓冲 区作为一个数据源使用。通过将其同一个 FilterInputStream 对象连接,可提供一个有用的接口。 StringBufferInputStream 将一个 String 转换成 InputStream 一个 String(字串)。基础的实施方 案 实际采用一个 StringBuffer (字串缓冲)作为一个数据源使用。通过将其同一个 FilterInputStream 对象连接,可提供一个有用的接口。 FileInputStream 用 于从文件读取信息 代表文件名的一个 String , 或 者 一个 File 或 FileDescriptor 对象作为一个数据源使用。通过将其同一个 FilterInputStream 对象连接,可提供 一个有用的接口。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 226 PipedInputString 产生为相关的 PipedOutputStream 写的数据。实现了“管道化”的概念。 PipedOutputStream 作为一个数据源使用。通过将其同一个 FilterInputStream 对象连接,可提供 一个有用的接口。 SequenceInputStream 将两个或更多的 InputStream 对象转换成单个 InputStream 使用 两个 InputStream 对象或者一个 Enumeration,用 于 InputStream 对象的一个容器/作为一个数据源使用。 通过将其同一个 FilterInputStream 对象连接,可提供一个有用的接口 FilterInputStream 对作为破坏器接口使用的类进行抽象;那个破坏器为其他 InputStream 类 提供了有用的功能。 2.1.1:InputStream 方法 三个 read 方法 int read() int read(byte []) int read(byte[], int ,int ) 这三个方法提供对输入管道数据的存取。简单读方法返回一个 int 值,它包含从流里读出 的一个字节或者-1,其中后者表明文件结束。其它两种方法将数据读入到字节数组中,并返回 所读的字节数。第三个方法中的两个 int 参数指定了所要填入的数组的子范围。 注-考虑到效率,总是在实际最大的块中读取数据。 void close() 你完成流操作之后,就关闭这个流。如果你有一个流所组成的栈,使用过滤器流,就关闭 栈顶部的流。这个关闭操作会关闭其余的流。 int available() 这个方法报告立刻可以从流中读取的字节数。在这个调用之后的实际读操作可能返回更多 的字节数。 skip(long) 这个方法丢弃了流中指定数目的字符。 boolean markSupported() void mark(int) void reset() 如果流支持“回放”操作,则这些方法可以用来完成这个操作。如果 mark()和 reset()方 法可以在特定的流上操作,则 markSupported()方法将返回 ture。mark(int)方法用来指明应当 标记流的当前点和分配一个足够大的缓冲区,它最少可以容纳参数所指定数量的字符。在随后 的 read()操作完成之后,调用 reset()方法来返回你标记的输入点。 2.2 OutputStream 这一类别包括的类决定了我们的输入往何处去:一个字节数组(但没有 String;假定我们可用 字节数组创建一个);一个文件;或者一个“管道”。 除 此以外,FilterOutputStream 为“破坏器”类提供了一个基础类,它将属性或者有用的接口 同输出流连接起来。这将在以后讨论。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 227 ByteArrayOutputStream 在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓 冲区。 可选缓冲区的初始大小用于指出数据的目的地。若将其同 FilterOutputStream 对象连接到 一起,可提供一个有用的接口。 FileOutputStream 将信息发给一个文件 用一个 String 代表文件名,或选用一个 File 或 FileDescriptor 对象用于指出数据的目的地。若将其同 FilterOutputStream 对象连接到一起,可 提供一个有用的接口。 PipedOutputStream 我们写给它的任何信息都会自动成为相关的 PipedInputStream 的输出。实 现 了“管道化”的概念 PipedInputStream 为 多 线 程 处 理 指 出 自己数据的目的地将其同 FilterOutputStream 对象连接到一起,便可提供一个有用的接口。 FilterOutputStream 对 作 为 破坏器接口使用的类进行抽象处理;那个破坏器为其他 OutputStream 类提供了有用的功能。 2.2.1:OutputStream 方法 三个基本的 write()方法 l void write(int) l void write(byte []) l void write(byte [], int, int) 这些方法写输出流。和输入一样,总是尝试以实际最大的块进行写操作。 void close() 当你完成写操作后,就关闭输出流。如果你有一个流所组成的栈,就关闭栈顶部的流。 这个关闭操作会关闭其余的流。 void flush() 有时一个输出流在积累了若干次之后才进行真正的写操作。flush()方法允许你强制执行 写操作。 3: IO 包中基本流类 在 java.io 包中定义了一些流类。下图表明了包中的类层次。一些更公共的类将在后面介绍。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 228 3.1: FileInputStream 和 FileOutputStream 这些类是结点流,而且正如这个名字所暗示的那样,它们使用磁盘文件。这些类的构造函数允 许你指定它们所连接的文件。要构造一个 FileInputStream,所关联的文件必须存在而且是可读的。 如果你要构造一个 FileOutputStream 而输出文件已经存在,则它将被覆盖。 FileInputStream infile = new FileInputStream("myfile.dat"); FileOutputStream outfile = new FileOutputStream("results.dat"); 3.2 BufferInputStream 和 BufferOutputStream 这些是过滤器流,它们可以提高 I/O 操作的效率。 3.3 DataInputStream 和 DataOutputStream DataInputStream 方法 byte readByte() long readLong() double readDouble() DataOutputStream 方法 void writeByte(byte) void writeLong(long) void writeDouble(double) 注意 DataInputStream 和 DataOutputStream 的方法是成对的。 这些流都有读写字符串的方法,但不应当使用这些方法。它们已经被后面所讨论的读者和作者 所取代。 3.4 PipedInputStream 和 PipedOutputStream 管 道流用来在线程间进行通信。一个线程的 PipedInputStream 对象从另一个线程的 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 229 PipedOutputStream 对象读取输入。要使管道流有用,必须有一个输入方和一个输出方。 4: URL 输入流 除了基本的文件访问之外,Java 技术提供了使用统一资源定位器(URL)来访问网络上的文件。 当你使用 Applet 的 getDocumentBase()方法来访问声音和图象时,你已经隐含地使用了 URL 对象。 String imageFile = new String ("images/Duke/T1.gif"); images[0] = getImage(getDocumentBase(), imageFile); 然而,你必须象下面的程序那样提供一个直接的 URL java.net.URL imageSource; try { imageSource = new URL("http://mysite.com/~info"); } catch ( MalformedURLException e) {} images[0] = getImage(imageSource, "Duke/T1.gif"); 4.1 打开一个输入流 你可以通过存储文档基目录下的一个数据文件来打开一个合适的 URL 输入流。 1.InputStream is = null; 2.String datafile = new String("Data/data.1-96"); 3.byte buffer[] = new byte[24]; 4.try { 5.// new URL throws a MalformedURLException 6.// URL.openStream() throws an IOException 7.is = (new URL(getDocumentBase(), datafile)).openStream(); 8.} catch (Exception e) {} 现在,你可以就象使用 FileInputStream 对象那样来用 it 来读取信息: 1.try { 2.is.read(buffer, 0, buffer.length); 3.} catch (IOException e1) {} 警告-记住大多数用户进行了浏览器的安全设置,以防止 Applet 存取文件。 5: Reader 和 Writer Java 中的 Reader 和 Writer 的结构建下图 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 230 5.1 Unicode Java 技术使用 Unicode 来表示字符串和字符,而且它提供了 16 位版本的流,以便用类似的方 法来处理字符。这些 16 位版本的流称为读者和作者。和流一样,它们都在 java.io 包中。 读者和作者中最重要的版本是 InputStreamReader 和 OutputStreamWriter。这些类用来作为字节流 与读者和作者之间的接口。 当你构造一个 InputStreamReader 或 OutputStreamWriter 时,转换规则定义了 16 位 Unicode 和其它平台的特定表示之间的转换。 缺省情况下,如果你构造了一个连接到流的读者和作者,那么转换规则会在使用缺省平台所定 义的字节编码和 Unicode 之间切换。在英语国家中,所使用的字节编码是:ISO 8859-1。 你可以使用所支持的另一种编码形式来指定其它的字节编码。在 native2ascii 工具中,你可以 找到一个关于所支持的编码形式的列表。 使用转换模式,Java 技术能够获得本地平台字符集的全部灵活性,同时由于内部使用 Unicode,所 以还能保持平台独立性。 5.2 缓冲读者和作者 因为在各种格式之间进行转换和其它 I/O 操作很类似,所以在处理大块数据时效率最高。在 InputStreamReader 和 OutputStreamWriter 的结尾链接一个 BufferedReader 和 BufferedWriter 是 一个好主意。记住对 BufferedWriter 使用 flush()方法。 5.3 读入字符串输入 下面这个例子说明了从控制台标准输入读取字符串所应当使用的一个技术。 1.import java.io.*; 2.public class CharInput { 3.public static void main (String args[]) throws 4.java.io.IOException { 5.String s; 6.InputStreamReader ir; 7.BufferedReader in; 8.ir = new InputStreamReader(System.in); 9.in = new BufferedReader(ir); 10. 11.while ((s = in.readLine()) != null) { 12.System.out.println("Read: " + s); 13.} 14.} 15.} http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 231 5.4 使用其它字符转换 如果你需要从一个非本地(例如,从连接到一个不同类型的机器的网络连接读取)的字符编码读 取输入,你可以象下面这个程序那样,使用显式的字符编码构造 ir=new InputStreamReader(System.in, “8859_1”); 注-如果你通过网络连接读取字符,就应该使用这种形式。否则,你的程序会总是试图将所读取的 字符当作本地表示来进行转换,而这并不总是正确的。ISO 8859-1 是映射到 ASCII 的 Latin-1 编码 模式。 6: 串行化 将一个对象存放到某种类型的永久存储器上称为保持。如果一个对象可以被存放到磁盘或磁带 上,或者可以发送到另外一台机器并存放到存储器或磁盘上,那么这个对象就被称为可保持的。 java.io.Serializable 接口没有任何方法,它只作为一个“标记者”,用来表明实现了这个接口的类 可以考虑串行化。类中没有实现 Serializable 的对象不能保存或恢复它们的状态。 6.1 对象图 当一个对象被串行化时,只有对象的数据被保存;方法和构造函数不属于串行化流。如果一个 数据变量是一个对象,那么这个对象的数据成员也会被串行化。树或者对象数据的结构,包括这些 子对象,构成了对象图。 因为有些对象类所表示的数据在不断地改变,所以它们不会被串行化;例如, java.io.FileInputStream 、java.io.FileOutputStream 和 java.lang.Thread 等流。如果一个可串行化对象包 含 对 某 个 不可串行化元素的引用,那么整个串行化操作就会失败,而且会抛出一个 NotSerializableException。 如果对象图包含一个不可串行化的引用,只要这个引用已经用 transient 关键字进行了标记,那 么对象仍然可以被串行化。 public class MyClass implements Serializable { public transient Thread myThread; private String customerID; private int total; 域存取修饰符对于被串行化的对象没有任何作用。写入到流的数据是字节格式,而且字符串被 表示为 UTF(文件系统安全的通用字符集转换格式)。transient 关键字防止对象被串行化。 public class MyClass implements Serializable { public transient Thread myThread; private transient String customerID; private int total; 7: 对象流的基本读写示例 7.1 写 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 232 对一个文件流读写对象是一个简单的过程。考虑如下代码段,它将一个 java.util.Data 对象的实 例发送到一个文件: 1.public class SerializeDate { 2.SerializeDate() { 3.Date d = new Date (); 4.try { 5.FileOutputStream f = new 6.FileOutputStream("date.ser"); 7.ObjectOutputStream s = new 8.ObjectOutputStream(f); 9.s.writeObject (d); 10.f.close (); 11.} catch (IOException e) { 12.e.printStackTrace (); 13.} 14.} 15. 16.public static void main (String args[]) { 17.new SerializeDate(); 18.} 19.} 7.2 读 读对象和写对象一样简单,只需要说明一点-readObject()方法将流作为一个 Object 类型返回, 而且在使用那个类的方法之前,必须把它转换成合适的类名。 1.public class UnSerializeDate { 2.UnSerializeDate () { 3.Date d = null; 4.try { 5.FileInputStream f = new 6.FileInputStream("date.ser"); 7.ObjectInputStream s = new 8.ObjectInputStream(f); 9.d = (Date) s.readObject (); 10.f.close (); 11.} catch (Exception e) { 12.e.printStackTrace (); 13.} 14. 15.System.out.println("Unserialized Date object from date.ser"); 16.System.out.println("Date: "+d); 17.} 18. 19.public static void main (String args[]) { http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 233 20.new UnSerializeDate(); 21.} 22.} 二:文件和过滤器 1: File 操作 File 类是 IO 中文件操作最常用的类。 File 类有一个欺骗性的名字——通常会认为它对付的是一个文件,但实情并非如此。它既代表 一个特定文件的名字,也代表目录内一系列文件的名字。若代表一个文件集,便可用 list()方法查询 这个集,返回的是一个字串数组。之所以要返回一个数组,而非某个灵活的集合类,是因为元素的 数量是固定的。而且若想得到一个不同的目录列表,只需创建一个不同的 File 对象即可。事实上, “FilePath”(文件路径)似乎是一个更好的名字。本节将向大家完整地例示如何使用这个类,其中 包括相关的 FilenameFilter(文件名过滤器)接口。 1.1:创建一个新的 File 对象 File 类提供了若干处理文件和获取它们基本信息的方法。 File myFile; myFile = new File("mymotd"); myFile = new File("/", "mymotd"); // more useful if the directory or filename is // a variable File myDir = new File("/"); myFile = new File(myDir, "mymotd"); 你所使用的构造函数经常取决于你所使用的其他文件对象。例如,如果你在你的应用程序中只 使用一个文件,那么就会使用第一个构造函数。如果你使用一个公共目录中的若干文件,那么使用 第二个或者第三个构造函数可能更容易。 File 类提供了独立于平台的方法来操作由本地文件系统维护的文件。然而它不允许你存取文件 的内容。 注-你可以使用一个 File 对象来代替一个 String 作为 FileInputStream 和 FileOutputStream 对象 的构造函数参数。这是一种推荐方法,因为它独立于本地文件系统的约定。 1.2:目录列表器 File 类并不仅仅是对现有目录路径、文件或者文件组的一个表示。亦可用一个 File 对象新建一 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 234 个目录,甚至创建一个完整的目录路径——假如它尚不存在的话。亦可用它了解文件的属性(长度、 上一次修改日期、读/写属性等),检查一个 File 对象到底代表一个文件还是一个目录,以及删除一 个文件等等。下列程序完整展示了如何运用 File 类剩下的这些方法: // MakeDirectories.java import java.io.*; public class MakeDirectories { private final static String usage = "Usage:MakeDirectories path1 ...\n" + "Creates each path\n" + "Usage:MakeDirectories -d path1 ...\n" + "Deletes each path\n" + "Usage:MakeDirectories -r path1 path2\n" + "Renames from path1 to path2\n"; private static void usage() { System.err.println(usage); System.exit(1); } private static void fileData(File f) { System.out.println( "Absolute path: " + f.getAbsolutePath() + "\n Can read: " + f.canRead() + "\n Can write: " + f.canWrite() + "\n getName: " + f.getName() + "\n getParent: " + f.getParent() + "\n getPath: " + f.getPath() + "\n length: " + f.length() + "\n lastModified: " + f.lastModified()); if(f.isFile()) System.out.println("it's a file"); else if(f.isDirectory()) System.out.println("it's a directory"); } public static void main(String[] args) { if(args.length < 1) usage(); if(args[0].equals("-r")) { if(args.length != 3) usage(); File old = new File(args[1]), rname = new File(args[2]); old.renameTo(rname); fileData(old); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 235 fileData(rname); return; // Exit main } int count = 0; boolean del = false; if(args[0].equals("-d")) { count++; del = true; } for( ; count < args.length; count++) { File f = new File(args[count]); if(f.exists()) { System.out.println(f + " exists"); if(del) { System.out.println("deleting..." + f); f.delete(); } } else { // Doesn't exist if(!del) { f.mkdirs(); System.out.println("created " + f); } } fileData(f); } } } 在 fileData()中,可看到应用了各种文件调查方法来显示与文件或目录路径有关的信息。 main()应用的第一个方法是 renameTo(),利用它可以重命名(或移动)一个文件至一个全新的路径(该 路径由参数决定),它属于另一个 File 对象。这也适用于任何长度的目录。 若试验上述程序,就 可 发 现 自己能制作任意复杂程度的一个目录路径,因为 mkdirs()会帮我们完成所 有工作。在 Java 1.0 中,-d 标志报告目录虽然已被删除,但它依然存在;但在 Java 1.1 中,目录会被 实际删除。 1.3:文件测试和工具 当你创建一个 File 对象时,你可以使用下面任何一种方法来获取有关文件的信息: 文件名 l String getName() http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 236 l String getPath() l String getAbsolutePath() l String getParent() l boolean renameTo(File newName) 文件测试 l boolean exists() l boolean canWrite() l boolean canRead() l boolean isFile() l boolean isDirectory() l boolean isAbsolute() 通用文件信息和工具 l long lastModified() l long length() l boolean delete() 目录工具 l boolean mkdir() l String[] list() 1.4:随机存取文件 你经常会发现你只想读取文件的一部分数据,而不需要从头至尾读取整个文件。你可能想访问 一个作为数据库的文本文件,此时你会移动到某一条记录并读取它的数据,接着 移动到另一个记录, 然 后再到其他记录――每一条记录都位于文件的不同部分。Java 编 程 语言提供了一个 RandomAccessFile 类来处理这种类型的输入输出。 你可以用如下两种方法来打开一个随机存取文件: l 用文件名 myRAFile = new RandomAccessFile(String name, String mode); l 用文件对象 myRAFile = new RandomAccessFile(File file, String mode); mode 参数决定了你对这个文件的存取是只读(r)还是读/写(rw)。 例如,你可以打开一个打开一个数据库文件并准备更新: RandomAccessFile myRAFile; myRAFile = new RandomAccessFile("db/stock.dbf","rw"); 存取信息 RandomAccessFile 对象按照与数据输入输出对象相同的方式来读写信息。你可以访问在 DataInputStrem 和 DataOutputStream 中所有的 read()和 write()操作。 Java 编程语言提供了若干种方法,用来帮助你在文件中移动。 l long getFilePointer(); 返回文件指针的当前位置。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 237 l void seek(long pos); 设置文件指针到给定的绝对位置。这个位置是按照从文件开始的字节偏移量给出的。 位置 0 标志文件的开始。 l long length() 返回文件的长度。位置 length()标志文件的结束。 添加信息 你可以使用随机存取文件来得到文件输出的添加模式。 myRAFile = new RandomAccessFile("java.log","rw"); myRAFile.seek(myRAFile.length()); // Any subsequent write()s will be appended to the file 2: 过滤器 FilterInputStream 和 FilterOutputStream(这两个名字不十分直观)提供了相应的装饰器接 口,用于控制一个特定的输入流(InputStream)或者输出流(OutputStream)。它们分别是从 InputStream 和 OutputStream 衍生出来的。此外,它们都属于抽象类,在理论上为我们与一个流的 不同通信手段都提供了一个通用的接口。事实上,FilterInputStream 和 FilterOutputStream 只是 简单地模仿了自己的基础类,它们是一个装饰器的基本要求。 2.1 通过 FilterInputStream 从 InputStream 里读入数据 FilterInputStream 类要完成两件全然不同的事情。其中,DataInputStream 允许我们读取不同 的基本类型数据以及 String 对象(所有方法都以“read”开头,比如 readByte(),readFloat()等 等)。伴随对应的 DataOutputStream,我们可通过数据“流”将基本类型的数据从一个地方搬到另 一个地方。若读取块内的数据,并自己进行解析,就不需要用到 DataInputStream。但在其他许多 情况下,我们一般都想用它对自己读入的数据进行自动格式化。 剩下的类用于修改 InputStream 的内部行为方式:是 否 进 行 缓冲,是否 跟 踪 自己读入的数据行, 以及是否能够推回一个字符等等。后两种类看起来特别象提供对构建一个编译器的支持(换言之, 添加它们为了支持 Java 编译器的构建),所以在常规编程中一般都用不着它们。 也许几乎每次都要缓冲自己的输入,无论连接的是哪个 IO 设备。所以 IO 库最明智的做法就是 将未缓冲输入作为一种特殊情况处理,同时将缓冲输入接纳为标准做法。 2.2 通过 FilterOutputStream 向 OutputStream 里写入数据 与 DataInputStream 对应的是 DataOutputStream,后者对各个基本数据类型以及 String 对象进行 格式化,并将其置入一个数据“流”中,以便任何机器上的 DataInputStream 都能正常地读取它们。 所有方法都以“wirte”开头,例如 writeByte(),writeFloat()等等。 若想进行一些真正的格式化输出,比如输出到控制台,请使用 PrintStream。利用它可以打印出 所有基本数据类型以及 String 对象,并可采用一种易于查看的格式。这与 DataOutputStream 正好相 反,后者的目标是将那些数据置入一个数据流中,以便 DataInputStream 能够方便地重新构造它们。 System.out 静态对象是一个 PrintStream。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 238 PrintStream 内两个重要的方法是 print()和 println()。它们已进行了覆盖处理,可打印出所有数据 类型。print()和 println()之间的差异是后者在操作完毕后会自动添加一个新行。 BufferedOutputStream 属于一种“修改器”,用于指示数据流使用缓冲技术,使自己不必每次都 向流内物理性地写入数据。通常都应将它应用于文件处理和控制器 IO。 三:I/O 流的基本应用 1: 输入流 1.1:缓冲的输入文件 程序示例 import java.io.*; public class Test { public static void main(String args[]) { String currentLine; try { DataInputStream in = new DataInputStream( new BufferedInputStream( new FileInputStream("Test.java") ) ); while ((currentLine = in.readLine()) != null) System.out.println(currentLine); } catch (IOException e) { System.err.println("Error: " + e); } // End of try/catch structure. } // End of method: main } // End of class 为打开一个文件以便输入,需要使用一个 FileInputStream,同时将一个 String 或 File 对象 作为文件名使用。为提高速度,最好先对文件进行缓冲处理,从而获得用于一个 BufferedInputStream 的构建器的结果句柄。为了以格式化的形式读取输入数据,我们将那个结果句柄赋给用于一个 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 239 DataInputStream 的构建器。DataInputStream 是我们的最终(final)对象,并是我们进行读取操 作的接口。 在这个例子中,只用到了 readLine()方法,但 理所当然任何 DataInputStream 方法都可以采用。 一旦抵达文件末尾,readLine()就会返回一个 null(空),以便中止并退出 while 循环。 1.2:格式化内存输入 StringBufferInputStream 的接口是有限的,所以通常需要将其封装到一个 DataInputStream 内,从而增强它的能力。然而,若选择用 readByte()每次读出一个字符,那么所有值都是有效的, 所以不可再用返回值来侦测何时结束输入。相反,可用 available()方法判断有多少字符可用。下 面这个例子展示了如何从文件中一次读出一个字符: // TestEOF.java import java.io.*; public class TestEOF { public static void main(String[] args) { try { DataInputStream in = new DataInputStream( new BufferedInputStream( new FileInputStream("TestEof.java"))); while(in.available() != 0) System.out.print((char)in.readByte()); } catch (IOException e) { System.err.println("IOException"); } } } 注意取决于当前从什么媒体读入,avaiable()的工作方式也是有所区别的。它在字面上意味着 “可以不受阻塞读取的字节数量”。 对 一个文件来说,它意味着整个文件。但对一个不同种类的数据 流来说,它却可能有不同的含义。因此在使用时应考虑周全。 为了在这样的情况下侦测输入的结束,也可以通过捕获一个违例来实现。然而,若真的用违例来控 制数据流,却显得有些大材小用。 2 输出流 两类主要的输出流是按它们写入数据的方式划分的:一种按人的习惯写入,另一种为了以后由 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 240 一个 DataInputStream 而写入。RandomAccessFile 是 独立的,尽管它的数据格式兼容于 DataInputStream 和 DataOutputStream。 2.1:保存与恢复数据 PrintStream 能格式化数据,使其能按我们的习惯阅读。但为了输出数据,以便由另一个数据 流恢复,则需用一个 DataOutputStream 写入数据,并用一个 DataInputStream 恢复(获取)数据。 当然,这些数据流可以是任何东西,但这里采用的是一个文件,并进行了缓冲处理,以加快读写速 度。 注意字串是用 writeBytes()写入的,而非 writeChars()。若使用后者,写入的就是 16 位 Unicode 字符。由于 DataInputStream 中没有补充的“readChars”方法,所以不得不用 readChar()每次取 出一个字符。所以对 ASCII 来说,更方便的做法是将字符作为字节写入,在后面跟随一个新行;然 后再用 readLine()将字符当作普通的 ASCII 行读回。 writeDouble()将 double 数字保存到数据流中,并用补充的 readDouble()恢复它。但为了保证 任何读方法能够正常工作,必须知道数据项在流中的准确位置,因为既有可能将保存的 double 数据 作为一个简单的字节序列读入,也有可能作为 char 或其他格式读入。所以必须要么为文件中的数据 采用固定的格式,要么将额外的信息保存到文件中,以便正确判断数据的存放位置。 2.2:读写随机访问文件 示例代码 import java.io.*; public class Test { public static void main(String args[]) { try { RandomAccessFile f =new RandomAccessFile(“test.txt","rw"); int i; double d; for (i= 0; i< 10; i++){ f.writeDouble(3.14f*i); } f.seek(16); f.writeDouble(0); f.seek(0); for (i= 0; i< 10; i++) { d=f.readDouble(); System.out.println("["+i+"]: "+d); } http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 241 f.close(); } catch (IOException io) { System.out.println(io); System.exit(-1); }// End of try/catch structure. } // End of method: main } // End of class: 正如早先指出的那样,RandomAccessFile 与 IO 层次结构的剩余部分几乎是完全隔离的,尽管它 也实现了 DataInput 和 DataOutput 接口。所以不可将其与 InputStream 及 OutputStream 子类的任何部 分关联起来。尽管也许能将一个 ByteArrayInputStream 当作一个随机访问元素对待,但只能用 RandomAccessFile 打开一个文件。必须假定 RandomAccessFile 已得到了正确的缓冲,因为我们不能 自行选择。 可以自行选择的是第二个构建器参数:可决定以“只读”(r)方式或“读写”(rw)方式打开一 个 RandomAccessFile 文件。 使用 RandomAccessFile 的时候,类似于组合使用 DataInputStream 和 DataOutputStream(因为它 实现了等同的接口)。除此以外,还可看到程序中使用了 seek(),以便在文件中到处移动,对某个值 作出修改。 3: 快捷文件处理 由于以前采用的一些典型形式都涉及到文件处理,所以大家也许会怀疑为什么要进行那么多的 代码输入——这正是装饰器方案一个缺点。本部分将向大家展示如何创建和使用典型文件读取和写 入配置的快捷版本。为了将每个类都添加到库内,只需将其置入适当的目录,并添加对应的 package 语句即可。 3.1:快速文件输入 若想创建一个对象,用它从一个缓冲的 DataInputStream 中读取一个文件,可将这个过程封装 到一个名为 InFile 的类内。如下所示: // InFile.java import java.io.*; public class InFile extends DataInputStream { public InFile(String filename) throws FileNotFoundException { super( new BufferedInputStream( new FileInputStream(filename))); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 242 } public InFile(File file) throws FileNotFoundException { this(file.getPath()); } } 无论构建器的 String 版本还是 File 版本都包括在内,用于共同创建一个 FileInputStream。 就象这个例子展示的那样,现在可以有效减少创建文件时由于重复强调造成的问题。 3.2:快速输出格式化文件 亦可用同类型的方法创建一个 PrintStream,令其写入一个缓冲文件。 // PrintFile.java import java.io.*; public class PrintFile extends PrintStream { public PrintFile(String filename) throws IOException { super( new BufferedOutputStream( new FileOutputStream(filename))); } public PrintFile(File file) throws IOException { this(file.getPath()); } } 注意构建器不可能捕获一个由基础类构建器“掷”出的违例。 3.3:快速输出数据文件 最后,利用类似的快捷方式可创建一个缓冲输出文件,用它保存数据(与由人观看的数据格式 相反): // OutFile.java package com.bruceeckel.tools; import java.io.*; public class OutFile extends DataOutputStream { public OutFile(String filename) throws IOException { super( http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 243 new BufferedOutputStream( new FileOutputStream(filename))); } public OutFile(File file) throws IOException { this(file.getPath()); } } 4:从标准输入中读取数据 以 Unix 首先倡导的“标准输入”、“标准输出”以及“标准错误输出”概念为基础,Java 提供 了相应的 System.in,System.out 以及 System.err。贯这一整本书,大家都会接触到如何用 System.out 进行标准输出,它已预封装成一个 PrintStream 对象。System.err 同样是一个 PrintStream,但 System.in 是一个原始的 InputStream,未进行任何封装处理。这意味着尽管能直 接使用 System.out 和 System.err,但必须事先封装 System.in,否则不能从中读取数据。 典型情况下,我们希望用 readLine()每次读取一行输入信息,所以需要将 System.in 封装到一 个 DataInputStream 中。这是 Java 1.0 进行行输入时采取的“老”办法。在本章稍后,大家还会看 到 Java 1.1 的解决方案。下面是个简单的例子,作用是回应我们键入的每一行内容: // Echo.java import java.io.*; public class Echo { public static void main(String[] args) { DataInputStream in = new DataInputStream( new BufferedInputStream(System.in)); String s; try { while((s = in.readLine()).length() != 0) System.out.println(s); // An empty line terminates the program } catch(IOException e) { e.printStackTrace(); } } } 之所以要使用 try 块,是由于 readLine()可能“掷”出一个 IOException。注意同其他大多数 流一样,也应对 System.in 进行缓冲。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 244 由于在每个程序中都要将 System.in 封装到一个 DataInputStream 内,所以显得有点不方便。 但采用这种设计方案,可以获得最大的灵活性。 练习实践课: 本章内容为IO 操作, 实践重点: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 245 l IO 基础类 l 文件操作,包括文件的建立、写入、读出、删除等 程序 1: 文件操作 需求:文件的建立、写入、读出、删除。 目标: 1、 目录的建立; 2、 文件的建立; 3、 文件内容写入; 4、 文件内容读取。 程序: //: FileProcess.java package com.useful.java.part6; import java.io.*; public class FileProcess{ public FileProcess(){ } public static void main(String[] args){ FileProcess process=new FileProcess(); String dirname="c:/testdir"; String filename="a.txt"; process.createDir(dirname); process.createFile(dirname+"/"+filename); process.writeFile(dirname+"/"+filename); process.readFile(dirname+"/"+filename); process.writeTxt(dirname+"/testtext.txt"); } public void createDir(String dirname){ File file=new File(dirname); if(file.exists()==false){ file.mkdirs(); } } public void createFile(String filename){ http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 246 try{ File file=new File(filename); file.createNewFile(); } catch(Exception e){ e.printStackTrace(); } } public void writeFile(String filename){ try{ File file=new File(filename); FileOutputStream output=new FileOutputStream(file); output.write("i am test.".getBytes()); output.close(); } catch(Exception e){ e.printStackTrace(); } } public void readFile(String filename){ try{ File file=new File(filename); FileInputStream input=new FileInputStream(file); byte byteValues[]=new byte[(int)file.length()]; input.read(byteValues); input.close(); System.out.println("file content is "+(new String(byteValues))); } catch(Exception e){ e.printStackTrace(); } } public void writeTxt(String filename){ try{ File file=new File(filename); Writer writer=new FileWriter(file); writer.write("Holen ,Holen"); writer.close(); } catch(Exception e){ e.printStackTrace(); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 247 } } } 说明: 1、 本程序将在 C 盘根目录下,建立 testdir 目录,并在此目录下建立两个文本文件; 注意上例中,往文件中写入内容的方式,一种是用流的方式,一种是文本方式。 程序 2: ZIP 压缩文件 需求:文件压缩 目标: 1、 压缩文件的原理; 2、 压缩文件定义及应用。 程序: //: ZipCompress.java package com.useful.java.part6; import java.io.*; import java.util.*; import java.util.zip.*; public class ZipCompress { public static void main(String[] args) { try { FileOutputStream f = new FileOutputStream("test.zip"); CheckedOutputStream csum = new CheckedOutputStream( f, new Adler32()); ZipOutputStream out = new ZipOutputStream( new BufferedOutputStream(csum)); out.setComment("A test of Java Zipping"); // Can't read the above comment, though for(int i = 0; i < args.length; i++) { System.out.println( "Writing file " + args[i]); BufferedReader in = new BufferedReader( new FileReader(args[i])); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 248 out.putNextEntry(new ZipEntry(args[i])); int c; while((c = in.read()) != -1) out.write(c); in.close(); } out.close(); // Checksum valid only after the file // has been closed! System.out.println("Checksum: " + csum.getChecksum().getValue()); // Now extract the files: System.out.println("Reading file"); FileInputStream fi = new FileInputStream("test.zip"); CheckedInputStream csumi = new CheckedInputStream( fi, new Adler32()); ZipInputStream in2 = new ZipInputStream( new BufferedInputStream(csumi)); ZipEntry ze; System.out.println("Checksum: " + csumi.getChecksum().getValue()); while((ze = in2.getNextEntry()) != null) { System.out.println("Reading file " + ze); int x; while((x = in2.read()) != -1) System.out.write(x); } in2.close(); // Alternative way to open and read // zip files: ZipFile zf = new ZipFile("test.zip"); Enumeration e = zf.entries(); while(e.hasMoreElements()) { ZipEntry ze2 = (ZipEntry)e.nextElement(); System.out.println("File: " + ze2); // ... and extract the data as before } } catch(Exception e) { e.printStackTrace(); } } http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 249 } 说明: 1、 Java 1.1 以上,提供了 Zip 支持。利用它可以方便地保存多个文件。有一个独立的类来 简化对 Zip 文件的读操作; 2、 这个库采采用的是标准 Zip 格式,所以能与当前因特网上使用的大量压缩、解 压 工 具 很 好地协作; 3、 对于要加入压缩档的每一个文件,都必须调用 putNextEntry(),并将其传递给一个 ZipEntry 对象。ZipEntry 对象包含了一个功能全面的接口,利用它可以获取和设置 Zip 文件内那个特定的 Entry(入口)上能够接受的所有数据:名字、压缩后和压缩前的长 度、日期、CRC 校验和、额外字段的数据、注释、压缩方法以及它是否一个目录入口 等。 作业 1:利用输入输出流编写一个程序,可以实现文件复制的功能,程序的命令行参数的形式及操作 功能均类似于 DOS 中的 copy 命令 2:打开一个文本文件,每次读取一行内容 3:做一个 Swing 应用程序,可以打开文本型文件,并显示其中的内容 第十四章 多线程 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 250 一:线程的基本概念 教学目标: i掌握线程的基本概念 i掌握多线程的编写 i掌握多线程的控制 i掌握多线程的同步 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 251 1:什么是线程 一个关于计算机的简化的视图是:它有一个执行计算的处理机、包含处理机所执行的程序的 ROM(只读存储器)、包含程序所要操作的数据的 RAM(只读存储器)。在这个简化视图中,只能执行一 个作业。一个关于现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。 你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。如果你要执行一个以上的 作业,这类似有一台以上的计算机。在这个模型中,线程或执行上下文,被认为是带有自己的程序 代码和数据的虚拟处理机的封装。java.lang.Thread 类允许用户创建并控制他们的线程。 注-在这个模块中,使用“Thread”时是指 java.lang.Thread 而使用“thread”时是指执行上下 文。 2:线程的三个部分 进程是正在执行的程序。一个或更多的线程构成了一个进程。一个线程或执行上下文由三个主 要部分组成 l 一个虚拟处理机 l CPU 执行的代码 l 代码操作的数据 代码可以或不可以由多个线程共享,这和数据是独立的。两个线程如果执行同一个类的实例代 码,则它们可以共享相同的代码。 类似地,数据可以或不可以由多个线程共享,这和代码是独立的。两个线程如果共享对一个公 共对象的存取,则它们可以共享相同的数据。 在 Java 编程中,虚拟处理机封装在 Thread 类的一个实例里。构造线程时,定义其上下文的代 码和数据是由传递给它的构造函数的对象指定的。 3:Java 中的线程的概念 几乎每种操作系统都支持进程的概念——进程就是在某种程度上相互隔离的、独立运行的程序。 线程化是允许多个活动共存于一个进程中的工具。大多数现代的操作系统都支持线程,而且线 线程的三个部分 l 处理机 l 代码 l 数据 CPU Code Data A thread or execution context http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 252 程的概念以各种形式已存在了好多年。Java 是第一个在语言本身中显式地包含线程的主流编程语 言,它没有把线程化看作是底层操作系统的工具。 有时候,线程也称作轻量级进程。就象进程一样,线程在程序中是独立的、并发的执行路径, 每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与分隔的进程相比,进程 中的线程之间的隔离程度要小。它们共享内存、文件句柄和其它每个进程应有的状态。 进程可以支 持多个线程,它们看似同时执行,但互相之间并不同步。一个进程中的多个线程共享相同的内存地 址空间,这就意味着它们可以访问相同的变量和对象,而且它们从同一堆中分配对象。尽管这让线 程之间共享信息变得更容易,但您必须小心,确保它们不会妨碍同一进程里的其它线程。 Java 线程工具和 API 看似简单。但是,编写有效使用线程的复杂程序并不十分容易。因为有 多个线程共存在相同的内存空间中并共享相同的变量,所以您必须小心,确保您的线程不会互相干 扰。 每个 Java 程序都至少有一个线程 — 主线程。当一个 Java 程序启动时,JVM 会创建主线程, 并在该线程中调用程序的 main() 方法。 JVM 还创建了其它线程,您通常都看不到它们 — 例如,与垃圾收集、对象终止和其它 JVM 内 务处理任务相关的线程。其它工具也创建线程,如 AWT(抽象窗口工具箱(Abstract Windowing Toolkit))或 Swing UI 工具箱、servlet 容器、应用程序服务器和 RMI(远程方法调用(Remote Method Invocation))。 在 Java 程序中使用线程有许多原因。如果您使用 Swing、servlet、RMI 或 Enterprise JavaBeans(EJB)技术,您也许没有意识到您已经在使用线程了。 使用线程的一些原因是它们可以帮助: 使 UI 响应更快 事件驱动的 UI 工具箱(如 AWT 和 Swing)有一个事件线程,它处理 UI 事件,如击键或 鼠标点击。 利用多处理器系统 多处理器(MP)系统比过去更普及了。以前只能在大型数据中心和科学计算设施中才能找 到它们。现在许多低端服务器系统 — 甚至是一些台式机系统 — 都有多个处理器。 调度的基本单位通常是线程;如果某个程序只有一个活动的线程,它一次只能在一个处理 器上运行。如果某个程序有多个活动线程,那么可以同时调度多个线程。在精心设计的程序中, 使用多个线程可以提高程序吞吐量和性能。 简化建模 在某些情况下,使用线程可以使程序编写和维护起来更简单。考虑一个仿真应用程序,您 要在其中模拟多个实体之间的交互作用。给每个实体一个自己的线程可以使许多仿真和对应用 程序的建模大大简化。 服务器应用程序从远程来源(如套接字)获取输入。当读取套接字时,如果当前没有可用 数据,那么对 SocketInputStream.read() 的调用将会阻塞,直到有可用数据为止。 如果单线程程序要读取套接字,而套接字另一端的实体并未发送任何数据,那么该程序只 会永远等待,而不执行其它处理。相反,程序可以轮询套接字,查看是否有可用数据,但通常 不会使用这种做法,因为会影响性能。 线程和进程的区别是: 每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大。 线程作为轻量的进程,同一类线程可以共享代码和数据空间,但每个线程有独立的运 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 253 行栈和程序计数器,因此线程切换的开销较小。 多进程——在操作系统中能同时运行多个任务(程序),也称多任务。 多线程——在同一应用程序中有多个顺序流同时执行。 4:Java 编程中的线程 4.1 第一个线程 下面来学习如何创建第一个线程,以及如何使用构造函数参数来为一个线程提供运行时的数据 和代码。 一个 Thread 类构造函数带有一个参数,它是 Runnable 的一个实例。一个 Runnable 是由一个实 现了 Runnable 接口(即,提供了一个 public void run()方法)的类产生的。 例如: 1.public class ThreadTest { 2.public static void main(String args[]) { 3.Xyz r = new Xyz(); 4.Thread t = new Thread(r); 5.} 6.} 7. 8.class Xyz implements Runnable { 9.int i; 10. 11.public void run() { 12.while (true) { 13.System.out.println("Hello " + i++); 14.if (i == 50) break; 15.} 16.} 17.} 首先,main()方法构造了 Xyz 类的一个实例 r。实例 r 有它自己的数据,在这里就是整数 i。因 为实例 r 是传给 Thread 的类构造函数的,所以 r 的整数 i 就是线程运行时刻所操作的数据。线程总 是从它所装载的 Runnable 实例(在本例中,这个实例就是 r。)的 run()方法开始运行。 一个多线程编程环境允许创建基于同一个 Runnable 实例的多个线程。这可以通过以下方法来做 到: Thread t1= new Thread(r); Thread t2= new Thread(r); 此时,这两个线程共享数据和代码。 CPU { New thread Thread t http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 254 总之,线程通过 Thread 对象的一个实例引用。线程从装入的 Runnble 实例的 run()方法开始执行。 线程操作的数据从传递给 Thread 构造函数的 Runnable 的特定实例处获得。 4.2 启动线程 一个新创建的线程并不自动开始运行。你必须调用它的 start()方法。例如,你可以发现上例 中第 4 行代码中的命令: t.start(); 调用 start()方法使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并 执行。这并不意味着线程就会立即运行。 4.3 线程状态 一个 Thread 对象在它的生命周期中会处于各种不同的状态。下图形象地说明了这点: 线程状态及状态转换规则具体说明如下: (1)新建状态——就绪状态: 一个新创建的线程(使用 new+Thread 构造方法创建的对象)不会自动运行,此时处于新建 New RunningRunnable Otherwise Blocked Dead Blocked in object`s wait()pool Blocked in object`s lock pool Scheduler completes run() start() sleep() or join() sleep() timeout or thread join()s or interupt() interupt() notify() Lock available synchronized() Thread states http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 255 (New)状态。当程序员显式调用线程的 start()方法时,该线程进入就绪(Runnable)状态, 也称可运行状态。进入就绪状态的线程不一定立即开始运行,因为此时计算机 CPU 可能正在运 行其它的线程。可能有多个线程同时进入就绪状态,在就绪队列中排队等候。 (2)就绪状态——运行状态: Java 运行时系统提供的线程调度器按照一定的规则进行调度,一但某个线程获得执行机会, 则立即进入运行(Running)状态、开始执行线程体代码。 (3)运行状态——阻塞状态: 处于运行状态的线程可能因某种事件的发生而进入阻塞(Blocked)状态、暂时停止执行。 例如,线程进行 I/O 操作,等待用户输入数据。当一个运行状态的线程发生阻塞时,调度器立 即调度就绪队列中的另一个线程开始运行。 (4)阻塞状态——就绪状态: 当处于阻塞状态的线程所等待的条件已经具备,例如用户输入操作已经完成时,该线程将 解除阻塞,进入就绪状态。注意,不是恢复执行,而是重新到就绪队列中去排队。 (5)运行状态——终止状态: 线程的 run()方法正常执行完毕后,其运行也就自然结束,线程进入终止(Dead)状态。 也可以在运行过程中,非正常地终止一个线程的执行,例如调用其 stop()方法。处于终止状态 的线程不能在重新运行,因此不允许在一个 Thread 对象上两次调用 start()方法。 二:多线程的编写和控制 1:多线程的创建 1.1:实现 Runnable 接口 Java 中引入线程机制的目的在于实现多线程,以提高程序的效能。这主要是通过多线程之间共 享代码和数据来实现的,例如可以使用同一个 Runnable 接口(的实现类)类型的实例构造多个线程。 示例 14-1 使用多线程 程序:TestThread2java public class TestThread2 { public static void main(String args[]) { Runner2 r = new Runner2(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); } } class Runner2 implements Runnable { public void run() { for(int i=0; i<20; i++) { http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 256 String s = Thread.currentThread().getName(); System.out.println(s + ": " + i); } } } 程序运行结果为: 程序 TestThread2.java 中创建了两个新的线程 t1 和 t2,他们共享代码——Runner2 中的 run() 方法,同时也共享数据——Runnable 类型的对象 r,两个线程在运行过程中分别操纵对象 r 调用其 run()方法。从输出结果可以看出,线程 t1 和 t2 作为独立的顺序控制流,并发地交替执行。可以想 象,如果先启动的线程因某种原因处于阻塞状态,例如等待用户键盘输入数据,CPU 会立即转而执 行其他的线程,而不必空置。 需要注意的是,这和在 main()方法中直接调用两个方法有本质的不同,那样不会出现交替的情 况,必须要前面的方法执行完才会执行后面的方法。 1.2:继承 Thread 在示例 14-1 中,直接定义了 Runnable 接口的实现类来提供线程体,这是创建线程的基本方式。 除此之外,还可以采用直接继承 Thread 类、重写其中的 run()方法并以之作为线程体的方式创建线 程,见示例 14-2 使用 sleep()方法: 程序:TestThread3.java public class TestThread3 { public static void main(String args[]){ Thread t = new Runner3(); t.start(); } } class Runner3 extends Thread { public void run() { for(int i=0; i<30; i++) { System.out.println("No. " + i); } } } 程序 TestThread3.java 与示例 10-1 中的 TestThread1.java 运行结果相同。从中可以看出,第二种 创建线程方式的一般步骤为: 1. 定义一个类继承 Thread 类,重写其中的 run()方法; 2. 创建该 Thread 子类的对象; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 257 3. 调用该对象的 start()方法,启动线程。 这种情况下,线程 t 所执行的线程体就是 t 本身的成员方法 run()。实际上,Thread 类也已经 实现了 Runnable 接口。 1.3:两种方法的比较 给定各种方法的选择,你如何决定使用哪个?每种方法都有若干优点。 实现 Runnable 的优点 从面向对象的角度来看,Thread 类是一个虚拟处理机严格的封装,因此只有当处理机 模型修改或扩展时,才应该继承类。正 因为这个原因和区别一个正在运行的线程的处理机、 代码和数据部分的意义,本教程采用了这种方法。 由于 Java 技术只允许单一继承,所以如果你已经继承了 Thread,你就不能再继承其 它任何类,例如 Applet。在某些情况下,这会使你只能采用实现 Runnable 的方法。 因为有时你必须实现 Runnable,所以你可能喜欢保持一致,并总是使用这种方法。 继承 Thread 的优点 当一个 run()方法体现在继承 Thread 类的类中,用 this 指向实际控制运行的 Thread 实例。因此,代码不再需要使用如下控制: Thread.currentThread().join(); 而可以简单地用: join(); 因为代码简单了一些,许多 Java 编程语言的程序员使用扩展 Thread 的机制。注意: 如果你采用这种方法,在你的代码生命周期的后期,单继承模型可能会给你带来困难。 2:线程调度 尽管线程变为可运行的,但它并不立即开始运行。在一个只带有一个处理机的机器上,在一个 时刻只能进行一个动作。下节描述了如果有一个以上可运行线程时,如何分配处理机。 在 Java 中,线程是抢占式的,但并不一定是分时的 (一个常见的错误是认为“抢占式”只不过 是“分时”的一种新奇的称呼而已) 。 抢占式调度模型是指可能有多个线程是可运行的,但只有一个线程在实际运行。这个线程会一 直运行,直至它不再是可运行的,或者另一个具有更高优先级的线程成为可运行的。对于后面一种 情形,低优先级线程被高优先级线程抢占了运行的机会。 下面几种情况下,当前线程会放弃 CPU,进入阻塞状态: l 线程调用了 yield(),suspend()或 sleep()方法主动放弃; l 由于当前线程进行 I/O 访问,外存读写,等待用户输入等操作,导致线程阻塞; l 为等候一个条件变量,线程调用 wait()方法; l 抢先式系统下,有高优先级的线程参与调度;时间片方式下,当前时间片用完,有同优先级的 线程参与调度。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 258 2.1:线程优先级 每个线程都有自己的优先级,通常优先级高的线程将先于优先级低的线程执行。线程的优先级 用数字来表示,范围从 1 到 10,主线程的缺省优先级是 5。其他线程的优先级默认与父线程相同, 也可以使用 Thread 类的下述线方法获得或设置线程对象的优先级: public final int getPriority(); public final void setPriority(int newPriority) 为使用方便,Thread 类还提供了几个 public static int 常量: Thread.MIN_PRIORITY = 1 Thread.MAX_PRIORITY = 10 Thread.NORM_PRIORITY = 5 示例 14-3 使用线程优先级程序:TestPriority.java public class TestPriority { public static void main(String args[]){ System.out.println("线程名\t 优先级"); Thread current = Thread.currentThread(); System.out.print(current.getName() + "\t"); System.out.println(current.getPriority()); Thread t1 = new Runner(); Thread t2 = new Runner(); Thread t3 = new Runner(); t1.setName("First"); t2.setName("Second"); t3.setName("Third"); t2.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); t3.start(); } } class Runner extends Thread { public void run() { System.out.print(this.getName() + "\t"); System.out.println(this.getPriority()); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 259 } } 程序运行结果为: 线程名 优先级 main 5 Second 10 First 5 Third 5 一个线程可能因为各种原因而不再是可运行的。线程的代码可能执行了一个 Thread.sleep()调 用,要求这个线程暂停一段固定的时间。这个线程可能在等待访问某个资源,而且在这个资源可访 问之前,这个线程无法继续运行。 所有可运行线程根据优先级保存在池中。当一个被阻塞的线程变成可运行时,它会被放回相应 的可运行池。优先级最高的非空池中的线程会得到处理机时间(被运行)。 因为 Java 线程不一定是分时的,所有你必须确保你的代码中的线程会不时地给另外一个线程运 行的机会。这可以通过在各种时间间隔中发出 sleep()调用来做到。 1.public class Xyz implements Runnable { 2.public void run() { 3.while (true) { 4.// do lots of interesting stuff 5.: 6.// Give other threads a chance 7.try { 8.Thread.sleep(10); 9.} catch (InterruptedException e) { 10.// This thread's sleep was interrupted 11.// by another thread 12.} 13.} 14.} 15.} 注意 try 和 catch 块的使用。Thread.sleep()和其它使线程暂停一段时间的方法是可中断的。 线 程可以调用另外一个线程的 interrupt() 方 法,这将向暂停的线程发出一个 InterruptedException。 注意 Thread 类的 sleep()方法对当前线程操作,因此被称作 Thread.sleep(x),它是一个静态 方法。sleep()的参数指定以毫秒为单位的线程最小休眠时间。除非线程因为中断而提早恢复执行, 否则它不会在这段时间之前恢复执行。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 260 Thread 类的另一个方法 yield(),可以用来使具有相同优先级的线程获得执行的机会。如果具 有相同优先级的其它线程是可运行的,yield()将把调用线程放到可运行池中并使另一个线程运行。 如果没有相同优先级的可运行进程,yield()什么都不做。 注意 sleep()调用会给较低优先级线程一个运行的机会。yield()方法只会给相同优先级线程一 个执行的机会。 3:线程控制 为有效地进行线程管理和状态控制,Object 类和 Thread 类中提供多个有用的方法,如表 14-1 所示: 表 14-1 线程控制基本方法。 方 法 功 能 isAlive() 判断线程是否还“活”着,即线程是否还未终止。 getPriority() 获得线程的优先级数值 setPriority() 设置线程的优先级数值 Thread.sleep() 将当前线程睡眠指定毫秒数 join() 调用某线程的该方法,将当前线程与该线程“合并”, 即等待该线程结束,再恢复当前线程的运行。 yield() 让出 CPU,当前线程进入就绪队列等待调度。 wait() 当前线程进入对象的 wait pool。 notify()/notifyAll() 唤醒对象的 wait pool 中的一个/所有等待线程。 suspend() 挂起当前线程,不提倡使用。 resume() 解除当前线程的挂起状态,不提倡使用。 stop() 终止当前线程的执行,不提倡使用。 示例 14-4 使用 sleep()方法 程序:TestSleep.java import java.awt.*; import java.util.Calendar; public class TestSleep{ public static void main(String args[]) { Frame f = new Frame("My Watch"); Label l = new Label(); f.add(l); f.setSize(100,50); f.setVisible(true); while(true){ Calendar c = Calendar.getInstance(); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 261 l.setText(c.get(Calendar.HOUR_OF_DAY) + ":" + c.get(Calendar.MINUTE) + ":" + c.get(Calendar.SECOND)); try{ Thread.sleep(1000); }catch(InterruptedException e){} } } } 本程序实现了电子时钟的功能,其图形界面如图 14-4 所示。 线程阻塞,休眠 1000 毫秒后再恢复运行时刷新显示时间。在其休眠期间,系统可以执行其他程 序或线程,以提高运行效率。 示例 使用 join()方法程序:TestJoin.java public class TestJoin { public static void main(String args[]){ MyRunner r = new MyRunner(); Thread t = new Thread(r); t.start(); try{ t.join(); }catch(InterruptedException e){ } for(int i=0;i<50;i++){ System.out.println("主线程:" + i); } } } class MyRunner implements Runnable { public void run() { for(int i=0;i<50;i++) { System.out.println("SubThread: " + i); } } 图 14-4 电子时钟 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 262 } 程序运行输出结果为: SubThread: 0 SubThread: 1 SubThread: 2 …… SubThread: 49 主线程:0 主线程:1 主线程:2 …… 主线程:49 从中可以看出,在主线程执行过程中,调用 t.join()方法导致当前线程(主线程)阻塞,直到 线程 t 运行终止后,主线程才获得执行的机会。如果多线程程序中,一个线程要用道另一个线程执 行后提供的条件,则可考虑使用 join() 方法。 3.1:创建线程和启动线程并不相同 在一个线程对新线程的 Thread 对象调用 start() 方法之前,这个新线程并没有真正开始执 行。Thread 对象在其线程真正启动之前就已经存在了,而且其线程退出之后仍然存在。这可以让您 控制或获取关于已创建的线程的信息,即使线程还没有启动或已经完成了。 通常在构造器中通过 start() 启动线程并不是好主意。这样做,会把部分构造的对象暴露给新 的线程。如果对象拥有一个线程,那么它应该提供一个启动该线程的 start() 或 init() 方法,而 不是从构造器中启动它。 3.2:结束线程 线程会以以下三种方式之一结束: · 线程到达其 run() 方法的末尾。 · 线程抛出一个未捕获到的 Exception 或 Error。 · 另一个线程调用一个弃用的 stop() 方法。弃用是指这些方法仍然存在,但是您不应该在新 代码中使用它们,并且应该尽量从现有代码中除去它们。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 263 当 Java 程序中的所有线程都完成时,程序就退出了。 3.3:加入线程 Thread API 包含了等待另一个线程完成的方法:join() 方法。当调用 Thread.join() 时,调 用线程将阻塞,直到目标线程完成为止。 Thread.join() 通常由使用线程的程序使用,以将大问题划分成许多小问题,每个小问题分配 一个线程。本章结尾处的示例创建了十个线程,启动它们,然后使用 Thread.join() 等待它们全部 完成。 3.4:调度 除了何时使用 Thread.join() 和 Object.wait() 外,线程调度和执行的计时是不确定的。如 果两个线程同时运行,而且都不等待,您必须假设在任何两个指令之间,其它线程都可以运行并修 改程序变量。如果线程要访问其它线程可以看见的变量,如从静态字段(全局变量)直接或间接引 用的数据,则必须使用同步以确保数据一致性。 在以下的简单示例中,我们将创建并启动两个线程,每个线程都打印两行到 System.out: 我们并不知道这些行按什么顺序执行,只知道“1”在“2”之前打印,以及“A”在“B”之前 打印。输出可能是以下结果中的任何一种: public class TwoThreads { public static class Thread1 extends Thread { public void run() { System.out.println("A"); System.out.println("B"); } } public static class Thread2 extends Thread { public void run() { System.out.println("1"); System.out.println("2"); } } public static void main(String[] args) { new Thread1().start(); new Thread2().start(); } } http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 264 · 1 2 A B · 1 A 2 B · 1 A B 2 · A 1 2 B · A 1 B 2 · A B 1 2 不仅不同机器之间的结果可能不同,而且在同一机器上多次运行同一程序也可能生成不同结果。 永远不要假设一个线程会在另一个线程之前执行某些操作,除非您已经使用了同步以强制一个特定 的执行顺序。 3.5:休眠 Thread API 包含了一个 sleep() 方法,它将使当前线程进入等待状态,直到过了一段指定时 间,或者直到另一个线程对当前线程的 Thread 对象调用了 Thread.interrupt(),从而中断了线程。 当过了指定时间后,线程又将变成可运行的,并且回到调度程序的可运行线程队列中。 如 果 线 程 是 由 对 Thread.interrupt() 的 调 用 而 中 断 的,那么休眠的线程会抛出 InterruptedException,这样线程就知道它是由中断唤醒的,就不必查看计时器是否过期。 Thread.yield() 方法就象 Thread.sleep() 一样,但它并不引起休眠,而只是暂停当前线程片 刻,这样其它线程就可以运行了。在大多数实现中,当较高优先级的线程调用 Thread.yield() 时, 较低优先级的线程就不会运行。 CalculatePrimes 示例使用了一个后台线程计算素数,然后休眠十秒钟。当计时器过期后,它 就会设置一个标志,表示已经过了十秒。 3.6:守护程序线程 我们提到过当 Java 程序的所有线程都完成时,该程序就退出,但这并不完全正确。隐藏的系 统线程,如垃圾收集线程和由 JVM 创建的其它线程会怎么样?我们没有办法停止这些线程。如果那 些线程正在运行,那么 Java 程序怎么退出呢? 这些系统线程称作守护程序线程。Java 程序实际上是在它的所有非守护程序线程完成后退出 的。 任何线程都可以变成守护程序线程。 三:多线程的同步 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 265 1: 临界资源问题 多个线程间共享的数据称为共享资源或临界资源,由于是线程调度器负责线程的调度,程序员 无法精确控制多线程的交替顺序。这种情况下,多线程对临界资源的访问有时会导致数据的不一致 性。 这个问题的产生可以看下面的例子: 想象一个表示栈的类。这个类最初可能象下面那样: 1.public class MyStack { 2. 3.int idx = 0; 4.char [] data = new char[6]; 5. 6.public void push(char c) { 7.data[idx] = c; 8.idx++; 9.} 10. 11.public char pop() { 12.idx--; 13.return data[idx]; 14.} 15.} 注意这个类没有处理栈的上溢和下溢,所以栈的容量是相当有限的。这些方面和本讨论无关。 这个模型的行为要求索引值包含栈中下一个空单元的数组下标。“ 先 进 后出”方法用来产生这个 信息。 现在想象两个线程都有对这个类里的一个单一实例的引用。一个线程将数据推入栈,而另一个 线程,或多或少独立地,将数据弹出栈。通常看来,数据将会正确地被加入或移走。然而,这存在 着潜在的问题。 假设线程 a 正在添加字符,而线程 b 正在移走字符。线程 a 已经放入了一个字符,但还没有使 下标加 1。因为某个原因,这个线程被剥夺(运行的机会)。这时,对象所表示的数据模型是不一致 的。 buffer |p|q|r| | | | idx = 2 ^ 特别地,一致性会要求 idx=3,或者还没有添加字符。 如果线程 a 恢复运行,那就可能不造成破坏,但假设线程 b 正等待移走一个字符。在线程 a 等 待另一个运行的机会时,线程 b 正在等待移走一个字符的机会。 pop()方法所指向的条目存在不一致的数据,然而 pop 方法要将下标值减 1。 buffer |p|q|r| | | | idx = 1 ^ 这实际上将忽略了字符“r”。此后,它将返回字符“q”。至此,从其行为来看,就好象没有 推入字母“r”,所以很难说是否存在问题。现在看一看如果线程 a 继续运行,会发生什么。 线程 a 从上次中断的地方开始运行,即在 push()方法中,它将使下标值加 1。现在你可以看到: buffer |p|q|r| | | | http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 266 idx = 2 ^ 注意这个配置隐含了:“ q ”是有效的,而含有“r ”的单元是下一个空单元。也就是说,读取“q” 时,它就象被两次推入了栈,而字母“r”则永远不会出现。 这是一个当多线程共享数据时会经常发生的问题的一个简单范例。需要有机制来保证共享数据 在任何线程使用它完成某一特定任务之前是一致的。 2 互斥锁 在 Java 语言中,为保证共享数据操作的完整性,引入了对象互斥锁的概念。每个对象都对应于 一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。这好 似学校里专用教室的使用规则:为保证高年级学生顺利进行课程设计,将个别教室规定为“专教” 并在门上标明。任意时刻,只能有一个班级使用该教室,直至他们课程设计活动结束。即使中午或 周末休息,也不必担心教室被其他的班级占用,或黑板上的文字被他人擦除。显然,这种做法会降 低教室的利用率,因此除特别标明以外,教室应该是公用的,哪个班级都可以使用,由教务处负责 安排调度。 Java 对象默认也是可以被多线程共用的,在需要时才启动“互斥”机制,成为专用对象。关键 字 synchronized 来与对象的互斥锁联系。当某个对象用 synchronized 修饰时,表明该对象已启动 “互斥”机制,在任一时刻只能由一个线程访问。即使该线程出现阻塞,该对象的被锁定状态也不 会解除,其他线程仍不能访问该对象。 synchronized 关键字的使用方式有两种: l 用在对象前面限制一段代码的执行 l 用在方法声明中,表示整个方法为同步方法。 看一看下面修改过的代码片断: public void push(char c) { synchronized(this) { data[idx] = c; idx++; } } 当线程运行到 synchronized 语句,它检查作为参数传递的对象,并在继续执行之前试图从对象 获得锁标志。 2.1:对象锁标志 Thread before synchronized(this) public void push(char c) { synchronized (this) { Object this http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 267 意识到它自身并没有保护数据是很重要的。因为如果同一个对象的 pop()方法没有受到 synchronized 的影响,且 pop()是由另一个线程调用的,那么仍然存在破坏 data 的一致性的危险。 如果要使锁有效,所有存取共享数据的方法必须在同一把锁上同步。 下图显示了如果 pop()受到 synchronized 的影响,且另一个线程在原线程持有那个对象的锁时 试图执行 pop()方法时所发生的事情: 当线程试图执行synchronized(this)语句时,它试图从 this 对象获取锁标志。由于得不到标志,所 以线程不能继续运行。然后,线程加入到与那个对象锁相关联的等待线程池中。当标志返回给对象 时,某个等待这个标志的线程将得到这把锁并继续运行。 Thread after synchronized(this) public void push(char c) { synchronized (this) { data[idx] = c; idx++; } } Code or behavior Data or state Object this http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 268 2.2 释放锁标志 由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行,所以让持有锁标志的线程在 不再需要的时候返回标志是很重要的。 锁标志将自动返回给它的对象。持有锁标志的线程执行到 synchronized()代码块末尾时将释放 锁。Java 技术特别注意了保证即使出现中断或异常而使得执行流跳出 synchronized()代码块,锁也 会自动返回。此外,如果一个线程对同一个对象两次发出 synchronized 调用,则在跳出最外层的块 时,标志会正确地释放,而最内层的将被忽略。 这些规则使得与其它系统中的等价功能相比,管理同步块的使用简单了很多。 2.3 synchronized――放在一起 正如所暗示的那样,只有当所有对易碎数据的存取位于同步块内,synchronized()才会发生作 用。 所有由 synchronized 块保护的易碎数据应当标记为 private。考虑来自对象的易碎部分的数据 的可存取性。如果它们不被标记为 private,则它们可以由位于类定义之外的代码存取。这样,你 必须确信其他程序员不会省略必需的保护。 一个方法,如果它全部属于与这个实例同步的块,它可以把 synchronized 关键字放到它的头部。 下面两段代码是等价的: public void push(char c) { synchronized(this) { : : } } public synchronized void push(char c) { : : } 为什么使用另外一种技术? 如果你把 synchronized 作为一种修饰符,那么整个块就成为一个同步块。这可能会导致不必要 地持有锁标志很长时间,因而是低效的。 然而,以这种方式来标记方法可以使方法的用户由 javadoc 产生的文档了解到:正在同步。这对 于设计时避免死锁(将在下一节讨论)是很重要的。注意 javadoc 文档生成器将 synchronized 关键字传 播到文档文件中,但它不能为在方法块内的 synchronized(this)做到这点。 3 线程的死锁 如果程序中有多个线程竞争多个资源,就可能会产生死锁。当一个线程等待由另一个线程持有 synchronized-放在一起 l 所有对易碎数据的存取应当同步。 l 由 synchronized 保护的易碎数据应当是 private 的。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 269 的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。在这种情况下,除非另一个已 经执行到 synchronized 块的末尾,否则没有一个线程能继续执行。由于没有一个线程能继续执行, 所以没有一个线程能执行到块的末尾。 Java 技术不监测也不试图避免这种情况。因而保证不发生死锁就成了程序员的责任。避免死锁 的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序。按 照 与 获取 相 反的 次序释放锁。 讨论下面的情况,两个线程 A、B 用到同一个对象 s(s 为共享资源),且线程 A 在执行中要用到 B 运行后所创造的条件(例如,使用 B.join()语句与 B 建立同步)。在这种前提下 A 先开始运行,进 入同步语句块后,对象 s 被锁定,接着线程 A 因等待 B 运行结束这一条件而进入阻塞状态。于是线 程 B 开始运行,但因无法访问对象 s(已在 A 中被锁定),线程 B 也进入阻塞状态,等待 s 被线程 A 解除锁定。最终的结果是,两个线程互相等待,都无法运行,这种状态称为线程的死锁。 要避免死锁,应该确保在获取多个锁时,在所有的线程中都以相同的顺序获取锁。 4 wait()和 notify()方法 使用 synchronized 关键字锁定 Java 对象,就好似规定某个教室为专用教室一样,不可避免的 会降低程序效率。 为了实现线程阻塞时释放其锁定的共享资源,以给其他的线程提供运行的机会。java.lang 包 中定义了几个有用的方法:wait(), notify(), notifyAll()。 在同步方法(语句块)中,被锁定的对象可以调用 wait()方法,这将导致当前线程被阻塞并放 弃该对象的互斥锁,即解除了 wait()方法的当前对象的锁定状态,其他的线程就有机会访问该对象。 因调用了 wait()方法而阻塞的线程,将被加入一个特殊的对象等待队列中,直到 调 用该 wait()方法 的对象在其他的线程中调用了 notify()或 notifyAll()方法,这种等待才可能解除。 如果有多个线程因调用了 wait()方法而在等待同一个对象,则应使用该对象在其他线程中调用 notifyAll()方法,以使等待队列上的所有线程离开阻塞状态。而 notify()方法每次运行只能唤醒 等待队列中一个线程,至于是哪一个被唤醒则由线程调度器决定,程序员是无法控制的。引入了 wait()/notify()方法后线程的状态转换情况如图所示: 经常创建不同的线程来执行不相关的任务。然而,有时它们所执行的任务是有某种联系的,为 此必须编写使它们交互的程序。 4.1 场景 把你自己和出租车司机当作两个线程。你需要出租车司机带你到终点,而出租车司机需要为乘 客服务来获得车费。所以,你们两者都有一个任务。 4.2 问题 你希望坐到出租车里,舒服地休息,直到出租车司机告诉你已经到达终点。如果每 2 秒就问一 下“我们到了哪里?”, 这 对 出 租车司机和你都会是很烦的。出租车司机想睡在出租车里,直到一个 乘客想到另外一个地方去。出租车司机不想为了查看是否有乘客的到来而每 5 分钟就醒来一次。所 以,两个线程都想用一种尽量轻松的方式来达到它们的目的。 4.3 解决方案 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 270 出租车司机和你都想用某种方式进行通信。当你正忙着走向出租车站时,司机正在车中安睡。 当你告诉司机你想坐他的车时,司机醒来并开始驾驶,然后你开始等待并休息。到达终点时,司机 会通知你,所以你必须继续你的任务,即走出出租车,然后去工作。出租车司机又开始等待和休息, 直到下一个乘客的到来。 4.4 wait()和 notify() java.lang.Object 类中提供了两个用于线程通信的方法:wait()和 notify()。如果线程对一个 同步对象 x 发出一个 wait()调用,该线程会暂停执行,直到另一个线程对同一个同步对象 x 也发出 一个 wait()调用。 在上个场景中,在 车 中 等 待 的 出 租车司机被翻译成执行 cab.wait()调用的“ 出 租车司机”线程, 而你使用出租车的需求被翻译成执行 cab.notify()调用的“你”线程。 为了让线程对一个对象调用 wait()或 notify(),线程必须锁定那个特定的对象。也就是说,只 能在它们被调用的实例的同步块内使用 wait()和 notify()。对于这个实例来说,需要一个以 synchronized(cab)开始的块来允许执行 cab.wait()和 cab.notify()调用。 关于池 当线程执行包含对一个特定对象执行 wait()调用的同步代码时,那个线程被放到与那个对象相 关的等待池中。此外,调用 wait()的线程自动释放对象的锁标志。可以调用不同的 wait(): wait() 或 wait(long timeout); 对一个特定对象执行 notify()调用时,将从对象的等待池中移走一个任意的线程,并放到锁池 中,那里的对象一直在等待,直到可以获得对象的锁标记。 notifyAll()方法将从等待池中移走所 有等待那个对象的线程并放到锁池中。只有锁池中的线程能获取对象的锁标记,锁标记允许线程从 上次因调用 wait()而中断的地方开始继续运行。 在许多实现了 wait()/notify()机制的系统中,醒来的线程必定是那个等待时间最长的线程。 然而,在 Java 技术中,并不保证这点。 注意,不管是否有线程在等待,都可以调用 notify()。如果对一个对象调用 notify()方法,而 在这个对象的锁标记等待池中并没有阻塞的线程,那 么 notify()调用将不起任何作用。对 notify() 的调用不会被存储。 4.5 同步的监视模型 协调两个需要存取公共数据的线程可能会变得非常复杂。你必须非常小心,以保证可能有另一 个线程存取数据时,共享数据的状态是一致的。因为线程不能在其他线程在等待这把锁的时候释放 线程交互 l wait()和 notify() l 池 l 等待池 l 锁池 同步的监视模型 l 使共享数据处于一致的状态 l 保证程序不死锁 l 不要将期待不同通知的线程放到同一个等待 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 271 合适的锁,所以你必须保证你的程序不发生死锁, 在出租车范例中,代码依赖于一个同步对象――出租车,在其上执行 wait()和 notify()。如果有 任何人在等待一辆公共汽车,你就需要一个独立的公共汽车对象,在它上面施用 notify()。记住,在 同一个等待池中的所有线程都因来自等待池的控制对象的通知而满足。永远不要设计这样的程序: 把线程放在同一个等待池中,但它们却在等待不同条件的通知。 4.6 放在一起 下面将给出一个线程交互的实例,它说明了如何使用 wait()和 notify()方法来解决一个经典的 生产者-消费者问题。 我们先看一下栈对象的大致情况和要存取栈的线程的细节。然后再看一下栈的详情,以及基于 栈的状态来保护栈数据和实现线程通信的机制。 实例中的栈类称为 SyncStack,用来与核心 java.util.Stack 相区别,它提供了如下公共的 API: public synchronized void push(char c); public synchronized char pop(); 生产者线程运行如下方法: public void run() { char c; for (int i = 0; i < 200; i++) { c = (char)(Math.random() * 26 + 'A'); theStack.push(c); System.out.println("Producer" + num + ": " + c); try { Thread.sleep((int)(Math.random() * 300)); } catch (InterruptedException e) { // ignore it } } } 这将产生 200 个随机的大写字母并将其推入栈中,每个推入操作之间有 0 到 300 毫秒的随机延 迟。每个被推入的字符将显示到控制台上,同时还显示正在执行的生产者线程的标识。 消费者 消费者线程运行如下方法: public void run() { char c; for (int i = 0; i < 200; i++) { c = theStack.pop(); System.out.println(" Consumer" + num + ": " + c); try { Thread.sleep((int)(Math.random() * 300)); } catch (InterruptedException e) { // ignore it } http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 272 } } 上面这个程序从栈中取出 200 个字符,每两个取出操作的尝试之间有 0 到 300 毫秒的随机延迟。 每个被弹出的字符将显示在控制台上,同时还显示正在执行的消费者线程的标识。 现在考虑栈类的构造。你将使用 Vector 类创建一个栈,它看上去有无限大的空间。按照这种设 计,你的线程只要在栈是否为空的基础上进行通信即可。 SyncStack 类 一个新构造的 SyncStack 对象的缓冲应当为空。下面这段代码用来构造你的类: public class SyncStack { private Vector buffer = new Vector(400,200); public synchronized char pop() { } public synchronized void push(char c) { } } 请注意,其中没有任何构造函数。包含有一个构造函数是一种相当好的风格,但为了保持简洁, 这里省略了构造函数。 现在考虑 push()和 pop()方法。为了保护共享缓冲,它们必须均为 synchronized。此外,如果 要执行 pop()方法时栈为空,则正在执行的线程必须等待。若执行 push()方法后栈不再为空,正在 等待的线程将会得到通知。 pop()方法如下: public synchronized char pop() { char c; while (buffer.size() == 0) { try { this.wait(); } catch (InterruptedException e) { // ignore it } } c = ((Character)buffer.remove(buffer.size()-1)).charValue(); return c; } 注意这里显式地调用了栈对象的 wait(),这说明了如何对一个特定对象进行同步。如果栈为空, 则不会弹出任何数据,所以一个线程必须等到栈不再为空时才能弹出数据。 由于一个 interrupt()的调用可能结束线程的等待阶段,所以 wait()调用被放在一个 try/catch 块中。对于本例,wait()还必须放在一个循环中。如果 wait()被中断,而栈仍为空,则线程必须继 续等待。 栈的 pop()方法为 synchronized 是出于两个原因。首先,将字符从栈中弹出影响了共享数据 buffer。其次,this.wait()的调用必须位于关于栈对象的一个同步块中,这个块由 this 表示。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 273 你将看到 push()方法如何使用 this.notify()方法将一个线程从栈对象的等待池中释放出来。 一旦线程被释放并可随后再次获得栈的锁,该线程就可以继续执行 pop()完成从栈缓冲区中移走字 符任务的代码。 注 - 在 pop()中,wait()方法在对栈的共享数据作修改之前被调用。这是非常关键的一点,因为 在对象锁被释放和线程继续执行改变栈数据的代码之前,数据必须保持一致的状态。你必须使你所 设计的代码满足这样的假设:在进入影响数据的代码时,共享数据是处于一致的状态。 需要考虑的另一点是错误检查。你可能已经注意到没有显式的代码来保证栈不发生下溢。这不 是必需的,因为从栈中移走字符的唯一方法是通过 pop()方法,而这个方法导致正在执行的线程在 没有字符的时候会进入 wait()状态。因此,错误检查不是必要的。push()在影响共享缓冲方面与此 类似,因此也必须被同步。此外,由于 push()将一个字符加入缓冲区,所以由它负责通知正在等待 非空栈的线程。这个通知的完成与栈对象有关。 push()方法如下: public synchronized void push(char c) { this.notify(); Character charObj = new Character(c); buffer.addElement(charObj); } 对 this.notify()的调用将释放一个因栈空而调用 wait()的单个线程。在共享数据发生真正的 改变之前调用 notify()不会产生任何结果。只有退出该 synchronized 块后,才会释放对象的锁, 所以当栈数据在被改变时,正在等待锁的线程不会获得这个锁。 4.7 SyncStack 范例 完整的代码 现在,生产者、消费者和栈代码必须组装成一个完整的类。还需要一个测试工具将这些代码集 成为一体。特别要注意,SyncTest 是如何只创建一个由所有线程共享的栈对象的。 SyncTest.java 1.package mod14; 2.public class SyncTest { 3.public static void main(String args[]) { 4. 5.SyncStack stack = new SyncStack(); 6. 7.Producer p1 = new Producer(stack); 8.Thread prodT1= new Thread(p1); 9.prodT1.start(); 10. 11.Producer p2 = new Producer(stack); 12.Thread prodT2= new Thread(p2); 13.prodT2.start(); 14. http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 274 15.Consumer c1 = new Consumer(stack); 16.Thread consT1 = new Thread(c1); 17.consT1.start(); 18. 19.Consumer c2 = new Consumer(stack); 20.Thread consT2 = new Thread(c2); 21.constT2.start(); 22.} 23.} Producer.java 1.package mod14; 2.public class Producer implements Runnable { 3.private SyncStack theStack; 4.private int num; 5.private static int counter = 1; 6.public Producer (SyncStack s) { 7.theStack = s; 8.num = counter++; 9.} 10. 11.public void run() { 12.char c; 13. 14.for (int i = 0; i < 200; i++) { 15.c = (char)(Math.random() * 26 + `A'); 16.theStack.push(c); 17.System.out.println("Producer" + num + ": " + c); 18.try { 19.Thread.sleep((int)(Math.random() * 300)); 20.} catch (InterruptedException e) { 21.// ignore it 22.} 23.} 24.} 25.} Consumer.java 1.package mod14; 2.public class Consumer implements Runnable { 3.private SyncStack theStack; 4.private int num; 5.private static int counter = 1; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 275 6. 7.public Consumer (SyncStack s) { 8.theStack = s; 9.num = counter++; 10.} 11. 12.public void run() { 13.char c; 14.for (int i=0; i < 200; i++) { 15.c = theStack.pop(); 16.System.out.println("Consumer" + num + ": " + c); 17.try { 18.Thread.sleep((int)(Math.random() * 300)); 19.} catch (InterruptedException e) { 20.// ignore it 21.} 22.} 23.} 24.} SyncStack.java 1.package mod14; 2. 3.import java.util.Vector; 4. 5.public class SyncStack { 6.private Vector buffer = new Vector(400,200); 7. 8.public synchronized char pop() { 9.char c; 10. 11.while (buffer.size() == 0) { 12.try { 13.this.wait(); 14.} catch (InterruptedException e) { 15.// ignore it 16.} 17.} 18. 19. c = ((Character)buffer.remove(buffer.size()- 1).charValue(); 20.return c; 21.} 22.public synchronized void push(char c) { 23.this.notify(); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 276 24.Character charObj = new Character(c); 25.buffer.addelement(charObj); 26.} 27.} 运行javamodB.SyncTest 的输出如下。请注意每次运行线程代码时,结果都会有所不同。 Producer2: F Consumer1: F Producer2: K Consumer2: K Producer2: T Producer1: N Producer1: V Consumer2: V Consumer1: N Producer2: V Producer2: U Consumer2: U Consumer2: V Producer1: F Consumer1: F Producer2: M Consumer2: M Consumer2: T Java 语言包含了内置在语言中的功能强大的线程工具。您可以将线程工具用于: · 增加 GUI 应用程序的响应速度 · 利用多处理器系统 · 当程序有多个独立实体时,简化程序逻辑 · 在不阻塞整个程序的情况下,执行阻塞 I/O 当使用多个线程时,必须谨慎,遵循在线程之间共享数据的规则,我们将在共享对数据的访问 中讨论这些规则。所有这些规则归结为一条基本原则:不要忘了同步。 5:同步的应用 5.1 可见性同步 跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就 必须使用 synchronized 以确保一个线程可以看见另一个线程做的更改。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 277 可见性同步的基本规则是在以下情况中必须同步: · 读取上一次可能是由另一个线程写入的变量 · 写入下一次可能由另一个线程读取的变量 5.2 用于一致性的同步 除了用于可见性的同步,从应用程序角度看,您还必须用同步来确保一致性得到了维护。当修 改多个相关值时,您想要其它线程原子地看到这组更改 — 要么看到全部更改,要么什么也看不到。 这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的 数据项的链)。 考虑以下示例,它实现了一个简单(但不是线程安全的)的整数堆栈: public class UnsafeStack { public int top = 0; public int[] values = new int[1000]; public void push(int n) { values[top++] = n; } public int pop() { return values[--top]; } } 如果多个线程试图同时使用这个类,会发生什么?这可能是个灾难。因为没有同步,多个线程 可以同时执行 push() 和 pop()。如果一个线程调用 push(),而另一个线程正好在递增了 top 并 要把它用作 values 的下标之间调用 push(),会发生什么?结果,这两个线程会把它们的新值存储 到相同的位置!当多个线程依赖于数据值之间的已知关系,但没有确保只有一个线程可以在给定时 间操作那些值时,可能会发生许多形式的数据损坏,而这只是其中之一。 对于这种情况,补救办法很简单:同 步 push() 和 pop() 这两者,您将防止线程执行相互干扰。 请注意,需要使用 synchronized 来确保 top 和 values 之间的关系保持一致。 5.3 不变性和 final 字段 许多 Java 类,包括 String、Integer 和 BigDecimal,都是不可改变的:一旦构造之后,它 们的状态就永远不会更改。如果某个类的所有字段都被声明成 final,那 么 这个类就是不可改变的。 (实际上,许多不可改变的类都有非 final 字段,用于高速缓存以前计算的方法结果,如 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 278 String.hashCode(),但调用者看不到这些字段。) 不可改变的类使并发编程变得非常简单。因为不能更改它们的字段,所以就不需要担心把状态 的更改从一个线程传递到另一个线程。在正确构造了对象之后,可以把它看作是常量。 同样,final 字段对于线程也更友好。因为 final 字段在初始化之后,它们的值就不能更改, 所以当在线程之间共享 final 字段时,不需要担心同步访问 5.4 什么时候不需要同步 在某些情况中,您不必用同步来将数据从一个线程传递到另一个,因为 JVM 已经隐含地为您执 行同步。这些情况包括: · 由静态初始化器(在静态字段上或 static{} 块中的初始化器)初始化数据时 · 访问 final 字段时 · 在创建线程之前创建对象时 · 线程可以看见它将要处理的对象时 5.5 性能考虑事项 关于同步的性能代价有许多说法 — 其中有许多是错的。同步,尤其是争用的同步,确实有性 能问题,但 这 些问题并没有象人们普遍怀疑的那么大。 许多人都使用别出心裁但不起作用的技巧以 试图避免必须使用同步,但最终都陷入了麻烦。一个典型的示例是双重检查锁定模式。这种看似无 害的结构据说可以避免公共代码路径上的同步,但却令人费解地失败了,而且所有试图修正它的尝 试也失败了。 在编写并发代码时,除非看到性能问题的确凿证据,否则不要过多考虑性能。瓶颈往往出现在 我们最不会怀疑的地方。投机性地优化一个也许最终根本不会成为性能问题的代码路径 — 以程序 正确性为代价 — 是一桩赔本的生意。 5.6 同步准则 当编写 synchronized 块时,有几个简单的准则可以遵循,这些准则在避免死锁和性能危险的 风险方面大有帮助: 使代码块保持简短。Synchronized 块应该简短 — 在保证相关数据操作的完整性的同时,尽量 简短。把不随线程变化的预处理和后处理移出 synchronized 块。 不 要阻塞。不要在 synchronized 块 或 方 法中调用可能引起阻塞的方法,如 InputStream.read()。 在持有锁的时候,不要对其它对象调用方法。这听起来可能有些极端,但它消除了最常见的死 锁源头. 6:线程有关的 API http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 279 除了使用轮询(它可能消耗大量 CPU 资源,而且具有计时不精确的特征),Object 类还包括一 些方法,可以让线程相互通知事件的发生。 Object 类定义了 wait()、notify() 和 notifyAll() 方 法。要执行这些方法,必须拥有相关对象的锁。 Wait() 会 让 调 用 线 程 休 眠 ,直到用 Thread.interrupt() 中断它、过了指定的时间、或者另一个线程用 notify() 或 notifyAll() 唤 醒它。 当对某个对象调用 notify() 时,如果有任何线程正在通过 wait() 等待该对象,那么就会 唤醒其中一个线程。当对某个对象调用 notifyAll() 时,会唤醒所有正在等待该对象的线程。 这些方法是更复杂的锁定、排队和并发性代码的构件。但是,notify() 和 notifyAll() 的使用很 复杂。尤其是,使用 notify() 来代替 notifyAll() 是有风险的。除非您确实知道正在做什么,否 则就使用 notifyAll()。 与其使用 wait() 和 notify() 来编写您自己的调度程序、线程池、队列和锁,倒不如使用 util.concurrent 包,这是一个被广泛使用的开放源码工具箱,里面都是有用的并发性实用程序。 JDK 1.5 将包括 java.util.concurrent 包;它的许多类都派生自 util.concurrent。 6.1:线程优先级 Thread API 让您可以将执行优先级与每个线程关联起来。但是,这些优先级如何映射到底层操 作系统调度程序取决于实现。在某些实现中,多个 — 甚至全部 — 优先级可能被映射成相同的底 层操作系统优先级。 在遇到诸如死锁、资源匮乏或其它意外的调度特征问题时,许多人都想要调整线程优先级。但 是,通常这样只会把问题移到别的地方。大多数程序应该完全避免更改线程优先级。 6.2:线程组 ThreadGroup 类原本旨在用于把线程集合构造成组。但是,结果证明 ThreadGroup 并没有那样 有用。您最好只使用 Thread 中的等价方法。 ThreadGroup 确实提供了一个有用的功能部件(Thread 中目前还没有):uncaughtException() 方 法。线程组中的某个线程由于抛出了未捕获的异常而退出时,会调用 ThreadGroup.uncaughtException() 方法。这就让您有机会关闭系统、将一条消息写到日志文件或 者重新启动失败的服务 6.3:SwingUtilities 虽然 SwingUtilities 类不是 Thread API 的一部分,但还是值得简单提一下。 正如前面提到的,Swing 应用程序有一个 UI 线程(有时叫称为事件线程),所有 UI 活动都必 须在这个线程中发生。有时,另一个线程也许想要更新屏幕上某样东西的外观,或者触发 Swing 对 象上的一个事件。 SwingUtilities.invokeLater() 方法可以让您将 Runnable 对象传送给它,并且在事件线程中 执 行 指 定 的 Runnable 。 它 的同类 invokeAndWait() 会 在 事 件 线 程 中 调 用 Runnable ,但 invokeAndWait() 会阻塞,直到 Runnable 完成执行之后。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 280 void showHelloThereDialog() throws Exception { Runnable showModalDialog = new Runnable() { public void run() { JOptionPane.showMessageDialog(myMainFrame, "Hello There"); } }; SwingUtilities.invokeLater(showModalDialog); } 对于 AWT 应用程序,java.awt.EventQueue 还提供了 invokeLater() 和 invokeAndWait()。 6.4:suspend()和 resume()方法 JDK1.2 中不赞成使用 suspend()和 resume()方法。resume()方法的唯一作用就是恢复被挂起的 线程。所以,如果没有 suspend(),resume()也就没有存在的必要。从设计的角度来看,有两个原 因使 suspend()非常危险:它容易产生死锁;它允许一个线程控制另一个线程代码的执行。下面将 分别介绍这两种危险。 假设有两个线程:threadA 和 threadB。当正在执行它的代码时,threadB 获得一个对象的锁, 然后继续它的任务。现在 threadA 的执行代码调用 threadB.suspend(),这将使 threadB 停止执行 它的代码。 如果 threadB.suspend()没有使 threadB 释放它所持有的锁,就会发生死锁。如果调用 threadB.resume()的线程需要 threadB 仍持有的锁,这两个线程就会陷入死锁。 假设 threadA 调用 threadB.suspend()。如果 threadB 被挂起时 threadA 获得控制,那 么 threadB 就永远得不到机会来进行清除工作,例如使它正在操作的共享数据处于稳定状态。为了安全起见, 只有 threadB 才可以决定何时停止它自己的代码。 你应该使用对同步对象调用 wait()和 notify()的机制来代替 suspend()和 resume()进行线程控 制。这种方法是通过执行 wait()调用来强制线程决定何时“挂起”自己。这使得同步对象的锁被自 动释放,并给予线程一个在调用 wait()之前稳定任何数据的机会。 6.5: stop()方法 suspend()和 resume()方法 l JDK1.2 不赞成使用它们 l 应当用 wait()和 notify()来代替它们 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 281 stop()方法的情形是类似的,但 结 果有 所不同。如果一个线程在持有一个对象锁的时候被停止, 它将在终止之前释放它持有的锁。这避免了前面所讨论的死锁问题,但它又引入了其他问题。 在前面的范例中,如果线程在已将字符加入栈但还没有使下标值加 1 之后被停止,你在释放锁 的时候会得到一个不一致的栈结构。 总会有一些关键操作需要不可分割地执行,而且在线程执行这些操作时被停止就会破坏操作的 不可分割性。 一个关于停止线程的独立而又重要的问题涉及线程的总体设计策略。创建线程来执行某个特定 作业,并存活于整个程序的生命周期。换言之,你不会这样来设计程序:随意地创建和处理线程, 或创建无数个对话框或 socket 端点。每个线程都会消耗系统资源,而系统资源并不是无限的。这并 不是暗示一个线程必须连续执行;它只是简单地意味着应当使用合适而安全的 wait()和 notify() 机制来控制线程。 6.6: 合适的线程控制 既然你已经知道如何来设计具有良好行为的线程,并使用 wait()和 notify()进行通信,而不需 要再使用 suspend()和 stop(),那就可以考察下面的代码。注意:其中的 run()方法保证了在执行 暂停或终止之前,共享数据处于一致的状态,这是非常重要的。 1.public class ControlledThread extends Thread { 2.static final int SUSP=1; 3.static final int STOP=2; 4.static final int RUN=0; 5.private int state = RUN; 6. 7.public synchronized void setState( int s){ 8.state = s; 9.if (s == RUN) 10.notify(); 11.} 12. 13.public synchronized boolean checkState() { 14.while(state == SUSP) { 15.try { 16.wait(); 17.} catch (InterruptedException e) { } 18.} 19.if (state == STOP){ 20.return false; 21.} stop()方法 l 在终止前释放锁。 l 可能使共享数据处于不一致的状态。 l 应当用 wait()和 notify()来代替它们 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 282 22.return true; 23.} 24. 25.public void run() { 26.while(true) { 27.doSomething(); 28.// be sure shared data is in 29.// consistent state in case the 30.// thread is waited or marked for 31.// exiting from run(). 32.if (!checkState()) 33.break; 34.} 35.}//of run 36.}//of producer 一个要挂起、恢复或终止生产者线程的线程用合适的值来调用生产者线程的 setState()方法。 当生产者线程确定进行上述操作是安全的时候,它会挂起自己(通过使用 wait()方法)或者停止自己 (通过退出 run()方法)。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 283 练习实践 本章的内容为多线程,实践重点: l 多线程的含义与应用 程序 1 多线程基础 需求:建立两个线程,各显示 0~500 的数。 目标: 1、 线程的概念; 2、 线程的创建; 3、 理解线程与进程的区别; 4、 线程的使用。 程序: package com.useful.java.part5; public class TestThread extends Thread { int intNumberThread = 0; int intNumberThreadMain = 0; int intTotal = 500; public TestThread() { } public void run(){ while(true){ System.out.println("Thread : " + intNumberThread); intNumberThread++; if(intNumberThread > intTotal){ return; } } } public static void main(String[] args) { TestThread testThread1 = new TestThread(); testThread1.start(); while(testThread1.intNumberThreadMain < testThread1.intTotal){ System.out.println("Main() -- : " + testThread1.intNumberThreadMain); testThread1.intNumberThreadMain++; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 284 } } } 说明: 1、 创建的常用方法之一就是继承 Thread,然后覆盖 run(); 2、 线程的执行无先后之分; 3、 运行情况如下图: 程序 2 线程的睡眠 需求:在线程应用 sleep,让线程执行时进行一段时间的睡眠。 目标: 1、 sleep 的使用; 2、 多线程应用。 程序: //: TestMultiThreadSleep.java package com.useful.java.part5; public class TestMultiThreadSleep extends Thread{ int intNumberThread = 0; int intNumberThreadMain = 0; int intTatol = 500; public TestMultiThreadSleep() { } public void run(){ http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 285 while(true){ System.out.println("Thread : " + intNumberThread); intNumberThread++; //Sleep 0.01 秒钟 try { sleep(10); } catch (InterruptedException e){ e.printStackTrace(); } if(intNumberThread > intTatol){ return; } } } public static void main(String[] args) { TestMultiThreadSleep testThread1 = new TestMultiThreadSleep(); testThread1.start(); //再加一个线程 testThread2 TestMultiThreadSleep testThread2 = new TestMultiThreadSleep(); testThread2.start(); while(testThread1.intNumberThreadMain < testThread1.intTatol){ System.out.println("Main() -- : " + testThread1.intNumberThreadMain); testThread1.intNumberThreadMain++; } } } 说明: 1、 sleep 只能在线程中使用; 2、 sleep(1000)表示睡眠 1 秒,这里的单位默认为微秒; 3、 当使用 sleep 时,需要捕获异常 InterruptedException。 作业 1、 从 Thread 继承一个类,并覆盖 run()方法。在 run()内,打印出一条消息,然后调用 sleep()。 重复三遍这些操作,然后从 run()返回。在构建器中放置一条启动消息,并覆盖 finalize(), 打印一条关闭消息。创建一个独立的线程类,使它在 run() 内 调 用 System.gc() 和 System.runFinalization(),并打印一条消息,表明调用成功。创建这两种类型的几个线程,然 后运行它们,看看会发生什么。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 286 第十五章 网络编程 教学目标: i掌握网络编程基本概念 i掌握 TCP/IP 编程 i掌握 UDP 编程 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 287 一:网络编程基本概念 1:网络通信协议及接口 计算机网络中实现通信必须有一些约定即通信协议,对速率、传输代码、代码结构、传 输控制 步骤、出错控制等制定标准。而为了使两个结点之间能进行对话,必须在它们之间建立通信工具, 即网络通信接口,使彼此之间能进行信息交换。接口包括两部分: l 硬件装置:实现结点之间的信息传送 l 软件装置:规定双方进行通信的约定协议 由于结点之间的联系很复杂,在制定协议时,把复杂成份分解成一些简单的成份,再将它们复 合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与再下一 层不发生关系。通信协议的分层原则为,把用户应用程序作为最高层,物理通信线路作为最低层, 将其间的协议处理分为若干层,规定每层处理的任务,也规定每层的接口标准,如图所示: 非计算机专业的读者常感困惑的是,在上述各层间进行通信时到底做了什么事情?简单的说, 就是进行了数据的封装和拆封:发送方数据在网络模型的各层传送过程中加入头尾说明性信息,而 接受方收到数据后去除相应的头尾。就好比张三与李四两人的通信过程:张三将写好的信纸装入信 封,在这一封装过程中添加了收信人的邮政编码、地址、姓名等信息;而李四收到信后,要先撕开 信封才能读取其中有用的信息。而在此期间,邮局以及运输公司等为保证信件传递的顺利进行,可 能还要进行中间层次的封装和拆封工作。 1.1:TCP/IP 协议 TCP(Transmission Control Protocol)/IP(Internet Protocol)协议是当前网络数据传输 的基础协议。他们可保证不同厂家生产的计算机能在共同网络环境下运行,解决异构网通信问题, TCP/IP 与低层的数据链路层和物理层无关,能广泛地支持由低两层协议构成的物理网络结构。TCP -- 面向连接的可靠数据传输协议;TCP 重发一切没有收到的数据,进行数据内容准确性检查并保证数 据分组的正确顺序。 IP 协议是网际层的主要协议,支持网间互连的数据报通信。它提供主要功能有:无连接数据报 传送、数据报路由选择和差错控制。IP 协议主要特性为,IP 协议将报文传送到目的主机后,无论传 送正确与否都不进行检验、不回送确认、不保证分组的正确顺序。 为实现网络中不同计算机之间的通信,每台机器都必须有一个与众不同的标识,这就是 IP 地址, TCP/IP 使用 IP 地址来标识源地址和目的地址。IP 地址格式:数字型,32 位,由 4 个 8 位的二进制 数组成,每 8 位之间用圆点隔开,如:166.111.78.98。 2:URL URL(统一资源定位器,Uniform Resource Locator)用于表示 Internet 上资源的地址。这里 所说的资源,可以是文件、目录或更为复杂的对象的引用。 URL 一般由协议名、资源所在的主机名和资源名等部分组成,例如下面的 URL: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 288 http://home.netscape.com/home/welcome.html 所用的协议是 http(Hypertext Transfer Protocol,超文本传输协议)协议,资源所在的主 机名为 home.netscape.com,资源名为 home/welcome.html。有时资源名也可省略,这样将指向默认 的主页面,如 http://www.sun.com。URL 还可以包含端口号来指定与远端主机相连接的端口。如果 不指定端口号,则使用默认值。例如,http 协议的默认端口号是 8080。显式指定端口号的 URL 形式 如下: http://java.cs.tsinghua.edu.cn:8888/ java.net 包定义了对应的 URL 类。其常用构造方法及用法举例如下: public URL(String spec); URL u1 = new URL("http://home.netscape.com/home/"); public URL(URL context, String spec); URL u2 = new URL(u1, "welcome.html"); public URL(String protocol,String host,String file); URL u3 = new URL("http","www.sun.com","index.html"); public URL (String protocol,String host,int port,String file); URL u4 = new URL("http", "www.sun.com", 80, "index.html"); 使用 URL 类的 openStream()方法可以建立到当前 URL 的连接并返回一个可用于从该连接读取数 据的输入流对象,方法格式为: public final InputStream openStream() throws IOException 示例 使用 URL 读取网络资源程序:URLReader.java import java.io.*; import java.net.*; public class URLReader{ public static void main(String args[]){ try{ URL tirc = new URL("http://www.tsinghua.edu.cn/"); BufferedReader in = new BufferedReader(new InputStreamReader(tirc.openStream())); String s; while((s = in.readLine())!=null) System.out.println(s); in.close(); }catch(MalformedURLException e) { http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 289 System.out.println(e); }catch(IOException e){ System.out.println(e); } } } 程序运行结果为: 清华大学网站首页 如使用了代理服务器,则应通过添加系统属性的方式指名代理服务器的机器名(或 IP 地址)及 端口号。此时运行命令格式如下: java -Dhttp.proxyHost= -Dhttp.proxyPort= 例如:java -Dhttp.proxyHost=196.168.100.2 -Dhttp.proxyPort=8129 URLReader 3:连接的地址 你发起电话呼叫时,你必须知道所拨的电话号码。如果要发起网络连接,你需要知道远程机器 的地址或名字。此外,每个网络连接需要一个端口号,你可以把它想象成电话的分机号码。一旦你 和一台计算机建立连接,你需要指明连接的目的。所以,就如同你可以使用一个特定的分机号码来 和财务部门对话那样,你可以使用一个特定的端口号来和会计程序通信。 4:端口号 TCP/IP 系统中的端口号是一个 16 位的数字,它的范围是 0~65535。实际上,小于 1024 的端口 号保留给预定义的服务,而且除非要和那些服务之一进行通信(例如 telnet,SMTP 邮件和 ftp 等), 否则你不应该使用它们。 客户和服务器必须事先约定所使用的端口。如果系统两部分所使用的端口不一致,那就不能进 行通信。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 290 5:Java 网络模型 在 Java 编程语言中,TCP/IP socket 连接是用 java.net 包中的类实现的。下图说明了服务器 和客户端所发生的动作。 l 服务器分配一个端口号。如果客户请求一个连接,服务器使用 accept()方法打开 socket 连接。 l 客户在 host 的 port 端口建立连接。 l 服务器和客户使用 InputStream 和 OutputStream 进行通信。 二:TCP/IP 编程 1:Socket 基础 计算机以一种非常简单的方式进行相互间的操作和通信。计算机芯片是以 1 和 0 的形式存储 并传输数据的开—闭转换器的集合。当计算机想共享数据时,它们所需做的全部就是以一致的速度、 顺序、定 时 等等来回传输几百万比特和字节的数据流。每次想在两个应用程序之间进行信息通信时, 您怎么会愿意担心那些细节呢? 为免除这些担心,我们需要每次都以相同方式完成该项工作的一组包协议。这将允许我们处理 应用程序级的工作,而不必担心低级网络细节。这些成包协议称为协议栈(stack)。TCP/IP 是当今 最常见的协议栈。多数协议栈(包括 TCP/IP)都大致对应于国际标准化组织(International Standards Organization,ISO)的开放系统互连参考模型(Open Systems Interconnect Reference Model,OSIRM)。OSIRM 认为在一个可靠的计算机组网中有七个逻辑层(见图)。各个地方的公司都 对这个模型某些层的实现做了一些贡献,从生成电子信号(光脉冲、射频等等)到提供数据给应用 程序。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 291 socket 是指在一个特定编程模型下,进程间通信链路的端点。因为这个特定编程模型的流行, socket 这个名字在其他领域得到了复用,包括 Java 技术。 当进程通过网络进行通信时,Java 技术使用它的流模型。一个 socket 包括两个流:一个输入 流和一个输出流。如果一个进程要通过网络向另一个进程发送数据,只需简单地写入与 socket 相关 联的输出流。一个进程通过从与 socket 相关联的输入流读来读取另一个进程所写的数据。 建立网络连接之后,使用与 socket 相关联的流和使用其他流是非常相似的。 我们不想涉及层的太多细节,但您应该知道套接字位于什么地方 使用套接字的代码工作于表示层。表示层提供应用层能够使用的信息的公共表示。假设您打算 把应用程序连接到只能识别 EBCDIC 的旧的银行系统。应用程序的域对象以 ASCII 格式存储信息。 在这种情况下,您得负责在表示层上编写把数据从 EBCDIC 转换成 ASCII 的代码,然后(比方说) 给应用层提供域对象。应用层然后就可以用域对象来做它想做的任何事情。 您编写的套接字处理代码只存在于表示层中。您的应用层无须知道套接字如何工作的任何事情。 简言之,一台机器上的套接字与另一台机器上的套接字交谈就创建一条通信通道。程序员可以 用该通道来在两台机器之间发送数据。当您发送数据时,TCP/IP 协议栈的每一层都会添加适当的报 头信息来包装数据。这些报头帮助协议栈把您的数据送到目的地。好消息是 Java 语言通过"流"为 您的代码提供数据,从而隐藏了所有这些细节,这也是为什么它们有时候被叫做流套接字( streaming socket)的原因。 把套接字想成两端电话上的听筒 — 我和您通过专用通道在我们的电话听筒上讲话和聆听。直 到我们决定挂断电话,对话才会结束(除非我们在使用蜂窝电话)。而且我们各自的电话线路都占线, 直到我们挂断电话。 如果想在没有更高级机制如 ORB(以及 CORBA、RMI、IIOP 等等)开销的情况下进行两台计算 机之间的通信,那么套接字就适合您。套接字的低级细节相当棘手。幸运的是,Java 平台给了您一 些虽然简单但却强大的更高级抽象,使您可以容易地创建和使用套接字。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 292 2:套接字的类型 一般而言,Java 语言中的套接字有以下两种形式: l TCP 套接字(由 Socket 类实现,稍后我们将讨论这个类) l UDP 套接字(由 DatagramSocket 类实现) TCP 和 UDP 扮演相同角色,但 做法 不 同。两者都接收传输协议数据包并将其内容向前传送到表 示层。TCP 把消息分解成数据包(数据报,datagrams)并在接收端以正确的顺序把它们重新装配起 来。TCP 还处理对遗失数据包的重传请求。有了 TCP,位于上层的层要担心的事情就少多了。UDP 不 提供装配和重传请求这些功能。它只是向前传送信息包。位于上层的层必须确保消息是完整的并且 是以正确的顺序装配的。 一般而言,UDP 强加给您的应用程序的性能开销更小,但只在应用程序不会突然交换大量数据 并且不必装配大量数据报以完成一条消息的时候。否则,TCP 才是最简单或许也是最高效的选择. 因为多数读者都喜欢 TCP 胜过 UDP,所以我们将把讨论限制在 Java 语言中面向 TCP 的类。 3:编写简单的 Socket 程序 java.net 包中定义了两个类 Socket 和 ServerSocket,分别用来表示双向连接的客户端和服务 器端。其常用的构造方法有: Socket(InetAddress address, int port); Socket(InetAddress address, int port, boolean stream); Socket(String host, int port); Socket(String host, int port, boolean stream); ServerSocket(int port); ServerSocket(int port, int count); 网络编程的四个基本步骤为: 创建 socket 打开连接到 socket 的输入/输出流 按照一定的协议对 socket 进行读/写操作 关闭 socket 示例 简单的 client/server 程序:TestServer.java import java.net.*; import java.io.*; public class TestServer { public static void main(String args[]) { http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 293 try { ServerSocket s = new ServerSocket(8888); while (true) { Socket s1 = s.accept(); OutputStream os = s1.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); dos.writeUTF("Hello," +s1.getInetAddress() + "port#" + s1.getPort() + "\nbye!"); dos.close(); s1.close(); } }catch (IOException e) { System.out.println("程序运行出错:" + e); } } } 程序:TestClient.java import java.net.*; import java.io.*; public class TestClient { public static void main(String args[]) { try { Socket s1 = new Socket("127.0.0.1", 8888); InputStream is = s1.getInputStream(); DataInputStream dis = new DataInputStream(is); System.out.println(dis.readUTF()); dis.close(); s1.close(); } catch (ConnectException connExc) { System.err.println("服务器连接失败!"); } catch (IOException e) { } } } TestServer.java 和 TestClient.java 程序可运行在两台不同的机器上,首先运行 TestServer 程序,创建 ServerSocket 对象、监听所在机器(服务器)的指定 8888 号端口,等待客户端连接请 求;然后运行 TestClient 程序,创建 Socket 对象,连接服务器的 8888 号端口。建立连接后,服务 器端程序打开其 Socket 对象关联的输出流,并向其中写出有关连接者的信息,然后关闭输出流及 Socket 对象、程序退出;客户端程序打开 Socket 对象关联的输入流,从中读取服务器端发送来的 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 294 信息并显示到屏幕上。程序运行结果为: Hello,zhangliguo/127.0.0.1port#1032 bye-bye! 其中,"zhangliguo/127.0.0.1"为客户端机器名和 IP 地址,1032 是客户端由机器指定的发出 连接请求的端口号。上述程序也可以运行在同一台计算机上,此时该机器既是服务器,又是客户机, 但运行原理是相同的。 4:JAVA 中的 socket 实现 Java 平台在 java.net 包中提供套接字的实现。在 本 教 程 中,我 们 将与 java.net 中的以下三个类 一起工作: URLConnection 、Socket 、ServerSocket java.net 中还有更多的类,但这些是您将最经常碰到的。让我们从 URLConnection 开始。这 个类为您不必了解任何底层套接字细节就能在 Java 代码中使用套接字提供一种途径 URLConnection. URLConnection 类是所有在应用程序和 URL 之 间 创建通信链路的类的抽象超类。 URLConnection 在获取 Web 服务器上的文档方面特别有用,但也可用于连接由 URL 标识的任何资 源。该类的实例既可用于从资源中读,也可用于往资源中写。例如,您可以连接到一个 servlet 并 发送一个格式良好的 XML String 到服务器上进行处理。URLConnection 的具体子类(例如 HttpURLConnection)提供特定于它们实现的额外功能。对于我们的示例,我们不想做任何特别的事 情,所以我们将使用 URLConnection 本身提供的缺省行为。 连接到 URL 包括几个步骤: 1. 创建 URLConnection 2. 用各种 setter 方法配置它 3. 连接到 URL 4. 用各种 getter 方法与它交互 5. 接着,我们将看一些演示如何用 URLConnection 来从服务器请求文档的样本代码。 4.1:URLClient 类 我们将从 URLClient 类的结构讲起。 import java.io.*; import java.net.*; public class URLClient { protected URLConnection connection; http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 295 public static void main(String[] args) { } public String getDocumentAt(String urlString) { } } 要做的第一件事是导入 java.net 和 java.io。我们给我们的类一个实例变量以保存一个 URLConnection。我们的类有一个 main() 方法,它处理浏览文档的逻辑流。我们的类还有一个 getDocumentAt() 方法,该方法连接到服务器并向它请求给定文档。下面我们将分别探究这些方法 的细节。 4.2:浏览文档 main() 方法处理浏览文档的逻辑流: public static void main(String[] args) { URLClient client = new URLClient(); String yahoo = client.getDocumentAt("http://www.yahoo.com"); System.out.println(yahoo); } 我们的 main() 方法只是创建一个新的 URLClient 并用一个有效的 URL String 调用 getDocumentAt()。当调用返回该文档时,我们把它存储在 String,然后将它打印到控制台。然而, 实际的工作是在 getDocumentAt() 方法中完成的。 4.3:从服务器请求一个文档 getDocumentAt() 方法处理获取 Web 上的文档的实际工作: public String getDocumentAt(String urlString) { StringBuffer document = new StringBuffer(); try { URL url = new URL(urlString); URLConnection conn = url.openConnection(); BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line = null; while ((line = reader.readLine()) != null) http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 296 document.append(line + "\n"); reader.close(); } catch (MalformedURLException e) { System.out.println("Unable to connect to URL: " + urlString); } catch (IOException e) { System.out.println("IOException when connecting to URL: " + urlString); } return document.toString(); } getDocumentAt() 方法有一个 String 参数,该参数包含我们想获取的文档的 URL。我们在开始 时创建一个 StringBuffer 来保存文档的行。然后我们用我们传进去的 urlString 创建一个新 URL。 接着创建一个 URLConnection 并打开它: URLConnection conn = url.openConnection(); 一旦有了一个 URLConnection,我们就获取它的 InputStream 并包装进 InputStreamReader,然 后我们又把 InputStreamReader 包装进 BufferedReader 以使我们能够读取想从服务器上获取的文档 的行。在 Java 代码中处理套接字时,我们将经常使用这种包装技术,但我们不会总是详细讨论它。 在我们继续往前讲之前,您应该熟悉它: BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 有了 BufferedReader,就使得我们能够容易地读取文档内容。我们在 while 循环中调用 reader 上的 readLine(): String line = null; while ((line = reader.readLine()) != null) document.append(line + "\n"); 对 readLine() 的调用将直至碰到一个从 InputStream 传入的行终止符(例如换行符)时才阻 塞。如果没碰到,它将继续等待。只有当连接被关闭时,它才会返回 null。在这个案例中,一旦我 们获取一个行(line),我们就把它连同一个换行符一起附加(append)到名为 document 的 StringBuffer 上。这保留了服务器端上读取的文档的格式。 我们在读完行之后关闭 BufferedReader: reader.close(); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 297 如果提供给 URL 构造器的 urlString 是无效的,那么将抛出 MalformedURLException。如果 发生了别的错误,例如当从连接上获取 InputStream 时,那么将抛出 IOException。 4.4:总结 实际上,URLConnection 使用套接字从我们指定的 URL 中读取信息(它只是解析成 IP 地址), 但我们无须了解它,我们也不关心。但有很多事;我们马上就去看看。 在继续往前讲之前,让我们回顾一下创建和使用 URLConnection 的步骤: 用 您 想 连 接的资源的有效 URL String 实 例 化 一个 URL (如有问题则抛出 MalformedURLException)。 打开该 URL 上的一个连接。 把该连接的 InputStream 包装进 BufferedReader 以使您能够读取行。 用 BufferedReader 读文档。 关闭 BufferedReader。 URLClient 的代码清单 import java.io.*; import java.net.*; public class URLClient { protected HttpURLConnection connection; public String getDocumentAt(String urlString) { StringBuffer document = new StringBuffer(); try { URL url = new URL(urlString); URLConnection conn = url.openConnection(); BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); String line = null; while ((line = reader.readLine()) != null) document.append(line + "\n"); reader.close(); } catch (MalformedURLException e) { System.out.println("Unable to connect to URL: " + urlString); http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 298 } catch (IOException e) { System.out.println("IOException when connecting to URL: " + urlString); } return document.toString(); } public static void main(String[] args) { URLClient client = new URLClient(); String yahoo = client.getDocumentAt("http://www.yahoo.com"); System.out.println(yahoo); } } 三:UDP 编程 TCP/IP 是面向连接的协议。而用户数据报协议(UDP)是一种无连接的协议。要区分这两种协议, 一种很简单而又很贴切的方法是把它们比作电话呼叫和邮递信件。 电话呼叫保证有一个同步通信;消息按给定次序发送和接收。而对于邮递信件,即使能收到所 有的消息,它们的顺序也可能不同。 用户数据报协议(UDP)由 Java 软件的 DatagramSocket 和 DatagramPacket 类支持。包是自包含 的消息,它包括有关发送方、消息长度和消息自身。 1:DatagramPacket UDP socket l 它们是无连接的协议。 l 不保证消息的可靠传输。 l 它们由 Java 技术中的 DatagramSocket 和 DatagramPacket 类 支持。 DatagramPacket DatagramPacket 有两个构造函数:一个用来接收数据,另一个用来 发送数据。 DatagramPacket(byte [] recvBuf, int readLength) DatagramPacket(byte [] sendBuf, int sendLength, InetAddress iaddr, int iport) http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 299 DatagramPacket 有两个构造函数:一个用来接收数据,另一个用来发送数据: DatagramPacket(byte [] recvBuf, int readLength) 用来建立一个字节数组以接收 UDP 包。byte 数组在传递给构造函数时是空的,而 int 值用 来设定要读取的字节数(不能比数组的大小还大)。 DatagramPacket(byte [] sendBuf, int sendLength, InetAddress iaddr, int iport) 用来建立将要传输的 UDP 包。sendLength 不应该比 sendBuf 字节数组的大小要大。 2:DatagramSocket DatagramSocket 用来读写 UDP 包。这个类有三个构造函数,允许你指定要绑定的端口号和 internet 地址: DatagramSocket()-绑定本地主机的所有可用端口 DatagramSocket(int port)-绑定本地主机的指定端口 DatagramSocket(int port, InetAddress iaddr)-绑定指定地址的指定端口 3:最小 UDP 服务器 最小 UDP 服务器在 8000 端口监听客户的请求。当它从客户接收到一个 DatagramPacket 时,它 发送服务器上的当前时间。 1.import java.io.*; 2.import java.net.*; 3.import java.util.*; 4. 5.public class UdpServer{ 6. 7.//This method retrieves the current time on the server 8.public byte[] getTime(){ 9.Date d= new Date(); 10.return d.toString().getBytes(); 11.} 12. 13.// Main server loop. 14.public void go() throws IOException { 15. 16.DatagramSocket datagramSocket; 17.DatagramPacket inDataPacket; // Datagram packet from the client 18.DatagramPacket outDataPacket; // Datagram packet to the client 19.InetAddress clientAddress; // Client return address 20.int clientPort; // Client return port 21.byte[] msg= new byte[10]; // Incoming data buffer. Ignored. 22.byte[] time; // Stores retrieved time http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 300 23. 24.// Allocate a socket to man port 8000 for requests. 25.datagramSocket = new DatagramSocket(8000); 26.System.out.println("UDP server active on port 8000"); 27. 28.// Loop forever 29.while(true) { 30. 31.// Set up receiver packet. Data will be ignored. 32.inDataPacket = new DatagramPacket(msg, msg.length); 33.// Get the message. 34.datagramSocket.receive(inDataPacket); 35. 36.// Retrieve return address information, including InetAddress 37.// and port from the datagram packet just recieved. 38.clientAddress = inDataPacket.getAddress(); 39.clientPort = inDataPacket.getPort(); 40. 41.// Get the current time. 42.time = getTime(); 43. 44.//set up a datagram to be sent to the client using the 45.//current time, the client address and port 46.outDataPacket = new DatagramPacket 47.(time, time.length, clientAddress, clientPort); 48. 49.//finally send the packet 50.datagramSocket.send(outDataPacket); 51.} 52.} 53. 54.public static void main(String args[]) { 55.UdpServer udpServer = new UdpServer(); 56.try { 57.udpServer.go(); 58.} catch (IOException e) { 59.System.out.println ("IOException occured with socket."); 60.System.out.println (e); 61.System.exit(1); 62.} 63.} 64.} http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 301 4:最小 UDP 客户 最小 UDP 客户向前面创建的客户发送一个空包并接收一个包含服务器实际时间的包。 1.import java.io.*; 2.import java.net.*; 3. 4.public class UdpClient { 5. 6.public void go() throws IOException, UnknownHostException { 7.DatagramSocket datagramSocket; 8.DatagramPacket outDataPacket; // Datagram packet to the server 9.DatagramPacket inDataPacket; // Datagram packet from the server 10.InetAddress serverAddress; // Server host address 11.byte[] msg = new byte[100]; // Buffer space. 12.String receivedMsg; // Received message in String form. 13. 14.// Allocate a socket by which messages are sent and received. 15.datagramSocket = new DatagramSocket(); 16. 17.// Server is running on this same machine for this example. 18.// This method can throw an UnknownHostException. 19.serverAddress = InetAddress.getLocalHost(); 20. 21.// Set up a datagram request to be sent to the server. 22.// Send to port 8000. 23.outDataPacket = new DatagramPacket(msg, 1, serverAddress, 8000); 24. 25.// Make the request to the server. 26.datagramSocket.send(outDataPacket); 27. 28.// Set up a datagram packet to receive server's response. 29.inDataPacket = new DatagramPacket(msg, msg.length); 30. 31.// Receive the time data from the server 32.datagramSocket.receive(inDataPacket); 33. 34.// Print the data received from the server 35.receivedMsg = new String 36.(inDataPacket.getData(), 0, inDataPacket.getLength()); 37.System.out.println(receivedMsg); 38. 39.//close the socket http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 302 40.datagramSocket.close(); 41.} 42. 43.public static void main(String args[]) { 44.UdpClient udpClient = new UdpClient(); 45.try { 46.udpClient.go(); 47.} catch (Exception e) { 48.System.out.println ("Exception occured with socket."); 49.System.out.println (e); 50.System.exit(1); 51.} 52.} 53.} http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 303 练习实践 本章的内容为 Socket,实践重点: l Socket 基础 程序 1: 用Socket 通讯 需求:用 Socket 创建一个服务器及客户端,并使其通讯。 目标: 1、 Socket 基础; 2、 服务器的建立,客户端的建立; 3、 通讯方式。 程序 1(服务器端程序): //: JabberServer.java package com.useful.java.part5; import java.io.*; import java.net.*; public class JabberServer { // Choose a port outside of the range 1-1024: public static final int PORT = 8080; public static void main(String[] args) throws IOException { ServerSocket s = new ServerSocket(PORT); System.out.println("Started: " + s); try { // Blocks until a connection occurs: Socket socket = s.accept(); try { System.out.println( "Connection accepted: "+ socket); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); // Output is automatically flushed // by PrintWriter: PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 304 socket.getOutputStream())),true); while (true) { String str = in.readLine(); if (str.equals("END")) break; System.out.println("Echoing: " + str); out.println(str); } // Always close the two sockets... } finally { System.out.println("closing..."); socket.close(); } } finally { s.close(); } } } 说明: 1、 ServerSocket 需要的只是一个端口编号,不需要 IP 地址(因为它就在这台机器上运行)。 调用 accept()时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。 换言之,尽管它在那里等候连接,但 其 他 进程仍能正常运行。建好一个连接以后,accept() 就会返回一个 Socket 对象,它是那个连接的代表。 2、 假如 ServerSocket 构建器失败,则程序简单地退出(注意必须保证 ServerSocket 的构建 器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main()会“掷”出一个 IOException 违例,所以不必使用一个 try 块。若 ServerSocket 构建器成功执行,则其他 所有方法调用都必须到一个 try-finally 代码块里寻求保护,以确保无论块以什么方式留 下,ServerSocket 都能正确地关闭。 3、 同样的道理也适用于由accept()返回的 Socket。若accept()失败,那 么 我 们 必须保证 Socket 不再存在或者含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必 须进入一个 try-finally 块内,以保障在它们失败的情况下,Socket 仍能得到正确的清除。 由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它 们清除(Java 中没有提供“破坏器”来帮助我们做这件事情)。 4、 无论 ServerSocket 还是由 accept()产生的 Socket 都打印到 System.out 里。这意味着它们 的 toString 方法会得到自动调用。这样便产生了: ServerSocket[addr=0.0.0.0,PORT=0,localport=8080] Socket[addr=127.0.0.1,PORT=1077,localport=8080] 5、 一般服务器应先启动,启动后,运行如下: http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 305 程序 2(客户端程序): //: JabberClient.java package com.useful.java.part5; import java.net.*; import java.io.*; public class JabberClient { public static void main(String[] args) throws IOException { // Passing null to getByName() produces the // special "Local Loopback" IP address, for // testing on one machine w/o a network: InetAddress addr = InetAddress.getByName(null); // Alternatively, you can use // the address or name: // InetAddress addr = // InetAddress.getByName("127.0.0.1"); // InetAddress addr = // InetAddress.getByName("localhost"); System.out.println("addr = " + addr); Socket socket = new Socket(addr, JabberServer.PORT); // Guard everything in a try-finally to make // sure that the socket is closed: try { System.out.println("socket = " + socket); BufferedReader in = new BufferedReader( http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 306 new InputStreamReader( socket.getInputStream())); // Output is automatically flushed // by PrintWriter: PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter( socket.getOutputStream())),true); for(int i = 0; i < 10; i ++) { out.println("howdy " + i); String str = in.readLine(); System.out.println(str); } out.println("END"); } finally { System.out.println("closing..."); socket.close(); } } } 说明: 1. 运行客户端时,不能关服务器端程序运行窗口,而应新开一个窗口; 2. 在 main()中,大家可看到获得本地主机 IP 地址的 InetAddress 的三种途径:使用 null,使用 localhost,或者直接使用保留地址 127.0.0.1。当然,如果想通过网络同一台远程主机连接,也可 以换用那台机器的 IP 地址。打印出 InetAddress addr 后(通过对 toString()方法的自动调用),结 果如下: localhost/127.0.0.1 通过向 getByName()传递一个 null,它会默认寻找 localhost,并生成特殊的保留地 址 127.0.0.1。 3. 一次独一无二的因特网连接是用下述四种数据标识的:clientHost(客户主机)、 clientPortNumber (客户端口号)、 serverHost(服务主机)以及 serverPortNumber(服务端口号)。服务程序启动 后,会在本地主机(127.0.0.1)上建立为它分配的端口(8080)。一旦客户程序发出请求,机器 上下一个可用的端口就会分配给它(这种情况下是 1077),这一行动也在与服务程序相同的机 器(127.0.0.1)上进行。 4. 现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到哪里。所 以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自 己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况: Socket[addr=127.0.0.1,port=1077,localport=8080] 这意味着服务器刚才已接受了来自 127.0.0.1 这台机器的端口 1077 的连接,同时监听自己的本 地端口(8080)。而在客户端: Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077] 这意味着客户已用自己的本地端口 1077 与 127.0.0.1 机器上的端口 8080 建立了连接。 http://www.javass.cn 咨询QQ:460190900 Java私塾跟我学系列——JAVA篇 欢迎大家前来北京JAVA私塾报名学习 联系电话:13651249175 张老师 307 5. 客户端程序运行如下: 与此同时,服务器端程序改变如下: 作业 1:修改程序 1,删去为输入和输出设计的所有缓冲机制,然后再次编译和运行,观察一下结果。 2:修改 JabberClient,禁止输出刷新,并观察结果。 3:可以尝试做一个类似 QQ 的聊天通讯工具
还剩182页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

573442002

贡献于2012-09-19

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf