Java IO 流详解

概述

流是一个抽象的概念,代表了数据的无结构化传递。流的本质是数据在不同设备之间的传输。在 Java 中,数据的读取和写入都是以流的方式进行的

在 Java 中,根据数据流向的不同,可以将流分为输入(Input)流和输出(Output)流。根据单位的不同,可以将流分为字节流和字符流。根据等级的不同,可以将流分为节点流和处理流

输入流和输出流

输入流用于将数据从控制台、文件、网络等外部设备输入应用程序进程中

输出流用于将应用程序进程中的数据输出到控制台、文件、显示器等中

字节流和字符流

字节流:字节流是以字节(1byte=8bit)为单位对数据进行读写操作的,也就是说,字节流进行一次读取或者写入都是以 8bit 为单位进行的,因此主要用于处理二进制据。在 Java 中使用 InputStream、OutputStream 处理字节数据,其中 InputStream 用于字节流输入,OutputStream 用于字节流输出

字符流:字符流以字符为单位对数据进行读写操作,一次读取或写入都是以 16bit 为单位进行的。Java 中的字符采用 Unicode 编码,一个字符占用 2 字节。字符流主要用于处理文本数据的读写,在处理过程中需要进行字符集的转化。在 Java 中使用 Reader、Writer 处理字符数据,其中 Reader 用于字符流输入,Writer 用于字符流输出

InputStream 字节输入流是一个抽象类,其子类包括:

  • FileInputStream(文件输入流)
  • ObjectInputStream(对象输入流)
  • ByteArrayInputStream(字节数组输入流)
  • PipedInputStream(管道输入流)
  • FilterInputStream(过滤器输入流)
    • BufferedInputStream(缓冲输入流)
    • PushbackInputStream(回牙输入流)
    • DataInputStream(数据输入流)
  • SequenceInputStream(顺序输人流)
  • StringBufferedInputStream(缓冲字符串流)

InputStream 类的所有方法在遇到错误时都会抛出 IOExcepiion 异常。InputStream 用于以字节形式将数据读入应用程序中,常用的方法及其作用如表所示

方法 作用
int read() 从输入流读取8字节数据并将其转换成一个0-255的整数,返回值为读取的总字节数,遇到数据流的末尾则返回-1
int read(byte[] b) 从输入流中读取最大长度为len字节的数据并保存到b字节数组中,遇到数据流的末尾则返回-1
int read(byte[] b, int off, int len) 以输入流中的off位置为开始位置读取最大长度为len字节的数据,并将其保存到b字节数组中
void close() 关闭输入流
int available() 返回可以从输入流中读取的位数
skip(long n) 从输入流跳过n字节

一段基于 FileInputStream 读取文件的代码如下:

java 复制代码
public static void main(string[] args) throws IoException {
  String path = "file_dir/";
  String fileName = "File-Test.txt";
  // 1:定义待读取的文件
  File file = new File(path, fileName);
  // 2:从文件中读取数据到 FileInputStream
  FileInputStream fileInputStream = new FileInputStream(file);
  byte[] bytes = new byte[fileInputstream.available()];
  int n = 0;
  // 3:从FileInputstream中不断循环读取字节数据并写入bytes,直到遇到数据流结尾时
  while ((n = fileInputstream.read(bytes)) != -1) {
    // 4:将byte[]转化为字符串
    String s = new String(bytes);
    System.out.printIn(s);
  }
  // 5:关闭输入文件流
  fileInputStream.close();
} 

OutputStream 字节输出流是一个抽象类,其子类包括:

  • FileOutputStream(文件输出流)
  • ByteArrayOutputStream(字节数组输出流)
  • FilterOutputStream(过滤器输出流)
    • BufferedOutputStream(缓冲输出流)
    • DataOutputStream(数据输出流)
    • PrintOutputStream(打印输出流)
  • ObjectOutputStream(对象输出流)
  • PipedOutputStream(管道输出流)

OutputStream 类的所有方法在遇到错误时都会抛出 IOException 异常。OutputStream 用于以字节形式将数据输出到目标设备,常用的方法及其作用如表所示

