Java-File与IO流

本文介绍Java常用API中与文件相关的File与IO流操作。

1. File

File是java.io.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件文件夹)。

File类可以用于获取文件信息、判断文件类型、创建文件/文件夹、删除文件/文件夹等操作。

File类只能对文件/文件夹本身进行操作,不能读写文件里存储的数据。

1.1 File对象构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
// 1. 创建File对象(构造器)
// File file = new File("D:/Document/temp/demo.txt");
// File file = new File("D:\\Document\\temp\\demo.txt");
// File file = new File("D:" + File.separator + "Document" + File.separator + "temp" + File.separator + "demo.txt");

// 2. 绝对路径与相对路径
// File file = new File("D:\\Code\\privatespace\\demos\\java-io-demo\\src\\main\\java\\com\\demo\\abc.txt");
File file = new File("java-io-demo/src/main/resources/abc.txt");
// 输出文件路径
System.out.println(file.getPath());
// 单位:字节
System.out.println(file.length());
}

执行结果:

image-20241009142949326

注意点:

  1. File对象:代指可以操作的系统文件对象,包括文件与文件夹。
  2. 绝对路径与相对路径
    1. 绝对路径:文件在系统中的完全路径,包括根路径,如系统盘。
    2. 相对路径:文件相对于项目的路径,一般以项目本身的根路径。
  3. 分割符:
    1. 斜杠:\\,相当于是转义字符
    2. 反斜杠:/(推荐)
    3. File.separator:自动获取当前系统的分割符(不推荐)

1.2 File常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
// 1. 创建文件
File file = new File("java-io-demo/src/main/resources/abc.txt");
// 2. 文件是否存在
System.out.println(file.exists());
// 3. 对象是否为文件
System.out.println(file.isFile());
// 4. 对象是否为目录
System.out.println(file.isDirectory());
// 5. 获取文件名称(包括后缀)
System.out.println(file.getName());
// 6. 返回文件字节数(目录仅返回目录本身字节数)
System.out.println(file.length());
// 7. 获取文件最后修改时间戳
System.out.println(LocalDateTime.ofInstant(Instant.ofEpochMilli(file.lastModified()), ZoneOffset.ofHours(8)));
// 8. 获取创建文件对象时使用的路径
System.out.println(file.getPath());
// 9. 获取绝对路径
System.out.println(file.getAbsolutePath());
}

执行结果:

image-20241009145049613

File常用方法:

方法声明 功能描述
boolean exists() 判断File对象对应的文件或目录是否存在,若存在则返回ture,否则返回false
boolean delete() 删除File对象对应的文件或目录,若成功删除则返回true,否则返回false
boolean createNewFile() 当File对象对应的文件不存在时,该方法将新建一个此File对象所指定的新文件,若创建成功则返回true,否则返回false
String getName() 返回File对象表示的文件或文件夹的名称
String getPath() 返回File对象对应的路径
String getAbsolutePath() 返回File对象对应的绝对路径(在Unix/Linux等系统上,如果路径是以正斜线/开始,则这个路径是绝对路径;在Windows等系统上,如果路径是从盘符开始,则这个路径是绝对路径)
String getParent() 返回File对象对应目录的父目录(即返回的目录不包含最后一级子目录)
boolean canRead() 判断File对象对应的文件或目录是否可读,若可读则返回true,反之返回false
boolean canWrite() 判断File对象对应的文件或目录是否可写,若可写则返回true,反之返回false
boolean isFile() 判断File对象对应的是否是文件(不是目录),若是文件则返回true,反之返回false
boolean isDirectory() 判断File对象对应的是否是目录(不是文件),若是目录则返回true,反之返回false
boolean isAbsolute() 判断File对象对应的文件或目录是否是绝对路径
long lastModified() 返回1970年1月1日0时0分0秒到文件最后修改时间的毫秒值
long length() 返回文件内容的长度
String[] list() 列出指定目录的全部内容,只是列出名称
String[] list(FilenameFilter filter) 接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件
File[] listFiles() 返回一个包含了File对象所有子文件和子目录的File数组

