JAVA IO/NIO

1、IO流基本介绍

1.1什么是IO流?我们为什么需要它?

​ 先想象一下在没有IO情况下你写的程序,他们都是在内存里自己跟自己玩的,还记得么?

​ 你在程序中声明了数组,声明了变量,但那只是在在内存中自己跟自己打交道。那么我们该如何跟外部信息进行交互呢?比如键盘。显示器,或者硬盘?

​ 所以为了与外部进行交互,我们需要IO框架,我们让数据好像流一样,自由的在内存和外部之间进行流通。

并且规定:

数据从外部流入到内存中,就是输入(读)

数据从内存流出到外部中,就是输出(写)

1.2IO的基础——字节流

​ API提供了两个顶层抽象类,用来表示操作所有的输出输出:InputStream,OutputStream。并且,这两个类表示字节的输入输出,因为输入输出的本质是字节流。

​ 注意体会一句话“字节流是最最基本的流”,这句话的由来就是因为计算机底层传递的就是字节。

当我们要操作文件的时候,就需要具体的对文件系统操作的IO实现类,于是我们需要学习FileInputStream和FileOutputStream,它们是文件输入输出字节流。

1.3字节流的升级——外挂式缓冲区字节流

​ 我们会发现原始的字节流对象用起来没那么高效,因为每个读或写请求都由底层操作系统处理,这些请求往往会触发磁盘访问、网络活动或其他一些相对昂贵的操作,只能一个字节一个字节的读,每次都调用底层的操作系统API,非常低效。

​ 而当我们外挂了一个缓冲区的流对象,可以一次读一个缓冲区,缓冲区空了才去调用一次底层API,这就能大大提高效率。他们的用法是把字节流对象传入后再使用,也相当于把它俩套在了字节流的外面,给字节流装了个“外挂”,让基本字节流如虎添翼。

1.4非英语国家该怎么办?多字节转换字符InputStreamReader和OutputStreamWriter

​ 对非英语国家的人来说,一个字节的大小无法表示他们所有的文字。因此,人们需要有能够处理字符的类,或者说这个类提供一个功能:就是把输入的字节转成字符,把要输出的字符转成计算机可以识别的字节。所以,你需要两个转换流:InputStreamReader和OutputStreamWriter。

​ 这两个类的作用分别是把字节流转成字符流,把字符流转成字节流。但是这两个流需要套在现成的字节流上才能使用,当中用到的设计模式也就是常说的装饰模式。

1.5字符流的升级操作:我也外挂缓冲区

​ 同上面说的缓冲区的作用,再把Reader和Writer做成高效的,就需要BufferedReader和BufferedWriter,把它们套在Reader和Writer上,就能实现高效的字符流。

2、IO模型

内核:内核级线程是操作系统内核实现、管理和调度的一种线程。

轮询:由CPU定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始

2.1 阻塞IO

​ 最传统的一种IO模型,在读写过程中会出现阻塞现象,当用户线程发出IO请求,内核会去查询数据是否准备就绪,如果没有就绪就会等待数据就绪,这时候的用户线程就处于阻塞状态,只有数据就绪,数据拷贝到用户线程了,才解除block状态

​ 典型的阻塞IO模型的例子为:data = socket.read();如果数据没有就 绪,就会一直阻塞在read方法。

​ 老老实实排队,啥也不能干,就等着就完了

2.2非阻塞IO

​ 与阻塞IO相同的地方在于,它也一样需要数据交给它,但是它不需要一直等待,而是一直在询问,每次询问得到的数据如果是ERROR,那就说明还没有准备好。

​ 用户线程:准备好了么?

​ 内核:没有(error)

​ 用户线程:准备好了么?

​ 内核:没有(error)

​ 用户线程:准备好了么?

​ 内核:滚啊!!

​ 在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO 不会交出CPU,而会一直占用CPU,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。

​ 黄牛党,代替你去厨师那里,并且连续不断的轰炸厨师

2.3多路复用IO

​ 多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。

​ 在多路复用IO 模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作

​ 因为在多路复用IO模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有 socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用

另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。

