【课前思考】
1. 什么是例外?Java中有哪两种例外处理机制?
2. 字节流和字符流的基类各是什么?
3. 什么是对象的串行化?对象串行化的作用是什么?
难点:
1.如何使用Java中两种例外处理机制,抛弃例外和声明抛弃例外的区别与联系。
2.处理字符流时,其构造方法的参数是一个字节流。
3.对象串行化的概念。
4.1.2 例外处理机制
抛弃(throw)例外:
在Java程序的执行过程中,如果出现了异常事件,就会生成一个例外对象。生成的例外对象将传递给Java运行时系统,这一例外的产生和提交过程称为抛弃(throw)例外。
两种处理例外的机制:
◇ 捕获例外:
当Java运行时系统得到一个例外对象时,它将会沿着方法的调用栈逐层回溯,寻找处理这一例外的代码。找到能够处理这种类型的例外的方法后,运行时系统把当前例外对象交给这个方法进行处理,这一过程称为捕获(catch)例外。这是积极的例外处理机制。如果Java运行时系统找不到可以捕获例外的方法,则运行时系统将终止,相应的Java程序也将退出。
◇ 声明抛弃例外:
如果一个方法并不知道如何处理所出现的例外,则可在方法声明时,声明抛弃(throws)例外。这是一种消极的例外处理机制。
4.1.3 例外类的层次
在jdk中,每个包中都定义了例外类,而所有的例外类都直接或间接地继承于Throwable类。图4-1为jdk中例外类的继承关系。
java中的例外类可分为两大类:
Error
动态链接失败,虚拟机错误等,通常Java程序不应该捕获这类例外,也不会抛弃这种例外。
Exception
1)运行时例外:
继承于RuntimeException的类都属于运行时例外,例如算术例外(除零错)、数组下标越界例外等等。由于这些例外产生的位置是未知的,Java 编译器允许程序员在程序中不对它们做出处理。
2)非运行时例外:
除了运行时例外之外的其他由Exception 继承来的例外类都是非运行时的例外,例如FileNotFoundException(文件未找到例外)。Java编译器要求在程序中必须处理这种例外,捕获例外或者声明抛弃例外。
4.2.1 捕获例外
捕获例外是通过try-catch-finally语句实现的:
try{
......
}catch( ExceptionName1 e ){
......
}catch( ExceptionName2 e ){
......
}
......
}finally{
......
}
◇ try
捕获例外的第一步是用try{…}选定捕获例外的范围,由try所限定的代码块中的语句在执行过程中可能会生成例外对象并抛弃。
◇ catch
每个try代码块可以伴随一个或多个catch语句,用于处理try代码块中所生成的例外事件。catch语句只需要一个形式参数指明它所能够捕获的例外类型,这个类必须是Throwable的子类,运行时系统通过参数值把被抛弃的例外对象传递给catch块。
在catch块中是对例外对象进行处理的代码,与访问其它对象一样,可以访问一个例外对象的变量或调用它的方法。getMessage( )是类Throwable所提供的方法,用来得到有关异常事件的信息,类Throwable还提供了方法printStackTrace( )用来跟踪异常事件发生时执行堆栈的内容。例如:
try{
......
}catch( FileNotFoundException e ){
System.out.println( e );
System.out.println( "message: "+e.getMessage() );
e.printStackTrace( System.out );
}catch( IOException e ){
System.out.println( e );
}
catch 语句的顺序:
捕获例外的顺序和catch语句的顺序有关,当捕获到一个例外时,剩下的catch语句就不再进行匹配。因此,在安排catch语句的顺序时,首先应该捕获最特殊的例外,然后再逐渐一般化。也就是一般先安排子类,再安排父类。
◇ finally
捕获例外的最后一步是通过finally语句为例外处理提供一个统一的出口,使得在控制流转到程序的其它部分以前,能够对程序的状态作统一的管理。不论在try代码块中是否发生了异常事件,finally块中的语句都会被执行。
4.2.2 声明抛弃例外
1.声明抛弃例外
如果在一个方法中生成了一个例外,但是这一方法并不确切地知道该如何对这一异常事件进行处理,这时,一个方法就应该声明抛弃例外,使得例外对象可以从调用栈向后传播,直到有合适的方法捕获它为止。
声明抛弃例外是在一个方法声明中的throws子句中指明的。例如:
public int read () throws IOException{
......
}
throws子句中同时可以指明多个例外,之间由逗号隔开。例如:
public static void main(String args[]) throws
IOException,IndexOutOfBoundsException {…}
2.抛出例外
抛出例外就是产生例外对象的过程,首先要生成例外对象,例外或者由虚拟机生成,或者由某些类的实例生成,也可以在程序中生成。在方法中,抛出例外对象是通过throw语句实现的。
例如:
IOException e=new IOException();
throw e ;
可以抛出的例外必须是Throwable或其子类的实例。下面的语句在编译时将会产生语法错误:
throw new String("want to throw");
自定义例外类必须是Throwable的直接或间接子类。
注意:一个方法所声明抛弃的例外是作为这个方法与外界交互的一部分而存在的。所以,方法的调用者必须了解这些例外,并确定如何正确的处理他们。
4.4 I/O 流概述
输入/输出处理是程序设计中非常重要的一部分,比如从键盘读取数据、从文件中读取数据或向文件中写数据等等。Java把这些不同类型的输入、输出源抽象为流(stream),用统一接口来表示,从而使程序简单明了。Jdk 提供了包java.io,其中包括一系列的类来实现输入/输出处理。下面我们对java.io包的内容进行概要的介绍。
4.4.1 I/O流的层次
1.字节流:
从InputStream和OutputStream派生出来的一系列类。这类流以字节(byte)为基本处理单位。
◇ InputStream、OutputStream
◇ FileInputStream、FileOutputStream
◇ PipedInputStream、PipedOutputStream
◇ ByteArrayInputStream、ByteArrayOutputStream
◇ FilterInputStream、FilterOutputStream
◇ DataInputStream、DataOutputStream
◇ BufferedInputStream、BufferedOutputStream
2.字符流:
从Reader和Writer派生出的一系列类,这类流以16位的Unicode码表示的字符为基本处理单位。
◇ Reader、Writer
◇ InputStreamReader、OutputStreamWriter
◇ FileReader、FileWriter
◇ CharArrayReader、CharArrayWriter
◇ PipedReader、PipedWriter
◇ FilterReader、FilterWriter
◇ BufferedReader、BufferedWriter
◇ StringReader、StringWriter
3.对象流
◇ ObjectInputStream、ObjectOutputStream
4.其它
◇ 文件处理:
File、RandomAccessFile;
◇ 接口
DataInput、DataOutput、ObjectInput、ObjectOutput;
4.4.2 InputStream 和OutputStream
1.InputStream
◇ 从流中读取数据:
int read( ); //读取一个字节,返回值为所读的字节
int read( byte b[ ] ); //读取多个字节,放置到字节数组b中,通常
//读取的字节数量为b的长度,返回值为实际
//读取的字节的数量
int read( byte b[ ], int off, int len ); //读取len个字节,放置
//到以下标off开始字节
//数组b中,返回值为实
//际读取的字节的数量
int available( ); //返回值为流中尚未读取的字节的数量
long skip( long n ); //读指针跳过n个字节不读,返回值为实际
//跳过的字节数量
◇ 关闭流:
close( ); //流操作完毕后必须关闭
◇ 使用输入流中的标记:
void mark( int readlimit ); //记录当前读指针所在位置,readlimit
//表示读指针读出readlimit个字节后
//所标记的指针位置才失效
void reset( ); //把读指针重新指向用mark方法所记录的位置
boolean markSupported( );//当前的流是否支持读指针的记录功能
2.OutputStream
◇ 输出数据:
void write( int b ); //往流中写一个字节b
void write( byte b[ ] ); //往流中写一个字节数组b
void write( byte b[ ], int off, int len ); //把字节数组b中从
//下标off开始,长度为len的字节写入流中
◇ flush( ) //刷空输出流,并输出所有被缓存的字节
由于某些流支持缓存功能,该方法将把缓存中所有内容强制输出到流中。
◇ 关闭流:
close( ); //流操作完毕后必须关闭
4.4.3 I/O中的例外
进行I/O操作时可能会产生I/O例外,属于非运行时例外,应该在程序中处理。如:FileNotFoundException, EOFException, IOException。
I/O处理中,最常见的是对文件的操作,java.io包中有关文件处理的类有:File、FileInputStream、FileOutputStream、RamdomAccessFile和FileDescriptor;接口有:FilenameFilter。
4.5.1 文件描述
类File提供了一种与机器无关的方式来描述一个文件对象的属性。下面我们介绍类File中提供的各种方法。
◇ 文件或目录的生成
public File(String path);/*如果path是实际存在的路径,则该File对象
/*表示的是目录;如果path是文件名,则该File对象表示的是文件。*/
public File(String path,String name);//path是路径名,name是文件名
public File(File dir,String name);//dir是路径名,name是文件名
◇ 文件名的处理
String getName( ); //得到一个文件的名称(不包括路径)
String getPath( ); //得到一个文件的路径名
String getAbsolutePath( );//得到一个文件的绝对路径名
String getParent( ); //得到一个文件的上一级目录名
String renameTo(File newName); //将当前文件名更名为给定文件的
完整路径
◇ 文件属性测试
boolean exists( ); //测试当前File对象所指示的文件是否存在
boolean canWrite( );//测试当前文件是否可写
boolean canRead( );//测试当前文件是否可读
boolean isFile( ); //测试当前文件是否是文件(不是目录)
boolean isDirectory( ); //测试当前文件是否是目录
◇ 普通文件信息和工具
long lastModified( );//得到文件最近一次修改的时间
long length( ); //得到文件的长度,以字节为单位
boolean delete( ); //删除当前文件
◇ 目录操作
boolean mkdir( ); //根据当前对象生成一个由该对象指定的路径
String list( ); //列出当前目录下的文件
在java的windows编程中,目录用如下形式:File dir = new File("e://myapp//src");
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个字节
过滤流在读/写数据的同时可以对数据进行处理,它提供了同步机制,使得某一时刻只有一个线程可以访问一个I/O流,以防止多个线程同时对一个I/O流进行操作所带来的意想不到的结果。类FilterInputStream和FilterOutputStream分别作为所有过滤输入流和输出流的父类。
为了使用一个过滤流,必须首先把过滤流连接到某个输入/出流上,通常通过在构造方法的参数中指定所要连接的输入/出流来实现。例如:
FilterInputStream( InputStream in );
FilterOutputStream( OutputStream out );
几种常见的过滤流:
◇ BufferedInputStream和BufferedOutputStream
缓冲流,用于提高输入/输出处理的效率。
◇ DataInputStream 和 DataOutputStream
不仅能读/写数据流,而且能读/写各种的java语言的基本类型,如:boolean,int,float等。
◇ LineNumberInputStream
除了提供对输入处理的支持外,LineNumberInputStream可以记录当前的行号。
◇ PushbackInputStream
提供了一个方法可以把刚读过的字节退回到输入流中,以便重新再读一遍。
◇ PrintStream
打印流的作用是把Java语言的内构类型以其字符表示形式送到相应的输出流。
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】
InputStreamReader ir;
BufferedReader in;
ir=new InputStreamReader(System.in);
//从键盘接收了一个字符串的输入,并创建了一个字符输入流的对象,in的定//义如下:public static final InputStream in ,输出流out的定义如下:public static final //PrintStream out
in=new BufferedReader(ir);
String s=in.readLine();
//从输入流in中读入一行,并将读取的值赋值给字符串变量s
System.out.println("Input value is: "+s);
注意:在读取字符流时,如果不是来自于本地的,比如说来自于网络上某处的与本地编码方式不同的机器,那么我们在构造输入流时就不能简单地使用本地缺省的编码方式,否则读出的字符就不正确;为了正确地读出异种机上的字符,我们应该使用下述方式构造输入流对象:
ir = new InputStreamReader(is, "8859_1");
采用ISO 8859_1编码方式,这是一种映射到ASCII码的编码方式,可以在不同平台之间正确转换字符。
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.2.1节
3. 定制串行化
缺省的串行化机制,对象串行化首先写入类数据和类字段的信息,然后按照名称的上升排列顺序写入其数值。如果想自己明确地控制这些数值的写入顺序和写入种类,必须定义自己的读取数据流的方式。就是在类的定义中重写接口serializeble的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.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 把几个输入流顺序连接起来。顺序输入流提供了把若干不同的流统一为同一个流的功能,使得程序变得更加简洁。