Java核心技术 输入与输出3

访问目录中的项

静态的Files.list方法会返回一个可以读取目录中各个项的Stream< Path >对象。目录是被惰性读取的,因为读取目录涉及需要关闭的系统资源,所以应该使用try块:

Path path = Paths.get("corejava\\src");
try(Stream<Path> entries = Files.list(path)) {
    entries.forEach(p -> System.out.println(p.getFileName()));
}

list方法不会进入子目录,为了处理目录中的所有子目录,需要使用Files.walk方法,只要遍历的项是目录,那么在进入它之前,会继续访问它的兄弟项。
可以通过File.walk(pathToRoot, depth)来限制想要访问的树的深度。两种walk方法都具有FileVisitOption…的可变长参数,但只能提供一种选项:FOLLOW_LINKS,即跟踪符号链接。
无法通过Files.walk方法来删除目录树,因为需要在删除父目录之前先删除子目录。

使用目录流

如果需要对遍历过程进行更加细粒度的控制,应该使用Files.newDirectoryStream,它会产生一个DirectoryStream,它不是java.util.stream.Stream的子接口,而是专门用于目录遍历的接口。它是Iterable的子接口,因此可以在增强for循环中使用目录流,访问目录中的项并没有具体顺序。
可以用glob模式来过滤文件:

// try块确保目录流被正确关闭
try (DirectoryStream<Path> directories = Files.newDirectoryStream(path, "*.java")) {
    for (Path directory : directories) {
        System.out.println(directory.getFileName());
    }
}
模式描述示例
*匹配路径组成部分中0个或多个字符*.java匹配当前目录中所有的java文件
**匹配跨目录边界的0个或多个字符**.java匹配在所有子目录中的Java文件
匹配一个字符???.java匹配所有四个字符的java文件
[…]匹配一个字符集合,可以使用连线符[0-9]和取反符[!0-9]Test[0-9A-F].java匹配Testx.java,x是一个十六机制数字
{…}匹配由逗号隔开的多个可选项之一*.{java,class}匹配鄋java文件和class文件
\转义上诉任意模式中的字符以及\字符* \ **匹配所有文件中包含*的文件

Windows的glob语法则必须对反斜杠转义两次,一次为glob语法转义,一次为java字符串转义:

Files.newDirectoryStream(dir, "C:\\\\");

如果想要访问某个子目录的所有子孙成员,可以调用walkFileTree方法,并向其传递一个FileVisitor类型的对象,这个对象会得到下列通知:
1.在遇到一个文件或目录时,FileVisitResult visitFile(T path, BasicFileAttributes attrs)
2.在一个目录被处理前,FileVisitResult preVisitDirectory(T dir, IOException ex)
3.在一个目录被处理后,FileVisitResult postVisitDirectory(T dir, IOException ex)
4.在视图访问文件或目录时发送错误,例如没有权限打开,FileVisitResult visitDirectory(path, IOException)
对于上述每种情况,都可以指定是否希望执行下面的操作:
1.继续访问下一个文件:FileVisitResult.CONTINUE
2.继续访问,但是不在访问这个目录的任何项,FileVisitResult.SKIP_SUBTREE
3.继续访问,但是不在访问这个文件的兄弟文件,FileVisitResult.SKIP_SIBLINGS
4.终止访问,FileVisitResult.TERMINATE
当有任何方法抛出异常时,就会终止访问,这个异常从walkFileTree方法抛出。
FileVisitor< ? super Path >接口是泛型类型,但也不太可能使用出Path之外的东西,因为Path并没有多少超类型。
便捷类SimpleFileVisitor实现了FileVisitor接口,但是其除visitFileFailed方法之外的所有方法并不做任何处理而是直接访问,而visitFileFailed方法会抛出由失败导致的异常,并进而终止访问:

Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult preVisitDirectory(Path p, BasicFileAttributes attrs) {
        System.out.println(p);
        return FileVisitResult.CONTINUE;
    }
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
       return FileVisitResult.CONTINUE;
    }
    
    @Override
    public FileVisitResult visitFileFailed(Path p, IOException exc) {
        return FileVisitResult.SKIP_SUBTREE;
    }
});

需要覆盖postVisitDirectory和visitFileFailed方法,否则,访问会遇到不允许打开或不允许任何访问的文件时立即失败。
路径的众多属性是作为preVisitDirectory和visitFile方法的参数传递的。访问者不得不通过操作系统调用来获得这些属性,因为它需要区分文件和目录。因此不需要再次执行系统调用了。
如果在进入或离开一个目录是要执行某些操作(删除目录树):

Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file);
        return FileVisitResult.CONTINUE;
    }
    
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
        if (e != null) {
            throw e;
        }
        Files.delete(dir);
        return FileVisitResult.CONTINUE;
    }
});