1.3 File创建文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws IOException {
// 1. 创建文件
File file = new File("java-io-demo/src/main/resources/123.txt");
System.out.println(file.createNewFile());
// 2. 创建文件夹(只能创建一级文件夹)
File directory1 = new File("java-io-demo/src/main/resources/directory");
System.out.println(directory1.mkdir());
// 3. 创建文件夹(创建多级文件夹)
File directory2 = new File("java-io-demo/src/main/resources/aaa/bbb/ccc");
System.out.println(directory2.mkdirs());
// 4. 删除文件或空文件夹
System.out.println(file.delete());
System.out.println(directory1.delete());
System.out.println(directory2.delete()); // 只能删除最下一级目录
}

执行结果:

image-20241009150709978

注意点:

  1. createNewFile:创建文件
  2. mkdir:只能创建一级文件夹
  3. mkdirs:能创建多级文件夹
  4. delete:只能删除文件,或删除空文件夹。文件夹中有对象(文件或文件夹)则不能删除

1.4 File遍历文件

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
File file = new File("D:\\Document");
// 1. 获取文件夹下的一级文件名称
String[] list = file.list();
for (String fileName : list) {
System.out.println(fileName);
}
// 2. 获取文件夹下的一级文件对象
File[] files = file.listFiles();
for (File subFile : files) {
System.out.println(subFile.getAbsolutePath());
}
}

执行结果:

image-20241009151417154

image-20241009151429440

注意点:

  1. list:遍历文件夹下一级文件,返回String类型的文件名称。
  2. listFiles:遍历文件下一级文件,返回File类型的文件对象。

1.5 File搜索文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
searchFile(new File("D:/"), "demo.txt");
long endTime = System.currentTimeMillis();
System.out.println("耗时" + (double)(endTime - startTime) / 1000 + "s");
}

/**
* 搜索文件夹下的某个文件
* @param dir
* @param fileName
*/
public static void searchFile(File dir, String fileName) {
// 1. 校验文件夹
if (null == dir || !dir.exists() || dir.isFile()) {
return;
}
// 2. 获取一级文件对象
File[] files = dir.listFiles();
// 3.判断当前目录是否存在一级文件对象
if (null != files && files.length > 0) {
// 4. 遍历一级文件对象
for (File file : files) {
if (file.isFile()) { // 是文件,判断文件是否为要搜索的文件
if (file.getName().contains(fileName)) {
System.out.println("找到了:" + file.getAbsolutePath());
}
} else { // 是文件夹,递归
searchFile(file, fileName);
}
}
}
}

执行结果:

image-20241009152117457

注意点:

  1. 采用递归算法进行搜索
  2. 循环条件:调用listFiles遍历文件夹下的一级文件
  3. 判断条件:是文件:是否为要搜索的文件;是文件夹:递归调用
  4. 终止条件:文件夹为空,或检索到文件

2. IO流

I代表Input,O代表Output,Java中的IO流用于对文本或网络中的数据进行输入和输出操作。

IO流按不同的分类方式,可以分为三种:

字节流与字符流

根据流操作的数据单位不同,可以划分为字节流与字符流。

字节流以字节(8bit)为单位读写数据,字符流以字符(如“A”)为单位读写数据。

字节流一般后缀带InputStream、OutputStream;字符流一般后缀带Reader、Writer。

输入流与输出流

根据流传输方向不同,可以划分为输入流与输出流。

传输方向是以内存为基准定义的,从文件中向内存输入数据,称为输入流,即为读;从内存中输出数据到文件,称为输出流,即为写。

输入流一般后缀带InputStream、Reader;输出流一般后缀带OutputStream、Writer。

节点流和处理流

根据流的功能不同,可以划分为节点流与处理流。

节点流又称为低级流,它只能直接连接数据源,进行数据的读写,如FileInputStream、FileOutputStream;处理流又称为高级流,它则是对低级流进行连接和封装,在低级流的基础上对数据进行处理,如BufferedInputStream、BufferedOutputStream,在使用高级流时,不会直接连接到数据源,而是连接到已存在的流上。

IO流的体系分类如下图所示:

image-20241014095458559

2.1 字节流

2.1.1 概览