方法 作用
int write() 将指定字节的数据写入输出流
int write(byte[] b) 将指定字节数组的内容写入输出流
int write(byte[] b, int off, int len) 将指定的字节数组从off位置开始的len字节的内容写入输出流
close() 关闭数据流
flush() 刷新输出流,强行将缓冲区的内容写入输出流

基于 FileOutputStream 读取文件的一段代码如下

java 复制代码
public static void main(String[] args) throws IOException {
  String path = "file_dir/";
  String fileName = "File-Test.txt";
  // 1:定义待写入的文件
  File file = new File(path, fileName);
  // 2:定义FileOutputStream
  FileOutputStream fileOutputStream = new FileOutputStream(file, false);
  // 3:将数据写入FileOutputStream
  fileOutputStream.write("hello FileOutputStream new " .getBytes());
  // 4:关闭FileOutputStream
  fileOutputStream.close();
}

Reader 类是所有字符流输入类的父类,用于以字符形式将数据读取到应用程序中,其子类包括:

  • CharArrayReader:将字符数组转换为字符输入流并从中读取字符
  • StringReader:将字符串转换为字符输入流并从中读取字符
  • BufferedReader:为其他字符输入流提供读缓冲区
  • PipedReader:连接到一个 PipedWriter
  • FilterReader:Reader 类的子类,用于丰富 Reader 类的功能
  • InputStreamReader:将字节输入流转换为字符输入流,可以指定字符编码

Reader 类常用方法如下

方法 作用
int read() 从输入流中读取一个字符并转化为 0-65535 的整数,当读取到流的末尾时返回-1
int read(char[] buf) 从输入流中读取若干个字符并保存到参数buf指定的字符数组中,当读取到流的末尾时返回-1
int read(char[] buf, int off, int len) 以输入流中的off位置为开始位置读取最大长度为len字节的数据并将其保存到buf字符数组中,当读取到流的末尾时返回-1

基于 BufferedReader 读取文件的一段代码如下:

java 复制代码
public static void main(string[] args) throws Exception {
  String path = "file_dir.mov";
  //1:创建FileReader
  FileReader fileReader = new FileReader(path);
  //2:基于FileReader创建BufferedReader
  BufferedReader bufferedReader = new BufferedReader(fileReader);
  //3:定义一个strLine,表示BufferedReader读取的结果
  String strLine = "";
  //4:调用readLine方法将缓冲区中的数据读取为字符串
  //当readLine返回-1时,表示已经读取到文件末尾了
  while((strLine = bufferedReader.readLine()) != null) {
    System.out.println(strLine);
  }
  //5:关团fileReader
  fileReader.close();
  //6:关闭bufferedReader
  bufferedReader.close();
}

Writer 类是所有字符流输出类的父类,用于以字符形式将数据写出到外部设备,其子类包括:

  • CharArrayWriter:用于向内存缓冲区的字符数组写数据
  • StringWriter:用于向内存缓冲区的字符串(StringBuffer)写数据
  • BufferedWriter:用于为其他字符输出流提供写缓冲区
  • PipedWriter:用于连接到一个 PipedReader
  • OutputStreamReader:用于将字节输出流转换为字符输出流,可以指定字符编码
  • FilterWriter:过滤器字符输出流

Writer 类常用方法如下

方法 作用
void write(int c) 向输出流中写入一个字符
void write(char[] cbuf) 将字符数组cbuf中的字符写入输出流中
void write(char[] cbuf,int off, int len) 将字符数组cbuf中从off位置开始获取长度为len的字符并写入输出流中
void write(String str) 将字符串写入输出流
void write(String str,int off, int len) 将字符串中的部分字符写入输出流
append(char c) 将字符c追加到输出流
append(charSequence csq) 将参数csq指定的字符序列追加到输出流
append(charSequence csq, int start, int end) 将参数csq指定的字符序列的子序列追加到输出流

基于 BufferedWriter 将字符串写入文件中的一段代码如下:

