疯狂的小鸡

IO-文件系统

字数统计: 5.2k阅读时长: 20 min
2018/10/12 Share

从java 7以来,引入了Files类和Path接口。他们两封装了用户对文件的所有可能的操作,相比于java 1的File类来说,使用起来方便很多。但是其实一些本质的操作还是很类似的。主要需要知道的是,Path表示路径可以使文件的路径也可以是目录的路径,Files中所有成员都是静态方法,通过路径实现了对文件的基本操作。

File类

File是文件和目录路径名的抽象表示形式,代表文件或者文件夹。

构造方法

1
2
3
4
5
6
7
8
// 根据parent抽象路径名和child路径名字符串创建一个新File实例
File(File parent, String child)
// 通过将给定路径名字符串转换为抽象路径名来创建一个新File实例
File(String pathname)
// 根据parent路径名字符串和child路径名字符串创建一个新File实例
File(String parent, String child)
// 通过将给定的file:URI转换为一个抽象路径名来创建一个新的File实例
File(URI uri)

创建功能

1
2
3
4
5
6
7
8
9
10
// 创建此抽象路径名指定的目录
boolean mkdir()
// 创建此抽象路径名指定的目录,包括所有必需但不存在的父目录
boolean mkdirs()
// 当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件
boolean createNewFile()
// 在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称
static File createTempFile(String prefix, String suffix)
// 在指定目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称
static File createTempFile(String prefix, String suffix, File directory)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 构造方法一
File file1 = new File("f://file1");
// 构造方法二
File file2 = new File("f://file1", "file2");
// 构造方法三
File file3 = new File(file2, "file3.txt");
// 创建目录并返回是否创建成功,如果目录存在则返回false
boolean b1 = file1.mkdir();
System.out.println(b1);// true
// 创建目录
boolean b2 = file2.mkdir();
System.out.println(b2);// true
// 创建文件
// 在F盘下创建/file1/file2/file3.txt文件
boolean b3 = file3.createNewFile();
System.out.println(b3);// true
// 创建空文件并指定前缀和后缀
// 在F盘下创建/file1/file2/file4.....exe文件
File.createTempFile("file4", ".exe", file2);

结果:
结果

删除功能

1
2
// 删除此抽象路径名表示的文件或目录
boolean delete()

删除操作时,删除的是目录,则必须保证是空目录。

3)判断功能

1
2
3
4
5
6
7
8
9
10
11
12
// 测试此抽象路径名表示的文件或目录是否存在
boolean exists()
// 测试此抽象路径名表示的文件是否是一个目录
boolean isDirectory()
// 测试此抽象路径名表示的文件是否是一个标准文件
boolean isFile()
// 测试此抽象路径名指定的文件是否是一个隐藏文件
boolean isHidden()
// 测试应用程序是否可以读取此抽象路径名表示的文件
boolean canRead()
// 测试应用程序是否可以修改此抽象路径名表示的文件
boolean canWrite()

4) 获取功能

基本获取功能:

1
2
3
4
5
6
7
8
9
10
// 返回由此抽象路径名表示的文件或目录的名称
String getName()
// 返回此抽象路径名的绝对路径名形式
File getAbsoluteFile()
// 返回此抽象路径名的绝对路径名字符串
String getAbsolutePath()
// 将此抽象路径名转换为一个路径名字符串
String getPath()
// 返回此抽象路径名表示的文件最后一次被修改的时间
long lastModified()

迭代获取功能:

1
2
3
4
5
6
7
8
9
10
// 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录
String[] list()
// 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录
String[] list(FilenameFilter filter)
// 返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件
File[] listFiles()
// 返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录
File[] listFiles(FileFilter filter)
// 返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录
File[] listFiles(FilenameFilter filter)

RandomAceessFile

RandomAccessFile是Java中输入,输出流体系中功能最丰富的文件内容访问类,它提供很多方法来操作文件,包括读写支持,与普通的IO流相比,它最大的特别之处就是支持任意访问的方式,程序可以直接跳到任意地方来读写数据。

RandomAccessFile以字节方式读写文件,众所周知,计算机以二进制形式存储文件(包括视频,音频,文字等等),RandomAccessFile是以低八位一个字节读写,更准确的操作二进制文件,可以这么说,运用RandomAccessFile文件就可以任意的读取二进制文件了。

构造方法

RandomAccessFile在文件随机访问操作时有两种模式,分别是只读模式和读写模式两种。

