| 議誌戰勝①切's profile議誌戰勝①切SpacesPhotosBlogLists | Help |
|
3/5/2006 谈论J2EE开发环境配置个人总结
引用 J2EE开发环境配置个人总结 大家使用link方式对各种插件进行统一的组织管理,网上有很多对这种方法的讲解,希望能明白它的工作方式以举一反三以后不同的插件也就能轻易安装而不再需要上网查了。与将插件压缩包直接解压到eclipse的plugins下类似,ec在启动时都会寻找并装载eclipse\plugins目录下的插件。若将插件单独管理(即所谓的link方式),则在磁盘上任意位置解压一个插件包后应保证eclipse\plugins目录,有些插件解压后需要我们自己创建eclipse目录。之后在ec下的links文件夹(可能需要自己新建)下创建指向某一插件位置的link文件(如tomcat.link),其中编辑path=<插件目录中的eclipse的父目录>,注意分隔符应为“\\"或“/”。之后应先删除eclipse\configuration下的org.eclipse.update文件后再启动eclipse以便对工作区间进行更新。之后在窗口\首选项中进行一些必要的参数设定。以上是安装所有插件的通用方法。
在安装第二个插件Lomboz之前想过myEclipse,听说是个优秀功能强大的东东,但由于myec是要money的而且俺学校的网速可受不了这玩意儿100多M的天文数字,而且郁闷的是官方网下软件还需要先注册帐号。所以还是决定用lomboz(只是听说这个的配置麻烦的多),我在www.javaresearch.org(推荐给大家,这个java论坛有很丰富的资源)下载了lomboz3.01版6.33M,我也差不多被它忽悠得差不多了。看着网上教程的时候就感觉不对,我这lomboz比教程所描述的少了一个features目录,结果一切都乱套,直到今天才发现官方网(http://forge.objectweb.org/project/showfiles.php?group_id=97)下载的是6.93M,内容当然就是完整的了。而且在此之前竟不知道在装lomboz前还需先装好另一插件emf(地址http://download.eclipse.org/tools/emf/scripts/downloads.php#goto2.1.0I)。按以上方法安装了插件后都需要删除eclipse\configuration下的org.eclipse.update后再启动eclipse进行设置。
在窗口\首选项中的设置,对于lomboz我复制了网上的一些资料如下:
4:设置Lomboz 在Preferences窗口中选定Lomboz,然后把右边面板中的JDK Tools.jar选择为你安装好JDK后,在JAVA_HOME\LIB\目录下面的tools.jar文件就可以了,一定不要搞错了,如果你的JDK是安装在F:\J2SDK\下面,那么这里的值就应该是F:\J2SDK\lib\tools.jar,是其它的都不对。其它的保持为默认值。 接着把Lomboz这一项展开,关键要设置的地方是Server Definitions这一项,另外两项Code Generation和JSP Editor可以不管。展开Lomboz的Server Definitions后,在Server types里选择Apache Tomcat v4.1.0(这是按照我的机器上的配置进行设置的,如果你安装的是其它版本的TOMCAT,你就要选择对应的选项),主要设置如下: Properties Application Server Directory:这是我的TOMCAT 4.1.30的安装目录,我装在F:\tomcat下面,所以它的值就是F:/tomcat Address:127.0.0.1 Port:80(因为我在TOMCAT中把8080改为80了,所以这里就是80了,以你的TOMCAT的监听端口为准) Classpath Variable Name: TOMCAT_HOME(可以修改成其它的字符串) Classpath Variable: F:/tomcat(TOMCAT的安装目录) 然后点击Apply按钮。 Server Classpath: JDK_TOOLS ${classPathVariableName}/bin/bootstrap.jar ${classPathVariableName}/common/servlet.jar ${classPathVariableName}/common/jasper-runtime.jar 然后点击Apply按钮。 Client Classpath: 这里为空,什么也没有。 10/29/2005 JAVA教程 第八讲 Java网络编程(三)8.3.10 据报Datagram通讯 可以看出使用UDP和使用TCP在程序上还是有很大的区别的。一个比较明显的区别是,UDP的Socket编程是不提供监听功能的,也就是说通信双方更为平等,面对的接口是完全一样的。但是为了用UDP实现C/S结构,在使用UDP时可以使用DatagramSocket.receive()来实现类似于监听的功能。因为receive()是阻塞的函数,当它返回时,缓冲区里已经填满了接受到的一个数据报,并且可以从该数据报得到发送方的各种信息,这一点跟accept()是很相象的,因而可以根据读入的数据报来决定下一步的动作,这就达到了跟网络监听相似的效果。 前面在介绍TCP/IP协议的时候,我们已经提到,在TCP/IP协议的传输层除了TCP协议之外还有一个UDP协议,相比而言UDP的应用不如TCP广泛,几个标准的应用层协议HTTP,FTP,SMTP…使用的都是TCP协议。但是,随着计算机网络的发展,UDP协议正越来越来显示出其威力,尤其是在需要很强的实时交互性的场合,如网络游戏,视频会议等,UDP更是显示出极强的威力,下面我们就介绍一下Java环境下如何实现UDP网络传输。 8.3.11 什么是Datagram 所谓数据报(Datagram)就跟日常生活中的邮件系统一样,是不能保证可靠的寄到的,而面向链接的TCP就好比电话,双方能肯定对方接受到了信息。在本章前面,我们已经对UDP和TCP进行了比较,在这里再稍作小节: TCP,可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。 UDP,不可靠,差错控制开销较小,传输大小限制在64K以下,不需要建立连接。 总之,这两种协议各有特点,应用的场合也不同,是完全互补的两个协议,在TCP/IP协议中占有同样重要的地位,要学好网络编程,两者缺一不可。 8.3.12 Datagram通讯的表示方法:DatagramSocket;DatagramPacket 包java.net中提供了两个类DatagramSocket和DatagramPacket用来支持数据报通信,DatagramSocket用于在程序之间建立传送数据报的通信连接, DatagramPacket则用来表示一个数据报。先来看一下DatagramSocket的构造方法: DatagramSocket(); DatagramSocket(int prot); DatagramSocket(int port, InetAddress laddr) 其中,port指明socket所使用的端口号,如果未指明端口号,则把socket连接到本地主机上一个可用的端口。laddr指明一个可用的本地地址。给出端口号时要保证不发生端口冲突,否则会生成SocketException类例外。注意:上述的两个构造方法都声明抛弃非运行时例外SocketException,程序中必须进行处理,或者捕获、或者声明抛弃。 用数据报方式编写client/server程序时,无论在客户方还是服务方,首先都要建立一个DatagramSocket对象,用来接收或发送数据报,然后使用DatagramPacket类对象作为传输数据的载体。下面看一下DatagramPacket的构造方法 : DatagramPacket(byte buf[],int length); DatagramPacket(byte buf[], int length, InetAddress addr, int port); DatagramPacket(byte[] buf, int offset, int length); DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port); 其中,buf中存放数据报数据,length为数据报中数据的长度,addr和port旨明目的地址,offset指明了数据报的位移量。 在接收数据前,应该采用上面的第一种方法生成一个DatagramPacket对象,给出接收数据的缓冲区及其长度。然后调用DatagramSocket 的方法receive()等待数据报的到来,receive()将一直等待,直到收到一个数据报为止。 DatagramPacket packet=new DatagramPacket(buf, 256); Socket.receive (packet); 发送数据前,也要先生成一个新的DatagramPacket对象,这时要使用上面的第二种构造方法,在给出存放发送数据的缓冲区的同时,还要给出完整的目的地址,包括IP地址和端口号。发送数据是通过DatagramSocket的方法send()实现的,send()根据数据报的目的地址来寻径,以传递数据报。 DatagramPacket packet=new DatagramPacket(buf, length, address, port); Socket.send(packet); 在构造数据报时,要给出InetAddress类参数。类InetAddress在包java.net中定义,用来表示一个Internet地址,我们可以通过它提供的类方法getByName()从一个表示主机名的字符串获取该主机的IP地址,然后再获取相应的地址信息。 8.3.13 基于UDP的简单的Client/Server程序设计 有了上面的知识,我们就可以来构件一个基于UDP的C/S 网络传输模型 1. 客户方程序 QuoteClient.java import java.io.*; import java.net.*; import java.util.*; public class QuoteClient { public static void main(String[] args) throws IOException { if(args.length!=1) { //如果启动的时候没有给出Server的名字,那么出错退出 System.out.println("Usage:java QuoteClient "); //打印出错信息 return; //返回 } DatagramSocket socket=new DatagramSocklet(); //创建数据报套接字 Byte[] buf=new byte[256]; //创建缓冲区 InetAddress address=InetAddress.getByName(args [0]); //由命令行给出的第一个参数默认为Server的名字,通过它得到Server的IP信息 DatagramPacket packet=new DatagramPacket (buf, buf.length, address, 4445); //创建DatagramPacket对象 socket.send(packet); //发送 packet=new DatagramPacket(buf,buf.length); //创建新的DatagramPacket对象,用来接收数据报 socket.receive(packet); //接收 String received=new String(packet.getData()); //根据接收到的字节数组生成相应的字符串 System.out.println("Quote of the Moment:"+received ); //打印生成的字符串 socket.close(); //关闭套接口 } } 2. 服务器方程序:QuoteServer.java public class QuoteServer{ public static void main(String args[]) throws java.io.IOException { new QuoteServerThread().start(); //启动一个QuoteServerThread线程 } } 3. 程序QuoteServerThread.java import java.io.*; import java.net.*; import java.util.*; //服务器线程 public class QuoteServerThread extends Thread { protected DatagramSocket socket=null; //记录和本对象相关联的DatagramSocket对象 protected BufferedReader in=null; //用来读文件的一个Reader protected boolean moreQuotes=true; //标志变量,是否继续操作 public QuoteServerThread() throws IOException { //无参数的构造函数 this("QuoteServerThread"); //以QuoteServerThread为默认值调用带参数的构造函数 } public QuoteServerThread(String name) throws IOException { super(name); //调用父类的构造函数 socket=new DatagramSocket(4445); //在端口4445创建数据报套接字 try{ in= new BufferedReader(new FileReader(" one-liners.txt")); //打开一个文件,构造相应的BufferReader对象 }catch(FileNotFoundException e) { //异常处理 System.err.println("Could not open quote file. Serving time instead."); //打印出错信息 } } public void run() //线程主体 { while(moreQuotes) { try{ byte[] buf=new byte[256]; //创建缓冲区 DatagramPacket packet=new DatagramPacket(buf,buf.length); //由缓冲区构造DatagramPacket对象 socket.receive(packet); //接收数据报 String dString=null; if(in= =null) dString=new Date().toString(); //如果初始化的时候打开文件失败了, //则使用日期作为要传送的字符串 else dString=getNextQuote(); //否则调用成员函数从文件中读出字符串 buf=dString.getByte(); //把String转换成字节数组,以便传送 InetAddress address=packet.getAddress(); //从Client端传来的Packet中得到Client地址 int port=packet.getPort(); //和端口号 packet=new DatagramPacket(buf,buf.length,address,port); //根据客户端信息构建DatagramPacket socket.send(packet); //发送数据报 }catch(IOException e) { //异常处理 e.printStackTrace(); //打印错误栈 moreQuotes=false; //标志变量置false,以结束循环 } } socket.close(); //关闭数据报套接字 } protected String getNextQuotes(){ //成员函数,从文件中读数据 String returnValue=null; try { if((returnValue=in.readLine())= =null) { //从文件中读一行,如果读到了文件尾 in.close( ); //关闭输入流 moreQuotes=false; //标志变量置false,以结束循环 returnValue="No more quotes. Goodbye."; //置返回值 } //否则返回字符串即为从文件读出的字符串 }catch(IOEception e) { //异常处理 returnValue="IOException occurred in server"; //置异常返回值 } return returnValue; //返回字符串 } } 8.3.14 用数据报进行广播通讯 DatagramSocket只允许数据报发送一个目的地址,java.net包中提供了一个类MulticastSocket,允许数据报以广播方式发送到该端口的所有客户。MulticastSocket用在客户端,监听服务器广播来的数据。 我们对上面的程序作一些修改,利用MulticastSocket实现广播通信。新程序完成的功能是使同时运行的多个客户程序能够接收到服务器发送来的相同的信息,显示在各自的屏幕上。 1. 客户方程序:MulticastClient.java import java.io.*; import java.net.*; import java.util.*; public class MulticastClient { public static void main(String args[]) throws IOException { MulticastSocket socket=new MulticastSocket(4446); //创建4446端口的广播套接字 InetAddress address=InetAddress.getByName("230.0.0.1"); //得到230.0.0.1的地址信息 socket.joinGroup(address); //使用joinGroup()将广播套接字绑定到地址上 DatagramPacket packet; for(int i=0;i<5;i++) { byte[] buf=new byte[256]; //创建缓冲区 packet=new DatagramPacket(buf,buf.length); //创建接收数据报 socket.receive(packet); //接收 String received=new String(packet.getData()); //由接收到的数据报得到字节数组, //并由此构造一个String对象 System.out.println("Quote of theMoment:"+received); //打印得到的字符串 } //循环5次 socket.leaveGroup(address); //把广播套接字从地址上解除绑定 socket.close(); //关闭广播套接字 } } 2. 服务器方程序:MulticastServer.java public class MulticastServer{ public static void main(String args[]) throws java.io.IOException { new MulticastServerThread().start(); //启动一个服务器线程 } } 3. 程序MulticastServerThread.java import java.io.*; import java.net.*; import java.util.*; public class MulticastServerThread extends QuoteServerThread //从QuoteServerThread继承得到新的服务器线程类MulticastServerThread { Private long FIVE_SECOND=5000; //定义常量,5秒钟 public MulticastServerThread(String name) throws IOException { super("MulticastServerThread"); //调用父类,也就是QuoteServerThread的构造函数 } public void run() //重写父类的线程主体 { while(moreQuotes) { //根据标志变量判断是否继续循环 try{ byte[] buf=new byte[256]; //创建缓冲区 String dString=null; if(in==null) dString=new Date().toString(); //如果初始化的时候打开文件失败了, //则使用日期作为要传送的字符串 else dString=getNextQuote(); //否则调用成员函数从文件中读出字符串 buf=dString.getByte(); //把String转换成字节数组,以便传送send it InetAddress group=InetAddress.getByName("230.0.0.1"); //得到230.0.0.1的地址信息 DatagramPacket packet=new DatagramPacket(buf,buf.length,group,4446); //根据缓冲区,广播地址,和端口号创建DatagramPacket对象 socket.send(packet); //发送该Packet try{ sleep((long)(Math.random()*FIVE_SECONDS)); //随机等待一段时间,0~5秒之间 }catch(InterruptedException e) { } //异常处理 }catch(IOException e){ //异常处理 e.printStackTrace( ); //打印错误栈 moreQuotes=false; //置结束循环标志 } } socket.close( ); //关闭广播套接口 } } 至此,Java网络编程这一章已经讲解完毕。读者通过学习,应该对网络编程有了一个清晰的认识,可能对某些概念还不是十分的清楚,还是需要更多的实践来进一步掌握。编程语言的学习不同于一般的学习,及其强调实践的重要性。读者应该对URL网络编程,Socket中的TCP,UDP编程进行大量的练习才能更好的掌握本章中所提到的一些概念,才能真正学到Java网络编程的精髓! 最后几个小节所举的例子,读者务必要亲自试验一下,如果遇到问题,想办法解决之。最好能根据自己的意图加以改进。这样才能更好的理解这几个程序,理解其中所包含的编程思想。 本讲小结】 本讲主要讲解了Java环境下的网络编程。因为TCP/IP协议是Java网络编程的基础知识,本讲开篇重点介绍了TCP/IP协议中的一些概念,TCP/IP协议本身是一个十分庞大的系统,用几个小节是不可能讲清楚的。所以我们只是联系实际,讲解了一些最基本的概念,帮助学生理解后面的相关内容。重点有一下几个概念:主机名,IP,端口,服务类型,TCP,UDP。 后续的内容分为两大块,一块是以URL为主线,讲解如何通过URL类和URLConnection类访问WWW网络资源,由于使用URL十分方便直观,尽管功能不是很强,还是值得推荐的一种网络编程方法,尤其是对于初学者特别容易接受。本质上讲,URL网络编程在传输层使用的还是TCP协议。 另一块是以Socket接口和C/S网络编程模型为主线,依次讲解了如何用Java实现基于TCP的C/S结构,主要用到的类有Socket,ServerSocket。以及如何用Java实现基于UDP的C/S结构,还讨论了一种特殊的传输方式,广播方式,这种方式是UDP所特有的,主要用到的类有DatagramSocket , DatagramPacket, MulticastSocket。这一块在Java网络编程中相对而言是最难的(尽管Java在网络编程这方面已经做的够"傻瓜"了,但是网络编程在其他环境下的却是一件极为头痛的事情,再"傻瓜"还是有一定的难度),也是功能最为强大的一部分,读者应该好好研究,领悟其中的思想。 最后要强调的是要学好Java网络编程,Java语言,最重要的还是在于多多练习! JAVA教程 第八讲 Java网络编程(二)8.3 基于Socket(套接字)的低层次Java网络编程 8.3.1 Socket通讯 网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。 在传统的UNIX环境下可以操作TCP/IP协议的接口不止Socket一个,Socket所支持的协议种类也不光TCP/IP一种,因此两者之间是没有必然联系的。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。 说Socket编程是低层次网络编程并不等于它功能不强大,恰恰相反,正因为层次低,Socket编程比基于URL的网络编程提供了更强大的功能和更灵活的控制,但是却要更复杂一些。由于Java本身的特殊性,Socket编程在Java中可能已经是层次最低的网络编程接口,在Java中要直接操作协议中更低的层次,需要使用Java的本地方法调用(JNI),在这里就不予讨论了。 8.3.2 Socket通讯的一般过 前面已经提到Socket通常用来实现C/S结构。 使用Socket进行Client/Server程序设计的一般连接过程是这样的:Server端Listen(监听)某个端口是否有连接请求,Client端向Server端发出Connect(连接)请求,Server端向Client端发回Accept(接受)消息。一个连接就建立起来了。Server端和Client端都可以通过Send,Write等方法与对方通信。 对于一个功能齐全的Socket,都要包含以下基本结构,其工作过程包含以下四个基本的步骤: (1) 创建Socket; (2) 打开连接到Socket的输入/出流; (3) 按照一定的协议对Socket进行读/写操作; (4) 关闭Socket. 第三步是程序员用来调用Socket和实现程序功能的关键步骤,其他三步在各种程序中基本相同。 以上4个步骤是针对TCP传输而言的,使用UDP进行传输时略有不同,在后面会有具体讲解。 8.3.3 创建Socket java在包java.net中提供了两个类Socket和ServerSocket,分别用来表示双向连接的客户端和服务端。这是两个封装得非常好的类,使用很方便。其构造方法如下: Socket(InetAddress address, int port); Socket(InetAddress address, int port, boolean stream); Socket(String host, int prot); Socket(String host, int prot, boolean stream); Socket(SocketImpl impl) Socket(String host, int port, InetAddress localAddr, int localPort) Socket(InetAddress address, int port, InetAddress localAddr, int localPort) ServerSocket(int port); ServerSocket(int port, int backlog); ServerSocket(int port, int backlog, InetAddress bindAddr) 其中address、host和port分别是双向连接中另一方的IP地址、主机名和端口号,stream指明socket是流socket还是数据报socket,localPort表示本地主机的端口号,localAddr和bindAddr是本地机器的地址(ServerSocket的主机地址),impl是socket的父类,既可以用来创建serverSocket又可以用来创建Socket。count则表示服务端所能支持的最大连接数。例如: Socket client = new Socket("127.0.01.", 80); ServerSocket server = new ServerSocket(80); 注意,在选择端口时,必须小心。每一个端口提供一种特定的服务,只有给出正确的端口,才能获得相应的服务。0~1023的端口号为系统所保留,例如http服务的端口号为80,telnet服务的端口号为21,ftp服务的端口号为23, 所以我们在选择端口号时,最好选择一个大于1023的数以防止发生冲突。 在创建socket时如果发生错误,将产生IOException,在程序中必须对之作出处理。所以在创建Socket或ServerSocket是必须捕获或抛出例外。 8.3.4 客户端的Socket 下面是一个典型的创建客户端Socket的过程。 try{ Socket socket=new Socket("127.0.0.1",4700); //127.0.0.1是TCP/IP协议中默认的本机地址 }catch(IOException e){ System.out.println("Error:"+e); } 这是最简单的在客户端创建一个Socket的一个小程序段,也是使用Socket进行网络通讯的第一步,程序相当简单,在这里不作过多解释了。在后面的程序中会用到该小程序段。 8.3.5 服务器端的ServerSocket 下面是一个典型的创建Server端ServerSocket的过程。 ServerSocket server=null; try { server=new ServerSocket(4700); //创建一个ServerSocket在端口4700监听客户请求 }catch(IOException e){ System.out.println("can not listen to :"+e); } Socket socket=null; try { socket=server.accept(); //accept()是一个阻塞的方法,一旦有客户请求,它就会返回一个Socket对象用于同客户进行交互 }catch(IOException e){ System.out.println("Error:"+e); } 以上的程序是Server的典型工作模式,只不过在这里Server只能接收一个请求,接受完后Server就退出了。实际的应用中总是让它不停的循环接收,一旦有客户请求,Server总是会创建一个服务线程来服务新来的客户,而自己继续监听。程序中accept()是一个阻塞函数,所谓阻塞性方法就是说该方法被调用后,将等待客户的请求,直到有一个客户启动并请求连接到相同的端口,然后accept()返回一个对应于客户的socket。这时,客户方和服务方都建立了用于通信的socket,接下来就是由各个socket分别打开各自的输入/输出流。 8.3.6 打开输入/出流 类Socket提供了方法getInputStream ()和getOutStream()来得到对应的输入/输出流以进行读/写操作,这两个方法分别返回InputStream和OutputSteam类对象。为了便于读/写数据,我们可以在返回的输入/输出流对象上建立过滤流,如DataInputStream、DataOutputStream或PrintStream类对象,对于文本方式流对象,可以采用InputStreamReader和OutputStreamWriter、PrintWirter等处理。 例如: PrintStream os=new PrintStream(new BufferedOutputStreem(socket.getOutputStream())); DataInputStream is=new DataInputStream(socket.getInputStream()); PrintWriter out=new PrintWriter(socket.getOutStream(),true); BufferedReader in=new ButfferedReader(new InputSteramReader(Socket.getInputStream())); 输入输出流是网络编程的实质性部分,具体如何构造所需要的过滤流,要根据需要而定,能否运用自如主要看读者对Java中输入输出部分掌握如何。 8.3.7 关闭Socket 每一个Socket存在时,都将占用一定的资源,在Socket对象使用完毕时,要其关闭。关闭Socket可以调用Socket的Close()方法。在关闭Socket之前,应将与Socket相关的所有的输入/输出流全部关闭,以释放所有的资源。而且要注意关闭的顺序,与Socket相关的所有的输入/输出该首先关闭,然后再关闭Socket。 os.close(); is.close(); socket.close(); 尽管Java有自动回收机制,网络资源最终是会被释放的。但是为了有效的利用资源,建议读者按照合理的顺序主动释放资源。 8.3.8 简单的Client/Server程序设计 下面我们给出一个用Socket实现的客户和服务器交互的典型的C/S结构的演示程序,读者通过仔细阅读该程序,会对前面所讨论的各个概念有更深刻的认识。程序的意义请参考注释。 1. 客户端程序 import java.io.*; import java.net.*; public class TalkClient { public static void main(String args[]) { try{ Socket socket=new Socket("127.0.0.1",4700); //向本机的4700端口发出客户请求 BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); //由系统标准输入设备构造BufferedReader对象 PrintWriter os=new PrintWriter(socket.getOutputStream()); //由Socket对象得到输出流,并构造PrintWriter对象 BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); //由Socket对象得到输入流,并构造相应的BufferedReader对象 String readline; readline=sin.readLine(); //从系统标准输入读入一字符串 while(!readline.equals("bye")){ //若从标准输入读入的字符串为 "bye"则停止循环 os.println(readline); //将从系统标准输入读入的字符串输出到Server os.flush(); //刷新输出流,使Server马上收到该字符串 System.out.println("Client:"+readline); //在系统标准输出上打印读入的字符串 System.out.println("Server:"+is.readLine()); //从Server读入一字符串,并打印到标准输出上 readline=sin.readLine(); //从系统标准输入读入一字符串 } //继续循环 os.close(); //关闭Socket输出流 is.close(); //关闭Socket输入流 socket.close(); //关闭Socket }catch(Exception e) { System.out.println("Error"+e); //出错,则打印出错信息 } } } 2. 服务器端程序 import java.io.*; import java.net.*; import java.applet.Applet; public class TalkServer{ public static void main(String args[]) { try{ ServerSocket server=null; try{ server=new ServerSocket(4700); //创建一个ServerSocket在端口4700监听客户请求 }catch(Exception e) { System.out.println("can not listen to:"+e); //出错,打印出错信息 } Socket socket=null; try{ socket=server.accept(); //使用accept()阻塞等待客户请求,有客户 //请求到来则产生一个Socket对象,并继续执行 }catch(Exception e) { System.out.println("Error."+e); //出错,打印出错信息 } String line; BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); //由Socket对象得到输入流,并构造相应的BufferedReader对象 PrintWriter os=newPrintWriter(socket.getOutputStream()); //由Socket对象得到输出流,并构造PrintWriter对象 BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); //由系统标准输入设备构造BufferedReader对象 System.out.println("Client:"+is.readLine()); //在标准输出上打印从客户端读入的字符串 line=sin.readLine(); //从标准输入读入一字符串 while(!line.equals("bye")){ //如果该字符串为 "bye",则停止循环 os.println(line); //向客户端输出该字符串 os.flush(); //刷新输出流,使Client马上收到该字符串 System.out.println("Server:"+line); //在系统标准输出上打印读入的字符串 System.out.println("Client:"+is.readLine()); //从Client读入一字符串,并打印到标准输出上 line=sin.readLine(); //从系统标准输入读入一字符串 } //继续循环 os.close(); //关闭Socket输出流 is.close(); //关闭Socket输入流 socket.close(); //关闭Socket server.close(); //关闭ServerSocket }catch(Exception e){ System.out.println("Error:"+e); //出错,打印出错信息 } } } 从上面的两个程序中我们可以看到,socket四个步骤的使用过程。读者可以分别将Socket使用的四个步骤的对应程序段选择出来,这样便于读者对socket的使用有进一步的了解。 读者可以在单机上试验该程序,最好是能在真正的网络环境下试验该程序,这样更容易分辨输出的内容和客户机,服务器的对应关系。同时也可以修改该程序,提供更为强大的功能,或更加满足读者的意图。 8.3.9 支持多客户的client/server程序设计 前面提供的Client/Server程序只能实现Server和一个客户的对话。在实际应用中,往往是在服务器上运行一个永久的程序,它可以接收来自其他多个客户端的请求,提供相应的服务。为了实现在服务器方给多个客户提供服务的功能,需要对上面的程序进行改造,利用多线程实现多客户机制。服务器总是在指定的端口上监听是否有客户请求,一旦监听到客户请求,服务器就会启动一个专门的服务线程来响应该客户的请求,而服务器本身在启动完线程之后马上又进入监听状态,等待下一个客户的到来。 客户端的程序和上面程序是完全一样的,读者如果仔细阅读过上面的程序,可以跳过不读,把主要精力集中在Server端的程序上。 2. 服务器端程序: MultiTalkServer.java import java.io.*; import java.net.*; import ServerThread; public class MultiTalkServer{ static int clientnum=0; //静态成员变量,记录当前客户的个数 public static void main(String args[]) throws IOException { ServerSocket serverSocket=null; boolean listening=true; try{ serverSocket=new ServerSocket(4700); //创建一个ServerSocket在端口4700监听客户请求 }catch(IOException e) { System.out.println("Could not listen on port:4700."); //出错,打印出错信息 System.exit(-1); //退出 } while(listening){ //永远循环监听 new ServerThread(serverSocket.accept(),clientnum).start(); //监听到客户请求,根据得到的Socket对象和 客户计数创建服务线程,并启动之 clientnum++; //增加客户计数 } serverSocket.close(); //关闭ServerSocket } } 3. 程序ServerThread.java import java.io.*; import java.net.*; public class ServerThread extends Thread{ Socket socket=null; //保存与本线程相关的Socket对象 int clientnum; //保存本进程的客户计数 public ServerThread(Socket socket,int num) { //构造函数 this.socket=socket; //初始化socket变量 clientnum=num+1; //初始化clientnum变量 } public void run() { //线程主体 try{ String line; BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); //由Socket对象得到输入流,并构造相应的BufferedReader对象 PrintWriter os=newPrintWriter(socket.getOutputStream()); //由Socket对象得到输出流,并构造PrintWriter对象 BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); //由系统标准输入设备构造BufferedReader对象 System.out.println("Client:"+ clientnum +is.readLine()); //在标准输出上打印从客户端读入的字符串 line=sin.readLine(); //从标准输入读入一字符串 while(!line.equals("bye")){ //如果该字符串为 "bye",则停止循环 os.println(line); //向客户端输出该字符串 os.flush(); //刷新输出流,使Client马上收到该字符串 System.out.println("Server:"+line); //在系统标准输出上打印该字符串 System.out.println("Client:"+ clientnum +is.readLine()); //从Client读入一字符串,并打印到标准输出上 line=sin.readLine(); //从系统标准输入读入一字符串 } //继续循环 os.close(); //关闭Socket输出流 is.close(); //关闭Socket输入流 socket.close(); //关闭Socket server.close(); //关闭ServerSocket }catch(Exception e){ System.out.println("Error:"+e); //出错,打印出错信息 } } } 通过以上的学习,读者应该对Java的面向流的网络编程有了一个比较全面的认识,这些都是基于TCP的应用,后面我们将介绍基于UDP的Socket编程 JAVA教程 第八讲 Java网络编程(一)8.1 网络编程的基本概念,TCP/IP协议简介 8.1.1 网络基础知识 计算机网络形式多样,内容繁杂。网络上的计算机要互相通信,必须遵循一定的协议。目前使用最广泛的网络协议是Internet上所使用的TCP/IP协议 网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。网络编程中有两个主要的问题,一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输。在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以唯一地确定Internet上的一台主机。而TCP层则提供面向应用的可靠的或非可靠的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据的。 目前较为流行的网络编程模型是客户机/服务器(C/S)结构。即通信双方一方作为服务器等待客户提出请求并予以响应。客户则在需要服务时向服务器提出申请。服务器一般作为守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服务端口,使后来的客户也能及时得到服务。 8.1.2网络基本概念 IP地址:标识计算机等网络设备的网络地址,由四个8位的二进制数组成,中间以小数点分隔。 如:166.111.136.3 , 166.111.52.80 主机名(hostname):网络地址的助记名,按照域名进行分级管理。 如:www.tsinghua.edu.cn www.fanso.com 端口号(port number):网络通信时同一机器上的不同进程的标识。 如:80,21,23,25,其中1~1024为系统保留的端口号 服务类型(service):网络的各种服务。 http, telnet, ftp, smtp 在Internet上IP地址和主机名是一一对应的,通过域名解析可以由主机名得到机器的IP,由于机器名更接近自然语言,容易记忆,所以使用比IP地址广泛,但是对机器而言只有IP地址才是有效的标识符。 通常一台主机上总是有很多个进程需要网络资源进行网络通讯。网络通讯的对象准确的讲不是主机,而应该是主机中运行的进程。这时候光有主机名或IP地址来标识这么多个进程显然是不够的。端口号就是为了在一台主机上提供更多的网络资源而采取得一种手段,也是TCP层提供的一种机制。只有通过主机名或IP地址和端口号的组合才能唯一的确定网络通讯中的对象:进程。 服务类型是在TCP层上面的应用层的概念。基于TCP/IP协议可以构建出各种复杂的应用,服务类型是那些已经被标准化了的应用,一般都是网络服务器(软件)。读者可以编写自己的基于网络的服务器,但都不能被称作标准的服务类型。 8.1.3两类传输协议:TCP;UDP 尽管TCP/IP协议的名称中只有TCP这个协议名,但是在TCP/IP的传输层同时存在TCP和UDP两个协议。 TCP是Tranfer Control Protocol的简称,是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。 UDP是User Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。 下面我们对这两种协议做简单比较: 使用UDP时,每个数据报中都给出了完整的地址信息,因此无需要建立发送方和接收方的连接。对于TCP协议,由于它是一个面向连接的协议,在socket之间进行数据传输之前必然要建立连接,所以在TCP中多了一个连接建立的时间。 使用UDP传输数据时是有大小限制的,每个被传输的数据报必须限定在64KB之内。而TCP没有这方面的限制,一旦连接建立起来,双方的socket就可以按统一的格式传输大量的数据。UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。而TCP是一个可靠的协议,它确保接收方完全正确地获取发送方所发送的全部数据。 总之,TCP在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输。相比之下UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。 读者可能要问,既然有了保证可靠传输的TCP协议,为什么还要非可靠传输的UDP协议呢?主要的原因有两个。一是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此TCP传输的效率不如UDP高。二是在许多应用中并不需要保证严格的传输可靠性,比如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。 8.2 基于URL的高层次Java网络编程 8.2.1一致资源定位器URL URL(Uniform Resource Locator)是一致资源定位器的简称,它表示Internet上某一资源的地址。通过URL我们可以访问Internet上的各种网络资源,比如最常见的WWW,FTP站点。浏览器通过解析给定的URL可以在网络上查找相应的文件或其他资源。 URL是最为直观的一种网络定位方法。使用URL符合人们的语言习惯,容易记忆,所以应用十分广泛。而且在目前使用最为广泛的TCP/IP中对于URL中主机名的解析也是协议的一个标准,即所谓的域名解析服务。使用URL进行网络编程,不需要对协议本身有太多的了解,功能也比较弱,相对而言是比较简单的,所以在这里我们先介绍在Java中如何使用URL进行网络编程来引导读者入门。 8.2.2 URL的组成 protocol://resourceName 协议名(protocol)指明获取资源所使用的传输协议,如http、ftp、gopher、file等,资源名(resourceName)则应该是资源的完整地址,包括主机名、端口号、文件名或文件内部的一个引用。例如: http://www.sun.com/ 协议名://主机名 http://home.netscape.com/home/welcome.html 协议名://机器名+文件名 http://www.gamelan.com:80/Gamelan/network.html#BOTTOM 协议名://机器名+端口号+文件名+内部引用 端口号是和Socket编程相关的一个概念,初学者不必在此深究,在后面会有详细讲解。内部引用是HTML中的标记,有兴趣的读者可以参考有关HTML的书籍。 8.2.3 创建一个URL 为了表示URL, java.net中实现了类URL。我们可以通过下面的构造方法来初始化一个URL对象: (1) public URL (String spec); 通过一个表示URL地址的字符串可以构造一个URL对象。 URL urlBase=new URL("http://www. 263.net/") (2) public URL(URL context, String spec); 通过基URL和相对URL构造一个URL对象。 URL net263=new URL ("http://www.263.net/"); URL index263=new URL(net263, "index.html") (3) public URL(String protocol, String host, String file); new URL("http", "www.gamelan.com", "/pages/Gamelan.net. html"); (4) public URL(String protocol, String host, int port, String file); URL gamelan=new URL("http", "www.gamelan.com", 80, "Pages/Gamelan.network.html"); 注意:类URL的构造方法都声明抛弃非运行时例外(MalformedURLException),因此生成URL对象时,我们必须要对这一例外进行处理,通常是用try-catch语句进行捕获。格式如下: try{ URL myURL= new URL(…) }catch (MalformedURLException e){ … //exception handler code here … } 8.2.4 解析一个URL 一个URL对象生成后,其属性是不能被改变的,但是我们可以通过类URL所提供的方法来获取这些属性: public String getProtocol() 获取该URL的协议名。 public String getHost() 获取该URL的主机名。 public int getPort() 获取该URL的端口号,如果没有设置端口,返回-1。 public String getFile() 获取该URL的文件名。 public String getRef() 获取该URL在文件中的相对位置。 public String getQuery() 获取该URL的查询信息。 public String getPath() 获取该URL的路径 public String getAuthority() 获取该URL的权限信息 public String getUserInfo() 获得使用者的信息 public String getRef() 获得该URL的锚 下面的例子中,我们生成一个URL对象,并获取它的各个属性。 import java.net.*; import java.io.*; public class ParseURL{ public static void main (String [] args) throws Exception{ URL Aurl=new URL("http://java.sun.com:80/docs/books/"); URL tuto=new URL(Aurl,"tutorial.intro.html#DOWNLOADING"); System.out.println("protocol="+ tuto.getProtocol()); System.out.println("host ="+ tuto.getHost()); System.out.println("filename="+ tuto.getFile()); System.out.println("port="+ tuto.getPort()); System.out.println("ref="+tuto.getRef()); System.out.println("query="+tuto.getQuery()); System.out.println("path="+tuto.getPath()); System.out.println("UserInfo="+tuto.getUserInfo()); System.out.println("Authority="+tuto.getAuthority()); } } 执行结果为: protocol=http host =java.sun.com filename=/docs/books/tutorial.intro.html port=80 ref=DOWNLOADING query=null path=/docs/books/tutorial.intro.html UserInfo=null Authority=java.sun.com:80 8.2.5 从URL读取WWW网络资源 当我们得到一个URL对象后,就可以通过它读取指定的WWW资源。这时我们将使用URL的方法openStream(),其定义为: InputStream openStream(); 方法openSteam()与指定的URL建立连接并返回InputStream类的对象以从这一连接中读取数据。 public class URLReader { public static void main(String[] args) throws Exception { //声明抛出所有例外 URL tirc = new URL("http://www.tirc1.cs.tsinghua.edu.cn/"); //构建一URL对象 BufferedReader in = new BufferedReader(new InputStreamReader(tirc.openStream())); //使用openStream得到一输入流并由此构造一个BufferedReader对象 String inputLine; while ((inputLine = in.readLine()) != null) //从输入流不断的读数据,直到读完为止 System.out.println(inputLine); //把读入的数据打印到屏幕上 in.close(); //关闭输入流 } } 8.2.6 通过URLConnetction连接WWW 通过URL的方法openStream(),我们只能从网络上读取数据,如果我们同时还想输出数据,例如向服务器端的CGI程序发送一些数据,我们必须先与URL建立连接,然后才能对其进行读写,这时就要用到类URLConnection了。CGI是公共网关接口(Common Gateway Interface)的简称,它是用户浏览器和服务器端的应用程序进行连接的接口,有关CGI程序设计,请读者参考有关书籍。 类URLConnection也在包java.net中定义,它表示Java程序和URL在网络上的通信连接。当与一个URL建立连接时,首先要在一个URL对象上通过方法openConnection()生成对应的URLConnection对象。例如下面的程序段首先生成一个指向地址http://edu.chinaren.com/index.shtml的对象,然后用openConnection()打开该URL对象上的一个连接,返回一个URLConnection对象。如果连接过程失败,将产生IOException. Try{ URL netchinaren = new URL ("http://edu.chinaren.com/index.shtml"); URLConnectonn tc = netchinaren.openConnection(); }catch(MalformedURLException e){ //创建URL()对象失败 … }catch (IOException e){ //openConnection()失败 … } 类URLConnection提供了很多方法来设置或获取连接参数,程序设计时最常使用的是getInputStream()和getOurputStream(),其定义为: InputSteram getInputSteram(); OutputSteram getOutputStream(); 通过返回的输入/输出流我们可以与远程对象进行通信。看下面的例子: URL url =new URL ("http://www.javasoft.com/cgi-bin/backwards"); //创建一URL对象 URLConnectin con=url.openConnection(); //由URL对象获取URLConnection对象 DataInputStream dis=new DataInputStream (con.getInputSteam()); //由URLConnection获取输入流,并构造DataInputStream对象 PrintStream ps=new PrintSteam(con.getOutupSteam()); //由URLConnection获取输出流,并构造PrintStream对象 String line=dis.readLine(); //从服务器读入一行 ps.println("client…"); //向服务器写出字符串 "client…" 其中backwards为服务器端的CGI程序。实际上,类URL的方法openSteam()是通过URLConnection来实现的。它等价于 openConnection().getInputStream(); 基于URL的网络编程在底层其实还是基于下面要讲的Socket接口的。WWW,FTP等标准化的网络服务都是基于TCP协议的,所以本质上讲URL编程也是基于TCP的一种应用。 JAVA教程 第七讲 Swing用户界面设计(二)4.5.2 文件的顺序处理 类FileInputStream和FileOutputStream用来进行文件I/O处理,由它们所提供的方法可以打开本地主机上的文件,并进行顺序的读/写。例如,下列的语句段是顺序读取文件名为text的文件里的内容,并显示在控制台上面,直到文件结束为止。 FileInputStream fis; try{ fis = new FileInputStream( "text" ); System.out.print( "content of text is : "); int b; while( (b=fis.read())!=-1 ) //顺序读取文件text里的内容并赋值 给整型变量b,直到文件结束为止。 { System.out.print( (char)b ); } }catch( FileNotFoundException e ){ System.out.println( e ); }catch( IOException e ){ System.out.println( e ); } 4.5.3 随机访问文件 对于InputStream 和OutputStream 来说,它们的实例都是顺序访问流,也就是说,只能对文件进行顺序地读/写。随机访问文件则允许对文件内容进行随机读/写。在java中,类RandomAccessFile 提供了随机访问文件的方法。类RandomAccessFile的声明为: public class RandomAccessFile extends Object implements DataInput, DataOutput 接口DataInput 中定义的方法主要包括从流中读取基本类型的数据、读取一行数据、或者读取指定长度的字节数。如:readBoolean( )、readInt( )、readLine( )、readFully( ) 等。 接口DataOutput 中定义的方法主要是向流中写入基本类型的数据、或者写入一定长度的字节数组。如:writeChar( )、writeDouble( )、write( ) 等。 下面详细介绍RandomAccessFile类中的方法。 ◇ 构造方法: RandomAccessFile(String name,String mode); //name是文件名,mode //是打开方式,例如"r"表示只读,"rw"表示可读写," RandomAccessFile(File file,String mode); //file是文件对象 ◇ 文件指针的操作 long getFilePointer( ); //用于得到当前的文件指针 void seek( long pos ); //用于移动文件指针到指定的位置 int skipBytes( int n ); //使文件指针向前移动指定的n个字节 4.6 过滤流 过滤流在读/写数据的同时可以对数据进行处理,它提供了同步机制,使得某一时刻只有一个线程可以访问一个I/O流,以防止多个线程同时对一个I/O流进行操作所带来的意想不到的结果。类FilterInputStream和FilterOutputStream分别作为所有过滤输入流和输出流的父类 过滤流类层次: java.lang.Object | +----java.io.InputStream | +----java.io.FilterInputStream 为了使用一个过滤流,必须首先把过滤流连接到某个输入/出流上,通常通过在构造方法的参数中指定所要连接的输入/出流来实现。例如: FilterInputStream( InputStream in ); FilterOutputStream( OutputStream out ); 4.6.1 几种常见的过滤流 ◇ BufferedInputStream和BufferedOutputStream 缓冲流,用于提高输入/输出处理的效率。 ◇ DataInputStream 和 DataOutputStream 不仅能读/写数据流,而且能读/写各种的java语言的基本类型,如:boolean,int,float等。 ◇ LineNumberInputStream 除了提供对输入处理的支持外,LineNumberInputStream可以记录当前的行号。 ◇ PushbackInputStream 提供了一个方法可以把刚读过的字节退回到输入流中,以便重新再读一遍。 ◇ PrintStream 打印流的作用是把Java语言的内构类型以其字符表示形式送到相应的输出流。 4.7 字符流的处理 java中提供了处理以16位的Unicode码表示的字符流的类,即以Reader和Writer 为基类派生出的一系列类。 4.7.1 Reader和Writer 这两个类是抽象类,只是提供了一系列用于字符流处理的接口,不能生成这两个类的实例,只能通过使用由它们派生出来的子类对象来处理字符流。 1.Reader类是处理所有字符流输入类的父类。 ◇ 读取字符 public int read() throws IOException; //读取一个字符,返回值为读取的字符 public int read(char cbuf[]) throws IOException; /*读取一系列字符到数组cbuf[]中,返回值为实际读取的字符的数量*/ public abstract int read(char cbuf[],int off,int len) throws IOException; /*读取len个字符,从数组cbuf[]的下标off处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现*/ ◇ 标记流 public boolean markSupported(); //判断当前流是否支持做标记 public void mark(int readAheadLimit) throws IOException; //给当前流作标记,最多支持readAheadLimit个字符的回溯。 public void reset() throws IOException; //将当前流重置到做标记处 ◇ 关闭流 public abstract void close() throws IOException; 2. Writer类是处理所有字符流输出类的父类。 ◇ 向输出流写入字符 public void write(int c) throws IOException; //将整型值c的低16位写入输出流 public void write(char cbuf[]) throws IOException; //将字符数组cbuf[]写入输出流 public abstract void write(char cbuf[],int off,int len) throws IOException; //将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流 public void write(String str) throws IOException; //将字符串str中的字符写入输出流 public void write(String str,int off,int len) throws IOException; //将字符串str 中从索引off开始处的len个字符写入输出流 ◇ flush( ) 刷空输出流,并输出所有被缓存的字节。 ◇ 关闭流 public abstract void close() throws IOException; 4.7.2 InputStreamReader和OutputStreamWriter java.io包中用于处理字符流的最基本的类,用来在字节流和字符流之间作为中介。 ◇ 生成流对象 public InputStreamReader(InputStream in); /*in是字节流,而InputStreamReader是字符流,但是其来源是字节流in, 因此InputStreamReader就可以把字节流in转换成字符流处理。/* public InputStreamReader(InputStream in,String enc) throws UnsupportedEncodingException; /*enc是编码方式,就是从字节流到字符流进行转换时所采用的编码方式, 例如 ISO8859-1,UTF-8,UTF-16等等*/ public OutputStreamWriter(OutputStream out); /*out是字节流,而OutputStreamReader是字符流 */ public OutputStreamWriter(OutputStream out,String enc) throws UnsupportedEncodingException; //enc是编码方式 InputStreamReader和OutputStreamWriter的方法: ◇ 读入和写出字符 基本同Reader和Writer。 ◇ 获取当前编码方式 public String getEncoding(); ◇ 关闭流 public void close() throws IOException; 4.7.3 BufferedReader和BufferedWriter ◇ 生成流对象 public BufferedReader(Reader in); //使用缺省的缓冲区大小 public BufferedReader(Reader in, int sz); //sz为缓冲区的大小 public BufferedWriter(Writer out); public BufferedWriter(Writer out, int sz); ◇ 读入/写出字符 除了Reader和Writer中提供的基本的读写方法外,增加对整行字符的处理。 public String readLine() throws IOException; //读一行字符 public void newLine() throws IOException; //写一行字符 【例4-4】 import java.io.*; public class NumberInput{ public static void main(String args[]){ try{ InputStreamReader ir; BufferedReader in; ir=new InputStreamReader(System.in); //从键盘接收了一个字符串的输入,并创建了一个字符输入流的对象 in=new BufferedReader(ir); String s=in.readLine(); //从输入流in中读入一行,并将读取的值赋值给字符串变量s System.out.println("Input value is: "+s); int i = Integer.parseInt(s);//转换成int型 i*=2; System.out.println("Input value changed after doubled: "+i); }catch(IOException e) {System.out.println(e);} } } 运行结果 D:\>java NumberInput 123 Input value is 123 Input value changed after doubled: 246 注意:在读取字符流时,如果不是来自于本地的,比如说来自于网络上某处的与本地编码方式不同的机器,那么我们在构造输入流时就不能简单地使用本地缺省的编码方式,否则读出的字符就不正确;为了正确地读出异种机上的字符,我们应该使用下述方式构造输入流对象: ir = new InputStreamReader(is, "8859_1"); 采用ISO 8859_1编码方式,这是一种映射到ASCII码的编码方式,可以在不同平台之间正确转换字符。 4.8 对象的串行化(Serialization) 4.8.1 串行化的定义 1. 什么是串行化 对象的寿命通常随着生成该对象的程序的终止而终止。有时候,可能需要将对象的状态保存下来,在需要时再将对象恢复。我们把对象的这种能记录自己的状态以便将来再生的能力,叫做对象的持续性(persistence)。对象通过写出描述自己状态的数值来记录自己,这个过程叫对象的串行化(Serialization)。 2. 串行化的目的 串行化的目的是为java的运行环境提供一组特性,其主要任务是写出对象实例变量的数值。 4.8.2 串行化方法 在java.io包中,接口Serializable用来作为实现对象串行化的工具,只有实现了Serializable的类的对象才可以被串行化。 1. 定义一个可串行化对象 public class Student implements Serializable{ int id; //学号 String name; //姓名 int age; //年龄 String department //系别 public Student(int id,String name,int age,String department){ this.id = id; this.name = name; this.age = age; this.department = department; } } 2. 构造对象的输入/输出流 要串行化一个对象,必须与一定的对象输入/输出流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。 java.io包中,提供了ObjectInputStream和ObjectOutputStream将数据流功能扩展至可读写对象。在ObjectInputStream中用readObject()方法可以直接读取一个对象,ObjectOutputStream中用writeObject()方法可以直接将对象保存到输出流中。 Student stu=new Student(981036,"Liu Ming",18, "CSD"); FileOutputStream fo=new FileOutputStream("data.ser"); //保存对象的状态 ObjectOutputStream so=new ObjectOutputStream(fo); try{ so.writeObject(stu); so.close(); }catch(IOException e ) {System.out.println(e);} FileInputStream fi=new FileInputStream("data.ser"); ObjectInputStream si=new ObjectInputStream(fi); //恢复对象的状态 try{ stu=(Student)si.readObject(); si.close(); }catch(IOException e ) {System.out.println(e);} 在这个例子中,我们首先定义一个类Student,实现了 Serializable接口,然后通过对象输出流的writeObject()方法将Student对象保存到文件data.ser中。之后,通过对象输入流的readObject()方法从文件data.ser中读出保存下来的Student对象。 4.8.3 串行化的注意事项 1.串行化能保存的元素 只能保存对象的非静态成员变量,不能保存任何的成员方法和静态的成员变量,而且串行化保存的只是变量的值,对于变量的任何修饰符,都不能保存。 2.transient关键字 对于某些类型的对象,其状态是瞬时的,这样的对象是无法保存其状态的,例如一个Thread对象,或一个FileInputStream对象,对于这些字段,我们必须用transient关键字标明 3. 定制串行化 缺省的串行化机制,对象串行化首先写入类数据和类字段的信息,然后按照名称的上升排列顺序写入其数值。如果想自己明确地控制这些数值的写入顺序和写入种类,必须定义自己的读取数据流的方式。就是在类的定义中重写writeObject()和readObject()方法。 例如可在4.8.2的例子中,加入重写的writeObject()和readObject()方法,对Student 类定制其串行化。 private void writeObject(ObjectOutputStream out)throws IOException { out.writeInt(id); out.writeInt(age); out.writeUTF(name); out.writeUTF(department); } private void readObject(ObjectInputStream in)throws IOException { id=in.readInt(); age=in.readInt(); name=in.readUTF(); department=in.readUTF(); } 4.9 其它常用的流 4.9.1 管道流 管道用来把一个程序、线程或代码块的输出连接到另一个程序、线程或代码块的输入 。 管道输入流作为一个通信管道的接收端,管道输出流作为发送端。在使用管道之前,管道输出流和管道输入流必须进行连接。下面有两种连接的方法: 1. 构造方法中连接 PipedInputStream(PipedOutputStream src); PipedOutputStream(PipedInputStream snk); 2. connect()方法进行连接 类PipedInputStream中定义为: void connect(PipedOutputStream src); 类PipedOutputStream中定义为: void connect(PipedInputStream snk); 4.9.2 内存的读/写 1. ByteArrayInputStream和ByteArrayOutputStream ByteArrayInputStream //从字节数组中读取以字节为单位的数据 ByteArrayOutputStream //向字节数组中写入以字节为单位的数据 2. StringBufferInputStream和StringBufferOutputStream StringBufferInputStream //从字符串缓冲区StringBuffer中读取以字符为单位的数据 StringBufferOutputStream //向字符串缓冲区StringBuffer中写入以字符为单位的数据 4.9.3 顺序输入流 SequenceInputStream 把几个输入流顺序连接起来。顺序输入流提供了把若干不同的流统一为同一个流的功能,使得程序变得更加简洁。 【本讲小结】 例外处理是java语言中一个独特之处,主要使用捕获例外和声明抛弃例外两种方法来处理程序中可能出现例外的语句块,其中捕获例外的方法是一种积极地处理例外的方法,而声明抛弃例外是一种消极的处理例外的方法。 Java中的输入/输出处理是通过使用流技术,用统一的接口表示而实现的。输入/输出流中,最常见的是对文件的处理。Java语言中提供专门处理文件和目录的类,例如:java.io.File,java.io.FileInputStream,java.io.FileOutputStream,java.io.RandomAccessFile和接口java.io.FilenameFilter。输入/输出流根据处理的内容,分为字符流和字节流两种,其中字节流是以byte为基本处理单位的流;而字符流是以16位的Unicode码为处理单位的流。 JAVA教程 第七讲 Swing用户界面设计(一)7.1 Swing简介 7.1.1 简介 第五讲中我们学习了AWT,AWT是Swing的基础。Swing的产生主要原因就是AWT不能满足图形化用户界面发展的需要。 AWT设计的初衷是支持开发小应用程序的简单用户界面。例如AWT缺少剪贴板、打印支持、键盘导航等特性,而且原来的AWT甚至不包括弹出式菜单或滚动窗格等基本元素。 此外AWT还存在着严重的缺陷,人们使AWT适应基于继承的、具有很大伸缩性的事件模型,基于同位体的体系结构也成为其致命的弱点。 随着发展的需要,Swing出现了,Swing组件几乎都是轻量组件,与重量组件相比,没有本地的对等组件,不像重量组件要在它们自己的本地不透明窗体中绘制,轻量组件在它们的重量组件的窗口中绘制。 这一讲我们讲一下基本的Swing组件使用方法和使用Swing组件创建用户界面的初步方法。 Swing是由100%纯Java实现的,Swing组件是用Java实现的轻量级( light-weight)组件,没有本地代码,不依赖操作系统的支持,这是它与AWT组件的最大区别。由于AWT组件通过与具体平台相关的对等类(Peer)实现,因此Swing比AWT组件具有更强的实用性。Swing在不同的平台上表现一致,并且有能力提供本地窗口系统不支持的其它特性。 Swing采用了一种MVC的设计范式,即"模型-视图-控制"(Model-View-Controller),其中模型用来保存内容,视图用来显示内容,控制器用来控制用户输入。 Swing外观感觉采用可插入的外观感觉(Pluggable Look and Feel,PL&F) 在AWT组件中,由于控制组件外观的对等类与具体平台相关,使得AWT组件总是只有与本机相关的外观。Swing使得程序在一个平台上运行时能够有不同的外观。用户可以选择自己习惯的外观。以下三幅图是在同一个操作系统下得到不同的外观。 7.1.2 Swing的类层次结构 在javax.swing包中,定义了两种类型的组件:顶层容器(JFrame,JApplet,JDialog和JWindow)和轻量级组件。Swing组件都是AWT的Container类的直接子类和间接子类。 java.awt.Component -java.awt.Container -java.awt.Window -java.awt.Frame-javax.swing.JFrame -javax.Dialog-javax.swing.JDialog -javax.swing.JWindow -java.awt.Applet-javax.swing.JApplet -javax.swing.Box -javax.swing.Jcomponet (在jdk1.3中,第一、第二和pending包没有了,增加了plaf.multi包,主要功能:给缺省的L&F加上附加的L&F,例如一个MultiButtonUI实例可以同时处理MotifButtonUI和AudioButtonUI.) swing包是Swing提供的最大包,它包含将近100个类和25个接口,几乎所有的Swing组件都在swing包中,只有JtableHeader和 JtextComponent是例外,它们分别在swing.table和swing.text中。 swing.border包中定义了事件和事件监听器类,与AWT的event包类似。它们都包括事件类和监听器接口。 swing.pending包包含了没有完全实现的Swing组件。 swing.table包中主要包括了表格组建(JTable)的支持类。 swing.tree同样是JTree的支持类。 swing.text、swing.text.html、swing.text.html.parser和swing.text.rtf都是用于显示和编辑文档的包。 7.1.3 Swing组件的多样化 Swing是AWT的扩展,它提供了许多新的图形界面组件。Swing组件以"J"开头,除了有与AWT类似的按钮(JButton)、标签(JLabel)、复选框(JCheckBox)、菜单(JMenu)等基本组件外,还增加了一个丰富的高层组件集合,如表格(JTable)、树(JTree)。 7.1.4 MVC(Model-View-Control)体系结构 Swing胜过AWT的主要优势在于MVC体系结构的普遍使用。在一个MVC用户界面中,存三个通讯对象:模型、视图和控件。模型是指定的逻辑表示法,视图是模型的可视化表示法,而控件则指定了如何处理用户输入。当模型发生改变时,它会通知所有依赖它的视图,视图使用控件指定其相应机制。 为了简化组件的设计工作,在Swing组件中视图和控件两部分合为一体。每个组件有一个相关的分离模型和它使用的界面(包括视图和控件)。比如,按钮JButton有一个存储其状态的分离模型ButtonModel对象。组件的模型是自动设置的,例如一般都使用JButton 而不是使用ButtonModel 对象。另外,通过Model类的子类或通过实现适当的接口,可以为组件建立自己的模型。把数据模型与组件联系起来用setModel( )方法。 MVC是现有的编程语言中制作图形用户界面的一种通用的思想,其思路是把数据的内容本身和显示方式分离开,这样就使得数据的显示更加灵活多样。比如,某年级各个班级的学生人数是数据,则显示方式是多种多样的,可以采用柱状图显示,也可以采用饼图显示,也可以采用直接的数据输出。因此在设计的时候,就考虑把数据和显示方式分开,对于实现多种多样的显示是非常有帮助的。 7.1.5 可存取性支持 所有Swing组件都实现了Accessible接口,提供对可存取性的支持,使得辅助功能如屏幕阅读器能够十分方便的从Swing组件中得到信息。 7.1.6 支持键盘操作 在Swing组件中,使用JComponent类的registerKeyboardAction()方法,能使用户通过键盘操作来替代鼠标驱动GUI上Swing组件的相应动作。有些类还为键盘操作提供了更便利的方法。 其实这就相当于热键,使得用户可以只用键盘进行操作。 7.1.7 设置边框 对Swing组件可以设置一个和多个边框。Swing中提供了各式各样的边框供用户选用,也能建立组合边框或自己设计边框。一种空白边框可以增大组件,协助布局管理器对容器中的组件进行合理的布局。 7.1.8 使用图标(Icon) 与AWT的部件不同,许多Swing组件如按钮、标签,除了使用文字外,还可以使用图标修饰自己。 例7.1: import javax.swing.*; //引入Swing包名 //import com.sun.java.swing.*; //使用JDK 1.2 Beta 4版和所有Swing 1.1 Beta 3 //之前的版本,引入Swing包名用此方法。 import java.awt.*; import java.awt.event.*; public class SwingApplication { private static String labelPrefix = "Number of button clicks: "; private int numClicks = 0; //计数器,计算点击次数 public Component createComponents() { final JLabel label = new JLabel(labelPrefix + "0 "); JButton button = new JButton("I'm a Swing button!"); button.setMnemonic(KeyEvent.VK_I); //设置按钮的热键为'I' button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { numClicks++; label.setText(labelPrefix + numClicks); //显示按钮被点击的次数 } }); label.setLabelFor(button); /* 在顶层容器及其内容之间放置空间的常用办法是把内容添加到Jpanel上,而Jpanel本身没有边框的。*/ JPanel pane = new JPanel(); pane.setBorder(BorderFactory.createEmptyBorder( 30, //top 30, //left 10, //bottom 30) //right ); pane.setLayout(new GridLayout(0, 1)); //单列多行 pane.add(button); pane.add(label); return pane; } public static void main(String[] args) { try { UIManager.setLookAndFeel( UIManager.getCrossPlatformLookAndFeelClassName()); //设置窗口风格 } catch (Exception e) { } //创建顶层容器并添加内容. JFrame frame = new JFrame("SwingApplication"); SwingApplication app = new SwingApplication(); Component contents = app.createComponents(); frame.getContentPane().add(contents, BorderLayout.CENTER); //窗口设置结束,开始显示 frame.addWindowListener(new WindowAdapter() { //匿名类用于注册监听器 public void windowClosing(WindowEvent e) { System.exit(0); } }); frame.pack(); frame.setVisible(true); } } 7.1.9 Swing程序结构简介 Swing的程序设计一般可按照下列流程进行: 1. 引入Swing包 2. 选择"外观和感觉" 3. 设置顶层容器 4. 设置按钮和标签 5. 向容器中添加组件 6. 在组件周围添加边界 7. 进行事件处理 例子7.1说明了Swing中程序设计的结构以及最基本的组件Button和Label的用法。在程序中,我们建立一个Swing风格的窗口,并在其中添加一个按钮,程序中保存一个计数器以计算按钮被点击的次数,并在每一次点击之后用一个Label显示。在这个程序中我们可以看到Swing组件的使用与AWT组件的使用基本方法一致,使用的事件处理机制也完全相同。这些在前面的AWT中已经讲过,不再赘述。 7.2 Swing组件和容器 在Swing中不但用轻量级的组件替代了AWT中的重量级的组件,而且Swing的替代组件中都包含有一些其他的特性。例如,Swing的按钮和标签可显示图标和文本,而AWT的按钮和标签只能显示文本。Swing中的大多数组件都是AWT组件名前面加了一个"J"。 7.2.1 组件的分类 Jcomponent是一个抽象类,用于定义所有子类组件的一般方法,其类层次结构如下所示: java.lang.Object | +--java.awt.Component | +--java.awt.Container | +--javax.swing.JComponent 并不是所有的Swing组件都继承于JComponent类,JComponent类继承于Container类,所以凡是此类的组件都可作为容器使用。 组件从功能上分可分为: 1) 顶层容器:JFrame,JApplet,JDialog,JWindow共4个 2) 中间容器:JPanel,JScrollPane,JSplitPane,JToolBar 3) 特殊容器:在GUI上起特殊作用的中间层,如JInternalFrame,JLayeredPane,JRootPane. 4) 基本控件:实现人际交互的组件,如Jbutton, JComboBox, JList, JMenu, JSlider, JtextField。 5) 不可编辑信息的显示:向用户显示不可编辑信息的组件,例如JLabel, JProgressBar, ToolTip。 6) 可编辑信息的显示:向用户显示能被编辑的格式化信息的组件,如JColorChooser, JFileChoose, JFileChooser, Jtable, JtextArea。 JComponent类的特殊功能又分为: 1) 边框设置:使用setBorder()方法可以设置组件外围的边框,使用一个EmptyBorder对象能在组件周围留出空白。 2) 双缓冲区:使用双缓冲技术能改进频繁变化的组件的显示效果。与AWT组件不同,JComponent组件默认双缓冲区,不必自己重写代码。如果想关闭双缓冲区,可以在组件上施加setDoubleBuffered(false)方法。 3) 提示信息:使用setTooltipText()方法,为组件设置对用户有帮助的提示信息。 4) 键盘导航:使用registerKeyboardAction( ) 方法,能使用户用键盘代替鼠标来驱动组件。JComponent类的子类AbstractButton还提供了便利的方法--用setMnemonic( )方法指明一个字符,通过这个字符和一个当前L&F的特殊修饰共同激活按钮动作。 5) 可插入L&F:每个Jcomponent对象有一个相应的ComponentUI对象,为它完成所有的绘画、事件处理、决定尺寸大小等工作。 ComponentUI对象依赖当前使用的L&F,用UIManager.setLookAndFeel( )方法可以设置需要的L&F. 6) 支持布局:通过设置组件最大、最小、推荐尺寸的方法和设置X、Y对齐参数值的方法能指定布局管理器的约束条件,为布局提供支持。 7.2.2 使用Swing的基本规则 与AWT组件不同,Swing组件不能直接添加到顶层容器中,它必须添加到一个与Swing顶层容器相关联的内容面板(content pane)上。内容面板是顶层容器包含的一个普通容器,它是一个轻量级组件。基本规则如下: (1)把Swing组件放入一个顶层Swing容器的内容面板上 (2)避免使用非Swing的重量级组件。 对JFrame添加组件有两种方式: 1) 用getContentPane( )方法获得JFrame的内容面板,再对其加入组件:frame.getContentPane().add(childComponent) 2) 建立一个Jpanel或 JDesktopPane之类的中间容器,把组件添加到容器中,用setContentPane()方法把该容器置为JFrame的内容面板: Jpanel contentPane=new Jpanel( ); ……//把其它组件添加到Jpanel中; frame.setContentPane(contentPane); //把contentPane对象设置成为frame的内容面板 7.2.3 各种容器面板和组件 根面板由一个玻璃面板(glassPane)、一个内容面板(contentPane)和一个可选择的菜单条(JMenuBar)组成,而内容面板和可选择的菜单条放在同一分层。玻璃面板是完全透明的,缺省值为不可见,为接收鼠标事件和在所有组件上绘图提供方便。 根面板提供的方法: Container getContentPane(); //获得内容面板 setContentPane(Container); //设置内容面 JMenuBar getMenuBar( ); //活动菜单条 setMenuBar(JMenuBar); //设置菜单条 JLayeredPane getLayeredPane(); //获得分层面板 setLayeredPane(JLayeredPane); //设置分层面板 Component getGlassPane(); //获得玻璃面板 setGlassPane(Component); //设置玻璃面板 7.2.3.2 分层面板(JLayeredPane) Swing提供两种分层面板:JlayeredPane和JDesktopPane。 JDesktopPane是JLayeredPane的子类,专门为容纳内部框架(JInternalFrame)而设置。 向一个分层面板种添加组件,需要说明将其加入哪一层,指明组件在该层中的位置:add(Component c, Integer Layer, int position)。 7.2.3.3 面板(JPanel) 面板(JPanel)是一个轻量容器组件,用法与Panel相同,用于容纳界面元素,以便在布局管理器的设置下可容纳更多的组件,实现容器的嵌套。Jpanel, JscrollPane, JsplitPane, JinteralFrame都属于常用的中间容器,是轻量组件。Jpanel的缺省布局管理器是FlowLayout。 java.lang.Object | +--java.awt.Component | +--java.awt.Container | +--javax.swing.JComponent | +--javax.swing.JPanel 7.2.3.4 滚动窗口(JScrollPane) JscrollPane是带滚动条的面板,主要是通过移动JViewport(视口)来实现的。JViewport是一种特殊的对象,用于查看基层组件,滚动条实际就是沿着组件移动视口,同时描绘出它在下面"看到"的内容。 7.2.3.5 分隔板(JSplitPane) java.lang.Object | +--java.awt.Component | +--java.awt.Container | +--javax.swing.JComponent | +--javax.swing.JSplitPane JSplitPane提供可拆分窗口,支持水平拆分和垂直拆分并带有滑动条。常用方法有: addImpl(Component comp,Object constraints,int index)//增加指定的组件 setTopComponent(Component comp) //设置顶部的组件 setDividerSize(int newSize) //设置拆分的大小 setUI(SplitPaneUI ui) //设置外观和感觉 7.2.3.6 选项板(JTabbedPane)] JTabbedPane提供一组可供用户选择的带有标签或图标的开关键。常用方法: add(String title,Component component) //增加一个带特定标签的组件 addChangeListener(ChangeListener l) //选项板注册一个变化监听器 7.2.3.7 工具栏(JToolBar) JtoolBar是用于显示常用工具控件的容器。用户可以拖拽出一个独立的可显示工具控件的窗口。 常用方法有: JToolBar(String name) //构造方法 getComponentIndex(Component c) //返回一个组件的序号 getComponentAtIndex(int i) //得到一个指定序号的组件 7.2.3.8 内部框架(JInternalFrame) 内部框架JInternalFrame就如同一个窗口在另一个窗口内部,其特点如下: 1) 必须把内部框架添加到一个容器中(通常为JDesktopPane),否则不显示; 2) 不必调用show()或setVisible()方法,内部框架随所在的容器一起显示; 3) 必须用setSize()或pack()或setBounds方法设置框架尺寸,否则尺寸为零,框架不能显示; 4) 可以用setLocation()或setBounds( ) 方法设置内部框架在容器中的位置,缺省值为0,0,即容器的左上角; 5) 象顶层JFrame一样,对内部框架添加组件也要加在它的内容面板上; 6) 在内部框架中建立对话框,不能使用JDialog作为顶层窗口,必须用JOptionPane或JInternalFrame; 7) 内部框架不能监听窗口事件,可以通过监听与窗口事件类似的内部框架(JInternalFrameEvent)处理内部框架窗口的操作。 JFrame frame=new JFrame("InternalFrameDemo"); //实例化窗口 JDesktopPane desktop=new JDesktopPane(); //实例化容器JDesktopPane MyInternalFrame myframe=new MyInternalFrame(); //实例化内部窗口 desktop.add(myframe); //把内部窗口添加到容器中 myframe.setSelected(true); //内部面板是可选择的 frame.setContentPane(desktop); //把desktop设为frame的内容面板 7.2.3.9 按钮(JButton) 按钮是一个常用组件,按钮可以带标签或图象。 java.lang.Object | +--java.awt.Component | +--java.awt.Container | +--javax.swing.JComponent | +--javax.swing.AbstractButton | +--javax.swing.JButton 常用的构造方法有: JButton(Icon icon) //按钮上显示图标 JButton(String text) //按钮上显示字符 JButton(String text, Icon icon) //按钮上既显示图标又显示字符 例7.2 public class ButtonDemo extends Jpanel implements ActionListener{ JButton b1,b2,b3; public ButtonDemo() { super(); ImageIcon leftButtonIcon=new ImageIcon("images/right.gif); //显示在左按钮上的图标 ImageIcon middleButtonIcon=new ImageIcon("images/middle.gif); //显示在中间按钮上的图标 ImageIcon middleButtonIcon=new ImageIcon("images/left.gif); //显示在右按钮上的图标 b1=new JButton("Disable middle button",leftButtonIcon); //按钮b1上同时显示文字和图标 b1.setVerticalTextPosition(AbstractButton.CENTER); //按钮b1上的文字在垂直方向上是居中对齐 b1.setHorizontalTextPosition(AbstractButton.LEFT); //按钮b1上的文字在水平居方向上是居左对齐 b1.setMnemonic('d'); //设置按钮b1的替代的键盘按键是'd' b1.setActionCommand("diaable"); …… } } 7.2.3.10 复选框(JCheckBox) 复选框提供简单的"on/off"开关,旁边显示文本标签。 7.2.3.11 单选框(JRadioButton) JAVA教程 第六讲 Java的线程和Java Applet(二) 6.3 Java Applet 前面的章节我们阐述了Application的应用,这一讲我们将介绍java的另一类应用java Applet,即java小应用程序。 在Java问世的头几年里,之所以如此热门,其根本原因还是在于Java具有"让Internet动起来"的能力。具体地说,就是Java能创建一种特殊类型的程序(通常称作"小应用程序"或者Applet),具备Java能力的Web浏览器可从网上下载这种程序,然后运行。 目前,几乎所有浏览器均支持动态HTML(DHTML)和脚本编制(支持XML的浏览器也有很多),所以比起Java刚刚问世的时候,浏览器能够做的事情要多得多。但尽管如此,由于小应用程序是用一种全功能的程序设计语言编制的,所以同HTML、XML和脚本语言的任何一种可能的组合相比,它仍然具有应用前景! 6.3.1 Applet 介绍(1) Applet就是使用Java语言编写的一段代码,它可以在浏览器环境中运行。它与Application的区别主要在于其执行方式的不同。application 是从其中的main() 方法开始运行的,而Applet 是在浏览器中运行的,必须创建一个HTML 文件,通过编写HTML 语言代码告诉浏览器载入何种Applet 以及如何运行。 例6.7 HelloWorld.java 源程序: import java.awt.Graphics; //引入图形类Graphics import java.applet.Applet; //引入Applet类 public class HelloWorld extends Applet { String hw_text ; public void init () { //init()方法是Applet首先执行的方法 hw_text = "Hello World"; } public void paint(Graphics g) { g.drawString (hw_text , 25, 25) ; //在坐标为(25,25)的地方显示字符串hw_text } } Applet程序编写完后,首先要用java编译器编译成为字节码文件,然后编写相应的HTML文件才能够正常执行,例如为运行上面的Applet程序所编写的HTML文件HelloWorld.html如下: 6.3.1 Applet 介绍(2) 2.Applet的 安全性 "沙箱"机制:Java虚拟机为Applet提供能够良好运行的沙箱,一旦它们试图离开沙箱则会被禁止。 由于小应用程序是通过网络传递的,这就不可避免地使人想到会发生安全问题。例如有人编写恶意程序通过小应用程序读取用户密码并散播到网络上,这将会是一件非常可怕的事情。所以,必须对小应用程序进行限制。 浏览器禁止Applet执行下列操作: (1)在运行时调用其它程序。 (2)文件读写操作。 (3)装载动态连接库和调用任何本地方法。 (4)试图打开一个socket进行网络通信,但是所连接的主机并不是提供Applet的主机。 ◇ Applet的类层次: ◇ Applet的生命周期 小应用程序的生命周期相对于Application而言较为复杂。在其生命周期中涉及到Applet类的四个方法(也被JApplet类继承):init()、start()、stop()和destroy()。下面首先用图来表示一个小应用程序的生命周期,然后再简要描述这四个方法。 看图 Applet的生命周期中有四个状态:初始态、运行态、停止态和消亡态。当程序执行完init()方法以后,Applet程序就进入了初始态;然后马上执行start()方法,Applet程序进入运行态;当Applet程序所在的浏览器图标化或者是转入其它页面时,该Applet程序马上执行stop()方法,Applet程序进入停止态;在停止态中,如果浏览器又重新装载该Applet程序所在的页面,或者是浏览器从图标中复原,则Applet程序马上调用start()方法,进入运行态;当然,在停止态时,如果浏览器关闭,则Applet程序调用destroy()方法,进入消亡态。 ◇ Applet的主要方法: 1. init( ) 创建Applet时执行,只执行一次 当小应用程序第一次被支持Java的浏览器加载时,便执行该方法。在小应用程序的生命周期中,只执行一次该方法,因此可以在其中进行一些只执行一次的初始化操作,如处理由浏览器传递进来的参数、添加用户接口组件、加载图像和声音文件等。 小应用程序有默认的构造方法,但它习惯于在init()方法中执行所有的初始化,而不是在默认的构造方法内。 2.start( ) 多次执行,当浏览器从图标恢复成窗口,或者是返回该主页时执行。 系统在调用完init()方法之后,将自动调用start()方法。而且每当浏览器从图标恢复为窗口时,或者用户离开包含该小应用程序的主页后又再返回时,系统都会再执行一遍start()方法。start()方法在小应用程序的生命周期中被调用多次,以启动小应用程序的执行,这一点与init()方法不同。该方法是小应用程序的主体,在其中可以执行一些需要重复执行的任务或者重新激活一个线程,例如开始动画或播放声音等。 3.stop( ) 多次执行,当浏览器变成图标时,或者是离开主页时执行,主要功能是停止一些耗用系统资源的工作,。 与start()相反,当用户离开小应用程序所在页面或浏览器变成图标时,会自动调用stop()方法。因此,该方法在生命周期中也被多次调用。这样使得可以在用户并不注意小应用程序的时候,停止一些耗用系统资源的工作(如中断一个线程),以免影响系统的运行速度,且并不需要去人为地去调用该方法。如果你的小应用程序中不包含动画、声音等程序,通常也不必重载该方法。 4.destroy( ) 用来释放资源,在stop( )之后执行 浏览器正常关闭时,Java自动调用这个方法。destroy()方法用于回收任何一个与系统无关的内存资源。当然,如果这个小应用程序仍然处于活动状态,Java会在调用destroy()之前,调用stop()方法。 下面的例子使用了小应用程序生命周期中的这几个方法。 例6.8 import java.awt.Graphics; import java.applet.Applet; public class HWloop extends Applet { AudioClip sound; //声音片断对象 public void init( ){ sound=getAudioClip("audio/hello.au"); //获得声音片断 } public void paint(Graphics g) { g.drawString("hello Audio",25,25); //显示字符串 } public void start( ) { sound.loop( ); //声音片断开始播放 } public void stop( ) { sound.stop( ); //声音片断停止 } } 在本例子中,Applet开始执行后就不停的播放声音,如果浏览器图标化或者是转到其它页面,则声音停止播放;如果浏览器恢复正常或者是从其它页面跳回,则程序继续播放声音。 由于Applet程序要嵌入到HTML文件中才能够正常执行,下面介绍与Applet程序有关的HTML文件标记。 注意: 1)不支持Java的浏览器会把之间的普通HTML文档显示出来;支持Java的浏览器,则把其中的普通HTML文档忽略。 2)AppletViewer仅支持标记,其它标记不会被显示出来。 经过精心设计,可以使java程序同时是Applet与Application,如下例所示: 例6.9 import java.awt.*; import java.awt.event.*; public class AppletApp extends Applet { public void main(String args[]) { Frame frame=new Frame("Application"); //构造一个Frame AppletApp app=new AppletApp(); frame.add("Center",app); frame.setSize(200,200); //改变Frame的尺寸 frame.validate(); frame.setVisible(true); //使Frame可见 frame.addWindwoListener(new WindowControl(app)); //给Frame加入监听器 app.init(); //初始化Applet app.start(); //运行该Applet } public void paint(Graphics g) { //重画方法 g.drawString("hello world",25,25); } public void destroy(){ System.exit(0); } } class WindowControl extends WindowAdapter { //监听器类 Applet c; public WindowControl(Applet c) { //构造函数 this.c=c; } public void windowClosing(WindowEvent e) { //关闭窗口时调用的方法 c.destroy( ); } } 6.3.2 Applet的AWT绘制 看图 Applet程序中所采用的AWT的绘图机制主要涉及三个方法:paint()方法、update()方法和repaint()方法,update()方法和paint()方法都有一个Graphics类参数。Graphics是画图的关键,它可以支持两种绘图:一种是基本的绘图,如:画线、矩形、圆等;另一种是画图象,主要用于动画制作。 要进行绘图,首先要找到一个Graphics类的对象。update()方法和paint()方法所传递的参数都是Graphics类的对象,因此主要是通过重载它们来进行绘图,这是在动画程序中经常使用的方法。我们还可以通过getGraphics()方法得到一个Graphics类的对象,这个对象和update()方法和paint()方法中所传递的对象一样,都是该成员所对应的Graphics类的对象。得到了Graphics类的对象,就可使用各种绘图方法。 Graphics中提供的图形绘制方法有: paint( ) //进行绘图的具体操作,必须有程序员重写 update( ) //用于更新图形,先清除背景、前景,再调用paint() repaint( ) /*用于重绘图形,在组件外形发生变化,即大小改变或位置移动时,repaint( )方法立即被系统自动调用,而实际上repaint()方法是自动调用update()方法*/ 下面的方法支持基本的绘图和画图像: void drawLine( ) void drawArc( ) void drawPolygon( ) void drawRect( ) void drawRoundRect( ) void fill3DRect( ) void fillOval( ) java.awt.Graphics类 输出文字: void drawBytes( ) void drawChars( ) void drawString( ) Applet 的AWT绘制举例如下: 例6.10 import java.awt.*; import java.awt.event.*; import java.applet.*; public class ArcTest extends Applet implements WindowListener { ArcControls controls; pulic void init(){ //Applet的入口方法 setLayout(new BorderLayout()); ArcCanvas c=new ArcCanvas(); Add("Center",c); add("South",controls=new ArcControls(C)); } public void start(){ controls.setEnabled(true); //激活controls } public void stop(){ controls.setEnabled(false); } public void windowActivated(WindowEvent e){ } //重写WindowListener的方法 public void windowClosed(WindowEvent e){ } //重写WindowListener的方法 public void windowClosing(WindowEvent e){ //重写WindowListener的方法 System.exit(0); } public void windowDeactivated(WindowEvent e){} //重写WindowListener的方法 public void windowDeiconified(WindowEvent e){} //重写WindowListener的方法 public void windowIconified(WindowEvent e){ } //重写WindowListener的方法 public void windowOpend(WindowEvent e){ } //重写WindowListener的方法 public static void main(String args[]) { Frame f=new Frame("ArcTest"); //构造Frame ArcTest arcTest=new ArcTest(); //构造arcTest atcTest.init(); arcTest.start(); f.add("Center",arcTest); f.setSize(300,300); f.show(); f.addWindowListener(arcTest); } } class ArcCanvas extends Canvas{ //类ArcCanvas int startAngle=0; int endAngle=45; boolean filled=false; Font font; public void paint(Graphics g){ //paint方法,该方法的作用是在Canvas上画图 Rectangle r=getBounds(); int hlines=r.height/10; int vlines=r.width/10; g.setColor(Color.pink); for(int i=1;i<=hlines;i++) { g.drawLine(0,i*10,r.width,i*10); } for(int i=1;i<=vlines;i++) { g.drawLine(i*10,0,i*10,r.height); } g.setColor(Color.red); if(filled) { g.fillArc(0,0,r.width-1,r.height-1,startAngle,endAngle); } else { g.drawArc(0,0,r.width-1,r.height-1,startAngle, endAngle); } g.setColor(Color.black); g.setFont(font); g.drawLine(0,r.height/2,r.width,r.height/2); g.drawLine(r.width/2,0,r.width/2,r.height); g.drawLine(0,,0,r.width,r.height); g.drawLine(r.width,0,0,r.height); int sx=10; int sy=r.height-28; g.drawString("S="+startAngle,sx,sy); g.drawString("E="+ednAngle,sx,sy+14); } public void redraw(boolean filled,int start,int end){ //重画方法 this.filled=filled; this.startAngle=start; this.endAngle=end; repaint(); //通过调用repaint()方法,从而最终调用paint方法完成重画 } } class ArcControls extends Panel implements ActionListener { //ArcControls类 TextFiled s; TextFiled e; ArcCanvas canvas; public ArcControls(ArcCanvas canvas) { Button b=null; this.canvas=canvas; add(s=new TextField("0",4)); add(e=new TextField("45",4)); b=new Button("Fill"); b.addActionListener(this); add(b); b=new Button("Draw"); b.addActionListener(this); add(b); } public void actionPerformed(ActionEvent ev) { //实现接口ActionListener的方法 String label=ev.getActionCommand(); canvas.redraw(label.equals("Fill"), Integer.parseInt(s.getText(),trim()), Integer.parserInt(e.getText().trim()); } } 6.3.3 Applet和浏览器间的通信 在Applet类中提供了许多方法,使之可以与浏览器进行通信。下面对这些方法进行简要的介绍: 一个Web页可包含一个以上的小应用程序。一个Web页中的多个小应用程序可以直接通过java.applet包中提供的方法进行通信。 getDocumentBase( ) //返回当前网页所在的URL getCodeBase( ) //返回当前applet所在的URL getImage(URL base,String target) //返回网址URL中名为target的图像 getAudioClip(URL base,String target) //返回网址URL中名为target的声音对象 getParameter(String target ) //提取HTML文件中名为target的参数的值 同页Applet间的通信 在java.applet.Applet类中提供了如下方法得到当前运行页的环境上下文AppletContext对象。 public AppletContext getAppletContext(); 通过AppletContext对象,可以得到当前小应用程序运行环境的信息。AppletContext是一个接口,其中定义了一些方法可以得到当前页的其它小应用程序,进而实现同页小应用程序之间的通信。 (1)得到当前运行页的环境上下文AppletContext对象 public AppletContext getAppletContext(); (2)取得名为name的Applet对象 public abstract Applet getApplet(String name); (3)得到当前页中所有Applet对象 public abstract Enumeration getApplets(); 例6.11 import java.applet.*; import java.awt.*; import java.awt.event.*; import java.util.Enumeration; public class GetApplets extends Applet implemements ActionListener { private TextArea textArea; //声明一个TextArea private String newline; public void init() { Button b=new Button("Click to call getApplets()"); b.addActionListener(this); setLayout(new BorderLayout()); add("North",b); textArea=new TextAred(5,40); textArea.setEditable(false); add("Center",textArea); newline=System.getProperty("line,separator"); //取得系统当前的换行符 } public void actionPerformed(ActionEvent event) { /*Button b点击后的事件处理函数*/ printApplets(); } public String getAppletInfo() { return "GetApplets by Dong.li"; } public void printApplets()} Enumeration e=getAppletContext().getApplets(); /*得到当前网页所有的Applet对象*/ textArea.append("Results of getApplets():" + newline); while(e.hasMoreElements()) { Applet applet=(Applet) e.nextElement(); String info=((Applet)applet).getAppletInfo(); /*逐个取得当前网页Applet对象的信息*/ if (info!=null) { textArea.append("-"+info+newline); /*在textArea中输出网页所有Applet的信息*/ } else { textArea.append("-"+applet.getClass().getName()+newline) ; } } textArea.append("__________"+newline+newline); } } 【本讲小结】 本讲介绍了Java的线程和Java Applet的一些基本知识和简单应用,通过对线程简介,阐明了线程与进程的区别,通过描述线程的概念模型的基本原理以及线程体的构造方法和应用实例,讲解了线程的基本特性和线程的不同状态的转换关系和调用方法,明确了线程的使用方法,然后,我们又进一步讲述了线程的几种调度策略,在不同的调度策略下优先级的作用。以及如何进行基本的线程的控制,线程的重点和难点在于多线程的互斥与同步,首先我们必须明白互斥锁的概念和作用,如何使用互斥锁来控制和处理多线程的同步问题。 本讲的后半部分我们对Java Applet做了介绍和一些基本的应用的讲解,例如Applet的创建,生命周期和Applet的主要方法以及Applet的AWT绘制,最后简单介绍了Applet和浏览器间的通信的方法。 JAVA教程 第六讲 Java的线程和Java Applet6.1 线程简介 随着计算机的飞速发展,个人计算机上的操作系统也纷纷采用多任务和分时设计,将早期只有大型计算机才具有的系统特性带到了个人计算机系统中。一般可以在同一时间内执行多个程序的操作系统都有进程的概念。一个进程就是一个执行中的程序,而每一个进程都有自己独立的一块内存空间、一组系统资源。在进程概念中,每一个进程的内部数据和状态都是完全独立的。Java程序通过流控制来执行程序流,程序中单个顺序的流控制称为线程,多线程则指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务。多线程意味着一个程序的多行语句可以看上去几乎在同一时间内同时运行。 线程与进程相似,是一段完成某个特定功能的代码,是程序中单个顺序的流控制;但与进程不同的是,同类的多个线程是共享一块内存空间和一组系统资源,而线程本身的数据通常只有微处理器的寄存器数据,以及一个供程序执行时使用的堆栈。所以系统在产生一个线程,或者在各个线程之间切换时,负担要比进程小的多,正因如此,线程被称为轻负荷进程(light-weight process)。一个进程中可以包含多个线程。 一个线程是一个程序内部的顺序控制流。 1. 进程:每个进程都有独立的代码和数据空间(进程上下文) ,进程切换的开销大。 2. 线程:轻量的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。 3. 多进程:在操作系统中,能同时运行多个任务程序。 4. 多线程:在同一应用程序中,有多个顺序流同时执行。 6.1.1 线程的概念模型 Java内在支持多线程,它的所有类都是在多线程下定义的,Java利用多线程使整个系统成为异步系统。Java中的线程由三部分组成,如图6.1所示。 1. 虚拟的CPU,封装在java.lang.Thread类中。 2. CPU所执行的代码,传递给Thread类。 3. CPU所处理的数据,传递给Thread类。 图6.1线程 6. 1. 2 线程体(1) Java的线程是通过java.lang.Thread类来实现的。当我们生成一个Thread类的对象之后,一个新的线程就产生了。 此线程实例表示Java解释器中的真正的线程,通过它可以启动线程、终止线程、线程挂起等,每个线程都是通过类Thread在Java的软件包Java.lang中定义,它的构造方法为: public Thread (ThreadGroup group,Runnable target,String name); 其中,group 指明该线程所属的线程组;target实际执行线程体的目标对象,它必须实现接口Runnable; name为线程名。Java中的每个线程都有自己的名称,Java提供了不同Thread类构造器,允许给线程指定名称。如果name为null时,则Java自动提供唯一的名称。 当上述构造方法的某个参数为null时,我们可得到下面的几个构造方法: public Thread (); public Thread (Runnable target); public Thread (Runnable target,String name); public Thread (String name); public Thread (ThreadGroup group,Runnable target); public Thread (ThreadGroup group,String name); 一个类声明实现Runnable接口就可以充当线程体,在接口Runnable中只定义了一个方法 run(): public void run(); 任何实现接口Runnable的对象都可以作为一个线程的目标对象,类Thread本身也实现了接口Runnable,因此我们可以通过两种方法实现线程体。 (一)定义一个线程类,它继承线程类Thread并重写其中的方法 run(),这时在初始化这个类的实例时,目标target可为null,表示由这个实例对来执行线程体。由于Java只支持单重继承,用这种方法定义的类不能再继承其它父类。 (二)提供一个实现接口Runnable的类作为一个线程的目标对象,在初始化一个Thread类或者Thread子类的线程对象时,把目标对象传递给这个线程实例,由该目标对象提供线程体 run()。这时,实现接口Runnable的类仍然可以继承其它父类。 每个线程都是通过某个特定Thread对象的方法run( )来完成其操作的,方法run( )称为线程体。图6.2表示了java线程的不同状态以及状态之间转换所调用的方法。 图6.2 线程的状态 1. 创建状态(new Thread) 执行下列语句时,线程就处于创建状态: Thread myThread = new MyThreadClass( ); 当一个线程处于创建状态时,它仅仅是一个空的线程对象,系统不为它分配资源。 2. 可运行状态( Runnable ) Thread myThread = new MyThreadClass( ); myThread.start( ); 当一个线程处于可运行状态时,系统为这个线程分配了它需的系统资源,安排其运行并调用线程运行方法,这样就使得该线程处于可运行( Runnable )状态。需要注意的是这一状态并不是运行中状态(Running ),因为线程也许实际上并未真正运行。由于很多计算机都是单处理器的,所以要在同一时刻运行所有的处于可运行状态的线程是不可能的,Java的运行系统必须实现调度来保证这些线程共享处理器。 3. 不可运行状态(Not Runnable) 进入不可运行状态的原因有如下几条: 1) 调用了sleep()方法; 2) 调用了suspend()方法; 3) 为等候一个条件变量,线程调用wait()方法; 4) 输入输出流中发生线程阻塞; 不可运行状态也称为阻塞状态(Blocked)。因为某种原因(输入/输出、等待消息或其它阻塞情况),系统不能执行线程的状态。这时即使处理器空闲,也不能执行该线程。 4. 死亡状态(Dead) 线程的终止一般可通过两种方法实现:自然撤消(线程执行完)或是被停止(调用stop()方法)。目前不推荐通过调用stop()来终止线程的执行,而是让线程执行完。 6. 1. 2 线程体(2) ◇线程体的构造 任何实现接口Runnable的对象都可以作为一个线程的目标对象,上面已讲过构造线程体有两种方法,下面通过实例来说明如何构造线程体的。 例6.1 通过继承类Thread构造线程体 class SimpleThread extends Thread { public SimpleThread(String str) { super(str); //调用其父类的构造方法 } public void run() { //重写run方法 for (int i = 0; i < 10; i++) { System.out.println(i + " " + getName()); //打印次数和线程的名字 try { sleep((int)(Math.random() * 1000)); //线程睡眠,把控制权交出去 } catch (InterruptedException e) {} } System.out.println("DONE! " + getName()); //线程执行结束 } } public class TwoThreadsTest { public static void main (String args[]) { new SimpleThread("First").start(); //第一个线程的名字为First new SimpleThread("Second").start(); //第二个线程的名字为Second } } 运行结果: 0 First 0 Second 1 Second 1 First 2 First 2 Second 3 Second 3 First 4 First 4 Second 5 First 5 Second 6 Second 6 First 7 First 7 Second 8 Second 9 Second 8 First DONE! Second 9 First DONE! First 仔细分析一下运行结果,会发现两个线程是交错运行的,感觉就象是两个线程在同时运行。但是实际上一台计算机通常就只有一个CPU,在某个时刻只能是只有一个线程在运行,而java语言在设计时就充分考虑到线程的并发调度执行。对于程序员来说,在编程时要注意给每个线程执行的时间和机会,主要是通过让线程睡眠的办法(调用sleep()方法)来让当前线程暂停执行,然后由其它线程来争夺执行的机会。如果上面的程序中没有用到sleep()方法,则就是第一个线程先执行完毕,然后第二个线程再执行完毕。所以用活sleep()方法是学习线程的一个关键。 例6.2 通过接口构造线程体 public class Clock extends java.applet.Applet implements Runnable {//实现接口 Thread clockThread; public void start() { //该方法是Applet的方法,不是线程的方法 if (clockThread == null) { clockThread = new Thread(this, "Clock"); /*线程体是Clock对象本身,线程名字为"Clock"*/ clockThread.start(); //启动线程 } } public void run() { //run()方法中是线程执行的内容 while (clockThread != null) { repaint(); //刷新显示画面 try { clockThread.sleep(1000); //睡眠1秒,即每隔1秒执行一次 } catch (InterruptedException e){} } } public void paint(Graphics g) { Date now = new Date(); //获得当前的时间对象 g.drawString(now.getHours() + ":" + now.getMinutes()+ ":" +now.getSeconds(), 5, 10);//显示当前时间 } public void stop() { //该方法是Applet的方法,不是线程的方法 clockThread.stop(); clockThread = null; } } 上面这个例子是通过每隔1秒种就执行线程的刷新画面功能,显示当前的时间;看起来的效果就是一个时钟,每隔1秒就变化一次。由于采用的是实现接口Runnable的方式,所以该类Clock还继承了Applet, Clock就可以Applet的方式运行。 构造线程体的两种方法的比较: 1. 使用Runnable接口 1) 可以将CPU,代码和数据分开,形成清晰的模型; 2) 还可以从其他类继承; 3) 保持程序风格的一致性。 2. 直接继承Thread类 1) 不能再从其他类继承; 2) 编写简单,可以直接操纵线程,无需使用Thread.currentThread()。 6.1.3 线程的调度 Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪些线程来执行。 线程调度器按线程的优先级高低选择高优先级线程(进入运行中状态)执行,同时线程调度是抢先式调度,即如果在当前线程执行过程中,一个更高优先级的线程进入可运行状态,则这个线程立即被调度执行。 抢先式调度又分为:时间片方式和独占方式。在时间片方式下,当前活动线程执行完当前时间片后,如果有其他处于就绪状态的相同优先级的线程,系统会将执行权交给其他就绪态的同优先级线程;当前活动线程转入等待执行队列,等待下一个时间片的调度。 在独占方式下,当前活动线程一旦获得执行权,将一直执行下去,直到执行完毕或由于某种原因主动放弃CPU,或者是有一高优先级的线程处于就绪状态。 下面几种情况下,当前线程会放弃CPU: 1. 线程调用了yield() 或sleep() 方法主动放弃; 2. 由于当前线程进行I/O 访问,外存读写,等待用户输入等操作,导致线程阻塞;或者是为等候一个条件变量,以及线程调用wait()方法; 3. 抢先式系统下,由高优先级的线程参与调度;时间片方式下,当前时间片用完,由同优先级的线程参与调度。 线程的优先级 线程的优先级用数字来表示,范围从1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。一个线程的缺省优先级是5,即Thread.NORM_PRIORITY。下述方法可以对优先级进行操作: int getPriority(); //得到线程的优先级 void setPriority(int newPriority); //当线程被创建后,可通过此方法改变线程的优先级 例6.3中生成三个不同线程,其中一个线程在最低优先级下运行,而另两个线程在最高优先级下运行。 例6.3 class ThreadTest{ public static void main( String args [] ) { Thread t1 = new MyThread("T1"); t1.setPriority( Thread.MIN_PRIORITY ); //设置优先级为最小 t1.start( ); Thread t2 = new MyThread("T2"); t2.setPriority( Thread.MAX_PRIORITY ); //设置优先级为最大 t2.start( ); Thread t3 = new MyThread("T3"); t3.setPriority( Thread.MAX_PRIORITY ); //设置优先级为最大 t3.start( ); } } class MyThread extends Thread { String message; MyThread ( String message ) { this.message = message; } public void run() { for ( int i=0; i<3; i++ ) System.out.println( message+" "+getPriority() ); //获得线程的优先级 } } 运行结果: T2 10 T2 10 T2 10 T3 10 T3 10 T3 10 T1 1 T1 1 T1 1 注意:并不是在所有系统中运行Java程序时都采用时间片策略调度线程,所以一个线程在空闲时应该主动放弃CPU,以使其他同优先级和低优先级的线程得到执行。 6.1.4基本的线程控制 1.终止线程 线程终止后,其生命周期结束了,即进入死亡态,终止后的线程不能再被调度执行,以下几种情况,线程进入终止状态: 1) 线程执行完其run()方法后,会自然终止。 2) 通过调用线程的实例方法stop()来终止线程。 2. 测试线程状态 可以通过Thread 中的isAlive() 方法来获取线程是否处于活动状态;线程由start() 方法启动后,直到其被终止之间的任何时刻,都处于'Alive'状态。 3. 线程的暂停和恢复 有几种方法可以暂停一个线程的执行,在适当的时候再恢复其执行。 1) sleep() 方法 当前线程睡眠(停止执行)若干毫秒,线程由运行中状态进入不可运行状态,停止执行时间到后线程进入可运行状态。 2) suspend()和resume()方法 线程的暂停和恢复,通过调用线程的suspend()方法使线程暂时由可运行态切换到不可运行态,若此线程想再回到可运行态,必须由其他线程调用resume()方法来实现。 注:从JDK1.2开始就不再使用suspend()和resume()。 3) join() 当前线程等待调用该方法的线程结束后, 再恢复执行. TimerThread tt=new TimerThread(100); tt.start(); … public void timeout(){ tt.join();// 当前线程等待线程tt 执行完后再继续往下执行 … } 6.2多线程的互斥与同步 临界资源问题 前面所提到的线程都是独立的,而且异步执行,也就是说每个线程都包含了运行时所需要的数据或方法,而不需要外部的资源或方法,也不必关心其它线程的状态或行为。但是经常有一些同时运行的线程需要共享数据,此时就需考虑其他线程的状态和行为,否则就不能保证程序的运行结果的正确性。例6.4说明了此问题。 例6.4 class stack{ int idx=0; //堆栈指针的初始值为0 char[ ] data = new char[6]; //堆栈有6个字符的空间 public void push(char c){ //压栈操作 data[idx] = c; //数据入栈 idx + +; //指针向上移动一位 } public char pop(){ //出栈操作 idx - -; //指针向下移动一位 return data[idx]; //数据出栈 } } 两个线程A和B在同时使用Stack的同一个实例对象,A正在往堆栈里push一个数据,B则要从堆栈中pop一个数据。如果由于线程A和B在对Stack对象的操作上的不完整性,会导致操作的失败,具体过程如下所示: 1) 操作之前 data = | p | q | | | | | idx=2 2) A执行push中的第一个语句,将r推入堆栈; data = | p | q | r | | | | idx=2 3) A还未执行idx++语句,A的执行被B中断,B执行pop方法,返回q: data = | p | q | r | | | | idx=1 4〕A继续执行push的第二个语句: data = | p | q | r | | , | | idx=2 最后的结果相当于r没有入栈。产生这种问题的原因在于对共享数据访问的操作的不完整性。 6.2.1 互斥锁 为解决操作的不完整性问题,在Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。 public void push(char c){ synchronized(this){ //this表示Stack的当前对象 data[idx]=c; idx++; } } public char pop(){ synchronized(this){ //this表示Stack的当前对象 idx--; return data[idx]; } } synchronized 除了象上面讲的放在对象前面限制一段代码的执行外,还可以放在方法声明中,表示整个方法为同步方法。 public synchronized void push(char c){ … } 如果synchronized用在类声明中,则表明该类中的所有方法都是synchronized的。 6.2.2多线程的同步 本节将讨论如何控制互相交互的线程之间的运行进度,即多线程之间的同步问题,下面我们将通过多线程同步的模型: 生产者-消费者问题来说明怎样实现多线程的同步。 我们把系统中使用某类资源的线程称为消费者,产生或释放同类资源的线程称为生产者。 在下面的Java的应用程序中,生产者线程向文件中写数据,消费者从文件中读数据,这样,在这个程序中同时运行的两个线程共享同一个文件资源。通过这个例子我们来了解怎样使它们同步。 例6.5 class SyncStack{ //同步堆栈类 private int index = 0; //堆栈指针初始值为0 private char []buffer = new char[6]; //堆栈有6个字符的空间 public synchronized void push(char c){ //加上互斥锁 while(index = = buffer.length){ //堆栈已满,不能压栈 try{ this.wait(); //等待,直到有数据出栈 }catch(InterruptedException e){} } this.notify(); //通知其它线程把数据出栈 buffer[index] = c; //数据入栈 index++; //指针向上移动 } public synchronized char pop(){ //加上互斥锁 while(index ==0){ //堆栈无数据,不能出栈 try{ this.wait(); //等待其它线程把数据入栈 }catch(InterruptedException e){} } this.notify(); //通知其它线程入栈 index- -; //指针向下移动 return buffer[index]; //数据出栈 } } class Producer implements Runnable{ //生产者类 SyncStack theStack; //生产者类生成的字母都保存到同步堆栈中 public Producer(SyncStack s){ theStack = s; } public void run(){ char c; for(int i=0; i<20; i++){ c =(char)(Math.random()*26+'A'); //随机产生20个字符 theStack.push(c); //把字符入栈 System.out.println("Produced: "+c); //打印字符 try{ Thread.sleep((int)(Math.random()*1000)); /*每产生一个字符线程就睡眠*/ }catch(InterruptedException e){} } } } class Consumer implements Runnable{ //消费者类 SyncStack theStack; //消费者类获得的字符都来自同步堆栈 public Consumer(SyncStack s){ theStack = s; } public void run(){ char c; for(int i=0;i<20;i++){ c = theStack.pop(); //从堆栈中读取字符 System.out.println("Consumed: "+c); //打印字符 try{ Thread.sleep((int)(Math.random()*1000)); /*每读取一个字符线程就睡眠*/ }catch(InterruptedException e){} } } } public class SyncTest{ public static void main(String args[]){ SyncStack stack = new SyncStack(); //下面的消费者类对象和生产者类对象所操作的是同一个同步堆栈对象 Runnable source=new Producer(stack); Runnable sink = new Consumer(stack); Thread t1 = new Thread(source); //线程实例化 Thread t2 = new Thread(sink); //线程实例化 t1.start(); //线程启动 t2.start(); //线程启动 } } 类Producer是生产者模型,其中的 run()方法中定义了生产者线程所做的操作,循环调用push()方法,将生产的20个字母送入堆栈中,每次执行完push操作后,调用sleep()方法睡眠一段随机时间,以给其他线程执行的机会。类Consumer是消费者模型,循环调用pop()方法,从堆栈中取出一个数据,一共取20次,每次执行完pop操作后,调用sleep()方法睡眠一段随机时间,以给其他线程执行的机会。 程序执行结果 Produced:V Consumed:V Produced:E Consumed:E Produced:P Produced:L ... Consumed:L Consumed:P 在上述的例子中,通过运用wait()和notify()方法来实现线程的同步,在同步中还会用到notifyAll()方法,一般来说,每个共享对象的互斥锁存在两个队列,一个是锁等待队列,另一个是锁申请队列,锁申请队列中的第一个线程可以对该共享对象进行操作,而锁等待队列中的线程在某些情况下将移入到锁申请队列。下面比较一下wait()、notify()和notifyAll()方法: (1) wait,nofity,notifyAll必须在已经持有锁的情况下执行,所以它们只能出现在synchronized作用的范围内,也就是出现在用 synchronized修饰的方法或类中。 (2) wait的作用:释放已持有的锁,进入等待队列. (3) notify的作用:唤醒wait队列中的第一个线程并把它移入锁申请队列. (4) notifyAll的作用:唤醒wait队列中的所有的线程并把它们移入锁申请队列. 注意: 1) suspend()和resume() 在JDK1.2中不再使用suspend()和resume(),其相应功能由wait()和notify()来实现。 2) stop() 在JDK1.2中不再使用stop(),而是通过标志位来使程序正常执行完毕。例6.6就是一个典型的例子。 例6.6 public class Xyz implements Runnable { private boolean timeToQuit=false; //标志位初始值为假 public void run() { while(!timeToQuit) {//只要标志位为假,线程继续运行 … } } public void stopRunning() { timeToQuit=true;} //标志位设为真,表示程序正常结束 } public class ControlThread { private Runnable r=new Xyz(); private Thread t=new Thread(r); public void startThread() { t.start(); } public void stopThread() { r.stopRunning(); } //通过调用stopRunning方法来终止线程运行 } JAVA教程 第五讲 AWT图形用户界面设计(二)5.2.1 事件类 与AWT有关的所有事件类都由java.awt.AWTEvent类派生,它也是EventObject类的子类。AWT事件共有10类,可以归为两大类:低级事件和高级事件。 java.util.EventObject类是所有事件对象的基础父类,所有事件都是由它派生出来的。AWT的相关事件继承于java.awt.AWTEvent类,这些AWT事件分为两大类:低级事件和高级事件,低级事件是指基于组件和容器的事件,当一个组件上发生事件,如:鼠标的进入,点击,拖放等,或组件的窗口开关等,触发了组件事件。高级事件是基于语义的事件,它可以不和特定的动作相关联,而依赖于触发此事件的类,如在TextField中按Enter键会触发ActionEvent事件,滑动滚动条会触发AdjustmentEvent事件,或是选中项目列表的某一条就会触发ItemEvent事件。 ◇ 低级事件 ComponentEvent( 组件事件:组件尺寸的变化,移动) ContainerEvent( 容器事件:组件增加,移动) WindowEvent( 窗口事件:关闭窗口,窗口闭合,图标化) FocusEvent( 焦点事件:焦点的获得和丢失) KeyEvent( 键盘事件:键按下、释放) MouseEvent( 鼠标事件:鼠标单击,移动) ◇ 高级事件(语义事件) ActionEvent(动作事件:按钮按下,TextField中按Enter键) AdjustmentEvent(调节事件:在滚动条上移动滑块以调节数值) ItemEvent(项目事件:选择项目,不选择"项目改变") TextEvent(文本事件,文本对象改变) 5.2.2 事件监听器 每类事件都有对应的事件监听器,监听器是接口,根据动作来定义方法。 例如,与键盘事件KeyEvent相对应的接口是: public interface KeyListener extends EventListener { public void keyPressed(KeyEvent ev); public void keyReleased(KeyEvent ev); public void keyTyped(KeyEvent ev); } 注意到在本接口中有三个方法,那么java运行时系统何时调用哪个方法?其实根据这三个方法的方法名就能够知道应该是什么时候调用哪个方法执行了。当键盘刚按下去时,将调用keyPressed( )方法执行,当键盘抬起来时,将调用keyReleased( )方法执行,当键盘敲击一次时,将调用keyTyped( )方法执行。 又例如窗口事件接口: public interface WindowListener extends EventListener{ public void windowClosing(WindowEvent e); //把退出窗口的语句写在本方法中 public void windowOpened(WindowEvent e); //窗口打开时调用 public void windowIconified(WindowEvent e); //窗口图标化时调用 public void windowDeiconified(WindowEvent e); //窗口非图标化时调用 public void windowClosed(WindowEvent e); //窗口关闭时调用 public void windowActivated(WindowEvent e); //窗口激活时调用 public void windowDeactivated(WindowEvent e); //窗口非激活时调用 } AWT的组件类中提供注册和注销监听器的方法: ◇ 注册监听器: public void add (listener); ◇ 注销监听器: public void remove (listener); 例如Button类:(查API) public class Button extends Component { …… public synchronized void addActionListener(ActionListener l); public synchronized void removeActionListener(ActionListener l); ……} 5.2.3 AWT事件及其相应的监听器接口(1) 5.2.3 AWT事件及其相应的监听器接口(2) 例5.10说明事件处理模型的应用。 例5.10 import java.awt.*; import java.awt.event.*; public class ThreeListener implements MouseMotionListener,MouseListener,WindowListener { //实现了三个接口 private Frame f; private TextField tf; public static void main(String args[]) { ThreeListener two = new ThreeListener(); two.go(); } public void go() { f = new Frame("Three listeners example"); f.add(new Label("Click and drag the mouse"),"North"); tf = new TextField(30); f.add(tf,"South"); //使用缺省的布局管理器 f.addMouseMotionListener(this); //注册监听器MouseMotionListener f.addMouseListener(this); //注册监听器MouseListener f.addWindowListener(this); //注册监听器WindowListener f.setSize(300,200); f.setVisible(true); } public void mouseDragged (MouseEvent e) { //实现mouseDragged方法 String s = "Mouse dragging : X="+e.getX()+"Y = "+e.getY(); tf.setText(s); } public void mouseMoved(MouseEvent e){} //对其不感兴趣的方法可以方法体为空 public void mouseClicked(MouseEvent e){} public void mouseEntered(MouseEvent e){ String s = "The mouse entered"; tf.setText(s); } public void mouseExited(MouseEvent e){ String s = "The mouse has left the building"; tf.setText(s); } public void mousePressed(MouseEvent e){} public void mouseReleased(MouseEvent e){ } public void windowClosing(WindowEvent e) { //为了使窗口能正常关闭,程序正常退出,需要实现windowClosing方法 System.exit(1); } public void windowOpened(WindowEvent e) {} //对其不感兴趣的方法可以方法体为空 public void windowIconified(WindowEvent e) {} public void windowDeiconified(WindowEvent e) {} public void windowClosed(WindowEvent e) {} public void windowActivated(WindowEvent e) { } public void windowDeactivated(WindowEvent e) {} } 上例中有如下几个特点: 1.可以声明多个接口,接口之间用逗号隔开。 ……implements MouseMotionListener, MouseListener, WindowListener; 2.可以由同一个对象监听一个事件源上发生的多种事件: f.addMouseMotionListener(this); f.addMouseListener(this); f.addWindowListener(this); 则对象f 上发生的多个事件都将被同一个监听器接收和处理。 3.事件处理者和事件源处在同一个类中。本例中事件源是Frame f,事件处理者是类ThreeListener,其中事件源Frame f是类ThreeListener的成员变量。 4.可以通过事件对象获得详细资料,比如本例中就通过事件对象获得了鼠标发生时的坐标值。 public void mouseDragged(MouseEvent e) { String s="Mouse dragging :X="+e.getX()+"Y="+e.getY(); tf.setText(s); } Java语言类的层次非常分明,因而只支持单继承,为了实现多重继承的能力,Java用接口来实现,一个类可以实现多个接口,这种机制比多重继承具有更简单、灵活、更强的功能。在AWT中就经常用到声明和实现多个接口。记住无论实现了几个接口,接口中已定义的方法必须一一实现,如果对某事件不感兴趣,可以不具体实现其方法,而用空的方法体来代替。但却必须所有方法都要写上。 5.2.4 事件适配器 Java语言为一些Listener接口提供了适配器(Adapter)类。可以通过继承事件所对应的Adapter类,重写需要方法,无关方法不用实现。事件适配器为我们提供了一种简单的实现监听器的手段, 可以缩短程序代码。但是,由于java的单一继承机制,当需要多种监听器或此类已有父类时,就无法采用事件适配器了。 1.事件适配器--EventAdapter 下例中采用了鼠标适配器: import java.awt.*; import java.awt.event.*; public class MouseClickHandler extends MouseAdaper{ public void mouseClicked(MouseEvent e) //只实现需要的方法 { ……} } java.awt.event包中定义的事件适配器类包括以下几个: 1.ComponentAdapter( 组件适配器) 2.ContainerAdapter( 容器适配器) 3.FocusAdapter( 焦点适配器) 4.KeyAdapter( 键盘适配器) 5.MouseAdapter( 鼠标适配器) 6.MouseMotionAdapter( 鼠标运动适配器) 7.WindowAdapter( 窗口适配器) 2. 用内部类实现事件处理 内部类(inner class)是被定义于另一个类中的类,使用内部类的主要原因是由于: ◇ 一个内部类的对象可访问外部类的成员方法和变量,包括私有的成员。 ◇ 实现事件监听器时,采用内部类、匿名类编程非常容易实现其功能。 ◇ 编写事件驱动程序,内部类很方便。 因此内部类所能够应用的地方往往是在AWT的事件处理机制中。 例5.11 import java.awt.* ; import java.awt.event.*; public class InnerClass{ private Frame f; private TextField tf; public InnerClass(){ f=new Frame("Inner classes example"); tf=new TextField(30); } public voidi launchFrame(){ Label label=new Label("Click and drag the mouse"); f.add(label,BorderLayout.NORTH); f.add(tf,BorderLayout.SOUTH); f.addMouseMotionListener(new MyMouseMotionListener());/*参数为内部类对象*/ f.setSize(300,200); f.setVisible(true); } class MyMouseMotionListener extends MouseMotionAdapter{ /*内部类开始*/ public void mouseDragged(MouseEvent e) { String s="Mouse dragging: x="+e.getX()+"Y="+e.getY(); tf.setText(s); } } ; public static void main(String args[]) { InnerClass obj=new InnerClass(); obj.launchFrame(); } }//内部类结束 } 3.匿名类(Anonymous Class) 当一个内部类的类声名只是在创建此类对象时用了一次,而且要产生的新类需继承于一个已有的父类或实现一个接口,才能考虑用匿名类,由于匿名类本身无名,因此它也就不存在构造方法,它需要显示地调用一个无参的父类的构造方法,并且重写父类的方法。所谓的匿名就是该类连名字都没有,只是显示地调用一个无参的父类的构造方法。 例5.12 import java.awt.* ; import java.awt.event.*; public class AnonymousClass{ private Frame f; private TextField tf; public AnonymousClass(){ f=new Frame("Inner classes example"); tf=new TextField(30); } public void launchFrame(){ Label label=new Label("Click and drag the mouse"); f.add(label,BorderLayout.NORTH); f.add(tf,BorderLayout.SOUTH); f.addMouseMotionListener(new MouseMotionAdapter(){ //匿名类开始 public void mouseDragged(MouseEvent e){ String s="Mouse dragging: x="+e.getX()+"Y="+e.getY(); tf.setText(s); } } ); //匿名类结束 f.setSize(300,200); f.setVisible(true); } public static void main(String args[]) { AnonymousClass obj=new AnonymousClass(); obj.launchFrame(); } } 其实大家仔细分析一下,例5.11和5.12实现的都是完全一样的功能,只不过采取的方式不同。5.11中的事件处理类是一个内部类,而5.12的事件处理类是匿名类,可以说从类的关系来说是越来越不清楚,但是程序也越来越简练。熟悉这两种方式也十分有助于大家编写图形界面的程序。 5.3 AWT组件库(1) 本节从应用的角度进一步介绍AWT的一些组件,目的使大家加深对AWT的理解,掌握如何用各种组件构造图形化用户界面,学会控制组件的颜色和字体。下面是一些常用的组件的介绍: 1. 按钮(Button) 按钮是最常用的一个组件,其构造方法是:Button b = new Button("Quit"); 当按钮被点击后,会产生ActionEvent事件,需ActionListener接口进行监听和处理事件。 ActionEvent的对象调用getActionCommand()方法可以得到按钮的标识名,缺省按钮名为label。 用setActionCommand()可以为按钮设置组件标识符。 2.复选框 (Checkbox) 复选框提供简单的"on/off"开关,旁边显示文本标签。 构造方法如下: setLayout(new GridLayout(3,1)); add(new Checkbox("one",null,true)); add(new Checkbox("two")); add(new Checkbox("three")); 复选框用ItemListener 来监听ItemEvent事件,当复选框状态改变时用getStateChange()获取当前状态。使用getItem()获得被修改复选框的字符串对象。 例5.13 class Handler implements ItemListener { public void itemStateChanged(ItemEvent ev){ String state = "deselected"; if (ev.getStateChange() = = ItemEvent.SELECTED){ state = "selected" } System.out.println(ev.getItem()+" "+state); } } 3.复选框组(CheckboxGroup) 使用复选框组,可以实现单选框的功能。方法如下: setLayout(new GridLayout(3, 1)); CheckboxGroup cbg = new CheckboxGroup(); add(new Checkbox("one", cbg, true)); add(new Checkbox("two", cbg, false)); add(new Checkbox("three", cbg, false)); 5.3 AWT组件库(2) 4. 下拉式菜单(Choice) 下拉式菜单每次只能选择其中的一项,它能够节省显示空间,适用于大量选项。 Choice Colorchooser=new Choice(); Colorchooser.add("Green"); Colorchooser.add("Red"); Colorchooser.add("Blue"); Choice 用ItemListener接口来进行监听 5. Canvas 一个应用程序必须继承Canvas类才能获得有用的功能,比如创建一个自定义组件。如果想在画布上完成一些图形处理,则Canvas类中的paint()方法必须被重写。 Canvas组件监听各种鼠标,键盘事件。当在Canvas组件中输入字符时,必须先调用requestFocus()方法。 例5.14 import java.awt.*; import java.awt.event.*; import java.util.*; public class MyCanvas implements KeyListener, MouseListener { Canvas c; //声明一个画布对象 String s =""; public static void main(String args[]) { Frame f=new Frame("Canvas"); MyCanvas mc=new MyCanvas(); mc.c=new Canvas(); f.add("Center",mc.c); f.setSize(150,150); mc.c.addMouseListener(mc); //注册监听器 mc.c.addKeyListener(mc); //注册监听器 f.setVisible(true); } public void mouseClicked(MouseEvent ev){ System.out.println("MouseClicked"); c.requestFocus();//获得焦点,表示该窗口将接收用户的键盘和鼠标输入 } public void keyTyped(KeyEvent ev) { System.out.println("KeyTyped"); s+=ev.getKeyChar(); //获取每个输入的字符,依次添加到字符串s中 c.getGraphics().drawString(s,0,20); //显示字符串s } public void keyPressed(KeyEvent ev) { System.out.println("KeyPressed"); } public void keyReleased(KeyEvent ev) { System.out.println("KeyReleased"); } public void mousePressed(MouseEvent ev) {System.out.println("MousePressed"); } public void mouseReleased(MouseEvent ev) {System.out.println("MouseReleased"); } public void mouseEntered(MouseEvent ev) {System.out.println("MouseEntered"); } public void mouseExited(MouseEvent ev) {System.out.println("MouseExited"); } } 6. 单行文本输入区(TextField) 只能显示一行,当回车键被按下时,会发生ActionEvent事件,可以通过ActionListener中的actionPerformed()方法对事件进行相应处理。可以使用setEditable(boolean)方法设置为只读属性。 单行文本输入区构造方法如下: TextField tf1,tf2,tf3,tf4: tf1=new TextField(); tf2=new TextField("",20); //显示区域为20列 tf3=new TextField("Hello!"); //按文本区域大小显示 tf4=new TextField("Hello!",30); //初始文本为Hello!, 显示区域为30列 5.3 AWT组件库(3) 7. 文本输入区(TextArea) TextArea可以显示多行多列的文本。使用setEditable(boolean)方法,可以将其设置为只读的。在TextArea中可以显示水平或垂直的滚动条。 要判断文本是否输入完毕,可以在TextArea旁边设置一个按钮,通过按钮点击产生的ActionEvent对输入的文本进行处理。 8. 列表(List) 列表中提供了多个文本选项,列表支持滚动条,可以浏览多项 List lst=new List(4,false); //两个参数分别表示显示的行数、是否允许多选 lst.add("Venus"); lst.add("Earth"); lst.add("JavaSoft"); lst.add("Mars"); cnt.add(lst); 9. 框架(Frame) Frame是顶级窗口,可以显示标题,重置大小。当Frame被关闭,将产生WindowEvent事件,Frame无法直接监听键盘输入事件。 10. 对话框(Dialog) 它是Window类的子类。对话框和一般窗口的区别在于它依赖于其它窗口。对话框分为非模式(non-modal)和模式(modal)两种。 11. 文件对话框(Filedialog) 当用户想打开或存储文件时,使用文件对话框进行操作。主要代码如下: FileDialog d=new FileDialog(ParentFr,"FileDialog"); d.setVisible(true); String filename=d.getFile(); 12. 菜单(Menu) 无法直接将菜单添加到容器的某一位置,也无法使用布局管理器对其加以控制。菜单只能被添加?quot;菜单容器"(MenuBar)中。 13. MenuBar 只能被添加到Frame对象中,作为整个菜单树的根基。 Frame fr = new Frame("MenuBar"); MenuBar mb = new MenuBar(); fr.setMenuBar(mb); fr.setSize(150,100); fr.setVisible(true); 14. Menu 下拉菜单。它可以被添加到MenuBar中或其它Menu中。 Frame fr = new Frame("MenuBar"); MenuBar mb = new MenuBar(); fr.setMenuBar(mb); Menu m1 = new Menu("File"); Menu m2 = new Menu("Edit"); Menu m3 = new Menu("Help"); mb.add(m1); mb.add(m2); mb.setHelpMenu(m3); fr.setSize(200,200); fr.setVisible(true); 15. MenuItem MenuItem是菜单树中的"叶子节点"。MenuItem通常被添加到一个Menu中。对于MenuItem对象可以添加ActionListener,使其能够完成相应的操作。 Menu m1 = new Menu("File"); MenuItem mi1 = new MenuItem("Save"); MenuItem mi2 = new MenuItem("Load"); MenuItem mi3 = new MenuItem("Quit"); m1.add(mi1); m1.add(mi2); m1.addSeparator(); m1.add(mi3); MenuBar和Menu都没有必要注册监听器,只需要对MenuItem添加监听器ActionListener,完成相应操作。 16. 组件与监听器的对应关系 JAVA教程 第五讲 AWT图形用户界面设计(一)5.1 用AWT生成图形化用户界面
抽象窗口工具包AWT (Abstract Window Toolkit) 是 API为Java 程序提供的建立图形用户界面GUI (Graphics User Interface)工具集,AWT可用于Java的applet和applications中。它支持图形用户界面编程的功能包括: 用户界面组件;事件处理模型;图形和图像工具,包括形状、颜色和字体类;布局管理器,可以进行灵活的窗口布局而与特定窗口的尺寸和屏幕分辨率无关;数据传送类,可以通过本地平台的剪贴板来进行剪切和粘贴。 5.1.1 java.awt包 java.awt包中提供了GUI设计所使用的类和接口,可从图5.1中看到主要类之间的关系。 java.awt包提供了基本的java程序的GUI设计工具。主要包括下述三个概念: 组件--Component 容器--Container 布局管理器--LayoutManager 5.1.2 组件和容器 Java的图形用户界面的最基本组成部分是组件(Component),组件是一个可以以图形化的方式显示在屏幕上并能与用户进行交互的对象,例如一个按钮,一个标签等。组件不能独立地显示出来,必须将组件放在一定的容器中才可以显示出来。 类java.awt.Component是许多组件类的父类,Component类中封装了组件通用的方法和属性,如图形的组件对象、大小、显示位置、前景色和背景色、边界、可见性等,因此许多组件类也就继承了Component类的成员方法和成员变量,相应的成员方法包括: getComponentAt(int x, int y) getFont() getForeground() getName() getSize() paint(Graphics g) repaint() update() setVisible(boolean b) setSize(Dimension d) setName(String name)等 容器(Container)也是一个类,实际上是Component的子类,因此容器本身也是一个组件,具有组件的所有性质,但是它的主要功能是容纳其它组件和容器。 布局管理器(LayoutManager):每个容器都有一个布局管理器,当容器需要对某个组件进行定位或判断其大小尺寸时,就会调用其对应的布局管理器。 为了使我们生成的图形用户界面具有良好的平台无关性,Java语言中,提供了布局管理器这个工具来管理组件在容器中的布局,而不使用直接设置组件位置和大小的方式。 在程序中安排组件的位置和大小时,应该注意以下两点: 1.容器中的布局管理器负责各个组件的大小和位置,因此用户无法在这种情况下设置组件的这些属性。如果试图使用Java 语言提供的setLocation(),setSize(),setBounds() 等方法,则都会被布局管理器覆盖。 2.如果用户确实需要亲自设置组件大小或位置,则应取消该容器的布局管理器,方法为: setLayout(null); 5.1.3 常用容器 容器java.awt.Container是Component的子类,一个容器可以容纳多个组件,并使它们成为一个整体。容器可以简化图形化界面的设计,以整体结构来布置界面。所有的容器都可以通过add()方法向容器中添加组件。 有三种类型的容器:Window、Panel、ScrollPane,常用的有Panel, Frame, Applet。 1.Frame 以下是容器的例子: 例5.1 import java.awt.*; public class MyFrame extends Frame{ public static void main(String args[ ]){ MyFrame fr = new MyFrame("Hello Out There!"); //构造方法 fr.setSize(200,200); //设置Frame的大小,缺省为(0,0) fr.setBackground(Color.red); //设置Frame的背景,缺省为红色 fr.setVisible(true); //设置Frame为可见,缺省为不可见 } public MyFrame (String str){ super(str); //调用父类的构造方法 } } 一般我们要生成一个窗口,通常是用Window的子类Frame来进行实例化,而不是直接用到Window类。Frame的外观就像我们平常在windows系统下见到的窗口,有标题、边框、菜单、大小等等。每个Frame的对象实例化以后,都是没有大小和不可见的,因此必须调用setSize( )来设置大小,调用setVisible(true)来设置该窗口为可见的。 另外,AWT在实际的运行过程中是调用所在平台的图形系统,因此同样一段AWT程序在不同的操作系统平台下运行所看到的图形系统是不一样的。例如在windows下运行,则显示的窗口是windows风格的窗口;而在UNIX下运行时,则显示的是UNIX风格的窗口。 2. Panel 例5.2 import java.awt.*; public class FrameWithPanel extends Frame{ public FrameWithPanel(String str){ super(str); } public static void main(String args[]){ FrameWithPanel fr = new FrameWithPanel("Frame with Panel"); Panel pan=new Panel(); fr.setSize(200,200); fr.setBackground(Color.red); //框架fr的背景颜色设置为红色 fr.setLayout(null); //取消布局管理器 pan.setSize(100,100); pan.setBackground(Color.yellow); //设置面板pan的背景颜色为黄色 fr.add(pan); //用add方法把面板pan添加到框架fr中 fr.setVisible(true); } } 一般我们要生成一个窗口,通常是用Window的子类Frame来进行实例化,而不是直接用到Window类。Frame的外观就像我们平常在windows系统下见到的窗口,有标题、边框、菜单、大小等等。每个Frame的对象实例化以后,都是没有大小和不可见的,因此必须调用setSize( )来设置大小,调用setVisible(true)来设置该窗口为可见的。 另外,AWT在实际的运行过程中是调用所在平台的图形系统,因此同样一段AWT程序在不同的操作系统平台下运行所看到的图形系统是不一样的。例如在windows下运行,则显示的窗口是windows风格的窗口;而在UNIX下运行时,则显示的是UNIX风格的窗口。 5.1.4 LayoutManager 布局管理器(1) |
||||
|
|