Java BIO

作者: beanlam | 来源:发表于2017-06-07 15:15 被阅读61次

File

File类位于JDK的java.io这个包下。
一个File类既可以代表一个文件,也可以代表一个目录。

构造器

要使用File,首先需要通过构造器构造它的一个实例

    File file1 = new File("/a/b");
    File file2 = new File("C:\\a\\b.dat");

构造File类需要给它指定一个路径,比如上面代码中的/a/bC:\\a\\b.dat.
路径可以代表一个文件,也可以代表一个目录。

路径分隔符依据操作系统的不同而不同,在类Unix系统中,分隔符是/,而在windows操作系统中,分隔符是\\,如果在代码中以硬编码的方式写死了路径分隔符,那么代码的可移植性就不高。可以通过File.separator或者是File.separatorChar来获取当前操作系统的路径分隔符。

File的构造器中的路径参数也支持绝对路径和相对路径,像上面的代码用的是绝对路径。那么相对路径相对的是哪个路径呢?Java会默认采用user.dir作为当前路径,可以通过System.getProperty("user.dir")来得到这个路径,这个路径也是JVM启动时所在的路径。

File也提供了另外一种构造器:

    File(String parent, String child)
    File(File parent, String child)

这两个构造器可以让你在构造文件或目录时指定它的父目录。

路径与名字

File类包含了诸多获取路径和路径名字的方法,这些方法看似差别不大却又别有洞天,可以通过下面几段代码来看看区别:

执行以下代码

File file = new File(".");
System.out.println("Absolute path = " + file.getAbsolutePath());
System.out.println("Canonical path = " + file.getCanonicalPath());
System.out.println("Name = " + file.getName());
System.out.println("Parent = " + file.getParent());
System.out.println("Path = " + file.getPath());
System.out.println("Is absolute = " + file.isAbsolute());

得到的结果是:

Absolute path = C:\prj\books\io\ch02\code\PathInfo\.
Canonical path = C:\prj\books\io\ch02\code\PathInfo
Name = .
Parent = null
Path = .
Is absolute = false

执行以下代码:

File file = new File("C:\reports\2015\..\2014\February");
System.out.println("Absolute path = " + file.getAbsolutePath());
System.out.println("Canonical path = " + file.getCanonicalPath());
System.out.println("Name = " + file.getName());
System.out.println("Parent = " + file.getParent());
System.out.println("Path = " + file.getPath());
System.out.println("Is absolute = " + file.isAbsolute());

得到的结果是:

Absolute path = C:\reports\2015\..\2014\February
Canonical path = C:\reports\2014\February
Name = February
Parent = C:\reports\2015\..\2014
Path = C:\reports\2015\..\2014\February
Is absolute = true

执行以下代码:

File file = new File("");
System.out.println("Absolute path = " + file.getAbsolutePath());
System.out.println("Canonical path = " + file.getCanonicalPath());
System.out.println("Name = " + file.getName());
System.out.println("Parent = " + file.getParent());
System.out.println("Path = " + file.getPath());
System.out.println("Is absolute = " + file.isAbsolute());

得到的结果是:

Absolute path = C:\prj\books\io\ch02\code\PathInfo
Canonical path = C:\prj\books\io\ch02\code\PathInfo
Name =
Parent = null
Path =
Is absolute = false

从这里可以看出来,file.getAbsolutePath()会把相对路径的信息也打印出来,读起来并不是非常直观的,而file.getCanonicalPath()总是以对人类阅读友好的方式打印路径。
如果File的入参是绝对路径,那么getNamegetPath只打印入参,并且getParent为null。

得到文件/目录信息

前面说过,File可以是一个文件,也可以代表一个目录,如何知道File代表的是哪一个呢?通过以下两个方法就可以知道

  • boolean isDirectory()
  • boolean isFile()

有时候我们想知道File代表的那个文件或目录是否在文件系统中存在,boolean exists()会告诉你。
在类Unix文件系统中,隐藏文件通常以.开头,比如用户的home目录下的.bash_profile,同样在windows中也会有隐藏文件,可通过isHidden()来判断一个文件是否是隐藏文件。通过length()可以获得文件的大小,通过lastModified()可以获得文件的最后修改时间,这个时间是距离(1970,1,1)的毫秒数。通常可以通过比较一个文件的最后修改时间来判断文件是否被修改过。

列举某个目录

可通过File[] listRoots()来列举当前文件系统的根目录。
在windows下,就是列出所有的盘符:

C:\
D:\
E:\
F:\

