Lundrea Lundrea's技术博客, 专注于后端与基础架构!

Java IO知识总结

2023-12-06
Lundrea

IO也就是Input/Output ,数据拿到计算机内存中的过程即为输入,反之,数据从内存输出到外部存储(可以是远程主机、磁盘、数据库等)的过程即为输出。数据传输过程类似于水流,因此称作IO流。IO流在Java中分为输出流和输入流,根据数据的处理方式又分为字节流和字符流。(这里的输入输出是以程序为中心的,输入指程序接收输入,输出指程序把数据输出到外部存储)

Java IO流

Java IO流有四个基类,分别是输入流InputStream(字节输入流),Reader(字符输入流),OutputStream(字节输出流),Writer(字符输出流),其余的IO相关类都是派生于这四个抽象基类。

字节流与字符流

字节流:

  1. 以字节为单位处理数据,适用于处理二进制数据
  2. 直接操作字节,不涉及编码转换,可以处理任何类型的数据

字符流:

  1. 以字符为单位处理数据,适合处理文本数据
  2. 自动处理字符编码和解码(将字节传为字符)
  3. 性能逊于字节流处理,因为还有编解码消耗
  4. 对于不知道编码类型的数据,使用字节流处理会带来乱码问题,而使用字符流就不会出现这样的问题

字节流

InputStream

InputStream用于从源读取字节流到内存中,它是一个抽象类,是所有字节输入流的父类。

常用方法

  1. read():返回输入流中下一个字节的数据,如果未读取任何字节,返回-1,表示结束
  2. read(byte b[]):从输入流中读取一些字节放到字节数组b中,如果数组b的长度为0,则不读取,如果没有可以读取的字节,返回-1。最多可以读b.length个字节,返回读取的字节数
  3. read(byte b[], int off, int len):off是偏移量,len是指定的要读取的最大字节数,其余与read(byte b[])一致(这里的偏移量off是针对字节数组b的,加入偏移为2,则从b的第3个下标开始填充)
  4. skip(long n):忽略输入流中的n个字节,返回实际忽略的字节数
  5. avaliable():返回输入流中可以读取的字节数
  6. close():关闭输入流并释放资源
  7. readAllBytes():读取输入流中的所有字节,返回字节数组
  8. readNBytes(byte[] b, int off, int len):阻塞直到读取len个字节
  9. transferTo(OutputStream out):将所有字节流从一个输入流传递到一个输出流,输出流自动写入

使用的输入文件为text.txt:

hello,world!
sjska
12345678910
qpwoeiruty
String filePath = "text.txt";
FileInputStream file = new FileInputStream(filePath);
System.out.println(file.available());
// 输出输入流中可以读取的字节数 44

byte[] bytes = new byte[10];
file.read(bytes);
for (byte b : bytes){
    System.out.println(b);
    System.out.println(Character.toChars(b));
}
// 输出bytes数组的内容,依次输出 104 h 101 e 108 l 108 l 111 o 44 , 119 w 111 o 114 r 108 l

int read = file.read();
System.out.println(Character.toChars(read));
// 读取了一个字节,输出 d

byte[] bytes1 = new byte[7];
file.read(bytes1, 2,5);
for(byte b : bytes1){
    System.out.println(b);
    System.out.println(Character.toChars(b));
}
// 读取5个字节,并且偏移量为2,输出bytes1数组的内容,依次输出
// 0 0 33 ! 13 10 115 s 106 j
// bytes1数组的前两个字节偏移了,为空,13和10分别表示换行符和回车符

// 跳过4个字节
file.skip(4);
file.read(bytes);
for(byte b : bytes){
    System.out.println(b);
    System.out.println(Character.toChars(b));
}
// 再读取10个字节存在bytes中,其内容为
// 10 49 1 50 2 51 3 52 4 53 5 54 6 55 7 56 8 57 9
// 第一个10是回车符,它之前的换行符已经被skip跳过了
// 之后的1~9为存储的文本,49~57为其对应的ASCII码

String outFilePath = "text1.txt"
OutputStream fileOut = new FileOutputStream("outFilePath");

// 将输入流file中的字节全部放入输出流fileOut
file.transferTo(fileOut);

// 如果没有执行transferTo方法,这里读取输入流中剩余全部字符放在返回的字符数组中
// 但是执行了transferTo,输入流中已经没有字节了,所以什么都没读到
byte[] bytes2 = file.readAllBytes();
for(byte b : bytes2){
    System.out.println(b);
    System.out.println(Character.toChars(b));
}

输出文件text1.txt:

10
qpwoeiruty

OutputStream

字节输出流,将字节输出到指定地方(文件等地),OutputStream是所有字节输出流的父类。

