JavaSE | 12-IO流

程序是在“内存”里跑的,所以永远以内存为原点。

  • 参照物:程序/内存

  • Input (输入):数据从硬盘/网络 流进 内存。—— 读(Read)

  • Output (输出):数据从内存 流出 到硬盘/网络。—— 写(Write)

维度字节流 (万能,图片/视频/文本)字符流 (仅限纯文本,自带编码)
输入 (Read)InputStreamReader
输出 (Write)OutputStreamWriter

字节流

FileOutputStream

  • 操作本地文件的字节输出流,可以把程序中的数据写到本地文件中

核心步骤:

  1. 创建对象FileOutputStream fos = new FileOutputStream("a.txt");

    • 细节1:如果文件不存在,会自动创建,但是要保证父级路径是存在的。
    • 细节2:如果文件存在,会清空原内容
  2. 写出数据fos.write(97); // 写出的是 ASCII 码,文件中显示 ‘a’。

  3. 释放资源fos.close(); // 极其重要! 不关流,文件会被程序锁死。

写出数据的三种方式

方法名说明适用场景
write(int b)一次写一个字节极少用,效率太低
write(byte[] b)一次写一个字节数组常用,适合写出大量数据
write(byte[] b, off, len) off:offset写出数组的一部分最常用,配合循环读取时能精确控制写出的量

FileOutputStream写数据的问题

换行

  • Windows: \r\n 回车 + 换行,java在底层会补全

  • Linux/Unix: \n

  • Mac: \r

System.lineSeparator() 获取操作系统的换行符

续写

public FileOutputStream(File file, boolean append) true则续写

FileInputStream

  • 功能:硬盘 $\rightarrow$ 内存 (读取字节)。

  • 结束标志:读取到 -1

  • 性能优化:定义 byte[] 数组作为缓冲区,一次读取多个字节。

  • 适用场景:文件拷贝(图片、音视频)、不需要处理文本内容的场景。

核心步骤:

  1. 创建对象FileIutputStream fis = new FileIutputStream("a.txt");

    • 细节1:如果文件不存在,会直接报错
  2. 读取数据int b = fis.read(97); // 读出的是 ASCII 码,读出来显示 ‘a’。

    • 细节1: 读到文件末尾,read返回-1
  3. 释放资源fis.close();

读入数据的方法

方法名返回值类型含义
read()int读取一个字节(效率低),返回该字节的值;读完返回 -1
read(byte[] buffer)int读取字节填充进数组,返回实际读取个数;读完返回 -1
available()int返回文件剩余的可读字节数(慎用,大文件会爆内存)

文件拷贝

	FileInputStream fis = new FileInputStream(PATH+"b.txt");  
	FileOutputStream fos = new FileOutputStream(PATH+"copy.txt");  
	byte[] buffer = new byte[1024];//1KB  
	int len;  
	while ((len = (fis.read(buffer))) != -1) {  
	    fos.write(buffer, 0, len);  
	}  
	fos.close();  
	fis.close();

[!NOTE] close关闭顺序:先开的后关,后开的先关

异常处理try-with-resources

try-catch-finally

finally代码块一定会执行,除非虚拟机退出

当流对象实现了AutoCloseable接口时,可以使用小括号

修正后的文件拷贝

	FileInputStream fis = new FileInputStream(PATH + "b.txt");  
	FileOutputStream fos = new FileOutputStream(PATH + "copy.txt");  
	  
	try (fis;fos){  
	    byte[] buffer = new byte[1024];//1KB  
	    int len;  
	    while ((len = (fis.read(buffer))) != -1) {  
	        fos.write(buffer, 0, len);  
	    }  
	} catch (IOException e) {  
	    e.printStackTrace();  
	}

字符集

编码格式中文占位字节流读取过程结果
GBK2 字节fis.read() 一次拿 1 字节拿到了半个字 $\rightarrow$ 乱码
UTF-83 字节fis.read() 一次拿 1 字节拿到了三分之一个字 $\rightarrow$ 乱码

字符流 = 字节流 + 字符集

特性字节流 (Stream)字符流 (Reader/Writer)
数据单位字节 (8 bit)字符 (16 bit)
处理对象所有文件(图片、视频、文本)仅限纯文本(txt, java, html)
读中文会乱码不会乱码
拷贝图片完美拷贝文件会损坏(千万别用字符流拷图片)

核心步骤:

  1. 创建对象FileReader fr = new FileReader("a.txt");

    • _细节_1:
  2. 读取数据while((ch = fr.read() )!= -1){}

    • 细节1:按字节读取,遇到中文,一次会读多个字节,读取后解码。返回整数

    • 细节2:读到文件末尾返回-1

      • read()的细节:
      • 在读取之后,方法的底层还会进行解码并转成十进制,最终会把这个十进制作为返回值,这个十进制的数据也表示在字符集上的数字。
  3. 释放资源fr.close();


