Java IO
简单做个总结:
- 1 .InputStream/OutputStream 字节流的抽象类。
- 2 .Reader/Writer 字符流的抽象类。
- 3 .FileInputStream/FileOutputStream 节点流:以字节为单位直接操作“文件”。
- 4 .ByteArrayInputStream/ByteArrayOutputStream 节点流:以字节为单位直接操作“字节数组对象”。
- 5 .ObjectInputStream/ObjectOutputStream 处理流:以字节为单位直接操作“对象”。
- 6 .DataInputStream/DataOutputStream 处理流:以字节为单位直接操作“基本数据类型与字符串类型”。
- 7 .FileReader/FileWriter 节点流:以字符为单位直接操作“文本文件”(注意:只能读写文本文件)。
- 8 .BufferedReader/BufferedWriter 处理流:将Reader/Writer对象进行包装,增加缓存功能,提高读写效率。
- 9 .BufferedInputStream/BufferedOutputStream 处理流:将InputStream/OutputStream对象进行包装,增加缓存功能,提高读写效率。
- 10 .InputStreamReader/OutputStreamWriter 处理流:将字节流对象转化成字符流对象。
- 11 .PrintStream 处理流:将OutputStream进行包装,可以方便地输出字符,更加灵活。
IO流分类
流按处理的数据单元分类:
- 字节流:以字节为单位获取数据,命名上以Stream结尾的流一般是字节流,如FileInputStream、FileOutputStream。
- 字符流:以字符为单位获取数据,命名上以Reader/Writer结尾的流一般是字符流,如FileReader、FileWriter。
流按处理对象不同分类:
- 节点流:可以直接从数据源或目的地读写数据,如FileInputStream、FileReader、DataInputStream等。
- 处理流:不直接连接到数据源或目的地,是”
处理流的流
”。通过对其他流的处理提高程序的性能,如BufferedInputStream、BufferedReader等。处理流也叫包装流。
节点流处于IO操作的第一线,所有操作必须通过它们进行;处理流可以对节点流进行包装,提高性能或提高程序的灵活性。
IO 流简介
IO 即 Input/Output
,输入和输出。
数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。
菜鸟雷区 输入/输出流的划分是相对程序而言的,并不是相对数据源。
数据传输过程类似于水流,因此称为 IO 流。我们把数据源和目的地可以理解为IO流的两端。
当然,通常情况下,这两端可能是文件或者网络连接。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
-
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。任何从InputStream或Reader派生而来的类都有read()基本方法,读取单个字节或字节数组;
适配器类InputStreamReader
可以将InputStream
转成为Reader
-
OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。任何从OutputStream或Writer派生的类都含有write()的基本方法,用于写单个字节或字节数组。
适配器类OutputStreamWriter
可以将OutputStream
转成为Writer
刚开始写IO代码,总被各种IO流类搞得晕头转向。这么多IO相关的类,各种方法,啥时候能记住。
其实只要我们掌握了IO类库的总体设计思路,理解了它的层次脉络之后,就很清晰。
知道啥时候用哪些流对象去组合想要的功能就好了(装饰器模式),API的话,可以查手册的。
一般在使用IO流的时候会有下面类似代码:
这里其实是一种装饰器模式的使用,IO流体系中使用了装饰器模式包装了各种功能流类。
在Java IO流体系中FilterInputStream/FilterOutStream
和FilterReader/FilterWriter
就是装饰器模式的接口类,从该类向下包装了一些功能流类。有DataInputStream、BufferedInputStream、LineNumberInputStream、PushbackInputStream
等,当然还有面向字节的输出的功能流类;面向字符的功能流类等。
Reader
和Writer
的基础功能类,可以对比InputStream
、OutputStream
来学习
面向字节 | 面向字符 |
---|---|
InputStream | Reader |
OutputStream | Writer |
FileInputStream | FileReader |
FileOutputStream | FileWriter |
ByteArrayInputStream | CharArrayReader |
ByteArrayOutputStream | CharArrayWriter |
PipedInputStream | PipedReader |
PipedOutputStream | PipedWriter |
StringBufferInputStream(已弃用) | StringReader |
无对应类 | StringWriter |
File类
File类其实不止是代表一个文件,它也能代表一个目录下的一组文件(代表一个文件路径)。
常用方法
盘点一下File类中最常用到的一些方法:
需要注意的是,不同系统对文件路径的分割符表是不一样的,比如Windows中是“\”,Linux是“/”。
而File类给提供了抽象的表示File.separator,屏蔽了系统层差异
。因此平时在代码中不要使用诸如“\”这种代表路径,可能造成Linux平台下代码执行错误。
File类在IO中的作用
当以文件作为数据源或目标时,除了可以使用文件路径字符串作为文件以及位置的指定以外,我们也可以使用File类指定。
如下示例:
package com.yoostar.coms;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
public class FileInIODemo {
public static void main(String[] args) {
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new FileReader(new File("d:/sxt.txt")));
bw = new BufferedWriter(new FileWriter(new File("d:/sxt8.txt")));
String temp = "";
int i = 1;
while ((temp = br.readLine()) != null) {
bw.write(i + "," + temp);
bw.newLine();
i++;
}
bw.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
if (bw != null) {
bw.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
字节流
InputStream(字节输入流)
InputStream是输入流,前面已经说到,它是从数据源对象将数据读入程序内容时,使用的流对象。
通过看InputStream的源码知道,它是一个抽象类:
InputStream
常用方法:
//从数据中读入一个字节,并返回该字节,遇到流的结尾时返回-1
int read()
//读入一个字节数组,并返回实际读入的字节数,最多读入b.length个字节,遇到流结尾时返回-1
//如果有可用字节读取,则最多读取的字节数最多等于 `b.length` , 返回读取的字节数。这个方法等价于read(b, 0, b.length)。
int read(byte[] b)
// 读入一个字节数组,返回实际读入的字节数或者在碰到结尾时返回-1.
//b:代表数据读入的数组, off:代表第一个读入的字节应该被放置的位置在b中的偏移量,len:读入字节的最大数量
int read(byte[],int off,int len)
// 返回当前可以读入的字节数量,如果是从网络连接中读入,这个方法要慎用,
int available()
//在输入流中跳过n个字节,返回实际跳过的字节数
long skip(long n)
//标记输入流中当前的位置
void mark(int readlimit)
//判断流是否支持打标记,支持返回true
boolean markSupported()
// 返回最后一个标记,随后对read的调用将重新读入这些字节。
void reset()
//关闭输入流,这个很重要,流使用完一定要关闭
void close()
从 Java 9 开始,InputStream
新增加了多个实用的方法:
readAllBytes()
:读取输入流中的所有字节,返回字节数组。readNBytes(byte[] b, int off, int len)
:阻塞直到读取len
个字节。transferTo(OutputStream out)
:将所有字节从一个输入流传递到一个输出流。
直接从InputStream抽象类
继承的流,可以发现基本上对应了每种数据源类型:
类 | 功能 |
---|---|
ByteArrayInputStream | 将字节数组作为InputStream. 即ByteArrayInputStream是把内存中的”字节数组对象”当做数据源。 |
StringBufferInputStream | 将String转成InputStream |
FileInputStream | 从文件中读取内容 |
PipedInputStream | 产生用于写入相关PipedOutputStream的数据。实现管道化 |
SequenceInputStream | 将两个或多个InputStream对象转换成单一的InputStream |
FilterInputStream | 抽象类,主要是作为“装饰器”的接口类,实现其他的功能流 |
DataInputStream
用于读取指定类型数据,不能单独使用,必须结合 FileInputStream
。
DataInputStream和DataOutputStream
提供了可以存取与机器无关的所有Java基础类型数据(如:int、double、String等)的方法。
FileInputStream fileInputStream = new FileInputStream("input.txt");
//必须将fileInputStream作为构造参数才能使用
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
//可以读取任意具体的类型数据
dataInputStream.readBoolean();
dataInputStream.readInt();
dataInputStream.readUTF();//writeUTF()和readUTF()来写入和读取字符串
OutputStream(字节输出流)
OutputStream
用于将数据(字节信息)写入到目的地(通常是文件)
OutputStream
是输出流的抽象基类,它是将程序内存中的数据写入到目的地(也就是接收数据的一端)。
OutputStream
常用方法:
同样地,OutputStream
也提供了一些基础流的实现,这些实现也可以和特定的目的地(接收端)对应起来,比如输出到字节数组或者是输出到文件/管道等:
类 | 功能 |
---|---|
ByteArrayOutputStream | 在内存中创建一个缓冲区,所有送往“流”的数据都要放在此缓冲区 |
FileOutputStream | 将数据写入文件 |
PipedOutputStream | 和PipedInputStream配合使用。实现管道化 |
FilterOutputStream | 抽象类,主要是作为“装饰器”的接口类,实现其他的功能流 |
DataOutputStream
用于写入指定类型数据,不能单独使用,必须结合 FileOutputStream
DataInputStream和DataOutputStream
提供了可以存取与机器无关的所有Java基础类型数据(如:int、double、String等)的方法。
// 输出流
FileOutputStream fileOutputStream = new FileOutputStream("out.txt");
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
// 输出任意数据类型
dataOutputStream.writeBoolean(true);
dataOutputStream.writeByte(1);
使用装饰器包装有用的流
Java IO 流体系使用了装饰器模式
来给基础的输入/输出流添加额外的功能。
额外的功能可能是:可以将流缓冲起来提高性能、使流能够读写基本数据类型等。
通过装饰器模式添加功能的流类型都是从FilterInputStream
和FilterOutputStream抽象类
扩展而来的。
FilterInputStream
类型
类 | 功能 |
---|---|
DataInputStream | 和DataOutputStream搭配使用,使得流可以读取int char long等基本数据类型 |
BufferedInputStream | 使用缓冲区,主要是提高性能. IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。 |
LineNumberInputStream | 跟踪输入流中的行号,可以使用getLineNumber、setLineNumber(int) |
PushbackInputStream | 使得流能弹出“一个字节的缓冲区”,可以将读到的最后一个字符回退 |
FilterOutStream
类型
类 | 功能 |
---|---|
DataOutputStream | 和DataInputStream搭配使用,使得流可以写入int char long等基本数据类型 |
PrintStream | 用于产生格式化的输出 |
BufferedOutputStream | 使用缓冲区,可以调用flush()清空缓冲区 |
因此要理解流的使用就是搭配起来或者使用功能流组合起来去转移或者存储数据。
通过缓冲区提高读写效率
方式一
通过创建一个指定长度的字节数组作为缓冲区,以此来提高IO流的读写效率。
该方式适用于读取较大图片时的缓冲区定义。
**注意:**缓冲区的长度一定是 2 的整数幂。一般情况下1024 长度较为合适。
package com.coms.util.excel;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class FileStreamBuffedDemo {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//创建文件字节输入流对象
fis = new FileInputStream("d:/1.png");
//创建文件字节输出流对象
fos = new FileOutputStream("d:/3.png");
//创建一个缓冲区,提高读写效率
byte[] buff = new byte[1024];
int temp = 0;
while ((temp = fis.read(buff)) != -1) {
fos.write(buff, 0, temp);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
方式二
通过创建一个字节数组作为缓冲区,数组长度是通过输入流对象的available()返回当前文件的预估长度来定义的。在读写文件时,是在一次读写操作中完成文件读写操作的。注意:如果文件过大,那么对内存的占用也是比较大的。所以大文件不建议使用该方法。
package com.yoostar.coms;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class FileStreamBuffer2Demo {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//创建文件字节输入流对象
fis = new FileInputStream("d:/itbz.jpg");
//创建文件字节输出流对象
fos = new FileOutputStream("d:/cc.jpg");
//创建一个缓冲区,提高读写效率
byte[] buff = new byte[fis.available()];
fis.read(buff);
//将数据从内存中写入到磁盘中。
fos.write(buff);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
通过缓冲流提高读写效率
Java缓冲流本身并不具有IO流的读取与写入功能,只是在别的流(节点流或其他处理流)上加上缓冲功能提高效率,就像是把别的流包装起来一样,因此缓冲流是一种处理流(包装流)。 当对文件或者其他数据源进行频繁的读写操作时,效率比较低,这时如果使用缓冲流就能够更高效的读写信息。因为缓冲流是先将数据缓存起来,然后当缓存区存满后或者手动刷新时再一次性的读取到程序或写入目的地。 因此,缓冲流还是很重要的,我们在IO操作时记得加上缓冲流来提升性能。BufferedInputStream
和BufferedOutputStream
这两个流是缓冲字节流,通过内部缓存数组来提高操作流的效率。
package com.yoostar.coms;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class FileStreamBuffed3Demo {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
fis = new FileInputStream("d:/itbz.jpg");
bis = new BufferedInputStream(fis);
fos = new FileOutputStream("d:/ff.jpg");
bos = new BufferedOutputStream(fos);
//缓冲流中的 byte 数组长度默认是 8192
int temp = 0;
while ((temp = bis.read()) != -1) {
bos.write(temp);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//注意:关闭流顺序:"后开的先关闭"
if (bis != null) {
bis.close();
}
if (fis != null) {
fis.close();
}
if (bos != null) {
bos.close();
}
if (fos != null) {
fos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
字符流
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。
那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
个人认为主要有两点原因:
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
- 如果字节流且我们不知道编码类型就很容易出现中文乱码问题。
因此,I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。
字符流默认采用的是 Unicode
编码,我们可以通过构造方法自定义编码。
utf8
:英文占 1 字节,中文占 3 字节,unicode
:任何字符都占 2 个字节,gbk
:英文占 1 字节,中文占 2 字节。
如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好
Reader(字符输入流)
Reader
用于从源头(通常是文件)读取数据(字符信息)到内存中,java.io.Reader
抽象类是所有字符输入流的父类。
Reader
与InputStream
类似,不同点在于,Reader基于字符而非基于字节。
Reader
常用方法:
read()
: 从输入流读取一个字符。read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组cbuf
中,等价于read(cbuf, 0, cbuf.length)
。read(char[] cbuf, int off, int len)
:在read(char[] cbuf)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。skip(long n)
:忽略输入流中的 n 个字符 ,返回实际忽略的字符数。close()
: 关闭输入流并释放相关的系统资源。
InputStreamReader
是字节流转换为字符流的桥梁,其子类 FileReader
是基于该基础上的封装,可以直接操作字符文件。
// 字节流转换为字符流的桥梁
public class InputStreamReader extends Reader {
}
// 用于读取字符文件
public class FileReader extends InputStreamReader {
}
FileReader
代码示例:
try (FileReader fileReader = new FileReader("input.txt");) {
int content;
long skip = fileReader.skip(3);
System.out.println("The actual number of bytes skipped:" + skip);
System.out.print("The content read from file:");
while ((content = fileReader.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
input.txt
文件内容:
输出:
The actual number of bytes skipped:3
The content read from file:我是Guide。
Writer(字符输出流)
Writer
用于将数据(字符信息)写入到目的地(通常是文件),java.io.Writer
抽象类是所有字符输出流的父类。
Writer
常用方法:
write(int c)
: 写入单个字符。write(char[] cbuf)
:写入字符数组cbuf
,等价于write(cbuf, 0, cbuf.length)
。write(char[] cbuf, int off, int len)
:在write(char[] cbuf)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。write(String str)
:写入字符串,等价于write(str, 0, str.length())
。write(String str, int off, int len)
:在write(String str)
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字符数)。append(CharSequence csq)
:将指定的字符序列附加到指定的Writer
对象并返回该Writer
对象。append(char c)
:将指定的字符附加到指定的Writer
对象并返回该Writer
对象。flush()
:刷新此输出流并强制写出所有缓冲的输出字符。close()
:关闭输出流释放相关的系统资源。
OutputStreamWriter
是字节流转换为字符流的桥梁,其子类 FileWriter
是基于该基础上的封装,可以直接将字符写入到文件。
// 字符流转换为字节流的桥梁
public class OutputStreamWriter extends Writer {
}
// 用于写入字符到文件
public class FileWriter extends OutputStreamWriter {
}
FileWriter
代码示例:
try (Writer output = new FileWriter("output.txt")) {
output.write("你好,我是Guide。");
} catch (IOException e) {
e.printStackTrace();
}
输出结果:
使用装饰器包装有用的流
当然也有类似字节流的装饰器实现方式,给字符流添加额外的功能或这说是行为。
这些功能字符流类主要有:
-
BufferedReader
BufferedReader是针对字符输入流的缓冲流对象,提供了更方便的按行读取的方法:
readLine()
;在使用字符流读取文本文件时,我们可以使用该方法以行为单位进行读取.
-
BufferedWriter
BufferedWriter是针对字符输出流的缓冲流对象,在字符输出缓冲流中可以使用
newLine()
;方法实现换行处理。 -
PrintWriter
在Java的IO流中专门提供了用于字符输出的流对象PrintWriter。
该对象具有自动行刷新缓冲字符输出流,特点是可以按行写出字符串,并且可通过
println();
方法实现自动换行。 -
LineNumberReader
-
PushbackReader
转换流
InputStreamReader/OutputStreamWriter
用来实现将字节流转化成字符流。比如,如下场景:
System.in是字节流对象,代表键盘的输入,如果我们想按行接收用户的输入时,就必须用到缓冲字符流BufferedReader特有的方法readLine(),但是经过观察会发现在创建BufferedReader的构造方法的参数必须是一个Reader对象,这时候我们的转换流InputStreamReader就派上用场了。而System.out也是字节流对象,代表输出到显示器,按行读取用户的输入后,并且要将读取的一行字符串直接显示到控制台,就需要用到字符流的write(Stringstr)方法,所以我们要使用OutputStreamWriter将字节流转化为字符流。(
还可以解决中文乱码的问题
)
对象序列化与反序列化
学习链接:https://mp.weixin.qq.com/s/0EfIUB9E-0Oh_Clwuxswuw
序列化就是将对象转成字节序列的过程,反序列化就是将字节序列重组成对象的过程。
为什么要有对象序列化机制
程序中的对象,其实是存在有内存中,当我们JVM关闭时,无论如何它都不会继续存在了。那有没有一种机制能让对象具有“持久性”呢?序列化机制提供了一种方法,你可以将对象序列化的字节流输入到文件保存在磁盘上。
序列化机制的另外一种意义便是我们可以通过网络传输对象了,Java中的 远程方法调用(RMI)
,底层就需要序列化机制的保证。
对象序列化的作用有如下两种:
- 持久化: 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中。
- 网络通信: 在网络上传送对象的字节序列。比如:服务器之间的数据通信、对象传递。
在Java中怎么实现序列化和反序列化
首先要序列化的对象必须实现一个Serializable接口(这是一个标识接口,起标识作用,不包括任何方法)
其次需要是用两个对象流类:ObjectInputStream 和ObjectOutputStream
。
主要使用ObjectInputStream对象的readObject方法从流中读入对象、ObjectOutputStream的writeObject方法将对象写到流中
ObjectOutputStream
代表对象输出流,它的writeObject(Objectobj)方法可对参数指定的obj对象进行序列化
,把得到的字节序列写到一个目标输出流中。ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列
,再把它们反序列化为一个对象,并将其返回。只有实现了Serializable接口的类的对象才能被序列化
。
Serializable
接口是一个空接口,只起到标记作用。
下面我们通过序列化机制将一个简单的pojo对象写入到文件,并再次读入到程序内存。
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
@Override
public String toString() {
return "Student:" + '\n' +
"name = " + this.name + '\n' +
"age = " + this.age + '\n' +
"score = " + this.score + '\n'
;
}
// 序列化
public static void serialize( ) throws IOException {
Student student = new Student();
student.setName("CodeSheep");
student.setAge( 18 );
student.setScore( 1000 );
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();
System.out.println("序列化成功!已经生成student.txt文件");
System.out.println("==============================================");
}
//反序列化
public static void deserialize( ) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
//读出来做一个强转
Student student = (Student) objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列化结果为:");
System.out.println( student );
}
}
不想序列化的数据使用transient(瞬时)关键字屏蔽
如果我们上面的user对象有一个password字段,属于敏感信息,这种是不能走序列化的方式的,但是实现了Serializable
接口的对象会自动序列化所有的数据域,怎么办呢?在password字段上加上关键字transient
就好了。
系统IO流:System类中的IO流
在标准IO模型中,Java提供了System.in、System.out和System.error。
System.in
是一个静态域,未被包装过的InputStream
。通常我们会使用BufferedReader
进行包装然后一行一行地读取输入,这里就要用到前面说的适配器流InputStreamReader
将inputStream
转为Reader
。
System.out
System.out是一个PrintStream流。
System.out一般会把你写到其中的数据输出到控制台上。
System.out通常仅用在类似命令行工具的控制台程序上。
System.out也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。
System.err
System.err是一个PrintStream流。
System.err与System.out的运行方式类似,但它更多的是用于打印错误文本。
系统流重定向
尽管System.in, System.out, System.err这3个流是java.lang.System类中的静态成员,并且已经预先在JVM启动的时候初始化完成,你依然可以更改它们.
可以使用setIn(InputStream)、setOut(PrintStream)、setErr(PrintStream)进行重定向。
比如可以将控制台的输出重定向到文件中。
解压缩数据流处理
Java IO类库是支持读写压缩格式的数据流的。
这些压缩相关的流类是按字节处理的。
看下设计压缩解压缩的相关流类:
压缩类 | 功能 |
---|---|
CheckedInputStream | getCheckSum()可以为任何InputStream产生校验和(不仅是解压缩) |
CheckedOutputStream | getCheckSum()可以为任何OutputStream产生校验和(不仅是压缩) |
DeflaterOutputStream | 压缩类的基类 |
ZipOutputStream | 继承自DeflaterOutputStream,将数据压缩成Zip文件格式 |
GZIPOutputStream | 继承自DeflaterOutputStream,将数据压缩成GZIP文件格式 |
InflaterInputStream | 解压缩类的基类 |
ZipInputStream | 继承自InflaterInputStream,解压缩Zip文件格式的数据 |
GZIPInputStream | 继承自InflaterInputStream,解压缩GZIP文件格式的数据 |
表格中CheckedInputStream
和 CheckedOutputStream
一般会和Zip压缩解压过程配合使用,主要是为了保证我们压缩和解压过程数据包的正确性,得到的是中间没有被篡改过的数据。
以CheckedInputStream
为例,它的构造器需要传入一个Checksum类型:
而Checksum 是一个接口,因为是接口,所以可以看到这里又用到了策略模式
,具体的校验算法是可以选择的。Java类库给我提供了两种校验和算法:Adler32 和 CRC32
,性能方面可能Adler32 会更好一些,不过CRC32可能更准确。
压缩(ZIP)
我们可以把一个或一批文件压缩成一个zip文档。
将多个文件压缩成zip包
1 public class ZipFileUtils {
2 public static void compressFiles(File[] files, String zipPath) throws IOException {
3
4 // 定义文件输出流,表明是要压缩成zip文件的
5 FileOutputStream f = new FileOutputStream(zipPath);
6
7 // 给输出流增加校验功能
8 CheckedOutputStream checkedOs = new CheckedOutputStream(f,new Adler32());
9
10 // 定义zip格式的输出流,这里要明白一直在使用装饰器模式在给流添加功能
11 // ZipOutputStream 也是从FilterOutputStream 继承下来的
12 ZipOutputStream zipOut = new ZipOutputStream(checkedOs);
13
14 // 增加缓冲功能,提高性能
15 BufferedOutputStream buffOut = new BufferedOutputStream(zipOut);
16
17 //对于压缩输出流我们可以设置个注释
18 zipOut.setComment("zip test");
19
20 // 下面就是从Files[] 数组中读入一批文件,然后写入zip包的过程
21 for (File file : files){
22
23 // 建立读取文件的缓冲流,同样是装饰器模式使用BufferedReader
24 // 包装了FileReader
25 BufferedReader bfReadr = new BufferedReader(new FileReader(file));
26
27 // 一个文件对象在zip流中用一个ZipEntry表示,使用putNextEntry添加到zip流中
28 zipOut.putNextEntry(new ZipEntry(file.getName()));
29
30 int c;
31 while ((c = bfReadr.read()) != -1){
32 buffOut.write(c);
33 }
34
35 // 注意这里要关闭
36 bfReadr.close();
37 buffOut.flush();
38 }
39 buffOut.close();
40 }
41
42 public static void main(String[] args) throws IOException {
43 String dir = "d:";
44 String zipPath = "d:/test.zip";
45 File[] files = Directory.getLocalFiles(dir,".*\\.txt");
46 ZipFileUtils.compressFiles(files, zipPath);
47 }
48}
解压缩(ZIP)
解压缩zip包到目标文件夹
1 public static void unConpressZip(String zipPath, String destPath) throws IOException {
2 if(!destPath.endsWith(File.separator)){
3 destPath = destPath + File.separator;
4 File file = new File(destPath);
5 if(!file.exists()){
6 file.mkdirs();
7 }
8 }
9 // 新建文件输入流类,
10 FileInputStream fis = new FileInputStream(zipPath);
11
12 // 给输入流增加检验功能
13 CheckedInputStream checkedIns = new CheckedInputStream(fis,new Adler32());
14
15 // 新建zip输出流,因为读取的zip格式的文件嘛
16 ZipInputStream zipIn = new ZipInputStream(checkedIns);
17
18 // 增加缓冲流功能,提高性能
19 BufferedInputStream buffIn = new BufferedInputStream(zipIn);
20
21 // 从zip输入流中读入每个ZipEntry对象
22 ZipEntry zipEntry;
23 while ((zipEntry = zipIn.getNextEntry()) != null){
24 System.out.println("解压中" + zipEntry);
25
26 // 将解压的文件写入到目标文件夹下
27 int size;
28 byte[] buffer = new byte[1024];
29 FileOutputStream fos = new FileOutputStream(destPath + zipEntry.getName());
30 BufferedOutputStream bos = new BufferedOutputStream(fos, buffer.length);
31 while ((size = buffIn.read(buffer, 0, buffer.length)) != -1) {
32 bos.write(buffer, 0, size);
33 }
34 bos.flush();
35 bos.close();
36 }
37 buffIn.close();
38
39 // 输出校验和
40 System.out.println("校验和:" + checkedIns.getChecksum().getValue());
41 }
42
43 // 在main函数中直接调用
44 public static void main(String[] args) throws IOException {
45 String dir = "d:";
46 String zipPath = "d:/test.zip";
47// File[] files = Directory.getLocalFiles(dir,".*\\.txt");
48// ZipFileUtils.compressFiles(files, zipPath);
49
50 ZipFileUtils.unConpressZip(zipPath,"F:/ziptest");
51 }
IO流的典型使用方式
IO流种类繁多,可以通过不同的方式组合I/O流类,但平时我们常用的也就几种组合。
缓冲输入文件
1 public class BufferedInutFile {
2 public static String readFile(String fileName) throws IOException {
3 BufferedReader bf = new BufferedReader(new FileReader(fileName));
4 String s;
5
6 // 这里读取的内容存在了StringBuilder,当然也可以做其他处理
7 StringBuilder sb = new StringBuilder();
8 while ((s = bf.readLine()) != null){
9 sb.append(s + "\n");
10 }
11 bf.close();
12 return sb.toString();
13 }
14
15 public static void main(String[] args) throws IOException {
16 System.out.println(BufferedInutFile.readFile("d:/1.txt"));
17 }
18}
格式化内存输入
要读取格式化的数据
,可以使用DataInputStream
。
1 public class FormattedMemoryInput {
2 public static void main(String[] args) throws IOException {
3 try {
4 DataInputStream dataIns = new DataInputStream(
5 new ByteArrayInputStream(BufferedInutFile.readFile("f:/FormattedMemoryInput.java").getBytes()));
6 while (true){
7 System.out.print((char) dataIns.readByte());
8 }
9 } catch (EOFException e) {
10 System.err.println("End of stream");
11 }
12 }
13}
基本的文件输出
FileWriter
对象可以向文件写入数据。
首先创建一个FileWriter和指定的文件关联
,然后使用BufferedWriter
将其包装提供缓冲功能,为了提供格式化机制,它又被装饰成为PrintWriter
。
1public class BasicFileOutput {
2 static String file = "BasicFileOutput.out";
3
4 public static void main(String[] args) throws IOException {
5 BufferedReader in = new BufferedReader(new StringReader(BufferedInutFile.readFile("f:/BasicFileOutput.java")));
6 PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)));
7
8 int lineCount = 1;
9 String s;
10 while ((s = in.readLine()) != null){
11 out.println(lineCount ++ + ": " + s);
12 }
13 out.close();
14 in.close();
15 }
16}
下面是我们写出的BasicFileOutput.out文件
,可以看到我们通过代码字节加上了行号
数据的存储和恢复
为了输出可供另一个“流”恢复的数据,我们需要使用DataOutputStream
写入数据,然后使用DataInputStream
恢复数据。
当然这些流可以是任何形式(这里的形式其实就是我们前面说过的流的两端的类型),比如文件。
1public class StoringAndRecoveringData {
2 public static void main(String[] args) throws IOException {
3 DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("data.txt")));
4 out.writeDouble(3.1415926);
5 out.writeUTF("三连走起");
6 out.writeInt(125);
7 out.writeUTF("点赞加关注");
8 out.close();
9
10 DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("data.txt")));
11 System.out.println(in.readDouble());
12 System.out.println(in.readUTF());
13 System.out.println(in.readInt());
14 System.out.println(in.readUTF());
15 in.close();
16 }
17}
随机访问流
这里要介绍的随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile
。
RandomAccessFile可以实现两个作用:
- 1 .实现对一个文件做读和写的操作。
- 2 .可以访问文件的任意位置。不像其他流只能按照先后顺序读取。
RandomAccessFile
的构造方法如下,我们可以指定 mode
(读写模式)。
// openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除
public RandomAccessFile(File file, String mode)
throws FileNotFoundException {
this(file, mode, false);
}
// 私有方法
private RandomAccessFile(File file, String mode, boolean openAndDelete) throws FileNotFoundException{
// 省略大部分代码
}
读写模式主要有下面四种:
r
: 只读模式。rw
: 读写模式rws
: 相对于rw
,rws
同步更新对“文件的内容”或“元数据”的修改到外部存储设备。rwd
: 相对于rw
,rwd
同步更新对“文件的内容”的修改到外部存储设备。
文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。
RandomAccessFile
中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。
我们可以通过 RandomAccessFile
的 seek(long pos)
方法来设置文件指针的偏移量(距文件开头 pos
个字节处)。
如果想要获取文件指针当前的位置的话,可以使用 getFilePointer()
方法。
学习这个流我们需掌握三个核心方法:
- 1 .RandomAccessFile(Stringname, String mode)name用来确定文件; mode取r(读)或rw(可读写),通过mode可以确定流对文件的访问权限。
- 2 .seek(long a) 用来定位流对象读写文件的位置,a确定读写位置距离文件开头的字节个数。
- 3 .getFilePointer() 获得流的当前读写位置。
RandomAccessFile
代码示例:
RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw");
System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer());
// 指针当前偏移量为 6
randomAccessFile.seek(6);
System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer());
// 从偏移量 7 的位置开始往后写入字节数据
randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'});
// 指针当前偏移量为 0,回到起始位置
randomAccessFile.seek(0);
System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ",当前读取到的字符" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer());
input.txt
文件内容:
输出:
读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1
读取之前的偏移量:6,当前读取到的字符G,读取之后的偏移量:7
读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1
input.txt
文件内容变为 ABCDEFGHIJK
。
RandomAccessFile
的 write
方法在写入对象的时候如果对应的位置已经有数据的话,会将其覆盖掉。
RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw");
randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'});
假设运行上面这段程序之前 input.txt
文件内容为 ABCD
,运行之后则变为 HIJK
。
RandomAccessFile的应用
RandomAccessFile
比较常见的一个应用就是实现大文件的 断点续传 。
何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要全部重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。
RandomAccessFile
可以帮助我们合并文件分片,示例代码如下:
我在《Java 面试指北》open in new window中详细介绍了大文件的上传问题。
RandomAccessFile
的实现依赖于 FileDescriptor
(文件描述符) 和 FileChannel
(内存映射文件)。
Apache IO包
JDK中提供的文件操作相关的类,但是功能都非常基础,进行复杂操作时需要做大量编程工作。实际开发中,往往需要你自己动手编写相关的代码,尤其在遍历目录文件时,经常用到递归,非常繁琐。
Apache-commons-io工具包中提供了IOUtils/FileUtils,可以让我们非常方便的对文件和目录进行操作。
Apache IOUtils和FileUtils类库为我们提供了更加简单、功能更加强大的文件操作和IO流操作功能。
FileUtils的使用
FileUtils类中常用方法:
- cleanDirectory:清空目录,但不删除目录。
- contentEquals:比较两个文件的内容是否相同。
- copyDirectory:将一个目录内容拷贝到另一个目录。可以通过FileFilter过滤需要拷贝的文件。
- copyFile:将一个文件拷贝到一个新的地址。
- copyFileToDirectory:将一个文件拷贝到某个目录下。
- copyInputStreamToFile:将一个输入流中的内容拷贝到某个文件。
- deleteDirectory:删除目录。
- deleteQuietly:删除文件。
- listFiles:列出指定目录下的所有文件。
- openInputSteam:打开指定文件的输入流。
- readFileToString:将文件内容作为字符串返回。
- readLines:将文件内容按行返回到一个字符串数组中。
- size:返回文件或目录的大小。
- write:将字符串内容直接写到文件中。
- writeByteArrayToFile:将字节数组内容写到文件中。
- writeLines:将容器中的元素的toString方法返回的内容依次写入文件中。
- writeStringToFile:将字符串内容写到文件中。
demo1
:
package com.yoostar.coms;
import org.apache.commons.io.FileUtils;
public class FileUtilsDemo1 {
public static void main(String[] args) throws Exception {
String content = FileUtils.readFileToString(new File("d:/sxt.txt"), "utf-8");
System.out.println(content);
}
}
demo2:
package com.yoostar.coms;
import org.apache.commons.io.FileUtils;
public class FileUtilsDemo2 {
public static void main(String[] args) throws Exception {
FileUtils.copyDirectory(new File("d:/a"), new File("c:/a"), new FileFilter() {
//在文件拷贝时的过滤条件
@Override
public boolean accept(File pathname) {
if (pathname.isDirectory() || pathname.getName().endsWith("html")) {
return true;
}
return false;
}
});
}
}
IOUtils的使用
- buffer方法:将传入的流进行包装,变成缓冲流。并可以通过参数指定缓冲大小。
- closeQueitly方法:关闭流。
- contentEquals方法:比较两个流中的内容是否一致。
- copy方法:将输入流中的内容拷贝到输出流中,并可以指定字符编码。
- copyLarge方法:将输入流中的内容拷贝到输出流中,适合大于 2 G内容的拷贝。
- lineIterator方法:返回可以迭代每一行内容的迭代器。
- read方法:将输入流中的部分内容读入到字节数组中。
- readFully方法:将输入流中的所有内容读入到字节数组中。
- readLine方法:读入输入流内容中的一行。
- toBufferedInputStream,toBufferedReader:将输入转为带缓存的输入流。
- toByteArray,toCharArray:将输入流的内容转为字节数组、字符数组。
- toString:将输入流或数组中的内容转化为字符串。
- write方法:向流里面写入内容。
- writeLine方法:向流里面写入一行内容
demo
package com.yoostar.coms;
import java.io.FileInputStream;
public class IOUtilsDemo {
public static void main(String[] args) throws Exception {
String content = IOUtils.toString(new FileInputStream("d:/sxt.txt"), "utf-8");
System.out.println(content);
}
}
IO流总结
按流的方向分类:
- 输入流:数据源到程序(InputStream、Reader读进来)。
- 输出流:程序到目的地(OutPutStream、Writer写出去)。
按流的处理数据单元分类:
- 字节流:按照字节读取数据(InputStream、OutputStream)。
- 字符流:按照字符读取数据(Reader、Writer)。
按流的功能分类:
- 节点流:可以直接从数据源或目的地读写数据。
- 处理流:不直接连接到数据源或目的地,是处理流的流。通过对其他流的处理提高程序的性能。
IO的四个基本抽象类:
InputStream、OutputStream、Reader、Writer
InputStream的实现类:
- FileInputStream
- ByteArrayInutStream
- BufferedInputStream
- DataInputStream
- ObjectInputStream
OutputStream的实现类:
- FileOutputStream
- ByteArrayOutputStream
- BufferedOutputStream
- DataOutputStream
- ObjectOutputStream
- PrintStream
Reader的实现类
- FileReader
- BufferedReader
- InputStreamReader
Writer的实现类
- FileWriter
- BufferedWriter
- OutputStreamWriter
Java IO设计模式
参考学习链接:Java IO 设计模式总结 | JavaGuide(Java面试 + 学习指南)
- 装饰器模式
- 适配器模式
- 工厂模式
- 观察者模式
JAVA IO模型详解
I/O
何为 I/O?
I/O(Input/Outpu) 即输入/输出 。
我们先从计算机结构的角度来解读一下 I/O。
根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。
冯诺依曼体系结构
输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
我们再先从应用程序的角度来解读一下 I/O。
根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
并且,用户空间的程序不能直接访问内核空间。
当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和响应)。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
当应用程序发起 I/O 调用后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
有哪些常见的 IO 模型?
UNIX 系统下, IO 模型一共有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
这也是我们经常提到的 5 种 IO 模型。
Java 中 3 种常见 IO 模型
BIO (Blocking I/O)
BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
图源:《深入拆解Tomcat & Jetty》
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (Non-blocking/New I/O)
Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio
包,提供了 Channel
, Selector
,Buffer
等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。
Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
跟着我的思路往下看看,相信你会得到答案!
我们先来看看 同步非阻塞 IO 模型。
图源:《深入拆解Tomcat & Jetty》
同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
这个时候,I/O 多路复用模型 就上场了。
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,目前几乎在所有的操作系统上都有支持。
- select 调用:内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
- epoll 调用:linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
AIO (Asynchronous I/O)
AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。
最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。
The End!!创作不易,欢迎点赞/评论!!欢迎关注个人GZH