第11章 Java数据流 [内容提要] 大部分程序都需要数据的输入和输出, 从键盘读取数据, 向文件写入数据或从文件获取数据, 将数据通过打印机打印出来, 通过网络进行信息交换 本章介绍Java标准程序库中各种处理I/O 操作的类 的用途及使用方法。
第1节 输入/输出概念 一、 流的概述 Java使用流(stream)来执行输入输出(I/O)的功能,流是一种数据的源头和目的之间的通信途径。 用于读入数据称为输入流(input stream), 用于写出数据称为输出流(output stream)。 引入流的概念使得处理不同的数据或数据存储时的编程更统一,无论你使用的是磁盘文件、内存缓冲区,还是网络,都可以以同样的方式处理输入和输出。 当使用流时需要使用java.io包,因此在涉及数据流操作的程序中都要先导入java.io包:import java.io.*;
二、 I/O类层次结构 Java提供超过60个不同的流类型,这些流类包含在上面提到的java.io包中, 四个基本的抽象类: InputStream、OutputStream、Reader和Writer。 你不能创建这四个类型的对象,但是其他的方法的返回值可以是他们
事实上,我们经常使用的是 派生自他们的子类。 java.io包的结构如图11-1:
三、 标准输入输出 在System类中有三个静态域System.in、System.out和System.err。 可以在标准位置使用这三个系统流进行I/O操作。 public static InputStream in 读取字符数据的标准输入流。 public static PrintStream out 显示或打印输出信息的标准输出流 public static PrintStream err 输出错误信息的标准错误流。
例11-1:标准输入输出举例 程序运行如下: 输入:a,b,c,d 输出:a,b,c,d (IODemo.java) import java.io.*; public class IODemo { public static void main(String args[]) throws IOException{ int b; int count = 0; //循环利用System.in接收输入存入b,在用System.out输出b //如果遇到文件结束则终止循环 while ((b = System.in.read()) != -1){ count++; System.out.print((char)b); } System.out.println(); System.out.println(“program end”); 程序运行如下: 输入:a,b,c,d 输出:a,b,c,d 输入:^Z (注:输入复合键CTRL-Z) 输出: program end
第2节 文件的顺序访问 一、 字节流 字节流是用来读写8位的数据,由于不会对数据作任何转换,因此可以用来处理二进制的数据。 (一)、OutputStream (二)、InputStream (三)、FileInputStream和FileOutputStream (四)、ByteArrayInputStream和ByteArrayOutputStream (五)、BufferedInputStream和BufferedOutputStream
输入流 输出流的类继承关系 InputStream FileInputStream StringBufferInputStream ByteArrayInputStream SequenceInputStream PipedInputStream FilterInputStream OutputStream FileOutputStream ByteArrayOutputStream PipedOutputStream FilterOutputStream BufferedInputStream LineNumberInputStream DataInputStream PushbackInputStream BufferedOutputStream PrintStream DataOutputStream
(一)、OutputStream OutputStream为所有的字节流输出流的父类,因此所有源自于它的类也会继承下列的方法: void write(int b):写入一个字节的数据。 void write(byte[] buttfer):讲数组buffer的数据写入流。 void write(byte[],int offset int len):从buffer[offset]开始,写入len个字节的数据。 void flush():强制将buffer内的数据写入流。 void close():关闭流。
InputStream为所有的字节输入流的父类,因此所有源于它的类也会继承下列的方法: int read():读入一个字节的数据,如果已达到文件的末端,返回值为-1。 int read(byte[] buffer):读出buffer大小的数据,返回值为实际所读出的字节。 int read(byte[] buffer,int offset,int len):将读出的数据从buffer[offset]开始,写入len个字节至buffer中,返回值为实际所读出的字节数目。 int available(): 返回流内可供读取的字节数目。 long skip(long n): 跳过n个字节的数据,返回值为实际所跳过的数据数。 void close(): 关闭流。
(三)、FileInputStream和FileOutputStream 常用的构造函数如下: FileInputStream(String name): 打开文件name用来读取数据。 FileInputStream(File file): 打开文件file用来读取数据。 FileOutputStream(String name): 打开文件name用来写入数据。 FileOutputStream(File file): 打开文件file用来写入数据。 FileOutputStream(String name,Boolean append): 打开文件name用来写入数据,若append为true,则写入的数据会加到原有文件后面,否则,覆盖原有的文件。
将FileExam.java复制为FileExam.java.bak。 使用read()将其内容逐字节读取出来(如果返回值为-1,则表明已到达文件尾端), 再使用write()将读出的字节逐一写入另一个文件FileExam.java.bak中。 若文件无法打开(例如FileExam.java不存在)或无法生成(FileExam.java.bak),会抛出FileNotFoundException异常,若读写出错,则会抛出IOException异常。 最后,我们在finally中关闭文件。
例11-2 FileIO.java import java.io.*; public class FileIO{ public static void main(String args[]){ FileInputStream inFile = null; FileOutputStream outFile = null; try{ //打开”FileExam.java”用来读取数据。 inFile = new FileInputStream("FileExam.java"); //打开”FileExam.java.bak”用来写入数据。 outFile = new FileOutputStream("FileExam.java.bak"); int date; while((date = inFile.read()) != -1){//从原文件读取字节 //每次向目标文件写入一个字节 outFile.write(date); } } //处理文件不存在情况 catch(FileNotFoundException e){ System.out.println(e.getMessage()); //处理IO异常 catch(IOException e){ //最后的清理工作 finally{ //关闭源文件 if(inFile != null){ inFile.close(); catch(IOException e){} //关闭目标文件 if(outFile != null){ outFile.close();
(四)、ByteArrayInputStream和 ByteArrayOutputStream ByteArrayInputStream和ByteArrayOutputStream并没有牵涉到真正的文件,他们使用内存(一个流缓冲区)作为I/O流的源头及目的。 ByteArrayInputStream用来从一个流缓冲区(字节数组)中读入数据, ByteArrayOutputStream用来将数据转换至一个流缓冲区中 由于流对象的内容存放在内存中,所以可提供较快的操作。
常用的构造函数如下: ByteArrayInputStream(byte[] buffer): 生成一个字节数组的输入流,并指定一个字节数组buffer为此输入流的流缓冲区。 ByteArrayOutputStream(): 生成一个字节数组的输出流,其缓冲区的默认初始大小为32字节,若有需要,缓冲区的大小会随之增加。 ByteArrayOutputStream(int size): 同上,但分配size字节的初始大小给此缓冲区。 ByteArrayOutputStream的一些常用方法: int size() : 返回此流缓冲区的大小。 byte[] toByteArray() : 生成一个新的字节数组,并将流的内容复制到此字节数组。 String toString() : 将流的内容转换为String对象。 下面程序读入文件data.in,将其内容写入内存中(一个字节数组),接着转换为String对象,并将该String对象内的令牌解析出来。
例11-3 ByteIO.java import java.io.*; import java.util.*; public class ByteIO { public static void main(String[] args){ try{ //声明源文件 File inFile = new File("data.in"); FileInputStream inData = new FileInputStream(inFile); ByteArrayOutputStream dataArray = new ByteArrayOutputStream(); int data; while((data = inData.read()) != -1){ dataArray.write(data); } inData.close(); //将字节数组转换成字符串 String dataStr = dataArray.toString(); //解析String对象内的令牌 StringTokenizer tokens = new StringTokenizer(dataStr); while(tokens.hasMoreTokens()){ System.out.println(tokens.nextToken()); catch(IOException e){ System.out.println(e.getMessage());
若输入数据(data.in中的数据)如下: Aa 00001 92 69 95 Bb 00002 77 92 96 则程序输出如下: Aa 00001 92 69 95 Bb 00002 77 96
(五)、BufferedInputStream和BufferedOutputStream 有时候我们在处理来自输入流的数据时,希望能够重设流并回到较靠前的位置。 这需要使用缓冲来实现,通过使用BufferedInputStream类,可以利用mark和reset方法在缓冲的输入流中往回移动; 同时,通过使用BufferedOutputStream类,可以先将输出写到内存缓冲区,再使用flush方法将数据写入磁盘,而不必每输出一个字节就向磁盘中写一次数据。
1.BufferedInputStream类的构造函数和方法 BufferedInputStream(InputStream in): 构造一个BufferedInputStream。 BufferedInputStream(InputStream in, int size): 构造一个具有给定的缓冲区大小的BufferedInputStream。 int available(): 得到可以从这个输入流读取的字节数。 void close(): 关闭流。 void mark(int readlimit): 在这个输入流的当前位置做标记。 boolean markSupported(): 检查这个输入流是否支持mark和reset方法。 int read(): 读取数据的下一个字节。 int read(byte[] b, int off, int len): 从这个字节输入流的给定偏移量处开始读取字节,存储到给定的字节数组。 void reset(): 将缓冲区重新设置到加标记的位置。 long skip(long n) : 跳过n个字节的数据。
2.BufferedOutputStream类的构造函数和方法: BufferedOutputStream(OutputStream out):构造一个BufferedOutputStream。 BufferedOutputStream(OutputStream out, int size):构造一个具有给定的缓冲区大小的BufferedOutputStream。 void flush(): 刷新这个流。 void write(byte[] b, int off, int len): 将给定的字节数组写到这个缓冲输出流。 void write(int b) : 将给定的字节写到这个缓冲输出流。 下面我们通过一个例子来演示BufferedInputStream类的作用,程序读取并显示字节数组中的字符,当遇到’%’字符时,如果和下一个’%’字符之间的字符没有空格,则两个’%’之间的字符不显示:
例11-4 bufinstreams.java 运行结果: This is a test of buffer#Streams import java.io.*; class bufinstreams { public static void main(String args[]) throws IOException { //定义数据源 byte data[] = "This is a test of buffer%Streams. %comment%".getBytes(); ByteArrayInputStream in = new ByteArrayInputStream(data); BufferedInputStream bufinstream = new BufferedInputStream(in); int character; boolean flag = false; while ((character = bufinstream.read()) != -1) { switch(character) { case '%': if (flag) {//第二个’%’ flag = false; } else {//第一个’%’ flag = true; //加标记 bufinstream.mark(100); } break; case ' ': if (flag) {//第一个’%’后面的空格 System.out.print("#"); //将缓冲区重新设置到加标记的位置 bufinstream.reset(); } else System.out.print((char) character); default: if (!flag) System.out.print((char) character);
二、 字符流 尽管字节流更快更高效,但是人们读起来十分困难(因为是二进制)。 接下来我们讲述文本格式的输入输出,即字符流。 字符流主要是用来支持Unicode的文字内容,绝大多数在字节流中所提供的类,都有相对应的字符流的类。 Reader BufferedReader InputStreamReader FileReader Writer PrintWriter BufferedWriter OutputStreamWriter FileWriter
(一)、Reader和Writer Reader是所有输入字符流的基类,Writer是所有输出字符流的基类。 你可以使用从这两个类派生出的类来读写字符流,这两个类常用的方法与InputStream、OutputStream相类似,区别在于参数内的byte[]需要改为char[]。
(二)、InputStreamReader和OutputStreamWriter 为了从键盘读取按键,常常从System.in构造一个InputStreamReader流,然后使用InputStreamReader类的read方法读取用户输入的内容。 而OutputStreamWriter与InputStreamReader相对,支持输出流。
1.InputStreamReader类的构造函数和方法: InputStreamReader(InputStream in) : 构造一个InputStreamReader。 InputStreamReader(InputStream in, String enc) : 使用命名的字符编码构造一个InputStreamReader。 void close() : 关闭流。 String getEncoding() : 得到字符编码的名称。 int read() : 读取一个字符。 int read(char[] cbuf, int off, int len) : 将字符读到数组中。 boolean ready() : 如果这个流已准备好,返回True。
2.OutputStreamWriter类的构造函数和方法: OutputStreamWriter(OutputStream out) : 构造一个OutputStreamWriter。 OutputStreamWriter(OutputStream out, String enc) : 使用命名的字符编码构造一个OutputStreamWriter。 void close() : 关闭流。 void flush() : 刷新流。 String getEncoding() : 得到这个流使用的字符编码的名称。 void write(char[] cbuf, int off, int len) : 写字符数组的一部分。 void write(int c) : 写一个字符。 void write(String str, int off, int len) : 写一个字符串的一部分。 下面的程序利用InputStreamReader从键盘接受输入,再将输入的字符显示出来:
例11-5 程序运行结果如下: 输入:a,b,c,d 输出:a,b,c,d 输入:1,2,3,4 输出:1,2,3,4 inputstreamreader.java import java.io.*; class inputstreamreader { public static void main(String args[]) { try { int character; InputStreamReader instreamread = new InputStreamReader(System.in); //InputStreamReader以字符形式读取输入 while ((character = instreamread.read()) != -1) { System.out.print((char) character); } catch (IOException e) {} System.out.print(“Bye”); 程序运行结果如下: 输入:a,b,c,d 输出:a,b,c,d 输入:1,2,3,4 输出:1,2,3,4 输入:^Z 输出:Bye
(三)、FileReader 你可以使用FileReader类创建一个字符流来读取一个文件,FileReader类只具有从InputStreamReader继承的功能,但是它有自己的构造函数: FileReader(File file) : 构造一个FileReader。 FileReader(FileDescriptor fd) : 从一个文件描述符构造一个FileReader。 FileReader(String filename) : 从一个文件名构造一个FileReader。 在下面的例子中,我们打开一个文件file.txt(内容为:”Hello from file”),将文件的内容读到缓冲区,然后显示数据:
例11-6 程序运行结果如下: Hello from file fileread.java import java.io.*; public class fileread{ public static void main(String args[]) throws Exception { FileReader fileread = new FileReader("file.txt"); char data[] = new char[1024]; int charsread = fileread.read(data); System.out.println(new String(data,0,charsread)); fileread.close(); }
(四)、FileWriter FileWriter与FileReader对应,用来将字符缓冲区中的数据写到文件中。这个类也只定义了构造函数,此外只具有从OutputStreamWriter继承的功能。 FileWriter(File file) : 从File对象构造一个FileWriter。 FileWriter(FileDescriptor fd) : 从文件描述符构造一个FileWriter。 FileWriter(String filename) : 从文件名构造一个FileWriter。 FileWriter(String filename, boolean append):构造一进行附加的FileWriter。 在下面例子中,我们分别采用三种方式:逐字符地写数据、一次写入所有数据、分批写数据,使用FileWriter:
例11-7 程序执行后: file1.txt和file2.txt的内容为:This is a string of text. filewrite.java import java.io.*; public class filewrite{ public static void main(String[] args) throws Exception{ char data[] = {'T','h','i','s',' ','i','s',' ','a',' ', 's','t','r','i','n','g',' ','o','f',' ','t','e','x','t','.'}; FileWriter filewriter1 = new FileWriter("file1.txt"); //逐字符地写数据 for(int loop_index = 0;loop_index < data.length;loop_index++){ filewriter1.write(data[loop_index]); } FileWriter filewriter2 = new FileWriter("file2.txt"); //一次写入所有数据 filewriter2.write(data); FileWriter filewriter3 = new FileWriter("file3.txt"); //分批写入数据 filewriter3.write(data,5,10); filewriter1.close(); filewriter2.close(); filewriter3.close(); 程序执行后: file1.txt和file2.txt的内容为:This is a string of text. file3.txt的内容为:is a strin
(五)、CharArrayReader和CharArrayWriter 可以用来从数组读写字符。 1.CharArrayReader类的构造函数和方法如下: CharArrayReader(char[] buf): 从给定的字符数组构造一个CharArrayReader。 CharArrayReader(char[] buf, int offset, int length): 同上。 void close(): 关闭流。 void mark(int readAheadLimit): 在流中的当前位置加标记。 boolean markSupported(): 如果这个流支持mark操作,那么返回True。 int read(): 读取一个字符。 int read(char[] b, int off, int len): 将字符读入数组的一部分。 boolean ready(): 检查这个流是否已经准备好被读取。 void reset(): 将流重设到最近的标记。 long skip(long n): 跳过n个字符。
2.CharArrayWriter类的构造函数和方法如下: CharArrayWriter(): 构造一个CharArrayWriter。 CharArrayWriter(int initialSize):构造一个具有给定初始大小的CharArrayWriter。 void close(): 关闭流。 void flush(): 刷新流。 void reset(): 重设流。 int size(): 得到缓冲区的当前大小。 char[] toCharArray(): 得到输入数据的副本。 String toString(): 将输入数据转换为字符串。 void write(char[] c, int off, int len): 将字符写到缓冲区中。 void write(int c): 将一个字符写到缓冲区中。 void write(String str, int off, int len): 将字符串的一部分写到缓冲区中。 void writeTo(Writer out): 将缓冲区的内容写到另一个字符流中。 下面我们演示CharArrayReader类的作用,该例从字符数组中逐个读取字符并显示:
例11-8 程序运行结果为: This is a string of text. chararrayreader.java import java.io.*; public class chararrayreader { public static void main(String args[]) throws IOException char data[] = {'T','h','i','s',' ','i','s',' ','a', ' ','s','t','r','i','n','g',' ','o','f', ' ','t','e','x','t','.'}; CharArrayReader chararrayreader = new CharArrayReader(data); int character; while((character = chararrayreader.read()) != -1) { System.out.print((char)character); }
第3节 文件的随机访问 前面所提及的流都是顺序的I/O流,无法随机的读写文件的某个位置,如果需要对文件进行随机的访问,必须使用RandomAccessFile类,其常用的构造函数如下:
RandomAccessFile(File file, String mode): 构造一个随机访问文件流,file为被访问的文件对象,mode是用来指定存取的模式,mode可以为r(读)、w(写)或rw(读写)。 RandomAccessFile(String name,String mode):构造一个随机访问文件流,以便访问由字符串name指定名字的文件,mode参数使用同上。 RandomAccessFile类提供的用来读取某种基本数据类型的数据或字符串的方法如下: boolean readBoolean() byte readByte() char readChar() double readDouble() float readFloat() int readInt() long readLong() short readShort() String readLine()
RandomAccessFile类提供的用来向文件中写入某种基本类型的数据或字符串的方法如下: void writeByte(byte b) void writeBytes(String s) void writeChar(char c) void writeChars(String s) void writeDouble(double d) void writeFloat(float f) void writeInt(int i) void writeLong(long l) void writeShort(short s)
和目前文件位置有关的方法: void seek(long pos): 将文件指针移到pos(不可为负)的位置,这是相对于文件的初始位置的值(初始值为0)。 long getFilePointer(): 得到目前文件指针的位置(相对于文件初始位置)。 long length(): 得到文件的长度。
第4节 目录和文件管理 File类是一个和流无关的类。 File对象可以用来生成与文件(及其所在的路径)或目录结构相关的对象, 由于不同的系统可能会有不同的目录结构表示法, 使用File类可以达到与系统无关的目的(使用抽象的路径表示法)。 类java.io.File提供了获得文件基本信息及操作文件的一些方法。 事实上,File类和流还是有关系的,因为在对一个文件进行I/O操作之前,必须先获得这个文件的基本信息。
File类的构造函数: File(String path): 将一个代表路径的字符串转换为抽象的路径表示法。 File(String parent, String child): parent代表目录,child代表文件(不可为空)。 File(File parent, String child): 同上。 例如: File myFile; myFile = new File(“file.txt”); 或 myFile = new File(“/”,”file.txt”); File myDir=new File(“/”); myFile = new File(myDir,”file.txt”);
使用何种构造函数经常由其他被访问的文件对象来决定。 例如, 当在应用程序中只用到一个文件时,那么使用第一种构造函数最为实用; 但是如果使用了一个共同目录下的几个文件,则使用第二种或第三种构造函数会更方便。
File类一些常用的方法 boolean exists(): 若该文件或目录存在,返回True。 boolean isDirectory(): 若为目录则返回True。 File[] listFiles(): 得到该对象所代表的目录下的File对象数组。 String[] list(): 同上。 long length(): 得到和该对象相关的文件大小,若不存在,返回0。 String toString(): 得到抽象路径表示法。 String getParent(): 得到抽象路径表示法的目录部分。 String getName(): 得到抽象路径表示法的最后一个部分。 boolean renameTo(File newName):将当前File对象所代表的路径名改为newName所代表的路径名。若成功,返回true。 boolean mkdir(): 生成一个新的目录。若成功,返回True。 boolean mkdirs(): 生成一个新的目录,包含子目录。若成功,返回true。 boolean delete(): 删除目前File对象代表的文件或目录,目录需为空。删除成功,返回true。
例11-9 程序运行结果如下: The file is exist? true The file can write? true The file can read? true The file is a file? true The file is a directory? false The file's name is: file.txt The file's all path is: C:\Java\file.txt The file's length is 15 import java.io.*; class file{ public static void main(String[] args){ File fl = new File("file.txt"); System.out.println("The file is exist? "+fl.exists()); System.out.println("The file can write? "+fl.canWrite()); System.out.println("The file can read? "+fl.canRead()); System.out.println("The file is a file? "+fl.isFile()); System.out.println("The file is a directory? "+fl.isDirectory()); System.out.println("The file's name is: "+fl.getName()); System.out.println("The file's all path is: " + fl.getAbsolutePath()); System.out.println("The file's length is "+fl.length()); } 使用File类的方法得到关于 文件file.txt的信息
第5节 其他常用的流处理 一、管道流 管道(pipe)提供一种线程之间的通信方法,可用于IPC(Inter-Process Communication进程间通信) 或是ITC(Inter-Thread Communication线程间通信),但不能够在不同主机间进行通信。 一个输入管道是用来接收一个输出管道所写出的数据,因此,一个线程会负责送出(PipedOutputStream对象)数据,而另一个线程序要负责接收(PipedInputStream对象)这些数据,PipedInputStream和PipedOutputStream对象总是成对出现的。并且由于管道的数据流是单向的
(一)、创建一组管道通信可以按照下面步骤进行(使用无参数的构造函数) 建立输入流: PipedInputStream pipedinputstream = new PipedInputStream(); 建立输出流: PipedOutputStream pipedoutputstream = new PipedOutputStream(); 将输入输出连接起来: pipedinputstream.connect(pipedoutputstream); 或者 pipedoutputstream.connect(pipedinputstream);
(二)、直接建立连接 PipedInputStream pis = new PipedInputStream(); PipedOutputStream pos = new PipedOutputStream(pis); 或 new PipedOutputStream(); new PipedInputStream(pos);
二、ZIP文件流 ZIP文件是存储了一个或多个文件的存档文件,通常使用压缩格式。 从Java 1.1起,Java能够处理GZIP和ZIP格式的存档文件。 与其他的流不同,处理ZIP文件的类ZipInputStream和ZipOutputStream在java.util.zip,而不在java.io中,因此在使用ZIP文件流时记住要加上:import java.util.*;
(一)、将文件写入ZIP文件 (二)、读取ZIP文件 生成和所要生成的ZIP文件相关联的ZipOutputStream对象。 一个ZIP文件往往不止含有一个压缩文件,我们将每个要加入的文件称为一个ZIP入口,我们使用ZipEntry(String fileName)来生成这些ZipEntry对象。 使用putNextEntry(ZipEntry entry)将此ZIP入口加入ZIP文件。 将文件内容写入此ZIP文件。 使用closeEntry()结束当前的ZIP入口,继续下一个ZIP入口。 (二)、读取ZIP文件 生成和所要读入的ZIP文件相关联的ZipInputStream对象。 利用getNextEntry()得到下一个ZIP入口。
三、DataInputStream和DataOutputStream DataInputStream类和DataOutputStream类允许通过数据流来读写Java的基本数据类型,包括布尔型、整型、浮点型等。他们的构造函数如下: DataInputStream(InputStream inputstream); DataOutputStream(OutputStream outputstream);
1.DataInputStream类中处理java基本数据类型的一些方法: byte readByte() long readLong() double readDouble() int readInt() short readShort() float readFloat() boolean readBoolean() 2.DataOutputStream类写数据的一些方法: void writeByte(byte b) void writeInt(int i) void writeShort(short sh) void writeLong(long l) void writeFloat(float f) void writeDouble(double d) void writeBoolean(boolean bl)
四、对象流 Java中的数据流不仅能对基本数据类型的数据进行操作,而且也提供了把对象写入文件或从文件中读取对象的功能,这一功能是通过java.io包中的ObjectInputStream和ObjectOutputStream两个类实现的。 由于他们和DataInputStream及DataOutputStream有共同的接口,因此他们也可使用相同的方法来读取或写入数据。 除此之外,这两个类还有如下方法用于读写对象: void writeObject(Object obj):ObjectOutputStream的方法,将对象写入流。 Object readObject(): ObjectInputStream的方法,将对象读出。
习题十一 9.1 什么叫做流?输入输出流分别对应那两个抽象类? 9.2 利用输入输出流编写程序,实现文件拷贝的功能。 9.3 编写程序,实现在屏幕上显示文本文件的功能。 9.4 Reader和Writer的作用是什么?