IO流
学习内容
什么是IO?
I:input (输入)
O: output (输出)
IO:通过io完成对文件的读和写
什么是IO流?
是内存和硬盘空间某个文件之间的通道,可以完成对文件的读和写
流的分类
-
按照流向 (以内存为参照物)
从硬盘空间 ———> 内存 (输入/读)
从内存 ———> 硬盘空间 (输出/写) -
按照操作单元(读取数据方式)
字节流 ——> 按字节读取 (可以操作所有的文件)
字符流——> 按字符读取 (只能操作文本文件[.txt/.html/.java等]) -
按角色划分
节点流:直接操作的是一个特定的IO流
处理流:在节点流的基础上,做进一步的处理当一个流的构造方法中需要一个流的时候,这个被传进去的流叫做节点流
外部负责包装的流叫做包装流/处理流,节点流和包装流是相对的
Colseable接口
所有的流都实现类Colseable接口,表示他们都是可关闭的close()
close() ----> 关闭流
- 所有的流(除了标准输出流)都需要手动关闭 (Java7之前)
- 要确保流一定能关闭,不关闭流会占用大量资源,可以将close()放到finally语句块中
- 对流关闭时,要先做非空判断,避免NPE
- 流的关闭顺序:先开后关,后开先关
- 包装流和节点流的关闭:只需要关闭包装流即可
- 对于输出流来说,使用close()后就不需要在flush()
AutoCloseable接口(Java7)
由于Closeable继承了AutoCloseable接口,表示所有的流,都是可自动关闭的
使用新语法:try-with-resource
-----> 在try的”()“中声明并初始化实现AutoClosealbe接口的流。
public class Demo {
@Test
public void testAutoColse() {
byte[] datas = {73, 32, 76, 111, 118, 101, 32, 89, 111, 117};
// 新语法 try-with-resource
try (OutputStream out = new FileOutputStream("1.txt")) {
out.write(datas);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在Java9之后,对try-with-resource语法进行了简化,”()“里面可以是一个变量,但必须是final的或者等同final才行。
public class Demo {
@Test
public void testAutoColse() throws FileNotFoundException {
byte[] datas = {73, 32, 76, 111, 118, 101, 32, 89, 111, 117};
OutputStream out = new FileOutputStream("1.txt");
try (out) {
out.write(datas);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Flushable接口
所有的输出流(InputStream/Writer)都实现了Flushable接口,表示所有的输出流都是可刷新的flush()
flush() ----> 刷新流
注意close()具有flush()的功能
flush()表示将通道剩余未输出的数据强行输出(清空管道),如果没有flush()可能会丢失数据
具体可以看下面的例子
public class Demo {
@Test
public void testFlush() throws IOException{
char[] datas = {73, 32, 76, 111, 118, 101, 32, 89, 111, 117};
BufferedWriter writer = new BufferedWriter(new FileWriter("1.txt"));
writer.write(datas);
// writer.flush();
}
}
流的四大家族首领
字节流
java.io.InputStream ----> 字节输入流
java.io.OutputStream ----> 字节输出流
字符流
java.io.Reader ----> 字符输入流
java.io.Writer ----> 字符输出流
需要掌握的16个流
文件专属
FileInputStream类
-
包路径:java.io.FileInputStream
-
继承关系:
-
构造方法
-
方法
FileOutputStream类
-
包路径:java.io.FileOutputStream
-
继承关系
-
构造方法(输出流初始化时——重点)
FileReader类
- 包路径:java.io.FileReader
- 继承关系
- 对于构造方法和方法,没什么需要特别注意的地方,具体内容请查看帮助文档
FileWriter类
-
包路径:java.io.FileWriter
-
继承关系
-
对于构造方法和方法,没什么需要特别注意的地方,具体内容请查看帮助文档
缓冲流
带缓冲区的输出流要记得flush()或close(),否则会导致数据丢失
BufferedInputSteam类
- 包路径:java.io.BufferedInputStream
- 继承关系
- 方法
BufferedOutputStream类
- 包路径:java.io.BufferedOutputStream
- 继承关系
BufferedReader类
-
包路径:java.io.BufferedReader
-
继承关系
-
构造方法
-
方法
BufferedWriter类
- 继承关系
- 构造方法
- 方法
转换流
将字节流转换成字符流:字节流 ----> 转换流(字符流) 可以指定字符集
InputStreamReader类
-
包路径:java.io.InputStreamReader
-
继承关系
-
构造方法
指定字符集
public class Demo01 {
@Test
public void testInputStreamReader() throws IOException {
// 未指定字符集,使用默认字符集(此处是UTF-8),在使用reader()方法读取文件数据时,以UTF-8字符集对文件数据进行(解码)
InputStreamReader reader1 = new InputStreamReader(new FileInputStream("temp.txt"));
// 指定字GBK符集,在使用reader()方法读取文件数据时,以GBK字符集对文件数据进行(解码)
InputStreamReader reader2 = new InputStreamReader(new FileInputStream("temp.txt"),"GBK");
}
}
读取文件时的乱码问题与解决方案
案例:当默认字符集为UTF-8时,尝试对这个GBK编码的文件进行读取
具体代码如下
public class Demo01 {
@Test
public void testInputStreamReader() {
// 未指定字符集,使用默认字符集(此处是UTF-8),在使用reader()方法读取文件数据时,以UTF-8字符集对文件数据进行(解码)
try(InputStreamReader reader1 = new InputStreamReader(new FileInputStream("temp.txt"));) {
int data;
while ((data = reader1.read()) != -1) {
System.out.println(data + " ----> " + (char) data);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
结果如下
为什么会出现乱码呢?
由于在reader1初始化时并未指定字符集,所以采用默认字符集(此处默认时UTF-8),所以使用reader对字节流进行读取时,会使用用UTF-8字符集进行解码1,而文件采用的编码方式时GBK,编码与解码所指定的字符集不同,所以此处出现乱码。
那么我们该怎么才能读取GBK呢?
- 设置默认字符集为GBK(不推荐)
将Global Encoding设置为GBK - 在InputStreamReader初始化时指定字符集为GBK
采用这两种方式,文件可以正常读取了
OutputStreamWriter类
- 包路径:java.io.OutputStreamWriter
- 继承关系
- 构造方法
指定字符集
public class Demo01 {
@Test
public void testOutputStreamWriter() throws FileNotFoundException {
// 未指定字符集,使用默认字符集(此处是UTF-8),在使用writer()方法将数据写入文件时,以UTF-8字符集对数据进行(编码)
OutputStreamWriter writer1 = new OutputStreamWriter(new FileOutputStream("temp.txt"));
// 未指定字符集,使用默认字符集(此处是UTF-8),在使用writer()方法将数据写入文件时,以UTF-8字符集对数据进行(编码)
OutputStreamWriter writer2 = new OutputStreamWriter(new FileOutputStream("temp.txt"));
}
}
注意:
如果文件不存在,则创建指定字符集的文件
如果文件存在,未设置追加,则创建指定字符集文件并覆盖原文件
如果文件存在,并设置了追加,则不会覆盖原文件也不会改变原文件编码所用字符集
写入文件时的乱码问题与解决方案
对于OutputStreamWriter,只有在文件存在、设置追加、字符集与原文件不同时,才可能会出现乱码情况
案例:当默认字符集为UTF-8时,尝试对这个GBK编码的文件进行读取
@Test
public void testOutputStreamWriter() {
// 未指定字符集,使用默认字符集(此处是UTF-8),在使用writer()方法将数据写入文件时,以UTF-8字符集对数据进行(编码)
try(OutputStreamWriter writer1 = new OutputStreamWriter(new FileOutputStream("temp.txt",true));) {
writer1.write("\n我是xxx");
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
结果如下
为什么会出现乱码?
由于在writer1初始化时并未指定字符集,所以采用默认字符集(此处默认时UTF-8),所以使用writer1将数据写入文件时,会使用用UTF-8字符集对数据进行编码2,而文件采用的编码方式是GBK,由于两者采用不同的字符编码,所以在文件中写入的数据会出现乱码问题。
如何解决乱码问题? ----> 与上面处理输入转换流方式相同
数据流
完成对数据的加密与解密
- 使用DataOutputStream进行加密 —> 将数据类型和数据一同写入
- 使用DataInputStream进行解密 —> 读取文件时必须保证和写入的顺序相同
DataOutputStream类
对数据进行加密
public class Demo01 {
@Test
public void testDataOutputStream() {
try(DataOutputStream dataOut = new DataOutputStream(new FileOutputStream("secret.key"))) {
dataOut.writeInt(1197667190);
dataOut.writeInt(123456);
dataOut.writeChar('男');
dataOut.writeInt(18);
dataOut.writeDouble(3.14);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
加密后,加密文件打开是这样的
DataInputStream类
对数据进行解密
public class Demo01 {
@Test
public void testDataInputStream() {
try(DataInputStream dataIn = new DataInputStream(new FileInputStream("secret.key"))) {
System.out.println("用户名:" + dataIn.readInt());
System.out.println("密码:" + dataIn.readInt());
System.out.println("性别:" + dataIn.readChar());
System.out.println("年龄:" + dataIn.readInt());
System.out.println("圆周率:" + dataIn.readDouble());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
解密后文件的内容
标准输出流
注意:标准输出流不需要手动close(),但一定要flush(),尤其是带缓冲区的流
PrintStream类
- 包路径:java.io.PrintStream
- 继承关系
- 对于构造方法和方法,没什么需要特别注意的地方,具体内容请查看帮助文档
我们常用的输出语句"System.out.println()",便用到了标准输出流,其中"out"便是System类下的一个静态属性,这个“out”是PrintStream类型,默认将内容输出到控制台。那么我们可以更改输出语句的输出位置么?答案是可以的,我们可以通过调用"System.setOut()"这个方法来设置out属性。
获取System.out
package com.jsoft.io;
import org.junit.Test;
import java.io.*;
public class Demo01 {
@Test
public void testPrintStream() {
// 获取System.out
PrintStream myOut = System.out;
// 向控制台输出内容
myOut.println("潘翀,我是你爹");
}
}
更改System.out (更改输出语句的输出位置)
package com.jsoft.io;
import org.junit.Test;
import java.io.*;
public class Demo01 {
@Test
public void testPrintStream() throws FileNotFoundException {
PrintStream myOut = new PrintStream("temp.txt");
// 更改System.out的输出位置(控制台 ---> temp.txt)
System.setOut(myOut);
System.out.println("向temp.txt输出的内容");
// 将System.out修改回原输出位置(temp.txt ---> 控制台)
System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
System.out.println("向控制台输出的内容");
}
}
PrintWriter类
-
包路径:java.io.PrintWriter
-
继承结构
-
对于构造方法和方法,和PrintStream基本没啥区别,具体内容请查看帮助文档
对象专属流
ObjectOutputStream类
完成对象的序列化(Serialize):
ObjectInputStream类
完成文件的反序列化(DeSerialize):
序列化与反序列化
- 序列化:将内存中的java对象存储在文件中(java对象 --> 文件)
- 反序列化:将文件中保存的java对象放到内存中(文件 --> java对象)
- 参与序列化的对象必须实现Serializable接口,否则会在该对象序列化时抛出异常
- Serializable中什么也没有是一个标志接口,这个标志接口是给JVM参考的
- 如果实现了Serializable接口后并未给定序列化版本号,则经过编译后会赠送一个随机的序列号版本号
- 序列化版本号是用来区分类的,在Java中是如何区分类的?2
- 对于参与序列化的对象,在编写代码时一定要指定一个确定的序列化版本号,因为如果使用编译器赠送的版本号,在代码经过改动后重新编译会生成一个不同的序列化版本号,最终可能会导致反序列化失败
对于第5条,我们来看一下
import java.io.Serializable;
public class Person implements Serializable {
private Integer id; // 身份证
private String name; // 姓名
// Constructor
public Person() {}
public Person(Integer id, String name) {
this.id = id;
this.name = name;
}
}
对于第6、7条,我们来验证一下,如果修改代码后是否会生成不同的版本号?,版本号不同(也就是说类不同)是否可以通过反序列化?
首先时对Person对象进行序列化,此时序列化文件serialFile中保存的序列号版本号是-1249726909036910803
import java.io.*;
public class Serialize {
public static void main(String[] args) throws Exception {
// 完成序列化(Person对象 ----> serialFile)
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialFile"));
out.writeObject(new Person(1001,"张三"));
}
}
在Person类中添加一个新的属性,并重新对Person进行编译,此时Person类中的版本号变成了41406859053625754
import java.io.Serializable;
public class Person implements Serializable {
private Integer id; // 身份证
private String name; // 姓名
private Integer age; // 新添加的年龄
// Constructor
public Person() {}
public Person(Integer id, String name) {
this.id = id;
this.name = name;
}
}
对serialFile进行反序列化,此时serialFile中保存的序列化版本号是-1249726909036910803,而Person类中保存的序列化版本号是41406859053625754,所以这两个类“并不相同”,所以抛出异常,无法完成序列化
import java.io.*;
public class DeSerialize {
public static void main(String[] args) throws Exception {
// 完成反序列化(serialFile ----> Person对象)
ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialFile"));
System.out.println(in.readObject());
}
}
transient关键字
transient(游离的、不参与序列化的)