Java 文件操作与IO(下):字节流、字符流实战IO读写
承接上篇File类,本篇重点讲解数据流读写文件内容,InputStream、OutputStream字节流,Scanner、PrintWriter字符封装,附带3个完整实战小案例。
一、IO流的形象理解
把数据比作水池里面的水:
- 输入流 Input :从水池(文件)往外取水 → 读文件
- 输出流 Output :往水池(文件)灌水 → 写文件
Java IO分为:
- 字节流 :以字节byte为单位,
InputStream / OutputStream,处理所有文件(文本、图片、视频) - 字符流:以字符char为单位,专门处理文本文件
注意:
InputStream、OutputStream都是抽象类,读取文件使用它的实现类:FileInputStream、FileOutputStream。
二、字节输入流 InputStream(读文件)
核心方法:
//读1个字节,读到文件末尾返回 -1
int read();
//最多读b.length字节,存入byte数组b;返回读到字节数量;读到末尾返回-1
int read(byte[] b);
//off:数组起始下标,len读取长度
int read(byte[] b, int off, int len);
void close(); //关闭流释放资源
资源关闭:IO流属于操作系统资源,必须关闭;推荐使用 try‑with‑resources语法,自动关闭流,不用手动写close。
FileInputStream构造
FileInputStream(File file);
FileInputStream(String pathName);
两种读取文件写法对比
-
一次读一个字节(性能差,IO次数多)
try (InputStream is = new FileInputStream("hello.txt")) {
int b;
while ((b = is.read()) != -1) {
System.out.print((char) b);
}
} -
缓冲区数组读取(性能好,推荐)
try (InputStream is = new FileInputStream("hello.txt")) {
byte[] buf = new byte[1024]; //缓冲区
int len;
while ((len = is.read(buf)) != -1) {
//buf[0 ~ len‑1] 是本次读到有效数据
for(int i = 0; i < len; i++){
System.out.print((char)buf[i]);
}
}
}
直接字节流读取中文很麻烦,需要手动做字符集解码,所以文本文件一般配合Scanner使用。
Scanner包装InputStream读取文本
try(InputStream is = new FileInputStream("hello.txt")){
try(Scanner sc = new Scanner(is,"UTF-8")){
while(sc.hasNext()){
String line = sc.next();
System.out.println(line);
}
}
}
三、字节输出流 OutputStream(写文件)
核心API:
void write(int b); //写单个字节
void write(byte[] b); //把整个字节数组写入
void write(byte[] b,int off,int len); //数组从off位置,写len个字节
void close();
void flush(); //❗刷新缓冲区,把缓冲区数据真正写入磁盘
缓冲区:操作系统为减少IO次数,数据先放到内存缓冲区,缓冲区满才写入磁盘。如果程序结束缓冲区没满,数据会留在内存丢失,必须调用flush()强制刷盘。
FileOutputStream:文件字节输出流,打开文件默认覆盖原有内容。
示例写文件:
try(OutputStream os = new FileOutputStream("output.txt")){
byte[] data = "你好中国".getBytes("UTF-8");
os.write(data);
os.flush(); //强制刷新
}
四、字符便捷打印:OutputStreamWriter + PrintWriter
直接写字节数组不方便,使用OutputStreamWriter把字节流转字符流,再套PrintWriter,就可以使用熟悉的print / println / printf。
try(OutputStream os = new FileOutputStream("output.txt")){
try(OutputStreamWriter writer = new OutputStreamWriter(os,"UTF-8")){
try(PrintWriter pw = new PrintWriter(writer)){
pw.println("第一行文本");
pw.printf("%d 号数据\n",100);
pw.flush(); //一定要刷新缓冲区
}
}
}
五、实战练习案例
注意全部使用try‑with‑resources自动释放资源。
案例1:文件复制工具
读取源文件,写入目标文件,支持判断是否覆盖。
import java.io.*;
import java.util.Scanner;
public class FileCopyDemo {
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(System.in);
System.out.print("输入源文件路径:");
String srcPath = sc.next();
System.out.print("输入目标路径:");
String destPath = sc.next();
File src = new File(srcPath);
File dest = new File(destPath);
if(!src.exists() || !src.isFile()){
System.out.println("源文件错误");
return;
}
if(dest.exists()){
System.out.println("目标文件已存在,是否覆盖 y/n");
String op = sc.next();
if(!"y".equalsIgnoreCase(op)){
System.out.println("复制终止");
return;
}
}
try(InputStream is = new FileInputStream(src);
OutputStream os = new FileOutputStream(dest)){
byte[] buf = new byte[1024];
int len;
while((len = is.read(buf)) != -1){
os.write(buf,0,len);
}
os.flush();
}
System.out.println("复制完成");
}
}
案例2:递归扫描目录,按文件名匹配删除文件
文件系统是树结构,使用递归深度优先遍历目录
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class ScanDeleteDemo {
public static void main(String[] args) throws IOException {
Scanner sc = new Scanner(System.in);
System.out.print("输入扫描根目录:");
String rootPath = sc.next();
File root = new File(rootPath);
if(!root.isDirectory()){
System.out.println("目录非法");
return;
}
System.out.print("输入文件名匹配关键字:");
String keyword = sc.next();
List<File> matchList = new ArrayList<>();
scanDir(root,keyword,matchList);
System.out.println("找到"+matchList.size()+"个匹配文件");
for(File f : matchList){
System.out.printf("文件:%s 是否删除(y/n) ",f.getCanonicalPath());
String op = sc.next();
if("y".equalsIgnoreCase(op)){
f.delete();
System.out.println("已删除");
}
}
}
private static void scanDir(File dir,String keyword,List<File> res){
File[] files = dir.listFiles();
if(files == null) return;
for(File f : files){
if(f.isDirectory()){
scanDir(f,keyword,res);
}else{
if(f.getName().contains(keyword)){
res.add(f);
}
}
}
}
}
案例3:扫描目录,匹配文件内容包含关键字
注意:读取大文件性能差,不适合超大文件
private static boolean contentContains(File file,String keyword) throws IOException{
StringBuilder sb = new StringBuilder();
try(InputStream is = new FileInputStream(file);
Scanner sc = new Scanner(is,"UTF-8")){
while(sc.hasNextLine()){
sb.append(sc.nextLine());
}
}
return sb.toString().contains(keyword);
}
六、核心要点总结
- try‑with‑resources:实现AutoCloseable接口的流对象,写在try()括号内,自动关闭资源,避免资源泄露;
- flush():输出流写操作结束务必flush,防止数据留在缓冲区丢失;
- 字节流
InputStream/OutputStream处理一切文件;文本优先使用Scanner、PrintWriter封装简化字符编解码; - 遍历文件系统使用递归,
listFiles()拿到目录全部子File对象; - read返回
-1才代表读到文件末尾,不是返回0。
拓展提醒:Java NIO是另一套IO模型,本篇讲解传统BIO,是面试基础必考内容。