java11-15

转载 2004年08月08日 08:55:00

第11章          Java基础类

 

    JDK1.2提供了Java基础类,其中的一部分就是SwingSwing是构筑在AWT上层的一些组件的集合(为了保证平台独立性,它是用100%的纯Java编写)。本模块介绍了JFCSwing图形用户界面的实现。

 

第一节  相关问题

 

    讨论-以下为与本模块内容有关的问题:

l      AWT本身是非常有用的,它是一个新的类集合的一部分。这个新的类集合称为Java基础类(JFC),它作为一个整体,将GUI提升到了一个新的水平层次。JFC究竟是什么,特别地,什么是Swing?什么事Swing可以做但AWT不能?

 

第二节 

 

    在完成了本模块的学习后,你应当能够:

l      认识Java基础类的关键特性

l      描述com.sun.java.swing包的关键特性

l      认识Swing组件

l      定义容器和组件,并解释如何联合使用它们来构造一个Swing GUI

l      编写,编译并运行一个基本的Swing应用程序

l      高效地使用诸如JframeJapplet等顶层容器

 

参考文献

 

    以下参考文献可提供有关本模块论题的其他细节内容:

l       The Java Tutorial,这是Sun Microsystems的一本在线教材,可以从http://java.sun.com/docs/books/tutorial得到。

 

第三节 

 

介绍

l      Java基础类包含5API

l           AWT

l           Java2D

l           Accessibility

l           Drag & Drop

l           Swing

 

 

 

 

 

 

 

 

 


    Java基础类是关于GUI组件和服务的完整集合,它大大简化了健壮Java应用程序的开发和实现。

    JFC,作为JDK1.2的一个有机部分,主要包含5APIAWTJava2DAccessibilityDrag & DropSwing。它提供了帮助开发人员设计复杂应用程序的一整套应用程序开发包。

    正如前面那些模块中所讨论的那样,AWT组件为各类Java应用程序提供了多种GUI工具。

    Java2D是一图形API,它为Java应用程序提供了一套高级的有关二维(2D)图形图像处理的类。Java2D API扩展了java.awtjava.awt. image类,并提供了丰富的绘图风格,定义复杂图形的机制和精心调节绘制过程的方法和类。这些API使得独立于平台的图形应用程序的开发更加简便。

    Accessibility API提供了一套高级工具,用以辅助开发使用非传统输入和输出的应用程序。它提供了一个辅助的技术接口,如:屏幕阅读器,屏幕放大器,听觉文本阅读器(语音处理)等等。

    Drag & Drop技术提供了Java和本地应用程序之间的互操作性,用来在Java应用程序和不支持Java技术的应用程序之间交换数据。

    JFC模块的重点在SwingSwing用来进行基于窗口的应用程序开发,它提供了一套丰富的组件和工作框架,以指定GUI如何独立于平台地展现其视觉效果。

 

11.3.1  Swing介绍

Swing介绍

l      可插的外观和感觉

l      应用程序看上去是与平台有关的

l      有客户化的Swing组件

l      Swing的体系结构

l      它是围绕着实现AWT各个部分的API构筑的

l      大多数组件不象AWT那样使用与平台相关的实现

 

 

 

 

 

 

 

 

 

 


    Swing提供了一整套GUI组件,为了保证可移植性,它是完全用Java语言编写的。

      

可插的外观和感觉

    可插的外观和感觉使得开发人员可以构建这样的应用程序:它们可以在任何平台上执行,而且看上去就象是专门为那个特定的平台而开发的。一个在Windows环境中执行的程序,似乎是专为这个环境而开发的;而同样的程序在Unix平台上执行,它的行为又似乎是专为Unix环境开发的。

    开发人员可以创建自己的客户化Swing组件,带有他们想设计出的任何外观和感觉。这增加了用于跨平台应用程序和Applet的可靠性和一致性。一个完整应用程序的GUI可以在运行时刻从一种外观和感觉切换到另一种。

 

Swing的体系结构

    AWT比较,Swing提供了更完整的组件,引入了许多新的特性和能力。Swing API是围绕着实现AWT各个部分的API构筑的。这保证了所有早期的AWT组件仍然可以使用。AWT采用了与特定平台相关的实现,而绝大多数Swing组件却不是这样做的,因此Swing的外观和感觉是可客户化和可插的。

 

    上图显示了JFC各个部分之间的相互关系。Java2DAccessibilityDrag & Drop,和Accessibility APIAWTJFC的一部分,但它们不属于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

 

l      导入Swing

l      选择外观和感觉

l           getLookAndFeel()

l      设置窗口容器

l           JFrameFrame相似

l           你不能直接将组件加入到JFrame

l           一个content pane包含了除菜单条外所有Frame的可视组件

 

 

 

 

 

 

 

 

 

 

 


    语句行import com.sun.java.swing.*装入整个Swing包,它包括了标准Swing组件和功能。

选择外观和感觉

    Hello Swing的第2228行给定了应用程序外观和感觉的格式。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中。你可以或者将组件加入到JFramecontent pane中,或者提供一个新的content pane

    一个content pane是一个包含除菜单条(如果有的话)外所有框架的可视组件的容器。要获得一个JFramecontent pane,可使用getContentPane()方法。要设置它的content pane(如前面本例所示),则可使用set ContentPane()方法。

 

11.4.4  建立Swing组件

Swing应用程序基础

l         建立Swing组件

l         Hello Swing.java示例实例化了4Swing组件,这4个组件是:JFrameJButtonJLabelJPanel

l         支持辅助技术

l        Hello Swing.java示例代码支持辅助技术

                     accContext = jButton.getAccessibleContext();

                accContext.setAccessibleDescription(

"Pressing this button increments " +

" the number of button clicks.");

 

 

 

 

 

 

 

 

 

 

 

 

 


    Hello Swing程序显式地实例化了4个组件:JFrameJButtonJLabelJPanelHello Swing用第3345行中的代码来初始化JButton

    33行创建了按钮。第37行将ACTI键组合设置为快捷键,用来模拟按钮的点击。第39行为点击注册了一个事件处理器。第4145行描述了一个按钮,使得辅助技术可以提供有关按钮功能的信息。

    4959行初始化了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);

    JFrameJButtonJLabel和其他所有组件中,都有内建的Accessibility支持。辅助技术可以很容易地获得文本,甚至与一组件某特定部分相关的文本。

 

第五节  构造一个Swing GUI

构造一个Swing GUI