缓冲流

	IO流体系
	├─ 字节流
	│  ├─ InputStream(字节输入流)
	│  │  ├─ FileInputStream(基本字节输入流)
	│  │  └─ BufferedInputStream(字节缓冲输入流)
	│  └─ OutputStream(字节输出流)
	│     ├─ FileOutputStream(基本字节输出流)
	│     └─ BufferedOutputStream(字节缓冲输出流)
	└─ 字符流
	   ├─ Reader(字符输入流)
	   │  ├─ FileReader(基本字符输入流)
	   │  └─ BufferedReader(字符缓冲输入流)
	   └─ Writer(字符输出流)
	      ├─ FileWriter(基本字符输出流)
	      └─ BufferedWriter(字符缓冲输出流)

1.为什么它快?

  • 普通流:每读写一个字节,就要调用一次操作系统的 API,直接操作硬盘。硬盘 IO 是计算机最慢的操作。

  • 缓冲流:在内存中开辟了一个 8KB (8192 byte) 的字节数组(缓冲区)。

    • 输入缓冲:一次性从硬盘读 8KB 进来,你调 read() 时,它是从内存数组里拿。

    • 输出缓冲:你调 write() 时,数据先存在内存数组里,攒满 8KB 后,一次性“喷”向硬盘。

2.家族结构与包装模式

  • 缓冲流属于装饰者模式(Decorator Pattern),它不具备读写文件的能力,必须依托于“基本流”。

语法模板:

// “套娃”式创建:外层关了,内层自动关
	BufferedInputStream bis = new BufferedInputStream(new FileInputStream("a.mp4"));
类型缓冲流名称包装的基本流
字节输入BufferedInputStreamFileInputStream
字节输出BufferedOutputStreamFileOutputStream
字符输入BufferedReaderFileReader
字符输出BufferedWriterFileWriter

字符缓冲流的特殊方法

BufferedReader -> readLine()

  • 功能:读取一整行,直到遇到换行符(\r\n)。

  • 返回值:返回 String注意:不包含换行符本身。

  • 结束标志:读到文件末尾返回 null(注意:不再是 -1)。

BufferedWriter -> newLine()

  • 功能:写出一个换行符。

  • 优势跨平台性。它会根据当前操作系统自动切换 \r\n (Win) 或 \n (Linux)。

性能对比

方式性能评价
基本流 + 单字节极慢可能需要几分钟
基本流 + 数组 (8KB)常用,性能已经很好了
缓冲流 + 单字节缓冲流弥补了单字节的频繁 IO
缓冲流 + 数组 (8KB)极快推荐使用

[!WARNING] 1.缓冲区溢出:输出流写完后,如果程序突然崩溃且没关流,缓冲区里没满 8KB 的那部分数据会丢失。所以必须 close()

2.刷新缓冲区flush() 只能清空缓冲区(强制写出),close() 会先调 flush() 再关流

3.内存占用:虽然缓冲流快,但每个流都会占 8KB 内存。如果同时开启几万个流,要注意内存溢出

带缓冲的字符流读取模板

	try (BufferedReader br = new BufferedReader(new FileReader("config.txt"));
	     BufferedWriter bw = new BufferedWriter(new FileWriter("copy.txt"))) {
	    
	    String line;
	    // 1. 读一行
	    while ((line = br.readLine()) != null) {
	        // 2. 写一行
	        bw.write(line);
	        // 3. 必须手动补换行,因为 readLine 不读换行符
	        bw.newLine(); 
	    }
	} catch (IOException e) {
	    e.printStackTrace();
	}

转换流

转换流名称构造参数作用方向常用场景
InputStreamReaderInputStream, Charset字节 $\rightarrow$ 字符 (解码)读取非 UTF-8 编码的文本文件
OutputStreamWriterOutputStream, Charset字符 $\rightarrow$ 字节 (编码)将文本以指定编码存入文件(如存为 GBK)

JDK11以后

// 底层其实就是封装了转换流
FileReader fr = new FileReader("gbk_file.txt", Charset.forName("GBK"));
BufferedReader br = new BufferedReader(fr);

如何实现字节流读一整行不出现乱码? 字节流 -> 字符流(可以指定编码) -> 字符缓冲流(可以读一整行)

	//1. 创建文件字节输入流
	FileInputStream fis = new FileInputStream(PATH + "d.txt");  
	// 2. 将字节流包装为字符流(默认使用系统编码)
	InputStreamReader isr = new InputStreamReader(fis); 
	//3. 将字符流包装为缓冲字符流,可以按行读取提高效率
	BufferedReader br = new BufferedReader(isr);  
	
	System.out.println(br.readLine());  
	br.close();//关闭高级流

序列化流/对象操作流

维度ObjectOutputStreamObjectInputStream
动作序列化 (Serialization)反序列化 (Deserialization)
方向内存 $\rightarrow$ 硬盘硬盘 $\rightarrow$ 内存
构造方法oos(OutputStream out)ois(InputStream in)
方法writeObject(Object obj)readObject()
前提条件Javabean必须实现 Serializable接口类路径必须存在,ID 必须匹配
	//前提:实现Serializable(标记性)接口
	Student stu = new Student("张三", 20);  
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(PATH+"student.txt"));  
	
	oos.writeObject(stu);  
	oos.close();  
	//--------------------------------------------//
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream(PATH+"student.txt"));  
	
	System.out.println(ois.readObject());  
	ois.close();