java 复制代码
public static void main(String[] args) throws Exception {
  //1:定义一个FileWriter
  String path = "File-Test.txt";
  FileWriter writer = new FileWriter(path);
  //2:基于FileWriter定义一个BufferWriter
  BufferedWriter bufferedWriter = new BufferedWriter(writer)
  //3:调用BufferedWriter的write方法将字符串写入BufferedWriter
  bufferedWriter.write("write by str");
  //4:关闭BufferedWriter
  bufferedWriter.close();
  //5:关闭FileWriter
  writer.close();
}

节点流和处理流

节点流是低级流,直接与数据源相接,对数据源上的流进行读写。

处理流是高级流,采用修饰器模式对节点流进行了封装,不直接与数据源相连,主用于消除不同节点流的实现差异,提供更方便的方法来完成数据的输入和输出。

例如,FileInputStream、FileOutputStream、FileReader、FileWriter 属于节点流;BufferInputStream、BufferOutputStream、BufferReader、BufferWriter 属于处理流。

相对于节点流,处理流有如下特性:

  • 性能高:处理流通过增加缓存的方式提高数据的输入和输出效率。
  • 操作方便:处理流封装了一系列高级方法来完成一次性大批量数据的输入和输出。

内存映射文件技术

操作系统可以利用虚拟内存实现将一个文件或者文件的一部分"映射"到内存中。然后,这个文件就可被当作内存数据来访问,比传统的文件要快得多,这种技术就是内存映射文件技术。

内存映射文件技术的一个关键优势是操作系统负责真正的文件读写,应用程序只需处理内存数据,就可以实现非常快速的 IO 操作。在写入过程中,即使应用程序在将数据写人内存后进程出错退出,操作系统仍然会将内存映射文件中的数据写入文件系统。另一个更突出的优势是共享内存,即内存映射文件可被多个进程同时访问,起到低时延共享内存的作用。

Java 中的 java.nio 包支持内存映射文件,具体使用方式是通过 MappedByteBuffer 读写内存,而且内存映射文件技术涉及的内存在 Java 的堆空间之外,这也是其效率高的一个原因。

在 Java 中将一个文件映射到内存并操作共分为如下三步:

从文件中获得一个通道(channel)

java 复制代码
RandomAccessFile raf = new RandomAccessFile(filePath, "rw");
FileChannel fc= raf.getChannel();

调用 FileChannel 的 map 方法将文件映射到虚拟内存

java 复制代码
MappedByteBuffer buffer = channel.map(mode, 0, length);

mode 参数用于指定映射模式,支持的模式有如下三种:

  • FileChannel.MapMode.READ_ONLY:所产生的缓冲区是只读的
  • FileChannel.MapMode.READ_WRITE:所产生的缓冲区是可写的,任何修改在某个时刻写回到文件中。注意,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的最终行为是依赖于操作系统的
  • FileChanncl.MapMode.PRIVATE:所产生的缓冲区是可写的,但任何修改对该缓冲区来说都是私有的,不会传播到文件中

调用 MappedByteBuffer 的 put(byte[] src) 向内存映射文件中写入数据,调用 get(int index) 获取文件中对应索引的数据,以字节形式返回

java 复制代码
public static void main(String[] args) throws Exception {
  //1:定义文件流
  String path = "file_path/File-Test.txt";
  RandomAccessFile raf = new RandomAccessFile(path,"rw");
  //2:获取FileChannel
  FileChannel fc = raf.getChannel();
  //3:定义MappedByteBuffer
  int start = 0:
  int len = 1024;
  //调用map函数的过程其实就是磁盘文件到内存数据的映射过程
  //对Filechannel调用map函数后,应用程序可以像使用内存一样使用该文件
  MappedByteBuffer mbb = fc.map(FileChannel.MapMode.PRIVATE, start, len);
  //4:进行MappedByteBuffer数据的输入,分别在内存映射文件中写入如下字符串
  mbb.put("12345".getBytes());
  mbb.put("6789".getBytes());
  mbb,put("wanglei".getBytes());
  //读取第9个字符,结果为"w"
  System.out.println((char)mbb.get(9));
  //5:MappedByteBuffer数据的读取:读取所有数据
  for (int i = start; i < mbb.position(); i++) {
    System.out.println((char)mbb.get(i));
  }
}