1
2
3
4
5
6
7
8
9
//第一个参数是需要访问的文件,第二个参数是访问模式。
/* 访问模式
*  r 代表以只读方式打开指定文件 
*   rw 以读写方式打开指定文件 
*   rws 读写方式打开,并对内容或元数据都同步写入底层存储设备 
*   rwd 读写方式打开,对文件内容的更新同步更新至底层存储设备
*/
RandomAccessFile(File file ,String mode)
RandomAccessFile(String filename, String mode)

在只读模式下,若要访问的文件不存在则会抛出FileNotFoundException异常。

在读写模式下,若该文件不存在,写入数据时则会自动创建文件,若创建不成功,则抛出异常。

读方法

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
/* read()方法,该方法读取一个byte(8位)填充到int
 * 的低八位,高24位为0,返回值范围在0~255,如果返回-1表示读到了文 
 * 件末尾。
 */
    @Test
    public void testRead() throws IOException{
        RandomAccessFile raf = new RandomAccessFile("test.txt","rw");               
int d = -1;
        while((d=raf.read())!=-1){
            System.out.println(d);
        }
    }

/* read(byte[] b)可以从文件中批量读取字节的方法
* 该方法会从文件中尝试最多读取给定数组的总长度的字节量,
* 并从给定的字节数组第一个位置开始,将读取到的字节顺序存放至数组中,
* 返回值为实际读取到的字节量
*/
@Test
public void testReadByByteArray() throws IOException{
    RandomAccessFile raf = new RandomAccessFile("test.txt","rw");
    RandomAccessFile raf2 = new RandomAccessFile("test_copy.txt","rw");
    byte[] b = new byte[1024*10];
    int len = -1;
    while((len=raf2.read(b))!=-1){
        raf.write(b,0,len);
   }
}

read(byte[] b)批量读取字节,参数是字节数组,即批量读取字节的数据,通俗讲就是该方法读取这么多字节数组所能装下的数据。字节数组的长度一般创建为1024*10,即为10K,这样JVM能够以最佳性能读取文件内容。

RandomAccessFile写方法

1
2
3
4
5
6
7
8
/* write(int d)方法,该方法只写参数int的“低8位”,
 * 其实只写int型参数的一个字节
 */
@Test
public void testWrite() throws IOException{
    RandomAccessFile raf = new RandomAccessFile("test.txt","rw");
    raf.write(1);   
}

write(int d)方法只能读取参数int的“低8位”,超过八位的字节无法读取,如写入256,其实只写入了0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* write(byte[] d,int offset,int len)
* 该方法会根据当前指针所在位置处连续写出改定数组中的部分字节,
* 这个部分是从数组的offset处开始,连续读len个字节
*/
@Test
public void testWriteByByteArray() throws IOException{
    RandomAccessFile raf = new RandomAccessFile("test.txt","rw");
    RandomAccessFile raf2 = new RandomAccessFile("test_copy.txt","rw");
    byte[] b = new byte[1024*10];
    int len = -1;
    while((len=raf.read(b))!=-1){
        raf2.write(b,0,len);
    }
}

该方法通俗讲,写入字节数组所装的数据,但是就上面的例子而言,如果读取的数据达到10K了,自然全部写入,如果读的数据不足10K,就4K,那么就要确定一下写入的数据长度了,于是write(byte[] b)有了一个重载方法write(byte[] b,int offset ,int len)。

RandomAccessFile的文件指针操作的方法

RandomAccessFile是基于指针操作文件的读写的,操作指针的位置,就可以任意的读写文件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* long getFilePointer()
* 该方法获取当前RandomAccessFile的指针位置
*/
@Test
public void testGetFilePointer()  throws IOException {
    RandomAccessFile raf = new RandomAccessFile("test.txt","rw");
    System.out.println(raf.getFilePointer()); //0
    raf.writeInt(1);
    System.out.println(raf.getFilePointer()); //4
}

/* void seek(long pos)
* 该方法用于移动指针位置
*/
@Test
public void testSeek() throws IOException{
RandomAccessFile raf = new RandomAccessFile("test.txt","rw");
    System.out.println(raf.getFilePointer());//0
    raf.writeInt(1);
    System.out.println(raf.getFilePointer());//4
    raf.seek(0);
    System.out.println(raf.getFilePointer());//0
}

RandomAccessFile的close()方法

RandomAccessFile文件访问结束后,别忘记用close()方法释放资源,作为一名环保卫士,节约能源人人有责。

Paths Path

Paths

Paths是一个工具类。这个类非常简单,只有两个方法加一个私有构造方法。

1
2
3
4
5
public final class Paths {
private Paths() {}
public static Path get(String first, String... more) {}
public static Path get(URI uri){}
}

