Java编程笔记17:I/O
I/O代表着输入(Input)和输出(Output),具体指从外部读取数据到内存中或者从内存中输出数据到外部。这里的“外部”可以是硬盘、磁带等本地存储介质,也可以是网络连接的远程设备。
输入和输出是以内存为中心而言的一个相对概念。毕竟对于一个最简单的计算机结构而言,内存和CPU是不可或缺的,我们的程序就是运行在内存中的,而其它的东西都是非必要的。所以虽然硬盘是存在于电脑内部的,但对于内存而言,依然是一个“外部存储器”。所以从硬盘读取数据到内存这个过程就称作输入(Input),而从内存读取数据到硬盘就称作输出(Output)。
Java中的I/O相关的类相当复杂,这和Java的历史包袱有关,也和其开发团队的设计有关。在介绍相关的类之前,我们先了解一个与文件直接相关的类File
。
File类
Java中的File
类可以用于读取目录信息:
package ch17.file;
import java.io.File;
public class Main {
public static void main(String[] args) {
File file = new File(".");
String[] paths = file.list();
for (String path : paths) {
System.out.println(path);
}
}
}
// .git
// .gitignore
// .vscode
// ch0
// exception.log
// exp.log
// LICENSE
// README.md
// test.txt
// xyz
实际上
File
类可以用于表示一个目录或文件,准确的说,其表示的是一个操作系统文件系统路径,FilePath
是一个更合理的名称。
list
方法可以接受一个FilenameFilter
接口,用以按照目录或文件名进行筛选,并输出结果:
package ch17.file2;
import java.io.File;
import java.io.FilenameFilter;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
File file = new File(".");
final String regex = ".*\\.log";
String[] paths = file.list(new FilenameFilter() {
private Pattern pattern = Pattern.compile(regex);
@Override
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
});
for (String path : paths) {
System.out.println(path);
}
}
}
// exception.log
// exp.log
这里使用正则表达式对子目录/文件进行筛选,正则表达式在Java编程笔记11:字符串 - 魔芋红茶’s blog (icexmoon.cn)中介绍过。
list
返回的子目录/文件信息是以字符串数组形式组成的,这种内容并不容易再次利用,我们往往需要获取到File
对象,以进一步处理。因此,File
类的listFiles
方法更有用,它可以返回由File
对象组成的数组。
package ch17.file3;
import java.io.File;
import java.io.FilenameFilter;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
File file = new File(".");
final String regex = ".*\\.log";
File[] paths = file.listFiles(new FilenameFilter() {
private Pattern pattern = Pattern.compile(regex);
@Override
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
});
for (File path : paths) {
System.out.println(path);
}
}
}
// .\exception.log
// .\exp.log
事实上,可以使用File
获取更多的信息:
...
public class Main {
public static void main(String[] args) {
...
for (File path : paths) {
System.out.println("================================");
printFileInfo(path);
}
}
private static void printFileInfo(File file) {
Fmt.printf("name:%s\n", file.getName());
boolean isFile = file.isFile();
String type = isFile ? "file" : "dir";
Fmt.printf("type:%s\n", type);
Fmt.printf("abs path:%s\n", file.getAbsolutePath());
try {
Fmt.printf("canonical path:%s\n", file.getCanonicalPath());
} catch (IOException e) {
throw new RuntimeException(e);
}
Fmt.printf("parent:%s\n", file.getParentFile().getAbsolutePath());
long lastModified = file.lastModified();
Fmt.printf("last modified:%s\n", date2Str(lastModified));
boolean canRead = file.canRead();
Fmt.printf("can read:%s\n", canRead);
}
private static String date2Str(long date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return sdf.format(new Date(date));
}
}
// ================================
// name:exception.log
// type:file
// abs path:D:\workspace\java\java-notebook\.\exception.log
// canonical path:D:\workspace\java\java-notebook\exception.log
// parent:D:\workspace\java\java-notebook\.
// last modified:2022-01-24 03:55:55
// can read:true
// ================================
// name:exp.log
// type:file
// abs path:D:\workspace\java\java-notebook\.\exp.log
// canonical path:D:\workspace\java\java-notebook\exp.log
// parent:D:\workspace\java\java-notebook\.
// last modified:2022-01-24 04:30:34
// can read:true
这个示例说明了可以通过File
获取文件或目录的大部分信息,包含:名称、种类、绝对路径、规范路径、父目录、最后修改时间、RWX权限等。
目录遍历
遍历目录是一个常见操作,这里编写了一个可以遍历目录,并生成目录结构的类:
package ch17.directory;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
class Directory implements Iterable<File> {
private List<File> files = new LinkedList<>();
private List<Directory> dirs = new LinkedList<>();
private File root;
private int deep = 0;
private String regex;
private static String DEFAULT_REGEX = ".*";
public Directory(String path) {
this(path, DEFAULT_REGEX);
}
public Directory(File path) {
this(path, DEFAULT_REGEX);
}
public Directory(String path, String regex) {
root = new File(path);
this.regex = regex;
init();
}
public Directory(File path, String regex) {
root = path;
this.regex = regex;
init();
}
private void init() {
FilenameFilter ff = new FilenameFilter() {
Pattern p = Pattern.compile(regex);
@Override
public boolean accept(File dir, String name) {
String currentFileStr = dir.getAbsolutePath() + "\\" + name;
File currentFile = new File(currentFileStr);
if (currentFile.isDirectory()) {
return true;
}
return p.matcher(name).matches();
}
};
for (File file : root.listFiles(ff)) {
if (file.isFile()) {
files.add(file);
} else {
Directory subDir = new Directory(file, regex);
subDir.deep = deep + 1;
dirs.add(subDir);
}
}
}
private void addPrefix(StringBuffer sb) {
for (int i = 0; i < deep; i++) {
sb.append("\t");
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
addPrefix(sb);
sb.append(root.getName());
sb.append("\n");
for (File file : files) {
addPrefix(sb);
sb.append("|-------");
sb.append(file.getName());
sb.append("\n");
}
for (Directory dir : dirs) {
sb.append(dir.toString());
}
return sb.toString();
}
@Override
public Iterator<File> iterator() {
return new Iterator<File>() {
private Iterator<File> filesIterator = files.iterator();
private int dirIndex = 0;
@Override
public boolean hasNext() {
if (filesIterator.hasNext()) {
return true;
} else {
if (dirIndex >= dirs.size()) {
return false;
}
filesIterator = dirs.get(dirIndex++).iterator();
return hasNext();
}
}
@Override
public File next() {
File file;
try {
file = filesIterator.next();
} catch (NoSuchElementException e) {
if (dirIndex >= dirs.size()) {
return null;
}
filesIterator = dirs.get(dirIndex++).iterator();
return next();
}
return file;
}
};
}
}
public class Main {
public static void main(String[] args) {
Directory dir = new Directory("D:\\workspace\\java\\java-notebook\\ch0", ".*\\.java");
System.out.println(dir);
for (File file : dir) {
System.out.println(file);
}
}
}
实用工具
除了遍历目录以外,使用File
类还可以完成对文件的删除、重命名等,下面是我用此类功能编写的一个对当前目录下文件进行管理的小工具:
package ch17.file_opt;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.regex.Pattern;
import util.Fmt;
public class Main {
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("File operation help:");
System.out.println("command options [files]");
System.out.println("options:");
System.out.println("-l [regex] print current directorie's files.");
System.out.println("-d file1 [file2 file3 ...] delete files.");
System.out.println("-r old_name new_name renmae file.");
} else if (args.length == 1 && args[0].equals("-l")) {
printFiles(".*");
} else if (args.length == 2 && args[0].equals("-l")) {
printFiles(args[1]);
} else if (args.length >= 2 && args[0].equals("-d")) {
String[] fileNames = Arrays.copyOfRange(args, 1, args.length);
removeFile(fileNames);
} else if (args.length == 3 && args[0].equals("-r")) {
rename(args[1], args[2]);
}
}
private static void printFiles(final String regex) {
File file = new File(".");
int counter = 0;
FilenameFilter ff = new FilenameFilter() {
Pattern pattern = Pattern.compile(regex);
@Override
public boolean accept(File dir, String name) {
if (pattern.matcher(name).matches()) {
return true;
}
return false;
}
};
for (File subFile : file.listFiles(ff)) {
if (subFile.isFile()) {
System.out.println(subFile.getName());
counter++;
}
}
Fmt.printf("total %d files.\n", counter);
}
private static void removeFile(String... fileNames) {
int success = 0;
int fail = 0;
for (String fileName : fileNames) {
File file = new File(fileName);
if (file.delete()) {
Fmt.printf("The file %s is deleted.", fileName);
success++;
} else {
fail++;
}
}
Fmt.printf("delete operation is done, %d success, %d fail.", success, fail);
}
private static void rename(String oldName, String newName) {
File file = new File(oldName);
if(file.renameTo(new File(newName))){
Fmt.printf("The file %s is renamed to %s.", oldName, newName);
}
else{
System.out.println("rename operation is failed.");
}
}
}
可以在命令行下执行该程序,不带参数时会输出帮助信息:
❯ java -cp D:\workspace\java\java-notebook\xyz\icexmoon\java_notes ch17.file_opt.Main
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
File operation help:
command options [files]
options:
-l [regex] print current directorie's files.
-d file1 [file2 file3 ...] delete files.
-r old_name new_name renmae file.
-l
参数可以打印出当前目录下的文件名,并可以指定一个正则表达式进行筛选:
❯ java -cp D:\workspace\java\java-notebook\xyz\icexmoon\java_notes ch17.file_opt.Main -l .*\.mp3
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
0. 22秒.mp3
0. 海之彼岸.mp3
moves-like-jagger.mp3
sugar.mp3
乡恋(央视2013中秋晚会)-李谷一.mp3
云水禅心(古筝独奏)-王珣.mp3
云水禅心-玉琳琅.mp3
国色天香加长版7737b7ba8a8.mp3
当爱离别时.mp3
桥边姑娘-小倩.mp3
桥边姑娘.mp3
梨花颂.mp3
梨花飞情人泪2.mp3
total 13 files.
-d
参数可以删除指定文件:
❯ java -cp D:\workspace\java\java-notebook\xyz\icexmoon\java_notes ch17.file_opt.Main -d 乡恋.mp3
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
The file 乡恋.mp3 is deleted.delete operation is done, 1 success, 0 fail.
-r
参数可以重命名文件:
❯ java -cp D:\workspace\java\java-notebook\xyz\icexmoon\java_notes ch17.file_opt.Main -r 梨花飞情人泪2.mp3 梨花飞情人泪.mp3
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
The file 梨花飞情人泪2.mp3 is renamed to 梨花飞情人泪.mp3.
这个工具可以看做是Linux系统上rm
和ls
等工具的集合。
rm
、ls
等shell
命令可以通过管道命令结合更多命令使用,所以在实用性上要强得多。
IO流
Java将输入和输出的数据当做“流”看待,也就是一个可以持续产生数据的东西,统称为IO流。
IO流具体被设计为标准类库java.io
中的相关类簇,可以用下面的思维导图表示:
图源:Java IO流详解 - Fuu - 博客园 (cnblogs.com)
IO流主要分为字节流和字符流,这实际上这是历史原因产生的。
我们知道计算机系统中的所有数据其实本质上都是以字节方式存储的,换句话说Java最初实现的字节流相关类库就可以解决所有的输入输出问题,但如果你用字节流的方式来处理文本文档,就需要你