在Unix中,只有一个,那就是/

如果要列出某个特定目录下的文件和目录呢,有以下方法:

String[] list()
String[] list(FilenameFilter filter)
File[] listFiles()
File[] listFiles(FileFilter filter)
File[] listFiles(FilenameFilter filter)

以上方法中,返回String[]的,则是列举出所有文件或目录的名字。返回File[]的,则是所有文件或目录所代表的File对象。
FileFilterFilenameFilter是过滤器,能让你在列举目录时选择过滤掉哪些文件或目录。

获取磁盘空间信息

File提供了三个方法可以让你得知某个分区的磁盘空间的信息:

long getFreeSpace() //获取剩余空间
long getTotalSpace() //获取总空间大小
long getUsableSpace() //获取剩余可用空间

尽管getFreeSpace和getUsableSpace看起来差不多,但实际上是有差别的,getUsableSpace会进行更多细致的检查,比如当前JVM进程是否对该目录有写权限,以及另外一些操作系统的限制等,但getFreeSpace和getUsableSpace返回的值只能当做一个参考值,因为有可能有其他的进程正在读写这个磁盘空间。
下面是一个例子:

File[] roots = File.listRoots();
for (File root: roots) {
    System.out.println("Partition: " + root);
    System.out.println("Free space on this partition = " +
    root.getFreeSpace());
    System.out.println("Usable space on this partition = " +
    root.getUsableSpace());
    System.out.println("Total space on this partition = " +
    root.getTotalSpace());
    System.out.println("***");
}

输出结果为:

Partition: C:\
Free space on this partition = 143271129088
Usable space on this partition = 143271129088
Total space on this partition = 499808989184
***
Partition: D:\
Free space on this partition = 0
Usable space on this partition = 0
Total space on this partition = 0
***
Partition: E:\
Free space on this partition = 733418569728
Usable space on this partition = 733418569728
Total space on this partition = 1000169533440
***
Partition: F:\
Free space on this partition = 33728192512
Usable space on this partition = 33728192512
Total space on this partition = 64021835776
***

对文件或目录进行修改

如果想创建一个文件,使用boolean createNewFile()将会创建一个新的空文件,同样,创建一个目录可以用boolean mkdir()或者boolean mkdirs(),如果中间目录不存在,后者会创建好所有中间目录,而前者将会报错某个目录不存在。

有时候你希望创建一个临时文件,可以使用static File createTempFile(String prefix, String suffix),这个方法将会默认把临时文件放在用户的临时文件夹中,如果你想指定临时文件存放的地方,可以使用static File createTempFile(String prefix, String suffix, File directory)指定该目录。

文件权限

从Java 1.6开始,增加了对文件权限修改的接口。

boolean setExecutable(boolean executable)
boolean setExecutable(boolean executable, boolean ownerOnly)
boolean setReadable(boolean readable)
boolean setReadable(boolean readable, boolean ownerOnly)
boolean setWritable(boolean writable)
and boolean setWritable(boolean writable boolean ownerOnly)

同时提供以下接口获取文件权限信息:

boolean canRead()
boolean canWrite()
boolean canExecute()

RandomAccessFile

对文件的读取,既可以按顺序,也可以以任意顺序来读取。
RandomAccessFile提供这样一种功能。其保存一个指向当前文件位置的指针,可以通过调整指针的位置,读取一个文件中任意的内容。通过一段简单的代码来有个大体的认识:

RandomAccessFile raf = new RandomAccessFile("abc.log", "r");
int logIndex = 10;
raf.seek(logIndex);
//接下来通过raf进行文件操作

构造器

RandomAccessFile提供了两个构造器

RandomAccessFile(File file, String mode)
RandomAccessFile(String path, String mode)

模式

通过RandomAccessFile打开一个文件需要指定打开的模式,构造参数中的mode有四种模式可以选择:

  1. "r",以只读的方式打开一个已存在的文件,不可对文件进行写操作。
  2. "rw",以读写的方式打开一个已存在的文件,若文件不存在,则创建一个,可对该文件进行读写操作。
  3. "rwd",除了具有"rw"的特点外,这个模式要求对文件内容的每一个更新都会同步更新至底层的物理存储。
  4. "rws",除了具有"rw"的特点外,这个模式要求对文件内容和文件元数据的每一个更新都会同步更新至底层的物理存储。

文件的元数据并非指文件的内容本身,文件的大小以及文件的最后修改时间等等算是元数据的一部分

