IO也就是Input/Output
,数据拿到计算机内存中的过程即为输入,反之,数据从内存输出到外部存储(可以是远程主机、磁盘、数据库等)的过程即为输出。数据传输过程类似于水流,因此称作IO流。IO流在Java中分为输出流和输入流,根据数据的处理方式又分为字节流和字符流。(这里的输入输出是以程序为中心的,输入指程序接收输入,输出指程序把数据输出到外部存储)
Java IO流
Java IO流有四个基类,分别是输入流InputStream
(字节输入流),Reader
(字符输入流),OutputStream
(字节输出流),Writer
(字符输出流),其余的IO相关类都是派生于这四个抽象基类。
字节流与字符流
字节流:
- 以字节为单位处理数据,适用于处理二进制数据
- 直接操作字节,不涉及编码转换,可以处理任何类型的数据
字符流:
- 以字符为单位处理数据,适合处理文本数据
- 自动处理字符编码和解码(将字节传为字符)
- 性能逊于字节流处理,因为还有编解码消耗
- 对于不知道编码类型的数据,使用字节流处理会带来乱码问题,而使用字符流就不会出现这样的问题
字节流
InputStream
InputStream
用于从源读取字节流到内存中,它是一个抽象类,是所有字节输入流的父类。
常用方法
read()
:返回输入流中下一个字节的数据,如果未读取任何字节,返回-1
,表示结束read(byte b[])
:从输入流中读取一些字节放到字节数组b
中,如果数组b
的长度为0,则不读取,如果没有可以读取的字节,返回-1
。最多可以读b.length
个字节,返回读取的字节数read(byte b[], int off, int len)
:off
是偏移量,len
是指定的要读取的最大字节数,其余与read(byte b[])
一致(这里的偏移量off
是针对字节数组b
的,加入偏移为2,则从b
的第3个下标开始填充)skip(long n)
:忽略输入流中的n
个字节,返回实际忽略的字节数avaliable()
:返回输入流中可以读取的字节数close()
:关闭输入流并释放资源readAllBytes()
:读取输入流中的所有字节,返回字节数组readNBytes(byte[] b, int off, int len)
:阻塞直到读取len
个字节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
是所有字节输出流的父类。
常用方法
write(int b)
:将特定字节写入到输出流write(byte b[])
:将字节数组b
写入到输出流write(byte b[], int off, int len)
:增加了off
偏移量以及len
(要写入的最大字节数),与字节输入流相同,这里的off
也是对于字节数组b
来说flush()
:刷新此输出流并强制写出所有缓冲的输出字节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
用于从文件读取字符流到内存,它是所有字符输入流的父类。
常用方法
read()
:从输入流读取一个字符read(char[] cbuf)
:用于从输入流读取字符到字符数组cbuf
中read(char[] cbuf, int off, int len)
:用于从输入流读取字符到字符数组cbuf
中,并增加了偏移量off
以及读取的字符数量len
skip(long n)
:忽略输入流中的n
个字符,返回实际忽略的字符数量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
用于将字符输出到文件中,它是所有字符输出流的父类。
常用方法
write(int c)
:向输出流写入单个字符write(char[] cbuf)
:向输出流写入字符数组write(char[] cbuf, int off, int len)
:向输出流写入字符数组,并包含偏移量off
以及字符数量len
write(String str)
:向输出流写入字符串write(String str, int off, int len)
:向输出流写入字符串,并且包含偏移量off
以及字符数量len
append(CharSequence csq)
:将指定的字符序列csp
附加到指定的Writer
对象并返回该Writer
对象append(char c)
:将指定的字符附加到指定的Writer
对象并返回该Writer
对象flush()
:刷新该输出流,强制输出所有缓冲的输出字符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操作,提高流的传输效率。
字节缓冲流
字节缓冲流采用装饰器模式来增强InputStream
和OutputStream
子类对象的功能。
Java的输入输出流有自带的内部缓冲区,为什么还需要字节缓冲流?
- 内部缓冲区的大小固定且较小,而字节缓冲流可以自定义缓冲区大小,更灵活
- 字节缓冲区性能更高
BufferedInputStream
BufferedInputStream
从源头读取数据到内存的过程不会一个字节一个字节读取,而是会先将读取到的字节存放在缓冲区,并从内部缓冲区中单独读取字节,大大减少IO次数,提高了读取效率。
这里的缓冲区减少的是系统调用的次数,而不是磁盘IO的次数,从而提高读取效率。
BufferedInputStream
维护的缓冲区其实是一个字节数组,并且默认的缓冲区大小为8192字节,但是可以在BufferedInputStream
对象构造时传入size
作为缓冲区大小。
性能对比
读取571MB的文件,每次读取一字节:
FileInputStream
耗时833447
毫秒BufferedInputStream
耗时9910
毫秒
可以看到,其读取效率提升是相当巨大的
读取571MB的文件,每次读取长度为2000的字节数组:
FileInputStream
耗时571
毫秒BufferedInputStream
耗时391
毫秒
经测试,这个用于接收的字节数组越小,两种方式的性能差异越大,当字节数组足够大,FileInputStream
的读取效率可能会比BufferedInputStream
更高。
我认为,造成这一情况的原因如下,当用于接收的字节数组大小等于BufferedInputStream
的默认缓冲区大小的时候,两种读取方式所产生的系统调用数量是一样的,这相当于缓冲区形同虚设了;而对于一次只读取一个字节的情况来说,没有缓冲区则每次去内核缓冲区拿数据,有缓冲区则每次去缓冲区拿数据,无需系统调用,大大减少了系统调用的次数,因此BufferedInputStream
效率更高。BufferedInputStream
减少的实际上是系统调用的次数
BufferedOutputStream
BufferedOutputStream
是字节缓冲输出流,首先将字节写入到缓冲区中,再从缓冲区写入到文件中,大大减少了IO次数。
BufferedOutputStream
缓冲区的默认大小也是8192字节
字符缓冲流
字符缓冲流BufferedReader
和BufferedWriter
与字节缓冲流类似,只不过它们操作的数据变成了字符。
在使用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
r
:只读模式rw
:读写模式rws
:同步更新对“文件的内容”或“元数据”的修改到外部存储设备rwd
:同步更新对“文件的内容”的修改到外部存储设备
常用方法
seek(long pos)
:指定写入或者读取字节的位置(偏移量)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中输入输出流的常用类以及常用操作啦,其余的类操作应该也是类似的,可以举一反三!