文件与数据流

## 什么是数据流

流的概念很形象，当程序需要读取数据的时候，就会开启一个通向数据源的流，这个数据源可以是文件、内存、或网络连接。当然，当程序需要写入数据的时候，就会开启一个通向目的地的流。如下图：

图 4-1 流的过程图

流可以看成是数据的导管。导管的两端一端是源端，一端是目的端。数据从源端输入，从目的端输出。流的使用者不用考虑流内部数据是如何传输的，只需要向源端输入数据和向目的端取出数据，即流的两端都有数据缓冲区可以暂存数据，这样接收端可以不必每时每刻都监视流是否有数据需要接收，数据来了之后放在缓冲区内，等需要的时候再去读取。而发送端也不必每一个字节都调用发送方法。而是等集中了—定数量的数据后再一起发送，这样可以大大提高发送效率。

## 数据流的分类

Java中提供了两个基本的流操作类InputStream和OutputStream，同时在这两个类的基础又派生了很多类，例如字符流、文件流等等。如下图4-3：

图 4-2 字节流

图 4-3 字符流

1.FileInputStream 和 FileOutputStream

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

FileInputStream infile =new FileInputStream("myfile.dat");

FileOutputStream outfile =new FileOutputStream("results.dat");

2.BufferInputStream 和 BufferOutputStream

BufferlnputStream和BufferOutputStream是过滤器流，它们可以提高I/O操作的效率。

3.DataInputStream 和 DataOutputStream

DataInputStream和DataOutputStream过滤器通过流来读写Java基本类。读写的过程如：

(1)DataInputStream 方法

byte readByte()

long readLong()

double readDouble()

（2)DataOutputStream方法

void writeByte(byte)

void writeLong(long)

void writeDouble(double)

注意：DataInputStream 和DataOutputStream的方法是成对的。

4.PipedInputStream 和 PipedOutputStream

PipedInputStream和PipedOutputStream管道流用来在线程间进行通信。

## 文件I/O

基于Java的平台无关性的特点，Java的文件访问机制也是独立于文件系统的。Java提供了一个类File，这个类提供了获取文件名和文件状态的机制。File类java.io包中定义了一个文件类来专门处理文件，并获取文件的有关信息。

文件类的构造器有以下几个：

public File(String Path)

public File (String Path，String name)

public File (File Dir，String name)

在第一个构造器中，可以通过指定一个目录结构来创建一个对象，这个目录结构既可以是个绝对目录，也可以是一个相对目录。例如可以通过下面的语句创建一个File对象：

File dir＝new File(”C:\\java\\projects”)

如果当前目录已经是“c：\\”的话，也可以通过下面的语句创建—个和上面等价的 File 类：

File dir＝new File(”\\java\\projects”)

第二个构造器则是为一个文件创建一个 File 类。在 dir 参数中指定了目录，在 name 参数中指定了文件名。下面是一个例子。

File dir＝new File(”C:\\java\\projects”,”File.java”)

第三个构造器和第二个构造器类似，只是 dir 参数通道一个 File 对象来指定路径，而不是通过字符串。下面是一个例子：

File dir＝new File(”C:\\java\\projects”)

File file= new File(dir,”File.java”)

Java语言中通过File类来建立与磁盘文件的联系。File类用来获取或设置文件或目录的属性，但不支持从文件读取数据或者往文件里写数据。

例如判断是否有所要查找的文件：

1. import java.io.*;

2. class IOExample

3. {

4. public static void main(String[] args)

5. {

6. System.out.println("输入要查找的文件名或绝对路径: ");

7. char c;

8. StringBuffer buf = new StringBuffer();

9. try {

10. while ((c = (char)System.in.read()) != '\n')

11. buf.append(c);

12. //将输入的字符加到buf中，直到出现回车符

13. }

14. catch (IOException e)

15. {

16. System.out.println("Error:"+e.toString());

17. }

18. File file = new File(buf.toString().trim());

19. //创建File 类的file对象

20. if (file.exists())

21. { //如果文件在当前目录存在

22. System.out.println("文件名:"+file.getName());

23. //显示文件的文件名

24. System.out.println("在当前目录下的路径:"+file.getPath()); //显示文件的路径

25. System.out.println("绝对路径:"+file.getAbsolutePath());

26. //显示文件的绝对路径

27. System.out.println("是否可写:"+file.canWrite());

28. //显示文件是否可写

29. System.out.println("是否可读:"+file.canRead());

30. //显示文件是否可读

31. System.out.println("大小:"+(file.length()) +"B");

32. //显示文件的大小

33. }

34. else //如果文件在当前目录不存在

35. System.out.println("Sorry,file not found.");

36. //显示文件没有找到的提示信息

37. }

38. }

