javaEE 初阶 — 文件内容的读写
文章目录
- 数据流
- 1. 字节流
- 1.1 InputStream 概述
- 1.1.1 无参数 read 的使用
- 1.1.2 一个参数 read 的使用
- 1.2 使用 OutputStream 写文件
- 1.2.1 对于关闭文件的解释
- 2. 字符流
- 2.1 Reader 概述
- 2.1.1 read 方法的使用
- 2.2 Writer 概述
- 2.2.1 write 的使用
- 2.3 Scanner 补充
数据流
针对文件内容,使用 “流对象” 进行操作的。
如果现在通过水龙头接 100ml 的水,可以一次接 100ml,也可以一次接10ml 分 10 次接完。
再比如说我兜里有 100 块钱,我可以一次花完,也可以分多次花完。
这就叫做 “流”。
在一个羽毛球桶里,只能一个一个的拿羽毛球,而不能把羽毛球分为半个拿,这就不是 “流”。
如果要从一个文件中读 100 个字节,就可以一次读 100 个字节,一次读完。
也可以分多次读完,这是可以随心所欲的读的。
因此就把读写文件相关的对象,称为 “流对象”。
java 标准库里的流对象,从类型上分成两个大类:
- 字节流 这是操作二级制数据的
例如:InputStream、OutputStream、FileInputStream、FileOutputStream - 字符流 这是操作文本数据的
例如:Reader、Writer、FileReader、FileWriter
这些类的使用方式是非常固定的。
核心操作只有4个:
- 打卡文件。(构造对象)
- 关闭文件。(close)
- 读文件。(read)针对 InputStream 或者 Reader
- 写文件。(write)针对 OutoutStream 或者 Writer
1. 字节流
1.1 InputStream 概述
InputStream、OutputStream、Reader、Writer 都是抽象类,不能直接 new。
而是要 new FileInputStream
InputStream inputStream = new FileInputStream("d:/test.txt");
FileInputStream 需要 FileNotFoundException 这个异常。
FileNotFoundException 这是一个 IOException 的子类。
- read 方法无参数版本:一次只读一个字节。
- read 方法一个参数版本:把读到的内容填充到参数的这个字节数组当中。(此处的参数是个“输出型参数”,返回值是实际读取的是字节数)
- read 方法三个参数版本:和 2 类似,只不过是往数组的一部分区间里尽可能填充。
1.1.1 无参数 read 的使用
read 读取的是一个字节,按理来说,返回一个 byte 就行了,但是实际上返回的是 int 。
除了要表示 byte 里的 0~255(-128 ~ 127)这样的情况之外,还需要表示一个特殊的情况。
那就是读取文件结束(文件读到末尾了)的情况,要使用 -1 表示。
代码
package io;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class IoDemo6 {
//使用字节流来读取文件
public static void main(String[] args) throws IOException {
//创建 InputStream 的时候,可以使用绝对和相对路径,也可以使用 File 对象
InputStream inputStream = new FileInputStream("d:/test.txt");
//进行读操作
while (true) {
int getFile = inputStream.read(); //读取到一个字节
if (getFile == -1) {
// 读取完毕
break;
}
System.out.println("" + (byte)getFile);
}
inputStream.close(); //关闭文件
}
}
test.txt 文件里的内容是 hello 。
读到的每个字节,就是一个打印出来的数字。这些数字就是 hello 的 ascii 码。
这里的 txt 文件使用字节流也是可以读的,但是不够方便,更希望使用的是一个一个字符读的字符流。
1.1.2 一个参数 read 的使用
public static void main(String[] args) throws IOException {
//创建 InputStream 的时候,可以使用绝对和相对路径,也可以使用 File 对象
InputStream inputStream = new FileInputStream("d:/test.txt");
//进行读操作
while (true) {
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
if ( len == -1) {
// 读完了
break;
}
//此时读取的结果就被放到数组当中了
for (int i = 0; i < len; i++) {
System.out.println(" " + (byte)bytes[i]);
}
}
inputStream.close(); //关闭文件
}
一个参数的 read 需要调用者提前准备好一个数组。
int len = inputStream.read(bytes);
这里的传参操作,相当于是把刚才准好的数组交给 read 方法。
让 read 方法内部针对这个数组进行天填写。(此处传参相当于是 “输出型参数”)
java 中一般习惯做法是,输入的信息作为参数,输出的信息作为返回值。
可以看到与上述读的结果是一致的,三个参数的版本和 2 类似,也就不再赘述。
read 会尽可能的把参数传进来的数组给填满。
上面的数组长度是 1024 ,read 就会尽可能的读取 1024 个字节填到数组中。
但是实际上,文件剩余长度是有限的,如果剩余长度超过了 1024 ,此时 1024 个字节都会填满,返回值就只是 1024 了。
如果剩余的长度不足 1024 ,此时有多少就填多少,read 方法就会返回当前实际读取的长度。
举个例子。
可以看到 d 盘中有一个图片
public static void main(String[] args) throws IOException {
//创建 InputStream 的时候,可以使用绝对和相对路径,也可以使用 File 对象
InputStream inputStream = new FileInputStream("d:/javaEE.jpg");
//进行读操作
while (true) {
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
System.out.println("len:" + len);
if ( len == -1) {
// 读完了
break;
}
}
inputStream.close(); //关闭文件
}
整个文件的大小是 17 kb ,而数组的大小是 1024 个字节,因此就要多次循环来进行 read 操作。
刚开始读取的时候,每次读取的长度都是 1024 个字节。(第二轮读的数据会覆盖第一轮的数据)
随着数据的读取,文件的大小不足 1024 个字节,这个时候能读多少就读多少。
这里剩下的是 158 个字节,别的文件可能会不同。
把这 158 个字节读完以后,文件就读到了末尾,再一次进入循环,文件就无内容可读了。
就会返回 -1 结束循环。
缓冲区 存在的意义,就是为了提高 IO 操作的效率。
单次 IO 操作,是要访问硬盘/IO设备,单词操作是比较消耗时间的。
如果频繁的进行单次 IO 操作,消耗的时间就更多了。
单次 IO 操作时间是一定的,如果可以缩短 IO 的次数,此时就可以提高程序整体的效率了。
无参数版本的代码,是一次读一个字节,循环的次数就比较多,read 的次数也很高。
一个参数的版本,是一次读 1024 个字节,循环次数就降低了很多,read 的次数也变少了。
1.2 使用 OutputStream 写文件
下面来使用 OutputStream 写文件
OutputStream outputStream = new FileOutputStream("d:/test.txt");
OutputStream 只能 new FileOutputStream
对于 OutputStream 来说,默认情况下打卡一个文件就会文件原有的内容。
如果不想清空,流对象还提供了一个 “追加写” 对象,通过这个就可以实现不清空文件就把新内容追加写到后面。
write 方法也是有三个版本的,这里和 read 方法使用类似,这里不再赘述。
off 表示要从数组的哪个下标开始写,len 表示要写多长。
可以看到文件原来的内容是 hello
package io;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class IoDem7 {
public static void main(String[] args) throws IOException {
OutputStream outputStream = new FileOutputStream("d:/test.txt");
//开始写文件
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
outputStream.close(); //关闭文件
}
}
执行程序后
a c b d 对应的 ascii 码值就是 97 98 99 100。
1.2.1 对于关闭文件的解释
在内核当中,使用 PCB 这样的数据结构表示进程。
一个线程对应一个 PCB ,一个进程可以对应一个 PCB 也可以对应多个。
PCB 中有一个重要的属性,文件描述符表,(相当于是一个数组)记录了该进程打开了哪些文件。
(即使一个进程里有多个线程多个 PCB 也没关系,这些 PCB 会共用同一个文件描述符表)
每次打开文件操作,就会在文件描述符表中,申请一个位置,把这个信息放进去。
每次关闭文件,也就会把这个文件描述符表对应的表象给释放掉。
如果没有 close ,对应的表项没有及时去释放。
虽然 java 有 GC (垃圾回收),GC 操作会在回收这个 OutputStream 对象的时候去完成这个释放操作,但是这个 GC 不一定会及时。
所以,如果不手动释放的话,意味着文件描述符表可能很快就被占满了。
(这个数组是不能自动扩容的,意味着存在上限)
如果占满了之后,后面再次打开文件,就会失败。
如何才能保证 close 被执行
下面的是推荐的写法
public static void main(String[] args) throws IOException {
try(OutputStream outputStream = new FileOutputStream("d:/test.txt")) {
//开始写文件
outputStream.write(97);
outputStream.write(98);
outputStream.write(99);
outputStream.write(100);
}
}
这个写法虽然没有显示的写 close ,实际上是会执行的,只要 try 语句块执行完毕,就可以自动执行到 close。
这个语法在 Java,被称为 try with resources
try with resources 不是随便哪一个对象 放到 try() 里就能自动释放的,需要满足一定的要求。
可以看到只有实现了 Closeable 这个接口的类才可以放到 try() 中被自动关闭。
这个方法提供的方法就是 close 方法。
2. 字符流
2.1 Reader 概述
Reader 里的也有 read 方法只不过比 InputStream 里的多了一个。
无参版本也是一次读一个字节,char[] cbuf 参数和三个参数的版本和 InputStream 里的一样。
(可以读一个字符数组 或者 一次读一个结果到字符数组的一部分)
虽然返回值类型是个 int ,但是实际上返回的是 char 。
真正有区别的是 CharBuffer target 参数的版本。
这个版本不做介绍,其实就是相当于是把这个字符数组分装了一下。
2.1.1 read 方法的使用
可以看到文件里是 a b c d。
下面演示无参版本。
package io;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class IoDemo8 {
public static void main(String[] args) throws IOException {
//可以使用绝对和相对路径,也可以使用 File 对象
try (Reader reader = new FileReader("d:/test.txt")){
while (true) {
//读操作
int ch = reader.read(); //一次读一个字节
if (ch == -1) {
//文件读完了
break;
}
System.out.println("" + (char)ch);
}
}
}
}
执行程序后也看到了a b c d。
如果文件里是中文也是可以被读出来的。
2.2 Writer 概述
Writer 有以下几个版本的 write。
int c 参数和 char[] cbuf 参数版本可以一次写一个字符也可以一次写一个字符数组。
String str 参数版本可以写一个字符串。
带字符数组的三个参数版本可以写到字符数组的一部分。
带字符串的三个参数版本可以写到字符串的一部分
2.2.1 write 的使用
文件内容本身是 hello 。
下面演示写一个字符串的版本。
package io;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class IoDemo9 {
public static void main(String[] args) {
try (Writer writer = new FileWriter("d:/test.txt")) {
//写文件
writer.write("hello world");
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行程序后发生更改。
2.3 Scanner 补充
Scanner scanner = new Scanner(System.in);
System.in 其实就是一个流对象,所以还可以写成下面的形式。
package io;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
public class IoDemo10 {
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("d:/test.txt")) {
Scanner scanner = new Scanner(inputStream);
// 此时读的就是文件里的内容
scanner.next();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如果 Scanner 里面填的是标准输入的流对象,那就是从键盘读;如果填的是文件的流对象,那就是从文件读。
Scanner 的 close 本质上是要关闭内部包含的这个流对象。
但是此时,内部的 inputStream 对象已经被 try() 关闭了。
里面的的这个 Scanner 不关闭也没事。