在第六章中我们介绍了简单的输入输出方法,包括标准输入输出和文件输出的例子。输入输出是个大课题,掌握了输入输出方法可使编程解决的问题范围大大地扩展。Java设计者编制了数量众多的输入输出流类,为输入输出提供统一的接口,保证了程序对设备和平台的无关性,方便了客户程序员能够集中精力解决自己程序中的问题。
输入输出是个复杂的问题,可能有不同的输出源控制台、文件、网络数据源等,可以是不同的信息流如简单文本、数值数据、二进制字节流、字符流等。因此,为输入输出而声明的预制类种类繁多,Java1.1版又比1.0版增加了许多类。这使初学者感到困惑和不知何从下手。所有我在第六章先介绍一点简单的输入输出,然后在这一章再深入分析不同的输入输出类的方法和用途。
10.1 字节流输入输出
字节流输入输出类Stream是在Java1.0就建立了,都封装在java.io包中。
输入流类InputStream
输入流类InputStream是各种输入流类的根,由它派生的类都以XXXInputStream方法命名。因此我们可以通过名字初步了解该类的含义。它派生的子类有
其中FileInputStream和ObjectInputStream我们将详细讨论,FilterInputStream类又派生了许多子类,其中包含缓冲区输入流我们将用到它。
InputStream类是抽象类,有一个抽象方法abstrct int read()从流中读入一个字节(返回读入的字节值),读入失败时返回-1。它另有两个重载的方法用来读到字节数组中:
int read (bytr[])
int read (bytr[],offset, length) 指定放置数据的起始位置
返回的是读入字节的个数。
InputStream类的其他方法是
InputStream家族的类都继承这些方法,当然它们可以增加(或重载)自己的方法。
输出流类OutputStream
OutputStream是所有输出流类的根,由OutputStream派生的子类有
由其中的FilterOutputStream类派生的子类PrintStream, BufferdOutputStream和DataOutputStream需要关注。与read()方法相对应,OutputStream有三个write方法把字节或字节数组写入流中,还有一个close()和flush():
输入输出流中的方法都是public的,并且都带有异常规范throws java.io.IOException。我们在应用中我们必须使用try-catch结构。
PrintStream类是我们熟悉的,System.out就是一个PrintStream类对象。这个类除了继承上述方法以外还有print和println方法,它可以方便的输出各种类型的数据。与其它输出流不同,它永远不会掷出IOException。当出现异常时,仅仅设置一个错误标志。它还有一个方法boolean checkError(),它清空流并检查错误标志。
下面我们通过几个例程来介绍输入输出流的一般有用法。
例1 用read方法从文件中读一个字节并显示到屏幕。
///
import java.io.*;
public class IOStreamDemo1 {
public static void main (String args[]) {
int b;
try {
BufferedInputStream in =
new BufferedInputStream(
new FileInputStream(“input.txt”));
while ((b=in.read()) !=-1)
System.out.print((char)b);
in.close();
}catch(FileNotFoundException e) {
System.out.println(“File Not Found.”);
}catch (IOException e) {
System.out.println(“IO Exception”);
}finally { }
}
}
///
本例程演示read()方法读一个字节。输入流不能用InputStream,因为它是抽象的不能创建实例对象。显示时把b强制转化为char类型,int和byte类型显示的是整数—ASCII码。
如果把例1中的输出也用文件,就可以变成一个拷贝任何类型文件的程序。
例2 用字节流复制文件。
///
import java.io.*;
public class IOStreamDemo2 {
public static void main (String args[]) {
int b;
try {
BufferedInputStream in =
new BufferedInputStream(
new FileInputStream(“input.txt”));
FileOutputStream out =
new FileOutputStream(“output.txt”,true);
while ((b=in.read()) !=-1)
out.write(b);
in.close();
out.close();
}catch(FileNotFoundException e) {
System.out.println(“File Not Found.”);
}catch (IOException e) {
System.out.println(“IO Exception”);
}finally { }
}
}
///
文件输出类FileOutputStream的构建器要求参数(“output.txt”,true),true表示文件不存在时创建新文件。out对象最好也套一个BufferedOutputStream类。据说使用缓冲区类可提高输入输出的速度,但这至少是一个良好的编程习惯。
10.2 文本文件的读写
使用文本文件最好用行为单位进行读写,即用readLine(),如果用字节或字符为单位,那就有点不方便了。要用行为单位进行读入必须使用DataInputStream ,因为它有一个readLine()方法,而BufferedInputStream没有(注意与例2的输入对象比较)。
例3 按行写入文件,然后逐行读出并显示在屏幕上。
///
import java.io.*;
public class IOStreamDemo3 {
public static void main (String args[]) {
String sj[] ={“月黑雁飞高,”,“单于夜遁逃。”,
“欲将轻骑逐,”,“大雪满弓刀。”};
try {
PrintStream out = new PrintStream (
new FileOutputStream(“poem.txt”,true));
for (int i=0; i<4; i++)
out.println(sj[i]);
System.out.println(“Write File OK!”);
out.close();
}catch (IOException e) {
System.out.println(“IO Exception”);
}finally { }
try {
DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(“poem.txt”)));
String s = null;
while((s = in.readLine())!=null)
System.out.println(s);
in.close();
}catch(FileNotFoundException e) {
System.out.println(“File Not Found.”);
}catch (IOException e) {
System.out.println(“IO Exception”);
}finally { }
}
}
///
简要说明:
程序使用PrintStream对象把诗句写入文件,再逐行读出。输入对象in使用DataInputStream类是为了用它的readLine方法。但程序在编译时会出一个“警告:在java.io.DataInputStream中的readLine()已经不提倡使用”。因为在Java V1.1开始引进了Reader/Writer类提出使用,BufferedReader就有readLine()。另外,查看poem.txt文件,输出是完全正确的,但在屏幕上显示时汉字出现乱码。而使用BufferedReader就解决了汉字乱码问题。Reader/Writer类在本章最后一节来介绍。
10.3 数据流和对象流
有时候我们需要不经字符串转换而只即直接保存int,double等各种数据,这样写入方便,取出使用更方便。这就是数据流输入输出DataInputStream和DataOutputStream类的主要功能,它们扩展了read和write方法,能够读写各种类型的数据。这两个类型的主要方法列表如下。
DataInputStream的方法 DataOutputStream的方法
boolean readBoolean() void writeBoolean (boolean)
byte readByte() void writeByte(byte)
double readDouble() void writeDouble(double)
float readFloat() void writeFloat(float)
int readInt() void writeInt(int)
long readLong() void writeLong(long)
short readShort() void writeShort(short)
String readUTF() void writeUTF(String str)
char readChar() void writeChar(char)
int read(byte[],int,int) void writeBytes(String)
- void writeChars(String)OutputStream
注意还有从InputStream和OutputStream继承的方法。下面的例程演示不同类型数据的读写。
例4 演示数据流的读写方法。
///
import java.io.*;
public class IOStreamDemo4{
public static void main(String args[]){
final int NUM = 10;
DataInputStream in = null;
DataOutputStream out = null;
int i = 0, i1=111;
short r1= 222;
long l1 = 333333;
byte b1 = 66;
float f1 = 3.14F;
double d1 = 3.14159265;
char c1 = ‘A’;
byte[] bt = new byte[6];
char[] ch = new char[12];
try{
out = new DataOutputStream(new FileOutputStream(“data.dat”));
out.writeInt(i1);
out.writeShort(r1);
out.writeLong(l1);
out.writeByte(b1);
out.writeFloat(f1);
out.writeDouble(d1);
out.writeChar(c1);
out.writeChars(“Java World!”);
out.writeBytes(“Hello “);
out.close();
}catch(IOException e){
}finally {System.out.println(“Writed OK!”);}
try {
in = new DataInputStream(new FileInputStream(“data.dat”));
int i2 = in.readInt(); System.out.print(i2 +” “);
short r2 = in.readShort(); System.out.print(r2 +” “);
long l2 =in.readLong(); System.out.print( l2+” “);
byte b2 = in.readByte(); System.out.print(b2 +” “);
float f2 = in.readFloat(); System.out.print(f2 +” “);
double d2 = in.readDouble(); System.out.print(d2 +” “);
char c2 = in.readChar(); System.out.print(c2 +” “);
System.out.println(“\n——————-”);
StringBuffer sb = new StringBuffer();
for (i=0; i<11;i++)
sb.append(in.readChar());
in.read(bt) ;
String s = new String(bt);
System.out.print(s);
System.out.println(sb);
in.close();
}catch(FileNotFoundException e){
System.out.println(“File Not Found”);
}catch(IOException e){
e.printStackTrace();
}finally {}
}
}
///
运行结果:
Writed OK!
111 222 333333 66 3.14 3.14159265 A
——————-
Hello Java World!
对象流的读写
有时把不同类型的数据封装在一起构成对象。用数据流保存这样的数据结构,如果各成员数据的长度相等还好说,如果不相等就难以读取使用了。Java提供了ObjectInputStream类和 ObjectOutputStream类对象流来解决这一问题。对象流和数据流的方法基本相同,只是多了读写对象的方法:
Object readObject() — void writeObject(Object obj) 读出和写入一个对象
需要使用对象流来读写的对象必须作系列化,方法是在对象的声明中使用接口,如:
class Student implements Serializable{ }
下面我们用程序来演示这些方法的使用。
例5 本例程展示用对象流来读出和写入对象的方法。
程序设计了一个Student类来存放数据。首先从一个文本文件读取信息,每一行数据构建一个对象。然后把对象写入一个文件Student.obj来保存信息。最后从Student.obj读取信息供使用。
///
// Student.java
import java.io.*;
class Student implements Serializable{
int age;
String id,name,sex,birthday;
Student(String rd){
id = rd.substring(0,2).trim();
name = rd.substring(2,5).trim();
sex = rd.substring(5,6).trim();
birthday = rd.substring(6,14);
age = Integer.parseInt(rd.substring(15).trim());
}
public String toString() {
String s = new String(id +name+sex + birthday);
s += String.valueOf(age);
return s;
}
}
class CreateFile {
int create() {
String s ;
Student student ;
DataInputStream in ;
ObjectOutputStream out;
int i = 0;
try {
in = new DataInputStream(
new BufferedInputStream (
new FileInputStream(“student.txt”))) ;
out =new ObjectOutputStream(
new FileOutputStream(“student.obj”)) ;
while ((s = in.readLine()) != null){
out.writeObject(new Student(s));
i++;
}
in.close();
out.close();
} catch(FileNotFoundException e) {
System.out.println(“File student.txt Not Found.”);
} catch(IOException e) {
e.printStackTrace();
} finally {
if (i !=0) return i ;
else return -1;
}
}
}
public class IOStreamDemo5 {
public static void main(String args[]){
CreateFile f = new CreateFile();
int n = f.create();
try {
Student students[] = new Student[n] ;
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream(“student.obj”)) ;
for (int i =0; i
in.close();
for (int i =0; i
System.out.println(students[i].toString());
System.out.println (students[4].name +students[4].sex);
} catch(FileNotFoundException e) {
System.out.println(“File student.obj not found.”);
}catch(ClassNotFoundException e) {
e.printStackTrace();
} catch(IOException e) {
e.printStackTrace();
}
}
}
///
简要说明
Student类要使用对象流进行读写,所以要用Serializable接口。它设计了一个toString方法为了方便的显示对象。
CreateFile类的作用在于封装“读取信息和创建对象流文件”的任务,它只有一个方法create来完成上述任务。
主程序在创建对象流文件后读取对象到一个数组,然后使用这个对象数组。我们可以进行运算、统计、查询、排序等等的使用,这里只简单地显示信息。
由于记录内容包含了汉字,在屏幕显示时出现乱码,这需要用下一节介绍的字符流类来处理才能解决。
10.4 字符流输入输出
上一节的例3到例5在编译时会出现警告,要求你用-deprecation参数重新编译。对于使用readLine()方法警告说“DataInputStream中的readLine()已经不提倡使用”。这是因为从Java v1.1开始增加了字符流输入输出类Reader和Writer。其中的BufferdReader类已经包含readLine(),而BufferedInputStream不包含。Reader和Writer类的主要功能是以16位的字符为单位而不是8为的字节来进行读写,因此可以容纳unicode码,这就解决了汉字显示乱码的问题。
Reader/Writer类
Java 1.1对IO流库进行了一些重大的改进。看到Reader和Writer类时,大多数人的第一个印象(就象我一样)就是它们用来替换原来的InputStream和OutputStream类。但实情并非如此。尽管不建议使用原始数据流库的某些功能(如使用它们,会从编译器收到一条警告消息),但原来的数据流依然得到了保留,以便维持向后兼容,而且:
(1) 在老式层次结构里加入了新类,所以Sun公司明显不会放弃老式数据流。
(2) 在许多情况下,我们需要与新结构中的类联合使用老结构中的类。为达到这个目的,需要使用一些“桥”类:InputStreamReader将一个InputStream转换成Reader,OutputStreamWriter将一个OutputStream转换成Writer。
所以与原来的IO流库相比,经常都要对新IO流进行层次更多的封装。同样地,这也属于装饰器方案的一个缺点——需要为额外的灵活性付出代价。
之所以在Java 1.1里添加了Reader和Writer层次,最重要的原因便是国际化的需求。老式IO流层次结构只支持8位字节流,不能很好地控制16位Unicode字符。由于Unicode主要面向的是国际化支持(Java内含的char是16位的Unicode),所以添加了Reader和Writer层次,以提供对所有IO操作中的Unicode的支持。除此之外,新库也对速度进行了优化,可比旧库更快地运行。Reader/Writer类是所有字符流输入输出类的根,由Reader/Writer派生的类列举如下:
BufferedReader BufferedWriter
CharArrayReader CharArrayWriter
FilterReader FilterWriter
InputStreamReader OutputStreamWriter
PipedReader PipedWriter
StribngReader StringWriter
– PrintWriter
其中InputStreamReader和OutputStreamWriter比较重要,下面将重点讨论,用作文件读写的FileReader和FileWriter也是由此派生的。BufferedReader和BufferedWriter也经常用到,特别是因为BufferdReader类包含了行读入的readLine()方法。
显然,Java库的设计人员觉得以前的一些类毫无问题,所以没有对它们作任何修改,可象以前那样继续使用它们。没有对应Java 1.1类的Java 1.0类有
DataOutputStream
File
RandomAccessFile
SequenceInputStream
特别未加改动的是DataOutputStream,所以为了用一种可转移的格式保存和获取数据,必须沿用InputStream和OutputStream层次结构。
Reader类的方法与InputStream的类似,只是read方法把字节该为字符,如
int read (char[]) //byte[]
int read (char[],offset, length)
Writer类也类似地有
write(char[])
write(char[],int,int)
write(String)
write(int)
InputStreamReader和OutputStreamWriter
重要的是InputStreamReader和OutputStreamWriter,它们是连接字节流和字符流的桥梁。inputStreamReader把InputStream流转化为Reader流,OutputStreamWriter把OutputStream流转化为Writer流。
inputStreamReader的构建器的语法为:
inputStreamReader(inputStream in[,String enc])
enc是字符集名称,如”gb2312″。例如,inputStreamReader(System.in)把控制台输入转化为Reader流。
OutputStreamWriter的构建器的语法为:
OutputStreamWriter(OutputStream out[,String enc])
enc是字符集名称,如”gb2312″。例如,OutputStreamWriter(DataOutputStream out)把一个数据流转化为Writer流。
inputStreamReader的常用方法是继承Reader的read([char[])方法。OutputStreamWriter的常用方法是继承Writer的write([char[])方法。另外它们都有一个String getEncoding()用来获得字符集名称。
与PrintStream对应的是PrintWriter,它格式化输出字符流,并且拥有print方法。不同的是PrintStream在调用println()时会自动清空输出流,而PrintWriter不会自动,但可以设置自动清空功能。另外,能够用PrintWriter写字节流,如write(byte[])。
BufferedReader和BufferedWriter
BufferedReader类和BufferedWriter类的构建器为
BufferedReader(Reader in [,int size])
BufferedWriter(Writer out[,int size])
其中size是缓冲区的大小(bit)。
BufferedReader类的常用方法与BufferedinputStream类似,只要注意两点。一是read()方法都把读字节该为读字符。二是多了String readLine()方法读一行。
BufferedWriter类常用方法与BufferedOutputStream类似,注意write(int)写的是字符,写入字符串write(String int, int)要加void newLine()写入一个换行符,才能与readLine()相匹配。
用字符流IO把上一节的例程修改一下,就可以解决汉字乱码问题,同时也使用流的使用更加规范,符合Java设计者的要求。
例6 例3的新版。
把输入流
DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(“poem.txt”)));
改为
BufferedReader in = new BufferedReader(new FileReader(“poem.txt”));
把输出流
PrintStream out = new PrintStream(
new FileOutputStream(“poem.txt”));
改为
PrintWriter out = new PrintWriter(
new FileWriter(“poem.txt”));
运行结果:
月黑雁飞高,
单于夜遁逃。
欲将轻骑逐,
大雪满弓刀。
例7 例4的新版。
只要把类CreateFile中的输入流改为
BufferedReader in = null;
in = new BufferedReader(
new FileReader(“student.txt”)) ;
汉字的显示就完全正常了。运行结果:
4王小红女 19780530 22
5李哓红女 19780530 40
6刘中兴男 19780815 22
7李姗姗女 19781214 22
8王仁华男 19760312 24
9许慎女 19781015 22
10杜倩女 19801201 20
11朱俊杰男 19750615 28
19张新雨女 19990219 3
王仁华男
例8 通过控制台按行输入保存为文本文件。用只打回车键的空行结束输入。文件名从命令行输入。
///
import java.io.*;
public class TextWrite {
public static void main(String args[]) {
String s,fname = null ;
if (args.length ==0) usage();
else fname = args[0];
try {
BufferedWriter out = new BufferedWriter(
new FileWriter(fname));
//把标准输入连接到BufferedReader,一般读入字符流
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
System.out.println(“Input some lines, END with empty-line:”);
while ((s = in.readLine())!= null) {
if (s.length() !=0)
out.write(s +“\r\n”);
else break;
}
out.close();
in.close ();
}catch(IOException e) {
e.printStackTrace();
}
}
static void usage() {
System.out.println(“Usage: TextWrite filename
System.exit(0);
}
}
///
本例程中用InputStreamReader作为桥梁,把原本为字节流的标准输入转化为字符流,利用BufferedReader的readLine方法方便地按行读写。args[0]是命令行参数中的第一个。对于多余一个的参数将被忽略,对于没有参数的情况必须处理。要使程序更加完善,应该对args[0]的参数值是否是合法的文件名做分析,这里省略了。
关于读写随机访问文件
读写随机访问文件必须使用RandomAccessFile类,它与IO层次结构几乎是完全隔离的,尽管它也实现了DataInput和DataOutput接口。所以不可将其与InputStream及OutputStream子类的任何部分关联起来。只能用RandomAccessFile打开一个文件。必须假定RandomAccessFile已得到了正确的缓冲,因为我们不能自行选择。可以自行选择的是第二个构建器参数:可决定以“只读”(r)方式或“读写”(rw)方式打开一个RandomAccessFile文件。使用RandomAccessFile的时候,类似于组合使用DataInputStream和DataOutputStream(因为它实现了等同的接口)。除此以外,还可使用seek()方法在文件中到处移动,对某个值作出修改等等。这个类也超出了输入输出流的范围,我们不准备探讨它了。输入输出流类中还有一些没有详细讨论的,您可以在需要的时候查阅java文档,或者用本书前面介绍的方法,查某个类的构建器和所有方法,差不多就可以知道如何使用了。