IO流简介

IO即Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库、文件、远程主机)的过程即输出。数据传输过程类似于水流,因此称为IO流。IO流在Java中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。

JavaIO 流的40多个类都是从以下4个抽象类基类中派生出来的。

  • InputStream/Reader:所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。

字节流

InputStream(字节输入流)

InputStream用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream抽象类是所有字节输入流的父类。

InputStream常用方法:

  • read():返回输入流中下一个字节的数据。返回的值介于0到255之间。如果未读取任何字节,则代码返回-1,表示文件结束。
  • read(byte b[]):从输入流中读取一些字节存储到数组b中。如果数组b的长度为0,则不读取。如果没有可用字节读取,返回-1。如果有可用字节读取,则最多读取的字节数最多等于b.length,返回读取的字节数。这个方法等价于read(b,0,b.length)
  • read(byte b[], int off, int len):在read(byte b[])方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。
  • skip(long n):忽略输入流中的n个字节,返回实际忽略的字节数。
  • available():返回输入流中可以读取的字节数。
  • close():关闭输入流释放相关的系统资源。

从java9开始,InputStream新增加了多个实用的方法:

  • readAllBytes():读取输入流中的所有字节,返回字节数组。
  • readNBytes(byte[] b,int off,int len):阻塞直到读取len个字节。
  • transferTo(OutputStream out):将所有字节从一个输入流传递到另一个输出流。

像下面这段代码在我们的项目中就比较常见,我们通过readAllBytes()读取输入流所有字节并将其直接赋值给一个String对象。

1
2
3
4
//新建一个BufferedInputStream对象
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
//读取文件的内容并复制到String对象中
String result = new String(bufferedInputStream.readAllBytes());

ObjectInputStream用于从输入流中读取Java对象(反序列化),ObjectOutputStream用于将对象写入到输出流(序列化)。

1
2
3
ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data"));
MyClass object = (MyClass)input.readObject();
input.close

另外,用于序列化和反序列化的类必须实现Serializable接口,对象中如果有属性不想被序列化,使用transient修饰。

OutputStream(字节输出流)

OutputStream用于将数据(字节信息)写入到目的地(通常是文件),java.io.OutputStream抽象类是所有字节输出流的父类。

OutputStream常用方法:

  • write(int b):将特定字节写入输出流。
  • write(byte b[]):将数组b写入到输入流,等价于write(b,0,b.length)
  • write(byte[] b,int off,int len):在write(byte b[])方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。
  • flush():刷新此输出流并强制写出所有缓冲的输出字节
  • close():关闭输出流释放相关的系统资源。

FileOutputStream是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。

FileOutputStream通常也会配合BufferedOutputStream来使用。

1
2
FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream)

OutputStream用于从输入流中读取Java对象(ObjectInputStream,反序列化)或者将对象写入到输出流(ObjectOutputStream,序列化)。

1
2
3
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("file.txt"))
Person person = new Person("zhangsan","Hello");
output.writeObject(person);

字符流

不管是文件读写还是网络发送接受,信息的最小存储单元都是字节。那为什么I/O流操作要分为字节流操作和字符流操作呢?

个人认为主要有两点原因:

  • 字符流是由Java虚拟机将字节转换得到的,这个过程比较耗时。
  • 如果我们不知道编码类型很容易出现乱码问题。

如果是音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

字符流默认采用的Unicode编码,我们可以通过构造方法自定义编码。

常用字符编码所占字节数?utf8:英文占1字节,中文占3字节,unicode:任何字符都占2个字节,gbk:英文占1字节,中文占2字节。

Reader(字符输入流)

Reader用于从源头(通常是文件)读取数据(字符信息)到内存中,java.io.Reader抽象类是所有字符输入流的父类。

Reader用于读取文件,InputStream用于读取原始字节。

Reader常用方法:

  • read():从输入流读取一个字符。
  • read(char[] cbuf):从输入流中读取一些字符。并将他们存储到字符数组cbuf中,等价于read(cbuf,0,cbuf.length)
  • read(char[] cbuf,int off,int len):在read(char[] cbuf)方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。
  • skip(long n):忽略输入流中的n个字符,返回实际忽略的字符数。
  • close():关闭输入流并释放相关的系统资源。

InputStreamReader是字节流展缓为字符流的桥梁,其子类FilterReader是基于该基础上的封装,可以直接操作字符文件。

1
2
3
4
5
6
// 字节流转换为字符流的桥梁
public class InputStreamReader extends Reader {
}
// 用于读取字符文件
public class FileReader extends InputStreamReader {
}

FileReader代码示例:

1
2
3
4
5
6
7
8
9
10
11
try (FileReader fileReader = new FileReader("input.txt");) {
int content;
long skip = fileReader.skip(3);
System.out.println("The actual number of bytes skipped:" + skip);
System.out.print("The content read from file:");
while ((content = fileReader.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}

Writer(字符输出流)

Writer用于将数据(字符信息)写入到目的地(通常是文件),java.io.Writer抽象类是是所有字节输出流的父类。

Writer常用方法:

  • write(int c):写入单个字符。
  • write(char[] cbuf):写入字符数组cbuf,等价于write(cbuf,0,cbuf.length)
  • write(char[] cbuf,int off,int len):在write(char[] cbuf)方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。
  • write(String str):写入字符串,等价于write(str,0,str.length())
  • write(String str,int off,int len):在write(String str)方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。
  • append(CharSequence csq):将指定的字符序列附加到指定的Write对象并返回该Write对象。
  • append(char c):将指定的字符附加到指定的Writer对象并返回Writer对象。
  • flush():刷新此输出流并强制写出所有缓冲的输出字符。
  • close():关闭输出流释放相关的系统资源。

OutputStreamWriter是字符流转换为字节流的桥梁,其子类FileWriter是基于该基础上的封装,可以直接将字符写入到文件。

1
2
3
4
5
6
// 字符流转换为字节流的桥梁
public class InputStreamReader extends Reader {
}
// 用于写入字符到文件
public class FileWriter extends OutputStreamWriter {
}

FilterWriter代码示例:

1
2
3
4
5
try (Writer output = new FileWriter("output.txt")) {
output.write("你好,我是Guide。");
} catch (IOException e) {
e.printStackTrace();
}

字节缓冲流

IO操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的Iowa操作,提高流的传输效率。

字节缓冲流这里采用了装饰器模式来增强InputStreamOutputStream子类对象的功能。

举个例子,我们可以通过BufferedInputStream(字节缓冲输入流)来增强FileInputStream的功能。

1
2
// 新建一个 BufferedInputStream 对象
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));

字节流和字节缓冲流的性能差别主要体现在我们使用两者的时候都是调用write(int b)read()这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓冲区,大幅减少IO次数,提高读取效率。

BufferedInputStream(字节缓冲输入流)

BufferedInputStream从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了IO次数,提高了读取效率。

BufferedInputStream内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组。

BufferedOutputStream(字节缓冲输出流)

BufferedOutputStream将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了IO次数,提高了读取效率。

类似于BufferedInputStreamBufferedOutputStream内部有维护了一个缓冲区,并且,这个缓存区的大小也是8192字节。

字符缓冲流

BufferedReader(字符缓冲输入流)和BufferedWriter(字符缓冲输出流)类似于BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。

打印流

System.out实际是用于获取一个PrintStream对象,print方法实际调用的是PrintStream对象的write方法。

PrintStream属于字节打印流,与之对应的是PrintWriiter(字符打印流)。PrintStreamOutputStream的子类,PrintWriterWriter的子类。