Java I/O深入学习之File和RandomAccessFile

时间:2021-12-14 05:20:50

前言

I/O系统即输入/输出系统,对于一门程序语言来说,创建一个好的输入/输出系统并非易事。因为不仅存在各种I/O源端和想要与之通信的接收端(文件、控制台、网络链接等),而且还需要支持多种不同方式的通信(顺序、随机存取、缓冲、二进制、按字符、按行、按字等)。

Java类库的设计者通过创建大量的类来解决这个难题,比如面向字节的类(字节流,InputStream、OutputStream)、面向字符和基于Unicode的类(字节流,Reader、Writer)、nio类(新I/O,为了改进性能及功能)等。所以,在充分理解Java I/O系统以便正确地运用之前,我们需要学习相当数量的类。因此一开始可能会对Java I/O系统提供的如此多的类感到迷惑,不过在我们系统地梳理完整个Java I/O系统并将这部分知识与融入到自我的整个知识体系中后,我们就能很快消除这种迷惑。

在I/O这个专题里面,我会总结Java 中涉及到的大多数I/O相关类的用法,从传统I/O诸如:File、字节流、字符流、序列化到新I/O:nio。在本节中我会先总结File和RandomAccessFile的相关知识,按照如下顺序:

1. File

1.1 File简介常用方法

根据官方文档的解释,Java中的File类是文件和目录路径的抽象,用户通过File直接执行与文件或目录相关的操作。我的理解就是File类的作用是用来指代文件或者目录的,通过File的抽象我们可以很方便的操作文件或目录,无需关心操作系统的差异。官方文档是这样描述的:

An abstract representation of file and directory pathnames.

User interfaces and operating systems use system-dependent pathname strings to name files and directories. This class presents an abstract, system-independent view of hierarchical pathnames.

用户接口和操作系统通过系统相关的路径名来命名文件和目录。而File类提供了一个抽象地、系统无关的视角来描述分层次路径名。File代表抽象路径名,有两个部分组成:  

  • 一个可选的系统相关的前缀,比如磁盘驱动器说明符(disk-drive specifier),unix系统中是“/”而windows系统中则是“\”;
  • 0或多个字符串名称组成的序列;

关于File的用法,我觉得直接通过示例来学习会比较高效:  

public class FileDemo { 
public static void main(String[] args) throws IOException {
File dir = new File("f:/dirDemo");
System.out.println("dir exists: " + dir.exists());
dir.mkdirs();
System.out.println("dir exists: " + dir.exists());
if(dir.isFile()) {
System.out.println("dir is a file.");
}else if(dir.isDirectory()) {
System.out.println("dir is a directory");
}
File file = new File("f:/dirDemo/fileDemo"); 
System.out.println(
"\n Absolute path: " + file.getAbsolutePath() +
"\n Can read: " + file.canRead() + 
"\n Can write: " + file.canWrite() +
"\n getName: " + file.getName() +
"\n getParent: " + file.getParent() +
"\n getPath: " + file.getPath() +
"\n length: " + file.length() +
"\n lastModified: " + file.lastModified() +
"\n isExist: " + file.exists());
file.createNewFile();
System.out.println("is file exist: " + file.exists());
if(file.isFile()) {
System.out.println("file is a file.");
}else if(file.isDirectory()) {
System.out.println("file is a directory");
}
System.out.println();
for(String filename : dir.list()) {
System.out.println(filename);
}
} 
}

输出结果:

dir exists: false
dir exists: true
dir is a directory
Absolute path: f:\dirDemo\fileDemo
Can read: false
Can write: false
getName: fileDemo
getParent: f:\dirDemo
getPath: f:\dirDemo\fileDemo
length: 0
lastModified: 0
isExist: false
is file exist: true
file is a file.
fileDemo

在这个简单demo中我们用到多种不同的文件特征查询方法来显示文件或目录路径的信息:

  • getAbsolutePath(),获取文件或目录的绝对路径;
  • canRead()、canWrite(),文件是否可读/可写;
  • getName(),获取文件名;
  • getParent(),获取父一级的目录路径名;
  • getPath(),获取文件路径名;
  • length(),文件长度;
  • lastModified(),文件最后修改时间,返回时间戳;
  • exists(),文件是否存在;
  • isFile(),是否是文件;
  • isDirectory(),是否是目录;
  • mkdirs(),创建目录,会把不存在的目录一并创建出来;
  • createNewFile(),创建文件;
  • list(),可以返回目录下的所有File名,以字符数组的形式返回;