ZIP文件系统

Paths类会在默认文件系统查找路径,即在用户本地磁盘中的文件。也可以使用别的文件系统,最有用之一是ZIP文件系统:

FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), null);

将建立一个文件系统,包含ZIP文档中的所有文件。如果知道文件名,那么从ZIP文档中复制出这个文件就变得容易:

Files.copy(fs.getPath(sourceName), targetPath);

列出ZIP文档中的所有文件,可以遍历文件树:

Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<Path>(){
   @Override
   public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
       System.out.println(file);
       return FileVisitResult.CONTINUE;
   }
});

6.内存映射文件

大多数操作系统都可以利用虚拟内存实现将一个文件或文件的一部分映射到内存中。然后这个文件就可以当做是内存数组一样地访问,这比传统的文件操作要快得多。

内存映射文件的性能

在这里插入图片描述
内存映射比使用带缓冲的顺序输入要稍微快一点,但比使用RandomAccessFile快很多。对于中等尺寸的顺序读入则没有必要使用内存映射。

首先从文件中获得一个通道(channel),通道是用于磁盘文件的一种抽象,它可以访问注入内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。

FileChannel channel = FileChannel.open(path, options);

然后通过FileChannel类的map方法从通道中获得ByteBuffer。可以指定想要的文件区域与映射模式,支持的模式有三种:
1.FileChannel.MapMode.READ_ONLY:所产生的缓冲区是只读的,任何对该缓冲区写入的尝试都会导致ReadOnlyBufferException异常
2.FileChannel.MapMode.READ_WRITE:所产生的缓冲区是可写的,任何修改都会在某个时刻写回到文件中。注意,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的
3.FileChannel.MapMode.PRIVATE:所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中
一旦有了缓存区,就可以使用ByteBuffer类和Buffer超类的方法读写数据了。缓冲区支持顺序和随机数据访问,它有一个可以通过get和put操作来移动的位置。

// 顺序遍历缓冲区的所有字节
while(buffer.hasRemaining()){
	byte b = buffer.get();
}
// 随机访问
for (int i = 0; i < buffer.limit(); i++) {
	byte b = buffer.get(i);
}

使用下面方法读写字节数组:
get(byte[] bytes)
get(byte[] bytes, int offset, int length)
还有getInt,getLong,getShort,getChar,getFloat,getDouble用来读写在文件中存储为二进制的基本类型。Java对二进制数据使用高位在前的排序机制。但是如果需要以低位在前方式处理二进制数字的文件:

// 查询缓冲区当前字节顺序
ByteOrder b = buffer.order();
buffer.order(ByteOrder.LITTLE_ENDIAN);

向缓冲区写数字,putInt,putLong,putShort,putChar,putFloat,putDouble,在恰当时机,以及当通道关闭时,会将这些修改写回到文件中。

下面程序是用于计算文件的32位的循环冗余校验和(CRC32),这个数值就是经常用来判断一个文件是否已损坏的,因为文件损坏极可能导致校验和改变。java.util.zip包中包含一个CRC32类,可以计算一个字节序列的校验和:

