一、IO流技术
输入(Input)指的是:可以让程序从外部系统获得数据(核心含义是“读”,读取外部数据)。
输出(Output)指的是:程序输出数据给外部系统从而可以操作外部系统(核心含义是“写”,将数据写出到外部系统)。
java.io包为我们提供了相关的API,实现了对所有外部系统的输入输出操作,这就是我们这章所要学习的技术。
数据源:
数据源data source,提供数据的原始媒介。常见的数据源有:数据库、文件、其他程序、内存、网络连接、IO设备:
数据源分为:
- 源设备:为程序提供数据,一般对应输入流;
- 目标设备:程序数据的目的地,一般对应输出流;
流:
流是一个抽象、动态的概念,是一连串连续动态的数据集合。
对于输入流而言,数据源就像水箱,流(stream)就像水管中流动着的水流,程序就是我们最终的用户。我们通过流(A Stream)将数据源(Source)中的数据(information)输送到程序(Program)中。
对于输出流而言,目标数据源就是目的地(dest),我们通过流(A Stream)将程序(Program)中的数据(information)输送到目的数据源(dest)中。
流与源数据源和目标数据源之间的关系:
1.1 两种程序写法
try-catch-finally
/**
* IO程序的经典写法
*/
public class Test01 {
public static void main(String[] args) {
FileInputStream fis = null; // 作用域为全局
StringBuilder sb = new StringBuilder(); // 用于字符串拼接
try{
fis = new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\a.txt");
int temp = 0;
// 当temp等于-1时,表示已经到了文件结尾,停止读取
while( (temp = fis.read()) != -1){
sb.append( (char)temp );
}
System.out.println(sb);
}catch (Exception e){
e.printStackTrace();
}finally {
try {
// 保证了即使遇到异常情况,也会关闭流对象
if( fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// 输出结果
kk 123456 sgsg
try-with-resource
在JDK7以及以后的版本中可以使用try-with-resource语法更优雅的关闭资源。
java.lang.AutoCloseable接口:
在java.lang.AutoCloseable
接口中包含了一个close
方法,该方法用于关闭资源。
只要是实现了java.lang.AutoCloseable
接口的对象,都可以使用try-with-resource
关闭资源,使用最新的try-with-resource
简化:
/**
* try-with-resource写法
*/
public class Test02 {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\a.txt")) {
StringBuilder sb = new StringBuilder();
int temp = 0;
while ((temp = fis.read()) != -1) {
sb.append((char) temp);
}
System.out.println(sb);
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.2 流概念的细分与体系
1.2.1 流的细分
按流的方向分类:
- 输入流:数据流向是数据源到程序(以InputStream、Reader结尾的流);
- 输出流:数据流向是程序到目的地(以OutPutStream、Writer结尾的流);
按处理的数据单元分类:
- 字节流:以字节为单位获取数据,命名上以Stream结尾的流一般是字节流,如FileInputStream、FileOutputStream;
- 字符流:以字符为单位获取数据,命名上以Reader/Writer结尾的流一般是字符流,如FileReader、FileWriter;
按处理对象不同分类:
- 节点流:可以直接从数据源或目的地读写数据,如FileInputStream、FileReader、DataInputStream等;
- 处理流:不直接连接到数据源或目的地,是”处理流的流”。通过对其他流的处理提高程序的性能,如BufferedInputStream、BufferedReader等。处理流也叫包装流;
节点流处于IO操作的第一线,所有操作必须通过它们进行;处理流可以对节点流进行包装,提高性能或提高程序的灵活性。
1.2.2 流的体系
Java为我们提供了多种多样的IO流,我们可以根据不同的功能及性能要求挑选合适的IO流,如图所示,为Java中IO流类的体系。
抽象类:
- InputStream / OutputStream:字节流的抽象类;
- Reader / Writer:字符流的抽象类;
节点流
- FileInputStream / FileOutputStream:节点流,以字节为单位直接操作“文件”;
- ByteArrayInputStream / ByteArrayOutputStream:节点流,以字节为单位直接操作“字节数组对象”;
- FileReader/FileWriter:节点流,以字符为单位直接操作“文本文件”(注意:只能读写文本文件);
处理流:
- ObjectInputStream / ObjectOutputStream:处理流,以字节为单位直接操作“对象”;
- DataInputStream / DataOutputStream:处理流,以字节为单位直接操作“基本数据类型与字符串类型”;
- BufferedReader / BufferedWriter:处理流,将Reader/Writer对象进行包装,增加缓存功能,提高读写效率;
- BufferedInputStream / BufferedOutputStream:处理流,将InputStream/OutputStream对象进行包装,增加缓存功能,提高读写效率;
- InputStreamReader / OutputStreamWriter:处理流,转换流,将字节流对象转化成字符流对象;
- PrintStream:处理流,将OutputStream进行包装,可以方便地输出字符,更加灵活;
1.3 IO中四大抽象类
InputStream / OutputStream
和 Reader / writer
类是所有IO流类的抽象父类。
InputStream
此抽象类是表示字节输入流的所有类的父类。InputSteam是一个抽象类,它不可以实例化。 数据的读取需要由它的子类来实现。根据节点的不同,它派生了不同的节点流子类 。
继承自InputSteam的流都是用于向程序中输入数据,且数据的单位为字节(8 bit)。
常用方法:
方法名 | 使用说明 |
int read() | 读取一个字节的数据,并将字节的值作为int类型返回(0-255之间的一个值)。如果未读出字节则返回-1(返回值为-1表示读取结束) |
void close() | 关闭输入流对象,释放相关系统资源 |
OutputStream
此抽象类是表示字节输出流的所有类的父类。输出流接收输出字节并将这些字节发送到某个目的地。
方法名 | 使用说明 |
void write(int n) | 向目的地中写入一个字节 |
void close() | 关闭输出流对象,释放相关系统资源 |
Reader
Reader用于读取的字符输入流抽象类,数据单位为字符。
方法名 | 使用说明 |
int read() | 读取一个字符的数据,并将字符的值作为int类型返回(0-65535之间的一个值,即Unicode值)。如果未读出字符则返回-1(返回值为-1表示读取结束) |
void close() | 关闭流对象,释放相关系统资源 |
Writer
Writer用于输出的字符输出流抽象类,数据单位为字符。
方法名 | 使用说明 |
void write(int n) | 向输出流中写入一个字符 |
void close() | 关闭输出流对象,释放相关系统资源 |
1.4 文件字节流-FileStream
FileInputStream通过字节的方式读取文件,适合读取所有类型的文件(图像、视频、文本文件等);
FileOutputStream通过字节的方式写数据到文件中,适合所有类型的文件(图像、视频、文本文件等);
FileInputStream:
/**
* 文件字节输入流
*/
public class FileInputStreamTest {
public static void main(String[] args) {
try(FileInputStream fis = new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\a.txt")){
StringBuilder sb = new StringBuilder();
int temp = 0;
while( (temp = fis.read()) != -1 ){
sb.append((char)temp);
}
System.out.println(sb);
}catch (IOException e) {
throw new RuntimeException(e);
}
}
}
FileOutputStream:
/**
* 字节输出流
*/
public class FileOutputStreamTest {
public static void main(String[] args) {
String str = "kkkkkkk 18";
// true表示内容会追加到文件末尾(append),false表示覆盖写
try(FileOutputStream fos = new FileOutputStream("D:\\JAVA\\workSpace\\ioDemo\\a.txt",true)){
// 将整个字节数组写入到文件中
fos.write(str.getBytes());
// 将数据从内存写入到磁盘中
fos.flush();
}catch(IOException e){
e.printStackTrace();
}
}
}
注意:
fos.flush()
操作才是将数据从内存写入磁盘!
1.4.1 通过字节缓冲区提高读写效率
通过创建一个指定长度的字节数组作为缓冲区,以此来提高IO流的读写效率。该方式适用于读取较大文件时的缓冲区定义。
注意:缓冲区的长度一定是2的整数幂。一般情况下1024B
的长度较为合适。
Q:为什么加缓冲区会提高读写IO的效率?
答:因为在操作系统中,CPU、内存处理的速度是非常快的,而与硬件交互的IO操作往往是低了很多个数量级的;如果还是采用上述一个字节一个字节进行一次写入的话,一个字节产生一次IO操作,但是实际上文件在内存中和磁盘中的存储单元是远远大于1个B的(以2的整数次幂为标准),也就是说一次IO写入或读出的数据只要不超过这个范围就可以一次性写入或读出;
所以建立缓冲区的作用就是把数据囤一囤再一块进行IO,这样最耗时的IO就用最少的次数却能达到一样的效果,速度和效率就提升了;
来看看chat老师的答案:
IO操作引入缓冲区可以提高效率的原因主要有以下几点:
- 减少系统调用次数: 操作系统在执行IO操作时需要切换上下文,这是一项相对较为昂贵的操作。使用缓冲区可以减少实际的IO操作次数,因为数据可以在内存中累积一段时间,然后一次性写入磁盘或从磁盘读取,从而减少了系统调用的频率。
- 减少磁盘访问次数: 磁盘IO是相对慢速的操作,通过使用缓冲区,可以将多个小的IO操作合并为一个较大的IO操作,从而减少了磁盘访问的次数。这有助于减少磁盘头寻道的时间,提高了IO操作的效率。
- 数据局部性: 缓冲区可以使数据在内存中形成局部性,这意味着相关数据项在内存中是相邻的。当数据按照顺序存储在内存中时,CPU可以更有效地访问这些数据,而不需要频繁地在内存中进行跳跃。
- 异步IO: 缓冲区可以用于实现异步IO操作。在异步IO中,应用程序可以继续执行其他任务,而不必等待IO操作完成。当IO操作完成后,数据会被复制到应用程序的缓冲区中,这提高了程序的并发性和响应性。
- 数据重用: 缓冲区允许多次访问相同的数据,而无需重新从磁盘读取或重新写入。这在需要多次处理相同数据的情况下可以大大减少IO操作的成本。
总之,引入缓冲区可以将多个小的IO操作合并成更大的操作,减少了系统调用和磁盘访问的次数,提高了IO操作的效率,同时也提供了更好的数据管理和数据访问方式,有助于提高应用程序的性能和响应速度。
测试:
/**
* 采用字节缓冲区进行文件复制
*/
public class FileStreamBuffer {
public static void main(String[] args) {
long t1 = System.currentTimeMillis();
copyFile1("D:\\JAVA\\workSpace\\ioDemo\\1.png","D:\\JAVA\\workSpace\\ioDemo\\2.png");
long t2 = System.currentTimeMillis();
System.out.println("未引入缓冲区耗时: " + (t2 - t1));
long t3 = System.currentTimeMillis();
copyFile2("D:\\JAVA\\workSpace\\ioDemo\\1.png","D:\\JAVA\\workSpace\\ioDemo\\3.png");
long t4 = System.currentTimeMillis();
System.out.println("引入缓冲区后耗时: " + (t4 - t3));
}
/**
* 实现文件的拷贝(未引入缓冲区)
* @param sour 源文件
* @param dest 目的文件
*/
public static void copyFile1(String sour, String dest){
try(FileInputStream fis = new FileInputStream(sour);
FileOutputStream fos = new FileOutputStream(dest);){
int temp = 0;
while( ( temp = fis.read()) != -1){
fos.write(temp);
}
// 将文件从内存写入磁盘
fos.flush();
}catch(IOException e){
e.printStackTrace();
}
}
/**
* 实现文件的拷贝(引入缓冲区)
* @param sour 源文件
* @param dest 目的文件
*/
public static void copyFile2(String sour, String dest){
try(FileInputStream fis = new FileInputStream(sour);
FileOutputStream fos = new FileOutputStream(dest);){
byte[] buffer = new byte[1024]; // 缓冲数组
int temp = 0;
while( ( temp = fis.read(buffer)) != -1){
// 每次都从buffer中取本次相对位置0开始temp字节长度的内容写入内存
fos.write(buffer,0,temp);
}
// 将文件从内存写入磁盘
fos.flush();
}catch(IOException e){
e.printStackTrace();
}
}
}
输出结果:
未引入缓冲区耗时: 10108
引入缓冲区后耗时: 16
结果还是很明显的,一千倍左右的差距;
注意:
- 为了减少对硬盘的读写次数,提高效率,通常设置缓存数组。相应地,
>读取时使用的方法为:read(byte[] b)
;
>写入时的方法为:write(byte[ ] b, int off, int length)
;
- 程序中如果遇到多个流,每个流都要单独关闭,防止其中一个流出现异常后导致其他流无法关闭的情况;关闭的顺序为:后开先关!
1.4.2 缓冲字节流-BufferedStream
Java缓冲流本身并不具有IO流的读取与写入功能,只是在别的流(节点流或其他处理流)上加上缓冲功能提高效率,就像是把别的流包装起来一样,因此缓冲流是一种处理流(包装流);
BufferedInputStream
和BufferedOutputStream
这两个流是缓冲字节流,通过内部缓存数组来提高操作流的效率;缓存区的大小默认是8192字节
,也可以使用其它的构造方法自己指定大小。
/**
* 字节缓冲流的基本使用
*/
public class BufferedStream {
public static void main(String[] args) {
long t1 = System.currentTimeMillis();
copyFile3("D:\\JAVA\\workSpace\\ioDemo\\1.png","D:\\JAVA\\workSpace\\ioDemo\\4.png");
long t2 = System.currentTimeMillis();
System.out.println("使用缓冲字节流耗时: " + (t2-t1));
}
/**
* 使用字节缓冲流进行文件复制
* @param sour 源文件
* @param dest 目的文件
*/
public static void copyFile3(String sour, String dest){
try(FileInputStream fis = new FileInputStream(sour);
FileOutputStream fos = new FileOutputStream(dest);
// 创建缓冲字节流(处理流)
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);){
int temp = 0;
while( ( temp = bis.read()) != -1){
bos.write(temp);
}
bos.flush();
}catch (IOException e ){
e.printStackTrace();
}
}
}
// 控制台输出
使用缓冲字节流耗时: 57
1.5 文件字符流-FileReader/FileWriter
当执行IO操作处理的文件类型是文本文件时,可以使用文件字符流进行读写,会以字符为单位进行处理;
FileReader:
/**
* 文件字符输入流
*/
public class FileReaderTest {
public static void main(String[] args) {
try(FileReader fr = new FileReader("D:\\JAVA\\workSpace\\ioDemo\\aa.txt")){
int temp = 0;
StringBuilder sb = new StringBuilder();
while( ( temp = fr.read()) != -1){
sb.append((char)temp);
}
System.out.println(sb);
}catch (IOException e){
e.printStackTrace();
}
}
}
// 输出结果
文件字符输入流 FileReader测试
FileWriter:
/**
* 文件字符输出流
*/
public class FileWriterTest {
public static void main(String[] args) {
try(FileWriter fw = new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aa.txt",true)){
String str = "当执行IO操作处理的文件类型是文本文件时,可以使用文件字符流进行读写,会以字符为单位进行处理;";
fw.write(str);
fw.flush();
}catch (IOException e ){
e.printStackTrace();
}
}
}
1.5.1 缓冲字符流-BufferedReader/BufferedWriter
乍一看文件字符流的作用,好像和文件字节流没有什么区别,这难道是鸡肋的类吗??
实际情况下,文本文件中包含有换行段落等间隔符,如果以文件字节流读取还需要进行转换,提高了操作成本;这个时候结合缓冲字符流(处理流)的文件字符流(节点流)就可以轻易地对文本文件进行行段的处理;
BufferedReader:
- 基本方法:
- 测试:
对于下面的文本文件,我们采用BufferedReader
进行读取:
/**
* 字符输入缓冲流
*/
public class BufferedReaderTest {
public static void main(String[] args) {
// 简化创建
try (BufferedReader br = new BufferedReader(new FileReader("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"))){
String temp = "";
// readLine() 一次读取一行
while( (temp = br.readLine()) != null){
System.out.println(temp);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
//输出结果
当执行IO操作处理的文件类型是文本文件时,可以使用文件字符流进行读写;
会以字符为单位进行处理;当执行IO操作处理的文件类型是文本文件时;
可以使用文件字符流进行读写,会以字符为单位进行处理;
当执行IO操作处理的文件类型是文本文件时\br,
可以使用文件字符流进行读写,会以字符为单位进行处理;
当执行IO操作处理的文件类型是文本文件时,
可以使用文件字符流进行读写,会以字符为单位进行处理;
BufferedWriter:
- 基本方法:
- 测试:
/**
* 字符输出缓冲流
*/
public class BufferedWriterTest {
public static void main(String[] args) {
try(BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"))){
bw.write("白日依山尽");
bw.newLine(); // 换行
bw.write("黄河入海流");
bw.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
案例1:为文件中的内容添加行号
/**
* 为文件的内容添加行号
*/
public class LineNumberTest {
public static void main(String[] args) {
try(BufferedReader br = new BufferedReader(new FileReader("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aaa.txt"))){
int index = 1; // 定义序号
String temp = "";
while( (temp = br.readLine()) != null){
bw.write(index +". " + temp);
index++;
bw.newLine(); // 换行
}
bw.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
//aa.txt
白日依山尽
黄河入海流
// aaa.txt
1. 白日依山尽
2. 黄河入海流
1.6 转换流-InputStreamReader/OutputStreamWriter
InputStreamReader / OutputStreamWriter用来实现将字节流转化成字符流。
即:
- 在输入端,将字节数据转换为字符数据进行处理;
- 在输出端,将字节数据转换为字符数据进行处理;
常用的转换流功能是:解决乱码问题;
乱码的产生是由于编码的类型和阶码的类型不匹配导致的;
案例1:解决乱码问题
采用ANSI
(实际上是GBK
)对文本文件进行编码,使用转换流进行乱码处理:
/**
* 转换流的使用1(出现乱码)
*/
public class InputStreamReaderTest {
public static void main(String[] args) {
try(InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aaa1.txt"))){
StringBuilder sb = new StringBuilder();
int temp = 0;
while( ( temp = isr.read()) != -1){
sb.append((char)temp);
}
System.out.println(sb);
}catch (IOException e){
e.printStackTrace();
}
}
}
//输出结果
1. ������ɽ��
2. �ƺ��뺣��
由于Java是采用UTF-8作为编码格式的,因此实际上就存在着文件编码和解码类型不匹配的问题;
解决:
try(InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aaa1.txt"),"gbk")){
只需要在转换流的创建中加入和编码对应的解码类型即可(此处指定为gbk
);
// 输出结果
1. 白日依山尽
2. 黄河入海流
案例2:通过字节流读取文本文件并添加行号
分析题意:
基于上图所示的代码如下:
/**
* 转换流的使用2
*/
public class InputStreamReaderTest {
public static void main(String[] args) {
// 依次创建转换流(字节输入流)、字符输出缓冲流
try(InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"),"utf-8");
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aaaa.txt"));){
//
StringBuilder sb = new StringBuilder();
int temp = 0;
int index = 1;
while( ( temp = isr.read()) != -1){
bw.write(index + ". "+(char)temp);
bw.newLine();
index++;
}
bw.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
注意第7-8行,程序只是将字节输入流转换成了字符输入流,此时读取的方式还是逐个字符逐个字符读取,这导致第13-14行中每一个while循环中isr.read()返回的都是一个字符的ASCII码,而非一行的!!!
写入文件的结果如上图所示,这个并非我们想要的结果;
改进:
上述问题的原因在于输入流最后的处理形式是“逐字符”而非“逐行”!!所以在转换流将字节流转为字符流后,还需要将字符流转换为字符缓冲流,使用其中的readLine()方法才能实现“逐行”;
/**
* 转换流的使用2
*/
public class InputStreamReaderTest {
public static void main(String[] args) {
// 依次创建转换流(字节输入流)、字符缓冲流、字符输出缓冲流
try(InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"),"utf-8");
BufferedReader br = new BufferedReader(isr); // 字符输入缓冲流
BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\JAVA\\workSpace\\ioDemo\\aaaa.txt"));){
//
StringBuilder sb = new StringBuilder();
String temp = null;
int index = 1;
while( ( temp = br.readLine()) != null){ // 逐行读取
bw.write(index + ". "+temp);
bw.newLine();
index++;
}
bw.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
结果:
案例3:通过转换流实现键盘输入屏幕输出
首先分析“键盘输入”是如何实现的,可能很快就会想到是System.in,那么同理屏幕输出是System.out,那么它们都是什么类型的输入输出流对象呢?
public final static PrintStream out = null; // out对象定义
public final static InputStream in = null; // in对象定义
可以看到它们都是字节流对象,那么就要考虑实际情况了,键盘的输入是包含多字符、可换行的,那么在处理输入流时就要将其转换成缓冲字符输入流进行逐行处理;同理屏幕的输出是包含多字符、可换行的,因此输出流应该是缓冲字符输出流;
/**
* 案例3:通过转换流实现键盘输入屏幕输出
*/
public class ConvertStreamTest3 {
public static void main(String[] args) {
// 实现 字节流->字符流->缓冲字符流 的转换
try(BufferedReader br = new BufferedReader( new InputStreamReader( System.in));
BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( System.out));){
String inputStr = "";
while( !( inputStr = br.readLine()).equals("quit")){
bw.write(inputStr);
bw.newLine();
bw.flush();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
输出结果:
sjakjl
sjakjl
456
456
撒寄快递
撒寄快递
quit
Process finished with exit code 0
1.7 字符输出流-PrintWriter
基于前文可以知道,要在文件中写入带有换行的字符是需要用到两个流对象的:缓冲字符输出流BufferedWriter(处理流)中嵌套文件字符输出流FileWriter(节点流);
PrintWriter集两者为一体,具有自动行刷新缓冲字符输出流,特点是可以按行写出字符串,并且可通过
println()
方法实现自动换行;
案例1:通过PrintWriter为文件添加行号
/**
* 添加行号
*/
public class PrintWriterTest {
public static void main(String[] args) {
try(BufferedReader br = new BufferedReader(new FileReader("D:\\JAVA\\workSpace\\ioDemo\\aa.txt"));
PrintWriter pw = new PrintWriter("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt");){
int index =1;
String temp = "";
while( ( temp = br.readLine()) != null ){
pw.println((index++) +". " + temp);
}
pw.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
//aa.txt
白日依山尽
黄河入海流
//aaaaa.txt
1. 白日依山尽
2. 黄河入海流
1.8 数据流-DataStream
数据流将“基本数据类型与字符串类型”作为数据源,从而允许程序以与机器无关的方式从底层输入输出流中操作Java基本数据类型与字符串类型。
DataInputStream
和DataOutputStream
提供了可以存取与机器无关的所有Java基础类型数据(如:int、double、String等)的方法;
public class TestDataStream {
public static void main(String[] args) {
//创建数据输出流对象与文件字节输出流对象
try(DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/data"));
//创建数据输入流对象与文件字节输入流对象
DataInputStream dis = new DataInputStream(new FileInputStream("d:/data"))){
//将如下数据写入到文件中
dos.writeChar('a');
dos.writeInt(10);
dos.writeDouble(Math.random());
dos.writeBoolean(true);
dos.writeUTF("你好中国");
//手动刷新缓冲区:将流中数据写入到文件中
dos.flush();
//直接读取数据:读取的顺序要与写入的顺序一致,否则不能正确读取数据。
System.out.println("char: " + dis.readChar());
System.out.println("int: " + dis.readInt());
System.out.println("double: " + dis.readDouble());
System.out.println("boolean: " + dis.readBoolean());
System.out.println("String: " + dis.readUTF());
}catch(IOException e){
e.printStackTrace();
}
}
}
1.9 对象流-ObjectStream
其中嵌套的是字节流;
1.9.1 基本使用:
/**
* 基本使用
*/
public class ObjectStreamTest1 {
public static void main(String[] args) {
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt",false));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt"));){
// 写入文件
oos.writeInt(100);
oos.writeChar('a');
oos.writeBoolean(false);
oos.writeUTF("卡卡的");
oos.flush(); // 刷新
// 读出文件内容
System.out.println("int: "+ois.readInt());
System.out.println("char: "+ois.readChar());
System.out.println("boolean: "+ois.readBoolean());
System.out.println("String: "+ois.readUTF());
}catch (IOException e){
e.printStackTrace();
}
}
}
1.9.2 序列化和反序列化
当两个进程远程通信时,彼此可以发送各种类型的数据。 无论是何种类型的数据,都会以二进制序列的形式在网络上传送。比如,我们可以通过http协议发送字符串信息;我们也可以在网络上直接发送Java对象。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象才能正常读取。
序列化:把数据结构或Java对象转换为二进制字节序列的过程;
反序列化:字节序列恢复为数据结构或Java对象的过程;
涉及的类和接口:
ObjectOutputStream
代表对象输出流,它的writeObject(Object obj)
方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;
ObjectInputStream
代表对象输入流,它的readObject()
方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回;
只有实现了Serializable
接口的类的对象才能被序列化。 Serializable
接口是一个空接口(标识接口),只起到标记作用;
序列化和反序列化常见应用场景:
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
Q:如果有些字段不想进行序列化怎么办?
对于不想进行序列化的变量,可以使用transient
关键字修饰;
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient
修饰的变量值不会被持久化和恢复;
几点注意:
- transient 只能修饰变量,不能修饰类和方法。
- transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰
int
类型,那么反序列后结果就是0
; static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
参考博客:
序列化
/**
* 实现序列化
*/
public class ObjectStreamTest2 {
public static void main(String[] args) {
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt"))){
User user = new User(1,"kk",18);
oos.writeObject(user);
oos.flush();
}catch (IOException e){
e.printStackTrace();
}
}
}
反序列化
/**
* 实现反序列化
*/
public class ObjectStreamTest3 {
public static void main(String[] args) {
try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\JAVA\\workSpace\\ioDemo\\aaaaa.txt"))){
User user = (User) ois.readObject();
System.out.println(user);
}catch (IOException | ClassNotFoundException e){
e.printStackTrace();
}
}
}
// 输出结果
Users{userid=1, username='kk', userage='18'}
1.10 IO中的设计模式思考
装饰器模式:是GOF23种设计模式中较为常用的一种模式。它可以实现对原有类的包装和装饰,使新的类具有更强的功能。
IO流体系中大量使用了装饰器模式,让流具有更强的功能、更强的灵活性。比如:
FileInputStream fis = new FileInputStream(src);
BufferedInputStream bis = new BufferedInputStream(fis);
显然BufferedInputStream装饰了原有的FileInputStream,让普通的FileInputStream也具备了缓存功能,提高了效率。
致谢&部分资源出处:百战程序员