IO流
Java IO流的介绍
1 Java IO原理
1)IO流是什么:
I/O
是Input/Output
的缩写,I/O
技术是用于数据传输的,如读/写文件,网络通讯等。- Java程序中,数据的输入/输出操作以
流(stream)
的方式进行。 java.io
包下提供了各种流
类和接口,用以获取不同种类的数据,并通过标准的步骤(方法)输入或输出数据。
2)IO是什么:输入/输出是相对于程序或者说内存而言的
- 输入Input:读取外部数据到程序(内存)中;
- 输出Output:将程序(内存)数据输出到磁盘、光盘等外部存储设备中。
3)流的分类:
按操作的
数据单位
不同分为:- 字节流(8bit)
- 字符流(16bit)
按数据流的
流向
不同分为:- 输入流
- 输出流
按流的
角色
不同分为:- 节点流:直接作用于文件和存储设备或内存之间的
- 处理流:作用于已有流之上的,常用于改善已有流的传输速度等传输特性
4)IO流的体系结构
IO流的体系结构是指Java中IO流的类或接口的整体结构。
Java的IO流共涉及40多个类,实际上非常规律,都是从如下4个抽象基类派生的。
由这4个类派生出来的子类名称都是以其父类名作为子类名后缀。
总得来说,
- IO流的抽象基类为:字节输入流InputStream、字节输出流OutputStream、字符输入流Reader、字符输出流Writer;
- IO流的节点流为:4个文件流-FileInputStream、FileOutputStream、FileReader、FileWriter;
- 其余流都是处理流;
- 所有抽象基类的子类的名称都是以其父类名作为后缀的。
文件字符流
1 FileReader读入数据的基本操作
使用流进行数据传输的步骤非常规范,通常是以下四步:
- 实例化File类的对象,指明要操作的数据对应的文件:读入的文件一定要存在,否则就会报
FileNotFoundException
错; 提供对应的流:这里是字符输入流;
- 如果直接使用已有文件路径,则可以省略第一步。
- 如果直接使用已有文件路径,则可以省略第一步。
- 数据的读入/写出:这里是数据的读入;
- 流资源的关闭:垃圾回收机制对于物理连接,如数据库连接、输入输出流、Socker连接无能为力。
使用不同的流或方法主要体现在第二、三两步的不同。
1)使用read()
方法进行数据的读入
第二步中实例化FileReader
通常利用第一步的File
对象作为参数,调用构造器:public FileReader(File file) throws FileNotFoundException
。
第三步中,使用第二步实例化的FileReader
,调用其read()
方法进行数据读入:
public int read() throws IOException
: 该方法读入并返回对应流对象中的一个字符对应的int
值,如果读到流的最后了,则返回-1。
第四步中,对流资源的关闭使用close()
方法:public abstract void close() throws IOException
。
在进行IO流
操作时,会有很多编译时异常需要处理,但一定要在最后对流资源进行关闭。因此,在进行编译时异常处理时,如果直接通过throws
处理,那么如果close()
语句之前就发生了异常,就无法关闭资源了,因此,要使用try-catch-finally
进行异常处理,将close()
放入finally
中。由于只有close()
放入finally
中,相当于其他步骤语句都有可能因为错误不执行,因此在try-catch
之前先用null
创建好流对象,以用于最后close
,考虑到try-catch
中真正创建流对象的语句由于异常没执行而导致最后close()
出现空指针异常,因此在finally
中进行一个流对象的null
判断。
示例:
2)使用read(char[] cbuf)
方法进行数据的读入
第三步中,使用第二步实例化的FileReader
,调用其read(char[] cbuf)
方法进行数据读入:
public int read(char[] cbuf) throws IOException
: 该方法返回读入到字符数组cbuf
中的字符个数,当读到文件结尾时返回-1。
注意虽然我们定义了cbuf
的长度,但每次调用read(char[] cbuf)
时,并不是每次都会读满cbuf
,因此一定要根据该方法返回的个数结合cbuf
来得到读取的数据,而不是直接根据cbuf
来得到读取的数据。
另外,除了可以用循环结构结合read(char[] cbuf)
返回的个数来得到cbuf
中的读取的数据,还可以用String
的构造器public String(char[] value, int offset, int count)
结合read(char[] cbuf)
返回的个数来得到cbuf
中的读取的数据。
示例:
2 FileWriter写出数据的基本操作
总体仍然遵循规范的四步:
实例化File类的对象,指明要操作的数据对应的文件:这里是指要写出的文件;
- 要写出的文件可以不存在,如果不存在,在输出过程中会自动创建;
提供对应的流:这里是字符输出流;
- 根据使用的输出流的构造器不同,输出效果不同:使用没有提供
append
参数的构造器,输出效果为覆盖已有文件内容;否则就是追加已有文件内容。
- 根据使用的输出流的构造器不同,输出效果不同:使用没有提供
- 数据的读入/写出:这里是数据的写出,使用对应的输出流对象调用
write()
方法即可; - 流资源的关闭:垃圾回收机制对于物理连接,如数据库连接、输入输出流、Socker连接无能为力。
示例:
- 不存在输出文件:
- 对已存在的文件追加内容:
3 FileReader和FileWriter结合练习:文件的复制
@Test
public void test2(){
FileReader fr = null;
FileWriter fw = null;
try {
// 1 实例化File类的对象,指明要操作的数据对应的文件: 这里是要读入的文件和要写出的文件
File srcfile = new File("hello.txt");
File destfile = new File("newhello.txt");
// 2 提供对应的流:这里是字符输入和输出流
fr = new FileReader(srcfile);
fw = new FileWriter(destfile);
// 3 数据的读入和写出
char[] cbuf = new char[5];
int read;
while ((read = fr.read(cbuf)) != -1){
fw.write(cbuf, 0, read);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 4 流资源的关闭
if (fw != null){
try {
fw.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (fr != null){
try {
fr.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
文件字节流
文件字符流是无法传输非文本数据的,以图片数据为例,可以这样理解,图片数据底层是二进制数据,即字节数据为单位,而字符流是以字符为单位传输,也就是说,如果使用字符流传输图片数据,它会将图片数据的每个字节数据作为字符数据处理,因此会出错。
对非文本数据的传输,适合用字节流。
使用文件字节流,即FileInputStream
和FileOutputStream
的基本操作与字符流基本相同,只不过是其中涉及到char
的变为byte
。
- 对于
FileInputStream
: - 对于
FileOutputStream
:
复制非文本文件的示例:
@Test
public void test1(){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 1 实例化File类的对象,指明要操作的数据对应的文件: 这里是要读入的非文本文件和要写出的非文本文件
File srcfile = new File("WallpaperDog-17170390.png");
File destfile = new File("new-png.png");
// 2 提供对应的流:这里是字节输入和输出流
fis = new FileInputStream(srcfile);
fos = new FileOutputStream(destfile);
// 3 数据的读入和写出
byte[] bytes = new byte[5];
int num;
while ((num = fis.read(bytes)) != -1){
fos.write(bytes, 0, num);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 4 流资源的关闭
if (fos != null){
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (fis != null){
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
对于定义一次要读取或要写出的字符或字节的大小,一般设置为:1024。
结论:
- 对于文本文件,使用字符流处理;
- 对于非文本文件,使用字节流处理。
实际上,也可以用字节流来传输文本文件,但是只适用于完全传输结束后再查看的文本的文件,不适用于传一个就查看一个字符的文本文件。因为,字符本质上仍是字节,字节流将字符中的数据按字节一点一点传输,如果传一个就查看一个,一般用字节流传一个不代表传一个字符,因此会乱码,而完全传输结束后再查看,就相当于完整传输完了字符对应的字节,即每个字符已经都完整了,此时再查看不存在乱码。
缓冲流
缓冲流是处理流的一种,其作用就是提高流的读写效率。
能够提高读写效率的原因:内部提供了一个缓冲区,每次读或写的数据会先存入这个缓冲区,默认这个缓冲区满时自动将缓冲区中所有数据一次性读入或写出。利用缓冲流每次写出会自动执行一个flush()
方法,就是刷新缓冲区的作用。
缓冲流包括以下四种,对应于四个抽象基类:
BufferedInputStream
BufferedOutputStream
BufferedReader
- 提供了一个新的
read
方式,即readLine()
,它可以以字符串形式读一行数据并返回字符串,但不会读取换行符,如果读到文件结尾,返回null。
- 提供了一个新的
BufferedWriter
- 提供了与
BufferedReader
中的readLine()
配对的方法newLine()
,该方法可以提供一个换行符,即写入一个换行符。
- 提供了与
对缓冲流的使用与文件流一样,遵循规范的四步,只不过现在流的使用是缓冲流,同时要注意的是缓冲流是作用在节点流之上的,因此创建流时要先有节点流,才能创建对应的缓冲流:
- 实例化File类的对象,指明要操作的数据对应的文件:读入的文件一定要存在,写出的文件可以不存在;
- 提供对应的流:使用处理流时,要先给出对应的节点流,才能创建对应的处理流;
- 数据的读入/写出:这里注意要使用缓冲流进行数据的传输;
流资源的关闭:
- 要先关闭外层的流(即外层处理流),再关闭内存的流;
- 关闭外层流的同时,内层流会自动被关闭,因此内层流的关闭可以省略。
1 使用缓冲字节流实现对非文本文件进行复制
@Test
public void test(){
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// 1 实例化File类的对象,指明要操作的数据对应的文件:读入的文件一定要存在,写出的文件可以不存在
File srcfile = new File("WallpaperDog-17170390.png");
File destfile = new File("new-buf.png");
// 2 提供对应的流:使用处理流时,要先给出对应的节点流,才能创建对应的处理流
FileInputStream fis = new FileInputStream(srcfile);
FileOutputStream fos = new FileOutputStream(destfile);
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
// 3 数据的读入/写出:这里注意要使用缓冲流进行数据的传输
byte[] bytes = new byte[10];
int num;
while ((num = bis.read(bytes)) != -1){
bos.write(bytes, 0, num);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 4 流资源的关闭
if (bos != null){
try {
bos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (bis != null){
try {
bis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
2 使用缓冲字符流对文本文件进行复制
这里用匿名对象的方式来合并第一二步骤,即文件和流的创建:
@Test
public void test(){
BufferedReader br = null;
BufferedWriter bw = null;
try {
// 1 实例化File类的对象并提供对应的流
br = new BufferedReader(new FileReader(new File("time-zone.txt")));
bw = new BufferedWriter(new FileWriter(new File("new-time.txt")));
// 2 数据的读入/写出
String line;
while ((line = br.readLine()) != null){
bw.write(line);
bw.newLine();
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 3 流资源的关闭
if (bw != null){
try {
bw.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (br != null){
try {
br.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
转换流
1 转换流的介绍
转换流是指InputStreamReader
和OutputStreamWriter
,作用是将输入的字节流转换为输入的字符流,和将输出的字符流转换为输出的字节流。
从字节到字符,对应解码过程;从字符到字节,对应编码过程,
编码和解码涉及编码集或者说字符集,因此转换流的构造都要提供对应的编码集或者说字符集,默认为当前系统的编码集。在使用转换流进行字节流到字符流的转换时编码集或者说字符集的确定取决于要处理的输入的字节流文件保存时指定的编码集或者说字符集,而使用转换流进行字符流到字节流的转换时编码集或者说字符集的确定取决于想要以哪种编码集或者说字符集保存。
InputStreamReader
的构造器和读入方法:
OutputStreamWriter
的构造器和写出方法:
InputStreamReader
和OutputStreamWriter
都是处理字符的,属于字符流的处理流,但它们都是处理在字节流之上。
2 使用转换流实现一种编码集对应的字节流转换为另一种编码集的字节流
@Test
public void test(){
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
FileInputStream fis = new FileInputStream("time-zone.txt");
FileOutputStream fos = new FileOutputStream("time-zone-gbk.txt");
isr = new InputStreamReader(fis, "utf-8");
osw = new OutputStreamWriter(fos, "gbk");
char[] cbuf = new char[10];
int data;
while ((data = isr.read(cbuf)) != -1){
osw.write(cbuf, 0, data);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (osw != null){
try {
osw.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (isr != null){
try {
isr.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
字符集(编码集)
标准输入、输出流
1 标准输入、输出流的介绍
标准输入流是指System.in
,即System
类的一个静态属性,其类型为InputStream
,代表着来自(默认是)键盘的输入流。
标准输出流是指System.out
,也是System
类的一个静态属性,其类型为PrintStream
,代表着从(默认是)控制台的输出流。
标准输入、输出流是System
的两个属性,System
类也提供了对这两个属性的set
方法,即setIn()
和setOut()
方法,通过这两个方法可以修改对应的默认输入或输出。
2 标准输入、输出流的使用示例
打印流
1 打印流的介绍
打印流是指PrintStream
和PrintWriter
,这两个流对应着字节和字符输出流,能够实现将基本数据类型的数据格式转换为字符串输出。
打印流PrintStream
和PrintWriter
的特点:
- 提供了一系列重载的
print()
和println()
方法,用于多种数据类型的输出; PrintStream
和PrintWriter
的输出不会抛出IOException
异常;PrintStream
和PrintWriter
有自动flush
的功能;PrintStream
打印的所有字符都使用平台的默认字符编码转换为字节,在需要写入字符而不是写入字节的情况下,应该使用PrintWriter
;System.out
返回的是PrintStream
的实例。
PrintStream
的构造器:
PrintWriter
的构造器:
打印流常用来与标准输出流配合,用于将程序中输出到控制台的数据转换为输出到文件中保存。
2 打印流的使用示例
数据流
1 数据流的介绍
数据流是指DataInputStream
和DataOutputStream
,用于读取或写出基本数据类型和String的数据,分别“套接”在InputStream
和OutputStream
子类的流上。
DataInputStream
的构造器和方法:
DataOutputStream
的构造器和方法:
2 数据流的使用示例
分别进行以下操作:
1)将内存中的基本数据类型和字符串数据写出到文件中。
2)将文件中存储的基本数据类型和字符串的数据读取到内存中,保存在变量中。
注意,读入的数据顺序与写出的数据顺序一致,或者说读入的数据顺序就是按照文件中第一个数据开始往后读的顺序。
对象流
1 对象流的介绍
对象流是指ObjectInputStream
和ObjectOutputStream
,是用于存储和读取基本数据类型数据和对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
对象流可以理解为一种对程序或者说内存上的基本类型数据和对象进行传输的处理流,形象理解就是,节点的两端仍是程序和文件,但传输的数据为基本类型数据和对象。
对象流对数据的传输涉及两个概念:
- 序列化:用
ObjectOutputStream
类保存基本类型数据和对象的机制; - 反序列化:用
ObjectInputStream
类读取基本数据类型和对象的机制。
需要注意的是,ObjectInputStream
和ObjectOutputStream
不能序列化或反序列化static
和transient
修饰的成员变量。换句话说,对于包含不能序列化的属性的对象的序列化,保存这种对象时,无法保存其不能序列化部分的数据,因此反序列化时,其不能序列化的部分的数据就为默认的null
或者0。
static
:该关键字的含义就是归属于类,只有一份,所有对应对象共享。因此并不是每个对象独有自己的一份。而对象流传输的对象可以理解为是一个独立的对象,因此不包括对象中“不专属”的部分;transient
:该关键字的含义就是不能序列化,因此可以理解为专门用于设置不能序列化的成员变量的修饰符。
对象序列化机制
允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其他程序获取了这种二进制流,就可以恢复成原来的Java对象。
序列化的好处在于可以将任何实现了Serializable
接口的对象转化为字节数据
,使其在保存和传输时可以被还原。
序列化是RMI(Remote Method Invoke - 远程方法调用)
过程的参数和返回值必须实现的机制,而RMI
是JavaEE的基础。因此,序列化机制是JavaEE平台的基础。
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的(默认基本数据类型是可序列化的),为了让某个类是可序列化的,该类必须实现如下两个接口之一,否则会抛出NotSerializableException
异常:
Serializable
Externalizable
2 简单示例
1) 序列化过程:将内存中的Java对象保存到磁盘中或通过网络传输出去,使用ObjectOutputStream
实现。
注意,".dat"文件扩展名通常用于表示通用的数据文件,因此其内容和格式可以各不相同。这种扩展名常常用于未经格式化的纯文本文件或二进制文件。可以简单理解为,将数据存储为这种文件上的数据不是用来查看的,而是用来传输或保存的,用于后续在具体程序上使用的。
注意,每序列化一个对象记得flush
以下,以直接保存一下。
2) 反序列化:将磁盘文件或网络中的对象还原为内存中的一个Java对象,使用ObjectInputStream
来实现。
3 类的可序列化
并不是所有对象都可以进行序列化或反序列化的,只有能够序列化的对象才能进行序列化或反序列化。
要想让一个对象能够序列化或反序列化,其对应的类要能够序列化,并且其对应的属性对应的类也要能够序列化,具体地就涉及到类的序列化。
类的序列化就是使一个类能够序列化的过程,需要对应的类满足以下两点:
- 需要实现接口
Serializable
:该接口并没有任何方法,即是一个标识接口,也就是说,只要实现了该接口的类都被标识为可序列化; - 需要提供一个全局常量
serialVersionUID
:序列版本号,是用于标识可序列化的类的唯一性,必须提供。可以理解为,当序列化多个对象时,想要将这些都序列化为字节的对象反序列化回来,是根据其序列版本号实现的,否则就还原不回来。提供了序列版本号,即使在序列化后的对象对应的类有修改,对该对象进行反序列化也能够准确地反序列化为更新后样子。而如果没有提供序列版本号,上面同样的修改类后,无法将序列化的对象反序列化回来,会报InvalidCastException
。
示例:
字节数组流
1 字节数组流的介绍
字节数组流是以自身的字节数组为端点的,即字节数组输出流就是从当前程序往自身数组输出,字节数组输入流就是从自身数组往当前程序输入。
字节数组流是指ByteArrayInputStream
和ByteArrayOutputStream
,该流的读写并不会每次都只读或写buff
大小的字节,而是内部有一个byte
数组,读或写buff
大小的字节只是读或写到对应数组中,直到全部读完或写完(数组大小不够大会自动扩充)。
这样的好处是,有的情况我们希望读取或写出字节能够完整地按照字符来读,这时就不应该用普通的字节流来读或写,这时可以使用字节数组流,全部读或写后使用其toByteArray()
或toString()`来按照字节后字符查看数据。
随机存取文件流
1 随机存取文件流的介绍
随机存取文件流是指RandomAccessFile
,其声明在java.io
包下,但直接继承于java.lang.Object
类。这意味着,该类并没有像其他流类一样继承于四个抽象基类。
随机存取文件流实现了DataInput
和DataOutput
这两个接口,也就意味着这个类可以读也可以写。
RandomAccessFile
类支持“随机访问”的方式,程序可以直接跳到文件的任意地方来读、写文件。换句话说,该类根据一个记录指针的位置,来读写文件,记录指针在内容种的哪个位置,就从哪个位置开始读写文件。
RandomAccessFile
对象包含一个记录指针,用于指示当前读写的位置,默认为0,代表当前记录指针指向第一个数据。
RandomAccessFile
类对象可以自由移动记录指针:
public long getFilePointer() throws IOException
:获取文件记录指针的当前位置;public void seek(long pos) throws IOException
:将文件记录指针定位到指定的pos位置。
2 随机存取文件流的使用
1)RandomAccessFile
的实例化
根据RandomAccessFile
的构造器可以清楚,该流类也是直接作用于文件的,并且在构造对应对象时要传入mode
参数以指定该RandomAccessFile
对象是输入流还是输出流。
RandomAccessFile
类示例时需要指定的mode
参数是用于指定RandomAccessFile
的访问模式:
r
:以只读方式访问,相当于指定为输入流;rw
:以读写方式访问,相当于指定为输出流;rwd
:以读写方式访问,并同步文件内容的更新;rws
:以读写方式访问,并同步文件内容和元数据的更新。
如果模式为只读r
,则会读取一个已经存在的文件,如果读取的文件不存在则会出现异常。如果模式为读写rw
,如果写的文件不存在,则会去创建文件。
2)RandomAccessFile
的读写
根据RandomAccessFile
的相关read()
方法可以清楚,该流类可以处理基本数据类型数据、字符串数据、字节数据等。
根据RandomAccessFile
的相关write()
方法可以清楚,与读入类似,该流类可以处理基本数据类型数据、字符串数据、字节数据等。
需要注意的是,不论是读入还是写出,该类都是从其记录指针开始。并且,如果写出的文件本身具有内容,该类的写出操作并不是从其记录指针开始插入数据,而是替换对应位置的数据。因此,一般只用来追加数据,这样才不会改变原有数据。
3 示例
1)直接替换从记录指针开始的已有内容
2)实现插入数据操作
思路:
- 将要插入位置的后面的数据先读出来,保存在
StringBulder
中,因为读出过程是一个byte数组一个byte数组的读数据,因此,需要追加数据,因此用StringBulder
; - 然后将要插入的数据写入到记录指针指定的要插入的位置;
- 最后再将之前保存下来的数据,写入到插入的数据后面。
3)实现追加数据操作
思路:
- 先获取要写入的文件当前数据的长度;
- 将记录指针指定到已有数据的最后位置;
- 写入要追加的数据即可。
NIO
第三方jar包的使用
apache
提供了很多开源的包,这个包中有一个jar
包,相当于提供了一些源码,也就是API。
在IDEA中导入第三方jar
包:
- 如果要导到一个
Module
下,右击该Module
新建一个目录,对于存放第三方jar
包的目录习惯上命名为lib
或libs
; - 将
jar
包复制进来; - 右击该
jar
包,选择Add as Library...
; - 这样该
jar
包中的各种API就可以作为API被正常使用了。