在计算机中,无论是文本、图片、音频还是视频,所有文件都是以二进制(字节)形式存在的,I/O流中针对字节的输入/输出提供了一系列的流,统称为字节流。字节流是程序中最常用的流,根据数据的传输方向可将其分为字节输入流和字节输出流。在JDK中,提供了两个抽象类InputStream和OutputStream,它们是字节流的顶级父类,所有的字节输入流都继承自InputStream,所有的字节输出流都继承自OutputStream。

InputStream被看成一个输入管道,OutputStream被看成一个输出管道,数据通过InputStream从源设备输入到程序,通过OutputStream从程序输出到目标设备,从而实现数据的传输。由此可见,I/O流中的输入/输出都是相对于程序(内存)而言的。

InputStream的常用方法

方法声明 功能描述
int read() 从输入流读取一个8位的字节,把它转换为0~255之间的整数,并返回这一整数。当没有可用字节时,将返回-1
int read(byte[] b) 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,返回的整数表示读取字节的数目
int read(byte[] b,int off,int len) 从输入流读取若干字节,把它们保存到参数b指定的字节数组中,off指定字节数组开始保存数据的起始下标,len表示读取的字节数目
void close() 关闭此输入流并释放与该流关联的所有系统资源

前三个read()方法都是用来读数据的,第一个read()方法是从输入流中逐个读入字节;第二个和第三个read()方法则将若干字节以字节数组的形式一次性读入,从而提高读数据的效率。

在进行I/O流操作时,当前I/O流会占用一定的内存,由于系统资源宝贵,因此,在I/O操作结束后,应该调用close()方法关闭流,从而释放当前I/O流所占的系统资源。

OutputStream的常用方法

方法声明 功能描述
void write(int b) 向输出流写入一个字节
void write(byte[] b) 把参数b指定的字节数组的所有字节写到输出流
void write(byte[] b,int off,int len) 将指定byte数组中从偏移量off开始的len个字节写入输出流
void flush() 刷新此输出流并强制写出所有缓冲的输出字节
void close() 关闭此输出流并释放与此流相关的所有系统资源

前三个是重载的write()方法,都用于向输出流写入字节,其中,第一个方法逐个写入字节,后两个方法是将若干个字节以字节数组的形式一次性写入,从而提高写数据的效率。

flush()方法用来将当前输出流缓冲区(通常是字节数组)中的数据强制写入目标设备,此过程称为刷新。close()方法是用来关闭流并释放与当前IO流相关的系统资源。

InputStream和OutputStream这两个类虽然提供了一系列和读写数据有关的方法,但是这两个类是抽象类,不能被实例化,因此,针对不同的功能,InputStream和OutputStream提供了不同的子类,这些子类形成了一个体系结构:

InputStream的子类:

image-20241015174338437

OutputStream的子类:

image-20241015174400627

2.1.2 读取文件

每次读取一个字节(byte)

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws IOException {
// 1. 创建文件字节输入流管道,与源文件接通
InputStream inputStream = new FileInputStream("java-io-demo/src/main/resources/abc.txt");
// 2. 开始读取文件字节数据,循环读取文件字节数据
int res;
while ((res = inputStream.read()) != -1) {
System.out.println((char)res);
}
inputStream.close();
}

文件内容:

image-20241014104037250

执行结果:

image-20241014104122699

每次读取一个字节的注意点:

  1. 性能很差
  2. 读汉字会乱码
  3. 流使用后要关闭,释放资源

每次读取多个字节(byte)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws IOException {
// 1. 创建文件字节输入流管道
InputStream inputStream = new FileInputStream("java-io-demo/src/main/resources/abc.txt");
// 2. 创建缓存
byte[] buffer = new byte[3];
/*// 3. 每次读取多个字节,存入缓存
int length = inputStream.read(buffer);
String str = new String(buffer);
System.out.println("当前读取的字节数量:" + length);
System.out.println(str);

// 4. 之后读取的字节会覆盖缓存中原来的字节,但是会出现之后读取字节的长度不足以覆盖之前缓存的字节长度的情况
int length2 = inputStream.read(buffer);
// 5. 读取多少个字节就解码多少个字节
String str2 = new String(buffer, 0, length2);
System.out.println("当前读取的字节数量:" + length2);
System.out.println(str2);*/

// 3. 循环读取字节
int length;
while ((length = inputStream.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, length));
}
inputStream.close();
}

