读文件、写文件,都是操作系统提供了 API,在 Java 中也进行了封装,叫“文件流”/“IO流”
Stream
流,形象比喻,水流/气流
水流的特点:我要通过水龙头,接 1000ml 水
- 直接一口气,把 1000ml 接完
- 一次接 500ml,分两次接完
- 一次接 100ml,分十次接完
- …
IO 流的特点:我要从文件读取 100 字节文件
- 直接一口气,把 100 字节读完
- 一次读 50 字节,分两次读
- 一次读 10 字节,分十次
- …
操作系统本身提供的文件读写 API 就是流式
Java 实现 IO 流,类有很多,主要分为两个大类:
字节流和字符流
- 字节流:二进制文件使用
- 读写数据的基本单位,就是字节
- 一次读的 bit 不可少于 8 个,因为一个字节 8 个 bit,至少得读一个字节
表示字节流的类
InputStream
,用来输入的OutputStream
,用来输出的- 字符流:文本文件使用
- 一个字符不确定有几个字节,取决于实际的编码方式(GBK—一个汉字两个字节、UTF 8—一个汉字三个字节,一个字母一个字节)
- 内部做的工作更多,会自动的查询码表,把二进制数据转换成对应字符
表示字符流的类
Reader
,输入Writer
,输出
比如,就像读取某个文件中的前 10 个汉字
使用字符流就可以非常方方便的实现
- 直接读取 10 个字符
- 字符流自动判定文件是哪种编码方式,再将字节分割好
- 再读取对数量字节就得到 10 个汉字了
理解清楚“输入/输出”的方向(人为定义的)
把内存中的数据,放到硬盘上,视为输入还是输出呢?
- 如果站在内存视角,就是输出
- 如果站在硬盘视角,就是输入
后面但凡谈到输入输出,都是以
CPU
的视角来谈的,内存离CPU
比硬盘离CPU
更近
- 数据远离
CPU
,就是输出,将内存中的数据写到硬盘里- 数据靠近
CPU
,就是输入,硬盘文件中的数据拿到内存里
上面四个输入输出的类,都是“抽象类”
实际上真正干活的,并非这四个类
另外,Java 中,提供了很多很多类,实现上述的这四个抽象类
因为类太多了,就使得我们对于 IO
流的理解就非常费劲
- 但虽然种类多,但其实大家的用法都差不多
- 但凡类的名字是以“
Read/Writer
”结尾的,就是实现了Read
和Writer
的字符流对象 - 但凡类的名字是以“
InputStream/OutputStream
”结尾的,就是实现了InputStream
和OutputStream
的字节流对象
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Demo8 { //这个异常是IOException的子类,是其特殊的情况,可以直接写成 IOException public static void main(String[] args) throws IOException { //因为他是一个抽象类,所以不能直接new //只能new一个实现了它的子类 InputStream inputStream = new FileInputStream("./text.txt"); //可以指定绝对路径,也可以指定相对路径,也可以指定 File 对象 inputStream.close(); } }
FileNotFoundException
这个异常是IOException
的子类,是他的一种特殊情况,可以就throws
这个父类异常- 抽象类不能直接被
new
,只能new
一个实现了它的子类
- 在这里还隐藏了一个操作,“打开文件”,针对文件进行读写,务必需要先打开(操作系统的基本要求)
- 指定路径的时候,可以指定绝对路径,也可以指定相对路径,也可以指定
File
对象 - 这个代码中,虽然要求文件使用完毕之后要关闭,但是局限于本代码,不写
close
也行。因为close
之后,紧接着就是进程结束了
close
是释放“文件描述符表”里的元素,进程结束,意味着PCB
就销毁了,PCB
上面的文件描述符表就整个释放了
文件资源泄露
打开文件之后,还需要关闭文件
打开文件,其实是在该进程的文件描述符表中,创建了一个新的表项
- 进程 =>
PCB
(进程控制块)=> 文件描述表 - 这个表描述了该进程都需要操作哪些文件
- 可以认为它是一个数组,数组的每个元素就是一个
struct file
对象(Linux
内核) - 每个结构体就描述了对应操作的文件的信息
- 数组的下标,就称为“文件描述符”
每次打开一个文件,就相当于在数组上占用一个位置,而在系统内核中,文件描述附表数组是固定长度&不可扩容的。除非主动调用 close
关闭文件,此时才会释放空间。如果代码里一直打开,不去关闭,就会使这里的资源越来越少,把数组填满了,后续再打开文件就会打开失败
这样的问题,不容易被发现,泄露不是一瞬间就泄露完耳朵,这是一个持续的过程。整个问题直到所有的资源泄露完毕,这一刻才会集中的爆发出来
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Demo8 { public static void main(String[] args) { try(InputStream inputStream = new FileInputStream("./text.txt")){ }catch(IOException e){ e.printStackTrace(); } } }
- 在
close
的时候,问了防止因为一些特殊原因代码执行不到close
,有一种特殊的try
方法——try with sources
- 这里
()
中的创建的资源可以是多个 try{}
执行完毕,最终都会自动执行这里的close
- 不过想在
()
里面写,必须是实现了Closable
接口的类
`
字节流
1. 读文件
在文件打开之后,就需要读文件了
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Demo8 { public static void main(String[] args) { try(InputStream inputStream = new FileInputStream("./text")){ while (true) { int b = inputStream.read(); if(b == -1){ //读取完毕 break; } System.out.printf("0x%x\n",b); } }catch(IOException e){ e.printStackTrace(); } } } //运行结果(text文件内容:hello) 0x68 0x65 0x6c 0x6c 0x6f //(text文件内容:你好) 0xe4 0xbd 0xa0 0xe5 0xa5 0xbd
- 当读到最后一个字节,就返回
-1
- 打印字节的时候,一般都用十六进制进行表示,方便随时换算成二进制
hello
,可在ASCII
码表中找到对应单词;“你好”因为是六个字节,所以可以确定是UTF8
编码方式,就可以在UTF8
码表中对应打印出的内容拼出“你好”
频繁读取多次硬盘,当前硬盘的 IO 就耗时比较大,希望能减少 IO 的次数
byte[] buffer = new byte[1024]; int n = inputStream.read(buffer);
- 这个操作就会把硬盘中读取到的对应的数据,填充到
buffer
内存的字节数组中,并且尽可能填满(只需要一次IO
)
- 此处是把
buffer
形参当成了“输出型参数” - 平时写代码,方法的参数一般是“输入型参数”,使用返回值表示输出结果
- 虽然是一次读的内容多了,但也比一次读 1 个字节,分很多次读效率高不少
- 返回的 n 代表实际读到的字节数
- 这个过程也非常类似于“去食堂打饭”
- 拿空盘递给阿姨打饭(空
buffer
给read
) - 阿姨打满后,再把盘给你(
read
把读完的内容装进buffer
)
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class Demo9 { public static void main(String[] args) { try(InputStream inputStream = new FileInputStream("./text")){ byte[] buffer = new byte[1024]; int n = inputStream.read(buffer); for (int i = 0; i < n; i++) { System.out.printf("0x%x\n",buffer[i]); } }catch (IOException e){ e.printStackTrace(); } } }
和 Scanner 结合:
import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Scanner; public class Demo14 { public static void main(String[] args) throws IOException{ try(InputStream inputStream = new FileInputStream("./text")){ Scanner scanner = new Scanner(inputStream); while(scanner.hasNextInt()){ System.out.println(scanner.nextInt()); } }catch (IOException e){ e.printStackTrace(); } } }
- 这样也可以完成文件内容的读取
2. 写文件
在文件中写入“你好
”
import java.io.*; public class Demo10 { public static void main(String[] args) throws IOException { try(OutputStream outputStream = new FileOutputStream("./text");){ outputStream.write(0xe4); outputStream.write(0xbd); outputStream.write(0xa0); outputStream.write(0xe5); outputStream.write(0xa5); outputStream.write(0xbd); }catch (IOException e){ e.printStackTrace(); } } }
- 这里是按照一个字节一个字节的方式进行写入的
- 每次执行写操作的时候,都会先把之前的内容清空
- 只要使用
OunputStream
打开文件,文件里面的内容就没了 - 这样的操作,可能就把文件内容搞没了,并且找不回来了
还有一种“追加写”的方式,保持原内容不变,在末尾写入新内容
try(OutputStream outputStream = new FileOutputStream("./text",true);)
- 在最后加上一个参数
true
,代表开启“追加写”的方式
一次把整个字节数组都写入:
import java.io.*; public class Demo10 { public static void main(String[] args) throws IOException { try(OutputStream outputStream = new FileOutputStream("./text",true);){ byte[] buffer = new byte[] {(byte)0xe4,(byte)0xbd,(byte)0xa0,(byte)0xe5,(byte)0xa5,(byte)0xbd}; outputStream.write(buffer); }catch (IOException e){ e.printStackTrace(); } } }
InputStream
/ OutputStream
读写数据就是按照字节来操作的。如果要读写字符的话(中文),此时就绪要靠程序员手动来区分出哪几个字节是一个字符,再确保把这几个字节作为整体来写入
字符流
1. 读文件
为了方便处理字符,引入字符流
一次读一个字符:
import java.io.FileReader; import java.io.IOException; import java.io.Reader; public class Demo11 { public static void main(String[] args) throws IOException { try(Reader reader = new FileReader("./text")){ while (true) { int c = reader.read(); if (c == -1) return; char ch = (char) c; System.out.println(ch); } }catch (IOException e){ e.printStackTrace(); } } }
- 每次
read
读到的就是一个汉字 - 最初按照字节来读的时候,是每个汉字三个字节,但在
Java
中一个char
是两个字节,怎么用两个字节表示出了一个汉字?
- 当使用
char
表示这里的汉字的时候,不再使用UTF8
,而是使用unicode
编码方式 - 在
unicode
中,一个汉字就是两个字节
- 使用字符流读取数据的过程,Java 标准库内部就自动针对数据的编码进行转码了
用字符数组一次读若干字符:
import java.io.FileReader; import java.io.IOException; import java.io.Reader; public class Demo12 { public static void main(String[] args) throws IOException { try(Reader reader = new FileReader("./text")){ char[] buffer = new char[1024]; int n = reader.read(buffer); System.out.println(n); for (int i = 0; i < n; i++) { System.out.println(buffer[i]); } }catch (IOException e){ e.printStackTrace(); } } }
2. 写文件
import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.nio.channels.WritableByteChannel; public class Demo13 { public static void main(String[] args) throws IOException { try(Writer writer = new FileWriter("./text")){ writer.write("你好世界"); }catch (IOException e){ e.printStackTrace(); } } }
- 直接写入一个
String
到文件中
小结:
当前设计的这八个类,虽然数目不少,但用法都很相似
基本流程:打开 —> 读写 —> 关闭