常用方法

  1. write(int b):将特定字节写入到输出流
  2. write(byte b[]):将字节数组b写入到输出流
  3. write(byte b[], int off, int len):增加了off偏移量以及len(要写入的最大字节数),与字节输入流相同,这里的off也是对于字节数组b来说
  4. flush():刷新此输出流并强制写出所有缓冲的输出字节
  5. close():关闭输出流并释放资源

FileOutputStream是使用最多的字节输出流对象,用于将字节写入到文件中,当调用write方法的时候,首先将数据写入到FileOutputStream的内存缓冲区,当缓冲区满、手动调用flush方法、手动调用close方法(其实也是触发了flush方法的调用)、程序退出触发close方法时,才会把数据写入到文件中。

String filePath = "test.txt";
FileOutputStream fis = new FileOutputStream(filePath);
// 将 Z 写入到输出流
fis.write(90);
// 往输出流写入换行符
fis.write(10);
// 网输出流写入回车
fis.write(13);
// 往输出流写入 hello,world!
byte[] bytes = new String("hello,world!").getBytes();
fis.write(bytes);
fis.write(10);
// 偏移量为3,网输出流写入字节数组
fis.write(bytes,3,9);

得到的输出文件`test.txt”:

Z

hello,world!
lo,world!

字符流

基于字节流的IO若不知道编码方式就容易出现乱码问题,字符流对象方便我们对字符进行流操作,对于音频、视频、图片等媒体文件建议使用字节流进行处理,而对于文本文件建议使用字符流进行处理。

字符流默认采用的编码方式是Unicode编码。

Reader

Reader用于从文件读取字符流到内存,它是所有字符输入流的父类。

常用方法

  1. read():从输入流读取一个字符
  2. read(char[] cbuf):用于从输入流读取字符到字符数组cbuf
  3. read(char[] cbuf, int off, int len):用于从输入流读取字符到字符数组cbuf中,并增加了偏移量off以及读取的字符数量len
  4. skip(long n):忽略输入流中的n个字符,返回实际忽略的字符数量
  5. close():关闭输入流并释放资源

FileReader是使用较多的字符输入流。

// 文件使用上面的 test.txt
String filePath = "test.txt";
FileReader fr = new FileReader(filePath);

// 读取一个字符,输出90,也就是Z
System.out.println(fr.read());
char [] cBuf = new char[10];

// 读取一系列字符串到字符数组中
// 依次输出 换行符 回车符 h e l l o , w o
fr.read(cBuf);
for(char c : cBuf){
    System.out.println(c);
}

// 跳过一个字符
fr.skip(1);
char [] cBuf1 = new char[6];

// 读取4个字符串到字符数组中,偏移量为2
// 输出 空 空 l d ! 换行符
fr.read(cBuf1,2,4);
for(char c : cBuf1){
    System.out.println(c);
}

Writer

Writer用于将字符输出到文件中,它是所有字符输出流的父类。

常用方法

  1. write(int c):向输出流写入单个字符
  2. write(char[] cbuf):向输出流写入字符数组
  3. write(char[] cbuf, int off, int len):向输出流写入字符数组,并包含偏移量off以及字符数量len
  4. write(String str):向输出流写入字符串
  5. write(String str, int off, int len):向输出流写入字符串,并且包含偏移量off以及字符数量len
  6. append(CharSequence csq):将指定的字符序列csp附加到指定的Writer对象并返回该Writer对象
  7. append(char c):将指定的字符附加到指定的Writer对象并返回该Writer对象
  8. flush():刷新该输出流,强制输出所有缓冲的输出字符
  9. close():关闭输出流并释放资源
String filePath = "simple.txt";
FileWriter fw = new FileWriter(filePath);

// 向输出流写入字母Z
fw.write(90);

// 写入换行符
fw.write(13);

// 写入字符串 hello,world!
fw.write("hello,world!");

// 写入换行符
fw.write(13);
char[] charArray = "the night".toCharArray();

// 写入字符数组
fw.write(charArray);

// 写入换行符
fw.write(13);

// 写入字符串,偏移量2,写入5个字符
// 因此写入的是 34567
fw.write("123456789",2,5);

// 写入换行符
fw.write(13);

// 写入字符数组,偏移量4,写入2个字符
// 因此写入的是 ni
fw.write(charArray,4,2);

// 写入换行符
fw.write(13);
CharSequence cs = new String("char sequence");

// 写入换行符
fw.write(13);

// 在输出流后追加字符序列 char sequence
Writer append = fw.append(cs);

// 刷新字符输入流
append.flush();

最终得到的simple.txt内容如下:

Z
hello,world!
the night
34567
ni

char sequence

缓冲流

由于IO操作很耗时,所以采用缓冲流,一次写入/读出多个字节,从而避免频繁的IO操作,提高流的传输效率。

字节缓冲流

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

Java的输入输出流有自带的内部缓冲区,为什么还需要字节缓冲流?

  1. 内部缓冲区的大小固定且较小,而字节缓冲流可以自定义缓冲区大小,更灵活
  2. 字节缓冲区性能更高

BufferedInputStream

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

这里的缓冲区减少的是系统调用的次数,而不是磁盘IO的次数,从而提高读取效率。

BufferedInputStream维护的缓冲区其实是一个字节数组,并且默认的缓冲区大小为8192字节,但是可以在BufferedInputStream对象构造时传入size作为缓冲区大小。

性能对比

读取571MB的文件,每次读取一字节:

  1. FileInputStream耗时833447毫秒
  2. BufferedInputStream耗时9910毫秒
    可以看到,其读取效率提升是相当巨大的

读取571MB的文件,每次读取长度为2000的字节数组:

  1. FileInputStream耗时571毫秒
  2. BufferedInputStream耗时391毫秒

经测试,这个用于接收的字节数组越小,两种方式的性能差异越大,当字节数组足够大,FileInputStream的读取效率可能会比BufferedInputStream更高。

我认为,造成这一情况的原因如下,当用于接收的字节数组大小等于BufferedInputStream的默认缓冲区大小的时候,两种读取方式所产生的系统调用数量是一样的,这相当于缓冲区形同虚设了;而对于一次只读取一个字节的情况来说,没有缓冲区则每次去内核缓冲区拿数据,有缓冲区则每次去缓冲区拿数据,无需系统调用,大大减少了系统调用的次数,因此BufferedInputStream效率更高。BufferedInputStream减少的实际上是系统调用的次数

BufferedOutputStream

BufferedOutputStream是字节缓冲输出流,首先将字节写入到缓冲区中,再从缓冲区写入到文件中,大大减少了IO次数。
BufferedOutputStream缓冲区的默认大小也是8192字节

字符缓冲流

字符缓冲流BufferedReaderBufferedWriter与字节缓冲流类似,只不过它们操作的数据变成了字符。

在使用Java处理标准输入流时,使用 Scanner 来处理会很慢,如果受到时间限制,应该采用 BufferedReader 更好。(例如2024-3-9美团春招笔试题第二题,Scanner 只能过一小部分用例,而采用 BufferedReader 则直接AC)

InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
String[] strs = br.readLine().split(" ");
for(int i=0;i<strs.length;i++){
    Integer num = Integer.parseInt(strs[i]);
    System.out.printf("%d %s\n",num,num.getClass());
}

首先批量读入字符串,再使用基本类型包装类的 parseInt 等方法将字符串转为数字。

随机访问流

RandomAccessFile支持随意跳转到文件的任意位置进行读写,在创建RandomAccessFile对象时可以指定读写模式mode

  1. r:只读模式
  2. rw:读写模式
  3. rws:同步更新对“文件的内容”或“元数据”的修改到外部存储设备
  4. rwd:同步更新对“文件的内容”的修改到外部存储设备

常用方法

  1. seek(long pos):指定写入或者读取字节的位置(偏移量)
  2. getFilePointer():得到当前的偏移量
String filePath = "test1.txt";
RandomAccessFile raf = new RandomAccessFile(filePath,"rws");
byte[] bytes = "hello,worle!ssssssssssssssssssaaaaaaaaaaaaaaaaeeeeeeeeeeee".getBytes();

// 向文件写入字符串 hello,worle!ssssssssssssssssssaaaaaaaaaaaaaaaaeeeeeeeeeeee
raf.write(bytes);

// 此时的指针位置在 58 ,也就是文件末尾
System.out.println(raf.getFilePointer());

// 通过 seek 方法把指针移到 0 位置,也就是文件开头
raf.seek(0);

// 输出 0 
System.out.println(raf.getFilePointer());

// 分别输出 h e l
System.out.println(Character.toChars(raf.read()));
System.out.println(Character.toChars(raf.read()));
System.out.println(Character.toChars(raf.read()));

// 把指针移至 20 位置
raf.seek(20);

// 输出 20
System.out.println(raf.getFilePointer());

// 分别输出 s s
System.out.println(Character.toChars(raf.read()));
System.out.println(Character.toChars(raf.read()));

// 输出 22
System.out.println(raf.getFilePointer());

可以看到,偏移量置0为文件开头

如上就是Java中输入输出流的常用类以及常用操作啦,其余的类操作应该也是类似的,可以举一反三!


Similar Posts

下一篇 IO及IO模型

Comments