程序运行结果：

图 4-3 程序运行结果

## 文件的输入/输出

Java语言向文件中写数据或读出文件中的内容，是通过文件输入流、文件输出流来实现。FileInputStream类用来打开一个输入文件，FileOutputStream类用来打开一个输出文件。建立文件流格式如下：

FileInputStream in=new FileInputStream(fp);

FileOutputStream out=new FileOutputStream(fp);

输入流的参数是用于输入的文件名，输出流的参数是用于输出的文件名。

1.文件输入流(FileReader)

因为大多数程序会涉及文件读/写，FileReader类是一个经常用到的类，从FileReader类可以在一指定文件上实例化一个文件输入流，利用流提供的方法从文件中读取一个字符或者一组数据。

例：

1. import java.io.FileReader;

2. public class FileReaderExample

3. {

4. public static void main(String args[]) throws Exception

5. {

6. int size;

7. char cbuf[]=new char[256];

8. FileReader f1 = new FileReader("abc.html"); //创建输入流

9. size=f1.read(cbuf,0,256); //从输入流读取数据到数组

10. System.out.println("Total Available Bytes: " + size);

11. System.out.println("content of the file:");

12. System.out.println(cbuf); //打印数组

13. f1.close(); //关闭输入流

14. }

15. }

在使用此例时，要在和类文件同一目录下创建一个名为 abc 后缀为

. html 的文件。abc.html 文件如图：

图 4-4 测试 Web

程序运行结果：

图 4-5 程序运行结果

2.文件输出流(FileWriter)

由 FileWriter 类可以实例化一个文件输出流，并提供向文件中写入一个字符或者一组数据的方法。

FileWriter 也有两个和 FileReader 类似的构造方法。

如果用 FileWriter 来打开一个只读文件会产生 IOException 异常。

例：

1. import java.io.*;

2. class FileWriterExample

3. {

4. public static char getInput() [] throws Exception

5. {

6. char buffer[] = new char[12];

7. for (int i=0;i<12;i++)

8. buffer[i] = (char) System.in.read();

9. //从键盘读入12个字符到缓冲区

10. return buffer;

11. }

12. public static void main(String args[]) throws Exception

13. {

14. char buf[] = getInput();

15. Writer f0 = new FileWriter("file1.txt");

16. Writer f1 = new FileWriter("file2.txt");

17. Writer f2 = new FileWriter("file3.txt");

18. for (int i=0; i < 12; i+=2)

19. f0.write(buf[i]);

20. //将缓冲区中偶数位置上的字符保存到文件file1.txt中

21. f0.close();

22. f1.write(buf);

23. // 将缓冲区中的字符写入文件流f1

24. f1.close(); //关闭输出流f1

25. f2.write(buf,12/4,12/2);

26. //将缓冲区中从位置3开始的连续6个字符写入文件流f2

27. f2.close(); //关闭文件输出流f2

28. }

29. }

如果键盘输入为“abcdefghijkl”，运行结束后查看三个文件的内容，将会看到如下所示的结果：

file1.txt 的内容为：

acegik

file2.txt 的内容为：

abcdefghijkl

file3.txt 的内容为：

defghi

例：

1. import java.io.*;

2. public class Example

3. {

4. public static void main(String[] args) throws IOException

5. {

6. File inputFile = new File("read.txt");

7. // 建立一个名字为inputFile的File类对象

8. File outputFile = new File("write.txt");

9. // 建立一个名字为outputFile的File类对象

10. FileReader in = new FileReader(inputFile);

11. // 建立一个名字为in的字符流

12. FileWriter out = new FileWriter(outputFile);

13. // 建立一个名字为out 的字符流

14. int c;

15. while ((c = in.read()) != -1)

16. //从文件read.txt中读数据

17. out.write(c);

18. // 将读入的数据写到文件write.txt中

19. in.close(); // 关闭输入流in

20. out.close(); // 关闭输出流out

21. }

22. }

在使用此例时，要在当前目录下创建 read.txt 和 write.txt 两个文本文件。且 read.txt 中要有内容，write.txt 为空。

平常所使用的文件中，有很多是二进制文件，它们以字节作为数据处理单位。对这些文件就要使用字节流来读写了，其实字符文件也可以用字节流来进行读写。字节流的使用和构造方法与其对应的字符流的使用和构造方法类似，在此不再赘述。