​ 要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件 逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

​ 会有一个服务生专门等着上菜,只不过菜上的太多会影响下一位准备用餐的客人

2.4信号驱动IO

​ 当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函 数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到 信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

​ 有点像去餐厅用餐时的号码牌,当到你的时候会用大喇叭呼叫

2.5异步IO

异步IO模型才是理想的IO模型。

内核会帮助用户线程完成一切操作,用户线程只需要发起read请求,然后内核会立即返回,表示请求成功发起,用户线程可以去做任何事。

它与信号驱动IO最大的不同就是信号驱动IO仍需要用户线程再次调用IO函数进行具体的读写。而收到信号的时候表示IO操作已经完成了,不需要再调用IO了

它有点像现在餐厅比较高端的手机排队系统,会在手机生成一个号码,并且帮助你完成缴费和登记等事项,只要手机提醒你就代表已经可以进去吃饭了

3、NIO包

3.1 JAVA NIO

NIO包括三个部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流。

而NIO基于 Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区 中,或者从缓冲区写入到通道中。

Selector(选择区)用于监听多个通道的事件(比如:连接打开, 数据到达)。因此,单个线程可以监听多个数据通道。

NIO和传统IO之间第一个大的区别是,IO是面向流的,NIO是面向缓冲区的。

3.2 NIO的缓冲区

NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式高伸缩性网络

3.3 NIO的非阻塞

Java NIO的非阻塞模式,意味着NIO无论是读还是写,都可以不用保持线程阻塞,如果目前没有数据可用时,就什么都不会获取,写操作也是一样,一个线程请求写入一些数据到某通道,但不需要等待它 完全写入,这个线程同时可以去做别的事情。

3.4 Channel

​ 首先说一下Channel,国内大多翻译成“通道”。

​ Channel和IO中的Stream(流)是差不多一个 等级的。只不过Stream是单向的,譬如:InputStream, OutputStream,而Channel是双向 的,既可以用来进行读操作,又可以用来进行写操作。

​ NIO中的Channel的主要实现有:

  1. FileChannel

  2. DatagramChannel

  3. SocketChannel

  4. ServerSocketChannel

    ​ 这里看名字就可以猜出个所以然来:分别可以对应文件IO、UDP和TCP(Server和Client)。 下面演示的案例基本上就是围绕这4个类型的Channel进行陈述的。

3.5 Buffer

​ Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。

​ 上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送 数据时,必须先将数据存入Buffer中,然后将Buffer中的内容写入通道。服务端这边接收数据必 须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。

​ 在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用的Buffer的子类有: ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、 ShortBuffe

3.6 Selector

​ Selector类是NIO的核心类.

Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。

​ 这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。

​ 这样使得只有在连接真正有读写事件发生时,才会调用 函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护 多个线程,并且避免了多线程之间的上下文切换导致的开销。

4、IO包

4.1文件

File类构造方法

