Java基础——IO流、NIO

Java I/O流

Java的IO通过Java.io包下的类和接口来支持,在Java.io包下主要输入、输出两种IO流,每种字符流则以字符来处理输入、输出操作。

Java的IO流使用了一种装饰器设计模式。

File类

File类是Java.io包下代表与平台无关的文件和目录。File类的实例代表文件对象。File能新建、删除、重命名文件和目录,File不能访问文件内容本身

访问文件和目录

File类可以使用文件路径字符串来创建File实例,该文件路径字符串可以是绝对路径也可以是相对路径。

File API

访问文件名相关的方法
  • String getName() 返回此File对象所表示的文件名或路径名
  • String getPath() 返回File对象对应的路径名
  • File getAbsoluteFile() 返回File对象的绝对路径
  • String getAbsolutePath() 返回此File对象所对应的绝对路径名
  • String getParent() 返回File对象所对应目录的父目录名
  • boolean renameTo(File newName):重命名此File对象所对应的文件或目录。如果重命名成功返回true,否则返回false
文件检测相关
  • boolean exists():判断File对象所对应的文件或目录是否存在
  • Boolean canWrite() 判断FIle对象所对应的文件和目录是否可写
  • Boolean canRead() 判断File对象对应的文件或目录是否可读
  • boolean isFIle():判断File对象所对应的是否是文件,而不是目录
  • boolean isDirectory():判断File对象所对应的是否是目录,而不是文件
  • boolean isAbsolute() 判断File对象所对应的文件或目录是否是绝对路径。该方法消除了不同平台的差异,可以直接判断File对象是否为绝对路径
获取常规文件信息
  • long lastModified() 返回文件的最后修改时间
  • Long length() 返回文件内容的长度、
文件操作相关
  • boolean createNewFile() 返回当此File对象所对应的文件不存在时,该方法将新建一个该File对象所指定的新文件,创建成功返回true 否则返回false
  • boolean delete() 删除File对象所对一个的文件或路径
  • void deleteOnExit():注册一个删除钩子,指定当Java虚拟机退出时,删除File对象所对应的文件和目录
目录操作相关
  • mkdir() 创建一个File对象所对应的的目录,创建成功返回true 否则返回false
  • String[] list():列出File对象的所有子文件名返回String 数组
  • File[] listFiles() 列出File对象的所有子文件和路径,返回File数组
  • static File[] listRoots() 列出系统所有的根路径,静态方法

文件过滤器

File类的list()方法中可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。

FilenameFilter接口里包含了一个accept(File dir,String name)方法,该方法将依次对指定File的所有子目录或文件进行迭代。如果该方法返回true,则list()方法会列出该子目录或者文件

public class Test {
    public static void main(String[] args) {
        File file = new File("/Users/wangxin/ideaProject");
        String[] fileList = file.list((f, name) -> {
            return name.endsWith(".java") || f.isDirectory();
        });
        for (String s : fileList) {
            System.out.println(s);
        }
    }
}

filenameFilter接口中有accept()方法,实现accept()方法就是指定自己的规则,指定哪些文件应该由list()方法列出

Java的IO流

输入流和输出流

  • 输入流:只能从中读取数据,而不能向其写入数据
  • 输出流:只能向其写入数据,而不能从中读取数据

字节流和字符流的概述

字节流主要由InputStream和OutputStream作为基类

字符流主要由Reader和Writer作为基类

字节流和字符流处理的输入/输出单位不同。输入流使用隐式的记录指针来表示当前正准备从哪个地方开始读取,当程序向OutputStream或Writer里输出一个或多个水滴后,记录指针自动向后移动。

InputStream和Reader

InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例。

InputStream

  • int read() 从输入流中读取单个字节,返回所读的字节数据
  • int read(byte[] b):从输入流中最多读取b.leng个字节数据,并将其存储在字节数组b中,返回实际读取的字节数
  • int read(byte[] b,int off,int len) 从输入流中最多读取len个字节数据,并将其存储在数组b中,放入数组b中时,从off位置开始,返回实际读取的字节数

