【学习笔记】IO 流(一)
流的概念
-
流是内存与存储设备之间传输数据的通道。
-
如果保存在内存中的程序,想要读取硬盘中的文件,那么内存和硬盘之间就要建立一条通道。反之如果想要把内存中的数据保存到硬盘中,也需要建立一条通道
-
-
数据借助流传输,就像水借助管道传输
流的分类
-
按方向:
-
输入流:将<存储设备>中的内容读入到<内存>中。
-
输出流:将<内存>中的内容写入到<存储设备>中。
-
文件 ----输入流-------> 程序 -------输出流--------> 文件
-
-
按单位:
-
字节流:以字节为单位,可以读写所有数据
-
字符流:以字符为单位,只能读写文本数据
-
-
按功能:
-
节点流:具有实际传输数据的读写功能,也叫底层流
-
过滤流:在节点流的基础之上增强功能,比如缓冲流
-
字节流
-
字节流的父类(抽象类):
-
InputStream: 字节输入流
-
public int read(){}
-
public int read(byte[] b){}
-
public int read(byte[] b, int off, int len){}
-
-
OutputStream: 字节输出流
-
public int write(){}
-
public int write(byte[] b){}
-
public int write(byte[] b, int off, int len){}
-
-
文件字节流
-
FileInputStream:继承InputStream
-
public int read(byte[] b){} //从流中读取多个字节,将读到的内容存入b数组,返回实际读到的字节数,如果达到文件的尾部,则返回-1
-
构造方法:FileInputStream(String name) //name 为文件的路径
-
public int read(){} //读取一个字节
-
使用FileInputStream
1.读取单个字节
package com.iosteam;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class Demo01 {
public static void main(String[] args) throws Exception {
//创建FileInputSteam,并指定文件路径
FileInputStream fis = new FileInputStream("f:\\aaa.txt");
//读单个字节
int data = 0; //用来保存读取的字节
while((data = fis.read()) != -1){
System.out.println((char) data);
}
}
}
这样的效率并不高,原因是读取一个字节就打印一个字节
我们先读取多个字节在存到数组中,最后打印
2.读取多个字节
//读取多个字节
byte[] bytes = new byte[3]; //先创建一个长度为3的数组,用来存储
int count = fis.read(bytes); //返回的是读取字节的个数
System.out.println(new String(bytes)); //把数组转成字符串
System.out.println(count);
这时候,只读出了 abc 3个字节,原因是我们创建的保存数组长度就是3
要想把其他的字节也读出来就要继续读
//读取多个字节
byte[] bytes = new byte[3]; //先创建一个长度为3的数组,用来存储
int count = fis.read(bytes); //返回的是读取字节的个数
System.out.println(new String(bytes)); //把数组转成字符串
System.out.println(count);
int count2 = fis.read(bytes); //返回的是读取字节的个数
System.out.println(new String(bytes)); //把数组转成字符串
System.out.println(count2);
int count3 = fis.read(bytes); //返回的是读取字节的个数
System.out.println(new String(bytes,0,count3)); //把数组转成字符串
System.out.println(count3);
需要注意的是:在最后一次读取的时候,只剩下一个字节,我们在转成字符串时,要给它多传两个参数,offset
- 要解码的第一个字节的索引length
- 要解码的字节数,即代码中的0和count3
否则,上一次读取的后两个字节 ef 就会在这个数组中,结果就是gef了.
这样读取三次不是我们想要读取的方式,应该用循环来写
//读取多个字节
byte[] bytes = new byte[3]; //先创建一个长度为3的数组,用来存储
int count = 0; //返回的是读取字节的个数
while((count = fis.read(bytes)) != -1){ //等于-1 就说明读取完毕
System.out.println(new String(bytes,0,count));
}
一般我们创建的数组缓冲区的大小不会这么小
-
FileOutputStream : 继承OutputStream
-
构造方法:
-
FileOutputStream (String name) //name 是文件的路径,每次写都覆盖前面的内容
-
FileOutputStream (String name,boolean append) // append 来控制每一次写入数据是覆盖还是继续往后写
-
-
wirte() //写单个字节
-
write(byte[] b) //一次写多个字节
-
package com.iosteam;
import java.io.FileOutputStream;
public class Demo02 {
public static void main(String[] args) throws Exception{
FileOutputStream fos = new FileOutputStream("f:\\bbb.txt");
fos.write(97);
fos.write('b'); //自动转换成 int
fos.write('c');
fos.close();
}
}
//写多个字节
String str = "helloworld";
fos.write(str.getBytes());
fos.close();
我们创建一个不覆盖,继续向后写的流
package com.iosteam;
import java.io.FileOutputStream;
public class Demo02 {
public static void main(String[] args) throws Exception{
FileOutputStream fos = new FileOutputStream("f:\\bbb.txt",true);
//写多个字节
String str = "helloworld";
fos.write(str.getBytes());
String str1 = "zhangsan";
fos.write(str1.getBytes());
fos.close();
}
}
getBytes() 是 获取字符串所对应的字节数组
-
案例:使用文件字节流复制文件
package com.iosteam;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class Demo03 {
public static void main(String[] args) throws Exception{
//创建输入流
FileInputStream fis = new FileInputStream("f:\\001.png");
//创建输出流
FileOutputStream fos = new FileOutputStream("f:\\002.jpg");
//边读边写
byte[] b = new byte[1024]; //存放数据,缓冲作用
int count = 0;
while ((count = fis.read(b)) != -1){ //等于-1说明读取完毕
fos.write(b,0,count);
}
fis.close();
fos.close();
System.out.println("完成");
}
}
字节缓冲流
-
如果单独使用文件字节流,其内部没有缓冲区,所以效率就没有那么高
-
缓冲流在使用过程中会创建缓冲区,这样的话,如果缓冲区中有,我们就不用从硬盘中读取了,直接从缓冲区中读。
-
提高IO效率,减少访问磁盘的次数
-
数据存储在缓冲区中,flush 是将缓冲区的内容写入文件中,也可以直接close
-
-
BufferedInputStream 继承于 FilterInputStream 过滤流
package com.iosteam;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
public class Demo04 {
public static void main(String[] args) throws Exception{
//创建BufferedInputStream
FileInputStream fis = new FileInputStream("f:\\aaa.txt");
BufferedInputStream bis = new BufferedInputStream(fis); //需要传入节点流
//读取
//单个读
// int data = 0;
// while((data = bis.read()) != -1){
// System.out.print((char) data);
// }
//读取多个
byte[] b = new byte[1024];
int count = 0;
while ((count = bis.read(b)) != -1){
System.out.println(new String(b,0,count));
}
bis.close();
}
}
在 BufferedInputStream 中有长度为 8kb 的缓冲数组
最后不用关闭 FileInputStream ,直接关闭 BufferedInputStream 就行,因为在BufferedInputStream 直接就把 FileInputStream 给关了
-
BufferedOutputStream 继承于 FilterOutputStream 过滤流
package com.iosteam;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
public class Demo05 {
public static void main(String[] args) throws Exception{
//创建BufferedOutputStream
FileOutputStream fos = new FileOutputStream("f:\\buf.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos); //需要传入节点流
//读取
for (int i = 0; i < 10; i++) {
bos.write("helloworld\r\n".getBytes());
}
}
}
如果只是这样,它没有写到硬盘,而是写到了 8 kb 的缓冲区
我们需要在写入后 加入 bos.flush(),这样才写入了硬盘
或者在最后关闭bos.close() 因为在关闭方法内部调用了flush方法
这两种方法都可以把缓冲区中的内容写到硬盘,
第一种方法 比较安全,可以防止数据丢失,但效率低,第二种方法效率高,但如果写着写着断电了,那么数据就丢失了。
package com.iosteam;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
public class Demo05 {
public static void main(String[] args) throws Exception{
//创建BufferedOutputStream
FileOutputStream fos = new FileOutputStream("f:\\buf.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos); //需要传入节点流
//读取
for (int i = 0; i < 10; i++) {
bos.write("helloworld\r\n".getBytes());
bos.flush();
}
bos.close();
}
}
对象流
-
对象流:ObjectOutputStream/ObjectInputStream
-
增强了缓冲区功能
-
增强了读写8种基本数据类型和字符串功能
-
增强了读写对象的功能
-
readObject() //从流中读取一个对象
-
writeObject(Object obj) //向流中写一个对象
-
-
-
序列化:把内存中的对象写入文件中
-
反序列化:从文件中读取一个对象
ObjectOutputStream 继承了 OutputStream
里面的方法:
1.构造方法:ObjectOutputStream (OutputStream out) //需要传递一个字节输出流
2.void writeObject(Object obj) //将对象写入流中
3.void writeBoolean(boolean val) //写入一个boolean值
4.void writeInt(int i) //写入一个32位的int值
5.void wirteUTF(String str) 以UTF-8格式写入String 的基本数据
八种基本类型,不一一列举
package com.iosteam;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class Demo06 {
public static void main(String[] args) throws Exception{
//创建ObjectOutputStream
FileOutputStream fos = new FileOutputStream("f:\\test\\stu.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
//序列化对象
Student s1 = new Student("张三",20);
oos.writeObject(s1);
//关闭
oos.close();
System.out.println("序列化完成");
}
}
注意:这个时候并没有序列化成功,报出了 不可序列化异常
原因是,我们的Student 类 需要实现 Serializable 接口,这个接口是一个标识符,标志着你这个类能不能被序列化,需要实现这个接口,才能被序列化
实现这个接口后,我们再次运行
序列化成功!!!
然后是反序列化
ObjectInputStream 继承了 InputStream
ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化
方法:
1.构造方法:ObjectInputStream(InputStream in) //需要传入字节输入流
2.boolean readBoolean() //读取Boolean值
3.String readUTF() //读取字符串
4.Object readObject() //读取对象
八大基本类型的方法不在一一列举
下面我们来反序列化刚刚序列化的那个对象
package com.iosteam;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class Demo07 {
public static void main(String[] args) throws Exception{
//创建一个ObjectInputStream
FileInputStream fis = new FileInputStream("f:\\test\\stu.bin");
ObjectInputStream ois = new ObjectInputStream(fis); //传入字节输入流
//反序列化
Student s1 = (Student) ois.readObject(); // 强制转换成 Student
// 关闭
ois.close();
System.out.println("反序列化完成");
System.out.println(s1.toString());
}
}
注意 只能从流中读一次,如果多读一次会报EOFException
读完一次,流中就没有这个对象了,再读就报错
序列化与反序列化中的注意事项:
-
1.序列化类必须实现 Serializable 接口
-
2.序列化类中的对象属性也需要实现 Serializable 接口
private Address address; //这是一个属性对象,它也需要实现Serializable 接口
-
3.序列化版本号ID:
/*
serialVersionUID:序列化版本号
*/
private static final long serialVersionUID = 100L;
它是一个静态常量,用来保证序列化的类和反序列化的类是同一个类
当我们定义了这个常量后,我们再去反序列化,会发现报错了
InvalidClassException 这个错误的意思是我们反序列化的类和序列化的类不是同一个类
因为我们一开始序列化时,没有给他定义序列化版本号ID,所以程序自动给他定义了一个序列化版本号ID:
然而这个时候,我们自己给他定义了
当我们再去反序列化时,就会报不是一个类的异常了
解决办法:我们重新序列化一遍,让这个类的序列化版本ID变成 100,再去反序列化就不会报错了
-
4.使用transient 修饰的属性不能被序列化
我们使用transient 来修饰 age
private transient int age;
我们来序列化和反序列化
我们发现age = 0,原因是age没有被序列化
-
5.静态属性也不能序列化
-
6.使用集合来序列化多个对象
序列化:
//序列化对象
Student s1 = new Student("张三",20);
Student s2 = new Student("李四",18);
ArrayList list = new ArrayList();
list.add(s1);
list.add(s2);
oos.writeObject(list);
反序列化:
//反序列化
ArrayList list = (ArrayList) ois.readObject(); // 强制转换成 Student
System.out.println(list.toString());