显然,如果指定了rwdrws模式,那么对于文件的操作将会相对比较慢一些。

读写

RandomAccessFile内部维护了一个指针,指向当前读取或者写入的位置,当通过RandomAccessFile打开一个已存在的文件或者创建一个新文件时,指针自动指向下标为0的位置。进行写入操作时,如果指针已经指向文件的末尾,那么文件的大小将会被扩大。

当需要进行读取或者写入时,首先通过void seek(long pos)将文件的指针指向你想要读取或写入的位置,读取时有以下常用的方法可以进行读取:

  • int read() //读取下一个字节
  • int read(byte[] b) //将读取的字节装入b数组中
  • char readChar() //读取两个字节,并将其转型为char类型
  • int readInt() //读取四个字节,并将其转型为int类型

写入时有以下方法:

  • void write(int b) //将b的低八位写入
  • void writeChars(String s) //将字符串s所代表的字节写入
  • void write(byte[] b) //将字节数组b写入
  • void writeInt(int i) //写入4个字节的i

除了读取写入的方法外,setLength(long newLength)方法可以设置文件的大小,如果newLength小于当前文件大小,那么文件将会被截肢,反之,文件将会被扩大到newLength。

FileDescriptor

值得注意的是,RandomAccessFile提供了一个FileDescriptor getFD()方法获取文件所对应的文件描述符对象,文件描述符代表是一种平台独立的文件描述结构,通过这个描述符可以对文件进行一些特殊的操作。

FileDescriptor定义了sync()方法,与之前提到的"rwd"和"rwd"一样,sync方法用来告诉操作系统将缓冲区的内容全部刷到物理的存储上。如果没有指定rwd或者rws模式,那么对文件的写入将会暂时存储于操作系统层面的缓冲区里面,当缓冲区满时,操作系统才会将内容刷至物理磁盘,通过sync()方式可以让操作系统对每一次写入操作都同步刷新至物理存储中,以下为一个例子:

RandomAccessFile raf = new RandomAccessFile("abc.log", "rw");
//这里的模式不是rwd或者rws
FileDescriptor fd = raf.getFD();
raf.write(...);
// 通过fd的sync方法,可以让写入操作同步地刷新至物理存储
fd.sync();
raf.close();

IO 流与装饰者模式

流概述

Java中,流是一种有序的字节序列,可以有任意的长度。从应用流向目的地称为输出流,从目的地流向应用称为输入流。

Java的流族谱

Java的 java.io 包中囊括了整个流的家族,输出流和输入流的谱系如下所示:

InputStream和OutputStream

InputStream和OutputStream分别是输入输出流的顶级抽象父类,只定义了一些抽象方法供子类实现。

在输出流OutputStream中,如果你需要向一个输出流写入数据,可以调用void write(int b)方法,这个方法会将b的低八位写入流中,高24位将会被自动忽略。如果想要批量写入数据呢,那么可以调用void write(byte[] b)方法将一个字节数组的内容全部写入流中,同时还有void write(byte[] b, int off, int len)可以让你指定从哪里写入多少数据。
如果你希望你向流中写入的数据能够尽快地输送到目的地,比如说文件,那么可以在写入数据后,调用flush()方法将当前输出流刷到操作系统层面的缓冲区中。不过需要注意的是,此方法并不保证数据立马就能刷到实际的物理目的地(比如说存储)。
使用完流后应该调用其close()方法将流关闭,流关闭时,将会先flush,后关闭。

在输入流InputStream中,可以通过int read()方法来从流中读取一个字节,批量读取字节可以通过int read(byte[] b)或者int read(byte[] b, int off, int len)来实现,这两个方法的返回值为实际读取到的字节数。如果需要重复读取流中某段数据,可以在读取之前先使用void mark(int readlimit)方法在当前位置做一个记号,之后通过void reset()方法返回到之前做标记的位置,不过在做这些标记操作之前,需要先通过boolean markSupported()方法来确定该流是否支持标记。如果对某些可预知的数据不感兴趣,可以使用long skip(long n)来调过一些流中的一些数据。

使用完流,无论是输入还是输出流,都要调用其close()方法对其进行关闭。

装饰器模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

这种设计模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

以InputStream为例,它是一个抽象类:

public abstract class InputStream implements Closeable {
    ...
    ...
}

并定义有抽象方法

public abstract int read() throws IOException;

该抽象方法由具体的子类去实现,通过InputStream的族谱图可以看到,直接继承了InputStream,并且提供某一特定功能的子类有:

  • ByteArrayInputStream
  • FileInputStream
  • ObjectInputStream
  • PipedInputStream
  • SequenceInputStream
  • StringBufferInputStream