Reader

  • int read():从输入流中读取单个字符,返回所读取的字符数据(字符数据可直接转换为int类型)
  • int read(char[] cbuf):从输入流中最多读取cbuf.length个字符的数据,并将其存储在字符数组中,返回实际读取的字符数
  • int read(char[] cbuf,int off,int len):从输入流中最多读取len个字符,并将其存储在字符数组cbuf中,放入数组cbuf中,从off位置开始,返回实际读取的字符数
OutputStream和Writer
  • void write(int c):将指定的字节/字符输出到输出流中,c可以代表字节,也可以代表字符
  • void write(byte[]/char[] buf):将字节数组/字符数据中的数据输出到指定输出流中
  • void write(byte[]/char[] buf,int off,int len)将字节数组/字符数组中从off位置开始,长度为len的字节输出到输出流中

Writer

  • void write(String str) 将str字符串里包含的字符输出到指定输出流中
  • void write(String str,int off,int len)将str字符串里从off位置开始,长度为len的字符输出到指定输出流中

处理流

处理流可以可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入/输出方法

我们可以使用处理流来包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层的I/O设备

转换流

Java的输入/输出体系中提供了两个转换流,转换流用于实现将字节流转换成字符流。

  • InputStreamReader 将字节输入流转换成字符输入流
  • OutputStreamWriter 将字节输出流转换成字符输出流

推回输入流

PushbackInputStream和PushbackReader

  • Void unread(byte[]/char[] buf)将一个字节/字符数据内容推回缓冲区里,从而允许重复读取刚刚读取的内容
  • void unread(byte[]/char[] b,int off,int len)将一个字节/字符数组里从off开始,长度为len字节/字符的内容推回缓冲区
  • void unread(int b)将一个字节/字符推回到推回缓冲区

推回输入流都带哟一个推回缓冲区,当程序调用这两个推回输入流的unread()时,系统会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用read()时 会先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,还没有装满read()所需的数组时才会从原输入流中读取

如果推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发IOException异常

重定向标准输入/输出

Java的标准输入/输出分别通过System.in和System.out来代表,默认情况下他们分别代表键盘和显示器。

在System类里提供了如下三个重定向标准输入/输出的方法

  • static void setErr(PrintStream err) 重定向“标准”错误输出流
  • static void setIn(InputStream in)重定向标准输入流
  • static void setOut(PrintStream out)重定向标准输出流

Java虚拟机读写其他进程的数据

使用Runtime对象的exec()可以产生一个Process对象,Process对象代表由该Java程序启动的子进程,Process类提供了用于让程序和其子进程进行通信的方法

  • InputStream getErrorStream() 获取子进程的错误流
  • InputStream getInputStream() 获取子进程的输入流
  • OutputStream getOutputStream() 获取子进程的输出流