细节一:serialVersionUID (版本号)

当你序列化一个对象后,如果修改了类的代码(比如加了个字段),再进行反序列化就会报 InvalidClassException

  • 解决:手动给类加一个固定的 ID。

  • private static final long serialVersionUID = 1L;

  • 这样即使类改了,只要 ID 没变,Java 就会认为它们是同一个类。

细节二:transient (瞬态关键字)

如果你不希望某个成员变量被保存到硬盘(比如用户的密码),只需在变量前加 transient

  • private transient String password;

  • 反序列化回来后,该字段会变成默认值(如 null)。

细节三:

  • 序列化时写入什么类型,反序列化时就必须用什么类型接收,类型必须完全一致。
	Student s1 =  new Student("张三",23,"南京");  
	Student s2 =  new Student("李四",22,"重庆");  
	Student s3 =  new Student("王五",24,"上海");  
	
	ArrayList<Student> list = new ArrayList<>();  
	list.add(s1);  list.add(s2);  list.add(s3);  
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(PATH+"student.txt"));  
	//序列化使用Array List写入
	oos.writeObject(list);  
	oos.close();
	
	//---------反序列化---------//
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream(PATH+"student.txt"));  
	//同样使用ArrayList反序列化
	ArrayList<Student> list =(ArrayList<Student>)ois.readObject();  
	for (Student s : list) {  
	    System.out.println(s);  
	}
	ois.close();

打印流

  • 地位:最方便的只写流。

  • 三大招

    1. println():写啥都行,写完换行。

    2. printf():格式化打印(如保留两位小数 % .2f)。

    3. System.setOut():重定向输出。

  • 注意

    • 打印流只有输出,没有输入。

    • 它是对其他输出流的包装,可以配合缓冲流一起用:new PrintWriter(new BufferedWriter(new FileWriter("a.txt")))

类别名 称对应家族常用场景
字节打印流PrintStreamOutputStream标准输出(控制台)、日志记录
字符打印流PrintWriterWriter网页响应 (Servlet)、文本写入

字节打印流

	PrintStream ps = new PrintStream(new FileOutputStream(PATH + "c.txt"),true,Charset.forName("UTF-8"));  
	ps.println(97);//写出 + 自动刷新 + 自动换行  
	ps.printf("%s+%s = %s",1,1,2);  
	ps.close();

字符打印流

	PrintWriter pw = new PrintWriter(new FileWriter(PATH+"c.txt"), true);  
	pw.println("你好");  
	pw.close();

核心区别

维度字节打印流 (PrintStream)字符打印流 (PrintWriter)
所属家族OutputStream 的子类Writer 的子类
处理单位字节(即使你传的是字符,它也会先转成字节)字符(直接按字符处理)
内部缓冲区没有(它直接调底层的 OutputStream)(它内部维护了一个字符缓冲区)
自动刷新 (AutoFlush)只要调了 println 就会自动刷新必须在构造器手动开启 true 才会自动刷新
主要用途标准输出 (System.out)、写原始字节数据写纯文本、Web 开发 (Servlet 响应)

标准输出流 (System.out)

  • 本质:一个预定义的 PrintStream 对象。

  • 特点

    • 随 JVM 启动而产生,随 JVM 关闭而销毁。

    • 专门用于向控制台输出数据。

  • 核心功能

    • print() / println():万能打印。

    • printf():格式化打印(如 %d, %f)。

    • System.setOut(PrintStream):修改输出目的地(常用于写简单的日志工具)。

解压缩流/压缩流

	InputStream(字节输入流)-> 解压缩流
	OutputStream(字节输出流)-> 压缩流

解压

什么是 ZipEntry?

  • 一个 ZIP 文件就像一个大包裹,里面的每个文件或文件夹都是一个 ZipEntry

  • 解压过程:就是通过 getNextEntry() 方法,一个一个地把包裹里的东西(Entry)拿出来,然后用普通的字节流(FileOutputStream)把它们写到本地。

public static void unzip(File src,File dest) throws IOException {  
    ZipInputStream zip = new ZipInputStream(new FileInputStream(src));  
    ZipEntry entry;  
    while ((entry = zip.getNextEntry()) != null) {  
  
        if(entry.isDirectory()){  
            File f = new File(dest,entry.getName());  
            f.mkdirs();  
        }else{  
            FileOutputStream fos = new FileOutputStream(new File(dest,entry.getName()));  
            int b;  
            while((b =  zip.read()) != -1){  
                fos.write(b);  
            }  
            fos.close();  
            zip.closeEntry();//表示在压缩包中的一个文件处理完毕了  
        }  
    }  
    zip.close();  
}
今日访问 ... 次 | 今日访客 ... 人 | 本页阅读 ...
小站已萌萌哒运行了 0 0 0
已累计耕耘 16 篇博文 · 共 42.69k 个字
总访问量 ...