Java中的IO流概念介绍及使用方法,此IO只针对于磁盘IO,没有网络IO相关知识
概述
IO就是input/output,这个是相对于内存而言的;
- iniput就是往内存里面放数据,数据从哪里来的呢?可以是本地磁盘,也可以是从网络获取的数据
- output就是从内存里往外面传数据,数据要传到哪里去呢?可以是本地磁盘,也可以是网络
IO是一种按照顺序读写的数据的模式,特点就是单向流动,就像自来水在水管里面流动一样,所以叫IO流
注意:InputStream流获取之后只能用一次,读取完了这个流就是空的了
InputStream/OutputStream
InputStream和OutputStream是以字节为单位读数据的,就是针对于字节流来做输入输出的,最小单位是byte,注意:nputStream/OutputStream这是两个抽象类
Reader/Writer
Reader和Writer是以字符为单位读数据的,针对于字符数据来做输入输出很方便,底层其实还是字节数据,只不过加了一层字节转字符和字符转字节的转化,最小单位是char。注意:Reader/Writer是抽象类
同步和异步
同步:读写IO代码时必须得等待数据返回后,才能执行后续代码,优点是代码编写简单,缺点是cpu利用率不足,InputStream/OutputStream和Reader/Writer都是同步IO
异步:读写IO时,仅仅发出请求,然后就可以执行后续代码了,优点是cpu利用率高,缺点是代码编写复杂
File使用
java.io提供了File类来操作文件和目录
创建File对象
File对象,创建的时候构造方法,可以传路径(相对/绝对),也可以传具体文件的路径(”D:\test.txt”),传目录就表示目录,传具体文件file就代表具体文件了,所以file对象可以表示目录,也可以表示文件
路径说明:
- 传一个 “.” 代表的是当前目录,当前目录就是你的java项目的目录
- 传一个 “/“ 或 “\“ 就是表示的java项目所在磁盘的根目录
- 传一个 “..” 就是代表着上一级目录,也就是项目所属文件夹
- 也可以传绝对路径,就是表示的绝对路径目录
- 如果里面传的是具体文件file就是表示的具体文件,传目录就是表示的目录,file就是用来操作文件和目录的
API
创建File相关
- File file = new File(“..”); 构造方法
- file.getAbsolutePath() 返回绝对路径
- file.getPath() 返回创建file时传入构造方法的路径
- file.getCanonicalPath() 返回规范路径
- file.isFile() 是否是文件
- file.isDirectory() 是否是做
- File.separator 可以获取当前系统路径分隔符的表示符号,比如win是 “" ,linux是 “/“
file相关操作:判断 文件/目录 读写权限、创建删除 文件/目录
- file.canRead():是否可读
- file.canWrite():是否可写
- file.canExecute():是否可执行,如果file是目录,canExecute代表就是是否可以列出它包含的文件夹和子目录
- long length():文件字节大小。
- file.exists() 文件是否存在
- file.createNewFile() 创建一个新文件
- file.delete() 删除文件或目录,如果是目录的话,目录下必须为空才会删除
- file.mkdir() 创建目录
- file.mkdirs() 创建当前file对象表示的目录,如果父目录不存在也会把不存在的父目录创建出来
遍历目录
方式一:File[] listFiles = file.listFiles() 直接列出当前目录下的所有东西
方式二:通过new FileFilter()一个过滤器,过滤出自己想要的东西
1
2
3
4
5
6
7File[] listFiles = file.listFiles(new FileFilter() {
public boolean accept(File pathname) {
// 这里只列出目录下所有文件,不列出目录
return pathname.isFile();
}
});方式三:方式二的lambda表达式写法:File[] listFiles1 = file.listFiles(File::isFile);
InputStream使用
基本说明
InputStream就是Java标准库提供的最基本的输入流,要特别注意的一点是,InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read()
1 | public abstract int read() throws IOException; |
这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。
使用方法
普通读取,一个字节一个字节读取,效率低
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/**
* InputStream基本使用
*/
private static void baseInputStream() throws IOException {
InputStream inputStream = null;
int n;
try {
inputStream = new FileInputStream("d:\\\\test.txt");
// read正常情况下返回的是读取的字节的int值(0-255),如果读完了就返回-1
while ( (n = inputStream.read()) != -1 ) {
System.out.println(n);
}
} finally {
// 记得及时释放资源,不释放资源得话程序就会一直占用着资源
if (inputStream != null) {
inputStream.close();
}
}
// 使用try(resources) 这种方式会自动释放资源,也就是编译器会自己添加finally块调用close方法
try ( InputStream inputStream1 = new FileInputStream("d:\\\\test.txt")) {
while ( (n = inputStream1.read()) != -1 ) {
System.out.println(n);
}
}
}使用缓冲区读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 普通的read方法一次读取一个字节,这样读取效率太低
* 现在搞一个缓冲区来读取,就是一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节的效率不错
* InputStream有两个重载方法来支持读取多个字节
* int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
* int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数,这个偏移量是针对byte数据来说的
*/
private static void readFile() throws IOException {
try (InputStream inputStream = new FileInputStream("d:\\\\test.txt")) {
// 定义一个1000个字节的缓冲区
byte[] buffer = new byte[1000];
int n;
while ( (n = inputStream.read(buffer)) != -1 ) {
System.out.println("read " + n + " bytes");
}
}
}
注意事项
- read方法是阻塞的,意思就是必须得等read这个方法执行完之后才能执行后面的代码,因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。
- 实际上,InputStream也有缓冲区。例如,从FileInputStream读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。
- 这个IO流里read和write的重载方法说明【重要】
OutputStream使用
基本说明
和InputStream类似,OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:
1 | public abstract void write(int b) throws IOException; |
这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分(相当于b & 0xff)。
和InputStream类似,OutputStream也提供了close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。
flush
为什么要有flush()
?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]
数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream
有个flush()
方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个flush()
方法,因为缓冲区写满了OutputStream
会自动调用它,并且,在调用close()
方法关闭OutputStream
之前,也会自动调用flush()
方法。
使用方法
普通写入:write(byte[]) / write(int n) / write(byte[], off, len)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* OutputStream基础使用
* @throws IOException IOException
*/
private static void baseUse() throws IOException {
OutputStream outputStream = new FileOutputStream("d:\\\\test.txt");
// 如果test.txt里面有值的话就会把内容覆盖掉!
// 和read一样,write也有三个方法,都是类似的
// write(byte[]) / write(int n) / write(byte[], off, len)
outputStream.write("覆盖内容".getBytes(StandardCharsets.UTF_8));
// 事实上,如果写入数据得话,会先写入缓冲区,然后攒的差不多了,才会将缓冲区内容真正写进磁盘,做一次磁盘IO
// flush就是强制将缓冲区中的内容刷进磁盘,不过close方法调用时也会将缓冲区的内容刷进磁盘
// 其实read的时候也有缓冲区,先读若干个字节到缓冲区,再执行接下来的read方法
outputStream.flush();
outputStream.close();
// 建议使用try(resources)方式来操作,编译器会帮我们在finally块中进行close操作~
try(OutputStream outputStream1 = new FileOutputStream("d:\\\\test.txt")) {
outputStream1.write("hello world!".getBytes(StandardCharsets.UTF_8));
}
}
注意事项
注意flush方法的使用
FilterStream
这个是干嘛的:当我们需要inputstream具有很多功能的时候,比如需要带缓冲,计算签名功能,我们不能搞出来多个子类继承inputstream然后再操作,所以就有了这个
具体使用
当我们需要给一个“基础”InputStream
附加各种功能时,我们先确定这个能提供数据源的InputStream
,因为我们需要的数据总得来自某个地方,例如,FileInputStream
,数据来源自文件:
1 | InputStream file =new FileInputStream("test.gz"); |
紧接着,我们希望FileInputStream
能提供缓冲的功能来提高读取的效率,因此我们用BufferedInputStream
包装这个InputStream
,得到的包装类型是BufferedInputStream
,但它仍然被视为一个InputStream
:
1 | InputStream buffered =new BufferedInputStream(file); |
最后,假设该文件已经用gzip压缩了,我们希望直接读取解压缩的内容,就可以再包装一个GZIPInputStream
:
1 | InputStream gzip =new GZIPInputStream(buffered); |
无论我们包装多少次,得到的对象始终是InputStream
,我们直接用InputStream
来引用它,就可以正常读取:
1 | ┌─────────────────────────┐ |
上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合,类似的,OutputStream也是以这种模式来提供各种功能:
自己编写一个定制化的filterstream
只需要继承FilterInputStream,即可,关键就是构造方法传入的是inputstream,所以可以进行各种包装(装饰)
操作zip
基本说明
明白一个概念,zipEntry,可以看作是一个zip包中的具体文件,也可以看作是目录,但是entry并不存储数据,读取数据或写入数据还是操作的ZipInputStream或ZipOutputStream对象
ZipInputStream和ZipOutputStream都是filterstream
1 | ┌───────────────────┐ |
具体使用
创建zip文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39/**
* 将一个目录中的文件打包
*/
private static void createZip() throws IOException {
// 这里构造方法填的是目标目录,就是压缩包放的地方
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("d:\\\\test.zip"))) {
// 获取要打包的目录
File dir = new File("d:\\\\test");
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
// 放入一个实体,new ZipEntry中如果传名字就是默认打包到zip的根目录下,若传相对路径,就打包到对应相对路径下,zip包是根目录
zip.putNextEntry(new ZipEntry(file.getName()));
zip.write(getFileDataAsBytes(file));
zip.closeEntry();
}
}
}
}
/**
* 读取文件中的字节流
* @param file File对象
* @return byte[]
* @throws IOException IOException
*/
private static byte[] getFileDataAsBytes(File file) throws IOException {
byte[] bytes = new byte[1024];
int length = 0;
// 神器,可以临时搞一个输出流存字节数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try(InputStream inputStream = new FileInputStream(file)) {
while ( (length = inputStream.read(bytes)) != -1 ) {
// 只写入读取了的长度,因为可能读不满缓冲池,会存在空字节
byteArrayOutputStream.write(bytes, 0, length);
}
}
return byteArrayOutputStream.toByteArray();
}读取zip文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* 读取zip包中的文件数据
* @throws IOException IOException
*/
private static void readZip() throws IOException {
try(ZipInputStream zip = new ZipInputStream(new FileInputStream("d:\\\\test.zip"))) {
ZipEntry entry;
// 缓冲区读
byte[] cache = new byte[1024];
// 存储读取出来的数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ( (entry = zip.getNextEntry()) != null) {
System.out.println("entryName: " + entry.getName());
if (!entry.isDirectory()) {
int n;
while ((n = zip.read(cache)) != -1) {
System.out.println("read " + n + " bytes");
byteArrayOutputStream.write(cache, 0, n);
}
}
}
System.out.println(byteArrayOutputStream);
}
}
读取ClassPath资源
基本说明
classPath就是编译后的classes文件,相对路径就是相对于classes文件夹来说的,classes文件夹就是classPath的根目录 “/“ ,所以从这里获取文件也是有特殊的方法的,一行代码
1 | InputStream resourceAsStream = ClassPathDemo.class.getResourceAsStream("/default.properties") |
使用方法
基本使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* classPath文件基本读取
*/
private static void baseUse() throws IOException {
try (InputStream resourceAsStream = ClassPathDemo.class.getResourceAsStream("/default.properties")) {
Properties properties = new Properties();
properties.load(resourceAsStream);
System.out.println(properties.getProperty("name"));
System.out.println(properties.getProperty("age"));
System.out.println(properties.getProperty("gender"));
// 使用过这个输入流之后,输入流resourceAsStream就是空了,下面再用这个流的话还需要重新获取,因为输入流只能用一次
byte[] cache = new byte[1024];
int length = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// 一定要判空
if (resourceAsStream != null) {
while ( (length = resourceAsStream.read(cache)) != -1) {
// 只往输出流中写入读取的字节长度,因为cache存在空字节问题
byteArrayOutputStream.write(cache, 0, length);
}
}
System.out.println(byteArrayOutputStream);
}
}从classPath获取,从外部获取;场景:jar包里打进去默认的配置文件,也就是classPath中的,然后还有一个方法是可以获取外部配置文件,给用户自定义配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// 从classPath获取和从外部文件获取
Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile(".conf.properties"));
/**
* 从classpath中获取配置文件
* @param filePath 文件相对路径
* @return InputStream
*/
private static InputStream inputStreamFromClassPath(String filePath) {
return ClassPathDemo.class.getResourceAsStream(filePath);
}
/**
* 从外部文件获取配置文件
* @param filePath 文件路径:相对/绝对
* @return InputStream
* @throws FileNotFoundException FileNotFoundException
*/
private static InputStream inputStreamFromFile(String filePath) throws IOException {
File file = new File(filePath);
// 如果文件不存在就新建
if (!file.exists()) {
System.out.println("file " + filePath + " is not exists!");
boolean success = file.createNewFile();
System.out.println(success ? "文件创建成功!" : "文件创建失败!");
}
return new FileInputStream(file);
}