public static void main(String[] args) {
        try {
            final Process process = Runtime.getRuntime().exec("javac");
            final BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

RandomAccessFile

RandomAccessFile是Java输入/输出体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。RandomAccessFile支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据

RandomAccessFile可以自由访问文件的任意位置,如果只需要访问文件部分内容,而不是把文件从头读到尾

RandomAccessFile允许自由定位文件记录指针,RandomAccessFile可以不从开始的地方开始输出,因此RandomAccessFile可以向已存在的文件后追加内容

RandomAccessFile只能读写文件,不能读写其他IO节点

RandomAccessFile读写文件的过程

RandomAccessFile对象包含了一个记录指针,用以标识当前读写处的位置

当程序新创建一个RandomAccessFile对象,该对象的文件记录指针位于文件头(0处),当读/写了n个字节后,文件记录指针将会向后移动n个字节。除此之外,RandomAccessFile可以自由移动该记录指针。既可以向前移动也可以向后移动

  • long getFilePointer():返回文件记录指针的当前位置
  • void seek(long pos):将文件记录指针定位到pos位置

RandomAccessFile可读可写,所以它既包含了完全类似于InputStream的三个read(),用法完全一样。也包含了完全类似于OutputStream的三个writer(),用法完全一样。

除此之外,RandomAccessFile还包含了一系列的readXxx()和writeXxx()方法

对于随机访问的理解

随机访问:可以自由访问文件的任意地方,任意访问

RandomAccessFile API

RandomAccessFile类有两个构造器

  • 一个使用String参数来指定文件名
  • 一个使用File参数来指定文件本身

除此之外,还需要指定一个mode参数

  • r:只读 执行写入抛出异常
  • rw: 以读、写方式打开文件,如果文件不存在则尝试创建文件
  • rws:以读写方式打开文件,除基本读写外还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备
  • rwd:以读、写方式打开指定文件,还要求对文件内容的每个更新都同步写入到底层存储设备
 public static void main(String[] args) {
        try {
            insertFileContent("/Users/wangxin/Documents/test.md",100,"插入的数据,*******¥¥¥¥¥¥");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void insertFileContent(String fileName, long pos, String insertContent) throws IOException {
        //创建临时文件输入输出流 用于存放到缓冲区
        final File tmp = File.createTempFile("tmp", null);
        final FileOutputStream fos = new FileOutputStream(tmp);
        final FileInputStream fis = new FileInputStream(tmp);
        final RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
        //设置文件指针
        accessFile.seek(pos);
        //读取文件
        byte[] buffer = new byte[1024];
        int readCount = 0;
        while ((readCount = accessFile.read(buffer)) != -1) {
            //写入到文件输出流中
            fos.write(buffer, 0, readCount);
        }
        accessFile.seek(pos);
        //写入要追加的内容
        accessFile.write(insertContent.getBytes());
        while ((readCount = fis.read(buffer)) != -1) {
            //写回到文件中
            accessFile.write(buffer,0,readCount);
        }

    }

Java9改进的对象序列化

对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列号机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点

Java9增强了对象序列化机制,它允许对读入的序列化数据进行过滤。

序列化的前提

如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的。为了让某个类是可序列化的,必须实现

  • Serializable
  • Externalizable

Serializable接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的

序列化的步骤

  1. 创建一个ObjectOutputStream 这是一个输出流的处理流,所以必须建立在其他流的基础上
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
  1. 调用ObjectOutputStream对象的writeObject()方法输出可序列化对象
oos.writeObject(obj);

反序列化的步骤

  1. 创建一个ObjectInputStream输入流,这个输入流是一个处理流,处理流也必须建立在其他节点流的基础上
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
  1. 调用readObject()方法读取流中对象,返回一个Object类型的Java对象
  • 如果序列化机制向文件中写入了多个Java对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取
  • 当一个可序列化类有多个父类,这些父类要么有无参数构造器,要么也是可序列化的

序列号

  • 所有保存到磁盘中的对象都有一个序列化编号
  • 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未被序列化过,系统才会对该对象转换成字节序列并输出
  • 如果某个对象已经序列化过,则直接输出一个序列化编号,不会再次序列化该对象

Java9增加的过滤功能

Java9为ObjectInputStream增加了setObjectInputFilter()、getObjectInputFilter()两个方法。

  • setObjectInputFilter() 用于为对象输入流设置过滤器,当程序通过ObjectInputStream反序列化对象时,过滤器的checkInput()会被自动激发

    • Status.REJECTED:拒绝恢复

    • Status.ALLOWED:允许恢复

    • Status.UNDECIDED:未决定状态,程序继续执行检查

      ObjectInputStream将会根据ObjectInputFilter的检查结果来决定是否执行反序列化,如果checkInput()方法返回Status.REJECTED 反序列化将会被组织,如果checkInput()返回Status.ALLOWED,则可执行反序列化

自定义序列化

可以使用transient关键字修饰实例变量,表示实例变量的类型是不可序列化的。

NIO

Java中传统的IO流都是阻塞式,而且传统的输入流、输出流都是通过字节的移动来处理的,

在JDK1.4开始,Java提供了一系列改进的输入/输出处理的新功能,称之为NIO

NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以达到像访问内存一样来访问文件

io包

  • java.nio包:各种与Buffer相关的类
  • java.nio.channels:主要包含与Channel和Selector相关类
  • java.nio.charset:主要包含与字符集相关的类
  • java.nio.channels.spi 主要包含与Channel相关的服务提供者编程接口
  • java.nio.charset.spi:包含与字符集相关的服务提供者编程接口

Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象

Channel

Channel是对传统的输入/输出系统的模拟,在NIO中所有的数据都需要通过通道传输

Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map()方法,通过map()方法可以直接将"一块数据"映射到内存中

Buffer

Buffer可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,从Channel中读取的数据也必须先放到Buffer中。

创建Buffer

Buffer都没有提供构造器,使用Buffer的静态方法来得到一个Buffer对象,Buffer最常用的子类是:ByteBuffer、CharBuffer

  • static XxxBuffer allocate(int capacity) 创建一个容量为capacity的XxxBuffer对象,间接Buffer 创建在程序的堆空间中

  • static ByteBuffer allocateDirect(int capacity) 创建一个新的字节缓冲区,这是一个直接Buffer 直接字节缓存,存储在系统内存中

    通过allocate()方法创建的Buffer对象时普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer。直接Buffer的创建成本比普通Buffer的创建成本高,但直接Buffer的读取效率跟高

    只有ByteBuffer才提供allocateDirect() 只能在ByteBuffer级别上创建直接Buffer

    直接Buffer的创建成本很高,直接Buffer只适用于长生存期的Buffer,而不适用于短生存期,一次用完就丢弃的Buffer。

  • static ByteBuffer wrap(byte[] array) 使用array字节数组包装到缓冲区中 创建间接Buffer

  • static ByteBuffer warp(byte[] array,int offset,int length)将字节数组的一部分包装到缓冲区中

使用Buffer

Buffer是一个抽象类,最常用的子类是ByteBuffer,可以在底层字节数组上进行get/set操作

基本所有的数据类型(boolean)都有相对应的Buffer类

  • CharBuffer(char)
  • ShortBuffer(short)
  • IntBuffer(int)
  • LongBuffer(long)
  • FloatBuffer(float)
  • DoubleBuffer(double)

Buffer中有三个重要的概念:容量(capacity)、界限(limit)、位置(position)

  • 容量(capacity):缓冲区的容量表示该Buffer的最大数据容量,即最多可以存储多少数据。缓冲区的容量不可能为负值,创建后不能改变
  • 界限(limit):第一个不应该被读出或者写入的缓冲区位置索引。也就是说,位于limit后的数据既不可被读,也不可被写
  • 位置(position):用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似于IO流中的记录指针)
  • 标记(mark):可以在当前缓冲区中设置mark为当前position,后期可以调用reset()将position转到mark所在的位置

0《mark《position《limit《capacity

工作过程

Buffer的主要作用就是装入数据,然后输出数据。开始时Buffer的position为0,limit为capacity。程序可以通过put() 向Buffer中放入一些数据,或者从Channel中获取一些数据,每放入一些数据,Buffer的position相应地向后移动一些位置

Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在的位置,并将position的位置设置为0,丢弃标记这样就将Buffer的读写指针又移到了开始位置。调用flip()方法之后,Buffer为输出数据做好准备,Buffer输出数据结束后,Buffer调用clear()方法,clear不清空Buffer的数据,仅仅将position置为0,将limit置为capacity,丢弃mark标记 这样为再次向Buffer中装入数据做好准备。

buffer调用flip()后后面的数据不可访问,避免读取数据出现Null值

buffer调用clear()后position设置为0后后面的空间又可以访问

  • int capacity():返回Buffer的capacity大小
  • boolean hasRemaining() 判断当前位置(position)和界限(limit)之间是否还有元素可供处理
  • int limit() 返回Buffer的界限limit位置
  • Buffer limit(int newLt) 重新设置界限limit的值,并返回一个具有新的limit的缓冲区对象
  • Buffer mark():设置Buffer的mark位置,只能在0和position之间调用mark
  • int position() 返回Buffer中的position值
  • Buffer position(int newPs) 设置Buffer的position并返回被修改的对象
  • Buffer reset() 将position转到mark所在的位置
  • Buffer rewind() 将position设置为0,取消设置的mark
  • public Buffer clear() 还原缓冲区状态
    • 将position置为0
    • 将限制limit设置为容量capacity
    • 丢弃mark
  • public Buffer flip() 缩小limit
    • 将limit设置设置为当前position位置
    • 将当前position位置设置为0
    • 丢弃mark

存入数据和放入数据 put get方法 ,使用put()和get()方法 放入、取出数据,Buffer即支持对单个数据的访问,也支持对批量数据的访问

当使用put()和get(),分为相对和绝对两种

  • 相对:从Buffer的当前position处开始读取或写入数据,然后将位置的值按处理元素的个数增加

    get() 表示相对读取,表示读取当前position的数据

    put() 表示相对写入,写入数据到当前position

  • 绝对:直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer里的数据,不会影响位置position的值

    get(position)表示绝对读取,使用绝对读取的方式访问Buffer里的数据

    put(position,data)表示绝对写入,使用绝对写入的

    //创建Buffer对象
        final CharBuffer charBuffer = CharBuffer.allocate(20);
        //打印buffer信息
        System.out.println("buffer的capacity" + charBuffer.capacity());
        System.out.println("buffer的limit" + charBuffer.limit());
        System.out.println("buffer的position" + charBuffer.position());
        //存入三个数据
        charBuffer.put("a");
        charBuffer.put("b");
        charBuffer.put("c");
        //存入数据后查看position
        System.out.println("存入数据后position" + charBuffer.position());
        //调用flip()方法 准备输出数据
        charBuffer.flip();
        System.out.println("flip()后position" + charBuffer.position());
        System.out.println("flip()后limit" + charBuffer.limit());
        //取出第一个元素
        System.out.println("取出的第一个元素" + charBuffer.get() + "取出第一个元素后position" + charBuffer.position());
        //clear
        charBuffer.clear();
        System.out.println("clear后position" + charBuffer.position());
        System.out.println("clear后limit" + charBuffer.limit());
        //执行clear后,查看数据
        System.out.println("clear后查看buffer中的数据" + charBuffer.get(2));

使用Channel

Channel类似于传统的流对象,但与传统的流对象有两个主要区别

  • Channel可以直接将指定文件的部分或全部直接映射成Buffer
  • 程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据,如果要将数据写入Channel,一样先让程序将数据放入Buffer中,再将Buffer里的数据写入Channel中

构建Channel

所有的Channel都不应该通过构造方法创建,而是通过传统的节点InputStream、OutputStream的getChannel()方法来返回对应的Channel

Channel中最常用的三类方法:

  • map() 用于将Channel对应的部分或全部数据映射成ByteBuffer,映射限制2G

    map()方法的方法签名为:MappedByteBuffer map(FileChannel.MapMode mode,long position,long size)

    第一个参数:执行映射时的模式,分别有只读、读写模式

    第二个参数、第三个参数用于控制将Channel的哪些位置的数据映射成ByteBuffer

  • read() 用于从Buffer中读取数据

  • write() 用于向Buffer写入数据

FileChannel fileChannel = new FileInputStream(f).getChannel();
FileChannel fileOutChannel = new FileOutputStream(f).getChannel();

FileInputStream创建的Channel 只能读取文件

FileOutStream创建的Channel只能写入文件

RandomAccessFile创建的Channel是否能读写取决于RandomAccessFile打开文件的模式

使用Channel Buffer复制a.txt文件,追加在该文件后面

public static void main(String[] args) {
        //构建File
        final File file = new File("/Users/wangxin/Documents/test.txt");
        try (RandomAccessFile raf = new RandomAccessFile(file, "rw");
             //获取Channel
             final FileChannel fileChannel = raf.getChannel()
        ) {
            final MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
            //移动文件指针
            fileChannel.position(file.length());
            //输出所有数据
            fileChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

使用Channel Buffer使用数组读取文件,并打印

public static void main(String[] args) {
        //创建文件输入流
        try (FileInputStream fis = new FileInputStream(filePath);
             //获取channel
             final FileChannel channel = fis.getChannel();
        ) {
            //创建Buffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //从channel读取数据到buffer中
            while ((channel.read(byteBuffer)) != -1) {
                //写入数据到文件中
                //锁定缓冲区
                byteBuffer.flip();
                //创建Charset对象
                final Charset charset = StandardCharsets.UTF_8;
                //创建解码器对象
                final CharsetDecoder charsetDecoder = charset.newDecoder();
                //将bytebuffer的内容转码
                //输出数据
                final CharBuffer cbuff = charsetDecoder.decode(byteBuffer);
                System.out.println(cbuff);
                //清空指针与范围
                byteBuffer.clear();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

字符集和Charset

编码Encode和解码Decode

  • 编码:把明文的字符序列转换成计算机理解的二进制序列称为编码
  • 解码:把二进制序列转换成普通人能看懂的明文字符串称为解码

计算机底层是没有文本文件、图片之分的,只是记录每个文件的二进制序列,当需要保存文本文件时,程序必须先把文件中的每个字符翻译成二进制序列,当需要读取文本文件时,程序必须把二进制序列转换为一个个的字符

Java默认使用Unicode字符集,但是很多操作系统并不使用Unicode,那么从系统中读取数据到Java程序时,就可能出现乱码的问题

JDK1.4提供了Charset来处理字节序列和字符序列之间的转换关系,该类包含了用于创建解码器和编码器的方法,还提供了获取Charset所支持字符集的方法,Charset类是不可变的

  • availableCharsets() 获取当前JDK所支持的所有字符集
  • Charset forName(String name) 获取对应的name的字符集Charset对象
  • newDecoder() 返回CharsetDecoder 代表该Charset的解码器
  • newEncoder() 返回CharsetEncoder代表该Charset的编码器
  • CharBuffer decode() 可以将ByteBuffer(字节序列)转换成CharBuffer(字符序列)
  • ByteBuffer encode() 可以将CharBuffer(字符学历额)转换成ByteBuffer(字节序列)

Java7中新增了一个StandardCharsets类,该类包含了各种平台的编码集的Charset对象

文件锁

如果多个运行的程序需要并发修改同一个文件,程序之间需要某种机制来进行通信。使用文件锁可以有效地组织多个进程并发修改同一个文件

在NIO中,Java提供了FileLock来支持文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁FileLock对象,从而锁定文件

lock() 试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞

tryLock()尝试锁定文件,将直接返回而不是阻塞,如果获取到了文件锁则直接返回文件锁,否则将返回null

  • lock(long position,long size,boolean shared)对文件从position开始,长度为size的内容加锁,该方法是阻塞式的
  • tryLock(long position,long size,boolean shared)非阻塞式的加锁方法

shared为true表示该锁是一个共享锁,它将允许多个进程来读取文件。但组织其他进程获得对该文件的排他所

shared为false时,表明该锁是一个排他锁,它将锁住对该文件的读写。程序可以调用FileLock的isShared来判断它获得的锁是否为共享锁

处理完文件后通过FileLock的release()来释放文件锁

Java7的NIO.2

Java7对原有的NIO进行了重大改进

  • 提供了全面的文件IO和文件系统访问支持
  • 基于异步Channel的IO

Path、Paths和Files核心API

为了解决早期的Java提供的File类来访问文件系统时的性能不高问题NIO.2引入了Path接口,Path接口代表一个平台无关的平台路径

  • Path接口代表一个平台无关的平台路径
  • Paths代表Path的工具类,包含了各种静态方法来获取Path
 public static void main(String[] args) {
        //以当前路径创建Path对象
        final Path path = Paths.get(".");
        System.out.println("path中路径的数量" + path.getNameCount());
        //获取Path的根路径
        System.out.println("path的根路径" + path.getRoot());
        //获取Path的绝对路径
        final Path absolutePath = path.toAbsolutePath();
        System.out.println(absolutePath);
        //以多个String来构建path对象
        final Path path1 = Paths.get("h", "e", "l");
        System.out.println(path1);// -> h/e/l
    }

FIles

Files是一个操作文件的工具类

static String filePath = "/Users/wangxin/Documents/test.txt";

    public static void main(String[] args) {
        //使用Files复制文件
        try {
            Files.copy(Paths.get(filePath), new FileOutputStream("./copy.txt"));
            //是否为隐藏文件
            System.out.println("file是否为隐藏文件" + Files.isHidden(Paths.get(filePath)));
            //一次性读取文件的所有行 并存储到集合中
            final List<String> fileLines = Files.readAllLines(Paths.get(filePath));
            fileLines.forEach(System.out::println);
            //判断指定文件的大小
            System.out.println("Files文件的大小为" + Files.size(Paths.get(filePath)));
            //将指定list写入文件中
            final ArrayList<String> poem = new ArrayList<>();
            poem.add("添加文字1");
            poem.add("添加文字2");
            poem.add("添加文字3");
            Files.write(Paths.get(filePath), poem, StandardCharsets.UTF_8);
            poem.forEach(s -> {
                System.out.println(s);
            });
            //列出指定的目录下的所有文件
            Files.list(Paths.get(".")).forEach(path -> {
                System.out.println(path);
            });
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

使用FileVisitor遍历文件和目录

在以前的Java版本中,如果要遍历指定目录下的所有文件和子目录,只能使用递归进行遍历。不仅复杂,而且灵活性也不高

现在可以使用Files工具类来遍历指定文件和子目录

  • walkFileTree(Path start,FileVisitor visitor) 遍历start路径下的所有文件和子目录
  • walkFileTree(Path start,Set<FileVisitOption> options,int maxDepth,FileVisitor viditor)该方法最多遍历maxDepth深度的文件

FileVisitor代表一个文件访问器,walkFileTree()方法会自动遍历start路径下的所有文件和子目录,遍历文件和子目录都会触发FileVisitor中相应的方法

  • FileVisitResult postVisitDirectory(T dir,IOException exc) 访问子目录之后触发该方法
  • FileVisitResult preVisitDirectory(T dir,BasicFileAttribute attrs)访问子目录之前触发该方法
  • FileVisitResult visitFile(T file,BasicFileAttributes attrs):访问file文件时触发该方法
  • FileVisitResult visitFileFailed(T file,IOException exc)访问file文件失败时触发该方法

FileVisitResult是一个枚举类

  • CONTINUE 代表继续访问后续的行为
  • SKIP_SIBLINGS:代表继续访问的后续行为,但不访问该文件或目录的兄弟文件或目录
  • SKIP_SUBTREE:代表继续访问的后续行文,但不访问该文件或目录的子目录树
  • TERMINATE:代表终止访问后续行为

在使用时,可以使用SimpleFileVisitor(FIleVisitor)的实现类,来实现文件访问器

 public static void main(String[] args) {
        //遍历当前文件目录下的文件和子目录
        try {
            Files.walkFileTree(Paths.get("."), new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    System.out.println("正在访问" + file);
                    if (file.endsWith(".java")) {
                        System.out.println("已经找到.java文件");
                        //终止后续访问
                        return FileVisitResult.TERMINATE;
                    }
                    //执行后续访问
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    System.out.println("当前访问目录" + dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }

WatchService监控文件的变化

在以前的Java版本中,如果程序需要监控文件的变化,需要很繁琐的操作

在NIO.2的Path类提供了监听的方法来监听文件系统的变化

  • register(WatchService watcher,WatchEvent.Kind<?>...events):用watcher监听该path代表的目录下的文件变化。events参数指定要监听哪些类型的事件

    • WatchService 代表一个文件系统监听服务,它负责监听path代表的目录下的文件的变化。一旦使用register()方法完成注册之后,接下来就可调用WatchService的如下方法来获取被监听的目录的文件变化事件

      • WatchKey poll():获取下一个WatchKey,如果没有WatchKey发生就立即返回Null
      • WatchKey poll(long timeout,TimeUnit unit):尝试等待timeout时间去获取下一个WatchKey
      • WatchKey take():获取下一个WatchKey,如果没有WatchKey发生就一直等待

      如果程序需要一直监控,则应该选择使用take()方法,如果程序只是需要监控指定时间,则考虑使用poll()方法

 public static void main(String[] args) {
        //要监控的目录路径
        String watchFile = "/Users/wangxin/Documents";
        //获取系统默认的WatchService
        try {
            final FileSystem fileSystem = FileSystems.getDefault();
            final WatchService watchService = fileSystem.newWatchService();
            Paths.get(watchFile)
                    .register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
            while (true) {
                //获取下一个变化的事件
                final WatchKey watchKey = watchService.take();
                for (WatchEvent<?> event : watchKey.pollEvents()) {
                    System.out.println(event.context() + "文件发生了" + event.kind() + "事件");
                }
                //重设key
                final boolean reset = watchKey.reset();
                if (!reset) {
                    break;
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容