Java基础教程(25)--I/O流

  I/O流表示输入源或输出目标。流可以表示许多不同类型的源和目标,例如磁盘文件、设备、其他程序等。
  流支持许多不同类型的数据,包括字节、原始数据类型、字符和对象等。有些流只传递数据; 有些流则可以操纵和转换数据。
  无论各种流的内部是如何工作的,所有流都提供相同的简单模型:流是一系列数据。程序使用输入流从源头获取数据,一次一项:

  程序使用输出流将数据写入目的地,一次一项:

  在本文中,我们会看到流可以处理各种各样的数据,无论是基本数据还是复杂对象。先来几张IO流的全家福:
  InputStream家族:

  OutputStream家族:

  Reader家族:

  Writer家族:

  点击此处可查看完整大图。

一.字节流

  一个字节(byte)代表8个二进制位(bit)。字节流,顾名思义,它所传递和操作的最小单位是字节。所有的字节流类都是抽象类InputStream和OutputStream的子类。
  I/O体系中有许多字节流类。为了演示字节流如何工作,我们选择了文件I/O字节流——FileInputStream和FileOutputStream。其他字节流类的使用方式都大致相同,不同之处主要在于它们的构造方式。
  下面的程序使用字节流将src.txt中的文本复制到dest.txt中,每次一个字节:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
    public static void main(String[] args) throws IOException {
        FileInputStream in = null;
        FileOutputStream out = null;
        try {
            in = new FileInputStream("src.txt");
            out = new FileOutputStream("dest.txt");
            int c;
            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

  在while循环中,程序每次从输入流in中读取一个字节,然后再将这个字节写入输出流out中,直到输入流中的字节全部被读取完。
  在不再需要流时关闭流是非常重要的。上面的程序中使用了finally块来保证无论是否发生错误都要关闭流。
  虽然上面的程序看起来很正常,但是实际上我们应该避免使用这种低级的,或者说底层的I/O。由于src.txt文件中存储的是字符数据,因此我们应该使用字符流,我们马上会在下一节中见到它。字节流应该仅用于最原始的I/O。那么为什么要谈论字节流呢?因为所有其他流类型都是基于字节流构建的。

二.字符流

  所有的字符流类都是Reader和Writer的子类。为了演示字符流如何工作,和上一节一样,这里我们选择文件I/O字符流——FileReader和FileWriter。
  下面的程序使用字符流将src.txt中的文本复制到dest.txt中,每次一个char(注意,这里不是每次一个字符,因为有些字符无法使用一个char类型来表示,具体可以参考我之前的文章《Java基础教程(5)--变量》中关于char类型的介绍):

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopyCharacters {
    public static void main(String[] args) throws IOException {
        FileReader inputStream = null;
        FileWriter outputStream = null;
        try {
            inputStream = new FileReader("src.txt");
            outputStream = new FileWriter("dest.txt");
            int c;
            while ((c = inputStream.read()) != -1) {
                outputStream.write(c);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

  字符流通常是字节流的“包装器”。字符流使用字节流来执行物理I/O,而字符流则负责处理字符和字节之间的转换。FileReader内部使用FileInputStream来读取数据,而FileWriter则使用FileOutputStream来写入数据。
  FileReader和FileWriter是专门用于文件读写的字符流。如果需要从其他的源读取字符,或者需要向其他目标写入字符,可以使用InputStreamReader和OutputStreamWriter来定制自己的流。这两个类只是简单的从输入源读取字符和向输出目标写入字符,我们可以使用它们自定义输入源和输出目标。
  FileReader和FileWriter所处理的最小单位是char类型。实际上,还可以每次处理一行字符。行是指一串字符与末尾的行终止符。现在我们修改上面的程序,来让我们的程序每次处理一行字符。这里会使用到两个新的类——BufferedReader和PrintWriter,我们会在下一节更深入地讨论这些类:

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;

public class CopyLines {
    public static void main(String[] args) throws IOException {
        BufferedReader inputStream = null;
        PrintWriter outputStream = null;
        try {
            inputStream = new BufferedReader(new FileReader("src.txt"));
            outputStream = new PrintWriter(new FileWriter("dest.txt"));
            String l;
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

三.缓冲流

  到目前为止,我们上面的例子使用的都是无缓冲的I/O。这意味着每次读取或写入请求都由底层操作系统直接处理。这样会使得程序的效率低很多,因为每个请求都会触发磁盘访问、网络请求或其他代价相对较高的操作。
  为了减少这种开销,Java平台实现了使用缓冲的I/O流。缓冲输入流从称为缓冲区的存储区读取数据,仅当缓冲区为空时才会重新获取数据。类似地,缓冲输出流将数据写入缓冲区,并且仅在缓冲区已满时才将数据写入输出目标。
  下面的两个例子将无缓冲的流转换为缓冲流:

inputStream = new BufferedReader(new FileReader("src.txt"));
outputStream = new BufferedWriter(new FileWriter("dest.txt"));

  有四个缓冲流类用于包装无缓冲流:BufferedInputStream与BufferedOutputStream创建缓冲字节流,而 BufferedReader与BufferedWriter创建缓冲字符流。
  有时候我们需要再缓冲区未填充满的时候就将它写出缓冲区,此时就需要刷新缓冲区。一些缓冲输出流支持自动刷新,这个行为由可选的构造方法参数指定。在启用自动刷新时,某些关键事件会导致刷新缓冲区。例如,PrintWriter会在每次调用println和format的时候刷新缓冲区。如果要手动刷新缓冲区,可以调用该输出流的flush方法。

四.格式化

  通过使用具有格式化功能的流可以将数据转换为格式标准、易于阅读的形式。提供格式化功能的流是是字符流类PrintWriter和字节流类PrintStream。
  注意,唯一需要使用PrintStream的地方是System.out和System.err(具体内容见下一小节)。当你需要格式化输出流,应该使用PrintWriter而不是PrintStream。
  正如其他输出流一样,PrintStream和PrintWriter为简单的字节或字符输出提供了一组write方法。除此之外,它们还提供了两种格式化方法来对输出内容进行格式化:

  • print和println——以标准方式格式化单个值。
  • format——基于格式化字符串来对任意数量的值进行格式化。

print和println方法

  print和println用于打印单个变量的值,如果是对象则会打印对该对象调用toString方法后返回的字符串,它们唯一的区别是println会在每次调用后换行,而print则不会。下面是一个使用print和println的例子:

public class Root {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);
        
        System.out.print("The square root of ");
        System.out.print(i);
        System.out.print(" is ");
        System.out.print(r);
        System.out.println(".");

        i = 5;
        r = Math.sqrt(i);
        System.out.println("The square root of " + i + " is " + r + ".");
    }
}

  该程序将会输出:

The square root of 2 is 1.4142135623730951.
The square root of 5 is 2.23606797749979.

format方法

  format方法使用指定的格式字符串对多个参数进行格式化。格式字符串是指嵌入格式说明符的字符串。格式字符串支持非常多的格式,本文中只会介绍一些基础知识。有关完整说明请参考官方提供的格式字符串语法
  下面的例子调用了一次format方法,但同时格式化了两个值:

public class Root2 {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);
        System.out.format("The square root of %d is %f.%n", i, r);
    }
}

  下面是输出:

The square root of 2 is 1.414214.

  上面的例子中,%d、%f和%n是三个格式说明符。所有的格式说明符都以一个%开头,并以一个或两个字符结束。这里使用的三个格式说明符是:

  • %d——将整数值转换为十进制数。
  • %f——将浮点数值转换为十进制数。
  • %n——表示基于当前平台的行结束符。

  还有很多格式说明符,由于篇幅所限,再加上本文的重点是I/O流,因此这里就不再列举其他格式说明符和其他格式化的细节,感兴趣的读者可以自行查阅官方文档。

五.标准流

  标准流是许多操作系统的特性。默认情况下,它们从键盘读取输入并将输出写入显示器。它们还支持文件I/O以及程序之间的I/O,但该功能由命令行解释器控制,而不是程序。
  Java平台支持三种标准流:标准输入,通过System.in访问;标准输出,通过System.out访问;标准错误,通过System.err访问。这些流是自动定义的并且不需要打开。标准输出和标准错误均用于输出。
  你可能觉得标准流失字符流,但由于历史原因,他们实际上是字节流。System.out和System.err都是PrintStream类型。虽然从技术上讲她们是字节流,但是PrintStream内部利用字符流对象来模拟字符流的许多功能。
  相比之下,System.in是一个没有字符流功能的字节流。如果要使用标准输入作为字符流,可以使用InputStreamReader或Scanner对它进行包装。

六.数据流

  文本格式是易于人类阅读的,因此使用起来很方便。但是它并不像以二进制格式传递数据那样高效。下面我们将会学习如何用二进制数据来完成输入和输出。
  DataOutput接口定义了一组用于以二进制格式写各种类型的值的方法。例如,writeInt总是将一个整数写出为4字节的二进制整数,writeDouble总是将一个Double值写出为8字节的二进制浮点数。这样产生的结果并非人类可阅读的,但是对于给定类型的每个值,所需的空间都是相同的,而且将其读回也比解析文本要更快。
  同理,为了读取二进制数据,可以使用在DataInput接口中定义的一组方法。DataInputStream类实现了DataInput接口。为了读入二进制数据,可以将DataInputStream与某个字节流相结合,例如:

DataInputStream in = new DataInputStream(new FileInputStream("a.dat"));

  类似的,如果要写出二进制数据,可以使用实现了DataOutput接口的DataOutputStream类:

DataOutputStream out = new DataOutputStream(new FileOutputStream("a.dat"));

七.对象流

  就像数据流支持基本数据类型的I/O,对象流支持对象的I/O。Java支持一种称为对象序列化的机制,它可以将对象写出到对象输出流中,也可以从对象输入流中将对象读入。这样我们就可以将对象通过文件、网络等环境来传递并随时将其恢复。所有需要在对象输出流中存储或从对象输入流中恢复的类都必须实现Serializable接口,这只是一个标记接口,它没有定义任何方法。
  Java中提供的对象输入流和输出流分别是ObjectInputStream和ObjectOutputStream。通过ObjectOutputStream的writeObject方法可以将一个对象写入到输出流中,通过ObjectInputStream的readObject方法则可以从输出流中读取一个对象。
  对于一些所有域都是基本数据类型的对象来说,对其进行序列化是很简单的。但是对于某些域是引用类型的对象来说,在对这些对象进行序列化时,还要对其引用的对象也进行序列化,并且这些引用的对象内部可能还含有对其他对象的引用。在这种情况下,writeObject方法将会遍历该对象内部所有的引用,并将这些对象写入流。
  下图演示了这种情况。调用writeObject方法将a对象写入流,但该对象包含了对对象b和c的引用,而b包含对d和e的引用。在将a写入输出流时,会同时写入其他四个对象。当读回a时,也会读回其他四个对象,并保留原有的引用关系。

  您可能想知道如果将一个对象向同一个流中写入两次会发生什么。当他们读回时,还会引用同一个对象吗?答案是肯定的。无论写入多少次,流只会包含一个对象的副本。因此,如果明确地将对象写入流两次,那么实际上只写入了两次引用。例如,下面的代码将对象ob写入两次:

Object ob = new Object();
out.writeObject(ob);
out.writeObject(ob);

  每次调用readObject方法都会读回一个ob对象:

Object ob1 = in.readObject();
Object ob2 = in.readObject();
System.out.println(ob1 == ob2);

  上面的程序会输出true,因为ob1和ob2引用了同一个对象。
  但是,如果将一个对象写入两个不同的流,那么读回的将是两个不同的对象。

猜你喜欢

转载自www.cnblogs.com/maconn/p/10851198.html