public class MemoryMaoTest {
    public static long checksumInputStream(Path filename) throws IOException {
        try (InputStream in = Files.newInputStream(filename)) {
            CRC32 crc = new CRC32();
            int c;
            while ((c = in.read()) != -1) {
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static long checksumBufferedInputStream(Path filename) throws IOException {
        try (InputStream in = new BufferedInputStream(Files.newInputStream(filename))) {
            CRC32 crc = new CRC32();
            int c;
            while ((c = in.read()) != -1) {
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static long checksumRandomAccessFile(Path filename) throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(filename.toFile(), "r")) {
            long length = file.length();
            CRC32 crc = new CRC32();
            for (long p = 0; p < length; p++) {
                file.seek(p);
                int c = file.readByte();
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static long checksumMappedFile(Path filename) throws IOException {
        try (FileChannel channel = FileChannel.open(filename)) {
            CRC32 crc = new CRC32();
            int length = (int) channel.size();
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, length);
            for (int p = 0; p < length; p++) {
                int c = buffer.get(p);
                crc.update(c);
            }
            return crc.getValue();
        }
    }

    public static void main(String[] args) throws IOException {
        System.out.println("Input Stream:");
        long start = System.currentTimeMillis();
        Path filename = Paths.get("C:\\Program Files\\Java\\jre1.8.0_77\\lib\\rt.jar");
        long crcValue = checksumInputStream(filename);
        long end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + " ms");

        System.out.println("Buffered Input Stream:");
        start = System.currentTimeMillis();
        crcValue = checksumBufferedInputStream(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + " ms");

        System.out.println("Random Access File:");
        start = System.currentTimeMillis();
        crcValue = checksumRandomAccessFile(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + " ms");

        System.out.println("Mapped File:");
        start = System.currentTimeMillis();
        crcValue = checksumMappedFile(filename);
        end = System.currentTimeMillis();
        System.out.println(Long.toHexString(crcValue));
        System.out.println((end - start) + " ms");
    }
    //Input Stream:
    //414112ba
    //98183 ms
    //Buffered Input Stream:
    //414112ba
    //288 ms
    //Random Access File:
    //414112ba
    //109077 ms
    //Mapped File:
    //414112ba
    //203 ms
}

缓冲区数据结构

缓冲区是由相同类型的数值构成的数组,Buffer类是一个抽象类,他有众多的具体子类,包括ByteBuffer、CharBuffer、DoubleBuffer、IntBuffer、LongBuffer和ShortBuffer(StringBuffer和缓冲区没关系)。
最常用的将是ByteBuffer和CharBuffer。每个缓冲区都具有:
1.一个容量,它永远不能改变
2.一个读写位置,下一个值将在此进行读写
3.一个界限,超过它进行读写是没有意义的
4.一个可选的标记,用于重复一个读入或写出操作
在这里插入图片描述
0<=标记<=位置<=界限<=容量
使用缓冲区的主要目的是执行“写,然后读入”循环。假设一个缓冲区在一开始,它的位置为0,界限等于容量。不断地调用put将值添加到这个缓冲区中,当耗尽所有的数据或写出的数据量达到容量大小时,就该切换到读入操作了。
这时调用flip方法将界限设置到当前位置,并把位置复位到0。现在在remaining方法返回正数时(它返回的值是“界限-位置”),不断地调用get。将缓冲区中所有的值都读入之后,调用clear使缓冲区为下一次写循环做准备。clear方法将位置复位到0,并将界限复位到容量。
如果想重读缓冲区,可以使用rewind或mark/reset方法。
要获取缓冲区,可以调用诸如ByteBuffer.allocate或ByteBuffer.wrap这样的静态方法。
然后可以用来自某个通道的数据填充缓冲区,或者将缓冲区内容写出通道中:

ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);

文件加锁机制

多个同时执行的程序修改同一个文件的情形,很明显这些程序需要以某种方式通信,不然文件很容易被损坏。文件锁可以解决这个问题,它可以控制对文件或文件中某个范围的字节的访问。
两个实例都会希望写一个配置文件,第一个实例应该锁定这个文件,当第二个实例发现这个文件被锁定时,必须决策是等待直至这个文件解锁,还是直接跳过这个写操作。
要锁定一个文件,可以调用FileChannel类的lock域或tryLock方法:

FileChannel channel = FileChannel.open(path);
FileLock lock = channel.lock();
// 或
// FileLock lock = channel.tryLock();

第一个调用会阻塞直至可获得锁,而第二个调用将立即返回,要么返回锁,要么在锁不可获得的情况下返回null。这个文件将保持锁定状态,直至这个通道关闭,或者在锁上调用了release方法。
可以通过调用锁文件的一部分:

FileLock lock(long start, long size, boolean shared)
FileLock tryLock(long start, long size, boolean shared)

如果锁定了文件的结尾,但这个文件的长度随增长超过了锁定部分,那么额外部分是未锁定的,如果想要锁定所有字节,可以使用Long.MAX_VALUE来表示尺寸。
如果shared标志为false,则锁定文件的目的是读写,而如果为true,则是一个共享锁,它允许多个进程从文件中读入,并阻止任何进程获得独占的锁。
并非所有操作系统都支持共享锁,因此可能在请求共享锁时候得到独占的锁。调用FileLock类的isShared方法可以查询锁的类型。

要确保在操作完释放锁时,与往常一样最好在一个try语句中释放锁:

try (FileLock lock = channel.lock()) {
	//访问锁定的文件或文件段
}

文件加锁机制依赖于操作系统,需要注意几点:
1.在某些系统中,文件加锁仅仅是建议性的,如果一个应用未能得到锁,它仍旧可以被另一个应用并发锁定的文件执行写操作
2.在某些系统中,不能在锁定一个问加你的同时将其映射到内存中
3.文件锁是由整个Java虚拟机持有的(两个程序由同一虚拟机启动,那不可能在每一个获得同一文件的锁),调用lock时,如果虚拟机已经在同一个文件锁持有了另一个重叠的锁,那么将抛出OverlappingFileLockException
4.在一些文件中,关闭一个通道会释放由Java虚拟机持有的底层文件上的所有锁。因此,在同一个锁定文件上应避免使用多个通道
5.在网络文件系统上锁定是高度依赖于系统的,因此应尽量避免

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值