文件内容:

image-20241014104037250

执行结果:

image-20241014105147133

每次读取多个字节的注意点:

  1. 减少了读取文件的次数,提高了效率
  2. 依然不能解决读取汉字乱码的问题
  3. 适合做文件拷贝类的操作

一次读取所有字节(byte)

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws IOException {
// 1. 创建文件字节输入流管道
File file = new File("java-io-demo/src/main/resources/abc.txt");
InputStream inputStream = new FileInputStream(file);
// 2. 创建大小与文件字节长度一样的缓冲字节数组
long length = file.length();
byte[] buffer = new byte[(int) length];
// 3. 读取所有字节,并解码
int readLength = inputStream.read(buffer);
System.out.println("缓冲字节数组长度:" + buffer.length);
System.out.println("读取字节字节长度:" + readLength);
System.out.println("读取字节解码内容:" + new String(buffer));
}

文件内容:

image-20241014105855669

执行结果:

image-20241014105911828

一次读取所有字节的注意点:

  1. 避免了出现汉字乱码的情况
  2. 读取大文件的话需要创建大的缓冲字节数组

2.1.3 写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) throws IOException {
// 1. 创建一个文件字节输出流,若文件不存在会自动创建
// 覆盖型管道
// OutputStream outputStream = new FileOutputStream("java-io-demo/src/main/resources/output.txt");
// 追加型管道
OutputStream outputStream = new FileOutputStream("java-io-demo/src/main/resources/output.txt", true);

// 2. 写入字节
// 每次写入一个字节
outputStream.write(97); // 表示'a'的字节码
outputStream.write('b');
outputStream.write('海'); // 只写入一个字节,但UTF-8的汉字有3个字节
// 每次写入多个字节
String str = "黑神话:悟空 Black Myth: Wukong";
byte[] bytes = str.getBytes();
outputStream.write(bytes); // 全部写入
outputStream.write(bytes, 0, 18); // 部分写入
outputStream.write("\r\n".getBytes()); // 换行符
outputStream.close();
}

执行结果:

image-20241014111619789

OutputStream写入文件的注意点:

  1. 文件写入分为覆盖型与追加型,前者会覆盖掉文件的原始内容,后者在原始内容的后面追加写入。
  2. 文件在写入时以字节为单位,可能会出现汉字乱码问题,需要注意。