//根据 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)
方法 说明
boolean canExecute() 测试应用程序是否可以执行此抽象路径名表示的文件
boolean canRead() 测试应用程序是否可以读取此抽象路径名表示的文件
boolean canWrite() 测试应用程序是否可以修改此抽象路径名表示的文件
int compareTo(File pathname) 按字母顺序比较两个抽象路径名
boolean createNewFile() 当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件
static File createTempFile(String prefix, String suffix) 在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称
static File createTempFile(String prefix, String suffix, File directory) 在指定目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称
boolean delete() 删除此抽象路径名表示的文件或目录
void deleteOnExit() 在虚拟机终止时,请求删除此抽象路径名表示的文件或目录
boolean equals(Object obj) 测试此抽象路径名与给定对象是否相等
boolean exists() 测试此抽象路径名表示的文件或目录是否存在
File getAbsoluteFile() 返回此抽象路径名的绝对路径名形式
String getAbsolutePath() 返回此抽象路径名的绝对路径名字符串
File getCanonicalFile() 返回此抽象路径名的规范形式
String getCanonicalPath() 返回此抽象路径名的规范路径名字符串
long getFreeSpace() 返回此抽象路径名指定的分区中未分配的字节数
String getName() 返回由此抽象路径名表示的文件或目录的名称
String getParent() 返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null
File getParentFile() 返回此抽象路径名父目录的抽象路径名;如果此路径名没有指定父目录,则返回 null
String getPath() 将此抽象路径名转换为一个路径名字符串
long getTotalSpace() 返回此抽象路径名指定的分区大小
long getUsableSpace() 返回此抽象路径名指定的分区上可用于此虚拟机的字节数
int hashCode() 计算此抽象路径名的哈希码
boolean isAbsolute() 测试此抽象路径名是否为绝对路径名
boolean isDirectory() 测试此抽象路径名表示的文件是否是一个目录
boolean isFile() 测试此抽象路径名表示的文件是否是一个标准文件
boolean isHidden() 测试此抽象路径名指定的文件是否是一个隐藏文件
long lastModified() 返回此抽象路径名表示的文件最后一次被修改的时间
long length() 返回由此抽象路径名表示的文件的长度
String[] list() 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录
String[] list(FilenameFilter filter) 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录
File[] listFiles() 返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件
File[] listFiles(FileFilter filter) 返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录
File[] listFiles(FilenameFilter filter) 返回抽象路径名数组,这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录
static File[] listRoots() 列出可用的文件系统根
boolean mkdir() 创建此抽象路径名指定的目录
boolean mkdirs() 创建此抽象路径名指定的目录,包括所有必需但不存在的父目录
boolean renameTo(File dest) 重新命名此抽象路径名表示的文件
boolean setExecutable(boolean executable) 设置此抽象路径名所有者执行权限的一个便捷方法
boolean setExecutable(boolean executable, boolean ownerOnly) 设置此抽象路径名的所有者或所有用户的执行权限
boolean setLastModified(long time) 设置此抽象路径名指定的文件或目录的最后一次修改时间
boolean setReadable(boolean readable) 设置此抽象路径名所有者读权限的一个便捷方法
boolean setReadable(boolean readable, boolean ownerOnly) 设置此抽象路径名的所有者或所有用户的读权限
boolean setReadOnly() 标记此抽象路径名指定的文件或目录,从而只能对其进行读操作
boolean setWritable(boolean writable) 设置此抽象路径名所有者写权限的一个便捷方法
boolean setWritable(boolean writable, boolean ownerOnly) 设置此抽象路径名的所有者或所有用户的写权限
String toString() 返回此抽象路径名的路径名字符串
URI toURI() 构造一个表示此抽象路径名的 file: URI

4.2文件流

  1. 用于读写本地文件系统中的文件:FileInputStream 和 FileOutputStream
  2. 描述本地文件系统中的文件或目录:File、FileDescriptor 和 FilenameFilter
  3. 提供对本地文件系统中文件的随机访问支持:RandomAccessFile

4.2.1FileInputStream 和 FileOutputStream

FileInputStream 类用于打开一个输入文件,若要打开的文件不存在,则会产生异常 FileNotFoundException,这是一个非运行时异常,必须捕获或声明抛弃;

//文件流的构造方法
//打开一个以 f 描述的文件作为输入
FileInputStream(File f)

//打开一个文件路径名为 name 的文件作为输入
FileInputStream(String name)

//创建一个以 f 描述的文件作为输出
//如果文件存在,则其内容被清空
FileOutputStream(File f)

//创建一个文件路径名为 name 的文件作为输出
//文件如果已经存在,则其内容被清空
FileOutputStream(String name)

//创建一个文件路径名为 name 的文件作为输出
//文件如果已经存在,则在该输出上输出的内容被接到原有内容之后
FileOutputStream(String name, boolean append)

从硬盘读入的文件必须存在,否则会报异常。