l      顶层容器(JFrameJAppletJDialog,和JWindow

l      轻质组件(如JButtonJPanelJMenu

l      Swing组件加入到与顶层容器相关联的content pane中。

 

 

 

 

 

 


    Swing包定义了两种类型的组件:

l       顶层容器(JFrameJAppletJWindow,和JDialog

l       轻质组件(其他的J…,如JButtonJPanelJMenu

    顶层容器定义了可以包含轻质组件的框架。特别地,一个顶层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)

                            x

                       ¼¼

                x

content pane

      x

`   +---------+-------+

    x         x                 x

JButton   JButton   JPanel

                                x

                            +---------+

x                              x

JTextField   JList

 

    下面是关于同样的GUI的另一个容器层次结构,只是在这里,GUI是在浏览器中运行的一个Applet

 ¼

  x

 ¼

  x

  JApplet  ( a top-level Swing container)

      x

content pane

                  x

`   +---------+-------+

    x         x                 x

JButton   JButton   JPanel

                                x

                            +---------+

x                              x

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类

JComponent

l       Swing组件是JComponent的子类

l       边框

l       双缓冲

l       提示框

l       键盘导航

l       应用程序范围的可插式外观和感觉

 

 

 

 

 

 

 

 

 


    所有Swing都作为JComponent的子类来实现,而JComponent类又是从Container类继承而来。Swing组件从JComponent继承了如下功能:

l      边框

你可以用setBorder()方法来指定在组件周围显示的边框。还可用一个EmptyBorder的实例来指定一个组件在其周围留有一定的额外空间。

l      双缓冲

双缓冲可以改善一个频繁被改变的组件的外观。现在你不需要编写双缓冲代码――Swing已为你提供了。缺省情况下,Swing组件是双缓冲的。

l      提示框

通过用setToolTipText()方法来指定一个字符串,你可以提供给用户有关某个组件的帮助信息。当光标暂停在组件上时,所指定的字符串就会在组件附近的一个小窗口中显示出来。

l      键盘导航

使用registerKeyboardAction()方法,你可以让用户以键盘代替鼠标来操作GUI。用户为启动一个动作所必须按下的修饰键与字符的组合,由一个KeyStroke对象来表示。

l      应用程序范围的可插式外观和感觉

每个Java应用程序在运行时刻有一个GUIManager对象,它用于确定运行时刻Swing组件的外观和感觉。由于安全性的限制,你可以通过调用UIManager.setLookAndFeel()方法选择所有Swing组件的外观和感觉。在你所看见的东西背后,每个JComponent对象都有一个对应的ComponentGUI对象,它用来执行所有关于该JComponent的绘制、事件处理、大小判定等任务。

 

练习:熟悉Swing

 

    练习目标-在本实验中,你将编写、编译和执行两个在GUI中使用Swing组件的程序。

 

一、准备

    为了更好地完成这个练习,你必须理解Swing组件和AWT组件的关系。

 

二、任务

       水平1:创建一个基本的Swing应用程序

1.    使用文本编辑器,创建一个与前面所讨论的HelloSwing类似的应用程序。

2.    将一个图标与按钮相关联。(提示-你可能需要使用ImageIcon类。)

3.    将一个提示框与按钮相关联,这样当鼠标移动到按钮之上时,会显示一个“JFC Button”的提示框。

       水平2:用Swing组件创建一个文本编辑器

1.    创建一个初始的JFrame,它包含一个JToolBarTextAreaJLabel

2.    将一个JMenuBarJFrame关联起来。

3.    创建JMenuBar上的第一个菜单。创建一个标记为JMenu,其JMenuItems包括NewOpenSaveClose

4.    为每个条目增加一个加速键。使用标签的第一个字母。

5.    为每个JMenuItem创建一个匿名的ActionListener,用来处理事件并调用与每个事件对应的方法。

6.    将带有About JMenuItemHelpJMenu加入到JMenuBar。分别为HA增加快捷键。

7.    在与About JMenuItem相关联的事件处理器中创建一个模式对话框。

8.    在工具条上创建4JButton,标为NewOpenSaveAbout

9.    为工具条上的每个按钮增加一个带有适当消息的提示框。此外,创建一个匿名ActionListener来处理适当的事件。

10.保存并编译程序。

 

三、练习小结

讨论 花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。

l       经验

l       解释

l       总结

l       应用

 

四、检查你的进度

在进入下一个模块的学习之前,请确认你能够:   

l       认识Java基础类的关键特性

l       描述com.sun.java.swing包的关键特性

l       认识Swing组件

l       定义容器和组件,并解释如何联合使用它们来构造一个Swing GUI

l       编写,编译并运行一个基本的Swing应用程序

l       高效地使用诸如JFrameJApplet等顶层容器

 

五、思考题

你现在已经知道了如何编写GUI应用程序。假设你想在一个Web浏览器中运行一个GUI应用程序,如何做到这点?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第12章          Jvav小程序介绍

 

本模块讨论了JDKApplet的支持,以及Applet在编程方式、操作上下文和如何开始等方面与应用程序的区别。

      

 

第一节  相关问题

 

讨论 以下为与本模块内容有关的问题:

l         Applet有那些优点?

 

 

 

第二节 

 

在完成了本模块的学习后,你应当能够:

l       区分独立应用程序和Applet

l       编写一个HTML标记来调用Java Applet

l       描述AppletAWT的类层次

l       创建HelloWorld.Java Applet

l       列出Applet的主要方法

l       描述和使用AWT的绘图模型

l       使用Applet方法从URL读取图像和文件

l       使用<param>标记配置Applet

 

第三节  什么是Applet?

什么是Applet

l      能嵌入到一个HTML页面中且可通过Web浏览器下载和执行的一种Java

l      可以以下方式装载:

l      浏览器装载URL

l      浏览器装载HTML文档

l      浏览器装载Applet

l      浏览器运行Applet

 

 

 

 

 

 

 

 

 

 


Applet是能够嵌入到一个HTML页面中,且可通过Web浏览器下载和执行的一种Java类。它是Java技术容器(container)的一种特定类型,其执行方式不同于应用程序。一个应用程序是从它的main()方法被调用开始的,而一个Applet的生命周期在一定程度上则要复杂得多。本模块分析了Applet如何运行,如何被装载到浏览器中,以及它是如何编写的。

 

 

12.3.1  装入Applet

由于AppletWeb浏览器环境中运行,所以它并不直接由键入的一个命令启动。你必须要创建一个HTML文件来告诉浏览器需装载什么以及如何运行它。

 

 

1.    浏览器装入URL

2.    浏览器装入HTML文档

3.    浏览器装入Applet

4.    浏览器运行Applet

 

12.3.2  Applet的安全限制

Applet的安全限制

l      多数浏览器禁止以下操作:

l      运行时执行另一程序

l      任何文件的输入/输出

l      调用任何本地方法

l      尝试打开除提供Applet的主机之外的任何系统的Socket

 

 

 

 

 

 

 

 


由于通过网络装载,Applet的代码具有一种内在的危险性。如果有人编写了一个恶意的类来读取你的密码文件,并把它通过Internet传送,会产生怎样的后果呢?

所能够控制的安全程度是在浏览器层次上实现的。大多数浏览器(包括Netscape Nevigator)缺省地禁止以下操作:

l       运行时执行另一程序

l       任何文件的输入/输出

l       调用任何本地方法

l       尝试打开除提供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的子类。AppletAWT类的层次如下:

  

这种层次关系显示,一个Applet可直接用作一个AWT布局的起始点。因为Applet为一Panel,所以它有一个缺省的流(flow)布局管理器。ComponentContainerPanel类的方法被Applet类继承了下来。

 

12.4.1  主要的Applet方法

主要的Applet方法

l       init()

l       start()

l       stop()

l       destroy()

l       paint()

 

 

 

 

 

 

 

 


在一个应用程序中,程序由main()方法处进入,而在一个Applet中却不是这样。在构造函数完成了它的任务后,浏览器调用init()Applet进行基本的初始化操作。init()结束后,浏览器调用另一个称为start()的方法。本模块稍后将对start()做更细致的剖析;start()通常在Applet成为可见时被调用。

方法init()start()都是在Applet成为“活动的”之前运行完成的,正因为这样,它们都不能用来编写Applet中继续下去的动作。实际上,与一个简单应用程序中的方法main()不同的是,没有什么方法的执行是贯穿于Applet的整个生命过程中的。你在后面将看到如何使用线程来实现这一特色。此外,你在编写Applet子类时可用的方法还有:stop()destroy()paint()

 

12.4.2  Applet显示

Applet显示

l      Applet在本质上是图形方式的

l      方法paint()由浏览器环境调用

 

 

 

 

 


Applet本质上是图形方式的,所以尽管你可以提出System.out.println()的调用请求,通常也不能这样做,而是应该在图形环境中创建你的显示。

你可以通过创建一个paint()方法在Appletpanel上绘图。只要Applet的显示需要刷新,paint()方法就会被浏览器环境调用。例如,当浏览器窗口被最小化或被要求以图标方式显示时,这种调用就会发生。

你应该编写自己的paint()方法,以使它在任何时候被调用时都能正常地工作。对它的调用是异步产生的,且由环境而不是程序来驱动。

 

12.4.3  paint()方法和图形对象

paint()方法带有一个参数,它是java.awt.Graphics类的一个实例。这个参数总是建立该Appletpanel的图形上下文。你能用这个上下文在你的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的方法和Applet的生命周期

l       init()

lApplet创建时被调用

l可用于初始化数据值

l       start()

lApplet成为可见时运行

l       stop()

lApplet成为不可见时运行

 

 

 

 

 

 

 

 

 


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绘图

AWT绘图

l       paint (Graphics g)

l       repaint()

l       update(Graphics g)

 

 

 

 

 


除了基本的生命周期外,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的显示策略

l      维护一个显示模型

l      使paint()提供仅仅基于这个模型的显示

l      更新这个模型并调用repaint()来改变显示

 

 

 

 

 

 

 


Applet模型要求你采取一种特定的策略来维护你的显示:

l      维护一个显示模型。这个模型是对为再次提供显示而所需做的事情的一个定义。关于如何去做的指令在paint()方法中被具体化;这些指令所用的数据通常是全局成员变量。

l      使paint()提供仅仅基于该模型的显示。这使得无论paint()何时被调用,它都能以一致的方法再生该显示,并正确地处理显露问题。

l      使得程序对显示的改变,通过更新该模型而调用repaint()方法来进行,以使update()方法(最终是paint()方法)被AWT线程调用。

 


注-一个单一AWT线程处理所有的绘图组件和输入事件的分发。应保持paint()和update()的简单性,以避免它们使AWT线程发生故障的可能性更大;在极端情况下,你将需要其他线程的帮助以达到这一目的。有关线程的编程是模块14的主题。

 

 

第七节  什么是appletviewer?

什么是appletviewer

l      使你不需Web浏览器就能运行Applet的一个Java应用程序

l      它把HTML文件作为一个参数来装载

              appletViewer HelloWorld.html

l      它至少需要以下HTML代码:

              <html>

        <applet code=HelloWorld.class width=100 height=100>

        </applet>

        </html>

 

 

 

 

 

 

 

 

 

 

 


Applet通常运行于一个Web浏览器中,如HotJava TMNetscape 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,它使得AppletJava调试器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>

其中

l      archive = archiveList 这一可选属性描述了一个或多个含有将被“预装”的类和其他资源的archives。类的装载由带有给定codebaseAppletClassLoader的一个实例来完成。ArchiveList中的archives以逗号(,)分隔。

l      code = appletFile.class 这是一个必需的属性,它给定了含有已编译好的Applet子类的文件名。也可用package.appletFile.class的格式来表示。

 

 


注-这个文件与你要装入的HTML文件的基URL有关,它不能含有路径名。要改变Applet的基URL,可使用<codebase>。

 

l      width = pixels  height = pixels 这些必需的属性给出了Applet显示区域的初始宽度和高度(以象素为单位),不包括Applet所产生的任何窗口或对话框。

12.8.2  描述

l      codebase = codebaseURL 这一可选属性指定了Applet的基URL――包含有Applet代码的目录。如果这一属性未指定,则采用文档的URL

l      alt = alternateText 这一可选属性指定了当浏览器能读取Applet标记但不能执行Java Applet时要显示的文本。

l      name = appletInstanceName 这个可选属性为Applet实例指定有关名称,从而使得在同一页面上的Applet可找到彼此(以及互相通信)。

l      align = alignment 这个可选属性指定了Applet的对齐方式。它的可取值与基本的HTMLIMG标记的相应属性相同,为:leftrighttoptexttopmiddleabsmiddlebaselinebottomabsbottom

l      vspace = pixels  hspace = pixels 这些可选属性指定了在Applet上下(vspace)及左右(hspace)的象素数目。其用法与IMG标记的vspacehspace属性相同。

l      <param  name = appletAttribute1  value = value> 这个标记提供了一种可带有由“外部”指定的数值的Applet,它对一个Java应用程序的作用与命令行参数相同。AppletgetParameter()方法来存取它们的属性,该方法将在本模块稍后作更详细的讨论。

l      不支持Java程序执行的浏览器将显示被包括在<applet></applet>标记之间的任何常规的HTML;而可支持Java技术的浏览器则忽略介于这两个标记之间的HTML代码。

 

 

 

 

 

第九节  其他的Applet工具

其他的Applet工具

l  getDocumentBase() 返回一个描述当前浏览器页面目录的URL对象

l  getCodeBase() 返回一个描述Applet类源目录的URL对象

l  getImage(URL base, String target)getAudioClip(URL base, String target) 采用该URL对象作为一个起始点

 

 

 

 


Applet中有若干其他特色。

所有的Java软件程序都具有访问网络的特色,这可使用模块15中所讲到的java.net包中的类来实现。此外,Applet还有些其他的方法可允许它们取得有关自己启动时所在的浏览器环境的信息。

java.net.URL描述了URL,并可用于它们之间的连接。在Applet类中有两个方法决定了URL的重要的值:

l      getDocumentBase()返回一个描述当前浏览器中带有Applet标记的HTML文件所属页面目录的URL对象

l      getCodeBase()返回一个描述Applet类文件本身源目录的URL对象。它通常与HTML文件目录相同,但并不是一定要这样。

其他的Applet特色

URL对象作为一个起始点,你可以将声音和图像取回到你的Applet中。

l      getImage(URL base, String target)从被命名为target且位于由base所指定目录的文件中取回一幅图像。其返回值是类Image的一个实例。

l      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()方法的参数是:

l      将要被绘出的Image对象

l      绘图的x轴坐标

l      绘图的y轴坐标

l      图像观察者。图像观察者是可以得知该图像的状态是否改变的一个接口(如:在装入过程中发生了什么)。

getImage()装载的图像在调用首次提出后过一段时间将会改变,这是由于装载是在后台完成的。每次,图像的更多部分被装入,paint()方法被又一次调用。这种对paint()方法调用的发生是因为Applet将自己作为drawImage()的第四个参数传递给了自己,从而使自己被注册为一个观察者。

 

第十一节  Audio Clips

 

Java编程语言也具有播放Audio Clips的方法。这些方法在java.applet.AudioClip类中。为了播放Audio Clips,你将需要为你的计算机装配适当的硬件。

 

12.11.1  播放一段Clip

欣赏一段audio clip的最简单的方式是通过Appletplay方法:

        play(URL soundDirectory, String soundFile);

或更简单的:

        play(URL soundURL);

例如:

        play(getDocumentBase(), "bark.au");

将播放存放在与HTML文件相同目录的bark.au

 

12.11.2  一个简单的Audio测试

以下的Appletappletviewer中打印出消息“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

l      装入一段Audio Clip

l      播放一段Audio Clip

l      停止一段Audio Clip

 

 

 

 

 

 


你可以用与装入图像相同的方式装入audio clip。在将它们装载之后进行播放。

 

装入一段Audio Clip

为了装入一段Audio Clip,可使用来自java.applet.Applet类的getAudioClip方法:

AudioClip sound;

sound = getAudioClip(getDocumentBase(), "bark.au");

一旦一段clip被装载,可选择与之相关的三个方法之一:playloop,或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接口的类来接收,它们可接收的事件为:

l      mouseClicked -鼠标已被点击(鼠标按钮被按下然后被释放,作为一个动作)

l      mouseEntered -鼠标光标进入一个组件

l      mouseExited -鼠标光标离开一个组件

l      mousePressed -鼠标按钮被按下

l      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

1.    打开一个新的外壳程序或Command Tool窗口。

2.    用一个文本编辑器,键入HwMouse.java程序或从course_example目录拷贝它。

3.    修改这个程序,使得你在Applet中点击时,它可以循环显示三种不同的消息。

4.    编译HwMouse.java.java程序

  c:/student> HwMouse.java

5.    用一个文本编辑器,创建一个HwMouse.html文件,文件中含有调用HwMouse.class程序的<applet>标记。

6.    appletviewer命令测试你的Applet

           c:/student> HwMouse.html

水平2:创建同心的正方形

1.    创建一个AppletSquares.java,它产生一系列如下图的同心正方形(或圆形):

2.    试图使每个正方形(或圆形)为一种不同的颜色。如果你导入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

1.    编写一个Applet,显示一幅图像,并在鼠标经过该图像时播放一个声音。

 

三、练习小结

讨论 花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。

l       经验

l       解释

l       总结

l       应用

 

四、检查你的进度

在进入下一个模块的学习之前,请确认你能够:

l         区分独立应用程序和Applet

l         编写一个HTML标记来调用Java Applet

l         描述AppletAWT的类层次树

l         创建HelloWorld.Java Applet

l         列出Applet的主要方法

l         描述和使用AWT的绘图模型

l         使用Applet方法从URL读取图像和文件

l         使用<param>标记配置Applet

 

五、思考题

如何把Applet应用在你公司的Web页面上,以改进它的整体表现效果?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第13章          线 程

 

本模块讨论多线程,它允许一个程序同时执行多个任务。

 

第一节  相关问题

 

讨论 以下为与本模块内容有关的问题:

l             我如何使我的程序执行多个任务?

 

 

 

 

第二节 

 

在完成了本模块的学习后,你应当能够:

l  定义一个线程

l  在一个Java程序中创建若干分离的线程,控制线程使用的代码和数据

l  控制线程的执行,并用线程编写独立于平台的代码

l  描述在多个线程共享数据时可能会碰到的困难

l  使用synchronized关键字保护数据不受破坏

l  使用wait()notify()使线程间相互通信

l  解释为什么在JDK1.2中不赞成使用suspend()resume()stop()方法?

 

第三节  线

线  

l       什么是线程?

l             虚拟处理机

 

 

 

 

 

 


13.3.1  什么是线程?

一个关于计算机的简化的视图是:它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器)、包含程序所要操作的数据的RAM(只读存储器)。在这个简化视图中,只能执行一个作业。一个关于最现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。

你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。如果你要执行一个以上的作业,这类似有一台以上的计算机。在这个模型中,线程或执行上下文,被认为是带有自己的程序代码和数据的虚拟处理机的封装。java.lang.Thread类允许用户创建并控制他们的线程。

 

 


注-在这个模块中,使用Thread时是指java.lang.Thread而使用thread时是指执行上下文。

 

 

13.3.2  线程的三个部分

线程的三个部分

l         处理机

l         代码

l         数据

 

 

 

 

 

 


进程是正在执行的程序。一个或更多的线程构成了一个进程。一个线程或执行上下文由三个主要部分组成

l   一个虚拟处理机

 

 

 

 

 

 

 

 

 

 


l    CPU执行的代码

l    代码操作的数据

代码可以或不可以由多个线程共享,这和数据是独立的。两个线程如果执行同一个类的实例代码,则它们可以共享相同的代码。

类似地,数据可以或不可以由多个线程共享,这和代码是独立的。两个线程如果共享对一个公共对象的存取,则它们可以共享相同的数据。

Java编程中,虚拟处理机封装在Thread类的一个实例里。构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。

 

 

 

 

第四节  Java编程中的线程

 

13.4.1  创建线程

创建线程

l             多线程编程

l             从同一个Runnbale实例派生的多线程

l             线程共享数据和代码。

 

 

 

 

 

 


本节介绍了如何创建线程,以及如何使用构造函数参数来为一个线程提供运行时的数据和代码。

一个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  启动线程

启动线程

l      使用start()方法

l      使线程置于可运行状态

 

 

 

 

 


一个新创建的线程并不自动开始运行。你必须调用它的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.}

 

注意trycatch块的使用。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  测试一个线程

l      测试一个线程

l      isAlive()

l    sleep()

l    join()

 

 

 

 

 


有时线程可处于一个未知的状态。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  使用那种方法?

使用那种方法?

l  实现Runnable

l  更符合面向对象的设计

l  单继承

l  一致性

l  扩展Thread

l  代码更简单

 

 

 

 

 

 

 

 


给定各种方法的选择,你如何决定使用哪个?每种方法都有若干优点。

 

实现Runnable的优点

l         从面向对象的角度来看,Thread类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承类。正因为这个原因和区别一个正在运行的线程的处理机、代码和数据部分的意义,本教程采用了这种方法。

l         由于Java技术只允许单一继承,所以如果你已经继承了Thread,你就不能再继承其它任何类,例如Applet。在某些情况下,这会使你只能采用实现Runnable的方法。

l         因为有时你必须实现Runnable,所以你可能喜欢保持一致,并总是使用这种方法。

 

继承Thread的优点

l         当一个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  对象锁标志

对象锁标志

l  每个对象都有一个标志,它可以被认为是“锁标志”。

l  synchronized允许和锁标志交互。

 

 

 

 

 


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  释放锁标志

释放锁标志

l      线程执行到synchronized()代码块末尾时释放

l      synchronized()代码块抛出中断或异常时自动释放

 

 

 

 

 


由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行,所以让持有锁标志的线程在不再需要的时候返回标志是很重要的。

锁标志将自动返回给它的对象。持有锁标志的线程执行到synchronized()代码块末尾时将释放锁。Java技术特别注意了保证即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。此外,如果一个线程对同一个对象两次发出synchronized调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。

这些规则使得与其它系统中的等价功能相比,管理同步块的使用简单了很多。

 

13.7.4  synchronized――放在一起

synchronized-放在一起

l      所有对易碎数据的存取应当同步。

l      synchronized保护的易碎数据应当是private的。

 

 

 

 


正如所暗示的那样,只有当所有对易碎数据的存取位于同步块内,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  死锁

死锁

l      两个线程相互等待来自对方的锁

l      它不能被监测到或避免

l      它可以通过以下方法来避免

l      决定获取锁的次序

l      始终遵照这个次序

l      按照相反的次序释放锁

 

 

 

 

 

 

 

 

 


如果程序中有多个线程竞争多个资源,就可能会产生死锁。当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。在这种情况下,除非另一个已经执行到synchronized块的末尾,否则没有一个线程能继续执行。由于没有一个线程能继续执行,所以没有一个线程能执行到块的末尾。

Java技术不监测也不试图避免这种情况。因而保证不发生死锁就成了程序员的责任。避免死锁的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序。按照与获取相反的次序释放锁。

 

第八节  线程交互-wait()和notify()

线程交互-wait()notify()

l      场景

l  把你和出租车司机当作两个线程

l      问题

l  如何决定你已经到达了你的终点

l      解决方案

l  你把你的终点和休息时间告诉出租车司机

l  出租车司机在到达终点时通知你

 

 

 

 

 

 

 

 

 


经常创建不同的线程来执行不相关的任务。然而,有时它们所执行的任务是有某种联系的,为此必须编写使它们交互的程序。

 

 

13.8.1  场景

把你自己和出租车司机当作两个线程。你需要出租车司机带你到终点,而出租车司机需要为乘客服务来获得车费。所以,你们两者都有一个任务。

 

13.8.2  问题

你希望坐到出租车里,舒服地休息,直到出租车司机告诉你已经到达终点。如果每2秒就问一下“我们到了哪里?”,这对出租车司机和你都会是很烦的。出租车司机想睡在出租车里,直到一个乘客想到另外一个地方去。出租车司机不想为了查看是否有乘客的到来而每5分钟就醒来一次。所以,两个线程都想用一种尽量轻松的方式来达到它们的目的。

 

13.8.3  解决方案

出租车司机和你都想用某种方式进行通信。当你正忙着走向出租车站时,司机正在车中安睡。当你告诉司机你想坐他的车时,司机醒来并开始驾驶,然后你开始等待并休息。到达终点时,司机会通知你,所以你必须继续你的任务,即走出出租车,然后去工作。出租车司机又开始等待和休息,直到下一个乘客的到来。

 

13.8.4  wait()notify()

线程交互

l      wait()notify()

l     

l  等待池

l  锁池

 

 

 

 

 

 

 


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  同步的监视模型

同步的监视模型

l      使共享数据处于一致的状态

l      保证程序不死锁

l      不要将期待不同通知的线程放到同一个等待池中。

 

 

 

 

 

 


协调两个需要存取公共数据的线程可能会变得非常复杂。你必须非常小心,以保证可能有另一个线程存取数据时,共享数据的状态是一致的。因为线程不能在其他线程在等待这把锁的时候释放合适的锁,所以你必须保证你的程序不发生死锁,

在出租车范例中,代码依赖于一个同步对象――出租车,在其上执行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个随机的大写字母并将其推入栈中,每个推入操作之间有0300毫秒的随机延迟。每个被推入的字符将显示到控制台上,同时还显示正在执行的生产者线程的标识。

消费者

消费者线程运行如下方法:

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个字符,每两个取出操作的尝试之间有0300毫秒的随机延迟。每个被弹出的字符将显示在控制台上,同时还显示正在执行的消费者线程的标识。

现在考虑栈类的构造。你将使用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()方法

suspend()resume()方法

l      JDK1.2不赞成使用它们

l      应当用wait()notify()来代替它们

 

 

 

 

 


JDK1.2中不赞成使用suspend()resume()方法。resume()方法的唯一作用就是恢复被挂起的线程。所以,如果没有suspend()resume()也就没有存在的必要。从设计的角度来看,有两个原因使suspend()非常危险:它容易产生死锁;它允许一个线程控制另一个线程代码的执行。下面将分别介绍这两种危险。

假设有两个线程:threadAthreadB。当正在执行它的代码时,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()方法

l      在终止前释放锁。

l      可能使共享数据处于不一致的状态。

l      应当用wait()notify()来代替它们

 

 

 

 

 

 


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:使用动画

1.        创建一个Applet ThreadedAnimation.java,它读取10DukeTM  waving图像(graphics/Duke目录中)并按照Duke波动的顺序来显示它们。

2.        MediaTracker类使这些图像的装载更平滑。

3.        允许用户连续点击鼠标来停止和启动动画。

 

三、练习小结

       讨论-花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。

l      经验

l      解释

l      总结

l      应用

 

四、检查你的进度

在进入下一个模块的学习之前,请确认你能够:

l      定义一个线程

l      在一个Java程序中创建若干分离的线程,控制线程使用的代码和数据

l      控制线程的执行,并用线程编写独立于平台的代码

l      描述在多个线程共享数据时可能会碰到的困难

l      使用synchronized关键字保护数据不受破坏

l      使用wait()notify()使线程间相互通信

l      使用synchronized关键字保护数据不受破坏

l      解释为什么在JDK1.2中不赞成使用suspend()resume()stop()方法?

 

五、思考

你是否有受益于多线程的应用程序?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第14章          流式I/O和文件

 

本模块讨论文件,socket和其他数据源使用的流式I/O机制。

 

第一节  相关问题

 

讨论 以下为与本模块内容有关的问题:

l      Java编程语言中使用什么机制来读写文件?

 

 

 

第二节 

 

在完成了本模块的学习后,你应当能够:

l         描述和使用java.io包的流式思想

l         构造文件和过滤器流,并恰当地使用它们

l         区别流与读者和作者,并进行合适的选择

l         考察并操作文件和目录

l         读、写和更新文本和数据文件

l         使用Serialization接口来保持对象的状态

 

第三节  流式I/O

流式I/O

l      流是字节的源或目的。

l      两种基本的流是:

l      输入流

l      输出流

l      结点流对特定的地方读写

l      过滤流使用结点流作为输入或输出

 

 

 

 

 

 

 

 


本模块考察了Java编程语言如何使用流来处理字节和字符I/O(包括stdiostdoutstderr)。下面几节将考察有关处理文件和操作它们所包含的数据的特定细节。

 

14.3.1  流的基础知识

一个流是字节的源或目的。次序是有意义的。例如,一个需要键盘输入的程序可以用流来做到这一点。

两种基本的流是:输入流和输出流。你可以从输入流读,但你不能对它写。要从输入流读取字节,必须有一个与这个流相关联的字符源。

java.io包中,有一些流是结点流,即它们可以从一个特定的地方读写,例如磁盘或者一块内存。其他流称作过滤器。一个过滤器输入流是用一个到已存在的输入流的连接创建的。此后,当你试图从过滤输入流对象读时,它向你提供来自另一个输入流对象的字符。

 

14.3.2  InputStream方法

InputStream方法

l      三个基本的read()方法

l       int read()

l       int read(byte [])

l       int read(byte[], int ,int )

l      其他方法

l       void close()

l       int available()

l       skip(long)

l       boolean markSupported()

l       void mark(int)

l       void reset(int)

 

 

 

 

 

 

 

 

 

 

 

 

 

 


l      int read()

l      int read(byte [])

l      int read(byte[], int ,int )

这三个方法提供对输入管道数据的存取。简单读方法返回一个int值,它包含从流里读出的一个字节或者-1,其中后者表明文件结束。其它两种方法将数据读入到字节数组中,并返回所读的字节数。第三个方法中的两个int参数指定了所要填入的数组的子范围。

 


注-考虑到效率,总是在实际最大的块中读取数据。

 

 

void close()

你完成流操作之后,就关闭这个流。如果你有一个流所组成的栈,使用过滤器流,就关闭栈顶部的流。这个关闭操作会关闭其余的流。

 

int available()

这个方法报告立刻可以从流中读取的字节数。在这个调用之后的实际读操作可能返回更多的字节数。

 

skip(long)

这个方法丢弃了流中指定数目的字符。

 

boolean markSupported()

void mark(int)

void reset()

 

如果流支持“回放”操作,则这些方法可以用来完成这个操作。如果mark()reset()方法可以在特定的流上操作,则markSupported()方法将返回turemark(int)方法用来指明应当标记流的当前点和分配一个足够大的缓冲区,它最少可以容纳参数所指定数量的字符。在随后的read()操作完成之后,调用reset()方法来返回你标记的输入点。

 

14.3.3  OutputStream方法

OutputStream方法

l       三个基本的write()方法

l       int write()

l       int write(byte [])

l       int write(byte[], int ,int )

l       其他方法

l       void close()

l       void flush()

 

 

 

 

 

 

 

 

 

 


l         void write(int)

l         void write(byte [])

l         void write(byte [], int, int)

这些方法写输出流。和输入一样,总是尝试以实际最大的块进行写操作。

 

void close()

当你完成写操作后,就关闭输出流。如果你有一个流所组成的栈,就关闭栈顶部的流。这个关闭操作会关闭其余的流。

 

void flush()

有时一个输出流在积累了若干次之后才进行真正的写操作。flush()方法允许你强制执行写操作。

 

第四节  基本的流类

 

java.io包中定义了一些流类。下图表明了包中的类层次。一些更公共的类将在后面介绍。

 

 

 

 

基本的流类

l       FileInputStreamFileOutputStream

l       BufferInputStreamBufferOutputStream

l       DataInputStreamDataOutputStream

l       PipedInputStreamPipedOutputStream

 

 

 

 

 

 

 


14.4.1  FileInputStreamFileOutputStream

这些类是结点流,而且正如这个名字所暗示的那样,它们使用磁盘文件。这些类的构造函数允许你指定它们所连接的文件。要构造一个FileInputStream,所关联的文件必须存在而且是可读的。如果你要构造一个FileOutputStream而输出文件已经存在,则它将被覆盖。

FileInputStream infile =

new FileInputStream("myfile.dat");

FileOutputStream outfile =

new FileOutputStream("results.dat");

 

14.4.2  BufferInputStreamBufferOutputStream

这些是过滤器流,它们可以提高I/O操作的效率。

 

14.4.3  DataInputStreamDataOutputStream

这些过滤器通过流来读写Java基本类。例如:

 

DataInputStream方法

 

byte readByte()

long readLong()

double readDouble()

 

DataOutputStream方法

 

void writeByte(byte)

void writeLong(long)

void writeDouble(double)

 

注意DataInputStreamDataOutputStream的方法是成对的。

 

这些流都有读写字符串的方法,但不应当使用这些方法。它们已经被后面所讨论的读者和作者所取代。

 

14.4.4  PipedInputStreamPipedOutputStream

管道流用来在线程间进行通信。一个线程的PipedInputStream对象从另一个线程的PipedOutputStream对象读取输入。要使管道流有用,必须有一个输入方和一个输出方。

 

第五节  URL输入流

URL输入流

java.net.URL imageSource;

try {

imageSource = new URL("http://mysite.com/~info");

} catch ( MalformedURLException e) {}

images[0] = getImage(imageSource, "Duke/T1.gif");

 

 

 

 

 

 

 


除了基本的文件访问之外,Java技术提供了使用统一资源定位器(URL)来访问网络上的文件。当你使用AppletgetDocumentBase()方法来访问声音和图象时,你已经隐含地使用了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包中。

读者和作者中最重要的版本是InputStreamReaderOutputStreamWriter。这些类用来作为字节流与读者和作者之间的接口。

当你构造一个InputStreamReaderOutputStreamWriter时,转换规则定义了16Unicode和其它平台的特定表示之间的转换。

 

14.6.2  字节和字符转换

缺省情况下,如果你构造了一个连接到流的读者和作者,那么转换规则会在使用缺省平台所定义的字节编码和Unicode之间切换。在英语国家中,所使用的字节编码是:ISO 8859-1

你可以使用所支持的另一种编码形式来指定其它的字节编码。在native2ascii工具中,你可以找到一个关于所支持的编码形式的列表。

使用转换模式,Java技术能够获得本地平台字符集的全部灵活性,同时由于内部使用Unicode,所以还能保持平台独立性。

 

14.6.3  缓冲读者和作者

因为在各种格式之间进行转换和其它I/O操作很类似,所以在处理大块数据时效率最高。在InputStreamReaderOutputStreamWriter的结尾链接一个BufferedReaderBufferedWriter是一个好主意。记住对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对象

l         File myFile;

myFile = new File("mymotd");

l         myFile = new File("/", "mymotd");

// more useful if the directory or filename is

// a variable

l         File myDir = new File("/");

myFile = new File(myDir, "mymotd");

 

 

 

 

 

 

 

 

 

 


File类提供了若干处理文件和获取它们基本信息的方法。

l         File myFile;

myFile = new File("mymotd");

l         myFile = new File("/", "mymotd");

// more useful if the directory or filename is

// a variable

l         File myDir = new File("/");

myFile = new File(myDir, "mymotd");

你所使用的构造函数经常取决于你所使用的其他文件对象。例如,如果你在你的应用程序中只使用一个文件,那么就会使用第一个构造函数。如果你使用一个公共目录中的若干文件,那么使用第二个或者第三个构造函数可能更容易。

File类提供了独立于平台的方法来操作由本地文件系统维护的文件。然而它不允许你存取文件的内容。

 

 


注-你可以使用一个File对象来代替一个String作为FileInputStream和FileOutputStream对象的构造函数参数。这是一种推荐方法,因为它独立于本地文件系统的约定。

 

 

第八节  文件测试和工具

文件测试和工具

l           文件名

String getName()

String getPath()

String getAbsolutePath()

String getParent()

boolean renameTo(File newName)

l           文件测试

boolean exists()

boolean canWrite()

boolean canRead()

boolean isFile()

boolean isDirectory()

boolean isAbsolute()

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


当你创建一个File对象时,你可以使用下面任何一种方法来获取有关文件的信息:

 

14.8.1  文件名

l           String getName()

l           String getPath()

l           String getAbsolutePath()

l           String getParent()

l           boolean renameTo(File newName)

 

14.8.2  文件测试

l           boolean exists()

l           boolean canWrite()

l           boolean canRead()

l           boolean isFile()

l           boolean isDirectory()

l           boolean isAbsolute()

 

14.8.3   通用文件信息和工具

l           long lastModified()

l           long length()

l           boolean delete()

 

14.8.4   目录工具

l                       boolean mkdir()

l                       String[] list()

 

第九节             随机存取文件

 

14.9.1  创建一个随机存取文件

创建一个随机存取文件

l用文件名

myRAFile = new RandomAccessFile(String name, String mode);

l用文件对象

myRAFile = new RandomAccessFile(File file, String mode);

 

 

 

 

 

 

 

 


你经常会发现你只想读取文件的一部分数据,而不需要从头至尾读取整个文件。你可能想访问一个作为数据库的文本文件,此时你会移动到某一条记录并读取它的数据,接着移动到另一个记录,然后再到其他记录――每一条记录都位于文件的不同部分。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");

 

14.9.2  存取信息

随机存取文件

l       long getFilePointer()

l       void seek(long pos)

l       long length()

 

 

 

 

 


RandomAccessFile对象按照与数据输入输出对象相同的方式来读写信息。你可以访问在DataInputStremDataOutputStream中所有的read()write()操作。

Java编程语言提供了若干种方法,用来帮助你在文件中移动。

l           long getFilePointer();

返回文件指针的当前位置。

l           void seek(long pos);

设置文件指针到给定的绝对位置。这个位置是按照从文件开始的字节偏移量给出的。位置0标志文件的开始。

l           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

 

第十节  串行化

串行化

l           将一个对象存放到永久存储器上称为保持。

l           只有对象的数据被串行化。

l           标记为transient关键字的数据不被串行化。

 

 

 

 

 


JDK1.1开始具有的新特性包括导入java.io.Serializable接口和改变JVM使之支持将一个Java技术对象存放到一个流的能力。

将一个对象存放到某种类型的永久存储器上称为保持。如果一个对象可以被存放到磁盘或磁带上,或者可以发送到另外一台机器并存放到存储器或磁盘上,那么这个对象就被称为可保持的。

java.io.Serializable接口没有任何方法,它只作为一个“标记者”,用来表明实现了这个接口的类可以考虑串行化。类中没有实现Serializable的对象不能保存或恢复它们的状态。

 

14.10.1  对象图

当一个对象被串行化时,只有对象的数据被保存;方法和构造函数不属于串行化流。如果一个数据变量是一个对象,那么这个对象的数据成员也会被串行化。树或者对象数据的结构,包括这些子对象,构成了对象图。

因为有些对象类所表示的数据在不断地改变,所以它们不会被串行化;例如,java.io.FileInputStream java.io.FileOutputStreamjava.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.javaJava应用程序,它将打开、读取并显示任何可读文件的内容。

2. 在你的应用程序中包含合适的异常处理,使之在不能显示文件时提示合适的出错信息。

水平2:创建一个简单的数据库程序

1.         创建一个称为DBTest.java的应用程序,它模仿了一个能存储和获取产品记录的小型数据库程序。使用RandomAccessFile类和平坦式文件。

u         数据库中的记录应当由字符串名称和整数量组成。

u         你的程序应当允许用户显示、更新和添加记录。

水平3:使用保持

1.         采用模块9中的Paint程序,并将画布的状态保存到一个文件中。

 

三、练习小结

       讨论 花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。

l         经验

l         解释

l         总结

l         应用

 

四、检查一下你的进度

在进入下一个模块的学习之前,请确认你能够:

l         描述和使用java.io包的流式思想

l         构造文件和过滤器流,并恰当地使用它们

l         区别流与读者和作者,并进行合适的选择

l         考察并操作文件和目录

l         读、写和更新文本和数据文件

l         使用Serialization接口来保持对象的状态

 

五、思考题

你是否有需要I/O的应用程序?

 

第15章          网 络

 

本模块讨论了JDKsocketsocket编程的支持。socket编程用来与在相同的网络上的另一台计算机上运行的程序进行通信。

 

第一节  相关问题

 

讨论 以下为与本模块内容有关的问题:

l      如何在网络上建立客户机与服务器之间的通信链路?

 

 

第二节 

 

在完成本模块之后,你应当能够:

l         开发代码来建立网络连接

l         理解TCP/IPUDP协议

l         ServerSocketSocket类来实现TCP/IP客户和服务器

l         DatagramPacketDatagramSocket来有效地进行基于UDP的网络通信。

 

第三节 

网络

l         socket

l         socket包含两个流

l         建立连接

l         建立过程与电话系统相似

 

 

 

 

 

 


15.3.1  socket

socket是指在一个特定编程模型下,进程间通信链路的端点。因为这个特定编程模型的流行,socket这个名字在其他领域得到了复用,包括Java技术。

当进程通过网络进行通信时,Java技术使用它的流模型。一个socket包括两个流:一个输入流和一个输出流。如果一个进程要通过网络向另一个进程发送数据,只需简单地写入与socket相关联的输出流。一个进程通过从与socket相关联的输入流读来读取另一个进程所写的数据。

建立网络连接之后,使用与socket相关联的流和使用其他流是非常相似的。

 

15.3.2  建立连接

如果要建立连接,一台机器必须运行一个进程来等待连接,而另一台机器必须试图到达第一台机器。这和电话系统类似;一方必须发起呼叫,而另一方在此时必须等待电话呼叫。

 

第四节  Java技术中的网络

Java技术中的网络

l         连接的地址

l         远程机器的地址和名字

l         端口号表明目的

l         端口号

l         范围从0~65535

 

 

 

 

 

 

 

 


15.4.1  连接的地址

你发起电话呼叫时,你必须知道所拨的电话号码。如果要发起网络连接,你需要知道远程机器的地址或名字。此外,每个网络连接需要一个端口号,你可以把它想象成电话的分机号码。一旦你和一台计算机建立连接,你需要指明连接的目的。所以,就如同你可以使用一个特定的分机号码来和财务部门对话那样,你可以使用一个特定的端口号来和会计程序通信。

 

15.4.2  端口号

TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。实际上,小于1024的端口号保留给预定义的服务,而且除非要和那些服务之一进行通信(例如telnetSMTP邮件和ftp),否则你不应该使用它们。

客户和服务器必须事先约定所使用的端口。如果系统两部分所使用的端口不一致,那就不能进行通信。

 

15.4.3  Java网络模型

Java编程语言中,TCP/IP socket连接是用java.net包中的类实现的。下图说明了服务器和客户端所发生的动作。

l         服务器分配一个端口号。如果客户请求一个连接,服务器使用accept()方法打开socket连接。

l         客户在hostport端口建立连接。

l         服务器和客户使用InputStreamOutputStream进行通信。

 

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

 

UDP socket

l         它们是无连接的协议。

l         不保证消息的可靠传输。

l         它们由Java技术中的DatagramSocketDatagramPacket类支持。

 

 

 

 

 

 

 


TCP/IP是面向连接的协议。而用户数据报协议(UDP)是一种无连接的协议。要区分这两种协议,一种很简单而又很贴切的方法是把它们比作电话呼叫和邮递信件。

电话呼叫保证有一个同步通信;消息按给定次序发送和接收。而对于邮递信件,即使能收到所有的消息,它们的顺序也可能不同。

用户数据报协议(UDP)Java软件的DatagramSocketDatagramPacket类支持。包是自包含的消息,它包括有关发送方、消息长度和消息自身。

 

15.5.1  DatagramPacket

DatagramPacket

DatagramPacket有两个构造函数:一个用来接收数据,另一个用来发送数据。

DatagramPacket(byte [] recvBuf, int readLength)

DatagramPacket(byte [] sendBuf, int sendLength, InetAddress iaddr, int iport)

 

 

 

 

 

 

 

 


DatagramPacket有两个构造函数:一个用来接收数据,另一个用来发送数据:

l      DatagramPacket(byte [] recvBuf, int readLength)-用来建立一个字节数组以接收UDP包。byte数组在传递给构造函数时是空的,而int值用来设定要读取的字节数(不能比数组的大小还大)

l      DatagramPacket(byte [] sendBuf, int sendLength, InetAddress iaddr, int iport)-用来建立将要传输的UDP包。sendLength 不应该比sendBuf字节数组的大小要大。

 

15.5.2  DatagramSocket

DatagramSocket

DatagramSocket有三个构造函数:

 

 

 

 


DatagramSocket用来读写UDP包。这个类有三个构造函数,允许你指定要绑定的端口号和internet地址:

l      DatagramSocket()-绑定本地主机的所有可用端口

l      DatagramSocket(int port)-绑定本地主机的指定端口

l      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

成对地进行这个练习,这样你可以使用其他人的机器的名字。你将要创建一个服务器和客户对,还有一个从其中之一请求一个文件的程序。

1.     服务器模板(FileServer.java)位于templates目录下。编写一个方法,它能使服务器接收来自客户的文件名字符串,试图打开这个文件并通过socket将它传回到客户。

2. 客户模板(ReadFile.java)也位于templates目录下。客户程序将文件名字符串作为一个参数并将它发给服务器,然后等待服务器发送错误响应或文件。

水平3:创建一个多线程的服务器(MultiServer.java)

1. 扩展客户代码,使客户能请求多个文件。

2.          扩展客户,使它在没有错误返回时,将文件存放到磁盘。

3.   使用线程扩展服务器,这样多个客户就可以同时连接到服务器。

 

三、练习小结

讨论 花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。

l      经验

l      解释

l      总结

l      应用

 

四、检查一下你的进度

在进入下一个模块的学习之前,请确认你能够:

l      开发代码来建立网络连接

l      理解TCP/IPUDP协议

l      ServerSocketSocket类来实现TCP/IP客户和服务器

l      DatagramPacketDatagramSocket来有效地进行基于UDP的网络通信。

 

五、思考题

有若干关于Java平台的高级话题,它们中的许多将在其他Sun教育课程中讨论。附录A给出了其中一些的简短描述。你还可以查看JavaSoftWeb站点(www.javasoft.com)

 

附录A  Java高级编程的元素

 

一、                                                                                                                                                                                                                                       目标

完成这个附录之后,你应当能够:

l         理解分布式计算的二层体系结构和三层体系结构

l         理解Java编程语言作为数据库应用程序的前台程序的作用

l         使用JDBC API

l         理解使用对象代理的数据交换方法

l         解释JavaBeans的组件模型

 

二、二层体系结构和三层体系结构

客户/服务器包括两个或更多的计算机共享一个与完整应用程序相关的任务。理想情况下,每台计算机执行的逻辑适合它的设计和所声称的功能。

使用最广泛的客户/服务器实现是二层体系结构。这包括一个前台的客户应用程序与在另外一台计算机上运行的后台数据库引擎通信。客户程序向数据库服务器发送SQL语句。服务器返回合适的结果,而客户负责处理数据。

应用程序所采用的基本二层客户/服务器模型得到了许多流行数据库系统的支持,包括:OracleSybaseInformix

二层客户/服务器结构在性能上损失很大。因为客户方要处理大部分的逻辑,所以客户方软件变得越来越大,越来越复杂。而服务器方的逻辑仅限于数据库操作。这里的客户称为肥客户。

肥客户可能为了访问远程数据库而产生频繁的网络流量。这对于Intranet和基于局域网的网络拓扑来说是不错的,并对台式机在内存和磁盘方面的需求产生了重要的影响。此外,后台的数据库在服务器所提供的逻辑方面并不都是相同的,而且它们都有自己的API集合,程序员必须使用这些API来优化和缩放性能。下面要介绍的三层客户/服务器结构,以更有效的方式来处理可伸缩性、性能和逻辑分割。

 

  、三层体系结构

三层体系结构是最先进的客户/服务器软件结构。三层体系结构一开始要求很陡的开发曲线,特别是当你必须支持不同的平台和网络环境。它的好处在于减少网络流量,优秀的Internetintranet性能以及对系统的扩展和增长有更多的控制。

 

三层客户/服务器定义

 

 

 

 


三层客户/服务器环境的三个组件或三个层次是表示,商业逻辑或功能,以及数据。它们是分离的,因此这样其中任何一层的软件都可以用一种不同的实现来代替而不影响其它层。例如,如果你想把一个面向字符的屏幕替换成一个图形用户界面(表示层),你只要使用已建立的API或接口来编写图形用户界面,用它来存取面向字符的屏幕中的功能程序。商业逻辑提供了定义所有商业规则的功能,而操作数据是通过商业规则进行的。商业政策的改变可以只对这一层产生影响,而不影响数据库。第三层,也就是数据层,包括系统、应用程序和数据,其中数据被封装起来,这是为了利用这个体系结构的优点,即最小的程序移植量。

 

数据库前台

Java编程语言为软件工程师创建面向数据库系统的前台应用程序提供了很多便利。由于它的“一次编写,到处使用”SM的特点,Java编程语言具有可适用于多种硬件和操作系统的优点。甚至在多平台环境中,程序员也不需要编写与特点平台有关的前台应用程序。

Java技术支持大量用于前台开发的类,这使得通过JDBC API来和数据库进行交互成为可能。JDBC API提供了到后台数据库的连接,它可以可以返回查询结果,并由前台处理。

在二层模型中,数据库位于数据库服务器。客户执行的前台应用程序打开一个socket,用来进行网络通信。socket提供了客户应用程序和后台服务器之间的通信。在下图中,客户程序向客户服务器发送SQL数据库请求。服务器将结果返回给客户,由客户格式化结果并进行表示。

经常使用的数据操作机制经常被作为嵌入的“存储过程”。在操作数据库的过程中,满足一定条件就会自动执行触发器。这个模型的主要缺点是所有的商业规则都在客户应用程序中实现,创建了庞大的客户方运行时(程序)以及增加客户端代码的重写量。

在三层模型中,所有的商业规则都嵌入到客户(前台)层。它和中间服务器交互,后者提供了关于后台应用程序的抽象。中间层管理商业规则,而商业规则通过应用程序的管理条件来操纵数据。中间服务器也接受基于各种通信协议的从若干客户到一个或多个服务器的连接。中间层为应用程序提供了一个与数据库无关的接口,并使得前台更加健壮。

JDBC API介绍

创建健壮且平台无关的应用程序和基于WebApplet的能力促使开发者提供具有前台连接性的解决方案。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

JDBCODBC桥是一个JDBC驱动,它把JDBC调用转换为ODBC操作。这个桥使得所有支持ODBCDBMS都可以和Java应用程序交互。

JDBCODBC桥接口作为一套共享动态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应用程序的步骤。这个过程包括以下步骤:

l         定义远程类的接口

l         创建并编译远程类的实现类

l         使用rmic命令创建存根和骨架类

l         创建并编译服务器应用程序

l         启动RMI注册器和服务器应用程序

l         创建并编译客户程序来存取远程对象

l         测试客户

 

 

 

CORBA

    CORBA是一个规范,它定义远程对象如何进行互操作。CORBA规范由对象管理小组(OMG)控制,它是一个有700多家公司共同参与,制定分布式计算开放标准的国际性开放组织。有关更多的细节,可以参考以下URL

http://www.omg.org

CORBA对象可以用任何编程语言重写,包括CC++。这些对象可以存在于任何平台之上,包括SolarisWindows95/NTopenVMSDigital UNIXHP_UX和其它许多平台。这意味着,在一个Windows95平台上运行的Java应用程序可以动态装载并使用由UNIX Web服务器存放在Internet上的C++对象。

通过使用接口定义语言(IDL)构造对象的接口,可以获得语言无关性。IDL允许用同样的方式描述所有的CORBA对象;唯一的要求是本地语言(C / C++COBOLJava)和IDL之间的一座“桥”。

CORBA的核心是对象资源代理(ORB)。ORB是在CORBA应用程序的客户和服务器之间传输信息的主要组件。ORB管理调度请求,建立到服务器的连接,发送数据,并在服务器上执行请求。服务器返回操作结果的过程与之类似。来自不同厂商的ORBTCP/IP上通信时,使用的是Internet内部ORB协议(IIOP),它是CORBA 2.0标准的一部分。

 

Java IDL

Java IDLJava编程语言增加了CORBA能力,它提供了基于标准的互操作性和连接性。Java IDL允许分布式Java web应用程序使用工业标准IDLIIOP来透明地请求远程网络服务器上的操作。

Java IDL不是OMGIDL的一个实现。事实上,它是一个使用IDL来定义接口的CORBA ORBidltojava编译器生成可移植的客户存根和服务器骨架。通过使用名字服务存取引用对象,这使得CORBA客户能够与在远程服务器上运行的另一个对象交互。与RMI注册器类似,名字服务是在远程服务器上运行的一个后台进程它包含一张关于名字服务和用来解析客户请求的远程对象引用的表。

建立一个CORBA对象的步骤可以概述如下:

l             使用接口定义语言(IDL)来创建对象的接口。

l             使用javatoidl编译器将接口转换成存根和骨架。

l             实现骨架对象,创建CORBA服务器对象。

l             编译并执行服务器对象,将它绑定到名字服务。

l             执行客户对象,通过CORBA名字服务存取服务器对象。

 

RMI与CORBA之比较

RMI最大的优点源于以下事实:它被设计成一个安全的解决方案。这意味着构筑RMI应用程序非常简单,而且所有的远程对象和本地对象有相同的特性,对于一个多层解决方案,有可能组合JDBCRMI两者的精华部分。

与此同时,CORBA受益于如下事实,它是一个语言无关的解决方案,但它在开发周期中引入了不可忽视的复杂性,并妨碍了垃圾回收特性。对于数据库应用程序的开发人员,CORBA为异构环境提供了最好的灵活性。服务器可以用CC++开发,而客户可以是一个Java applet

 

Java Beans组件模型

Java Beans是一个集成技术和组件框架,它允许可重用的组件对象(称为Beans)相互通信,以及与框架通信。

Java Beans是一个独立的、可重用的软件组件,可以用构造工具来直观地操纵它。Bean可以象AWT组件那样是可视的对象,也可以象队列和栈那样是不可视的对象。一个构造器/综合工具操作Beans来创建Applet和应用程序。组件模型由Java Beans 1.00A规范指定,这个规范定义了五个主要的服务:

l      内省

这个过程给出了Java Bean组件支持的特性、方法和事件。这个服务在运行时刻且正在使用可视化构造工具创建Java Bean时被使用。

l      通信

事件处理机制创建一个事件,这个事件将作为到其他组件的消息。

l      保持

保持是存储组件状态的一种途径。支持保持的最简单的方法是利用Java对象串行化,而使用bean来真正地保存Bean的状态取决于浏览器或应用程序。

l      属性

属性是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作为实现addXXXListenerEventObject e)和removeXXXListenerEventObject e)方法的源,其中XXX是事件类型的名称。然后开发者可以通过实现XXXListener接口使其他Bean作为事件目标。调用sourceBean.addXXXListner(targetBean),将sourceBeantargetBean结合在一起。

 