这些子类都具有特定的功能,比如说,FileInputStream代表一个文件输入流并提供读取文件内容的功能,ObjectInputStream提供了对象反序列化的功能。

InputStream这个抽象类有一个子类与上述其它子类非常不同,这个子类就是FilterInputStream,可参见上图中的InputStream族谱图。

翻开FilterInputStream的代码,我们可以看到,它内部又维护了一个InputStream的成员对象,并且它的所有方法,都是调用这个成员对象的同名方法。
换句话说,FilterInputStream它什么事都不做。就是把调用委托给内部的InputStream成员对象。如下所示:

public class FilterInputStream extends InputStream {
    protected volatile InputStream in;
    
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
    
    public int read() throws IOException {
        return in.read();
    }
    
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    
    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
    
    public long skip(long n) throws IOException {
        return in.skip(n);
    }
    
    public int available() throws IOException {
        return in.available();
    }
    
    public void close() throws IOException {
        in.close();
    }
    
    public synchronized void mark(int readlimit) {
        in.mark(readlimit);
    }

    public synchronized void reset() throws IOException {
        in.reset();
    }
    
    public boolean markSupported() {
        return in.markSupported();
    }

FilterInputStream的又有其子类,分别是:

  • BufferedInputStream
  • DataInputStream
  • LineNumberInputStream
  • PushbackInputStream

虽然从上面代码看FilterInputStream并没有做什么有卵用的事,但是它的子类可不同了,以BufferedInputStream为例,这个类提供了提前读取数据的功能,也就是缓冲的功能。可以看看它的read方法:

    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }

可以看到,当pos>=count时,意即需要提前缓冲一些数据的时候到了,那么就会调用fill()将缓冲区加满,以便后续读取。由于本文只讨论io流的装饰器模式,所以关于具体实现细节将不会展开讨论,比如本文不会讨论fill()方法是如何实现的,在这里可以先将它当做一个黑盒子。

从这里可以开始感受到,BufferedInputStream就是一个装饰者,它能为一个原本没有缓冲功能的InputStream添加上缓冲的功能。

比如我们常用的FileInputStream,它并没有缓冲功能,我们每次调用read,都会向操作系统发起调用索要数据。假如我们通过BufferedInputStream来装饰它,那么每次调用read,会预先向操作系统多拿一些数据,这样就不知不觉中提高了程序的性能。如以下代码所示:

BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("/home/user/abc.txt")));

同理,对于其它的FilterInputStream的子类,其作用也是一样的,那就是装饰一个InputStream,为它添加它原本不具有的功能。OutputStream以及家属对于装饰器模式的体现,也以此类推。

JDK中的io流的设计是设计模式中装饰器模式的一个经典示范,如果细心发现,JDK中还有许多其它设计模式的体现,比如说监听者模式等等。

相关文章

  • BIO

    BIO 概述 服务端 客户端 bio的优点 bio的缺点 概述 Java BIO (blocking I/O): ...

  • BIO、NIO、AIO整理

    Java对BIO、NIO、AIO的支持: Java BIO (blocking I/O): 同步并阻塞,服务器实现...

  • Java IO 解读

    针对Java I/O 中的BIO 进行简单记录 BIO后续新增

  • java BIO和NIO

    BIO: blocking I/O 。 BIO就是传统I/O,其相关的类和接口在 java.io 包下 BIO 是...

  • BIO,NIO,AIO 总结

    BIO,NIO,AIO 总结 Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 ...

  • BIO,NIO,AIO 总结

    BIO,NIO,AIO 总结 Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 ...

  • 初识NIO之Java小Demo

    Java中的IO、NIO、AIO: BIO:在Java1.4之前,我们建立网络连接均使用BIO,属于同步阻塞IO。...

  • Nginx高性能原因

    nginx高性能原因 epoll多路复用 非阻塞IO操作 java bio模型,阻塞进程式java bio模型 l...

  • 高性能NIO编程详解,实现无服务器文件传输

    BIO NIO(new io) java1.4开始推出放入可非阻塞IO。java.nio包可解决BIO阻塞的不足但...

  • Java知识点总结

    JAVA知识点概括 BIO 阻塞式IO(BIO):数据在写入OutputStream或者InputStream都有...

网友评论

      本文标题:Java BIO

      本文链接:https://www.haomeiwen.com/subject/fptafxtx.html