反过来,读出的文件不用强制存在,系统会创建

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Test {

    public static void main(String[] args) {
        try {
            //inFile 作为输入流的数据文件必须存在,否则抛出异常
            File inFile = new File("/home/project/file1.txt");

            //file2.txt没有,系统可以创建
            File outFile = new File("file2.txt");
            FileInputStream fis = new FileInputStream(inFile);
            FileOutputStream fos = new FileOutputStream(outFile);
            int c;
            //read(byte[])方法的返回值为实际读取的字节数
            //read(byte[])方法是一次读取一格byte数组,且满了就覆盖重读
            //read方法返回值为读取字节的ascii码
            while((c = fis.read()) != -1){
                fos.write(c);
            }
            //打开了文件一定要记着关,释放系统资源
            fis.close();
            fos.close();
        }catch(FileNotFoundException e) {
            System.out.println("FileStreamsTest:" + e);
        }catch(IOException e){
            System.err.println("FileStreamTest:" + e);
        }
    }
}

如果文件内容保存的是字符信息,如 txt 文件等,还可以使用 FileReader 来读取文件内容。

代码示例:

FileReader file = new FileReader("/home/project/shiyanlou.txt");
//声明一个文件输入流file,并指明该文件在系统中的路径以方便定位

int data = 0;
//声明一个整型变量用于存放读取的数据

while((data=file.read())!=-1){
    //在while循环中使用read()方法持续读取file,数据赋到data中
    //如果读取失败或者结束,则将返回-1,这个特殊的返回值可以作为读取结束的标识

    System.out.print((char)data);
    //输出读取到数据
}

file.close();
//一定要记得读取结束后要关闭文件

4.2.2随机读写

类 RandomAccessFile 则允许文件内容同时完成读和写操作,它直接继承 Object,并且同时实现了接口 DataInput 和 DataOutput。

RandomAccessFile 提供了支持随机文件操作的方法:

  1. readXXX() 或者 writeXXX(): 如 readInt(), readLine(), writeChar(), writeDouble() 等。
  2. int skipBytes(int n): 将指针向下移动若干字节。
  3. int length(): 返回文件长度。
  4. long getFilePointer(): 返回指针当前位置。
  5. void seek(long pos): 将指针调用所需位置。

在生成一个随机文件对象时,除了要指明文件对象和文件名之外,还需要指明访问文件的模式。

来看看 RandomAccessFile 的构造方法:

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

mode 的取值:

  • r: 只读,任何写操作都讲抛出 IOException
  • rw: 读写,文件不存在时会创建该文件,文件存在时,原文件内容不变,通过写操作改变文件内容。
  • rws: 打开以便读取和写入,对于 "rw",还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
  • rwd: 打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备。

  • 下载文件 randomAccess.file

  • 从偏移量为 10 的位置开始读取文件 randomAccess.file 的内容;
  • 输出文件内容(以字符串形式,不能直接输出字节内容)。
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;