属性

属性定义了Bean的特征。在运行时刻可以通过它们的getset方法来改变它们。

属性可以用来在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交互的示例

在图A1中,容器A和容器B包含6Bean。一个Bean可以和位于同一个容器中的Bean以及位于其他容器中的Bean交互。在这个示例中,Bean1Bean4交互。它不与位于同一个容器中的Bean2Bean3交互,这表明一个Bean可以和任何其他Bean通信,而不限于是否在同一个容器中。然而,Bean4和位于同一个容器中的Bean5通信。源Bean1向目标Bean4发送一个事件,这使得它在事件监听者上监听消息。所有其他的容器内与容器间的Bean交互可以用类似的方式进行解释。

 

Bean开发箱BDK

BDKJavaSoft开发的Java应用程序,它允许Java开发者使用Bean模型创建可重用的组件。它是一个完整的系统,包括所有示例的源代码和文档。BDK还带有一个示例性的Bean构造器和称为BeanBox的客户化应用程序。BeanBox是一个测试者容器,可用来做以下事情:

l        改变Bean的大小和移动Bean

l        用属性表改变Bean

l        用自定义应用程序来自定义Beans

l        Beans连接在一起

l        Beans放到一个复合窗口中

l        通过串行化保存Beans

l        恢复Beans

       它带有12个示例Bean,覆盖Java Beans API的所有方面。

 