通过Paths的get静态方法,我们可以获得一个Path对象,而实际上我们通常都是通过Paths的这个get方法来获取Path对象。

Path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Path中的常用方法
boolean isAbsolute();
//getFileName这个方法类似于File类的getName方法,返回路径的文件名称(目录名或者文件名)
Path getFileName();
Path getParent();
int getNameCount();
Path getName(int index);
//subpath方法和我们String中的substring类似,给定开始和结束位置的索引值,获取他们之间的路径字符串。
Path subpath(int beginIndex, int endIndex);
Path resolve(Path other);
Path resolveSibling(Path other);
Path relativize(Path other);
Path toAbsolutePath();
//这个方法对应于FIle中的toPath,为什么要实现这么两个方法,其实还是为了兼容旧的File类,方便一些旧的系统成功的跨度到新的java标准中来。
File toFile();

部分方法示例:

1
2
3
4
5
6
7
Path p = Paths.get("a","b","c","d","e");
System.out.println(p.getNameCount());
// 执行代码可以看到输出结果:5

Path p = Paths.get("a/t","b","c","d","e");
System.out.println(p.getNameCount());
//执行代码输出结果:6

可见getNameCount方法并不是直接数构建Path对象时传入了几个字符串。当我们调用Path.get方法传入可变字符串作为路径时,它将每个独立的字符串当成一个目录名,使用默认路径分隔符连接这些路径名形成Path路径,而调用getNameCount方法是根据默认路径分隔符的个数来统计返回的。

getName方法要求传入一个int型索引,在构建路径时,从根路径开始每一层都被编号了,根目录为0,子目录依次加一所以getName方法可以获取任意层次目录的名称。

1
2
3
Path p = Paths.get("a","b","c","d","e");
System.out.println(p.getName(1));
//输出:b

resolve方法返回一个规范化的绝对路径

1
2
3
4
5
6
7
8
9
Path p = Paths.get("a","b","c","d","e");
Path q = Paths.get("c:/users");
System.out.println(p.resolve(q));
// 输出:c:/users

Path p = Paths.get("a","b","c","d","e");
Path q = Paths.get("users");
System.out.println(p.resolve(q));
// 输出结果:a\b\c\d\e\users

resolveSibling方法是通过解析当前路径的父目录,产生兄弟路径。

1
2
3
4
Path p = Paths.get("a","b","c","d","e");
Path q = Paths.get("users");
System.out.println(p.resolveSibling(q));
//输出结果:a\b\c\d\users

替换了e为users,因为e作为当前目录,而此方法就是在当前目录下生成一个和他同级的兄弟目录。

relativize方法是一个用来生成一个相对路径的方法。需要额外传入一个Path对象。

1
2
3
4
5
6
7
8
9
Path p = Paths.get("a","b","c","d","e");
Path q = Paths.get("users");
System.out.println(p.relativize(q));
// 输出结果:..\..\..\..\..\users

Path p = Paths.get("a","b","c","d","e");
Path q = Paths.get("a","b","c","d","e","f");
System.out.println(p.relativize(q));
/*输出结果:f*/

Files

整个FIles类中,都是静态方法,没有一个实例域。也是一个工具类。这个类就是为了实现对文件的各种操作。

对文件的读写操作

1
2
3
4
5
6
7
8
9
10
11
12
13
//根据Path路径返回InputStream/OutputStream字节流对象
public static InputStream newInputStream(Path path, OpenOption... options)
public static OutputStream newOutputStream(Path path, OpenOption... options)
//通过Path对象返回BufferedReader/BufferedWriter对象
public static BufferedReader newBufferedReader(Path path, Charset cs)
public static BufferedReader newBufferedReader(Path path)
public static BufferedReader newBufferedWriter(Path path, Charset cs)
public static BufferedReader newBufferedWriter(Path path)
// readAllBytes内部通过创建InputStream对象来读取所有的字节到给定的字节数组中并返回
public static byte[] readAllBytes(Path path)
// readAllLines内部通过创建List数组,使用BufferedReader创建字符缓冲流,一行一行的读取。最后返回List集合
public static List<String> readAllLines(Path path)
public static Path write(Path path, byte[] bytes, OpenOption... options)

copy这个方法有多个重载

1
2
3
4
private static long copy(InputStream source, OutputStream sink)
public static long copy(InputStream in, Path target, CopyOption... options)
public static long copy(Path source, OutputStream out)
public static Path copy(Path source, Path target, CopyOption... options)

第一个重载是一个私有的方法,是被被人调用的工具方法。主要的功能是:从一个source流中读取所有的字节并写入sink流中,返回实际读入或写入的字节数。