与文件有关的字节流有两个：FileInputStream 和 FileOutputStream，分别与字符流的 FileReader 和 FileWriter 相对应。

例：

1. import java.io.*;

2. import java.util.*;

3. class FileInputStreamExample

4. {

5. public static void main(String args[]) throws Exception

6. {

7. int size;

8. FileInputStream f1 = new FileInputStream("abc.html");

9. size = f1.available(); //获取文件的可读字节数

10. System.out.println("Total Available Bytes: " + size);

11. System.out.println("First 1/4 of the file: read()");

12. for (int i=0;i<size/4;i++)

13. System.out.print((char) f1.read()); //获取文件的1/4的数据

14. System.out.println("Total Still Available: " + f1.available());

15. System.out.println("Reading the next 1/8: read(b[])");

16. byte b[] = new byte[size/8];

17. if (f1.read(b) != b.length)

18. System.err.println("Something bad happened");//判断读入的字节长度是否跟预计长度一致

19. String tmpstr = new String(b,0,0,b.length);

20. System.out.println(tmpstr);

21. System.out.println("Still Available: " + f1.available());

22. System.out.println("Skipping another 1/4: skip()");

23. f1.skip(size/4); //跳过文件1/4的数据

24. System.out.println("Still Available: " + f1.available());

25. System.out.println("Reading 1/16 into the end of array");

26. if (f1.read(b,b.length-size/16,size/16) != size/16)//判断读入的字节长度是否跟预计长度一致

27. {

28. System.err.println("Something bad happened");

29. }

30. System.out.println("Still Available: " + f1.available());

31. f1.close(); //关闭流

32. }

33. }

注意：在使用上面的例子时，要在类文件相同目录下创建一个名为abc后缀为.html的文件。

## 随机存取文件

目前，没有直接的方法可以将对象写入到随机存取文件中。但是可以使用 ByteArray 输入/输出流作为中介，来向随机存取文件中写入或从随机存取文件中读出字节，并且可以利用字节流来创建对象输入/输出流，以用于读写对象。需要注意的是在字节流中要包含一个完整的对象，否则读写对象时将发生错误。例如：java.io.ByteArrayOutputStream 可用于获取 ObjectOutputStream 的字节流，从中可得到 byte 数组并可将之写入到随机存取文件中。相反,可以从随机存取文件中读出字节数组，利用它可构造 ByteArrayInputStream，进而构造出 ObjectInputStream，以读取对象。

## 串行化

使用基本的文件读写方式存取数据，如果仅仅保存相同类型的数据，则可以用同一种格式保存，有时需要保存一个复合类型；相反，如果有多种不同类型的数据，要么把它分解成若干部分，以相同类型(如 String)保存，需要在程序中添加解析不同类型数据格式的逻辑。串行化操作为大家提供花尽可能少的时间和代码对数据进行解析的一条途径。

串行化可以把对象以某种特定的编码格式写入或从外部字节流(即 ObjectInputStream/ObjectOutputStream)中读取。串行化一个对象仅仅需要实现一下 Serializable 接口即可：

public class Example implements java.io.Serializable

{

...

}

但在要串行化的类中的每个属性都必须是可串行化的。其实所有基本类型都是可串行化的，可以看看 JDK 文档，会发现很多类其实已经实现了 Serializable，于是这些类的对象以及基本数据类型都可以直接作为需要串行化的那个类的内部属性。如果在要串行化的类中的属性不是可串行化的，那么这个类还需要实现 Serializable 接口，把所有属性都变为可串行化的。

ObjectOutputStream.writeObject() 将一个对象序列化到外部字节流。

ObjectInputStream.readObject() 从外部字节流读取并重新构造对象。

实际应用中，Serializable 接口并没有定义任何方法，仿佛它只是一个标记(或者说像是 Java 的关键字)而已，一旦虚拟机看到这个“标记”，就会尝试调用自身预定义的序列化机制，除非在实现 Serializable 接口的同时还定义了私有的 readObject()或 writeObject()方法。如果不愿意让系统使用缺省的方式进行序列化，那就必须定义上面提到的两个方法：

public class Example implements java.io.Serializable

{

private void writeObject(java.io.ObjectOutputStream out) throws IOException

{

...

}

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException

{

...

}

...

}

如可以在上面的 writeObject()里调用默认的序列化方法：ObjectOutputStream.defaultWriteObject()。

如不愿意将某些敏感的属性和信息序列化，也可以调用 ObjectOutputStream.writeObject()方法明确指定需要序列化那些属性。