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()
等快捷方法操作文件和目录。