exists()方法可以返回一个File实例是否存在,这里的存在是指是否在磁盘上存在,而不是指File实例存在于虚拟机堆内存中。一个File类的实例可能表示一个实际的文件系统如文件或目录,也可能没有实际意义,仅仅只是一个File类,并没有关联实际文件,如果没有则exists()返回false。

1.2 File过滤器

list()方法返回的数组中包含此File下的所有文件名,如果想要获得一个指定的列表,比如,希望得到所有扩展名为.java的文件,可以使用“目录过滤器”(实现了FilenameFilter接口),在这个类里面可以指定怎样显示符合条件的File对象。我们把一个自己实现的FilenameFilter传入list(FilenameFilter filter)方法中,在这个被当做参数的FilenameFilter中重写其accept()方法,指定我们自己想要的逻辑即可,这其实是策略模式的体现。

比如我们只要获取当前项目跟目录下的xml文件:

public class XmlList { 
public static void main(final String[] args) {
File file = new File(".");
String list;
list = file.list(new FilenameFilter(){
@Override
public boolean accept(File dir, String name) {
Pattern pattern = Pattern.compile("(.*)\\.xml");
return pattern.matcher(name).matches();
}
});
Arrays.sort(list,String.CASE_INSENSITIVE_ORDER);
for(String dirItem : list)
System.out.println(dirItem);
}
}

在这个例子中,我们用匿名内部类的方式给list()传参,accept()方法内部我们指定了正则过滤策略,在调用File的list()方法时会自动为此目录对象下的每个文件名调用accept()方法,来判断是否要将该文件包含在内,判断结果由accept()返回的布尔值来表示。

如上也只是罗列了一些个人认为File类较常用的方法,也只是一部分,若需要更详细信息请参考官方文档。

1.3 目录工具

接下来我们来看一个实用工具,可以获得指定目录下的所有或者符合要求的File集合:

public class Directory {
// local方法可以获得指定目录下指定文件的集合
public static File[] local(File dir,String regex) {
return dir.listFiles(new FilenameFilter() {
private Pattern pattern = Pattern.compile(regex);
@Override
public boolean accept(File dir, String name) {
return pattern.matcher(new File(name).getName()).matches();
}
});
}
public static File[] local(String dir,String regex) {
return local(new File(dir),regex);
}
// walk()方法可以获得指定目录下所有符合要求的文件或目录,包括子目录下
public static TreeInfo walk(String start,String regex) {
return recurseDirs(new File(start),regex);
}
public static TreeInfo walk(File start,String regex) {
return recurseDirs(start,regex);
}
public static TreeInfo walk(String start) {
return recurseDirs(new File(start),".*");
}
public static TreeInfo walk(File start) {
return recurseDirs(start,".*");
}
static TreeInfo recurseDirs(File startDir,String regex) {
TreeInfo treeInfo = new TreeInfo();
for(File item : startDir.listFiles()) {
if(item.isDirectory()) {
treeInfo.dirs.add(item);
treeInfo.addAll(recurseDirs(item,regex));
}else {
if(item.getName().matches(regex))
treeInfo.files.add(item);
}
}
return treeInfo;
}
public static class TreeInfo implements Iterable<File>{

public List<File> files = new ArrayList();
public List<File> dirs = new ArrayList();
@Override
public Iterator<File> iterator() {
return files.iterator();
}
void addAll(TreeInfo other) {
files.addAll(other.files);
dirs.addAll(other.dirs);
}
}
}

通过工具中的local()方法,我们可以获得指定目录下符合要求文件的集合,通过walk()方法可以获得指定目录下所有符合要求的文件或目录,包括其子目录下的文件,这个工具只是记录在这里以备不时之需。

2. RandomAccessFile

因为File类知识文件的抽象表示,并没有指定信息怎样从文件读取或向文件存储,而向文件读取或存储信息主要有两种方式:

  • 通过输入输出流,即InputStream、OutputStream;
  • 通过RandomAccessFile;

输入输出流的方式我们后面会专门总结,这也是Java I/O系统中很大的一块,本文会讲一下RandomAccessFile,因为它比较独立,和流的相关性不大。

