JAVA学习(九)| IO
IO⁍
IO流是一种流式的数据输入/输出模型:
- 二进制数据以
byte为最小单位在InputStream/OutputStream中单向流动; - 字符数据以
char为最小单位在Reader/Writer中单向流动。
Java标准库的java.io包提供了同步IO功能:
- 字节流接口:
InputStream/OutputStream; - 字符流接口:
Reader/Writer。
而java.nio则是异步IO
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
File对象⁍
Java标准库的java.io.File对象表示一个文件或者目录:
-
创建
File对象本身不涉及IO操作;1
2import java.io.*;
File f = new File("C:\\Windows\\notepad.exe");Windows平台使用
\作为路径分隔符,在Java字符串中需要用\\表示一个\。Linux平台使用/作为路径分隔符 -
可以获取路径/绝对路径/规范路径:
getPath()/getAbsolutePath()/getCanonicalPath(); -
调用
isFile(),判断该File对象是否是一个已存在的文件,调用isDirectory(),判断该File对象是否是一个已存在的目录 -
用
File对象获取到一个文件时,还可以进一步判断文件的权限和大小:boolean canRead():是否可读;boolean canWrite():是否可写;boolean canExecute():是否可执行;long length():文件字节大小。
-
可以创建或删除文件和目录。
createNewFile()创建一个新文件,用delete()删除该文件- File对象如果表示一个目录,可以通过以下方法创建和删除目录:
boolean mkdir():创建当前File对象表示的目录;boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。
- File对象提供了
createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。
1
2
3
4
5
6
7
8
9import java.io.*;
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());
}
} -
(遍历)可以获取目录的文件和子目录:
list()/listFiles();listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录
Path对象⁍
Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单
1 | |
如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。
InputStream⁍
Java标准库的java.io.InputStream定义了所有输入流的超类(抽象类)
常用方法:InputStream (Java SE 11 & JDK 11 ) (runoob.com)
int read():读取输入流的下一个字节,并返回字节表示的int值(0~255)int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
总是使用try(resource)来保证InputStream正确关闭,代替try ... finally。推荐的写法如下:
1 | |
所有与IO操作相关的代码都必须正确处理IOException。
阻塞⁍
在调用InputStream的read()方法读取数据时,我们说read()方法是阻塞(Blocking)的。它的意思是,对于下面的代码:
1 | |
执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。
InputStream实现类⁍
-
用
FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。 -
ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream,测试常用 -
ServletInputStream:从HTTP请求读取数据;属于Javax(Java扩展包)
OutputStream⁍
整体与InputStream相似,常用方法是write
OutputStream (Java SE 11 & JDK 11 ) (runoob.com)
Java标准库的java.io.OutputStream定义了所有输出流的超类:
FileOutputStream实现了文件流输出;ByteArrayOutputStream在内存中模拟一个字节流输出。
⭐ 某些情况下需要手动调用OutputStream的flush()方法来强制输出缓冲区。
为什么要有
flush()?⁍因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个
byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以
OutputStream有个flush()方法,能强制把缓冲区内容输出。实际上,
InputStream也有缓冲区。例如,从FileInputStream读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。
总是使用try(resource)来保证OutputStream正确关闭。
输入写入到输出⁍
long transferTo(OutputStream out): 从该输入流中读取所有字节,并按读取顺序将字节写入给定的输出流
1 | |
Filter模式(或者装饰器模式:Decorator)⁍
Ⅰ. 直接提供数据的基础InputStream |
Ⅱ. 提供额外附加功能的InputStream( FilterInputSream) |
|---|---|
| FileInputStream | BufferedInputStream |
| ByteArrayInputStream | DigestInputStream |
| ServletInputStream | CipherInputStream |
可以用第Ⅱ类对第Ⅰ类进行包装
1 | |
1 | |
- OutputStream同理
- 可以自己编写
FilterInputStream,以便可以把自己的FilterInputStream“叠加”到任何一个InputStream中
1 | |
操作Zip⁍
ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容:
1 | |
读Zip⁍
传入一个FileInputStream作为数据源,然后,循环调用getNextEntry(),直到返回null,表示zip流结束。
一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1:
1 | |
写Zip⁍
先创建一个ZipOutputStream,通常是包装一个FileOutputStream,
然后,每写入一个文件前,先调用putNextEntry(),然后用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件的打包。
1 | |
如果要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径。
读取classpath资源⁍
路径无关的读取文件的方式:把资源存储在classpath中可以避免文件路径依赖;
在classpath中的资源文件,路径总是以/开头,我们先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件;
调用getResourceAsStream()需要特别注意的一点是,如果资源文件不存在,它将返回null。因此,我们需要检查返回的InputStream是否为null,如果为null,表示资源文件在classpath中没有找到:
1 | |
如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:
1 | |
这样读取配置文件,应用程序启动就更加灵活。
序列化⁍
概念⁍
把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程
通过反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
条件⁍
一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:
1 | |
Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
实现⁍
需要使用ObjectOutputStream包装ByteArrayOutputStream,可以写入任何实现了Serializable接口的Object。
1 | |
反序列化⁍
ObjectInputStream负责从一个字节流读取Java对象:
1 | |
除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。
readObject()可能抛出的异常有:
ClassNotFoundException:没有找到对应的Class;A电脑把传递对象给B电脑,但B电脑上没有定义该对象的类InvalidClassException:Class不匹配。反序列化时的字段类型与元对象不一致
为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,这样就能自动阻止不匹配的class版本:
1 | |
要特别注意反序列化的几个重要特点:
反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。
安全性问题⁍
一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。
更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。
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是所有字符输入流的超类,它最主要的方法是:
1 | |
这个方法读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1。
Reader定义了所有字符输入流的超类:
-
FileReader实现了文件字符流输入,使用时需要指定编码;1
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8); -
CharArrayReader和StringReader可以在内存中模拟一个字符流输入。
Reader是基于InputStream构造的:可以通过InputStreamReader在指定编码的同时将任何InputStream转换为Reader。
总是使用try (resource)保证Reader正确关闭。
Writer⁍
Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
| 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) |
Writer定义了所有字符输出流的超类:
FileWriter实现了文件字符流输出;CharArrayWriter和StringWriter在内存中模拟一个字符流输出。
使用try (resource)保证Writer正确关闭。
Writer是基于OutputStream构造的,可以通过OutputStreamWriter将OutputStream转换为Writer,转换时需要指定编码。
1 | |
PrintStream和PrintWriter⁍
PrintStream是一种能接收各种数据类型的输出,打印数据时比较方便:
System.out是标准输出;System.err是标准错误输出。
PrintWriter是基于Writer的输出。
使用Files⁍
对于简单的小文件读写操作(不可一次读入几个G的大文件),可以使用Files工具类简化代码。
读文件⁍
1 | |
写文件⁍
1 | |
此外,Files工具类还有copy()、delete()、exists()、move()等快捷方法操作文件和目录。