第二个重载是选择将Path对象通过方法newOutputStream,构建了一个OutputStream对象,然后调用第一个重载方法实现copy。完成的功能是:从一个InputStream流中读取所有的字节并写入一个指定的文件中。

第三个重载方法主要是:从一个Path文件中读取所有的字节并写入一个OutputStream对象流中。操作流程类似,不在赘述。

最后一个重载方法实现的是从一个Path对象复制到另一个Path对象。

1
2
3
4
5
//根目录下只有hello.txt文件,没有world文件
Path p = Paths.get("hello.txt");
Path q = Paths.get("world.txt");
Files.copy(p,q);
/*world文件被创建并且hello中的内容被复制到此*/

如果q在磁盘为位置的文件已经存在将不能完成复制操作,如果p在磁盘位置上没有对应文件此操作依然失败,如果p是一个目录文件,结果会复制一个名为world的目录文件,如果q是一个目录文件则会创建一个无类型的文件(hello中的内容已经被复制进去)

文件或目录的创建和获取文件的基本信息操作

1
2
3
4
5
6
7
8
public static Path createFile(Path path, FileAttribute<?>... attrs)
public static Path createDirectory(Path dir, FileAttribute<?>... attrs)
public static Path createDirectories(Path dir, FileAttribute<?>... attrs)
public static Path createTempFile(Path dir,
String prefix,
String suffix,
FileAttribute<?>... attrs
)

因为Path路径中存放的可以是文件类型,也可以是目录类型。那么在创建的时候就需要进行区分了。createFile根据指定路径创建一个指定类型的文件,createDirectory和createDirectories的区别在,如果Path路径上存在着没有被创建的目录,后者会将他们全部都创建。

一些判断方法

1
2
3
4
5
6
7
8
9
public static boolean isSameFile(Path path, Path path2)
public static boolean isHidden(Path path)
public static String probeContentType(Path path)
public static boolean isDirectory(Path path, LinkOption... options)
public static boolean isRegularFile(Path path, LinkOption... options)
public static long size(Path path)
public static boolean exists(Path path, LinkOption... options)
public static boolean isReadable(Path path)
public static boolean isWritable(Path path)

迭代和过滤

在Files类中,设计了一个方法newDirectoryStream,返回了一个目录流,可以显著提高效率。

1
2
3
public static DirectoryStream<Path> newDirectoryStream(Path dir)
public static DirectoryStream<Path> newDirectoryStream(Path dir, String glob)
public static DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter)

第一个只需要提供一个Path路径即可

第二个方法提供了一个Path对象和一个glob字符串

第三个方法还外部指定了一个过滤器。

示例:

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
DirectoryStream<Path> d = Files.newDirectoryStream(Paths.get("f:/360"));
for (Path p : d){
System.out.println(p.getFileName());
}
//输出结果:
360defender
360sdSetup.exe
360zip

DirectoryStream<Path> d = Files.newDirectoryStream(Paths.get("f:/360"),"*.exe");
for (Path p : d){
System.out.println(p.getFileName());
}
//输出结果:
360sdSetup.exe

DirectoryStream<Path> d = Files.newDirectoryStream(Paths.get("/"),
entry -> Files.isDirectory(entry));
for (Path p : d){
System.out.println(p.getFileName());
}
for (Path p : d){
System.out.println(p.getFileName());
}
//输出结果:
360defender
360zip

FileAttribute/ FileAttributeView

旧版本中File类只提供了一些非常简单的文件属性访问功能,基本上只是可以获取文件属性值,但通常不能修改;NIO.2专门在java.nio.file包下增加了一个attribute包(即java.nio.file.attribute),里面提供了大量的文件属性访问及修改API,基本上可以全面使用OS的文件系统功能。

一般文件属性都是由OS自己来管理,用户只不过是在执行某个操作时“不经意地“、”自动地“触发了文件属性修改的动作而已,因此Java就将属性的集合放在”视图“中。

Java按照功能和类别将文件属性分成了好几种,而最顶层的是FileAttributeView,这是个接口,表示所有文件属性视图的父视图,而其低下则派生了很多各种类型的文件属性视图:这里介绍几个最常用的

  • BasicFileAttributeView:基础文件属性,包括文件修改时间、创建时间、最后修改时间、文件大小、是否为目录、是否为快捷方式等,这也是最最常用的;

  • FileOwnerAttributeView:文件主人的相关属性,其只有两个功能,一个是获取当前文件的所属人,另一个是修改当前文件的所属人;

  • DosFileAttributeView:获取和修改文件的Dos属性(Windows中的特性),比如检查文件是否为隐藏、是否是归档文件,其使用程度仅次于BasicFileAttributeView;

  • UserDefinedFileAttributeView:用户自定义文件属性,一般OS在开发层面都允许用户自定义文件的一些属性,自定义属性必然使用键值对来表示,键是属性的名称(需要自定义),而值则是属性的值(属性的值可以是任意类型的,因此需要用二进制字节来保存,读取和保存时需要用到ByteBuffer);