RandomAccessFile是一个完全独立的类,其拥有和我们后面将总结的IO类型有本质不同的行为,可以在一个文件内向前和向后移动。我们来看一下其主要方法:

  • void write(int d) 向文件中写入1个字节,写入的是传入的int值对应二进制的低8位;
  • int read() 读取1个字节,并以int形式返回,如果返回-1则代表已到文件末尾;
  • int read(byte[] data) 一次性从文件中读取字节数组总长度的字节量,并存入到该字节数组中,返回的int值代表读入的总字节数,如果返回-1则代表未读取到任何数据。通常字节数组的长度可以指定为1024*10(大概10Kb的样子,效率比较好);
  • int read(byte[] data, int off, int len) 一次性从文件中读取最多len个字节,并存入到data数组中,从下标off处开始;
  • void write(int b) 往文件中写入1个字节的内容,所写的内容为传入的int值对应二进制的低8位;
  • write(byte b[]) 往文件中写入一个字节数组的内容;
  • write(byte b[], int off, int len) 往文件中写入从数组b的下标off开始len个字节的内容;
  • seek(long pos) 设置文件指针偏移量为指定值,即在文件内移动至新的位置;
  • long getFilePointer() 获取文件指针的当前位置;
  • void close() 关闭RandomAccessFile;

上面只是一部分方法,更多请参考官方文档。我们再来看一个简单demo学习一下:

public class RandomAccessFileDemo { 
public static void main(String[] args) {
File file = new File("./test.txt");
if(!file.exists()) {
try {
file.createNewFile();
} catch (IOException e1) {
e1.printStackTrace();
}
}
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile("./test.txt","rw");
raf.write(1000);
raf.seek(0);
System.out.println(raf.read());
raf.seek(0); 
System.out.println(raf.readInt());
} catch (FileNotFoundException e) {
System.out.println("file not found");
} catch (EOFException e) {
System.out.println("reachs end before read enough bytes");
e.printStackTrace();
} catch(IOException e) {
e.printStackTrace();
}finally {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

输出结果:

232
reachs end before read enough bytes

在RandomAccessFile的构造器中有两个参数,第一个是文件路径或者File,代表该RandomAccessFile要操作的文件,第二个是读写模式。如果操作的文件不存在,在模式为“rw”时会直接创建文件,如果是“r”则会抛出异常。

这是一个简单的例子,首先创建文件test.txt,然后创建一个和该文件关联的RandomAccessFile,指定读写模式为读写,调用write()写入1000,这里只会写入一个字节,跳到文件头部,读取1个字节,输出232(正好是1000对应二进制的低8位),再跳到文件头部,调用readInt()读取1个整数,这时候因为文件中只有1个字节,所以抛出EOFException异常,最后关闭RandomAccessFile。

如上例子我们学习了RandomAccessFile的基本用法,这里有一点需要注意,RandomAccessFile是基于文件指针从当前位置来读写的,并且写入操作是直接将插入点后面的内容覆盖而不是插入。如果我们想实现插入操作,则需要将插入点后面的内容先保存下来,再写入要插入的内容,最后将保存的内容添加进来,看下面的例子:

public class RandomAccessFileDemo {
public static void main(String[] args) throws IOException {
File file = new File("f:/test.txt");
file.createNewFile();
// 创建临时空文件用于缓冲,并指定在虚拟机停止时将其删除
File temp = File.createTempFile("temp", null);
temp.deleteOnExit();
RandomAccessFile raf = null;
try {
// 首先往文件中写入下面的诗句,并读取出来在控制台打印
raf = new RandomAccessFile(file,"rw");
raf.write("明月几时有,把酒问青天".getBytes());
raf.seek(0); 
byte[] b = new byte[60];
raf.read(b, 0, 30);
System.out.println(new String(b));
// 接下来在诗句中间再插入一句诗
raf.seek(12);
FileOutputStream fos = new FileOutputStream(temp);
FileInputStream fis = new FileInputStream(temp);
byte[] buffer = new byte[10];
int num = 0;
while(-1 != (num = raf.read(buffer))) {
fos.write(buffer, 0, num);
}
raf.seek(12);
raf.write("但愿人长久,千里共婵娟。".getBytes());
// 插入完成后将缓冲的后半部分内容添加进来
while(-1 != (num = fis.read(buffer))) {
raf.write(buffer, 0, num);
}
raf.seek(0);
raf.read(b, 0, 60);
System.out.println(new String(b));
System.out.println();
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
raf.close();
}
}
}

输出结果,插入诗句成功:

明月几时有,把酒问青天
明月几时有,但愿人长久,千里共婵娟。把酒问青天

3. 总结

本文是Java I/O系统系列第一篇,主要总结了File和RandomAccessFile的一些知识。

File类是对文件和目录路径的抽象,用户通过File来直接执行与文件或目录相关的操作,无需关心操作系统的差异。

RandomAccessFile类可以写入和读取文件,其最大的特点就是可以在任意位置读取文件(random access的意思),是通过文件指针实现的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。