12 - IO
以内存为中心:
-
Input指从外部读入数据到内存,如把文件从磁盘读取到内存,从网络读取数据到内存等;
-
Output指把数据从内存输出到外部,如把数据从内存写入到文件,把数据从内存输出到网络等。
IO流是一种顺序读写数据的模式,特点是单向流动。
字节流接口
IO流以byte(字节)为最小单位,因此也称为字节流。在Java中,InputStream代表输入字节流,OutputStream代表输出字节流,是最基本的两种IO流。
Reader/Writer
字符流接口
如果需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么按照char来读写更加方便,这种流称为字符流。
Java提供了Reader和Writer表示字符流,字符流传输的最小数据单位是char。Reader和Writer本质上是一个能自动编解码的InputStream和OutputStream。使用Reader,数据源虽然是字节,但读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了解码,转换成了char。使用InputStream,读入的数据和原始二进制数据一模一样,是byte[]数组,但是可以自己把二进制byte[]数组按照某种编码转换为字符串。选择Reader和InputStream,取决于具体的使用场景,如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更为方便。
同步和异步
同步IO:读写IO时代码必须等待数据返回后才继续执行后续代码,优点是代码编写简单,缺点是CPU执行效率低;
异步IO:读写IO时仅发出请求,然后立刻执行后续代码,优点是CPU执行效率高,缺点是代码编写复杂。
Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。InputStream、OutputStream、Reader和Writer都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStream、FileOutputStream、FileReader和FileWriter。
1. File对象
Java的标准库java.io提供了File对象来操作文件和目录。
要构造一个File对象,需要传入文件路径,既可以传入绝对路径,也可以传入相对路径。绝对路径是以根目录开头的完整路径;传入相对路径时,相对路径前面加上当前目录就是绝对路径。
注意:Windows平台使用“\”作为路径分隔符,在Java字符串中需要用“\\”表示一个“\”。Linux平台使用“/”作为路径分隔符。
File对象有3种形式表示的路径:
①getPath():返回构造方法传入的路径;
②getAbsolutePath():返回绝对路径;
③getCanonicalPath():和绝对路径类似,但是返回的是规范路径。
文件和目录
File对象既可以表示文件,也可以表示目录。构造File对象不会导致任何磁盘操作,只有调用File对象的某些方法时才真正进行磁盘操作。调用isFile()判断该File对象是否是一个已存在的文件;调用isDirectory()判断File对象是否是一个已存在的目录。
用File对象获取到一个文件时,判断文件的权限和大小:
-
boolean canRead()
-
boolean canWrite()
-
boolean canExecute()
-
boolean length()
对目录而言,是否可执行表示能否列出它包含的文件和目录。
创建和删除文件
当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除文件。File对象提供了createTempFile()来创建一个临时文件,以及deletOnExit()在JVM退出时自动删除该文件。
遍历文件和目录
当File对象表示一个目录时,可以使用list()和listFiles()列出目录下的文件和子目录名。listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录。
File对象表示一个目录时,可以通过以下方法创建和删除目录:
-
boolean mkdir()
-
boolean mkdirs():在必要时将不存在的父目录也创建出来;
-
boolean delete():当前目录必须为空才能删除成功。
Path
Java标准库提供的Path对象位于java.nio.file包,Path对象和File对象类似,但操作更加简单。需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。
2. InputStream
不是一个接口,而是一个抽象类,它是所有输入流的超类。int read()方法读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取。
注意:打开一个文件进行读写,完成后要及时关闭,以便操作系统把资源释放掉。InputStream和OutputStream都是通过close()方法来关闭流,关闭流就会释放对应的底层资源。总是使用try(resource)来保证InputStream正确关闭。
缓冲
在读取流时,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:
-
int read(byte[] b):返回读取的字节数;
-
int read(byte[] b, int off, int len):指定byte数组的偏移量和最大填充数。
阻塞
在调用InputStream的read()方法读取数据时,read()方法是阻塞的,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底花费多长时间。
InputStream实现类
FileInputStream:从文件获取输入流;
ByteArrayInputStream:可以在内存中模拟一个InputStream。实际上是把一个byte[]数组在内存中变成一个InputStream。
3. OutputStream
所有输出流的超类。void write(int b)方法写入一个字节到输出流。虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分。close()方法关闭输出流,以便释放系统资源。flush()方法将缓冲区的内容真正输入到目的地,缓冲区写满了或者调用close()方法前OutputStream会自动调用它。
FileOutputStream
每次写入一个字节非常麻烦,更常见的方法是一次性写入若干字节,可以用OutputStream提供的重载方法void write(byte[])来实现。
阻塞
OutputStream的write()方法也是阻塞的。
OutputStream实现类
FileOutputStream:从文件获取输出流;
ByteArrayOutputStream:在内存中模拟一个OutputStream。实际上是把一个byte[]数组在内存中变成一个OutputStream。
4. Filter模式
InputStream根据来源可以包括:
-
FileInputStream:从文件读取数据;
-
ServletInputStream:从HTTP请求读取数据;
-
Socket.getInputStream():从TCP连接读取数据。
-
……
针对依赖继承会导致子类数量失控的问题,JDK首先将InputStream分为两大类:
-
一类是直接提供数据的基础InputStream,如,FileInputStream, ByteArrayInputStream, ServletInputStream……
-
一类是提供额外附加功能的InputStream,如,BufferedInputStream, DigestInputStream, CipherInputStream……
在为“基础”InputStream附加功能时,①先确定这个能提供数据源的InputStream(需要的数据必须来自某个地方),②使用BufferedInputStream包装这个InputStream以提供缓冲的功能来提高读取效率,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream;③同样地,再使用GZIPInputStream包装这个InputStream以直接读取解压缩的内容。无论包装多少次,得到的对象始终是InputStream,直接用InputStream引用它就可以正常读取。
这种通过一个“基础”组件再叠加更各种“附加”功能组件的模式,称之为Filter模式/装饰器模式(Decorator),可以在运行期动态增加功能。
编写FilterInputStream
在叠加多个FilterInputStream时,只需要持有最外层的InputStream,并且当最外层的InputStream关闭时,内层的InputStream的close()方法也会被自动调用,并最终调用到最核心的“基础”InputStream,因此不存在资源泄露。
5. 操作Zip
ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容;ZipOutputStream是一种FilterOutputStream,可以直接写入内容到zip包。
读取zip包
首先创建一个ZipInputStream,通常是传入一个FileInputStream作为数据源,然后循环调用getNextEntry(),直到返回null,表示zip流结束。
一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,就用read()方法不断读取,直到返回-1。
写入zip包
首先创建一个ZipOutputStream,通常是包装一个FileOutputStream,然后每写入一个文件前,先调用putNextEntry(),再用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件的打包。
6. 读取classpath资源
从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果把default.properties文件放到classpath中,就不用关心它的实际存放路径。
在classpath中的资源文件,路径总是以“/”开头,先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件。
注意:调用getResourceAsStream()时,如果资源文件不存在,它将返回null。
把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置,使得应用程序启动更加灵活。
7. 序列化
序列化和反序列化
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,就相当于把Java对象存储到文件或者通过网络传出出去了。
反序列化是指把一个二进制内容(即byte[]数组)变回Java对象。
Java对象要能序列化必须实现一个特殊的java.io.Serializable接口,它没有定义任何方法,是一个空接口。把这样的空接口称为“标记接口”,实现了标记接口的类仅仅是自身贴了个“标记”,并没有增加任何方法。
为了避免class定义变动导致的不兼容问题,Java序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,就能自动阻止不匹配的class版本。
注意:反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。
安全性
Java的序列化机制可以导致一个实例能直接从byte[]数组创建,而不经过构造方法,因此存在一定的安全隐患。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。
Java的反序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,如JSON。
8. Reader
InputStream | Reader |
---|---|
字节流,以byte为单位 | 字符流,以char为单位 |
读取字节(-1,0~255):int read() | 读取字符(-1,0~65535):int read() |
读到字节数组:int read(byte[] b) | 读到字符数组:int read(char[] c) |
FileReader
Reader的一个子类,可以打开文件并获取Reader。读取纯ASCII编码的文本文件,无需指定编码;若文件中包含中文,为避免乱码问题,需要在创建FileReader时指定编码:
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
同样的,需要注意使用try(resource)来包装Reader在无论有没有IO错误时都能正确关闭。
CharArrayReader
可以在内存中模拟一个Reader,实际上是把一个char[]数组变成了一个Reader,与ByteArrayInputStream类似。
StringReader
可以直接把String作为数据源,和CharArrayReader几乎一样。
InputStreamReader
Reader和InputStream的关系:
普通的Reader(除特殊的CharArrayReader和StringReader外)实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后根据编码设置再转换为char来实现字符流。
Reader本质上是一个基于InputStream的byte到char的转换器。已经有一个InputStream,将其转换为Reader是可行的,通过InputStreamReader就可以把任何InputStream转换为Reader(构造时需要指定编码)。
9. Writer
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) |
FileWriter
向文件中写入字符流的Writer,使用方法和FileReader类似。
CharArrayWriter
可以在内存中创建一个Writer,实际上时构造一个缓冲区,可以写入char,最后得到写入的char[]数组,和ByteArrayOutputStream类似。
StringWriter
一个基于内存的Writer,和CharArrayWriter类似。实际上它在内部维护了一个StringBuffer,并对外提供了Writer接口。
OutputStreamWriter
普通的Writer(除特殊的CharArrayWriter和StringWriter外)实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。
OutputStreamWriter是一个将任意的OutputStream转换为Writer的转换器(需要指定编码)。
10. PrintStream和PrintWriter
PrintStream是FilterOutputStream的一种,是一种能接收各种数据类型的输出,打印数据时比较方便(print()/println()),且不会抛出IOException,编写代码时就不用进行捕获:
-
System.out:系统默认提供的标准输出;
-
System.err:系统默认提供的标准错误输出。
PrintWriter