public class RandomFile {
    public static void main(String[] args) {
        RandomAccessFile file;
        try {
            File d = new File("/home/project/randomAccess.file");//新建文件实例
            file = new RandomAccessFile(d, "rw");//初始化随机读取实例,以读写模式传入
            file.seek(10);//将文件指针漂移10
            byte[] b = new byte[(int) file.length()-10];//读取文件长度,并减少10赋,以此为byte数组长度
            file.read(b);//随机读取读取文件
            System.out.println(new String(b));//将byte数组变为字符串输出
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

byte数组可以直接作为String的参数。

4.2.3文件操作

4.2.3.1拷贝

可以使用 Files 工具类的 copy(Path source,Path target,CopyOption... options) 拷贝文件或者目录。

如果目标文件存在,那么赋值将失败,除非我们在 options 中指定了 REPLACE_EXISTING 属性,当该命令复制目录时,如果目录中已经有了文件,目录中的文件将不会被复制。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

public class CopyDemo {
    public static void main(String[] args) {
        try {
            //被拷贝的文件一定要存在 否则会抛出异常  这里的1.txt一定要存在
            Files.copy(Paths.get("/home/project/1.txt"), Paths.get("/home/project/2.txt"), StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
4.2.3.2移动和重命名

Files 类的 move(Path, Path, CopyOption... options) 方法移动文件或者目录,同样目标目录存在,那么比如使用REPLACE_EXISTING

options 参数支持 StandardCopyOption 的以下枚举:

  • REPLACE_EXISTING:即使目标文件已存在,也执行移动。如果目标是符号链接,则替换符号链接,但它指向的内容不受影响。
  • ATOMIC_MOVE:将移动作为原子文件操作执行。如果文件系统不支持原子移动,则抛出异常。使用,ATOMIC_MOVE 您可以将文件移动到目录中,并保证观察目录的任何进程都可以访问完整的文件。

move 方法除了可以移动之外,也可以用与重命名。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

public class MoveDemo {
    public static void main(String[] args) {
        try {
            //将1.txt 重命名为3.txt 如果只需要移动到不同的目录,文件名不变即可
            Files.move(Paths.get("/home/project/1.txt"), Paths.get("/home/project/3.txt"), StandardCopyOption.REPLACE_EXISTING);

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

    }
}
4.2.3.3删除

可以通过 Files 的 delete(Path path) 方法或者 deleteIfExists(Path path) 方法删除文件。

编程实例:/home/project/ 目录下新建源代码文件 DeleteDemo.java

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class DeleteDemo {
    public static void main(String[] args) {
        try {
            //删除文件,文件必须存在,否则抛出异常
            Files.delete(Paths.get("/home/project/3.txt"));
            //删除文件,返回是否删除成功 即使文件不存在,也不会保存,直接返回false
            System.out.println(Files.deleteIfExists(Paths.get("/home/project/3.txt")));
            //或者使用File类的delete方法
            File file = new File("/home/project/4.txt");
            System.out.println(file.delete());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
4.2.3.4文件属性

Java 使用 File 类表示文件或者目录,可以通过 File 类获取文件或者目录的相关属性。

编程实例:/home/project/ 目录下新建源代码文件FileInfo.java

import java.io.File;
import java.util.Arrays;

public class FileInfo {
    public static void main(String[] args) {
        File file = new File("/home/project");
        System.out.println("文件或者目录名:" + file.getName());
        System.out.println("绝对路径:" + file.getAbsolutePath());
        System.out.println("父目录:" + file.getParent());
        System.out.println("文件路径:" + file.getPath());
        //判断文件是否是目录
        if (file.isDirectory()) {
            //打印目录中的文件
            Arrays.stream(file.list()).forEach(System.out::println);
        }
        System.out.println("是否隐藏:" + file.isHidden());
        System.out.println("是否存在:" + file.exists());
    }
}
4.2.3.5 目录读取

Java 中读取目录中的文件可以直接使用 listFiles() 方法读取,但是也只能读取当前目录中的文件,如果当前目录中还有二级目录如何解决呢?三级目录呢?接下来将使用 Java 读取当前目录和子目录中的所有文件。

编程实战

/home/project/ 目录下新建源代码文件 ReadDir.java

import java.io.File;
public class ReadDir {
    public static void main(String[] args) {
        readDir(new File("/home"));
    }

    static void readDir(File file) {
        if (file == null) {
            return;
        }
        //如果当前file是目录
        if (file.isDirectory()) {
            File[] files;
            //如果目录不为空
            if ((files = file.listFiles()) != null) {
                for (File file1 : files) {
                    //递归读取目录内容
                    readDir(file1);
                }
            }
        } else {
            //如果不是目录 直接输出文件名
            System.out.println(file.getName());
        }
    }
}

练习:输出目录树

import java.io.File;
import java.io.IOException;

public class PrintDirTree {

    public static void main(String[] args) {
        printDirTree(new File("目录的地址"), "");
    }

    public static void printDirTree(File file, String s) {
        if (file.isDirectory()) {
            File[] files;
            System.out.println(s + file.getName());
            if ((files = file.listFiles()) != null) {
                s = s + "   ";
                for (File file1 : files) {
                    printDirTree(file1, s);
                }
            }
        } else {
            System.out.println(s + file.getName());
        }
    }
}
/*
1.第一步先接收目录
2.判断是否是目录
3.如果是目录,输出目录名,并循环递归目录内对象
4.如果不是目录,直接输出名字
5.传递字符串是为了显示出目录树的感觉,只要递归一层就加个打空格,显示的时候比较好看
*/


醉后不知天在水,满船清梦压星河