2.1.4 拷贝文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void main(String[] args) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
// 文件字节输入流
inputStream = new FileInputStream("java-io-demo/src/main/resources/pic.jpg");
// 文件字节输出流
outputStream = new FileOutputStream("java-io-demo/src/main/resources/pic-copy.jpg");
// 缓冲字节数组 1kb
byte[] buffer = new byte[1024];
// 输入流读取的字节长度
int len;
while ((len = inputStream.read(buffer)) != -1) {
// 输出流写入缓冲字节数组中的数据
outputStream.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭输入输出流
try {
if (null != inputStream) {
inputStream.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
if (null != outputStream) {
outputStream.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

拷贝文件注意点:

  1. 使用finally关闭输入输出流

2.1.5 关闭资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
try (// 文件字节输入流
InputStream inputStream = new FileInputStream("java-io-demo/src/main/resources/pic.jpg");
// 文件字节输出流
OutputStream outputStream = new FileOutputStream("java-io-demo/src/main/resources/pic-copy.jpg")
) {
// 缓冲字节数组 1kb
byte[] buffer = new byte[1024];
// 输入流读取的字节长度
int len;
while ((len = inputStream.read(buffer)) != -1) {
// 输出流写入缓冲字节数组中的数据
outputStream.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}

在Java 7中引入了try-with-resources的异常处理机制,便于关闭在try-catch中声明的资源。

在此之前,使用try-catch-finally关闭资源十分繁琐,不方便。

使用try-with-resources,需要在try后紧跟括号,在括号里声明使用的资源,若是单行则不用分号,若是多行,则需要以分号结尾,最后使用的资源会被自动关闭。

需要注意的是,使用try-with-resources的前提条件,是资源必须实现了 java.lang.AutoCloseable 接口(它包含了实现java.io.Closeable 的所有对象)。

2.1.6 字节缓冲流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
try (
InputStream fileInputStream = new FileInputStream("java-io-demo/src/main/resources/abc.txt");
OutputStream fileOutputStream = new FileOutputStream("java-io-demo/src/main/resources/abc.txt", true);
InputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
OutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
){
byte[] buffer = new byte[1024];
int len;
while ((len = bufferedInputStream.read(buffer)) != -1) {
bufferedOutputStream.write(buffer, 0, len);
}
System.out.println("复制完成");
} catch (IOException e) {
e.printStackTrace();
}
}

代码中创建了BufferedInputStream和BufferedOutputStream两个缓冲流对象,这两个流内部都定义了一个大小为8192的字节数组,当调用read()或者write()方法读写数据时,首先将读写的数据存入到定义好的字节数组,然后将字节数组的数据一次性读写到文件中,这种方式与前面小节中讲解的字节流的缓冲区类似,都对数据进行了缓冲,降低了IO频次,从而有效的提高了数据的读写效率。

2.2 字符流

2.2.1 概览

同字节流一样,字符流也有两个抽象的顶级父类,分别是Reader和Writer。其中Reader是字符输入流,用于从某个源设备读取字符。Writer是字符输出流,用于向某个目标设备写入字符。Reader和Writer作为字符流的顶级父类,也有许多子类,接下来通过继承关系图来列出Reader和Writer的一些常用子类。

字符流的常用方法与字节流相似。

Reader的子类:

image-20241015175430240

Writer的子类:

image-20241015175451251

2.2.2 读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
try (Reader reader = new FileReader("java-io-demo/src/main/resources/abc.txt")){
// 1. 读取单个字符
/*int c;
// read方法返回单个字符的字节码
while ((c = reader.read()) != -1) {
System.out.println((char) c);
}*/

// 2. 读取多个字符
char[] buffer = new char[3];
int len;
// read方法返回读取的字符个数
while ((len = reader.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

读取文件注意点:

  1. 读取单个字符返回的是字符的Unicode编码,读取多个字符返回的是字符个数。

2.2.3 写入文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void main(String[] args) {
// 创建文件字符输出流
// Writer writer = new FileWriter("java-io-demo/src/main/resources/abc-copt.txt") // 覆盖型写入
try (Writer writer = new FileWriter("java-io-demo/src/main/resources/abc-copt.txt", true)) {
// 1. 写一个字符
writer.write(97);
writer.write('b');
writer.write('海');
writer.write("\r\n");
// 2. 写一个字符串
String str = "要做神仙,驾鹤飞天。点石成金,妙不可言。";
writer.write(str);
writer.write("\r\n");
// 3. 写字符串的一部分
writer.write(str, 0, 10);
writer.write("\r\n");
// 4. 写一个字符数组
char[] buffer = {'春', '眠', '不', '觉', '晓'};
writer.write(buffer);
writer.write("\r\n");
// 5. 写字符数组的一部分
writer.write(buffer, 0, 3);
writer.write("\r\n");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

2.2.4 字符转换流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
try (
// 字节输入流 -> 字符输入转换流 -> 字符输入缓冲流
InputStream inputStream = new FileInputStream("java-io-demo/src/main/resources/abc-gbk.txt");
Reader inputStreamReader = new InputStreamReader(inputStream, "GBK");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 字节输出流 -> 字符输出转换流 -> 字符输出缓冲流
OutputStream outputStream = new FileOutputStream("java-io-demo/src/main/resources/abc-gbk.txt", true);
Writer outputStreamWriter = new OutputStreamWriter(outputStream, "GBK");
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
) {
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
bufferedWriter.newLine();
bufferedWriter.write(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}

转换流也是一种处理流,它提供了字节流和字符流之间的转换。在Java IO流中提供了两个转换流:InputStreamReader 和 OutputStreamWriter,这两个类都属于字符流。其中InputStreamReader将字节输入流转为字符输入流,继承自Reader。OutputStreamWriter是将字符输出流转为字节输出流,继承自Writer。

转换流的原理是:字符流 = 字节流 + 编码表,是字符流和字节流之间的桥梁。使用转换流可以使用指定编码进行文件的读写,可以有效避免乱码的问题。