Java IO 文件与流全套详解
前言
IO(Input/Output,输入输出)是 Java 后端、文件操作、网络编程的基础,分为文件系统 IO(本地文件读写)与网络 IO。本文完整梳理两大核心模块:
- java.io.File 类:本地文件、目录的增删改查、属性判断、递归遍历目录;
- 四大 IO 流:字符流 Reader/Writer、字节流 InputStream/OutputStream,包含读写模板、资源关闭、缓冲区、try-with-resources 语法;
- 综合实战:递归遍历目录,根据关键词匹配文件名,交互式确认删除文件。
基本概念:硬件和文件概述
文件定义
操作系统里,会把很多硬件设备和软件资源都抽象成"文件",统一进行管理。
但大部分情况下,谈到的文件指的是硬盘里的文件 ⇒ 文件就相当于是对硬盘数据的一种抽象。
那硬盘都有哪些呢?接下来介绍一下硬盘的类别。
硬盘
硬盘有机械硬盘和固态硬盘两种。
机械硬盘(HDD)
- 适合顺序读写,不适合随机读写。
- 磁头的移动需要时间。
固态硬盘(SSD)
- 固态硬盘里是集成程度很高的芯片(类似于CPU、内存)。
- 固态硬盘比机械硬盘效率高,无论是顺序读写还是随机读写。
- 一般系统盘都要用固态硬盘,若想存储更多的资料,可以额外加机械硬盘。
服务器开发中,两种硬盘都会涉及到,尤其是一些用来存储大规模数据的机器,仍然是机械硬盘为主。
实际的操作是通过文件的方式来操作硬盘。
即使是固态硬盘,读写速度还是要比内存低很多。
内存 VS 外存
内存:
速度快,空间小,贵,数据是断电就消失。
外存:
速度慢,空间大,便宜,数据是断电后还在。
文件路径
- 一台计算机上有很多文件,这些文件是通过"文件系统"来进行组织的。
- 操作系统,使用"目录"这样的结构来组织文件,使用目录的层次结构来描述文件所在的位置。
- 文件所在的位置也就是路径。
- 路径又有两种表示:绝对路径和相对路径。
绝对路径
以C:(或者是D:)盘符开头的路径。
相对路径
需要先指定一个目录作为基准目录,从基准目录出发,看沿着哪条路线能找到指定文件(往往以 . 或者 ... 开头的; . 表示当前目录;... 表示当前目录的上一级目录)。
若是命令行进行操作,基准目录就是当前所处的目录;若是图形化界面的程序,基准目录就不好说了。
对于IDEA来说,基准目录就是项目目录。
文件类型
从编程的角度,文件有两种类型:文本文件和二进制文件,它们的代码编写方式不同。
文本文件
文件中保存的数据都是字符串(本质上也是二进制数据,它的每一段二进制都严格对应某一套字符编码里的合法字符),保存的内容都是合法的字符。
二进制文件
文件中保存的数据,仅仅是二进制数据,不要求保存的内容是合法字符。
那么如何判定一个文件是文本文件还是二进制文件?
直接使用记事本打开这个文件,若打开之后乱码,那么该文件就是二进制文件,否则,就是文本文件。
对文件的操作
对文件的操作分成两种:文件系统的操作+文件内容的操作。
- 文件系统的操作:File类,创建文件、删除文件、判定文件是否存在、判定文件类型、重命名...
- 文件内容的操作:流对象,读文件、写文件。
文件系统的操作
File类的常见属性
| 修饰符及类型 | 属性 | 说明 |
|---|---|---|
| static String | pathSeparator | 依赖于系统的路径分隔符,String类型的表示 |
| static char | separator | 依赖于系统的目录分隔符,char类型的表示 |
File类的构造方法
- File(File parent,String child):父目录+孩子路径
- File(String pathname):文件路径
- File(String parent,String child):父目录(用路径表示)+孩子路径
一个File对象,就表示了一个硬盘上的文件,在构造对象时,就需要把这个文件的路径指定进来。
文件名=前缀+扩展名。⇒ 使用路径构造File对象,一定要把前缀和扩展名都带上。
获取文件信息代码示例
java
import java.io.File;import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
File file = new File("./test.txt");
System.out.println(file.getParent());//获取当前文件的父目录文件路径
System.out.println(file.getName());//获取当前文件的文件名
System.out.println(file.getPath());//获取当前文件的路径
System.out.println(file.getAbsolutePath());//获取当前文件的绝对路径
System.out.println(file.getCanonicalPath()); //获取当前文件的修饰过的文件路径
}
}
运行结果:
.
test.txt
.\test.txt
D:\Java\FileIO.\test.txt
D:\Java\FileIO\test.txt
文件的创建代码示例
java
import java.io.File;import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
File file = new File("./test.txt");
System.out.println(file.exists());//文件是否存在
System.out.println(file.isDirectory());//是否为目录
System.out.println(file.isFile());//是否为普通文件
//创建文件
boolean ret=file.createNewFile();//返回值为boolean
System.out.println("ret = "+ret);
System.out.println(file.exists());//文件是否存在
System.out.println(file.isDirectory());//是否为目录
System.out.println(file.isFile());//是否为普通文件
}
}
运行结果:
true
false
true
ret = false
true
false
true
文件的删除代码示例
java
import java.io.File;import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
File file = new File("./test.txt");
file.createNewFile();
boolean ret= file.delete();
System.out.println("ret = "+ret);
File file1 = new File("./test.txt");
file1.createNewFile();
file1.deleteOnExit();//JVM运行结束时才会进行删除
System.out.println(file1.exists());
}
}
运行结果:
ret = true
true
打印目录下所有文件名的代码示例
java
import java.io.File;import java.io.IOException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
File f=new File("d:/");//若f为文件,则打印结果为null
String[] files=f.list();//返回File对象代表的目录下的所有文件名
System.out.println(Arrays.toString(files));
}
}
创建单层文件夹代码示例
java
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
File f=new File("./java123");
boolean ret=f.mkdir();//创建File对象代表的目录,单层目录
if(ret){
System.out.println("文件夹创建成功");
}else{
System.out.println("创建失败,文件夹已存在或路径错误");
}
}
}
运行结果:
文件夹创建成功
创建多层文件夹的代码示例
java
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
File f=new File("./java234/aaa/bbb/ccc");
boolean ret=f.mkdirs();//创建File对象代表的目录,多层目录
if(ret){
System.out.println("文件夹创建成功");
}else{
System.out.println("创建失败,文件夹已存在或路径错误");
}
}
}
运行结果:
文件夹创建成功
文件重命名的代码示例
java
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
File srcFile=new File("./java123");//这个文件必须存在,对它重命名
File destFile=new File("./java456");//这个文件可以不存在
boolean ret=srcFile.renameTo(destFile);
if(ret) System.out.println("重命名成功");
else System.out.println("重命名失败");
}
}
运行结果:
重命名成功
文件内容的读写
对文件内容的操作有三个步骤:
- 打开文件(构造方法);
- 读(输入)/写(输出);
- 关闭文件。
关闭文件这个步骤很重要,close() 是为了释放文件描述符。
文件描述符表:一个顺序表(数组),一个进程每次打开一个文件,就需要在这个表里面分配一个元素,但这个数组的长度存在上限。
若代码一直打开文件,而不关闭,就会使文件描述符表里的元素越来越多,一直到把这个数组占满了,后续再尝试打开文件,就会出错。
对文件内容的操作是通过流对象来实现的。
在标准库中,将流对象归并为两大类别:字节流、字符流。
字节流 VS 字符流
字节流:
- 对应着二进制文件。
- 每次读/写的最小单位是字节。
- 提供了两个父类:输入InputStream、输出OutputStream。
注意:判断是输入还是输出,要站在CPU的角度,比如说把一个数据保存在硬盘中,这个过程就是输出(写)。
字符流:
- 对应着文本文件。
- 每次读/写的最小单位是字符。(一个字符对应几个字节,要看当前的字符集是哪种)
- 提供了两个父类:输入Reader、输出Writer。
- 字符流本质上是对字节流进行了又一层封装。字符流能自动把文件中几个相邻的字节,转换成一个字符,帮助我们完成了一个自动查字符集表。
Reader 的使用
简要介绍
- Reader 的修饰符及类型是 public abstract,抽象类,不能new出实例,只能new子类。
- 创建Reader对象的过程就是"打开文件"过程。
- 主要方法:
(1)read():无参,一次读取一个字符,返回值为int(0-65535),当返回值为-1时表示文件已读完;
(2)read(char\[\] cbuf):一次读取若干个字符,会把参数指定的cbuf数组给填充满。
(3)read(char\[\] cbuf,int off,int len):一次读取若干个字符,会把参数指定的cbuf数组中从off位置开始,到len这么长的范围尽量填满。- 为什么字符流Reader能精准一次读一个完整字符,不会读到半个汉字乱码呢?
把多个Unicode连续放到一起,很难区分从哪里到哪里是一个完整字符,但utf8可以区分。
read()的使用代码示例
java
import java.io.Reader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Reader reader=new FileReader("./test.txt");//该文件中的内容为:你好
while(true){
int c= reader.read();
if(c==-1){//文件中的数据已经读完了
break;
}
char ch=(char)c;
System.out.println(ch);
}
reader.close();//一个文件使用完了,要记得close
}
}
运行结果:
你
好
read(char\[\] cbuf)的代码示例
java
import java.io.Reader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
Reader reader=new FileReader("./test.txt");//该文件中的内容为:你好,java
while(true){
char[] cubf=new char[4];
int c= reader.read(cubf);//返回的是读取元素的个数
if(c==-1){//文件中的数据已经读完了
break;
}
System.out.println("c = "+c);
for(int i=0;i<c;i++){
System.out.println(cubf[i]);
}
}
reader.close();
}
}
运行结果:
c = 4
你
好
,
j
c = 3
a
v
a
注意:一个文件使用完毕之后一定要close()。
以上的书写方式仍然可能会出现文件资源泄露的风险。
可以采用try-finally方式或try with resource方式(要求必须实现Closeable接口)。
修改后的代码:
java
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class Test {
public static void main(String[] args) throws IOException {
// try() 括号内声明实现AutoCloseable接口的流,代码块结束自动close
try (Reader reader = new FileReader("./test.txt")) {
while (true) {
char[] cubf = new char[4];
int c = reader.read(cubf); // 返回本次实际读取到的字符数量
if (c == -1) { // 读到文件末尾
break;
}
System.out.println("c = " + c);
for (int i = 0; i < c; i++) {
System.out.println(cubf[i]);
}
}
}
// 不需要写 reader.close()!try-with-resources 无论正常结束/异常都会自动关闭流
}
}
Writer 的使用
简要介绍
- Writer写入文件,默认情况下会把原有文件的内容清空掉再写入,若不想清空,需要在构造方法中加个参数true。
- 主要方法:
(1)write(int c):一次写一个字符。
(2)write(String str):一次写一个字符串。
(3)write(char\[\] cbuf):一次写多个字符。
(4)write(String str,int off,int len)。
(5)write(char\[\] cbuf,int off,int len)。
代码示例
java
public class Main {
public static void main(String[] args) {
try(Writer writer=new FileWriter("./test.txt",true)){
writer.write("我在学习");
}catch(IOException e){
throw new RuntimeException(e);
}
}
}
InputStream 的使用
简要介绍
主要方法:
(1)read():方法返回值为int(0-255),值为-1时代表读到文件末尾。
(2)read(byte\[\] b).
(3)read(byte\[\] b,int off,int len)。
代码示例
java
public class Main {
public static void main(String[] args) {
try(InputStream inputStream=new FileInputStream("./test.txt")){
byte[] buffer=new byte[1024];
int n=inputStream.read(buffer);
System.out.println("n = "+n);
for (int i = 0; i < n; i++) {
System.out.printf("%x\n",buffer[i]);//字节一般用十六进制来表示,ASCII码
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
n = 25
e4
bd
a0
e5
a5
bd
ef
bc
8c
6a
61
76
61
e6
88
91
e5
9c
a8
e5
ad
a6
e4
b9
a0
OutputStream 的使用
简要介绍
- 和Writer类似,写入文件,默认情况下会把原有文件的内容清空掉再写入,若不想清空,需要在构造方法中加个参数true。
- 主要方法:
(1)write(int b)。
(2)write(byte\[\] b)。
(3)write(byte\[\] b,int off,int len)。
代码示例
java
public class Main {
public static void main(String[] args) {
try(OutputStream outputStream=new FileOutputStream("./test.txt")){
String s="HelloWorld";
outputStream.write(s.getBytes());
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
字节流的代码可以转成字符流
代码示例1
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try(InputStream inputStream=new FileInputStream("./test.txt");){
Scanner scanner=new Scanner(inputStream);
String s=scanner.next();
System.out.println(s);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
HelloWorld
代码示例1
java
public class Main {
public static void main(String[] args) {
try(OutputStream outputStream=new FileOutputStream("./test.txt");
PrintWriter writer=new PrintWriter(outputStream)){
writer.printf("hi");
writer.flush();//PrintWriter这样的类,在写入时,不一定是直接写硬盘,而是先把数据写入到一个内存构成的缓冲区中,此时需要刷新缓冲区,flush
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
实战训练
扫描指定⽬录,并找到名称中包含指定字符的所有普通⽂件(不包含⽬录),并且后续询问⽤⼾是否要删除该⽂件。
分析:文件系统操作:
- list列出目录内容;
- 判定文件类型;
- 删除文件。
java
public class Main {
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
//1.让用户输入指定目录
System.out.println("请输入要扫描的指定目录:");
String s=scanner.next();
File file=new File(s);
if(!file.isDirectory()){//用户输入的不是目录
System.out.println("您输入的路径有误");
return;
}
//2.让用户输入要删除文件的关键词
System.out.println("请输入要删除文件的关键词:");
String key=scanner.next();
//3.进行递归扫描
scanDir(file,key);
}
public static void scanDir(File root,String key){
//1.先列出该目录下的所有文件和目录
File[] list=root.listFiles();
if(list==null){
//当前目录为空,直接返回
return;
}
//2.遍历每个元素并处理
for(File f:list){
if(f.isDirectory()){
//是目录,就递归判定子目录中的内容
scanDir(f,key);
}else{
//普通文件,检查该文件是否要删除
checkDelete(f,key);
}
}
}
public static void checkDelete(File f,String key){
if(!f.getName().contains(key)){
//无需删除,直接返回
return;
}
//需要删除
System.out.println("当前文件为:"+f.getAbsolutePath()+",请确认是否要删除(Y/N):");
Scanner scanner=new Scanner(System.in);
String choice =scanner.next();
if(choice.equals("Y")||choice.equals("y")){
//删除
f.delete();
System.out.println("删除完毕");
}else{
//不删除
System.out.println("取消删除");
}
}
}
全文总结
- 区分createNewFile()/mkdir()/mkdirs()、delete()/deleteOnExit()、list()/listFiles()常用 API;
- IO 流分为字节流(任意文件)、字符流(仅限文本);牢记 CPU 视角区分输入 / 输出;
- 资源释放重中之重,优先使用try-with-resources自动关闭流,杜绝文件描述符泄露;
PrintWriter、Writer 存在缓冲区,必要时调用flush()防止数据滞留内存丢失;- 递归遍历目录是 IO 经典编程题,核心思路:listFiles()遍历 + 子目录递归 + 文件业务判断。
本文全部示例代码均可直接复制运行,,欢迎点赞收藏,评论区交流学习问题!
🔗 系列文章导航
本篇是「Java并发编程系列」的连载内容,点击链接查看完整系列:
🔹 上一篇:Java 并发编程(七)|JUC 工具类与线程安全集合考点