六、IO
文章目录
- 六、IO
IO是指Input/Output,这里的输入和输出是针对内存而言的:
- Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
- Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。
为什么要把数据读到内存中才能处理这些数据呢?
是因为代码都是在内存中运行的,数据也必须读到内存,最终的表示方式无非是byte数组,字符串等,都必须存放在内存里。
从Java代码来看,输入实际上就是从外部,例如,硬盘上的某个文件,把内容读到内存,并且以Java提供的某种数据类型表示,例如,byte[]
,String
,这样,后续代码才能处理这些数据。又因为内存有“易失性”,所以必须要把处理后的数据输出保存。
I/O 流是一种顺序读写数据的模式,特点是单向流动。
InputStream/OutputStream
是以byte
(字节)为最小单位单向流动;
Reader/Writer
是以 char
(字符) 为最小单位单向流动。
同步和异步
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
Java标准库的包java.io
提供了同步IO,而java.nio
则是异步IO。上面我们讨论的InputStream
、OutputStream
、Reader
和Writer
都是同步IO的抽象类。
1、File
文件是一种十分重要的存储方式。在Java的标准库java.io
中提供了File
对象来操作文件和目录。
构造一个 File
对象,需要传入一个文件路径。
File f = new File("C:\\Windows\\notepad.exe");
ps:
构造File对象时,既可以传入绝对路径,也可以传入相对路径。
在Java字符串中需要用
\\
表示Windows的分隔符\
。Linux平台使用/
作为路径分隔符可以用
.
表示当前目录,..
表示上级目录。例如:// 假设当前目录是C:\Docs File f1 = new File("sub\\javac"); // 绝对路径是C:\Docs\sub\javac File f3 = new File(".\\sub\\javac"); // 绝对路径是C:\Docs\sub\javac File f3 = new File("..\\sub\\javac"); // 绝对路径是C:\sub\javac
File对象有一个静态变量用于表示当前平台的系统分隔符:
System.out.println(File.separator); // 根据当前平台打印"\"或"/"
1.1 文件和目录
isFile()
:判断该File
对象是否是一个已存在的文件;
isDirectory()
:判断该 File
对象是否是一个已存在的目录;
用 File
对象获取到一个文件时,还可以判断文件的权限和大小:
-
boolean canRead()
:是否可读; -
boolean canWrite()
:是否可写; -
boolean canExecute()
:是否可执行; -
long length()
:文件字节大小。
对于目录而言,不能直接通过
length()
方法获取大小。
1.2 创建和删除文件
当File对象表示一个文件时,可以通过createNewFile()
创建一个新文件,用delete()
删除该文件:
@Test
public void m0() throws IOException {
File file = new File("./aaa.txt");
if (!file.exists()) {
if (file.createNewFile()) {
System.out.println("创建文件成功");
if (file.delete()) {
System.out.println("文件已删除");
}
}
}
}
有些时候,程序需要读写一些临时文件,File对象提供了createTempFile()
来创建一个临时文件,以及deleteOnExit()
在JVM退出时自动删除该文件。
public class Main {
public static void main(String[] args) throws IOException {
File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除
System.out.println(f.isFile());
System.out.println(f.getAbsolutePath());
}
}
1.3 遍历文件和目录
当File对象表示一个目录时,可以使用 list()
和 listFiles()
列出目录下的文件和目录。两者的区别如下:
-
public String[] list()
:返回的是一个字符串数组,内容是File
对象表示目录中的文件名和目录名,并不是完整的路径。 -
public File[] listFiles()
:返回的是一个File
数组,表示该目录下的文件和目录名,可以通过gtePath()
获得路径。
list()
和 listFiles()
还提供了一系列的重载方法,可以过滤不想要的文件和目录。
@Test
public void m1() {
File file = new File("/Users/zhang/Desktop/2020春上课");
String[] list = file.list();
for (String str : list) {
System.out.println(str);
}
System.out.println("========================");
File[] files = file.listFiles();
for (File f : files) {
System.out.println(f.getName());
}
System.out.println("========================");
File[] files1 = file.listFiles(new FilenameFilter() { // 仅列出.png文件
public boolean accept(File dir, String name) {
return name.endsWith(".png"); // 返回true表示接受该文件
}
});
for (File f : files1) {
System.out.println(f.getPath());
}
}
和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:
-
boolean mkdir()
:创建当前File对象表示的目录; -
boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来; -
boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功。
2、InputStream
InputStream
是Java标准库提供的最基本的输入流。它位于java.io
这个包里。java.io
包提供了所有同步IO的功能。
InputStream
只是一个抽象类,它是所有输入流的超类。
2.1 FileInputStream
FileInputStream
是InputStream
的一个子类。它是从文件流中读取数据。下面是读取 FileInputStream
所有字节的例子:
@Test
public void m2() throws IOException {
try (InputStream inputStream = new FileInputStream("/Users/zhangsan/Desktop/111.png")) {
byte[] bytes = new byte[1024];
int n = 0;
while ((n = inputStream.read(bytes)) != -1) {
System.out.println(bytes);
}
}
}
上面的
try(resource)
的语法,是和在finally
中调用close()
一样的效果 ,是让编译器自动为我们关闭资源。编译器是看try(resource = ...)
中的对象是否实现了java.lang.AutoCloseable
接口,如果实现了,就自动加上finally
语句并调用close()
方法。
2.2 ByteArrayInputStream
ByteArrayInputStream
可以在内存中模拟一个InputStream
,虽然在实际应该中并不多,但在测试时,可以使用它来构造一个 InputStream
,不用真实的创建一个文件来读取。
@Test
public void m2() throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
}
}
3、OutputStream
OutputStream
是Java标准库提供的最基本的输出流。
OutputStream
也是抽象类,它是所有输出流的超类。
需要特别注意的是:OutputStream
还提供了一个 flush()
方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有
flush()
方法?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个
byte[]
数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream
有个flush()
方法,能强制把缓冲区内容输出。
OutputStream
会在缓冲区满了之后,自动调用 flush()
,并且,在调用close()
关闭OutputStream
之前,也会自动调用flush()
方法。但是如果我们想要把缓冲区中的内容立刻发送出去,就必须自己调用 flush()
。
3.1 FileOutputStream
@Test
public void m2() throws IOException {
try (OutputStream output = new FileOutputStream("./readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入finally并调用close()
}
3.2 ByteArrayOutputStream
ByteArrayOutputStream
可以在内存中模拟一个OutputStream
:
@Test
public void m2() throws IOException {
byte[] data;
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
output.write("Hello ".getBytes("UTF-8"));
output.write("world!".getBytes("UTF-8"));
data = output.toByteArray();
}
System.out.println(new String(data, "UTF-8"));
}
4、Filter模式
在Java中,JDK将InputStream
分为两大类:
一类是直接提供数据的基础InputStream
,例如:
FileInputStream
ByteArrayInputStream
ServletInputStream
- …
一类是提供额外附加功能的InputStream
,例如:
BufferedInputStream
DigestInputStream
CipherInputStream
- …
当我们想给一个“基础”InputStream
,例如FileInpuStream
,提供缓冲的功能时,可以用BufferedInputStream
包装这个InputStream
,得到的包装类型是BufferedInputStream
,但它仍然被视为一个InputStream
:
InputStream buffered = new BufferedInputStream(file);
如果还想要其他附加功能,可以继续包装。无论我们包装多少次,得到的对象始终是 InputStream
。
上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)
5、操作Zip
ZipInputStream
是一种FilterInputStream
,它可以直接读取zip包的内容:
┌───────────────────┐
│ InputStream │ ->java.io
└───────────────────┘
▲
│
┌───────────────────┐
│ FilterInputStream │ ->java.io
└───────────────────┘
▲
│
┌───────────────────┐
│InflaterInputStream│ ->java.util.zip
└───────────────────┘
▲
│
┌───────────────────┐
│ ZipInputStream │ ->java.util.zip
└───────────────────┘
▲
│
┌───────────────────┐
│ JarInputStream │ ->java.util.jar
└───────────────────┘
6、Reader
Reader
是Java的IO库提供的另一个输入流接口。和InputStream
的区别是,InputStream
是一个字节流,即以byte
为单位读取,而Reader
是一个字符流,即以char
为单位读取:
InputStream | Reader |
---|---|
字节流,以byte 为单位 |
字符流,以char 为单位 |
读取字节(-1,0~255):int read()
|
读取字符(-1,0~65535):int read()
|
读到字节数组:int read(byte[] b)
|
读到字符数组:int read(char[] c)
|
java.io.Reader
是所有字符输入流的超类。
6.1 FileReader
FileReader
是Reader
的一个子类,它可以打开文件并获取Reader
。下面是如何读取一个 FileReader
的所有字符:
/*缓冲区的方法*/
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1000]; //设置缓冲区
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}
如果文件中包含中文,可能会乱码,因为
FileReader
默认的编码与系统相关,例如,Windows系统的默认编码可能是GBK
,打开一个UTF-8
编码的文本文件就会出现乱码。要避免这样的问题,需要在创建FileReader
时指定编码。Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
6.2 CharArrayReader
和ByteArrayInputStream
类似,CharArrayReader
可以在内存中模拟一个Reader
,它的作用实际上是把一个char[]
数组变成一个Reader
:
try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}
6.3 StringReader
StringReader
和CharArrayReader
几乎一样,只是把 String
作为数据源:
try (Reader reader = new StringReader("Hello")) {
}
6.4 Reader 和 InputStream关系
除了特殊的CharArrayReader
和StringReader
,普通的Reader
实际上是基于InputStream
构造的,因为Reader
需要从InputStream
中读入字节流(byte
),然后,根据编码设置,再转换为char
就可以实现字符流。
6.5 InputStreamReader
根据上面的解释可以知道,把一个InputStream
转换为Reader
是完全可行的。InputStreamReader
就是这样一个转换器,它可以把任何InputStream
转换为Reader
。
// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");
简洁的写法:
try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
// TODO:
}
7、Writer
Reader
是带编码转换器的InputStream
,它把byte
转换为char
,类似的Writer
就是带编码转换器的OutputStream
,它把char
转换为byte
并输出。
Writer
和OutputStream
的区别如下:
OutputStream | Writer |
---|---|
字节流,以byte 为单位 |
字符流,以char 为单位 |
写入字节(0~255):void write(int b)
|
写入字符(0~65535):void write(int c)
|
写入字节数组:void write(byte[] b)
|
写入字符数组:void write(char[] c)
|
无对应方法 | 写入String:void write(String s)
|
7.1 FileWriter
FileWriter
就是向文件中写入字符流的Writer
:
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}
7.2 CharArrayWriter
CharArrayWriter
可以在内存中创建一个Writer
,它的作用实际上是构造一个缓冲区,可以写入char
,最后得到写入的char[]
数组,这和ByteArrayOutputStream
非常类似:
try (CharArrayWriter writer = new CharArrayWriter()) {
writer.write(65);
writer.write(66);
writer.write(67);
char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}
7.3 StringWriter
StringWriter
也是一个基于内存的Writer
,它和CharArrayWriter
类似。实际上,StringWriter
在内部维护了一个StringBuffer
,并对外提供了Writer
接口。
7.4 OutputStreamWriter
和 InputStreamReader
类似,可以通过OutputStreamWriter
将OutputStream
转换为Writer
,转换时需要指定编码:
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
// TODO:
}