欢迎来到Introzo百科
Introzo百科
当前位置:网站首页 > 技术 > 【第203期】彻底理解Java IO:字节流、字符流、缓冲流

【第203期】彻底理解Java IO:字节流、字符流、缓冲流

日期:2023-10-04 16:15

IO流是Java的重要组成部分,也是我们经常打交道的。这篇关于 Java IO 的博文充满了有用的信息,在互联网上排名前三(请温柔!) 如果你能流利地回答下面的问题(问题会持续补充),那么恭喜你,你已经掌握了IO知识,可以立即关闭文章了。否则,您可以在下面的文章中找到答案。 JavaIO流有什么特点? Java IO流 有多少种类型? 字节流和字符流有什么关系和区别? 字符流是否使用缓冲? 缓冲流的效率一定高吗?为什么? 缓冲流体现了Java中的哪种设计模式思想? 为什么需要实现序列化?如何实现序列化? 序列化数据后,如果再次修改class文件,读取数据就会出现问题。怎么解决呢? IO,即in和out,即输入和输出,是指应用程序与外部设备之间的数据传输。常见的外部设备包括文件、管道和网络连接。 Java通过流来处理IO,那么什么是流呢? 流是一个抽象概念,指一系列数据(字符或字节),是以先进先出方式发送信息的通道。 当程序需要读取数据时,它会打开一个流到数据源。该数据源可以是文件、内存或网络连接。同样,当程序需要写入数据时,它会打开一个流到目的地。这时,你可以想象数据似乎在里面“流动”。 一般来说,流的特点有以下几点: 先进先出:先写入输出流的数据先从输入流中读取。 顺序访问:可以将一串字节依次写入流中。读取时,也会按照写入的顺序读取一串字节。中间的数据不能随机访问。 (随机访问文件除外) 只读或只写:每个流只能是输入流或输出流,不能同时具有这两种功能。输入流只能读取,输出流只能写入。在一个数据传输通道中,如果要写入数据和读取数据,则需要分别提供两个流。 1.1 IO流分类 IO流的分类主要有以下三种方式: 按数据流向:输入流、输出流 按处理数据单元分:字节流、字符流 按功能:节点流程、处理流程 1. 输入流和输出流 输入和输出是与应用程序相关的,例如文件的读取和写入。读文件是输入流,写文件是输出流。这很容易逆转。 2、字节流和字符流字节流和字符流的用法几乎完全相同。区别在于字节流和字符流操作的数据单元不同。字节流操作的单位是数据单位是8位字节,而字符流操作的是数据。单位是16位字符。 为什么我们需要字符流? Java中的字符采用Unicode标准。在Unicode编码中,1个英文字符为1个字节,1个汉字为2个字节。在UTF-8编码中,一个汉字占3个字节。例如下图中,“云神之子”5个汉字对应15个字节:-28-70-111-26-73-79-28-72-115-25-97-91-27 -92 -124 那么问题来了。如果使用字节流处理中文,每次读写一个字符对应的字节数是没有问题的。一旦一个字符对应的字节被分割,就会出现乱码。为了更方便地处理这些汉字,Java引入了字符流。 字节流和字符流的其他区别: 字节流一般用于处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本文件,例如TXT文件,但不能处理图像、视频等非文本文件。一句话简介:字节流可以处理所有文件,而字符流只能处理纯文本文件。 字节流本身没有缓冲区。与字节流相比,缓冲字节流有非常高的效率提升。字符流本身有缓冲区,有缓冲的字符流相对于字符流来说效率提升并不是那么大。详情请参见文末的效率对比。 以写文件为例,我们查看了字符流的源码,发现确实使用了缓冲区: 3. 节点流程及处理流程 节点流:直接操作数据读写的流类,如FileInputStream 处理流:链接封装现有的流,通过处理数据为程序提供强大灵活的读写功能,如BufferedInputStream(缓冲字节流) 处理流程和节点流程应用了Java的装饰器设计模式。 下图形象地描绘了节点流程和处理流程。处理流程封装了节点流,最终的数据处理由节点流完成。在众多的处理流中,有一个非常重要,那就是缓冲流。 我们知道,程序与磁盘的交互相比内存操作是非常慢的,很容易成为程序的性能瓶颈。减少程序与磁盘的交互是提高程序效率的有效手段。缓冲流应用了这种思想:普通流一次读取和写入一个字节,而缓冲流在内存中设置一个缓冲区。缓冲区首先存储足够的数据以供操作,然后再与内存或磁盘交互。这样在数据总量不变的情况下,通过增加每次交互的数据量来减少交互次数。想想生活中的例子。当我们搬运砖块时,将它们一一装载到车上肯定是非常低效的。我们可以用小推车先把砖块装到小推车上,然后把小推车推到车前,把砖块装到车上。在这个例子中,手推车可以被视为一个缓冲区。小车的存在减少了我们装车的次数,从而提高了效率。需要注意的是,缓冲流效率一定高吗?不必要。在某些情况下,缓冲液流动效率甚至更低。详情请参见IO流效率对比。 完整的IO分类图如下: 1.2 案例实践 接下来我们看看如何使用Java IO。 文本读写的一个例子,如文章开头提到的,就是写“松下问童子,老师去采药。唯此山中,云深不知处”。写入本地文本,然后从文件中读取内容并输出到控制台。往期面试题:001~180期总结 1.FileInputStream、FileOutputStream(字节流) 字节流方式效率较低,不推荐。 public class IOTest { public static void main(String[] args) throws IOException { File file = new File("D:/test.txt"); } 写(文件); System.out.println(读取(文件)); } public static void write(File file) throws IOException { OutputStream os = new FileOutputStream(file, true); } // 要写入的字符串 String string = "松下问童子,说老师去采药,唯此山中,云深不知处。"; // 写入文件 os.write(string.getBytes()); // 关闭流 os.close(); } public static String read(File file) throws IOException { InputStream in = new FileInputStream(file); }//一次取多少个字节 byte[] bytes = new byte[1024]; //用于接收读取的字节数组 StringBuilder sb = new StringBuilder(); //读取的字节数组的长度,为-1时表示没有数据 int length = 0; // 循环获取数据 while ((length = www.introzo.com(bytes)) != -1) { // 将读取的内容转换为字符串 sb.append( new String(bytes, 0, length)); } // 关闭流 in.close(); 返回 sb.toString(); }} 2.BufferedInputStream、BufferedOutputStream(缓冲字节流) 缓冲字节流旨在提高效率。真正的读写操作还是依赖于FileOutputStream和FileInputStream,所以它的构造方法的参数是这两个类的对象也就不足为奇了。 公共类 IOTest { public static void write(File file) throws IOException { // 缓冲字节流,提高效率 BufferedOutputStream bis = new BufferedOutputStream(new FileOutputStream(file, true)); // 要写的字符串 String string = "松下问童子,说老师去采药,唯此山中,云深不知处。"; // 写入文件 bis.write(string.getBytes()); // 关闭流 bis.close(); } public static String read(File file) throws IOException { BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file));//一次取多少个字节 byte[] bytes = new byte[1024]; //用于接收读取的字节数组 StringBuilder sb = new StringBuilder(); //读取的字节数组的长度,为-1时表示没有数据 int length = 0; // 循环获取数据 while ((length = www.introzo.com(bytes)) != -1) { // 将读取的内容转换为字符串 sb.append( new String(bytes, 0, length)); } // 关闭流 fis.close(); 返回 sb.toString(); }} 3.InputStreamReader、OutputStreamWriter(字符流) 字符流适合读写文本文件。 OutputStreamWriter类实际上是借助FileOutputStream类来实现的,所以它的构造方法就是FileOutputStream的对象。 public class IOTest { public static void write(File file) throws IOException { // OutputStreamWriter 可以显示指定字符集,否则使用默认字符集 OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8 ”); // 要写入的字符串 String string = "松下问童子,请老师去采药,唯此山中,云深不知处。"; osw.write(字符串); osw.close(); }public static String read(File file) throws IOException { InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8"); } //字符数组:一次读取多少个字符 char[] chars = new char[1024 ]; // 每次读取的字符数组先追加到StringBuilder中 StringBuilder sb = new StringBuilder(); // 读取的字符数组的长度,为-1时表示没有数据 int length; // 循环获取数据 while ((length = www.introzo.com(chars)) != -1) { // 将读取的内容转换为字符串 sb.append(chars, 0, length); } // 关闭流 isr.close(); 返回 sb.toString() }} 4.字符流便利类 Java提供了FileWriter和FileReader来简化字符流的读写。 new FileWriter 相当于 new OutputStreamWriter(new FileOutputStream(file, true)) 公共类 IOTest { 公共静态 void write(File file) 抛出 IOException { FileWriter fw = new FileWriter(file, true); // 要写入的字符串 String string = "松下问童子,说老师去采药,唯此山中,云深不知处。"; fw.write(字符串); fw.close(); }public static String read(File file) throws IOException { FileReader fr = new FileReader(file); //一次取多少个字节 char[] chars = new char[1024]; //用于接收读取的字节 Array StringBuilder sb = new StringBuilder(); // 读取的字节数组的长度,为-1时表示没有数据 int length; // 循环获取数据 while ((length = www.introzo.com(chars)) != - 1) { // 将读取的内容转换为字符串 sb.append(chars, 0, length); } // 关闭流 fr.close(); 返回 sb.toString(); }} 5.BufferedReader、BufferedWriter(字符缓冲流) public class IOTest { public static void write(File file) throws IOException { // BufferedWriter fw = new BufferedWriter(new OutputStreamWriter(new // FileOutputStream(file, true), "UTF-8")); // FileWriter 可以大大简化代码 BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 要写入的字符串 String string = "松下问童子,说老师去采药,唯此山中,云深不知处。"; bw.write(字符串); bw.close(); }public static String read(File file) throws IOException { BufferedReader br = new BufferedReader(new FileReader(file)); // 用于接收读取的字节数组 StringBuilder sb = new StringBuilder(); // 按行读取数据 String line; // 循环获取数据 while ((line = br.readLine()) != null) { // 将读取的内容转换为字符串 sb.append(line); } //关闭流 br.close(); 返回 sb.toString(); }} 在第一节中,我们对IO有了一个大概的了解,并完成了几个案例,但对IO还缺乏更详细的了解。接下来我们将对Java IO进行详细分解,整理出一个完整的知识体系。 Java提供了40多个类。我们只需要详细了解比较重要的就可以满足日常应用了。 2.1 文件类 File类是用来操作文件的类,但是它不能操作文件中的数据。 公共类文件扩展对象实现可序列化,可比较 File类实现了Serialized和Comparable,表明它支持序列化和排序。 File类的构造方法 File类的常用方法 文件类使用示例 public class FileTest { public static void main(String[] args) throws IOException { File file = new File("C:/Mu/fileTest.txt"); }// 判断文件是否存在 if (!file.exists()) { // 如果不存在则创建 file.createNewFile(); } System.out.println("文件的绝对路径:" + file.getAbsolutePath()); System.out.println("文件大小:" + file.length()); //删除文件 file.delete(); }} 2.2 字节流 InputStream和OutputStream是两个抽象类,它们是字节流的基类。所有具体的字节流实现类都分别继承这两个类。搜索公众号java面试题精选,回复实际项目,送你项目源码+教程 以InputStream为例,它继承Object并实现Closeable 公共抽象类 InputStreamextends Objectimplements Closeable InputStream 类有许多实现子类。下面列出了一些比较常用的类:我们来详细解释一下上图中的类: InputStream:InputStream是所有字节输入流的抽象基类。正如前面提到的,抽象类不能被实例化。它们实际上作为模板存在,并为所有实现类定义处理输入流的方法。 FileInputSream:文件输入流,非常重要的字节输入流,用于读取文件。 PipedInputStream:管道式字节输入流,可以实现多线程之间的管道通信。 ByteArrayInputStream:字节数组输入流,从字节数组(byte[])中以字节为单位读取,即将资源文件以字节的形式存储到该类go中的字节数组中。 FilterInputStream:装饰器类。具体的装饰器继承这个类。这些类都是处理类。它们的作用是封装节点类,实现一些特殊的功能。 DataInputStream:数据输入流,用于装饰其他输入流。它的功能是“允许应用程序以与机器无关的方式从底层输入流读取基本的Java数据类型”。 BufferedInputStream:缓冲流,装饰节点流。里面会有一个缓冲区来存储字节。每次缓冲区填满再发送,而不是发送一个字节或两个字节,效率很高。更高。ObjectInputStream:对象输入流,用于提供基本数据或对象的持久存储。通俗地说,就是可以直接传递对象,通常用于反序列化。它也是一个处理流,构造函数的输入参数是InputStream的实例对象。 OutputStream类继承图: OutputStream类的继承关系与InputStream类似。需要注意的是PrintStream。 往期面试题:001~180期总结 2.3 字符流 与字节流类似,字符流也有两个抽象基类,分别是Reader和Writer。其他字符流实现类都继承这两个类。 以Reader为例,其主要实现子类如下所示: 各类详细说明: InputStreamReader:字节流到字符流的桥梁(InputStreamReader构造函数的输入参数是FileInputStream的实例对象)。它读取字节并使用指定的字符集将它们解码为字符。它使用的字符集可以通过名称指定,显式给出,或者可以接受平台的默认字符集。 BufferedReader:从字符输入流中读取文本并设置缓冲区以提高效率。 BufferedReader是InputStreamReader的封装。前者的构造函数的输入参数是后者的实例对象。 FileReader:读取字符文件的便利类,new FileReader(File file) 等价于 new InputStreamReader(new FileInputStream(file, true), "UTF-8"),但 FileReader 不能指定字符编码和默认字节缓冲区大小。 PipedReader:管道字符输入流。实现多线程之间的管道通信。 CharArrayReader:从 Char 数组读取数据的媒体流。 StringReader:从String读取数据的媒体流。 Writer和Reader结构类似,方向相反,不再赘述。唯一的区别是 PrintWriter 是 Writer 的子类。 2.4 序列化 待续… 3.1 字节流法 字节输入流InputStream主要方法: read():从此输入流读取数据字节。 read(byte[] b):从此输入流中读取最多 b.length 个字节的数据到字节数组中。read(byte[] b, int off, int len):从此输入流中读取最多 len 个字节的数据到字节数组中。 close():关闭此输入流并释放与该流关联的所有系统资源。 字节输出流OutputStream主要方法: write(byte[] b):将指定字节数组中的 b.length 个字节写入此文件输出流。 write(byte[] b, int off, int len):将指定字节数组中从offset开始的len个字节写入此文件输出流。 write(int b):将指定字节写入此文件输出流。 close() :关闭此输入流并释放与该流关联的所有系统资源。 3.2 字符流法 字符输入流Reader主要方法: read():读取单个字符。 read(char[] cbuf):将字符读入数组。 read(char[] cbuf, int off, int len):将字符读入数组的某一部分。 read(CharBuffer target):尝试将字符读入指定的字符缓冲区。 lush():刷新该流的缓冲区。 close():关闭此流,但首先刷新它。 字符输出流Writer主要方法: write(char[] cbuf):写入字符数组。 write(char[] cbuf, int off, int len):写入字符数组的某一部分。 write(int c):写入单个字符。 write(String str):写入字符串。 write(String str, int off, int len):写入字符串的某一部分。 lush():刷新该流的缓冲区。 close():关闭此流,但首先刷新它。 另外,字符缓冲流还有两个独特的方法: BufferedWriter类newLine():写入行分隔符。此方法将自动适应您系统的行分隔符。 BufferedReader 类 readLine():读取文本行。 往期面试题:001~180期总结 4.1 位、字节、字符 字节是一种计量单位,表示数据量。它是计算机信息技术用来衡量存储容量的计量单位。通常,一个字节等于八位。字符是计算机中使用的字母、数字、单词和符号,例如“A”、“B”、“$”、“&”等。 一般来说,英文中一个字母或字符占用一个字节,而一个汉字则用两个字节表示。 字节和字符: ASCII码中,1个英文字母(不区分大小写)为1个字节,1个汉字为2个字节。 UTF-8编码中,1个英文单词为1个字节,1个中文单词为3个字节。 在Unicode编码中,1个英文单词为1个字节,1个中文单词为2个字节。 符号:英文标点符号为一个字节,中文标点符号为两个字节。例如:英语句号。占用1字节大小,中文句号。占用2字节大小。 在UTF-16编码中,每个英文字母字符或一个汉字需要2个字节来存储(Unicode扩展区的一些汉字需要4个字节来存储)。 在UTF-32编码中,世界上任何字符的存储都需要4个字节。 4.2 IO流效率对比 首先对比一下普通字节流和缓冲字节流的效率: public class MyTest { public static void main(String[] args) throws IOException { File file = new File("C:/Mu/test.txt"); } StringBuilder sb = new StringBuilder(); for (int i = 0; i < 3000000; i++) { sb.append("abcdefghigklmnopqrstuvwsyz"); } byte[] bytes = sb.toString().getBytes(); 长开始 = System.currentTimeMillis();写入(文件,字节);长结束= System.currentTimeMillis(); 长start2 = System.currentTimeMillis(); bufferedWrite(文件,字节);长end2 = System.currentTimeMillis();System.out.println("普通字节流耗时:" + (end - start) + " ms"); System.out.println("缓冲字节流需要时间:" + (end2 - start2) + " ms"); } // 普通字节流 public static void write(File file, byte[] bytes) throws IOException { OutputStream os = new FileOutputStream(file); os.write(字节); os.close(); } // 缓冲字节流 public static void bufferedWrite(File file, byte[] bytes) throws IOException { BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(file)); bo.write(字节); bo.close(); }} 运行结果: 普通字节流需要250 ms。缓冲字节流需要 268 毫秒。 这个结果让我很惊讶。不是说缓冲流效率很高吗?要知道为什么,只能到源码中寻找答案。看一下字节缓冲流的write方法:public synchronized void write(byte b[], int off, int len) throws IOException { if (len >= buf.length) { /* 如果请求长度超过输出缓冲区的大小,则刷新输出缓冲区,然后写入直接数据。通过这种方式,缓冲流将无害地级联。 */ 刷新缓冲区(); out.write(b, 关闭, len);返回; } if (len > buf.length - count) {lushBuffer(); } System .arraycopy(b, off, buf, count, len);计数 += 长度;} 注释说得很清楚:如果请求长度超过输出缓冲区的大小,则刷新输出缓冲区并直接写入数据。这样缓冲的流将无害地级联。 不过,至于为什么要这样设计,我还没有想明白。有懂的人可以留言指点一下吗? 基于以上情况,如果要比较普通字节流和缓冲字节流的效率差距,就必须避免直接读写较长的字符串。因此,设计了以下比较案例: 使用字节流和缓冲字节流 流单独复制文件。 public class MyTest { public static void main(String[] args) throws IOException { File data = new File("C:/Mu/www.introzo.com"); }文件a = new File("C:/Mu/www.introzo.com ");文件b = new File("C:/Mu/www.introzo.com"); StringBuilder sb = new StringBuilder();长开始= System.currentTimeMillis();复制(数据,a);长结束= System.currentTimeMillis(); 长start2 = System.currentTimeMillis(); bufferedCopy(数据, b); long end2 = System.currentTimeMillis();   System.out.println("普通字节流耗时:" + (end - start) + " ms");  System.out.println("缓冲字节流耗时:" + (end2 - start2) + " ms"); }  // 普通字节流 public static void copy(File in, File out) throws IOException {  // 封装数据源  InputStream is = new FileInputStream(in);  // 封装目的地  OutputStream os = new FileOutputStream(out);    int by = 0;  while ((by = www.introzo.com()) != -1) {   os.write(by);  }  is.close();  os.close(); }  // 缓冲字节流 public static void bufferedCopy(File in, File out) throws IOException {  // 封装数据源  BufferedInputStream bi = new BufferedInputStream(new FileInputStream(in));  // 封装目的地  BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(out));    int by = 0;  while ((by = www.introzo.com()) != -1) {   bo.write(by);  }  bo.close();  bi.close(); }} 运行结果: 普通字节流耗时:184867 ms缓冲字节流耗时:752 ms 这次,普通字节流和缓冲字节流的效率差异就很明显了,达到了245倍。 再看看字符流和缓冲字符流的效率对比: public class IOTest { public static void main(String[] args) throws IOException {  // 数据准备  dataReady();   File data = new File("C:/Mu/data.txt");  File a = new File("C:/Mu/a.txt");  File b = new File("C:/Mu/b.txt");  File c = new File("C:/Mu/c.txt");   long start = System.currentTimeMillis();  copy(data, a);  long end = System.currentTimeMillis();   long start2 = System.currentTimeMillis();  copyChars(data, b);  long end2 = System.currentTimeMillis();   long start3 = System.currentTimeMillis();  bufferedCopy(data, c);  long end3 = System.currentTimeMillis();   System.out.println("普通字节流1耗时:" + (end - start) + " ms,文件大小:" + a.length() / 1024 + " kb");  System.out.println("普通字节流2耗时:" + (end2 - start2) + " ms,文件大小:" + b.length() / 1024 + " kb");  System.out.println("缓冲字节流耗时:" + (end3 - start3) + " ms,文件大小:" + c.length() / 1024 + " kb"); }  // 普通字符流不使用数组 public static void copy(File in, File out) throws IOException {  Reader reader = new FileReader(in);  Writer writer = new FileWriter(out);   int ch = 0;  while ((ch = www.introzo.com()) != -1) {   writer.write((char) ch);  }  reader.close();  writer.close(); }  // 普通字符流使用字符流 public static void copyChars(File in, File out) throws IOException {  Reader reader = new FileReader(in);  Writer writer = new FileWriter(out);   char[] chs = new char[1024];  while ((www.introzo.com(chs)) != -1) {   writer.write(chs);  }  reader.close();  writer.close(); }  // 缓冲字符流 public static void bufferedCopy(File in, File out) throws IOException {  BufferedReader br = new BufferedReader(new FileReader(in));  BufferedWriter bw = new BufferedWriter(new FileWriter(out));   String line = null;  while ((line = br.readLine()) != null) {   bw.write(line);   bw.newLine();   bw.flush();  }   // 释放资源  bw.close();  br.close(); }  // 数据准备 public static void dataReady() throws IOException {  StringBuilder sb = new StringBuilder();  for (int i = 0; i < 600000; i++) {   sb.append("abcdefghijklmnopqrstuvwxyz");  }  OutputStream os = new FileOutputStream(new File("C:/Mu/data.txt"));  os.write(sb.toString().getBytes());   os.close();  System.out.println("完毕"); }} 运行结果: 普通字符流1耗时:1337 ms,文件大小:15234 kb普通字符流2耗时:82 ms,文件大小:15235 kb缓冲字符流耗时:205 ms,文件大小:15234 kb 测试多次,结果差不多,可见字符缓冲流效率上并没有明显提高,我们更多的是要使用它的readLine()和newLine()方法。

关灯