在第一节中,我们了解了 File 类对于文件的操作。File对象封装了文件或者路径属性,但是不包括从/向文件读/写数据的方法,本节主要讲解Java的字节流。
I/O流解决的问题:设备与设备之间数据传输的问题。
Java流的分类:
- 从流的方向上可以分为输入流和输出流,应用程序通过输入流读取数据,通过输出流发送数据。
- 按照数据传输单位可以划分为字节流和字符流。
字节流读取的是文件中的二进制数据,不会对数据进行任何处理。
字符流读取的也是文件中的二进制数据,但是会对这些数据进行处理(解码)
Java I/O类的设计是应用继承一个很好的例子,使用装饰者模式进行设计。关于I/O中装饰者模式的内容请关注后续章节。
1、字节流
1.1、FileInputStream
构造方法:
- FileInputStream(File file) 由file对象创建一个FileInputStream
FileInputStream(String filename) 由文件名创建一个FileInputStream
- int read() 从输入流中读取一个字节的数据,返回读取的数据,如果达到输入流末尾,则返回-1。
- int read(byte[] buffer) 从输入流中读取b.length个字节的数据到字节数组buffer中,返回实际读取的字节数。如果到达输入流末尾,则返回-1。
- int read(byte[] buffer,int off,int len) 从输入流中读取多个字节的数据,并存储在buffer[off]~buffer[off+len-1]中,返回实际读取的字节数。如果到达输入流末尾,则返回-1。
最常见的FileInputStream读取文件的方式:
private static void readFile() {
FileInputStream fis = null;
File f = new File("./src/a.txt");
try {
fis = new FileInputStream(f);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
byte[] buf = new byte[4]; // 一次性读取buf.length个字节的数据
int length = 0;
try {
while ((length = fis.read(buf)) != -1) { //将读取的字节数据装入byte数组中,返回的是读取的字节数;读至文件末尾返回 -1
/**
* read方法将数据读进byte数组采用覆盖的方式,而并非清空重新赋值的方式。
* 故,在获取输出时要指定byte数组中有效的元素。
*/
System.out.print(new String(buf, 0, length));
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
int read(byte[] b) 方法将数据读进byte数组中,采用覆盖的方式,并不是清空后重新添加的方式。所以从byte数组中获取输出数据时需要指定有效元素。
使用流读取文件后务必要关闭资源 fis.close() 。
1.2、FileOutputStream
构造方法:
- FileOutputStream(File file) 由file对象创建一个FileOutputStream
- FileOutputStream(String filename) 由文件名创建一个FileOutputStream
- FileOutputStream(File file,boolean append) append为真,数据追加至文件末尾
FileOutputStream(File file,boolean append) append为真,数据追加至文件末尾
- void write(int b) 将指定字节写入输出流
- void write(byte[] b) 将字节数组b中的所有字节写入输出流
- void write(byte[] b,int off,int len) 将b[off]~b[off+len-1]写入输出流
- void close() 关闭输出流并释放与其有关的系统资源
- void flush() 刷新输出流并强制写出所有缓冲的输出字节
常见的使用FileOutputStream写文件的方法:
private static void writeFile() throws IOException {
FileOutputStream fos = null;
File file = new File("./src/b.txt");
fos = new FileOutputStream(file,true);
String data = "xiaopeng";
fos.write(data.getBytes()); //写入字节数组
fos.close();
}
write(int b)方法传入int类型的变量(4个字节),但是该方法只会将最低位的那个字节写入文件,其余的三个字节会被舍弃。二进制数:00000000-00000000-00000001-01100001(353),将353写入,但是文件中却只有低八位(97)。
private static void writeTest() throws IOException {
FileOutputStream fos = null;
FileInputStream fis = null;
File file = new File("./src/b.txt");
fos = new FileOutputStream(file);
fis = new FileInputStream(file);
fos.write(353); //00000000-00000000-00000001-01100001
byte[] bytes = new byte[4];
fis.read(bytes);
System.out.println(Arrays.toString(bytes));
fis.close();
fos.close();
}
输出:
[97, 0, 0, 0]
1.3、BufferedInputStream/BufferedOutputStream
Java提供了专门的缓冲字节流来提高文件读取的效率。BufferedInputStream/BufferedOutputStream类内部有一个缓冲字节数组(默认8192B),用来提高处理效率。
private static void bufferedReadTest() throws IOException {
BufferedInputStream bufferedInputStream = null;
//BufferedInputStream本身不具备读取文件的功能,需要借助FileInputStream进行读取文件
bufferedInputStream = new BufferedInputStream(new FileInputStream(new File("./src/a.txt")));
int content = 0;
while((content = bufferedInputStream.read()) != -1){
System.out.print((char)content);
}
bufferedInputStream.close(); //内部其实调用 fileInputStream.close()
}
BufferedInputStream不具备读取文件的功能,需要借助FileInputStream进行文件读取,查看BufferedInputStream的read方法可以发现:
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
- count:代表BufferedInputStream内部维护的字节数组大小
- pos:代表当前读取字节数组中那个下标的数据
若pos<=count,那么调用 fill() 方法(其实是FileInputStream的int read(byte[] b,int off,int len))将数据读入字节数组;否则直接从字节数组中读取一个字节的数据并返回。
BufferedInputStream的 close() 方法其实内部调用传入的FileInputStream的 close() 方法,因此关闭资源时只需要调用BufferedInputStream即可。
public void close() throws IOException {
byte[] buffer;
while ( (buffer = buf) != null) {
if (bufUpdater.compareAndSet(this, buffer, null)) {
InputStream input = in;
in = null;
if (input != null)
input.close(); //FileInputStream.close()
return;
}
}
}
关于BufferedOutputStream将数据写入磁盘的实现方式:
- 调用父类FilterOutputStream的 close() 方法关闭资源:
public void close() throws IOException {
try (OutputStream ostream = out) {
flush();
}
}
- 手工调用BufferOutputStream的 flush() 方法将数据写入:
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
- 查看BufferedOutputStream的 write() 方法可以发现,当缓冲字节数组容量满时,也会调用将数据写入:
public synchronized void write(int b) throws IOException {
if (count >= buf.length) {
flushBuffer(); //实际调用FilterOutputStream.write()写入文件
}
buf[count++] = (byte)b;
}