Java基础类 JDK1.2提供了Java基础类,其中的一部分就是Swing。Swing是构筑在AWT上层的一些组件的集合(为了保证平台独立性,它是用100%的纯Java编写)。本模块介绍了JFC和Swing图形用户界面的实现。  第一节 相关问题 讨论-以下为与本模块内容有关的问题: AWT本身是非常有用的,它是一个新的类集合的一部分。这个新的类集合称为Java基础类(JFC),它作为一个整体,将GUI提升到了一个新的水平层次。JFC究竟是什么,特别地,什么是Swing?什么事Swing可以做但AWT不能? 第二节 目 标 在完成了本模块的学习后,你应当能够: 认识Java基础类的关键特性 描述com.sun.java.swing包的关键特性 认识Swing组件 定义容器和组件,并解释如何联合使用它们来构造一个Swing GUI 编写,编译并运行一个基本的Swing应用程序 高效地使用诸如Jframe和Japplet等顶层容器 参考文献 以下参考文献可提供有关本模块论题的其他细节内容: The Java Tutorial,这是Sun Microsystems的一本在线教材,可以从http://java.sun.com/docs/books/tutorial得到。 第三节 介 绍 Java基础类是关于GUI组件和服务的完整集合,它大大简化了健壮Java应用程序的开发和实现。 JFC,作为JDK1.2的一个有机部分,主要包含5个API:AWT,Java2D,Accessibility,Drag & Drop,Swing。它提供了帮助开发人员设计复杂应用程序的一整套应用程序开发包。 正如前面那些模块中所讨论的那样,AWT组件为各类Java应用程序提供了多种GUI工具。 Java2D是一图形API,它为Java应用程序提供了一套高级的有关二维(2D)图形图像处理的类。Java2D API扩展了java.awt和java.awt. image类,并提供了丰富的绘图风格,定义复杂图形的机制和精心调节绘制过程的方法和类。这些API使得独立于平台的图形应用程序的开发更加简便。 Accessibility API提供了一套高级工具,用以辅助开发使用非传统输入和输出的应用程序。它提供了一个辅助的技术接口,如:屏幕阅读器,屏幕放大器,听觉文本阅读器(语音处理)等等。 Drag & Drop技术提供了Java和本地应用程序之间的互操作性,用来在Java应用程序和不支持Java技术的应用程序之间交换数据。 JFC模块的重点在Swing。Swing用来进行基于窗口的应用程序开发,它提供了一套丰富的组件和工作框架,以指定GUI如何独立于平台地展现其视觉效果。 11.3.1 Swing介绍 Swing提供了一整套GUI组件,为了保证可移植性,它是完全用Java语言编写的。 可插的外观和感觉 可插的外观和感觉使得开发人员可以构建这样的应用程序:它们可以在任何平台上执行,而且看上去就象是专门为那个特定的平台而开发的。一个在Windows环境中执行的程序,似乎是专为这个环境而开发的;而同样的程序在Unix平台上执行,它的行为又似乎是专为Unix环境开发的。 开发人员可以创建自己的客户化Swing组件,带有他们想设计出的任何外观和感觉。这增加了用于跨平台应用程序和Applet的可靠性和一致性。一个完整应用程序的GUI可以在运行时刻从一种外观和感觉切换到另一种。 Swing的体系结构 与AWT比较,Swing提供了更完整的组件,引入了许多新的特性和能力。Swing API是围绕着实现AWT各个部分的API构筑的。这保证了所有早期的AWT组件仍然可以使用。AWT采用了与特定平台相关的实现,而绝大多数Swing组件却不是这样做的,因此Swing的外观和感觉是可客户化和可插的。  上图显示了JFC各个部分之间的相互关系。Java2D,Accessibility,Drag & Drop,和Accessibility API是AWT和JFC的一部分,但它们不属于Swing。这是因为,这些组件使用了一些本地代码,而Swing却不是这样的。 Swing是围绕着一个称为JComponent的新组件构建的,而JComponent则由AWT的容器类扩展而来。 Swing的层次结构 下图说明了Swing组件的层次结构:  Swing GUI使用两种类型的类,即GUI类和非GUI支持类。GUI类是可视的,它从JComponent继承而来,因此称为“J”类。非GUI类为GUI类提供服务,并执行关键功能;因此它们不产生任何可视的输出。 注-Swing的事件处理类是非GUI类的一例。 Swing组件 Swing组件主要为文本处理、按钮、标签、列表、pane、组合框、滚动条、滚动pane、菜单、表格和树提供了组件。其中一些组件如下所示:  JApplet JButton JComboBox JOptionPane  JList JLabel Swing组件(续)  JScrollPane JTable  JScrollBar JSlider  JTooltip JTree 第四节 基本的Swing应用程序 HelloSwing应用程序的输出产生下图所示的窗口:  每次用户点击按钮时,标签就会更新。 11.5.1 HelloSwing 1.import java.awt.*; 2.import java.awt.event.*; 3.import com.sun.java.swing.*; 4.import java.awt.accessibility.*; 5. 6.public class HelloSwing implements ActionListener { 7.private JFrame jFrame; 8.private JLabel jLabel; 9.private JPanel jPanel; 10.private JButton jButton; 11.private AccessibleContext accContext; 12. 13.private String labelPrefix = 14."Number of button clicks: "; 15.private int numClicks = 0; 16. 17.public void go() { 18. 19.// Here is how you can set up a particular 20.// lookAndFeel. Not necessary for default. 21.// 22.// try { 23.// UIManager.setLookAndFeel( 24.// UIManager.getLookAndFeel()); 25.// } catch (UnsupportedLookAndFeelException e) { 26.// System.err.println("Couldn't use the " + 27.// "default look and feel " + e); 28.// } 29. 30.jFrame = new JFrame("HelloSwing"); 31.jLabel = new JLabel(labelPrefix + "0"); 32. 33.jButton = new JButton("I am a Swing button!"); 34. 35.// Create a shortcut: make ALT-A be equivalent 36.// to pressing mouse over button. 37.jButton.setMnemonic('i'); 38. 39.jButton.addActionListener(this); 40. HelloSwing(续) 1.// Add support for accessibility. 2.accContext = jButton.getAccessibleContext(); 3.accContext.setAccessibleDescription( 4."Pressing this button increments " + 5."the number of button clicks"); 6. 7.// Set up pane. 8.// Give it a border around the edges. 9.jPanel = new JPanel(); 10.jPanel.setBorder( 11.BorderFactory.createEmptyBorder( 12.30,30,10,30)); 13. 14.// Arrange for compts to be in a single column. 15.jPanel.setLayout(new GridLayout(0, 1)); 16. 17.// Put compts in pane, not in JFrame directly. 18.jPanel.add(jButton); 19.jPanel.add(jLabel); 20.jFrame.setContentPane(jPanel); 21. 22.// Set up a WindowListener inner class to handle 23.// window's quit button. 24.WindowListener wl = new WindowAdapter() { 25.public void windowClosing(WindowEvent e) { 26.System.exit(0); 27.} 28.}; 29.jFrame.addWindowListener(wl); 30. 31.jFrame.pack(); 32.jFrame.setVisible(true); 33.} 34. HelloSwing(续) 1.// Button handling. 2.public void actionPerformed(ActionEvent e) { 3.numClicks++; 4.jLabel.setText(labelPrefix + numClicks); 5.} 6. 7.public static void main(String[] args) { 8. 9.HelloSwing helloSwing = new HelloSwing(); 10.helloSwing.go(); 11.} 12.} 11.4.2 导入Swing包 语句行import com.sun.java.swing.*装入整个Swing包,它包括了标准Swing组件和功能。 选择外观和感觉 Hello Swing的第22-28行给定了应用程序外观和感觉的格式。getLookAndFeel()方法返回在Windows环境中的外观和感觉。在运行Solaris操作系统的机器上,这个方法则返回一个公共桌面环境(CDE)/Motif的外观和感觉。因为都是缺省值,所以对本例来说,这些行都不是必需的。 11.4.3 建立窗口 Swing程序用JFrame对象实现了它们的窗口。JFrame类是AWT Frame类的一个子类。它还加入了一些Swing所独有的特性。Hello Swing中,处理JFrame的代码如下: public HelloSwing() { JFrame jFrame; JPanel jPanel; ..... jFrame = new JFrame("HelloSwing"); jPanel = new JPanel(); ....... jFrame.setContentPane(jPanel); 这段代码与使用Frame的代码十分相似。唯一的区别在于,你不能将组件加入到JFrame中。你可以或者将组件加入到JFrame的content pane中,或者提供一个新的content pane。 一个content pane是一个包含除菜单条(如果有的话)外所有框架的可视组件的容器。要获得一个JFrame的content pane,可使用getContentPane()方法。要设置它的content pane(如前面本例所示),则可使用set ContentPane()方法。 11.4.4 建立Swing组件 Hello Swing程序显式地实例化了4个组件:JFrame,JButton,JLabel和JPanel。Hello Swing用第33-45行中的代码来初始化JButton。 第33行创建了按钮。第37行将ACT-I键组合设置为快捷键,用来模拟按钮的点击。第39行为点击注册了一个事件处理器。第41-45行描述了一个按钮,使得辅助技术可以提供有关按钮功能的信息。 第49-59行初始化了JPanel。这些代码创建了JPanel对象,设置它的边框,并将它的布局管理器设置为单列地放置panel的内容。最后,将一个按钮和一个标签加入到Panel中。Hello Swing中的Panel使用了一个不可见的边框,用来在它周围放入额外的填充。 11.4.5 支持辅助技术 Hello Swing.java中唯一支持辅助技术的代码是: accContext = jButton.getAccessibleContext(); accContext.setAccessibleDescription( "Pressing this button increments " + " the number of button clicks."); 下列信息集也可由辅助技术使用: jButton = new JButton("I'm a Swing button!"); jLabel = new JLabel(labelPrefix + "0"); jLabel.setText(labelPrefix + numClicks); 在JFrame,JButton,JLabel和其他所有组件中,都有内建的Accessibility支持。辅助技术可以很容易地获得文本,甚至与一组件某特定部分相关的文本。 第五节 构造一个Swing GUI Swing包定义了两种类型的组件: 顶层容器(JFrame,JApplet,JWindow,和JDialog) 轻质组件(其他的J…,如JButton,JPanel和JMenu) 顶层容器定义了可以包含轻质组件的框架。特别地,一个顶层Swing容器提供了一个区域,轻质组件可在这个区域中绘制自身。顶层容器是它们对应的重质AWT组件的Swing子类。这些Swing容器依靠它们的AWT超类的本地方法与硬件进行适当的交互。 通常,每个Swing组件在其容器层次结构中都应当有一个位于组件上面的顶层Swing容器。例如,每个包含Swing组件的Applet都应作为JApplet(而它自身又是java.applet.Applet的一个子类)的子类来实现。相似地,每个包含Swing组件的主窗口都应用JFrame来实现。典型地,如果你在使用Swing组件,你将只能使用Swing组件和Swing容器。 Swing组件可以加入到一个与顶层容器关联的content pane中,但绝不能直接加入到顶层容器中。content pane是一个轻质Swing组件,如JPanel。 下面是一个典型Swing程序的GUI容器层次结构图,这个程序实现了 一个包含2个按钮,一个文本域和一个列表: Jframe ( a top-level Swing container) ( (( ( content pane ( ` +---------+-------+ ( ( ( JButton JButton JPanel ( +---------+ ( JTextField JList 下面是关于同样的GUI的另一个容器层次结构,只是在这里,GUI是在浏览器中运行的一个Applet。 ( ( ( ( JApplet ( a top-level Swing container) ( content pane ( ` +---------+-------+ ( ( ( JButton JButton JPanel ( +---------+ ( JTextField JList 下面是构造如上图所示的GUI层次结构的代码: 1.import com.sun.java.swing.*; 2.import java.awt.*; 3. 4.public class SwingGUI { 5. 6.JFrame topLevel; 7.JPanel jPanel; 8.JTextField jTextField; 9.JList jList; 10. 11.JButton b1; 12.JButton b2; 13.Container contentPane; 14. 15.Object listData[] = { 16.new String("First selection"), 17.new String("Second selection"), 18.new String("Third selection") 19.}; 20. 21.public static void main (String args[]) { 22.SwingGUI swingGUI = new SwingGUI(); 23.swingGUI.go(); 24.} 25. 26.public void go() { 27.topLevel = new JFrame("Swing GUI"); 28. 29.// Set up the JPanel, which contains the text field 30.// and list. 31.jPanel = new JPanel(); 32.jTextField = new JTextField(20); 33.jList = new JList(listData); 34. 35.contentPane = topLevel.getContentPane(); 36.contentPane.setLayout(new BorderLayout()); 37. 38.b1 = new JButton("1"); 39.b2 = new JButton("2"); 40.contentPane.add(b1, BorderLayout.NORTH); 41.contentPane.add(b2, BorderLayout.SOUTH); 42. 43.jPanel.setLayout(new FlowLayout()); 44.jPanel.add(jTextField); 45.jPanel.add(jList); 46.contentPane.add(jPanel, BorderLayout.CENTER); 47. 48.topLevel.pack(); 49.topLevel.setVisible(true); 50.} 51.} 第六节 JComponent类 所有Swing都作为JComponent的子类来实现,而JComponent类又是从Container类继承而来。Swing组件从JComponent继承了如下功能: 边框 你可以用setBorder()方法来指定在组件周围显示的边框。还可用一个EmptyBorder的实例来指定一个组件在其周围留有一定的额外空间。 双缓冲 双缓冲可以改善一个频繁被改变的组件的外观。现在你不需要编写双缓冲代码――Swing已为你提供了。缺省情况下,Swing组件是双缓冲的。 提示框 通过用setToolTipText()方法来指定一个字符串,你可以提供给用户有关某个组件的帮助信息。当光标暂停在组件上时,所指定的字符串就会在组件附近的一个小窗口中显示出来。 键盘导航 使用registerKeyboardAction()方法,你可以让用户以键盘代替鼠标来操作GUI。用户为启动一个动作所必须按下的修饰键与字符的组合,由一个KeyStroke对象来表示。 应用程序范围的可插式外观和感觉 每个Java应用程序在运行时刻有一个GUIManager对象,它用于确定运行时刻Swing组件的外观和感觉。由于安全性的限制,你可以通过调用UIManager.setLookAndFeel()方法选择所有Swing组件的外观和感觉。在你所看见的东西背后,每个JComponent对象都有一个对应的ComponentGUI对象,它用来执行所有关于该JComponent的绘制、事件处理、大小判定等任务。 练习:熟悉Swing 练习目标-在本实验中,你将编写、编译和执行两个在GUI中使用Swing组件的程序。 一、准备 为了更好地完成这个练习,你必须理解Swing组件和AWT组件的关系。 二、任务 水平1:创建一个基本的Swing应用程序 使用文本编辑器,创建一个与前面所讨论的HelloSwing类似的应用程序。 将一个图标与按钮相关联。(提示-你可能需要使用ImageIcon类。) 将一个提示框与按钮相关联,这样当鼠标移动到按钮之上时,会显示一个“JFC Button”的提示框。 水平2:用Swing组件创建一个文本编辑器 创建一个初始的JFrame,它包含一个JToolBar,TextArea和JLabel。 将一个JMenuBar与JFrame关联起来。 创建JMenuBar上的第一个菜单。创建一个标记为JMenu,其JMenuItems包括New,Open,Save和Close。 为每个条目增加一个加速键。使用标签的第一个字母。 为每个JMenuItem创建一个匿名的ActionListener,用来处理事件并调用与每个事件对应的方法。 将带有About JMenuItem的HelpJMenu加入到JMenuBar。分别为H和A增加快捷键。 在与About JMenuItem相关联的事件处理器中创建一个模式对话框。 在工具条上创建4个JButton,标为New,Open,Save和About。 为工具条上的每个按钮增加一个带有适当消息的提示框。此外,创建一个匿名ActionListener来处理适当的事件。 保存并编译程序。 三、练习小结 讨论 - 花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。 经验 解释 总结 应用 四、检查你的进度 在进入下一个模块的学习之前,请确认你能够: 认识Java基础类的关键特性 描述com.sun.java.swing包的关键特性 认识Swing组件 定义容器和组件,并解释如何联合使用它们来构造一个Swing GUI 编写,编译并运行一个基本的Swing应用程序 高效地使用诸如JFrame和JApplet等顶层容器 五、思考题 你现在已经知道了如何编写GUI应用程序。假设你想在一个Web浏览器中运行一个GUI应用程序,如何做到这点? Jvav小程序介绍 本模块讨论了JDK对Applet的支持,以及Applet在编程方式、操作上下文和如何开始等方面与应用程序的区别。  第一节 相关问题 讨论 - 以下为与本模块内容有关的问题: Applet有那些优点? 第二节 目 标 在完成了本模块的学习后,你应当能够: 区分独立应用程序和Applet 编写一个HTML标记来调用Java Applet 描述Applet和AWT的类层次 创建HelloWorld.Java Applet 列出Applet的主要方法 描述和使用AWT的绘图模型 使用Applet方法从URL读取图像和文件 使用<param>标记配置Applet 第三节 什么是Applet? Applet是能够嵌入到一个HTML页面中,且可通过Web浏览器下载和执行的一种Java类。它是Java技术容器(container)的一种特定类型,其执行方式不同于应用程序。一个应用程序是从它的main()方法被调用开始的,而一个Applet的生命周期在一定程度上则要复杂得多。本模块分析了Applet如何运行,如何被装载到浏览器中,以及它是如何编写的。 12.3.1 装入Applet 由于Applet在Web浏览器环境中运行,所以它并不直接由键入的一个命令启动。你必须要创建一个HTML文件来告诉浏览器需装载什么以及如何运行它。  浏览器装入URL 浏览器装入HTML文档 浏览器装入Applet类 浏览器运行Applet 12.3.2 Applet的安全限制 由于通过网络装载,Applet的代码具有一种内在的危险性。如果有人编写了一个恶意的类来读取你的密码文件,并把它通过Internet传送,会产生怎样的后果呢? 所能够控制的安全程度是在浏览器层次上实现的。大多数浏览器(包括Netscape Nevigator)缺省地禁止以下操作: 运行时执行另一程序 任何文件的输入/输出 调用任何本地方法 尝试打开除提供Applet的主机之外的任何系统的Socket 这些限制的关键在于,通过限制Applet对系统文件的存取来阻止它侵犯一个远程系统的隐私或破坏该系统。禁止执行另一程序和不允许调用本地方法限制了Applet启动未经JVM检查的代码。对Socket的限制则禁止了与另一个可能有危害性的程序的通信。 JDK1.2提供了一种方式,它指定了一个特殊的“保护域”或一个特殊Applet运行的安全性环境。远程系统检查原始的URL以及它下载的Applet的签名,和一个含有从特殊的Applet到特殊保护域的映射入口的本地文件进行比较。因此,来自特别位置的特殊Applet具有一些运行特权。 编写一个Applet 要编写一个Applet,必须首先用以下方式创建一个类: import java.applet.*; public class HelloWorld extends Applet { Applet的类必须为public,且它的名称必须与它所在的文件名匹配;在这里,就是HelloWorld.java。而且,该类必须为java.applet.Applet的子类。 Applet类的层次 Java.applet.Applet类实际上是java.awt.Panel的子类。Applet和AWT类的层次如下:  这种层次关系显示,一个Applet可直接用作一个AWT布局的起始点。因为Applet为一Panel,所以它有一个缺省的流(flow)布局管理器。Component,Container和Panel类的方法被Applet类继承了下来。 12.4.1 主要的Applet方法 在一个应用程序中,程序由main()方法处进入,而在一个Applet中却不是这样。在构造函数完成了它的任务后,浏览器调用init()对Applet进行基本的初始化操作。init()结束后,浏览器调用另一个称为start()的方法。本模块稍后将对start()做更细致的剖析;start()通常在Applet成为可见时被调用。 方法init()和start()都是在Applet成为“活动的”之前运行完成的,正因为这样,它们都不能用来编写Applet中继续下去的动作。实际上,与一个简单应用程序中的方法main()不同的是,没有什么方法的执行是贯穿于Applet的整个生命过程中的。你在后面将看到如何使用线程来实现这一特色。此外,你在编写Applet子类时可用的方法还有:stop(),destroy()和paint()。 12.4.2 Applet显示 Applet本质上是图形方式的,所以尽管你可以提出System.out.println()的调用请求,通常也不能这样做,而是应该在图形环境中创建你的显示。 你可以通过创建一个paint()方法在Applet的panel上绘图。只要Applet的显示需要刷新,paint()方法就会被浏览器环境调用。例如,当浏览器窗口被最小化或被要求以图标方式显示时,这种调用就会发生。 你应该编写自己的paint()方法,以使它在任何时候被调用时都能正常地工作。对它的调用是异步产生的,且由环境而不是程序来驱动。 12.4.3 paint()方法和图形对象 paint()方法带有一个参数,它是java.awt.Graphics类的一个实例。这个参数总是建立该Applet的panel的图形上下文。你能用这个上下文在你的Applet中绘图或写入文本。下面是使用paint()方法写出文字的一例。 1.import java.awt.*; 2.import java.applet.*; 3. 4.public class HelloWorld extends Applet { 5. 6.public void paint(Graphics g){ 7.g.drawString("Hello World!", 25, 25); 8.} 9.} 注-drawString方法的数字型参数为文本起始处的x和y的象素坐标。(0,0)表示左上角。这些坐标是针对字体的基线来讲的,所以在y坐标为0处写的结果是:文字的大部分在显示器顶部的上方,只有象字母y尾部那样的下面部分是可见的。 第五节 Applet的方法和Applet的生命周期 Applet的生命周期比所讨论的要稍微复杂一些。与其生命周期相关的有三个主要方法:init(),start()和stop()。 12.5.1 init() 本成员函数在Applet被创建并装入一个能支持Java技术的浏览器(如appletviewer)时被调用。Applet可用这个方法来初始化数据的值。本方法只在Applet首次装入时被调用,并且在调用start()之前执行完成。 12.5.2 start() init()方法一完成,start()就开始执行。它的执行使得Applet成为“活动”的。无论Applet何时成为可见的,它同样要执行一次,如:当浏览器在被图标化后又恢复时,或者当浏览器在链接到另一个URL后又返回含有这个Applet的页面时。这一方法的典型用法是启动动画和播放声音。 1.public void start() { 2.musicClip.play(); 3.} 12.5.3 stop() stop()方法是在Applet成为不可见时被调用的,这种情况一般在浏览器被图标化或链接到另一个URL时会出现。Applet用该方法使动画停止。 1.public void stop() { 2.musicClip.stop(); 3.} start()和stop()形成一对动作:典型地,start()激活Applet中的某一行为,而stop()则可将它禁止。 第六节 AWT绘图 除了基本的生命周期外,Applet还有与其显示有关的一些重要的方法。这些方法的声明和文档在AWT组件类中。使用AWT做显示处理时遵循正确的模型是非常重要的。 更新显示由一种被称为AWT线程的独立的线程来完成。这个线程可用来处理与显示更新相关的两种情况。 第一种情况是显露(exposure),它或在首次显示时,或在部分显示已被破坏而必须刷新时出现。显示的破坏可能发生在任何时刻,因此,你的程序必须能在任意时刻更新显示。 第二种情况是在程序重画带有新内容的画面时。这种重画可能会要求首先擦除原来的图像。 12.6.1 Paint(Graphics g)方法 显露处理自动地发生,且导致对paint()方法的一次调用。一种Graphics类的被称为裁剪矩形的设备常用于对paint()方法进行优化。除非必要,更新不会完全覆盖整个图形区域,而是严格限制在被破坏的范围内。 12.6.2 repaint()方法 对repaint()的调用可通知系统:你想改变显示,于是系统将调用paint()。 12.6.3 update(Graphics g)方法 repaint()实际上产生了一个调用另一方法update()的AWT线程。update方法通常清除当前的显示并调用paint()。update()方法可以被修改,如:为了减少闪烁可不清除显示而直接调用paint()。 12.6.4 方法的交互 下面的框图描述了paint(),update()和repaint()方法间的内在关系。  12.6.5 Applet的显示策略 Applet模型要求你采取一种特定的策略来维护你的显示: 维护一个显示模型。这个模型是对为再次提供显示而所需做的事情的一个定义。关于如何去做的指令在paint()方法中被具体化;这些指令所用的数据通常是全局成员变量。 使paint()提供仅仅基于该模型的显示。这使得无论paint()何时被调用,它都能以一致的方法再生该显示,并正确地处理显露问题。 使得程序对显示的改变,通过更新该模型而调用repaint()方法来进行,以使update()方法(最终是paint()方法)被AWT线程调用。 注-一个单一AWT线程处理所有的绘图组件和输入事件的分发。应保持paint()和update()的简单性,以避免它们使AWT线程发生故障的可能性更大;在极端情况下,你将需要其他线程的帮助以达到这一目的。有关线程的编程是模块14的主题。 第七节 什么是appletviewer? Applet通常运行于一个Web浏览器中,如HotJava TM或Netscape Navigator,它们有支持Java软件程序运行的能力。为了简化和加速开发过程,JDK应运而生,它附带有一个专为查看Applet而设计但不支持HTML页面查看的工具。这个工具就是appletviewer。 appletviewer是使你不必使用Web浏览器即可运行Applet的一个Java应用程序。它犹如一个“最小化的浏览器”。 appletviewer读取命令行中URL所指定的HTML文件。这个文件必须包含装入及执行一个或多个Applet的指令。appletviewer忽略了所有其他的HTML代码。它不能显示普通的HTML或嵌人在一个文本页中的Applet。 12.7.1 用appletviewer启动Applet appletviewer将一个框架样式的区域粘贴在屏幕上,然后实例化该Applet并将这个Applet实例贴在已有的框架中。 appletviewer带有一个命令行参数形式的URL,它指向一个含有Applet引用的HTML文件。这个Applet引用是一个指定了appletviewer要装载的代码的HTML标记。 <html> <applet code=HelloWorld.class width=100 height=100> </applet> </html> 注意,这个标记的通用格式与任何其他的HTML相同,即,用<和>两个符号来分隔指令。上例中显示的所有部分都是必需的,你必须使用<applet . . .>和</applet>。<applet . . .>部分指明了代码的入口,以及宽度和高度。 注-通常,你应该把Applet当作是固定大小的,并且使用<applet>标记中所指定的大小。 12.7.2 使用appletviewer 提要 appletviewer带有一个指向包含<applet>标记的HTML文件的URL,这个URL被作为命令行参数。 appletviewer [-debug] URLs ... appletviewer仅有的合法选项是 –debug,它使得Applet在Java调试器jdb中启动。若用带 –g选项的方式编译你的Java代码,则可在调试器中看到源代码。 范例 以如下所示的appletviewer命令启动appletviewer: c:\jdk1.2\source> appletviewer HelloWorld.html 于是它创建并显示出如下的小窗口:  第八节 Applet标记 12.8.1 句法 以下为Applet标记的完整句法: <applet [archive= archiveList] code= appletFile. class width= pixels height= pixels [codebase= codebaseURL ] [alt= alternateText ] [nam e= appletInstanceName ] [alig n= alignment ] [vspace = pixels ] [hspace= pixels ] > [<param name= appletAttribute1 value= value >] [<param name= appletAttribute2 value= value >] . . . [alternateHTML] </applet> 其中 archive = archiveList - 这一可选属性描述了一个或多个含有将被“预装”的类和其他资源的archives。类的装载由带有给定codebase的AppletClassLoader的一个实例来完成。ArchiveList中的archives以逗号(,)分隔。 code = appletFile.class - 这是一个必需的属性,它给定了含有已编译好的Applet子类的文件名。也可用package.appletFile.class的格式来表示。 注-这个文件与你要装入的HTML文件的基URL有关,它不能含有路径名。要改变Applet的基URL,可使用<codebase>。 width = pixels height = pixels - 这些必需的属性给出了Applet显示区域的初始宽度和高度(以象素为单位),不包括Applet所产生的任何窗口或对话框。 12.8.2 描述 codebase = codebaseURL - 这一可选属性指定了Applet的基URL――包含有Applet代码的目录。如果这一属性未指定,则采用文档的URL。 alt = alternateText - 这一可选属性指定了当浏览器能读取Applet标记但不能执行Java Applet时要显示的文本。 name = appletInstanceName - 这个可选属性为Applet实例指定有关名称,从而使得在同一页面上的Applet可找到彼此(以及互相通信)。 align = alignment - 这个可选属性指定了Applet的对齐方式。它的可取值与基本的HTML中IMG标记的相应属性相同,为:left,right,top,texttop,middle,absmiddle,baseline,bottom和absbottom。 vspace = pixels hspace = pixels - 这些可选属性指定了在Applet上下(vspace)及左右(hspace)的象素数目。其用法与IMG标记的vspace和hspace属性相同。 <param name = appletAttribute1 value = value> - 这个标记提供了一种可带有由“外部”指定的数值的Applet,它对一个Java应用程序的作用与命令行参数相同。Applet用getParameter()方法来存取它们的属性,该方法将在本模块稍后作更详细的讨论。 不支持Java程序执行的浏览器将显示被包括在<applet>和</applet>标记之间的任何常规的HTML;而可支持Java技术的浏览器则忽略介于这两个标记之间的HTML代码。 第九节 其他的Applet工具 在Applet中有若干其他特色。 所有的Java软件程序都具有访问网络的特色,这可使用模块15中所讲到的java.net包中的类来实现。此外,Applet还有些其他的方法可允许它们取得有关自己启动时所在的浏览器环境的信息。 类java.net.URL描述了URL,并可用于它们之间的连接。在Applet类中有两个方法决定了URL的重要的值: getDocumentBase()返回一个描述当前浏览器中带有Applet标记的HTML文件所属页面目录的URL对象 getCodeBase()返回一个描述Applet类文件本身源目录的URL对象。它通常与HTML文件目录相同,但并不是一定要这样。 其他的Applet特色 用URL对象作为一个起始点,你可以将声音和图像取回到你的Applet中。 getImage(URL base, String target)从被命名为target且位于由base所指定目录的文件中取回一幅图像。其返回值是类Image的一个实例。 getAudioClip(URL base, String target) 从被命名为target且位于由base所指定目录的文件中取回一声音。其返回值是类Audio Clip的一个实例。 注-getImage(URL, String)和getAudioClip(URL, String)方法中的String target能包括一个来自于URL的相对目录路径。但是请注意,在目录层次中向上的相对路径名,在某些系统上可能是不允许的。 第十节 一个简单的图像测试 下面的Applet获得了相对于getDocumentBase方法返回的目录路径为graphics/joe.gif的图像文件,并将它显示在appletviewer中: 1.// HelloWorld extended to draw an image 2.// Assumes existence of 3.//"graphics/SurferDuke.gif" 4.// 5.import java.awt.*; 6.import java.applet.Applet; 7. 8.public class HwImage extends Applet { 9.Image duke; 10. 11.public void init() { 12.duke = getImage(getDocumentBase(), 13."graphics/SurferDuke.gif"); 14.} 15. 16.public void paint(Graphics g) { 17.g.drawImage(duke, 25, 25, this); 18.} 19.} drawImage()方法的参数是: 将要被绘出的Image对象 绘图的x轴坐标 绘图的y轴坐标 图像观察者。图像观察者是可以得知该图像的状态是否改变的一个接口(如:在装入过程中发生了什么)。 由getImage()装载的图像在调用首次提出后过一段时间将会改变,这是由于装载是在后台完成的。每次,图像的更多部分被装入,paint()方法被又一次调用。这种对paint()方法调用的发生是因为Applet将自己作为drawImage()的第四个参数传递给了自己,从而使自己被注册为一个观察者。 第十一节 Audio Clips Java编程语言也具有播放Audio Clips的方法。这些方法在java.applet.AudioClip类中。为了播放Audio Clips,你将需要为你的计算机装配适当的硬件。 12.11.1 播放一段Clip 欣赏一段audio clip的最简单的方式是通过Applet的play方法: play(URL soundDirectory, String soundFile); 或更简单的: play(URL soundURL); 例如: play(getDocumentBase(), "bark.au"); 将播放存放在与HTML文件相同目录的bark.au。 12.11.2 一个简单的Audio测试 以下的Applet在appletviewer中打印出消息“Audio Test”,然后播放audio文件sounds/cuckoo.au: 1.// 2.// HelloWorld extended to play an Audio sound 3.// Assumes existence of "sounds/cuckoo.au" file 4.// 5. 6.import java.awt.Graphics; 7.import java.applet.Applet; 8. 9.public class HwAudio extends Applet { 10. 11.public void paint(Graphics g) { 12.g.drawString("Audio Test", 25, 25); 13.play(getDocumentBase(),"sounds/cuckoo.au"); 14.} 15.} 12.11.3 循环播放一段Audio Clip 你可以用与装入图像相同的方式装入audio clip。在将它们装载之后进行播放。 装入一段Audio Clip 为了装入一段Audio Clip,可使用来自java.applet.Applet类的getAudioClip方法: AudioClip sound; sound = getAudioClip(getDocumentBase(), "bark.au"); 一旦一段clip被装载,可选择与之相关的三个方法之一:play,loop,或stop。 播放Audio Clip 使用java.applet.AudioClip接口中的play方法将已装入的audio clip播放一遍: sound.play(); 为了启动clip的播放并使它不断循环(自动重放),可使用java.applet.AudioClip中的loop方法: sound. loop(); 停止Audio Clip 要停止一段正在播放的clip,可用java.applet.AudioClip中的stop方法: sound. stop(); 12.11.4 一个简单的Audio循环测试 下例中将一段装入的audio clip自动循环播放: 1.// 2.// HelloWorld extended to loop an audio track 3.// Assumes existence of "sounds/cuckoo.au" 4.// 5. 6.import java.awt.Graphics; 7.import java.applet.*; 8. 9.public class HwLoop extends Applet { 10.AudioClip sound; 11. 12.public void init() { 13.sound = getAudioClip(getDocumentBase(), 14."sounds/cuckoo.au"); 15.} 16. 17.public void paint(Graphics g) { 18.g.drawString("Audio Test", 25, 25); 19.} 20. 21.public void start() { 22.sound.loop(); 23.} 24. 25.public void stop() { 26.sound.stop(); 27.} 注-JDK1.2支持一种新的声音引擎,这个引擎提供了对MIDI文件和全部 .wav,aiff及 .au文件的回放功能。它给出了一个新方法newAudioClip(URL url),这个方法从给定的URL获取一段audio clip,参数URL指向该audio clip。第13行中的getAudioClip方法可用这个方法替换。NewAudioClip方法不需要第二个参数String,只有URL参数要求被传递。 第十二节 鼠标输入 Java编程语言所支持的最有用的特色之一是直接的交互动作。Java Applet,同应用程序一样,能注意到鼠标,并对鼠标事件作出反应。在这里,我们将对鼠标的支持作一次快速的回顾,以帮助理解下面的例子。 回想一下模块9中,JDK1.2事件模型对每一类交互动作都支持一种事件类型。鼠标事件由实现MouseListener接口的类来接收,它们可接收的事件为: mouseClicked -鼠标已被点击(鼠标按钮被按下然后被释放,作为一个动作) mouseEntered -鼠标光标进入一个组件 mouseExited -鼠标光标离开一个组件 mousePressed -鼠标按钮被按下 mouseReleased -鼠标按钮被释放 12.12.1 一个简单的Mouse测试 下面的程序显示了鼠标在Applet中点击的位置: 1.// 2.// HelloWorld extended to watch for mouse input 3.// "Hello World!" is reprinted at the location of 4.// the mouse click. 5.// 6. 7.import java.awt.*; 8.import java.awt.event.*; 9.import java.applet.Applet; 10. 11.public class HwMouse extends Applet 12.implements MouseListener { 13. 14.int mouseX=25; 15.int mouseY=25; 16. 17.// Register this applet instance to catch // MouseListener events 18.public void init () { 19.addMouseListener (this); 20.} 21. 22.public void paint(Graphics g) { 23.g.drawString("Hello World!", mouseX, mouseY); 24.} 25. 26.// Process the mousePressed MouseListener event 27.public void mousePressed(MouseEvent evt){ 28.mouseX = evt.getX(); 29.mouseY = evt.getY(); 30.repaint(); 31.} 32. 33.// We are not using the other mouse events 34.public void mouseClicked (MouseEvent e) {} 35.public void mouseEntered (MouseEvent e) {} 36.public void mouseExited (MouseEvent e) {} 37.public void mouseReleased (MouseEvent e) {} 38. 39.} 第十三节 读取参数 在一个HTML文件中,上下文为<applet>的<param>标记能够为Applet传递配置信息。例如: <applet code=DrawAny.class width=100 height=100> <param name=image value=duke.gif> </applet> 在这个Applet内部,你可用方法getParameter()来读取这些值。 1.import java.awt.*; 2.import java.applet.*; 3. 4.public class DrawAny extends Applet { 5.Image im; 6. 7.public void init() { 8.URL url = getDocumentBase(); 9.String imageName = getParameter( " image " ); 10.im = getImage(url, imageName); 11.} 12. 13.public void paint(Graphics g) { 14.g.drawImage(im, 0, 0, this); 15.} 16.} 读取参数 方法getParameter()搜索匹配的名称,并将与之相关的值以字符串的形式返回。 如果这个参数名称在位于<applet></applet>标记对中的任何<param>标记中都未找到,则getParameter()返回null。一个商业化程序应该很好地处理这种情况。 参数的类型都是String。如果你需要其他类型的参数,则必须自己做一些转换处理;例如,读取应为int类型的参数: int speed = Integer.parseInt (getParameter ( " speed " )); 由于HTML的本性,参数名称对大小写不敏感;但是,使它们全部为大写或小写是一种良好的风格。如果参数值的字符串中含有空格,则应把整个字符串放入双引号中。值的字符串对大小写敏感;不论是否使用双引号,它们的大小写都保持不变。 第十四节 双重目的代码 是可以在一个单一的类文件中创建既可用作Java Applet,又可用作Java应用程序的Java软件代码。为了理解应用程序的要求,需要做较多的工作,但是一旦已经创建,Applet/应用程序代码可作为一个更复杂程序的模板来使用。 1.// Applet/Application which shows an image of Duke in 2.// surfing mode 3.import java.applet.Applet; 4.import java.awt.*; 5.import java.awt.event.*; 6.import java.util.*; 7. 8.public class AppletApp extends Applet { 9. 10.Date date; 11. 12.// An application will require a main() 13.public static void main (String args[]) { 14. 15.// Create a Frame to house the applet 16.Frame frame = new Frame("Application"); 17. 18.// Create an instance of the class (applet) 19.AppletApp app = new AppletApp(); 20. 21.// Add it to the center of the frame 22.frame.add(app, BorderLayout.CENTER); 23.frame.setSize (250, 150); 24. 25.// Register the AppletApp class as the 26.// listener for a Window Destroy event 27.frame.addWindowListener (new WindowAdapter() { 28.public void windowClosing (WindowEvent e) { 29.System.exit(0); 30.} 31.} ); 32. 33.// Call the applet methods 1.app.init(); 2.app.start(); 3.frame.setVisible(true); // Invokes paint() 4.} 5. 6.public void init() { 7.date = new Date(); 8.} 9. 10.public void paint (Graphics g) { 11.g.drawString("This Java program started at", 25, 25); 12.g.drawString(date.toString(), 25, 60); 13.} 14.} 注-应用程序没有浏览器所提供的资源,因此不能使用getImage()或getAudioClip()。 练习:创建Applet 练习目标 - 在本实验中,你将熟悉Applet编程,尤其是用于屏幕更新和刷新的paint()方法。 一、准备 为了成功地完成本实验,你必须能够用浏览器来显示一个Applet。 二、任务 水平1:编写一个Applet 打开一个新的外壳程序或Command Tool窗口。 用一个文本编辑器,键入HwMouse.java程序或从course_example目录拷贝它。 修改这个程序,使得你在Applet中点击时,它可以循环显示三种不同的消息。 编译HwMouse.java.java程序 c:\student> HwMouse.java 用一个文本编辑器,创建一个HwMouse.html文件,文件中含有调用HwMouse.class程序的<applet>标记。 用appletviewer命令测试你的Applet。 c:\student> HwMouse.html 水平2:创建同心的正方形 创建一个Applet,Squares.java,它产生一系列如下图的同心正方形(或圆形): 试图使每个正方形(或圆形)为一种不同的颜色。如果你导入java.awt.Color类,则可用setColor方法对Java applet加入色彩。 import java.awt.Color; . . . public void paint(Graphics g) { g.setColor(Color.blue); g.drawRect(5, 5, 50, 50); . . . } 水平3:创建一个滚动的Java applet 编写一个Applet,显示一幅图像,并在鼠标经过该图像时播放一个声音。 三、练习小结 讨论 - 花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。 经验 解释 总结 应用 四、检查你的进度 在进入下一个模块的学习之前,请确认你能够: 区分独立应用程序和Applet 编写一个HTML标记来调用Java Applet 描述Applet和AWT的类层次树 创建HelloWorld.Java Applet 列出Applet的主要方法 描述和使用AWT的绘图模型 使用Applet方法从URL读取图像和文件 使用<param>标记配置Applet 五、思考题 如何把Applet应用在你公司的Web页面上,以改进它的整体表现效果? 线 程 本模块讨论多线程,它允许一个程序同时执行多个任务。  第一节 相关问题 讨论 - 以下为与本模块内容有关的问题: 我如何使我的程序执行多个任务? 第二节 目 标 在完成了本模块的学习后,你应当能够: 定义一个线程 在一个Java程序中创建若干分离的线程,控制线程使用的代码和数据 控制线程的执行,并用线程编写独立于平台的代码 描述在多个线程共享数据时可能会碰到的困难 使用synchronized关键字保护数据不受破坏 使用wait()和notify()使线程间相互通信 解释为什么在JDK1.2中不赞成使用suspend()、resume()和stop()方法? 第三节 线 程 13.3.1 什么是线程? 一个关于计算机的简化的视图是:它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器)、包含程序所要操作的数据的RAM(只读存储器)。在这个简化视图中,只能执行一个作业。一个关于最现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。 你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。如果你要执行一个以上的作业,这类似有一台以上的计算机。在这个模型中,线程或执行上下文,被认为是带有自己的程序代码和数据的虚拟处理机的封装。java.lang.Thread类允许用户创建并控制他们的线程。 注-在这个模块中,使用“Thread”时是指java.lang.Thread而使用“thread”时是指执行上下文。 13.3.2 线程的三个部分 进程是正在执行的程序。一个或更多的线程构成了一个进程。一个线程或执行上下文由三个主要部分组成 一个虚拟处理机 CPU执行的代码 代码操作的数据 代码可以或不可以由多个线程共享,这和数据是独立的。两个线程如果执行同一个类的实例代码,则它们可以共享相同的代码。 类似地,数据可以或不可以由多个线程共享,这和代码是独立的。两个线程如果共享对一个公共对象的存取,则它们可以共享相同的数据。 在Java编程中,虚拟处理机封装在Thread类的一个实例里。构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。 第四节 Java编程中的线程 13.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); 此时,这两个线程共享数据和代码。 总之,线程通过Thread对象的一个实例引用。线程从装入的Runnble实例的run()方法开始执行。线程操作的数据从传递给Thread构造函数的Runnable的特定实例处获得。 13.4.2 启动线程 一个新创建的线程并不自动开始运行。你必须调用它的start()方法。例如,你可以发现上例中第4行代码中的命令: t.start(); 调用start()方法使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。 13.4.3 线程调度 一个Thread对象在它的生命周期中会处于各种不同的状态。下图形象地说明了这点: 尽管线程变为可运行的,但它并不立即开始运行。在一个只带有一个 处理机的机器上,在一个时刻只能进行一个动作。下节描述了如果有一个以上可运行线程时,如何分配处理机。 在Java中,线程是抢占式的,但并不一定是分时的 (一个常见的错误是认为“抢占式”只不过是“分时”的一种新奇的称呼而已) 。 抢占式调度模型是指可能有多个线程是可运行的,但只有一个线程在实际运行。这个线程会一直运行,直至它不再是可运行的,或者另一个具有更高优先级的线程成为可运行的。对于后面一种情形,低优先级线程被高优先级线程抢占了运行的机会。 一个线程可能因为各种原因而不再是可运行的。线程的代码可能执行了一个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()的参数指定以毫秒为单位的线程最小休眠时间。除非线程因为中断而提早恢复执行,否则它不会在这段时间之前恢复执行。 Thread类的另一个方法yield(),可以用来使具有相同优先级的线程获得执行的机会。如果具有相同优先级的其它线程是可运行的,yield()将把调用线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行进程,yield()什么都不做。 注意sleep()调用会给较低优先级线程一个运行的机会。yield()方法只会给相同优先级线程一个执行的机会。 第五节 线程的基本控制 13.5.1 终止一个线程 当一个线程结束运行并终止时,它就不能再运行了。 可以用一个指示run()方法必须退出的标志来停止一个线程。 1.public class Xyz implements Runnable { 2.private boolean timeToQuit=false; 3. 4.public void run() { 5.while(! timeToQuit) { 6.... 7.} 8.// clean up before run() ends 9.} 10. 11.public void stopRunning() { 12.timeToQuit=true; 13.} 14.} 15. 16.public class ControlThread { 17.private Runnable r = new Xyz(); 18.private Thread t = new Thread(r); 19. 20.public void startThread() { 21.t.start(); 22.} 23. 24.public void stopThread() { 25.// use specific instance of Xyz 26.r.stopRunning(); 27.} 28.} 在一段特定的代码中,可以使用静态Thread方法currentThread()来获取对当前线程的引用,例如: 1.public class Xyz implements Runnable { 2.public void run() { 3.while (true) { 4.// lots of interesting stuff 5.// Print name of the current thread 6.System.out.println("Thread" + 7.Thread.currentThread().getName()+ 8."completed"); 9.} 10.} 11.} 13.5.2 测试一个线程 有时线程可处于一个未知的状态。isAlive()方法用来确定一个线程是否仍是活的。活着的线程并不意味着线程正在运行;对于一个已开始运行但还没有完成任务的线程,这个方法返回true。 13.5.3 延迟线程 存在可以使线程暂停执行的机制。也可以恢复运行,就好象什么也每发生过一样,线程看上去就象在很慢地执行一条指令。 sleep() sleep()方法是使线程停止一段时间的方法。在sleep时间间隔期满后,线程不一定立即恢复执行。这是因为在那个时刻,其它线程可能正在运行而且没有被调度为放弃执行,除非 (a)“醒来”的线程具有更高的优先级 (b)正在运行的线程因为其它原因而阻塞 1.public class Xyz implements Runnable { 2.public void run() { 3.while (true) { 4.// lots of interesting stuff 5.// Print name of the current thread 6.System.out.println("Thread" + 7.Thread.currentThread().getName()+ 8."completed"); 9.} 10.} 11.} join() join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。例如: public void doTask() { TimerThread tt = new TimerThread (100); tt.start (); ... // Do stuff in parallel with the other thread for // a while ... // Wait here for the timer thread to finish try { tt.join (); } catch (InterruptedException e) { // tt came back early } ... // Now continue in this thread ... } 可以带有一个以毫秒为单位的时间值来调用join方法,例如: void join (long timeout); 其中join()方法会挂起当前线程。挂起的时间或者为timeout毫秒,或者挂起当前线程直至它所调用的线程终止。 第六节 创建线程的其它方法 到目前为止,你已经知道如何用实现了Runnable的分离类来创建线程上下文。事实上,这不是唯一的方法。Thread类自身实现了Runnable接口,所以可以通过扩展Thread类而不是实现Runnable来创建线程。 1.public class MyThread extends Thread { 2.public void run() { 3.while (running) { 4.// do lots of interesting stuff 5.try { 6.sleep(100); 7.} catch (InterruptedException e) { 8.// sleep interrupted 9.} 10.} 11.} 12. 13.public static void main(String args[]) { 14.Thread t = new MyThread(); 15.t.start(); 16.} 17.} 13.6.1 使用那种方法? 给定各种方法的选择,你如何决定使用哪个?每种方法都有若干优点。 实现Runnable的优点 从面向对象的角度来看,Thread类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承类。正因为这个原因和区别一个正在运行的线程的处理机、代码和数据部分的意义,本教程采用了这种方法。 由于Java技术只允许单一继承,所以如果你已经继承了Thread,你就不能再继承其它任何类,例如Applet。在某些情况下,这会使你只能采用实现Runnable的方法。 因为有时你必须实现Runnable,所以你可能喜欢保持一致,并总是使用这种方法。 继承Thread的优点 当一个run()方法体现在继承Thread类的类中,用this指向实际控制运行的Thread实例。因此,代码不再需要使用如下控制: Thread.currentThread().join(); 而可以简单地用: join(); 因为代码简单了一些,许多Java编程语言的程序员使用扩展Thread的机制。注意:如果你采用这种方法,在你的代码生命周期的后期,单继承模型可能会给你带来困难。 第七节 使用Java技术中的synchronized 本节讨论关键字synchronized的使用。它提供Java编程语言一种机制,允许程序员控制共享数据的线程。 13.7.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| | | | idx = 2 ^ 注意这个配置隐含了:“q”是有效的,而含有“r”的单元是下一个空单元。也就是说,读取“q”时,它就象被两次推入了栈,而字母“r”则永远不会出现。 这是一个当多线程共享数据时会经常发生的问题的一个简单范例。需要有机制来保证共享数据在任何线程使用它完成某一特定任务之前是一致的。 注-有一种方法可以保证线程a在执行完成关键部分的代码时不被调出。这种方法常用在底层的机器语言编程中,但不适合多用户系统。 注-另外一种方法,它可以被Java技术采用。这种方法提供精细地处理数据的机制。这种方法允许无论线程是否会在执行存取的中间被调出,线程对数据的存取都是不可分割的, 13.7.2 对象锁标志 在Java技术中,每个对象都有一个和它相关联的标志。这个标志可以被认为是“锁标志”。 synchronized关键字使能和这个标志的交互,即允许独占地存取对象。看一看下面修改过的代码片断: public void push(char c) { synchronized(this) { data[idx] = c; idx++; } } 当线程运行到synchronized语句,它检查作为参数传递的对象,并在继续执行之前试图从对象获得锁标志。 对象锁标志 意识到它自身并没有保护数据是很重要的。因为如果同一个对象的pop()方法没有受到synchronized的影响,且pop()是由另一个线程调用的,那么仍然存在破坏data的一致性的危险。如果要使锁有效,所有存取共享数据的方法必须在同一把锁上同步。 下图显示了如果pop()受到synchronized的影响,且另一个线程在原线程持有那个对象的锁时试图执行pop()方法时所发生的事情:  当线程试图执行synchronized(this)语句时,它试图从this对象获取锁标志。由于得不到标志,所以线程不能继续运行。然后,线程加入到与那个对象锁相关联的等待线程池中。当标志返回给对象时,某个等待这个标志的线程将得到这把锁并继续运行。 13.7.3 释放锁标志 由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行,所以让持有锁标志的线程在不再需要的时候返回标志是很重要的。 锁标志将自动返回给它的对象。持有锁标志的线程执行到synchronized()代码块末尾时将释放锁。Java技术特别注意了保证即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。此外,如果一个线程对同一个对象两次发出synchronized调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。 这些规则使得与其它系统中的等价功能相比,管理同步块的使用简单了很多。 13.7.4 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)做到这点。 13.7.5 死锁 如果程序中有多个线程竞争多个资源,就可能会产生死锁。当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。在这种情况下,除非另一个已经执行到synchronized块的末尾,否则没有一个线程能继续执行。由于没有一个线程能继续执行,所以没有一个线程能执行到块的末尾。 Java技术不监测也不试图避免这种情况。因而保证不发生死锁就成了程序员的责任。避免死锁的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序。按照与获取相反的次序释放锁。 第八节 线程交互-wait()和notify() 经常创建不同的线程来执行不相关的任务。然而,有时它们所执行的任务是有某种联系的,为此必须编写使它们交互的程序。 13.8.1 场景 把你自己和出租车司机当作两个线程。你需要出租车司机带你到终点,而出租车司机需要为乘客服务来获得车费。所以,你们两者都有一个任务。 13.8.2 问题 你希望坐到出租车里,舒服地休息,直到出租车司机告诉你已经到达终点。如果每2秒就问一下“我们到了哪里?”,这对出租车司机和你都会是很烦的。出租车司机想睡在出租车里,直到一个乘客想到另外一个地方去。出租车司机不想为了查看是否有乘客的到来而每5分钟就醒来一次。所以,两个线程都想用一种尽量轻松的方式来达到它们的目的。 13.8.3 解决方案 出租车司机和你都想用某种方式进行通信。当你正忙着走向出租车站时,司机正在车中安睡。当你告诉司机你想坐他的车时,司机醒来并开始驾驶,然后你开始等待并休息。到达终点时,司机会通知你,所以你必须继续你的任务,即走出出租车,然后去工作。出租车司机又开始等待和休息,直到下一个乘客的到来。 13.8.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()的调用不会被存储。 13.8.5 同步的监视模型 协调两个需要存取公共数据的线程可能会变得非常复杂。你必须非常小心,以保证可能有另一个线程存取数据时,共享数据的状态是一致的。因为线程不能在其他线程在等待这把锁的时候释放合适的锁,所以你必须保证你的程序不发生死锁, 在出租车范例中,代码依赖于一个同步对象――出租车,在其上执行wait()和notify()。如果有任何人在等待一辆公共汽车,你就需要一个独立的公共汽车对象,在它上面施用notify()。记住,在同一个等待池中的所有线程都因来自等待池的控制对象的通知而满足。永远不要设计这样的程序:把线程放在同一个等待池中,但它们却在等待不同条件的通知。 13.8.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 } } } 上面这个程序从栈中取出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表示。 你将看到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块后,才会释放对象的锁,所以当栈数据在被改变时,正在等待锁的线程不会获得这个锁。 13.8.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. 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; Producer.java(续) 1. 2.public Producer (SyncStack s) { 3.theStack = s; 4.num = counter++; 5.} 6. 7.public void run() { 8.char c; 9. 10.for (int i = 0; i < 200; i++) { 11.c = (char)(Math.random() * 26 + `A'); 12.theStack.push(c); 13.System.out.println("Producer" + num + ": " + c); 14.try { 15.Thread.sleep((int)(Math.random() * 300)); 16.} catch (InterruptedException e) { 17.// ignore it 18.} 19.} 20.} 21.} 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; 6. 7.public Consumer (SyncStack s) { 8.theStack = s; 9.num = counter++; 10.} 11. 12.public void run() { Consumer.java(续) 1.char c; 2. 3.for (int i=0; i < 200; i++) { 4.c = theStack.pop(); 5.System.out.println("Consumer" + num + ": " + c); 6.try { 7.Thread.sleep((int)(Math.random() * 300)); 8.} catch (InterruptedException e) { 9.// ignore it 10.} 11.} 12.} 13.} 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(); SyncStack.java(续) 1.return c; 2.} 3. 4.public synchronized void push(char c) { 5.this.notify(); 6. 7.Character charObj = new Character(c); 8.buffer.addelement(charObj); 9.} 10.} 运行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 第九节 JDK1.2中的线程控制 13.9.1 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()之前稳定任何数据的机会。 13.9.2 stop()方法 stop()方法的情形是类似的,但结果有所不同。如果一个线程在持有一个对象锁的时候被停止,它将在终止之前释放它持有的锁。这避免了前面所讨论的死锁问题,但它又引入了其他问题。 在前面的范例中,如果线程在已将字符加入栈但还没有使下标值加1之后被停止,你在释放锁的时候会得到一个不一致的栈结构。 总会有一些关键操作需要不可分割地执行,而且在线程执行这些操作时被停止就会破坏操作的不可分割性。 一个关于停止线程的独立而又重要的问题涉及线程的总体设计策略。创建线程来执行某个特定作业,并存活于整个程序的生命周期。换言之,你不会这样来设计程序:随意地创建和处理线程,或创建无数个对话框或socket端点。每个线程都会消耗系统资源,而系统资源并不是无限的。这并不是暗示一个线程必须连续执行;它只是简单地意味着应当使用合适而安全的wait()和notify()机制来控制线程。 13.9.3 合适的线程控制 既然你已经知道如何来设计具有良好行为的线程,并使用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.} 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()方法)。 关于此问题更详细的讨论已超出本模块的范围。 练习:使用多线程编程 练习目标-在这个练习中,你将通过编写一些多线程的程序来熟悉多线程的概念。创建一个多线程的Applet。 一、准备 为了很好地完成这个练习,你必须理解本模块中讨论的多线程概念。 二、任务 水平1:创建三个线程 1. 创建简单的程序ThreeThreads.java,它将创建三个线程。每个线程应当显示它所运行的时间。(考虑使用Date()类) 水平2:使用动画 创建一个Applet ThreadedAnimation.java,它读取10幅DukeTM waving图像(在graphics/Duke目录中)并按照Duke波动的顺序来显示它们。 用MediaTracker类使这些图像的装载更平滑。 允许用户连续点击鼠标来停止和启动动画。 三、练习小结 讨论-花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。 经验 解释 总结 应用 四、检查你的进度 在进入下一个模块的学习之前,请确认你能够: 定义一个线程 在一个Java程序中创建若干分离的线程,控制线程使用的代码和数据 控制线程的执行,并用线程编写独立于平台的代码 描述在多个线程共享数据时可能会碰到的困难 使用synchronized关键字保护数据不受破坏 使用wait()和notify()使线程间相互通信 使用synchronized关键字保护数据不受破坏 解释为什么在JDK1.2中不赞成使用suspend()、resume()和stop()方法? 五、思考 你是否有受益于多线程的应用程序? 流式I/O和文件 本模块讨论文件,socket和其他数据源使用的流式I/O机制。  第一节 相关问题 讨论 - 以下为与本模块内容有关的问题: Java编程语言中使用什么机制来读写文件? 第二节 目 标 在完成了本模块的学习后,你应当能够: 描述和使用java.io包的流式思想 构造文件和过滤器流,并恰当地使用它们 区别流与读者和作者,并进行合适的选择 考察并操作文件和目录 读、写和更新文本和数据文件 使用Serialization接口来保持对象的状态 第三节 流式I/O 本模块考察了Java编程语言如何使用流来处理字节和字符I/O(包括stdio,stdout和stderr)。下面几节将考察有关处理文件和操作它们所包含的数据的特定细节。 14.3.1 流的基础知识 一个流是字节的源或目的。次序是有意义的。例如,一个需要键盘输入的程序可以用流来做到这一点。 两种基本的流是:输入流和输出流。你可以从输入流读,但你不能对它写。要从输入流读取字节,必须有一个与这个流相关联的字符源。 在java.io包中,有一些流是结点流,即它们可以从一个特定的地方读写,例如磁盘或者一块内存。其他流称作过滤器。一个过滤器输入流是用一个到已存在的输入流的连接创建的。此后,当你试图从过滤输入流对象读时,它向你提供来自另一个输入流对象的字符。  14.3.2 InputStream方法 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()方法来返回你标记的输入点。 14.3.3 OutputStream方法 void write(int) void write(byte []) void write(byte [], int, int) 这些方法写输出流。和输入一样,总是尝试以实际最大的块进行写操作。 void close() 当你完成写操作后,就关闭输出流。如果你有一个流所组成的栈,就关闭栈顶部的流。这个关闭操作会关闭其余的流。 void flush() 有时一个输出流在积累了若干次之后才进行真正的写操作。flush()方法允许你强制执行写操作。 第四节 基本的流类 在java.io包中定义了一些流类。下图表明了包中的类层次。一些更公共的类将在后面介绍。  14.4.1 FileInputStream和FileOutputStream 这些类是结点流,而且正如这个名字所暗示的那样,它们使用磁盘文件。这些类的构造函数允许你指定它们所连接的文件。要构造一个FileInputStream,所关联的文件必须存在而且是可读的。如果你要构造一个FileOutputStream而输出文件已经存在,则它将被覆盖。 FileInputStream infile = new FileInputStream("myfile.dat"); FileOutputStream outfile = new FileOutputStream("results.dat"); 14.4.2 BufferInputStream和BufferOutputStream 这些是过滤器流,它们可以提高I/O操作的效率。 14.4.3 DataInputStream和DataOutputStream 这些过滤器通过流来读写Java基本类。例如: DataInputStream方法 byte readByte() long readLong() double readDouble() DataOutputStream方法 void writeByte(byte) void writeLong(long) void writeDouble(double) 注意DataInputStream和DataOutputStream的方法是成对的。 这些流都有读写字符串的方法,但不应当使用这些方法。它们已经被后面所讨论的读者和作者所取代。 14.4.4 PipedInputStream和PipedOutputStream 管道流用来在线程间进行通信。一个线程的PipedInputStream对象从另一个线程的PipedOutputStream对象读取输入。要使管道流有用,必须有一个输入方和一个输出方。 第五节 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"); 14.5.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存取文件。 第六节 读者和作者  14.6.1 Unicode Java技术使用Unicode来表示字符串和字符,而且它提供了16位版本的流,以便用类似的方法来处理字符。这些16位版本的流称为读者和作者。和流一样,它们都在java.io包中。 读者和作者中最重要的版本是InputStreamReader和OutputStreamWriter。这些类用来作为字节流与读者和作者之间的接口。 当你构造一个InputStreamReader或OutputStreamWriter时,转换规则定义了16位Unicode和其它平台的特定表示之间的转换。 14.6.2 字节和字符转换 缺省情况下,如果你构造了一个连接到流的读者和作者,那么转换规则会在使用缺省平台所定义的字节编码和Unicode之间切换。在英语国家中,所使用的字节编码是:ISO 8859-1。 你可以使用所支持的另一种编码形式来指定其它的字节编码。在native2ascii工具中,你可以找到一个关于所支持的编码形式的列表。 使用转换模式,Java技术能够获得本地平台字符集的全部灵活性,同时由于内部使用Unicode,所以还能保持平台独立性。 14.6.3 缓冲读者和作者 因为在各种格式之间进行转换和其它I/O操作很类似,所以在处理大块数据时效率最高。在InputStreamReader和OutputStreamWriter的结尾链接一个BufferedReader和BufferedWriter是一个好主意。记住对BufferedWriter使用flush()方法。 14.6.4 读入字符串输入 下面这个例子说明了从控制台标准输入读取字符串所应当使用的一个技术。 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.} 14.6.5 使用其它字符转换 如果你需要从一个非本地(例如,从连接到一个不同类型的机器的网络连接读取)的字符编码读取输入,你可以象下面这个程序那样,使用显式的字符编码构造ir=new InputStreamReader(System.in, “8859_1”); 注-如果你通过网络连接读取字符,就应该使用这种形式。否则,你的程序会总是试图将所读取的字符当作本地表示来进行转换,而这并不总是正确的。ISO 8859-1是映射到ASCII的Latin-1编码模式。 第七节 文 件 14.7.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对象的构造函数参数。这是一种推荐方法,因为它独立于本地文件系统的约定。 第八节 文件测试和工具 当你创建一个File对象时,你可以使用下面任何一种方法来获取有关文件的信息: 14.8.1 文件名 String getName() String getPath() String getAbsolutePath() String getParent() boolean renameTo(File newName) 14.8.2 文件测试 boolean exists() boolean canWrite() boolean canRead() boolean isFile() boolean isDirectory() boolean isAbsolute() 14.8.3 通用文件信息和工具 long lastModified() long length() boolean delete() 14.8.4 目录工具 boolean mkdir() String[] list() 随机存取文件 14.9.1 创建一个随机存取文件 你经常会发现你只想读取文件的一部分数据,而不需要从头至尾读取整个文件。你可能想访问一个作为数据库的文本文件,此时你会移动到某一条记录并读取它的数据,接着移动到另一个记录,然后再到其他记录――每一条记录都位于文件的不同部分。Java编程语言提供了一个RandomAccessFile类来处理这种类型的输入输出。 你可以用如下两种方法来打开一个随机存取文件: 用文件名 myRAFile = new RandomAccessFile(String name, String mode); 用文件对象 myRAFile = new RandomAccessFile(File file, String mode); mode参数决定了你对这个文件的存取是只读(r)还是读/写(rw)。 例如,你可以打开一个打开一个数据库文件并准备更新: RandomAccessFile myRAFile; myRAFile = new RandomAccessFile("db/stock.dbf","rw"); 14.9.2 存取信息 RandomAccessFile对象按照与数据输入输出对象相同的方式来读写信息。你可以访问在DataInputStrem和DataOutputStream中所有的read()和write()操作。 Java编程语言提供了若干种方法,用来帮助你在文件中移动。 long getFilePointer(); 返回文件指针的当前位置。 void seek(long pos); 设置文件指针到给定的绝对位置。这个位置是按照从文件开始的字节偏移量给出的。位置0标志文件的开始。 long length() 返回文件的长度。位置length()标志文件的结束。 14.9.3 添加信息 你可以使用随机存取文件来得到文件输出的添加模式。 myRAFile = new RandomAccessFile("java.log","rw"); myRAFile.seek(myRAFile.length()); // Any subsequent write()s will be appended to the file 第十节 串行化 从JDK1.1开始具有的新特性包括导入java.io.Serializable接口和改变JVM使之支持将一个Java技术对象存放到一个流的能力。 将一个对象存放到某种类型的永久存储器上称为保持。如果一个对象可以被存放到磁盘或磁带上,或者可以发送到另外一台机器并存放到存储器或磁盘上,那么这个对象就被称为可保持的。 java.io.Serializable接口没有任何方法,它只作为一个“标记者”,用来表明实现了这个接口的类可以考虑串行化。类中没有实现Serializable的对象不能保存或恢复它们的状态。 14.10.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; 第十一节 读写一个对象流 14.11.1 写 对一个文件流读写对象是一个简单的过程。考虑如下代码段,它将一个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.} 14.11.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[]) { 20.new UnSerializeDate(); 21.} 22.} 练习:熟悉I/O 练习目标-在这个练习中,你将熟悉通过编写执行文件I/O的程序来熟悉流式I/O。 一、准备 你应当理解数据库和向流写入数据的基本概念。 二、任务 水平1:打开文件 1. 创建一个称为DisplayFile.java的Java应用程序,它将打开、读取并显示任何可读文件的内容。 2. 在你的应用程序中包含合适的异常处理,使之在不能显示文件时提示合适的出错信息。 水平2:创建一个简单的数据库程序 创建一个称为DBTest.java的应用程序,它模仿了一个能存储和获取产品记录的小型数据库程序。使用RandomAccessFile类和平坦式文件。 数据库中的记录应当由字符串名称和整数量组成。 你的程序应当允许用户显示、更新和添加记录。 水平3:使用保持 采用模块9中的Paint程序,并将画布的状态保存到一个文件中。 三、练习小结 讨论 - 花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。 经验 解释 总结 应用 四、检查一下你的进度 在进入下一个模块的学习之前,请确认你能够: 描述和使用java.io包的流式思想 构造文件和过滤器流,并恰当地使用它们 区别流与读者和作者,并进行合适的选择 考察并操作文件和目录 读、写和更新文本和数据文件 使用Serialization接口来保持对象的状态 五、思考题 你是否有需要I/O的应用程序? 网 络 本模块讨论了JDK对socket和socket编程的支持。socket编程用来与在相同的网络上的另一台计算机上运行的程序进行通信。  第一节 相关问题 讨论 - 以下为与本模块内容有关的问题: 如何在网络上建立客户机与服务器之间的通信链路? 第二节 目 标 在完成本模块之后,你应当能够: 开发代码来建立网络连接 理解TCP/IP和UDP协议 用ServerSocket和Socket类来实现TCP/IP客户和服务器 用DatagramPacket和DatagramSocket来有效地进行基于UDP的网络通信。 第三节 网 络 15.3.1 socket socket是指在一个特定编程模型下,进程间通信链路的端点。因为这个特定编程模型的流行,socket这个名字在其他领域得到了复用,包括Java技术。 当进程通过网络进行通信时,Java技术使用它的流模型。一个socket包括两个流:一个输入流和一个输出流。如果一个进程要通过网络向另一个进程发送数据,只需简单地写入与socket相关联的输出流。一个进程通过从与socket相关联的输入流读来读取另一个进程所写的数据。 建立网络连接之后,使用与socket相关联的流和使用其他流是非常相似的。 15.3.2 建立连接 如果要建立连接,一台机器必须运行一个进程来等待连接,而另一台机器必须试图到达第一台机器。这和电话系统类似;一方必须发起呼叫,而另一方在此时必须等待电话呼叫。 第四节 Java技术中的网络 15.4.1 连接的地址 你发起电话呼叫时,你必须知道所拨的电话号码。如果要发起网络连接,你需要知道远程机器的地址或名字。此外,每个网络连接需要一个端口号,你可以把它想象成电话的分机号码。一旦你和一台计算机建立连接,你需要指明连接的目的。所以,就如同你可以使用一个特定的分机号码来和财务部门对话那样,你可以使用一个特定的端口号来和会计程序通信。 15.4.2 端口号 TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。实际上,小于1024的端口号保留给预定义的服务,而且除非要和那些服务之一进行通信(例如telnet,SMTP邮件和ftp等),否则你不应该使用它们。 客户和服务器必须事先约定所使用的端口。如果系统两部分所使用的端口不一致,那就不能进行通信。 15.4.3 Java网络模型 在Java编程语言中,TCP/IP socket连接是用java.net包中的类实现的。下图说明了服务器和客户端所发生的动作。  服务器分配一个端口号。如果客户请求一个连接,服务器使用accept()方法打开socket连接。 客户在host的port端口建立连接。 服务器和客户使用InputStream和OutputStream进行通信。 15.4.4 最小TCP/IP服务器 TCP/IP服务器应用程序依靠Java技术语言提供的网络类。ServerSocket类完成了建立一个服务器所需的大部分工作。 1.import java.net.*; 2.import java.io.*; 3. 4.public class SimpleServer { 5.public static void main(String args[]) { 6.ServerSocket s = null; 7.Socket s1; 8.String sendString = "Hello Net World!"; 9.OutputStream s1out; 10.DataOutputStream dos; 11. 12.// Register your service on port 5432 13.try { 14.s = new ServerSocket(5432); 15.} catch (IOException e) { } 16. 17.// Run the listen/accept loop forever 18.while (true) { 19.try { 20.// Wait here and listen for a connection 21.s1=s.accept(); 22. 23.// Get a communication stream for soocket 24.s1out = s1.getOutputStream(); 25.dos = new DataOutputStream (s1out); 26. 27.// Send your string! (UTF provides machine-independent format) 28.dos.writeUTF(sendString); 29. 30.// Close the connection, but not the server socket 31.s1out.close(); 32.s1.close(); 33.} catch (IOException e) { } 34.} 35.} 36.} 15.4.5 最小TCP/IP客户 一个TCP/IP应用程序的客户方依靠Socket类。Socket类完成了建立一个连接所需的大部分工作。客户连接到上一页所示的服务器上,并将服务器发送的所有数据显示在控制台上。 1.import java.net.*; 2.import java.io.*; 3. 4.public class SimpleClient { 5.public static void main(String args[]) throws IOException { 6.int c; 7.Socket s1; 8.InputStream s1In; 9.DataInputStream dis; 10. 11.// Open your connection to sunbert, at port 5432 12.s1 = new Socket("sunbert",5432); 13. 14.// Get an input file handle from the socket and read the input 15.s1In = s1.getInputStream(); 16.dis = new DataInputStream(s1In); 17. 18.String st = new String (dis.readUTF()); 19.System.out.println(st); 20. 21.// When done, just close the connection and exit 22.s1In.close(); 23.s1.close(); 24.} 25.} 第五节 UDP socket TCP/IP是面向连接的协议。而用户数据报协议(UDP)是一种无连接的协议。要区分这两种协议,一种很简单而又很贴切的方法是把它们比作电话呼叫和邮递信件。 电话呼叫保证有一个同步通信;消息按给定次序发送和接收。而对于邮递信件,即使能收到所有的消息,它们的顺序也可能不同。 用户数据报协议(UDP)由Java软件的DatagramSocket和DatagramPacket类支持。包是自包含的消息,它包括有关发送方、消息长度和消息自身。 15.5.1 DatagramPacket DatagramPacket有两个构造函数:一个用来接收数据,另一个用来发送数据: DatagramPacket(byte [] recvBuf, int readLength)-用来建立一个字节数组以接收UDP包。byte数组在传递给构造函数时是空的,而int值用来设定要读取的字节数(不能比数组的大小还大)。 DatagramPacket(byte [] sendBuf, int sendLength, InetAddress iaddr, int iport)-用来建立将要传输的UDP包。sendLength 不应该比sendBuf字节数组的大小要大。 15.5.2 DatagramSocket DatagramSocket用来读写UDP包。这个类有三个构造函数,允许你指定要绑定的端口号和internet地址: DatagramSocket()-绑定本地主机的所有可用端口 DatagramSocket(int port)-绑定本地主机的指定端口 DatagramSocket(int port, InetAddress iaddr)-绑定指定地址的指定端口 15.5.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 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); 最小UDP服务器(续( 1. 2.// Get the message. 3.datagramSocket.receive(inDataPacket); 4. 5.// Retrieve return address information, including InetAddress 6.// and port from the datagram packet just recieved. 7. 8.clientAddress = inDataPacket.getAddress(); 9.clientPort = inDataPacket.getPort(); 10. 11.// Get the current time. 12.time = getTime(); 13. 14.//set up a datagram to be sent to the client using the 15.//current time, the client address and port 16.outDataPacket = new DatagramPacket 17.(time, time.length, clientAddress, clientPort); 18. 19.//finally send the packet 20.datagramSocket.send(outDataPacket); 21.} 22.} 23. 24.public static void main(String args[]) { 25.UdpServer udpServer = new UdpServer(); 26.try { 27.udpServer.go(); 28.} catch (IOException e) { 29.System.out.println ("IOException occured with socket."); 30.System.out.println (e); 31.System.exit(1); 32.} 33.} 34.} 15.5.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. 最小UDP客户(续( 1.// Receive the time data from the server 2.datagramSocket.receive(inDataPacket); 3. 4.// Print the data received from the server 5.receivedMsg = new String 6.(inDataPacket.getData(), 0, inDataPacket.getLength()); 7.System.out.println(receivedMsg); 8. 9.//close the socket 10.datagramSocket.close(); 11.} 12. 13.public static void main(String args[]) { 14.UdpClient udpClient = new UdpClient(); 15.try { 16.udpClient.go(); 17.} catch (Exception e) { 18.System.out.println ("Exception occured with socket."); 19.System.out.println (e); 20.System.exit(1); 21.} 22.} 23.} 练习:使用Socket编程 练习目标-通过实现一个使用socket进行通信的客户和服务器来获得关于使用socket的经验。 一、准备工作 为了很好地完成这个练习,你必须对HTML和网络有清晰的理解。 二、任务 水平1:创建socket 成对地进行这个练习,这样你可以使用其他人的机器的名字。你将要创建一个服务器和客户对,还有一个从其中之一请求一个文件的程序。 服务器模板(FileServer.java)位于templates目录下。编写一个方法,它能使服务器接收来自客户的文件名字符串,试图打开这个文件并通过socket将它传回到客户。 2. 客户模板(ReadFile.java)也位于templates目录下。客户程序将文件名字符串作为一个参数并将它发给服务器,然后等待服务器发送错误响应或文件。 水平3:创建一个多线程的服务器(MultiServer.java) 1. 扩展客户代码,使客户能请求多个文件。 扩展客户,使它在没有错误返回时,将文件存放到磁盘。 使用线程扩展服务器,这样多个客户就可以同时连接到服务器。 三、练习小结 讨论 - 花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。 经验 解释 总结 应用 四、检查一下你的进度 在进入下一个模块的学习之前,请确认你能够: 开发代码来建立网络连接 理解TCP/IP和UDP协议 用ServerSocket和Socket类来实现TCP/IP客户和服务器 用DatagramPacket和DatagramSocket来有效地进行基于UDP的网络通信。 五、思考题 有若干关于Java平台的高级话题,它们中的许多将在其他Sun教育课程中讨论。附录A给出了其中一些的简短描述。你还可以查看JavaSoft的Web站点(www.javasoft.com)。 附录A Java高级编程的元素 目标 完成这个附录之后,你应当能够: 理解分布式计算的二层体系结构和三层体系结构 理解Java编程语言作为数据库应用程序的前台程序的作用 使用JDBC API 理解使用对象代理的数据交换方法 解释JavaBeans的组件模型 二、二层体系结构和三层体系结构 客户/服务器包括两个或更多的计算机共享一个与完整应用程序相关的任务。理想情况下,每台计算机执行的逻辑适合它的设计和所声称的功能。 使用最广泛的客户/服务器实现是二层体系结构。这包括一个前台的客户应用程序与在另外一台计算机上运行的后台数据库引擎通信。客户程序向数据库服务器发送SQL语句。服务器返回合适的结果,而客户负责处理数据。 应用程序所采用的基本二层客户/服务器模型得到了许多流行数据库系统的支持,包括:Oracle、Sybase和Informix。 二层客户/服务器结构在性能上损失很大。因为客户方要处理大部分的逻辑,所以客户方软件变得越来越大,越来越复杂。而服务器方的逻辑仅限于数据库操作。这里的客户称为肥客户。 肥客户可能为了访问远程数据库而产生频繁的网络流量。这对于Intranet和基于局域网的网络拓扑来说是不错的,并对台式机在内存和磁盘方面的需求产生了重要的影响。此外,后台的数据库在服务器所提供的逻辑方面并不都是相同的,而且它们都有自己的API集合,程序员必须使用这些API来优化和缩放性能。下面要介绍的三层客户/服务器结构,以更有效的方式来处理可伸缩性、性能和逻辑分割。 、三层体系结构 三层体系结构是最先进的客户/服务器软件结构。三层体系结构一开始要求很陡的开发曲线,特别是当你必须支持不同的平台和网络环境。它的好处在于减少网络流量,优秀的Internet和intranet性能以及对系统的扩展和增长有更多的控制。 三层客户/服务器定义 三层客户/服务器环境的三个组件或三个层次是表示,商业逻辑或功能,以及数据。它们是分离的,因此这样其中任何一层的软件都可以用一种不同的实现来代替而不影响其它层。例如,如果你想把一个面向字符的屏幕替换成一个图形用户界面(表示层),你只要使用已建立的API或接口来编写图形用户界面,用它来存取面向字符的屏幕中的功能程序。商业逻辑提供了定义所有商业规则的功能,而操作数据是通过商业规则进行的。商业政策的改变可以只对这一层产生影响,而不影响数据库。第三层,也就是数据层,包括系统、应用程序和数据,其中数据被封装起来,这是为了利用这个体系结构的优点,即最小的程序移植量。 四、数据库前台 Java编程语言为软件工程师创建面向数据库系统的前台应用程序提供了很多便利。由于它的“一次编写,到处使用”SM的特点,Java编程语言具有可适用于多种硬件和操作系统的优点。甚至在多平台环境中,程序员也不需要编写与特点平台有关的前台应用程序。 Java技术支持大量用于前台开发的类,这使得通过JDBC API来和数据库进行交互成为可能。JDBC API提供了到后台数据库的连接,它可以可以返回查询结果,并由前台处理。 在二层模型中,数据库位于数据库服务器。客户执行的前台应用程序打开一个socket,用来进行网络通信。socket提供了客户应用程序和后台服务器之间的通信。在下图中,客户程序向客户服务器发送SQL数据库请求。服务器将结果返回给客户,由客户格式化结果并进行表示。  经常使用的数据操作机制经常被作为嵌入的“存储过程”。在操作数据库的过程中,满足一定条件就会自动执行触发器。这个模型的主要缺点是所有的商业规则都在客户应用程序中实现,创建了庞大的客户方运行时(程序)以及增加客户端代码的重写量。 在三层模型中,所有的商业规则都嵌入到客户(前台)层。它和中间服务器交互,后者提供了关于后台应用程序的抽象。中间层管理商业规则,而商业规则通过应用程序的管理条件来操纵数据。中间服务器也接受基于各种通信协议的从若干客户到一个或多个服务器的连接。中间层为应用程序提供了一个与数据库无关的接口,并使得前台更加健壮。  五、JDBC API介绍 创建健壮且平台无关的应用程序和基于Web的Applet的能力促使开发者提供具有前台连接性的解决方案。JavaSoft与数据库和数据库工具厂商合作创造了一个与数据库管理系统无关的机制,它使得开发者可以编写能在各种数据库上运行的客户应用程序。这种努力的成果就是Java 数据库连接应用程序编程接口(JDBC API)。 1.JDBC,概述 JDBC提供了访问数据库的标准接口。JDBC的模型对开放数据库连接(ODBC)进行了改进,它包含一套发出SQL语句、更新表和调用存储过程的类和方法。 如下图所示,Java编程语言前台应用程序使用JDBC API来和JDBC驱动管理器进行交互。JDBC驱动管理器使用JDBC Driver API来装载合适的JDBC驱动。JDBC可以从不同的数据库厂商处得到,它用来和底层的DBMS通信。  2.JDBC驱动 Java应用程序使用JDBC API,并通过数据库驱动和数据库连接。大多数数据库引擎带有与它们关联的JDBC驱动。JavaSoft定义了四种类型的驱动。关于更多的细节,可以参考: http://java.sun.com/products/jdbc/jdbc.drivers.html 3.JDBC ODBC桥 JDBC-ODBC桥是一个JDBC驱动,它把JDBC调用转换为ODBC操作。这个桥使得所有支持ODBC的DBMS都可以和Java应用程序交互。  JDBC-ODBC桥接口作为一套共享动态C库提供的。ODBC提供了客户方一套适合于客户方操作系统的库和驱动。这些ODBC调用都是C调用,而且客户必须带有ODBC驱动和相关的客户方库的本地副本。这限制了它在基于web的应用程序中的使用。 六、分布式计算 Java技术可用于创建分布式计算环境。两种流行的技术是远程方法调用(RMI)和公共对象请求代理体系结构(CORBA)。RMI和远过程调用(RPC)类似,它很受Java编程语言的程序员青睐。CORBA提供了在异构开发环境中的灵活性。 1.RMI RMI特性使在客户计算机上运行的程序可以调用远程服务器机器上的对象的方法。它提供了程序员在网络环境进行分布式计算的能力。面向对象的设计要求每个任务都是由最适合那个任务的对象执行。RMI扩展了这个概念,它允许任务由最适合它的机器执行。RMI定义了一套可以创建远程对象的远程接口。客户可以用和调用本地对象的方法相同的句法来调用远程对象的方法。RMI API提供了处理所有底层通信和访问远程方法所需的参数引用的类。 在所有分布式体系结构上,一个应用程序进程或对象服务器(监护者)向世界广播自身,这是通过使用本地机器(结点)上的名字服务进行注册来做到的。使用RMI时,一个称为RMI注册器的名字服务监护者在RMI端口上运行,它缺省地监听主机的1099IP端口。 RMI注册器包含了一张关于远程对象引用的内部表。对于每个远程对象,表中包含一个注册名和这个对象的引用。通过实例化和用不同的名字将对象多次绑定到注册器,就可以在表中存放一个对象的多个实例。 当一个RMI客户通过注册器绑定一个远程对象时,它将通过接口接收到关于远程实例化对象的本地引用,并通过这个引用和对象通信。 通过导入远程的RMI包和创建远程对象的引用,Applet开始运行。一旦Applet建立链接,它会调用远程对象的方法,就好象这些方法可以在本地得到。 2.RMI的体系结构 RMI的体系结构提供了三个层次:存根(stub)/骨架(skeleton)、远程引用和传输层。  传输层创建并维护客户和服务器之间的物理链接。它处理在客户和服务器之上的远程/引用层(RRL)中传输的数据流。 远程引用层提供了一个用户客户和服务器之间虚拟网络的独立引用协议。它提供了下面的传输层和上面的存根/骨架层之间的接口。 存根是表示远程对象的客户方代理。客户通过接口和存根交互。存根对于客户就象一个本地对象。服务器方的骨架作为RRL和在服务器方实现的对象之间的一个接口。 3.创建RMI应用程序 这一节指导你有关创建、编译和运行一个RMI应用程序的步骤。这个过程包括以下步骤: 定义远程类的接口 创建并编译远程类的实现类 使用rmic命令创建存根和骨架类 创建并编译服务器应用程序 启动RMI注册器和服务器应用程序 创建并编译客户程序来存取远程对象 测试客户 七、CORBA CORBA是一个规范,它定义远程对象如何进行互操作。CORBA规范由对象管理小组(OMG)控制,它是一个有700多家公司共同参与,制定分布式计算开放标准的国际性开放组织。有关更多的细节,可以参考以下URL: http://www.omg.org CORBA对象可以用任何编程语言重写,包括C和C++。这些对象可以存在于任何平台之上,包括Solaris、Windows95/NT、openVMS、Digital UNIX、HP_UX和其它许多平台。这意味着,在一个Windows95平台上运行的Java应用程序可以动态装载并使用由UNIX Web服务器存放在Internet上的C++对象。 通过使用接口定义语言(IDL)构造对象的接口,可以获得语言无关性。IDL允许用同样的方式描述所有的CORBA对象;唯一的要求是本地语言(C / C++,COBOL,Java)和IDL之间的一座“桥”。 CORBA的核心是对象资源代理(ORB)。ORB是在CORBA应用程序的客户和服务器之间传输信息的主要组件。ORB管理调度请求,建立到服务器的连接,发送数据,并在服务器上执行请求。服务器返回操作结果的过程与之类似。来自不同厂商的ORB在TCP/IP上通信时,使用的是Internet内部ORB协议(IIOP),它是CORBA 2.0标准的一部分。 八、Java IDL Java IDL给Java编程语言增加了CORBA能力,它提供了基于标准的互操作性和连接性。Java IDL允许分布式Java web应用程序使用工业标准IDL和IIOP来透明地请求远程网络服务器上的操作。 Java IDL不是OMG的IDL的一个实现。事实上,它是一个使用IDL来定义接口的CORBA ORB。idltojava编译器生成可移植的客户存根和服务器骨架。通过使用名字服务存取引用对象,这使得CORBA客户能够与在远程服务器上运行的另一个对象交互。与RMI注册器类似,名字服务是在远程服务器上运行的一个后台进程它包含一张关于名字服务和用来解析客户请求的远程对象引用的表。 建立一个CORBA对象的步骤可以概述如下: 使用接口定义语言(IDL)来创建对象的接口。 使用javatoidl编译器将接口转换成存根和骨架。 实现骨架对象,创建CORBA服务器对象。 编译并执行服务器对象,将它绑定到名字服务。 执行客户对象,通过CORBA名字服务存取服务器对象。 九、RMI与CORBA之比较 RMI最大的优点源于以下事实:它被设计成一个安全的解决方案。这意味着构筑RMI应用程序非常简单,而且所有的远程对象和本地对象有相同的特性,对于一个多层解决方案,有可能组合JDBC和RMI两者的精华部分。 与此同时,CORBA受益于如下事实,它是一个语言无关的解决方案,但它在开发周期中引入了不可忽视的复杂性,并妨碍了垃圾回收特性。对于数据库应用程序的开发人员,CORBA为异构环境提供了最好的灵活性。服务器可以用C或C++开发,而客户可以是一个Java applet。 十、Java Beans组件模型 Java Beans是一个集成技术和组件框架,它允许可重用的组件对象(称为Beans)相互通信,以及与框架通信。 Java Beans是一个独立的、可重用的软件组件,可以用构造工具来直观地操纵它。Bean可以象AWT组件那样是可视的对象,也可以象队列和栈那样是不可视的对象。一个构造器/综合工具操作Beans来创建Applet和应用程序。组件模型由Java Beans 1.00-A规范指定,这个规范定义了五个主要的服务: 内省 这个过程给出了Java Bean组件支持的特性、方法和事件。这个服务在运行时刻且正在使用可视化构造工具创建Java Bean时被使用。 通信 事件处理机制创建一个事件,这个事件将作为到其他组件的消息。 保持 保持是存储组件状态的一种途径。支持保持的最简单的方法是利用Java对象串行化,而使用bean来真正地保存Bean的状态取决于浏览器或应用程序。 属性 属性是Bean的特性,它通过名称来引用。这些特性通常由专为这一目的而创建的Bean的方法来读写。某些特性除了会影响特性所属的Bean之外,还可能影响邻近的组件。 自定义 Bean的一个主要特征就是可重用性。Bean框架提供了将已存在的Bean自定义为新Bean的方法。 Bean的体系结构 Bean由用户可见的接口来表示。如果环境想和这个Bean交互,它必须连接到这个接口。Bean由三个通用用途的接口组成:事件、属性和方法。由于Bean依赖于它们的状态,所以它们必须保持一致。 事件 Bean事件是在Bean之间或Bean和容器之间传送异步消息的机制。Bean用事件通知另一个Bean做某个动作或者告诉某个Bean状态已经发生改变。事件允许你的Bean在发生某些感兴趣的事时进行通信;为了做到这点,它们使用JDK1.1的事件模型。这个通信包含三部分:事件对象,事件监听者和事件源。 Java Beans主要使用继承了EventListener的事件监听者接口来通信。 Bean的开发者可以使用它们自己的事件类型和事件监听者接口,并可以使他们的Bean作为实现addXXXListener(EventObject e)和removeXXXListener(EventObject e)方法的源,其中XXX是事件类型的名称。然后开发者可以通过实现XXXListener接口使其他Bean作为事件目标。调用sourceBean.addXXXListner(targetBean),将sourceBean和targetBean结合在一起。 属性 属性定义了Bean的特征。在运行时刻可以通过它们的get和set方法来改变它们。 属性可以用来在Bean之间进行两路同步通信。Bean还可以通过特殊事件通信支持异步特性改变。 方法 方法是一种用于和Bean交互的操作。由合适的事件源方法来调用Bean,可以使它们接收到事件通知。某些方法是特殊的,用来处理属性和事件。这些方法必须遵循Bean规范中的命名规定。其他方法可能和事件或属性没有关系。Bean的所有公共方法对于Bean框架是可存取的,且能用来将一个Bean连接到其他Bean。 Bean内省 Java Bean内省过程显示了Bean的属性、方法和事件。如果有设置或获取特性类型的方法,Bean类就被认为含有属性。 Java BeanAPI提供的BeanInfo接口,使Bean的设计者能显示Bean的属性、事件和方法以及任何全局信息。BeanInfo接口提供了一系列方法来存取Bean的信息,但一个Bean的开发者也可以包含一个BeanInfo类用来定义Bean信息的私有描述文件。缺省地,在Bean上运行内省时创建BeanInfo对象。  图A-1 一个Bean交互的示例 一个Bean交互的示例 在图A-1中,容器A和容器B包含6个Bean。一个Bean可以和位于同一个容器中的Bean以及位于其他容器中的Bean交互。在这个示例中,Bean1和Bean4交互。它不与位于同一个容器中的Bean2和Bean3交互,这表明一个Bean可以和任何其他Bean通信,而不限于是否在同一个容器中。然而,Bean4和位于同一个容器中的Bean5通信。源Bean1向目标Bean4发送一个事件,这使得它在事件监听者上监听消息。所有其他的容器内与容器间的Bean交互可以用类似的方式进行解释。 Bean开发箱(BDK) BDK是JavaSoft开发的Java应用程序,它允许Java开发者使用Bean模型创建可重用的组件。它是一个完整的系统,包括所有示例的源代码和文档。BDK还带有一个示例性的Bean构造器和称为BeanBox的客户化应用程序。BeanBox是一个测试者容器,可用来做以下事情: 改变Bean的大小和移动Bean。 用属性表改变Bean。 用自定义应用程序来自定义Beans 将Beans连接在一起 将Beans放到一个复合窗口中 通过串行化保存Beans 恢复Beans 它带有12个示例Bean,覆盖Java Beans API的所有方面。 十一、JAR文件 JAR(库)是一个独立于平台的文件格式,它允许将许多文件集成为一个文件。多个Java applet和它们所需的组件(.class文件、图像和声音)可以捆绑到一个JAR文件中,然后在单个超文本传输协议(HTTP)的传输中下载到浏览器,这大大提高了下载速度。JAR格式还支持压缩,减少了文件大小,从而进一步加快了下载速度。此外,Applet的作者可以在JAR文件中采取数字签名来认证文件的起源。这对于已存在的Applet,完全可以保证向后兼容,而且可以被执行。 改变你的HTML页面中的applet标记来包含一个JAR文件是很容易的。服务器上的JAR由ARCHIVE参数标识,它包含了JAR文件相对于HTML页面的目录位置。例如: <applet code=Animator.class archive="jars/animator.jar" width=460 height=160> <param name=foo value="bar"> </applet> 十二、检查一下你的进度 在进入下一个模块的学习之前,请确认你能够: 理解分布式计算的二层体系结构和三层体系结构 理解Java编程语言作为数据库应用程序的前台程序的作用 使用JDBC API 理解使用对象代理的数据交换方法 解释JavaBeans的组件模型 附录B GridBagLayout管理器 一、目标 本附录详细探讨GridBagLayout管理器。 二、GridBagLayout管理器 GridBagLayout使得在一个网格布局中能够对独立单元的大小和位置进行更精细的控制。每个网格区域或组件与一个特别的GridBagConstraints实例相关,该实例指定了如何在该显示区域中设计组件的布局。GridBagLayout涉及比前面所描述的布局更多的工作,但是你可以获得你想要的内容。  GridBagLayout是标准Java平台上,目前为止所提供的最灵活的布局管理器。它对于你的整体应用程序是一个非常有用的补充。 GridBagLayout的功能 与GridLayout类似,GridBagLayout将组件在一个矩形的“单元”网格中对齐。GridLayout和GridBagLayout的区别在于,GridBagLayout使用了一种称为GridBagConstraints的对象。 通过在GridBagConstraints对象中设置值,组件可水平或垂直地对齐(带有或不带有小插图和填充),被告知扩展以填充给定的区域,以及被指示如何对窗口大小的改变采取相应的行动。 依照如下所示,可初始化并利用一个GridBagLayout以及与之相关的GridBagConstraints对象。 private GridBagLayout guiLayout; private GridBagConstraints guiConstraints; ... guiLayout = new GridBagLayout(); setLayout(guiLayout); guiConstraints = new GridBagConstraints(); guiConstraints.anchor = GridBagConstraints.CENTER; 使用GridBagConstraints 前一页代码片断中的前5个语句对于你并不陌生,因为在Java编程语言学习的前些日子里你已实例化过对象了。你也已多次使用setLayout()建立过布局管理器以控制你的组件的布局。 这一代码片断中的新内容是最后一个语句: guiConstraints.anchor = GridBagConstraints.CENTER; 与GridBagLayout布局管理器相关的GridBagConstraints对象,包含着可被设置以控制组件布局的实例变量。上面的Java代码语句设置了实例变量热区。这一实例变量用于指导GridBagLayout,在它小于它的单元时如何放置一个组件。有效值为: GridBagConstraints.CENTER (default) GridBagConstraints.NORTH GridBagConstraints.NORTHEAST GridBagConstraints.EAST GridBagConstraints.SOUTHEAST GridBagConstraints.SOUTH GridBagConstraints.SOUTHWEST GridBagConstraints.WEST GridBagConstraints.NORTHWEST 三、GridBagConstraints实例变量 关于GridBagLayout的API页解释了用于控制组件布局的实例变量。它们是: * gridx和gridy 这些变量指定了位于组件显示区域左上方的单元的特征,其中,最左上方的单元具有地址gridx=0,gridy=0。你可用GridBagConstraints.RELATIVE(缺省值)来指定某组件的相对位置,即,它相对于在它之前被加入到容器中组件的右方(gridx)或下方(gridy)的位置。 * gridwidth和gridheight 这些变量指定了在组件的显示区域中,行(gridwidth)或列(gridheight)中的单元数目。缺省值为1。你可用GridBagConstraints.REMAINDER来指定某组件在其行(gridwidth)或列(gridheight)中为最后一个。用GridBagConstraints.RELATIVE可指定某组件在其行(gridwidth)或列(gridheight)中与最后一个相邻。 * fill fill在某组件的显示区域大于它所要求的大小时被使用;fill决定了是否(和怎样)改变组件的大小。有效值为GridBagConstraints.NONE(缺省),GridBagConstraints.HORIZONTAL(使该组件足够大,以填充其显示区域的水平方向,但不改变其高度),GridBagConstraints.VERTICAL(使该组件足够大,以填充其显示区域的垂直方向,但不改变其宽度),GridBagConstraints.BOTH(使该组件填充其整个显示区域)。 * ipadx和ipady 这些变量指定了内部填充;即,在该组件的最小尺寸基础上还需增加多少。组件的宽度必须至少为其最小宽度加ipadx*2个象素(因为填充作用于组件的两边)。同样地,组件的高度必须至少为其最小高度加ipady*2个象素。 * insets insets指定了组件的外部填充;即,组件与其显示区域边界之间的最小空间大小。 * anchor 本变量在组件小于其显示区域时使用;anchor决定了把组件放置在该区域中的位置。有效值为GridBagConstraints.CENTER(缺省),.NORTH,.NORTHEAST,.EAST,.SOUTHEAST,.SOUTH,.SOUTHWEST,.WEST,和.NORTHWEST。 * weightx和weighty 这些变量用来决定如何分布空白和改变它的大小。除非你为一行(weightx)和一列(weighty)中至少一个组件指定了重量,否则,所有组件都会集中在容器的中央。这是因为,当重量为0(缺省值)时,GridBagLayout在其单元网格间以及容器边界加入一些额外的空白。 四、GridBagConstraints范例 下面的讨论将基于产生如下GUI的代码:  在接下来的描述之后,我们将给出本例的完整代码。 前面的GUI使用了几个GridBagConstraints的实例变量: public void init() { GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); setFont(new Font("Helvetica", Font.PLAIN, 14)); setLayout(gridbag); c.fill = GridBagConstraints.BOTH; c.weightx = 1.0; makebutton("Button1", gridbag, c); makebutton("Button2", gridbag, c); makebutton("Button3", gridbag, c); 上面的第2行中,创建了GridBagLayout的一个实例,称之为gridbag,对它的使用贯穿于本例后面的代码中。 第3行创建了一个称为c的GridBagConstraints的实例。你每次要指定如何设计一个组件的某些方面时,都要设置c内部的变量。 第6行把GridBagLayout建立为布局管理器。 第8行指示GridBagLayout,在一个组件未充满它的整个容器的任何时候,填充容器在水平及垂直方向的空白。由于c.fill在范例代码的剩余部分一直未被改变,因此每个组件都保持在两个方向上填充的模式。 第9行将x 重量赋值为1.0。这影响了如何在任意给定的行中填充,并决定了如何改变位于该行中组件的大小。 第10到12行调用makebutton()方法以创建按钮。 在GridBagLayout的控制下,每次加入一个组件都按照如下的基本序列进行: * 创建组件。 * 设置相应的GridBagConstraints。 * 用add()添加该组件。 如果把这些步骤收集到一个方法makebutton()中,则代码会具有更好的可读性,代码冗余度也会大大降低。 protected void makebutton(String name, GridBagLayout gridbag, GridBagConstraints c) { Button button = new Button(name); gridbag.setConstraints(button, c); add(button); } 第4行创建了一个组件。 在前一页的代码中,你用GridBagConstraints设置了c中的一些变量。现在GridBagLayout布局管理器必须得知关于你想要添加的组件的限制。这在第5行中使用GridBagLayout的setConstraints()方法来完成。 第6行加入了该组件。GridBagLayout管理器对它的布局进行控制。 范例代码的剩余部分为每个新按钮设置属性,并调用makebutton创建这些按钮。 c.gridwidth = GridBagConstraints.REMAINDER; //end row makebutton("Button4", gridbag, c); c.weightx = 0.0; //reset to the default makebutton("Button5", gridbag, c); //another row c.gridwidth = GridBagConstraints.RELATIVE; //next-to-last in row makebutton("Button6", gridbag, c); c.gridwidth = GridBagConstraints.REMAINDER; //end row makebutton("Button7", gridbag, c); c.gridwidth = 1; //reset to the default c.gridheight = 2; c.weighty = 1.0; makebutton("Button8", gridbag, c); 第1和第2行创建Button4。第1行指示GridBagLayout填充本按钮所在行的剩余空间。它还有一个额外的作用,即,使得Button5出现在下一行。 第4和第5行创建Button5。由于c.gridwidth仍保持着在第1行中被设置的GridBagConstraints.REMAINDER,所以该按钮独占一行。同时,第4行把x weight重置为缺省值。 第7和第8行创建Button6。第7行指示GridBagLayout使本按钮成为本行中紧邻最后按钮的一个。 第10和第11行创建Button7。第10行指示GridBagLayout填充本按钮所在行中的剩余空间。因而,Button8显示在下一行上。 第13到16行创建Button8。第14行使得本按钮有两个单位的高度,而第15行则指示GridBagLayout重置这一按钮垂直向上的大小。 以下为GridBagLayout示例的完整代码: import java.awt.*; import java.util.*; import java.applet.Applet; public class GridBagEx1 extends Applet { protected void makebutton(String name, GridBagLayout gridbag, GridBagConstraints c) { Button button = new Button(name); gridbag.setConstraints(button, c); add(button); } public void init() { GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); setFont(new Font("Helvetica", Font.PLAIN, 14)); setLayout(gridbag); c.fill = GridBagConstraints.BOTH; c.weightx = 1.0; makebutton("Button1", gridbag, c); makebutton("Button2", gridbag, c); makebutton("Button3", gridbag, c); c.gridwidth = GridBagConstraints.REMAINDER; //end row makebutton("Button4", gridbag, c); c.weightx = 0.0; //reset to the default makebutton("Button5", gridbag, c); //another row c.gridwidth = GridBagConstraints.RELATIVE; //next-to-last in row makebutton("Button6", gridbag, c); c.gridwidth = GridBagConstraints.REMAINDER; //end row makebutton("Button7", gridbag, c); c.gridwidth = 1; //reset to the default c.gridheight = 2; c.weighty = 1.0; makebutton("Button8", gridbag, c); c.weighty = 0.0; //reset to the default c.gridwidth = GridBagConstraints.REMAINDER; //end row c.gridheight = 1; //reset to the default makebutton("Button9", gridbag, c); makebutton("Button10", gridbag, c); resize(300, 100); } public static void main(String args[]) { Frame f = new Frame("GridBag Layout Example"); GridBagEx1 ex1 = new GridBagEx1(); ex1.init(); f.add("Center", ex1); f.pack(); f.resize(f.preferredSize()); f.show(); } } 附录C Java本地接口 一、本地方法 现在,你已经了解了Java编程语言中的许多特性和基本功能。你可能希望用Java编程语言来编写应用程序,但Java编程语言却不能独立支持你所要执行的任务。在这种情况下,你可以使用本地方法,并将它与处理你的特殊需求的C语言相链接。 这一附加功能将带来一定的代价――你的应用程序将不再易于移植。其他共享你体系结构的机器必须有一个已编译过的C程序的本地拷贝。具有不同体系结构的其他机器会要求把你的C语言程序移植到它们自己的体系结构中,使其能由它的一种本地编译器进行编译。 这一过程对结构复杂的程序来说是困难的,而对依赖于下层操作系统中独有特性的程序来说则是不可能的。但是,如果你所面对的是一个单一类型体系结构的环境,或者只想加入具有良好适应性的特性,本地方法无疑可成为满足你要求的最佳选择。 二、本地的HelloWorld 第一个任务将是从Java程序中调用一个本地方法,因此首先要为HelloWorld程序创建一个本地方法。 以下为将本地方法集成到你的Java程序中的四个基本步骤的概述: * 用适当的本地方法声明定义一个Java类。 * 为你的C模块的使用创建一个头文件,用javah工具来完成。 * 编写包含该本地方法的C模块。 * 将这段C代码编译成一个动态装载库。 定义本地方法 与其他方法相似,你必须声明要用的所有本地方法,并且必须将它们定义在一个类中。 你可将你的本地HelloWorld方法定义为如下形式: public native void nativeHelloWorld(); 这与你所写的其他public void方法比较有两个变化: * 关键字native被用作方法修饰符。 * 方法的主体部分(实际实现部分)在这里没有定义,而是代之以一个分号(;)。 你必须把本地方法的声明放到类的定义中。含有这个本地方法的类还包含一个静态代码块,它负责装载带有该方法实现的动态库。这里是为简单的nativeHelloWorld()方法而写的类定义的一个例子: class NativeHello { 1.public native void nativeHelloWorld(); 2.static { 3.System.loadLibrary("hello1"); 4.} 5.} Java运行环境在类被装载时执行定义的static代码块。在上例中,当类NativeHello被装载时,库hello1被装入。 调用本地方法 一旦你已将本地方法放入到一个类中,就可以为该类创建对象以存取这个本地方法,这与处理普通的类方法相似。这里,我们举例说明程序是如何创建一个新的NativeHello对象并调用你的nativeHelloWorld方法的: 1.class UseNative { 2.public static void main (String args[]) { 3.NativeHello nh = new NativeHello(); 4.nh.nativeHelloWorld(); 5.} 6.} 用javac来编译.java文件。.class文件在创建头文件时会被使用。 javah工具 你可用javah工具来创建基于NativeHello.class文件的C头文件。对javah的调用方式如下: % javah -jni NativeHello 所产生的文件,NativeHello.h,为你提供了编写C程序所需的消息。这里给出了对于本例javah所产生的文件: /* DO NOT EDIT THIS FILE - it is machine generated */ 1.#include <jni.h> 2./* Header for class NativeHello */ 3. 4.#ifndef _Included_NativeHello 5.#define _Included_NativeHello 6.#ifdef __cplusplus 7.extern "C" { 8.#endif 9./* 10.* Class: NativeHello 11.* Method: nativeHelloWorld 12.* Signature: ()V 13.*/ 14.JNIEXPORT void JNICALL Java_NativeHello_nativeHelloWorld 15.(JNIEnv *, jobject); 16. 17.#ifdef __cplusplus 18.} 19.#endif 20.#endif 21. 其中的黑体字符部分给出了将要实现的本地方法的签名。 为本地方法编写C函数代码 到此为止,C程序是唯一缺少的代码部分。你所编写的C代码必须包含上面的头文件,以及在$JAVA_HOME/include目录中由JDK所提供的jni.h。($JAVA_HOME指JDK的“根”目录。)当然,也要包含你的函数所必需的其他头文件。 对每一个在头文件中声明的函数,你都要提供函数体。对本例来说,称为MyNativeHello.c的C文件如下: #include <jni.h> 1.#include "NativeHello.h" 2.#include <stdio.h> 3. 4.void Java_NativeHello_nativeHelloWorld 5.(JNIEnv *env, jobject obj) { 6.printf ("Hello from C"); 7.} 8. 三、将它合并 现在你已准备好了所有片断,必须告诉系统如何将它们集合起来。首先,编译你的C程序。也许你必须指定include文件的位置。 下例使用了Solaris操作系统中的C编译器: % cc -I$JAVA_HOME/include -I$JAVA_HOME/include/solaris -G MyNativeHello.c -o libhello1.so 为了编译代码,你必须存取的两个包含目录是$JAVA_HOME/include和$JAVA_HOME/include/solaris。你可以在命令行上指定它们,或者采用修改INCLUDE环境变量的方式。 C:\> cl MyNativeHello.c -Fehello1.dll -MD -LD javai.lib 一旦创建完库文件,你就可运行你的本地方法测试文件了: % java UseNative Hello Native World! 或 C:\> java UseNative Hello Native World! 如果你被给出java.lang.UnsatisfiedLinkError信息,则需要更新系统的LD_LIBRARY_PATH变量来包含当前目录(使得JVM可找到libhello1.so)。 四、向本地方法传递信息 在前面的例子中,本地方法既不处理从定义的类中所存取的信息,也不接受任何参数。然而,以下的任务在编程过程中却经常遇到: 将一个Java原语作为参数传递 在Java程序中,可以象其他方法一样为本地方法提供参数。假设在一个称为NativeHello2.java的文件中存在如下的代码声明,该声明定义了打印count次的本地方法: public native void nativeHelloWorld2(int count); 这一声明将在头文件NativeHello2.h中产生如下的入口: JNIEXPORT void JNICALL Java_NativeHello2_nativeHelloWorld2 1.(JNIEnv *, jobject, jint); 现在重写你的C方法,使它循环所给定的次数(它作为该方法的第三个参数出现)。 #include <jni.h> 1.#include "NativeHello2.h" 2.#include <stdio.h> 3. 4.JNIEXPORT void JNICALL Java_NativeHello2_nativeHelloWorld2 5.(JNIEnv *env, jobject obj, jint countMax) { 6.int count; 7.for (count = 0; count < countMax; count++) { 8.printf ("Hello from C, count = %d\n",count); 9.} 10.} 将一个Java基本类型作为对象数据成员存取 在一个本地方法中,最常见的需求是对类数据成员的存取。jni.h文件包含几个接口函数,以便使用本地代码模块内部的对象。 例如:考虑编写一个具有两个int变量,其中之一为static的类,用一个本地方法来存取和修改这些变量: class NativeHello4 { 1.static int statInt = 2; 2.int instInt = 4; 3.public native int nativeHelloWorld4(); 4.static { 5.System.loadLibrary("hello4"); 6.} 7.} 在你的C语言内部,你可用<jni.h>中的函数来存取这些变量: #include <jni.h> 1.#include "NativeHello4.h" 2.#include <stdio.h> 3. 4./* The names of the Java object fields to be accessed. */ 5.#define STAT_FIELD_NAME "statInt" 6.#define INST_FIELD_NAME "instInt" 7. 8./* This method displays the statInt and instInt fields and 9.returns the product of the two. */ 10.jint Java_NativeHello4_nativeHelloWorld4 11.(JNIEnv *env, jobject obj) { 12. 13./* Class object. Used to find all fields and access 14.static ones. 15.jclass class = (*env)->GetObjectClass(env,obj); 16. 17.jfieldID fid; /* A field reference. */ 18.jint staticInt; /* A C copy of the static int. */ 19.jint instanceInt; /* A C copy of the int. */ 20. 将一个Java基本类型作为对象数据成员存取(续) 1. 2./* Get reference to the static field. The "class" 3.argument connects the field to a class. The third 4.argument is the field's name, and the last argument is 5.the field's type. See the union jvalue entry in jni.h, 6.then capitalize for the proper primitive value. */ 7.fid = (*env)->GetStaticFieldID(env, class, 8.STAT_FIELD_NAME, "I"); 9.if (fid == 0) 10.return; 11. 12./* Get that field's data. */ 13.staticInt = (*env)->GetStaticIntField(env, class, fid); 14. 15./* Process it, change it... */ 16.printf 17.("In C, doubling original %s value of %d to %d\n", 18.STAT_FIELD_NAME, staticInt, staticInt*2); 19.staticInt *= 2; 20. 21./* ... and store it back into the class object. */ 22.(*env)->SetStaticIntField(env, class, fid, staticInt); 23. 24. 25./* Now for the nonstatic int, part of the current 26.object. Get the field reference as before... */ 27.fid = (*env)->GetFieldID 28.(env, class, INST_FIELD_NAME, "I"); 29.if (fid == 0) 30.return; 31. 32./* Get the field. Refer to the object, not the class. */ 33.instanceInt = (*env)->GetIntField(env, obj, fid); 34. 35./* Process it, change it... */ 36.printf 37.("In C, tripling original %s value of %d to %d\n", 38.INST_FIELD_NAME, instanceInt, instanceInt*3); 39.instanceInt *= 3; 40. 存取字符串 可以回想一下,Java编程语言中的字符串由16-bit的Unicode字符构成。 但是,C的字符串则由8-bit的美国标准信息交换编码(ASCII)字符构成。由于字符串是Java代码与本地代码间传递的最常见的对象,所以,在jni.h中定义了几个函数来帮助实现对字符串操作的简单性。在Java编程语言中,字符串的C数据类型是jstring。 假设在Java程序中声明了如下打印一字符串的本地方法: #include <jni.h> 1.#include "NativeHello3.h" 2.#include <stdio.h> 3. 4.void Java_NativeHello3_nativeHelloWorld3 ( 5.JNIEnv *env, jobject obj, jstring javaString) { 6. 7.const char * CString; 8. 9./* Convert java string to C string. */ 10.CString = (*env)->GetStringUTFChars(env, javaString, 0); 11. 12.printf ("In C, string is %s\n", CString); 13. 14./* Tell VM to release mem for CString as done w/ it. */ 15.(*env)->ReleaseStringUTFChars(env, javaString, CString); 16.} 下面的C函数为其实现了本地代码。请注意完成时调用ReleaseStringUTFChars来释放为C字符串所分配内存的重要性。 作为最后的一例,考虑将字符串作为一个对象数据成员来存取。假设所定义的类中带有一个String域和本地方法,如下所示: class NativeHello5 { 1.String stringField = "original"; 2.public native String nativeHelloWorld5(); 3.static { 4.System.loadLibrary("hello5"); 5.} 6.} 在下面的程序中,这个类被实例化,并且调用了该本地方法: class UseNative5 { 1.public static void main (String args[]) { 2.String changedString; 3.NativeHello5 nh = new NativeHello5(); 4. 5.System.out.println ("In Java, nh's string says '" 6.+ nh.stringField + "'"); 7. 8./* Call native method to print and change string in 9.the current object, and return a third string. */ 10.changedString = nh.nativeHelloWorld5(); 11. 12.System.out.println ("In Java, nh's string says '" 13.+ nh.stringField + "'"); 14. 15.System.out.println ("Native method returned '" + 16.changedString + "'"); 17. 18.} 19.} 下面的本地代码将字符串从这个对象中抽取出来,并打印;然后改变并存入新的版本;最后,在函数的返回值中给出另一个新的字符串: #include <jni.h> 1.#include "NativeHello5.h" 2.#include <stdio.h> 存取字符串(续) 1. 2.#define NEW_STRING1 "Revised" 3.#define NEW_STRING2 "Revised again" 4. 5.jstring Java_NativeHello5_nativeHelloWorld5 6.(JNIEnv *env, jobject obj) { 7. 8.jclass class = (*env)->GetObjectClass(env,obj); 9.jfieldID fid; 10.jstring javaString; 11.const char *CString; 12. 13.fid = (*env)->GetFieldID 14.(env, class, "stringField", "Ljava/lang/String;"); 15.if (fid == 0) 16.return; 17. 18./* Get the field reference for the java string. */ 19.javaString = (*env)->GetObjectField(env, obj, fid); 20. 21./* Retrieve the C string from the object. */ 22.CString = (*env)->GetStringUTFChars(env, javaString, 0); 23. 24.printf ("In C, changing string from %s to %s\n", 25.CString, NEW_STRING1); 26. 27./* Tell VM to release memory for CString as done w/it. */ 28.(*env)->ReleaseStringUTFChars(env, javaString, CString); 29. 30./* Alloc a new Java string to store back in the obj. */ 31.javaString = (*env)->NewStringUTF(env, NEW_STRING1); 32. 33./* Store it back. */ 34.(*env)->SetObjectField(env, obj, fid, javaString); 35. 36./* Allocate one more new Java string to return. */ 37.javaString = (*env)->NewStringUTF(env, NEW_STRING2); 38. 39.return (javaString); 40.} 41. 五、小结 集成本地方法的精确过程是: 创建一个包含本地方法声明和装载动态库的静态代码的程序: vi Native.java 创建一个包含对该本地方法调用的程序: vi UseNative.java 编译.java文件: javac NativeHello.java UseNative.java 创建C头文件: javah –jni NativeHello 创建实现你的本地方法的C程序: vi MyNativeHello.c 编译动态库: cc –I$JAVA_HOME/include I$JAVA_HOME/include/solaris –G MyNativeHello.c –o libhello.so 设置LD_LIBRARY_PATH变量: setenv LD_LIBRARY_PATH .:$LD_LIBRARY_PATH 运行程序: java UseNative 关于其他JNI功能的信息,请参见JavaSoft Web站点上的JNI tutorial。它可从你本地装载的master Java API索引通过超级链接存取。 附录D 事件1.0.x到事件1.1的转换 本附录给出了在JDK1.0.x和JDK1.1下有关事件处理的概述,并提供了一张1.0.x事件和相应方法与其1.1的对应部分的映射表。 参考文献 其他资源 - 本附录的内容可由Web页面“如何将程序转换到1.1 AWT API”获得[在线]。可从下列URL得到: http://www.javasoft.com/products/JDK/1.1/docs/guide/awt/HowToUpgrade.html。 一、事件处理 JDK1.1之前的事件处理 在JDK1.1之前,由组件处理事件方法(以及它所调用的方法,如action方法)是事件处理的中心。只有组件对象能处理事件,而且处理一事件的组件必须满足如下条件:或者该事件发生于其中,或者为在组件容器层次中位于其上的组件。 JDK1.1中的事件处理 在JDK1.1中,事件处理不再被限制于组件容器层次中的对象,handleEvent方法也不再是事件处理的中心。取而代之的是,任何类型的对象都可注册为事件监听者。事件监听者只能接收与它们所注册的兴趣范围相关的事件类型。你不必创建一个组件子类来处理事件。 当升级到JDK1.1版本时,转换事件处理代码的最简单的方法是,把它留在同一个类中,并使其成为该事件类型的监听者。 另一个可行的方案是,在一个或多个非组件监听者(适配器或过滤器)中使事件处理代码集中化。这种方法允许你把程序的GUI与实现细节分离。它要求你修改已有的代码,以使监听者能获得它们从组件所请求的任何信息。如果你试图保持程序体系结构的清晰,这种方法不失为一个好的选择。 把大多数采用AWT的1.0程序转换为1.1 API需改动最多的部分,是转换事件处理代码。一旦你计划好了程序要处理哪些事件,以及这些事件由哪些组件产生,整个过程就能很顺利地进行下去。在源文件中搜索“Event”可帮助你找到这些事件处理代码。 注 - 在寻找这些代码时,你应注意某些类的存在是否只是为了事件处理;你可将这样的类清除。 表D-1可用于帮助转换过程,它给出了1.0的事件和方法到1.1相应部分的映射。 * 第一栏列出了每一个1.0的事件类型,以及(如果有)为该事件所指定的方法名称。未列出方法的事件,总是由handleEvent方法来处理的。 * 第二栏为能够产生该事件类型的1.0组件。 * 第三栏为监听者接口,它可帮助你处理所列出的1.1的等价事件。 * 第四栏列出了每个监听者接口中的方法。 表D-1 事件转换表 1.0. x 1.1  Event/Method Generated by Interface Methods  ACTION_EVENT/action Button List MenuItem TextField ActionListener actionPerformed(ActionEvent)   Checkbox CheckboxMenuItem Choice ItemListener itemStateChanged(ItemEvent)   表D-1 事件转换表(续) WINDOW_DESTROY WINDOW_EXPOSE WINDOW_ICONIFY WINDOW_DEICONIFY Dialog Frame WindowListener windowClosing(WindowEvent) windowOpened(WindowEvent) windowIconified(WindowEvent) windowDeiconified(WindowEvent) windowClosed(WindowEvent) * 1 windowActivated(WindowEvent) * windowDeactivated(WindowEvent) *  WINDOW_ MOVED Dialog Frame ComponentListener componentMoved(ComponentEvent) ComponentHidden(ComponentEvent)* componentResized(ComponentEvent)* componentShown(ComponentEvent)*  SCROLL_LINE_UP SCROLL_LINE_DOWN SCROLL_PAGE_UP SCROLL_PAGE_DOWN SCROLL_ABSOLUTE SCROLL_BEGIN SCROLL_END Scrollbar AdjustmentListener (Or use the newScrollPane class) adjustmentValueChanged(AdjustmentEvent)   表D-1 事件转换表(续) LIST_SELECT LIST_DESELECT Checkbox CheckboxMenuItem Choice List ItemListener itemStateChanged(ItemEvent)  MOUSE_DRAG/mouseDrag MOUSE_MOVE/mouseMove Canvas Dialog Frame Panel Window MouseMotionListener mouseDragged(MouseEvent) mouseMoved(MouseEvent)  MOUSE_DOWN/mouseDown MOUSE_UP/mouseUp MOUSE_ENTER/mouseEnter MOUSE_EXIT/mouseExit Canvas Dialog Frame Panel Window MouseListener mousePressed(MouseEvent) mouseReleased(MouseEvent) moseEntered(MouseEvent) mouseExited(MouseEvent) mouseClicked(MouseEvent)*  KEY_PRESS/keyDown KEY_RELEASE/keyUp KEY_ACTION/keyDown KEY_ACTION_RELEASE/keyUp Component KeyListener keyPressed(KeyEvent) keyReleased(KeyEvent) keyTyped(KeyEvent) *  GOT_FOCUS/gotFocus LOST_FOCUS/lostFocus Component FocusListener focusGained(FocusEvent) focusLost(FocusEvent)  No 1.0 equivalent  ContainerListener componentAdded(ContainerEvent) componentRemoved(ContainerEvent)  No 1.0 equivalent  TextListener textValueChanged(TextEvent)  a.no 1.0 equivalent 二、使组件成为监听者 按照以下的通用步骤,可将一个1.0的组件转换为1.1监听者: 改变源文件,使它导入java.awt.event包: import java.awt.event.* 利用表D-1确定每种事件类型是由哪些组件产生的。 例如,如果你正在转换action方法中的事件代码,则应该查找Button,List,MenuItem,TextField,Checkbox,CheckboxMenuItem,和Choice对象。 改变该类的声明,以便按照表D-1的指示实现适当的监听者接口。 例如:如果你正试图处理由Button产生的action事件,表D-1告诉你必须实现ActionListener接口。 public class MyClass extends SomeComponent implements ActionListener { 决定产生该事件的组件在哪里创建。在创建每个组件的代码之后,应紧接着注册this为适当的监听者类型。例如: newComponentObject .addActionListener( this ); 在你的类必须实现的监听者接口中创建所有方法的空实现。拷贝事件处理代码到适当的方法中。 例如,ActionListener只有一个方法――actionPerformed。创建新的方法和拷贝事件处理代码到该方法中的一种较简单的方法是,把action方法的签名由 public boolean action(Event event, Object arg) { 改变为 public void actionPerformed(ActionEvent event) { 依照下面的方法修改事件处理代码: 删除所有的return语句。 把对event.target的引用改变为event.getSource()。 删除关于事件来自于哪个组件的不必要测试。(现在,仅当产生它的组件有一监听者时,该事件才向前进行,因此你不必担心收到来自非所希望组件的事件。) 做一些可使程序编译简洁和正确执行所要求的其他修改。