十一JAR文件

       JAR(库)是一个独立于平台的文件格式,它允许将许多文件集成为一个文件。多个Java applet和它们所需的组件(.class文件、图像和声音)可以捆绑到一个JAR文件中,然后在单个超文本传输协议(HTTP)的传输中下载到浏览器,这大大提高了下载速度。JAR格式还支持压缩,减少了文件大小,从而进一步加快了下载速度。此外,Applet的作者可以在JAR文件中采取数字签名来认证文件的起源。这对于已存在的Applet,完全可以保证向后兼容,而且可以被执行。

       改变你的HTML页面中的applet标记来包含一个JAR文件是很容易的。服务器上的JARARCHIVE参数标识,它包含了JAR文件相对于HTML页面的目录位置。例如:

       <applet code=Animator.class

archive="jars/animator.jar"

width=460 height=160>

<param name=foo value="bar">

</applet>

 

十二、检查一下你的进度

在进入下一个模块的学习之前,请确认你能够:

l         理解分布式计算的二层体系结构和三层体系结构

l         理解Java编程语言作为数据库应用程序的前台程序的作用

l         使用JDBC API

l         理解使用对象代理的数据交换方法

l         解释JavaBeans的组件模型

 

 

附录B  GridBagLayout管理器

 

一、目标

