JavaSE丨IO流全解:从基础概念到序列化实战
一、IO流
1.1 流的概念
在计算机中,流是个抽象的概念,是对输入输出设备的抽象。在Java程序中,对于数据的输入/输出操作,都是以"流"的方式进行。
数据以二进制的形式在程序与设备之间流动传输,就像水在管道里流动一样,所以就把这种数据传输的方式称之为输入流、输出流。这里描述的设备,可以是文件、网络、内存等。
流具有方向性,可以分为输入和输出。
以java程序本身作为参照点,如果数据是从程序“流向”文件,那么这个流就是输出流,如果数据是从文件“流向”程序,那么这个流就是输入流。例如:
这里是以文件进行举例,java程序中还可以把数据写入到网络中、内存中等
1.2 流的分类
Java中的IO流可以根据很多不同的角度进行划分,最常见的是以数据的流向和数据的类型来划分
根据数据的流向分为:输入流和输出流
- 输入流 :把数据从其他设备上读取到程序中的流
- 输出流 :把数据从程序中写出到其他设备上的流
根据数据的类型分为:字节流和字符流
- 字节流 :以字节为单位(byte),读写数据的流
- 字符流 :以字符为单位(char),读写数据的流
字节输入流:在程序中,以字节的方式,将设备(文件、内存、网络等)中的数据读进来
字节输出流:在程序中,以字节的方式,将数据写入到设备(文件、内存、网络等)中
字符输入流:在程序中,以字符的方式,将设备(文件、内存、网络等)中的数据读进来
字符输出流:在程序中,以字符的方式,将数据写入到设备(文件、内存、网络等)中
注意:字节指的是byte,字符指的的是char
1.3 流的结构
在Java中,和IO流相关的类,主要是在 java.io 包下的定义的
几乎所有的流,都是派生自四个抽象的父类型:
- InputStream ,代表字节输入流类型
- OutputStream ,代表字节输出流类型
- Reader ,代表字符输入流类型
- Writer ,代表字符输出流类型
Java中常用的流及其继承结构:
1.4 字节流
一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
java.io.InputStream 是所有字节输入流的抽象父类型
java.io.OutputStream 是所有字节输出流的抽象父类型
一般情况,使用字节流来操作数据的时候,往往是使用一对:一个字节输入流,负责读取数据,一个字节输出流,负责将数据写出去,而这些流都将是 InputStream 和 OutputStream 的子类型。
在代码中,使用流操作数据的的基本步骤是:
- 声明流
- 创建流
- 使用流
- 关闭流
1)文件输入流
文件字节输入流 FileInputStream ,用于从文件中读取字节数据。
案例展示:使用 FileInputStream 读取文件内容
import java.io.FileInputStream;
import java.io.IOException;public class FileInputStreamExample {public static void main(String[] args) {// 要读取的文件路径,这里假设在项目根目录下有test.txt文件String filePath = "test.txt";try (FileInputStream fis = new FileInputStream(filePath)) {int data;// 循环读取文件内容,read()方法每次读取一个字节,返回值为字节对应的ASCII码值,读到文件末尾返回-1while ((data = fis.read()) != -1) {System.out.print((char) data);}} catch (IOException e) {System.out.println("读取文件时发生错误: " + e.getMessage());}}
}
2)文件输出流
文件字节输出流, FileOutputStream ,用于写入字节数据到文件中。
案例展示:使用 FileOutputStream 写入文件内容
import java.io.FileOutputStream;
import java.io.IOException;public class FileOutputStreamExample {public static void main(String[] args) {// 要写入的文件路径,这里假设在项目根目录下创建testWrite.txt文件String filePath = "testWrite.txt";String content = "这是通过FileOutputStream写入文件的内容。";try (FileOutputStream fos = new FileOutputStream(filePath)) {// 将字符串转换为字节数组,getBytes()方法将字符串按平台默认字符编码转换为字节数组byte[] bytes = content.getBytes();// 写入字节数组到文件中fos.write(bytes);System.out.println("文件写入成功!");} catch (IOException e) {System.out.println("写入文件时发生错误: " + e.getMessage());}}
}
3)综合案例
结合使用 FileInputStream 和 FileOutputStream 实现文件复制
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;public class FileCopyExample {public static void main(String[] args) {// 源文件路径String sourceFilePath = "source.txt";// 目标文件路径String targetFilePath = "target.txt";try (FileInputStream fis = new FileInputStream(sourceFilePath);FileOutputStream fos = new FileOutputStream(targetFilePath)) {int data;while ((data = fis.read()) != -1) {fos.write(data);}System.out.println("文件复制成功!");} catch (IOException e) {System.out.println("文件复制时发生错误: " + e.getMessage());}}
}
1.5 字符流
在 Java 中,字符流是一种用于处理字符数据的输入和输出流。相比于字节流,字符流提供了更方便和高效的字符处理方式。
字符流以字符为单位进行读写操作,可以直接读写字符,而不需要进行字节与字符的转换。这使得字符流更适合处理文本数据,可以方便地进行字符的查找、替换、拼接等操作。
字节流和字符流的关系:
本质上,字符流底层借助字节流实现功能。通过借助字节流,字符流可以在更高的抽象层次上处理字符数据,提供更方便和高效的字符处理方式。字节流提供了底层的数据传输能力,而字符流在此基础上提供了字符编码、字符集转换、缓冲功能以及其他高级功能。
1)文件字符流
java.io.FileReader 类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
java.io.FileWriter 类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
案例展示:使用文件字符流拷贝a.txt文件内容到b.txt文件末尾
public class Test_FileReaderWriter {public static void main(String[] args) throws IOException {// 1.实例化流对象File file1 = new File("src/dir/a.txt");File file2 = new File("src/dir/b.txt");Reader reader = new FileReader(file1);// 设置文件追加Writer writer = new FileWriter(file2, true);// 2.使用流进行文件拷贝int len = -1;char[] buf = new char[8];while ((len = reader.read(buf)) != -1) {writer.write(buf, 0, len);}// 刷新流writer.flush();// 3.关闭流writer.close();reader.close();}
}
2)节点流总结
在 Java 中,IO 流按照功能划分可以分为两类: 节点流(原始流) 增强流(包装流)
节点流(Node Streams)是最基本的 IO 流,直接与数据源或目标进行交互,但缺乏一些高级功能。 它们提供了最底层的读写功能,可以直接读取或写入底层数据源或目标,如文件、网络连接等。
上面我们学习的所有流都是节点流:
FileInputStream 和 FileOutputStream :用于读取和写入文件的字节流。
FileReader 和 FileWriter :用于读取和写入文件的字符流
ByteArrayInputStream 和 ByteArrayOutputStream :用于读取和写入字节数组的流
CharArrayReader 和 CharArrayWriter :用于读取和写入字符数组的流
在实际开发中,通常会使用增强流来提供更高级的功能和操作,以便更方便地进行 IO 操作。 节点流则作为增强流的基础,提供最底层的读写功能。
增强流也称为包装流:
- 其在节点流的基础上提供了额外的功能和操作
- 增强流提供了更高级的操作和便利性,使得 IO 操作更加方便、高效和灵活
- 增强流通过装饰器模式包装节点流,可以在节点流上添加缓冲、字符编码转换、对象序列化等功能
节点流、增强流理解:
1.6 缓冲流
1)缓冲思想
在 Java 的 I/O 流中,缓冲思想是一种常见的优化技术,用于提高读取和写入数据的效率。它通过在内存中引入缓冲区(Buffer)来减少实际的 I/O 操作次数, 从而提高数据传输的效率。
缓冲思想的基本原理是将数据暂时存储在内存中的缓冲区中,然后按照一定的块大小进行读取或写入操作。相比于直接对磁盘或网络进行读写操作,使用缓冲区可以减少频繁的 I/O 操作,从而提高效率。
缓冲流概述: 缓冲流(Buffered Streams)也叫高效流,是一种非常有用的增强流,提供了缓冲功能,可以提高 IO 操作的效率。 缓冲流通过在内存中创建一个缓冲区,将数据暂时存储在缓冲区中,然后批量读取或写入数据,减少了频繁的磁盘或网络访问,从而提高了读写的性能。
常见缓冲流:
- BufferedInputStream 缓冲字节输入流
- BufferedOutputStream 缓冲字节输出流
- BufferedReader 缓冲字符输入流
- BufferedWriter 缓冲字符输出流
2)缓冲字节流
- public BufferedInputStream(InputStream in) :创建一个新的缓冲输入流。
- public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。
案例展示:
准备图片 D:\\test\\1.png ,拷贝到 D:\\test\\2.png ,分别使用文件子节点(节点流)和缓冲流拷贝图片,对比两种方式拷贝所需时间。
节点流:
public class Test_Buffered {public static void main(String[] args) throws Exception {// 1. 创建流对象FileInputStream fis = new FileInputStream("D:\\test\\1.png");FileOutputStream fos = new FileOutputStream("D:\\test\\2.png");// 2. 借助节点流进行逐行读取long start = System.currentTimeMillis();int data;while ((data = fis.read()) != -1) {fos.write(data);}long end = System.currentTimeMillis();System.out.println("拷贝完成,拷贝时长: " + (end - start) + "ms");// 3. 只需要关闭最后的增强流对象即可fos.close();fis.close();}
}
缓冲流:
public static void main(String[] args) throws Exception {// 1. 创建缓冲流FileInputStream fis = new FileInputStream("D:\\test\\1.png");// 注意:缓冲流是增强流,其底层借助节点流实现功能,所以创建时须传入一个节点流对象BufferedInputStream bis = new BufferedInputStream(fis);BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\test\\2.png"));// 2. 借助缓冲流进行逐行读取long start = System.currentTimeMillis();byte[] buff = new byte[1024];int len;while ((len = bis.read(buff)) != -1) {bos.write(buff, 0, len);// 刷新缓冲流bos.flush();}long end = System.currentTimeMillis();System.out.println("拷贝完成,拷贝时长: " + (end - start) + "ms");// 3. 只需要关闭最后的增强流对象即可bos.close();bis.close();
}
对比两个程序运行结果可知,缓冲流能明显提高IO效率。
3)缓冲字符流
- public BufferedReader(Reader in) :创建一个新的缓冲输入流。
- public BufferedWriter(Writer out): 创建一个新的缓冲输出流。
字符缓冲流的构造器,要求一定要传入一个字符流对象,然后缓冲流就可以对这个字符流的功能进行增强,提供缓冲数据的功能,从而提高读写的效率
案例展示:
使用缓冲字符流完成字符文件的拷贝,注意使用缓冲流的新方法。
public class Test_BufferedChar {public static void main(String[] args) throws Exception {//1.BufferedReader br = new BufferedReader(new FileReader("src/dir/a.txt"));BufferedWriter bw = new BufferedWriter(new FileWriter("src/dir/b.txt"));//2.逐行读取 输出 最后拷贝String line;//读取整行数据 不包含 换行符while ((line = br.readLine()) != null) {//输出读取整行数据System.out.println("read: " + line);//将读取的整行数据 写入b.txtbw.write(line);//额外写换行符: 如果后续没有数据了,则不要再写换行符// ready()返回false可理解为:马上要读取到文件尾if (br.ready())bw.newLine();}//3.bw.close();br.close();}
}
1.7 对象流
1)序列化机制
Java 提供了一种对象序列化的机制,可以将对象和字节序列之间进行转换
- 序列化
程序中,可以用一个字节序列来表示一个对象,该字节序列包含了对象的类型、对象中的数据等。如果这个字节序列写出到文件中,就相当于在文件中持久保存了这个对象的信息
- 反序列化
相反的过程,从文件中将这个字节序列读取回来,在内存中重新生成这个对象,对象的类型、对象中的数据等,都和之前的那个对象保持一致。(注意,这时候的对象和之前的对象,内存地址可能是不同的)
如图:
完成对象的序列化和反序列化,就需要用到对象流
2)对象流介绍
- java.io.ObjectOutputStream 将Java对象转换为字节序列,并输出到内存、文件、网络等地方
- java.io.ObjectInputStream 从某一个地方读取出对象的字节序列,并生成对应的对象
案例展示:
准备Student类,使用对象流将学生对象保存在文件中,并读取出来。
//基础类
public class Student {private String name;private int age;public Student() {}public Student(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}
}
测试类:
public class Test_WriteObject {public static void main(String[] args) throws IOException {//把对象保存文件ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/com/briup/chap11/test/stu.txt"));Student stu = new Student("tom", 20);oos.writeObject(stu);System.out.println("writeObject success!");//操作完成后,关闭流oos.close();}
}
运行代码时会抛出异常 java.io.NotSerializableException
异常信息为Student的类型无法进行序列化,那Student类型还需要做哪些操作才可以完成序列化呢?
3)序列化接口
在Java中,只有实现了 Serializable接口的对象才可以进行进行序列化和反序列化。
java.io.Serializable 接口
注意:这只是一个“标识”接口,接口中没有抽象方法。
上述问题解决: 让Student类实现序列化接口,就可以解决上述问题。
//注意,必须实现接口
public class Student implements Serializable {//省略...
}
结论:对象流操作的基础类,一定要实现序列化接口
4)集合序列化
实际开发中往往有以下场景: 程序员A往文件中写入多个对象;程序员B需要从该文件中读取所有对象;但B不知道文件中对象个数,这种情况下如何获取所有对象,同时避免 EOFException 异常?
推荐方案: 序列化多个对象时,先将所有对象添加到一个集合中,然后序列化集合对象; 反序列化时,从文件中读取单个集合对象,再从集合中获取所有对象。
集合序列化案例: 往list.txt中写入多个对象,然后再从文件中读取所有对象并遍历输出。
写入list.txt功能实现:
public class Test_WriteList {public static void main(String[] args) throws Exception {//1.实例化流对象ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/com/briup/chap11/test/list.txt"));//2.准备多个Student对象并加入List集合Student s1 = new Student("tom", 20);Student s2 = new Student("zs", 21);Student s3 = new Student("jack", 19);List<Student> list = new ArrayList<>();list.add(s1);list.add(s2);list.add(s3);//3.将集合写入文件oos.writeObject(list);System.out.println("write list success!");//4.操作完成后,关闭流oos.close();}
}
读取功能实现:
public class Test_ReadList {public static void main(String[] args) throws IOException, ClassNotFoundException {ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/com/briup/chap11/test/list.txt"));//2.读取集合对象List<Student> list = (List<Student>) ois.readObject();if (list == null) {System.out.println("read null");return;}System.out.println("read list size: " + list.size());//3.遍历输出for (Student stu : list) {System.out.println(stu);}//4.关闭资源ois.close();}
}