Java IO 学习笔记
一、字符集、编码和解码
1. 三种常见字符集(必考)
| 字符集 | 汉字字节 | 英文/数字字节 | 特点 |
|---|---|---|---|
| ASCII | 不支持汉字 | 1 字节 | 只有字母、数字、符号 |
| GBK | 2 字节 | 1 字节 | Windows 默认中文编码 |
| UTF-8 | 3 字节 | 1 字节 | 互联网通用、项目最常用 |
两个核心结论
- 乱码根本原因:编码、解码使用的字符集不一致
- 英文数字几乎不会乱码:GBK、UTF-8 都兼容 ASCII
2. 编码 & 解码 核心概念
- 编码:字符 → 字节(往外传输、存储)
- 解码:字节 → 字符(读取展示)
编码方法(String → byte[])
byte[] getBytes():使用系统默认字符集编码(不推荐)byte[] getBytes("字符集"):指定字符集编码(推荐)
解码方法(byte[] → String)
new String(byte[]):系统默认字符集解码new String(byte[], "字符集"):指定字符集解码(推荐)
3. 演示代码
importjava.util.Arrays;publicclassCharSetDemo{publicstaticvoidmain(String[]args)throwsException{Stringstr="中国";// UTF-8 编码:汉字3字节byte[]bytesUtf8=str.getBytes("UTF-8");System.out.println("UTF-8 字节:"+Arrays.toString(bytesUtf8));// GBK 编码:汉字2字节byte[]bytesGbk=str.getBytes("GBK");System.out.println("GBK 字节:"+Arrays.toString(bytesGbk));// 编码GBK、解码UTF-8 → 乱码StringbadStr=newString(bytesGbk,"UTF-8");System.out.println("乱码结果:"+badStr);// 编码、解码一致 → 正常StringgoodStr=newString(bytesGbk,"GBK");System.out.println("正确结果:"+goodStr);}}4. 相关问题
问:为什么英文、数字一般不会乱码?
UTF-8、GBK 都向下兼容 ASCII,英文字节完全一致。
问:UTF-8 和 GBK 汉字占用字节?
UTF-8 汉字 3 字节,GBK 汉字 2 字节。
问:乱码产生的根本原因?
编码时使用的字符集与解码时使用的字符集不统一。
问:Java 如何手动完成编码解码?
编码调用
String.getBytes(指定字符集);解码调用new String(字节数组,指定字符集),二者字符集必须一致。
二、IO流作用读写文件中的数据。
本质:把数据从一个地方搬运到另一个地方,流就是数据传输的通道
- 输入 Input:读数据 → 从外部(文件、网络、内存)读到程序内存
- 输出 Output:写数据 → 把程序内存的数据写到外部(文件、网络、控制台)
三、四大抽象父类(按数据类型+流向划分)
口诀:输入读,输出写;Stream字节,Reader/Writer字符
| 类型 | 父类 | 作用 | 适用场景 |
|---|---|---|---|
| 字节输入流 | InputStream | 读字节数据(文件→程序) | 所有文件:文本、图片、视频、音频 |
| 字节输出流 | OutputStream | 写字节数据(程序→文件) | 所有文件 |
| 字符输入流 | Reader | 读字符数据(文件→程序) | 仅文本文件,自动处理中文 |
| 字符输出流 | Writer | 写字符数据(程序→文件) | 仅文本文件 |
区分技巧
- 类名以
Stream结尾 → 字节流- 类名以
Reader/Writer结尾 → 字符流- 输入:数据流向程序(读)
- 输出:数据从程序向外(写)
四、文件节点流(最常用实现类)
1. 字节流
FileInputStream:文件字节输入流,读文件字节FileOutputStream:文件字节输出流,写文件字节
字节流万能,任何类型文件都可以复制。
2. 字符流
FileReader:文件字符输入流,读取文本FileWriter:文件字符输出流,写出文本
只适合文本文件,读写中文更方便。
五、核心方法
✅ 字节输入流 FileInputStream
int read():读取1个字节,返回字节;读到末尾返回-1int read(byte[] buf):读取一批字节存入字节数组,返回读到的字节数量;末尾-1byte[] readAllBytes()一次性读取全部字节,返回字节数组(JDK11+,适合小文件)void close()关闭流,释放操作系统资源,必须调用
读取中文注意事项
- UTF-8 中文占 3 字节,GBK 中文占 2 字节;单个字节循环读取会拆分汉字,乱码。
- 解决方案:一次性读取全部字节,
new String(bytes,"UTF-8")转字符串。 - 缺点:
readAllBytes()/ 定义和文件等大数组读取,大文件会内存溢出 OOM。大文件要用字节数组分段循环读取。
// 根据文件路径创建输入流FileInputStreamfis=newFileInputStream("a.txt");// 根据File对象创建FileInputStreamfis=newFileInputStream(newFile("a.txt"));注意:文件必须存在,否则抛出 `FileNotFoundException`-----------------------------------------------FileInputStreamfis=newFileInputStream("a.txt");byte[]buffer=newbyte[1024];// 缓冲区 ,定义一个字节数组用于每次读取字节intlen;//定义一个变量记住每次读取了多少个字节while((len=fis.read(buffer))!=-1){Strings=newString(buffer,0,len);//把读取到的字节数组转换成字符串输出System.out.print(s);}fis.close();------------------------------------------------byte[]bytes=is.readAllBytes();//一次性读完所有的字节Strings=newString(bytes);✅ 字节输出流 FileOutputStream
void write(int b):写入1个字节void write(byte[] b):写入整个字节数组void write(byte[] b, off, len):写入数组一部分(off起始索引,len长度)
⚠️ FileOutputStream 默认覆盖原有文件;构造器第二个参数传
true代表追加写入,不覆盖new FileOutputStream("a.txt", true)
// 覆盖模式:文件不存在自动创建;存在则清空原有内容FileOutputStreamfos=newFileOutputStream("a.txt");// 追加模式:第二个参数true,文件末尾追加,不清空旧内容FileOutputStreamfos=newFileOutputStream("a.txt",true);-------------------------------------------------//中文字符串转字节数组:`str.getBytes("UTF-8")`Stringstr="你好";//写中文fos.write(str.getBytes(StandardCharsets.UTF_8));✅ 字符流 FileReader / FileWriter
方法和字节流几乎一样,只是数组是char[]。
⚠️字符输出流必须刷新/关闭:flush() 刷新缓冲区;close()关闭同时自动flush。不刷新数据留在缓冲区写不到文件。
文件字符输入流 FileReader(读文本文件):字符流只能操作文本文件,自动按编码解码,读取中文不会乱码。底层还是字节流 + 编码。
六、异常处理(重点)
JDK7推荐try-with-resources
实现
AutoCloseable接口的流,可以写在try()里,代码执行完毕自动关闭流,不用手动写finally。
try(FileInputStreamfis=newFileInputStream("a.txt")){//读写代码}catch(Exceptione){e.printStackTrace();}七、缓冲流(包装流,提速!)
黑马视频
缓冲流也叫高效流、包装流,本身不能直接操作文件,需要包裹节点流,对原始字节 / 字符流的增强
核心目的:减少磁盘 IO 次数,提升读写性能。
【方案1:普通字节流,数组1KB】
D盘文件 --(每次读1KB,频繁磁盘IO)–> FileInputStream --> byte[1KB]数组(内存) --> 应用程序
✅ 特点:IO次数多,速度最慢
【方案2:普通字节流,数组8KB】
D盘文件 --(每次读8KB,IO次数变少)–> FileInputStream --> byte[8KB]数组(内存) --> 应用程序
✅ 特点:比1KB快;没有预读,你要多少才读多少
【方案3:BufferedInputStream缓冲流(自带8KB缓冲区)】推荐
D盘文件 --(一次性预读满8KB到内置缓冲区)–> FileInputStream --> 缓冲流内置8KB缓存池(内存) --> 应用程序
✅ 特点:预加载,后续read直接读内存,磁盘IO最少,性能最优
字节缓冲流 :BufferedInputStream字节缓冲输入流BufferedOutputStream字节缓冲输出流
字符缓冲流:BufferedReader字符缓冲输入流( 特有方法:String readLine()读取一行文本,读到末尾返回 null)BufferedWriter字符缓冲输出流(特有方法:newLine()跨平台换行)
| 缓冲流 | 缓冲大小 | 包装原始流 | 特有方法 | 适用文件类型 |
|---|---|---|---|---|
| BufferedInputStream | 8192 字节 | FileInputStream | 无 | 任意文件 |
| BufferedOutputStream | 8192 字节 | FileOutputStream | 无 | 任意文件 |
| BufferedReader | 8192 字符 | FileReader | readLine() 读一行 | 仅文本 |
| BufferedWriter | 8192 字符 | FileWriter | newLine() 换行 | 仅文本 |
重点:缓冲流只是加速,底层还是原来的流。关闭包装流时,内部包裹的流会自动关闭。
✅ 缓冲流属于包装流,不能直接对接文件,需要包装原始流(FileInputStream/FileOutputStream)。
✅ 缓冲区默认大小:8192 字节(8KB),可以在构造方法自定义缓冲区大小。
✅ 关闭资源:只关闭外层缓冲流即可,内部被包装的流会自动关闭。
✅ 性能核心:减少磁盘 IO 操作次数,IO 操作最耗时的就是硬盘读写。
配套的缓冲字节输出流
BufferedOutputStream:数据先写到内存缓冲区,缓冲区满了才一次性写入硬盘;也可以手动flush()强制刷新缓冲区。
publicclassTest{publicstaticvoidmain(String[]args){//需求:把《出师表》的文章顺序进行恢复到一个新文件中。try(//1. 定义一个(高级流)缓存字符输入流管道与源文件接通。BufferedReaderbr=newBufferedReader(newFileReader("src\\main\\resources\\a.txt"));//5. 定义一个缓存字符输出管道与目标文件接通。BufferedWriterbw=newBufferedWriter(newFileWriter("src\\main\\resources\\a01.txt"))){//2. 定义一个 List 集合存储读取的每行数据。ArrayList<String>lists=newArrayList<>();//3. 按照读取行数,存入到文件中去,定义一个循环按照行读取数据,存入到 List 集合中去。Stringline;while((line=br.readLine())!=null){lists.add(line);}// 4. 对List集合中的每行数据按照首字符编号升序排序。Collections.sort(lists);//只能处理个位数的序号Collections.sort(lists,newComparator<String>(){//重写排序规则@Overridepublicintcompare(Strings1,Strings2){// 按 . 分割,取出前面的数字部分StringnumStr1=s1.split("\\.")[0];StringnumStr2=s2.split("\\.")[0];intnum1=Integer.parseInt(numStr1);intnum2=Integer.parseInt(numStr2);returnInteger.compare(num1,num2);}});System.out.println(lists);//6. 遍历 List 集合中的每个元素,用缓冲输出管道写出并换行。for(Stringlist:lists){bw.write(list);bw.newLine();//换行}}catch(Exceptione){e.printStackTrace();}}}拓展 : InputStreamReader字符输入转换流(读)
作用:解决文件编码和程序默认编码不一致时,字符流读取文本乱码问题。public InputStreamReader(InputStream is)把原始的字节输入流,按照代码默认编码转成字符输入流(效果等价 FileReader)public InputStreamReader(InputStream is , String charset)把原始的字节输入流,按照指定字符集编码转成字符输入流(⭐重点)
OutputStreamWriter 字符输出转换流(写)
作用:把程序中的字符,按照指定编码转成字节写入文件,控制写出文件的编码格式。public OutputStreamWriter(OutputStream os)字符按 JVM 默认编码转为字节输出流等价 FileWriter,不推荐自定义编码场景public OutputStreamWriter(OutputStream os, String charset)字符按指定字符集转为字节输出流 ⭐重点控制输出文件编码为 GBK/UTF-8
- ✅ 身份:
InputStreamReader / OutputStreamWriter属于字符流,不是字节流,只是包装字节流。 - ✅ FileReader =
new InputStreamReader(new FileInputStream(file), 默认编码) - ✅ 编码必须和文件真实编码一致,否则依然乱码。
- ✅ 关闭流:只需要关闭外层转换流 / 缓冲流,内部包装的字节流会自动关闭。
- ✅ OutputStreamWriter 写出数据,数据先放缓冲区;
flush()/close()才会刷入磁盘。 - ❌ 误区:转换流可以提高读写速度。
转换流不提升性能,它的唯一职责是编码转换;想要提速要搭配 BufferedReader/BufferedWriter。
八、打印流 PrintStream & PrintWriter
一、打印流概述
打印流包含两个类:PrintStream、PrintWriter
- 作用:便捷、高效输出数据,打印啥,输出就是啥
System.out本质就是PrintStream对象,控制台输出就是打印流
✅ 共同点:
打印API使用方式完全一样,都拥有print()/println(),可以输出任意类型数据。
二、两个打印流区别
| 类名 | 流类型 | 继承 | 特点 |
|---|---|---|---|
| PrintStream | 字节打印流 | OutputStream | 底层字节流,支持写字节数据 |
| PrintWriter | 字符打印流 | Writer | 底层字符流,支持写字符,文本输出优先推荐 |
三、PrintStream 构造器
| 构造器 | 说明 |
|---|---|
public PrintStream(OutputStream/File/String) | 打印流直接通向字节输出流 / 文件 / 文件路径 |
public PrintStream(String fileName, Charset charset) | 指定写出的字符编码 |
public PrintStream(OutputStream out, boolean autoFlush) | 开启自动刷新 |
public PrintStream(OutputStream out, boolean autoFlush, String encoding) | 自动刷新 + 指定字符编码(重点) |
四、PrintStream 核心方法
| 方法 | 说明 |
|---|---|
println(任意类型) | 打印任意类型数据,自带换行 |
print(任意类型) | 打印任意类型数据,不带换行 |
write(int / byte[] / byte[]部分) | 继承父类,支持写字节数据 |
⭐重点区分:
println(97):原样输出字符串97write(97):输出ASCII码对应的字符a
五、PrintWriter 构造器(字符打印流)
// 1. 直接传入文件路径,指定编码PrintWriterpw=newPrintWriter("test.txt","UTF-8");// 2. 包装字符输出流,开启自动刷新PrintWriterpw=newPrintWriter(newFileWriter("test.txt"),true);打印流优势
- 使用简单,支持直接输出任意类型(int、double、boolean、String 等),原样打印
- 性能高效
- 支持自动刷新 autoFlush
- 可以自定义输出文件的字符编码
- 打印方法不强制抛出 IO 异常,内部自行捕获处理
九、IO 流选择原则(必背)
- 如果是图片、视频、音频:只能用字节流
- 如果是纯文本文件:字节流 / 字符流都可以,优先字符流(中文友好)
- 文件复制:一律推荐字节流(万能)
十、常见坑
- 判断文件读取结束标志:返回 -1(字节流) /null(BufferedReader 的 readLine),不要用判断!=0
- FileWriter 不调用 flush/close,数据丢失
- 输出流默认覆盖文件,追加需要开启追加开关 true
- 字符流不能复制图片视频!会损坏文件
- 流用完必须关闭,释放操作系统资源;try-with-resources 自动关闭
十一、相关问题
问:IO 流分为哪四大类?各自作用?
字节输入流InputStream(读字节)
字节输出流OutputStream(写字节)
字符输入流Reader(读字符)
字符输出流Writer(写字符)
字节流支持所有文件,字符流只支持文本文件。
问 :字节流和字符流区别?
字节流操作字节,可以处理任意文件;字符流操作字符,仅文本文件,自动处理中文。
问:缓冲流作用?
增加缓冲区,减少磁盘 IO 次数,提升读写性能;BufferedReader 有 readLine 读一行,BufferedWriter 有 newLine 换行。
问:try-with-resources 作用?
自动关闭实现 AutoCloseable 接口的流,简化代码,避免忘记关闭流。
问 : 缓冲流为什么快
减少磁盘 IO 次数。磁盘读写速度远低于内存,缓冲流批量加载数据到内存缓冲区,程序直接读写内存,不用频繁访问硬盘。
问:缓冲流关闭的时候,只关外层吗?
是的。缓冲流的 close () 内部会自动调用被包装流的 close (),只需要关闭缓冲流即可。
问:字符缓冲流缓冲区是 8192 字符,等于 8KB 吗?
不等于。char 占 2 字节,8192 字符占用内存为 16KB;字节缓冲流 8192 字节就是 8KB。
问:flush () 和 close () 的区别?
flush ():把缓冲区数据刷入磁盘,流还能继续读写;
close ():底层自动执行 flush (),然后关闭流资源,关闭之后流不能继续使用。