本附录详细探讨GridBagLayout管理器。

 

GridBagLayout管理器

GridBagLayout使得在一个网格布局中能够对独立单元的大小和位置进行更精细的控制。每个网格区域或组件与一个特别的GridBagConstraints实例相关,该实例指定了如何在该显示区域中设计组件的布局。GridBagLayout涉及比前面所描述的布局更多的工作,但是你可以获得你想要的内容。

GridBagLayout是标准Java平台上,目前为止所提供的最灵活的布局管理器。它对于你的整体应用程序是一个非常有用的补充。

 

GridBagLayout的功能

GridLayout类似,GridBagLayout将组件在一个矩形的“单元”网格中对齐。GridLayoutGridBagLayout的区别在于,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实例变量

关于GridBagLayoutAPI页解释了用于控制组件布局的实例变量。它们是:

 

* gridxgridy

这些变量指定了位于组件显示区域左上方的单元的特征,其中,最左上方的单元具有地址gridx=0gridy=0。你可用GridBagConstraints.RELATIVE(缺省值)来指定某组件的相对位置,即,它相对于在它之前被加入到容器中组件的右方(gridx)或下方(gridy)的位置。

 

* gridwidthgridheight

这些变量指定了在组件的显示区域中,行(gridwidth)或列(gridheight)中的单元数目。缺省值为1。你可用GridBagConstraints.REMAINDER来指定某组件在其行(gridwidth)或列(gridheight)中为最后一个。用GridBagConstraints.RELATIVE可指定某组件在其行(gridwidth)或列(gridheight)中与最后一个相邻。

 

* fill

fill在某组件的显示区域大于它所要求的大小时被使用;fill决定了是否(和怎样)改变组件的大小。有效值为GridBagConstraints.NONE(缺省),GridBagConstraints.HORIZONTAL(使该组件足够大,以填充其显示区域的水平方向,但不改变其高度),GridBagConstraints.VERTICAL(使该组件足够大,以填充其显示区域的垂直方向,但不改变其宽度),GridBagConstraints.BOTH(使该组件填充其整个显示区域)。

 

* ipadxipady

这些变量指定了内部填充;即,在该组件的最小尺寸基础上还需增加多少。组件的宽度必须至少为其最小宽度加ipadx*2个象素(因为填充作用于组件的两边)。同样地,组件的高度必须至少为其最小高度加ipady*2个象素。

 

* insets

insets指定了组件的外部填充;即,组件与其显示区域边界之间的最小空间大小。

 

* anchor