通过Files的工厂方法获取一个具体的文件属性视图对象

1
static <V extends FileAttributeView> V Files.getFileAttributeView(Path path, Class<V> type, LinkOption... options);

BasicFileAttributeView:只读视图(只能查看的属性,不能修改的属性)

基础文件属性不能直接通过视图来访问,必须要通过视图的readAttributes方法获得对应的BasicFileAttributes属性集合对象,然后用该属性集合对象才能正常访问和修改文件属性。

1
BasicFileAttributes bfa = BasicFileAttributeView.readAttributes();

BasicFileAttriubtes中共有8大方法获取文件的基础属性信息:3时间-4类型-1大小(都是BasicFileAttributes的对象方法)

1
2
3
4
5
6
7
8
9
10
11
FileTime lastModifiedTime();  // 最后修改时间
FileTime lastAccessTime(); // 最近访问时间
FileTime creationTime(); // 创建时间
//一般会调用FileTime的toMillis得到毫秒时间:long FileTime.toMillis();

boolean isRegularFile(); // 是否是正常文件(txt、exe等非目录类型的文件)
boolean isDirectory(); // 是否是目录
boolean isSymbolicLink(); // 是否为符号链接,即快捷方式
boolean isOther(); // 其它类型(特别是Unix中的块设备文件等,但是在Windows中不常见)

long size(); // 文件的字节大小

DosFileAttributeView:可查看可修改的视图

DosFileAttributes直接继承自BasicFileAttributes,因此BasicFileAttributes中的查看功能DosFileAttributes里全都有,这里只介绍DosFileAttributes里独有的查看功能:都是DosFileAttributes的对象方法

1
2
3
4
boolean isReadOnly(); // 是否是只读
boolean isHidden(); // 是否是隐藏文件
boolean isArchive(); // 是否是归档文件(即压缩文件)
boolean isSystem(); // 是否是操作系统组件(Windows操作系统的组件)

由于DosFileAttributes是Dos系统特有的,因此如果专门是Windows上的应用可以直接用DosFileAttributeView,但如果是跨平台的或者在其它平台上运行的最好是只用BasicFileAttributeView;

修改Dos文件属性直接使用视图修改:都是DosFileAttributeView的对象方法,和查看是相对应的

1
2
3
void setReadOnly(boolean value);  // 设置只读
void setHidden(boolean value); // 设置隐藏
void setArchive(boolean value); // 设置归档

自定义属性——UserDefinedFileAttributeView:

Java允许用视图UserDefinedFileAttributeView为文件自定义属性。属性用键值对定义,键即属性的名称,是String类型的,键所对应的值就是属性值,直接用二进制字节码来表示(这样可以表示任意类型的数据)。

UserDefinedFileAttributeView的对象方法

1
2
3
4
5
int write(String name, ByteBuffer src);  // 直接向视图中写入一个键值对
int read(String name, ByteBuffer dst); // 读取一个键值对
//上面的返回值都表示写入/读取了多少个字节;
int size(String name); // 属性值的大小,字节
List<String> list(); // 列出所有的自定义属性名

参考文档:
Jenkov.com/java-io
Jenkov.com/java-nio

更多Java基础系列文章,参见Java基础大纲

CATALOG
  1. 1. File类
    1. 1.1. 构造方法
    2. 1.2. 创建功能
    3. 1.3. 删除功能
  2. 2. RandomAceessFile
    1. 2.1. 构造方法
    2. 2.2. 读方法
    3. 2.3. RandomAccessFile写方法
    4. 2.4. RandomAccessFile的文件指针操作的方法
    5. 2.5. RandomAccessFile的close()方法
  3. 3. Paths Path
    1. 3.1. Paths
    2. 3.2. Path
  4. 4. Files
    1. 4.1. 对文件的读写操作
    2. 4.2. 一些判断方法
    3. 4.3. 迭代和过滤
  5. 5. FileAttribute/ FileAttributeView
    1. 5.1. BasicFileAttributeView:只读视图(只能查看的属性,不能修改的属性)
    2. 5.2. DosFileAttributeView:可查看可修改的视图
    3. 5.3. 自定义属性——UserDefinedFileAttributeView: