Java高级知识 — IO、NIO、AIO

文章目录

  • 一、Java Socket
  • 二、Java IO流基础
      • 2.1 IO流的概念
      • 2.2 IO流的分类
        • 1)输入流和输出流
        • 2)字节流和字符流
        • 3)节点流和处理流
      • 2.3 常用IO流介绍
        • 1) 缓冲流
        • 2) 转换流
        • 3) 字节数组流(内存流)
        • 4) 数据处理流
        • 5) 序列化流
      • 2.4 流的操作步骤
        • 1) 字节输入流读取文件内容
        • 2) 字节输出流写入文件内容
        • 3) 流关闭的顺序
      • 2.5 File类
  • 三、BIO、NIO、AIO
    • 3.1 BIO
      • 3.1.1 传统 BIO
      • 3.1.2 伪异步 IO
    • 3.2 NIO
      • 3.2.1 NIO的特性/NIO与IO区别
        • 1) Non-blocking IO (非阻塞IO)
        • 2) Buffer (缓冲区)
        • 3) Channel (通道)
        • 4) Selectors (选择器)
      • 3.2.2 NIO 读写数据方式
      • 3.2.3 NIO 核心组件
  • 3.3 AIO
    • 3.3.1 NIO和AIO区别

一、Java Socket

参考:JAVA Socket详解

二、Java IO流基础

参考:
JavaIO流详解
Java IO流详解(一)——预备知识
Java IO流详解(二)——IO流的框架体系
IO流常见面试题

2.1 IO流的概念

  Java的IO流是实现输入/输出的基础,它可以方便地实现数据的输入/输出操作,在Java中把不同的输入/输出源抽象表述为"流"。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
  流有输入和输出,输入时是流从数据源流向程序。输出时是流从程序传向数据源,而数据源可以是内存,文件,网络或程序等。

2.2 IO流的分类

1)输入流和输出流

按照数据流向或者说是功能来划分

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

2)字节流和字符流

按照处理的数据单元来划分

字节流和字符流操作的方式基本上完全相同,操作的数据单元不同 。
字节流:操作的是8位的字节,InputStream/OutputStream作为字节流的基类。
字符流:操作的是16位的字符,Reader/Writer 作为字符流的基类。

字符流的由来
  因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。
字节流和字符流的区别
   · 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
   · 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。
只要是处理纯文本数据,就优先考虑使用字符流。 除此之外都使用字节流。

3)节点流和处理流

按照角色进行划分

节点流:可以直接从/向外部设备读取/写入数据的流,称之为节点流,节点流也被称之为低级流。
处理流(包装流、过滤流):对于已经存在的流进行了连接和封装,扩展了原来的读/写的功能。处理流也被称之为高级流。

//节点流,直接传入的参数是IO设备
FileInputStream fis = new FileInputStream("test.txt");
//处理流,直接传入的参数是流对象
BufferedInputStream bis = new BufferedInputStream(fis);

根据流的流向以及操作的数据单元不同,将流分为了四种类型,每种类型对应一种抽象基类
这四种抽象基类分别为:InputStream、OutputStream、Reader以及Writer。
四种基类下,对应不同的实现类,具有不同的特性。在这些实现类中,又可以分为节点流和处理流。
需要注意,过滤流的四个抽象类( “Filter” 开头)也分别继承自四大基类。

下图是各类之间的继承关系:
Java高级知识 — IO、NIO、AIO_第1张图片
下图标出了节点流和处理流:
Java高级知识 — IO、NIO、AIO_第2张图片

2.3 常用IO流介绍

下图是常用IO流之间的拓扑关系:
Java高级知识 — IO、NIO、AIO_第3张图片
常用流使用(含代码)参考这篇文章:Java 常用IO流操作详解

以下是常用的几种流的介绍:

1) 缓冲流

缓冲字节流 BufferedInputStream / BufferedOutputStream
缓冲字符流 BufferedReader / BufferedWriter

缓冲流的步骤

缓冲流内部包含一个缓冲区域,默认8kb

  • 每一次程序调用read方法其实都是从缓冲区域当中读取内容;
  • 如果读取失败就说明缓冲区域当中没有内容,那么就从数据源当中读取内容;
  • 然后会尽可能读取更多的字节放入到缓冲区域当中;
  • 最后缓冲区域当中的内容,会全部返回给程序。

简单地说:
 没有缓存区,那么每read一次,就会发送一次IO操作;
 有缓存区,第一次read时,会一下读取x个字节放入缓存区,然后后续的read都会从缓存中读取,当read到缓存区末尾时,会再次读取x个字节放入缓存区。

缓冲流的好处

从缓冲区读取数据会比直接从数据源读取数据的速度快,效率也更高,性能更好。

使用示例:

/*
	BufferedInputStream构造方法:
		// 创建一个 BufferedInputStream并保存其参数,即输入流in,以便将来使用。
		BufferedInputStream(InputStream in)
		创建具有指定缓冲区大小的 BufferedInputStream并保存其参数,即输入流in以便将来使用
		BufferedInputStream(InputStream in, int size)
*/
 	InputStream in = new FileInputStream("test.txt");
	// 字节缓存流
	BufferedInputStream bis = new BufferedInputStream(in);
	byte[] bs = new byte[20];
	int len = 0;
	while ((len = bis.read(bs)) != -1) {
	    System.out.print(new String(bs, 0, len));
	    // do something
	}
	// 关闭流
	bis.close();
/*
	BufferedOutputStream 构造方法:
	     // 创建一个新的缓冲输出流,以将数据写入指定的底层输出流
	     BufferedOutputStream(OutputStream out)
	     // 创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流
	     BufferedOutputStream(OutputStream out, int size)
	
	常用方法:
	     // 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此缓冲的输出流
	     void write(byte[] b, int off, int len)
	     // 将指定的字节写入此缓冲的输出流
	     void write(int b)
	     // 刷新此缓冲的输出流
	     void flush()
*/
     BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("test.txt", true));
     // 输出换行符
     bos.write("\r\n".getBytes());
     // 输出内容
     bos.write("Hello Android".getBytes());
     // 刷新此缓冲的输出流
     bos.flush();
     // 关闭流
     bos.close();

2) 转换流

转换流作用:把字节流转换成字符流,可以解决出现的因为编码集和解码集造成的乱码问题。
InputStreamReader: 编码:字符 —— 设置编码字符集 ——> 二进制
OutputStreamWriter: 解码:二进制 — 设置解码字符集 ——> 字符

在处理文件时,如果文件的字符格式编译器处理格式不一样时,会出现乱码问题。比如文件字符格式GBK,而编译器是UTF-8格式,那么就会产生该问题。

出现乱码问题的原因:

  • 编码和解码字符集不一致造成了乱码
  • 字节的缺失,长度的丢失

大部分情况下,出现乱码问题是因为中国汉字,因为中国汉字在不同的字符编码当中占据的字节数不相同,但是都占据多个字节。
而英文字母没有这个问题,因为英文字母在所有的字符编码当中都占据一个字节。
InputStreamReader可以防止文件使用字符输入流处理时出现乱码问题。

/*
	 InputStreamReader 构造方法:
	    // 创建一个使用默认字符集的 InputStreamReader
	    InputStreamReader(InputStream in)
	    // 创建使用给定字符集的 InputStreamReader
	    InputStreamReader(InputStream in, Charset cs)
	    // 创建使用给定字符集解码器的 InputStreamReader
	    InputStreamReader(InputStream in, CharsetDecoder dec)
	    // 创建使用指定字符集的 InputStreamReader
	    InputStreamReader(InputStream in, String charsetName)
	 特有方法:
	    //返回此流使用的字符编码的名称 
	    String getEncoding() 
*/
	//使用默认编码        
	 InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"));
	 int len;
	 while ((len = reader.read()) != -1) {
	     System.out.print((char) len);//爱生活,爱Android
	
	 }
	 reader.close();
	
	  //指定编码 
	 InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"),"utf-8");
	 int len;
	 while ((len = reader.read()) != -1) {
	     System.out.print((char) len);//????????Android
	 }
	 reader.close();
/*
	OutputStreamWriter 构造方法:
	   // 创建使用默认字符编码的 OutputStreamWriter
	   OutputStreamWriter(OutputStream out)
	   // 创建使用给定字符集的 OutputStreamWriter
	   OutputStreamWriter(OutputStream out, Charset cs)
	   // 创建使用给定字符集编码器的 OutputStreamWriter
	   OutputStreamWriter(OutputStream out, CharsetEncoder enc)
	   // 创建使用指定字符集的 OutputStreamWriter
	   OutputStreamWriter(OutputStream out, String charsetName)
	特有方法:
	   //返回此流使用的字符编码的名称 
	   String getEncoding() 
*/

3) 字节数组流(内存流)

字节数组流(内存流):ByteArrayInputStream / ByteArrayOutputStream
  因为内存输出流当中又新增了方法,不能使用多态,不能够让父类的引用指向子类的对象。
作用:
  可以在循环当中把所有的数据存放到统一的容器当中,然后在循环结束之后可以把容器当中所有的内容一起取出来。
注意事项:
  内存流属于内存当中的资源,所以数据量不要过大,如果太大,会造成内存溢出的错误。

4) 数据处理流

数据处理流:DataOutputStream / DataInputStream
特点:既能够保存数据本身,又能够保存数据类型(基本数据类型+String)

5) 序列化流

对象序列化:将对象转换成字节序列的过程。序列化流 (输出流): ObjectOutputStream writeObject()
对象反序列化:将字节序列恢复为对象的过程。反序列化流 (输入流): ObjectInputStream readObject()
序列化流的作用:保留对象(引用数据类型数据的)类型+数据。

注意事项:

  • 先序列化然后再反序列化,而且反序列化的顺序必须和序列化的顺序保持一致。
  • 并不是所有的对象都能够被序列化。只有实现了Serializable接口的类的对象才能够被序列化。 对象当中并不是所有的属性都能够被序列化。

对象序列化的主要用途:

  • 把对象转换成字节序列,保存到硬盘当中,持久化存储,通常保存为文件。
  • 在网络上传递的是对象的字节序列

对象序列化的步骤:
  1.创建对象输出流,在构造方法当中可以包含其他输出节点流,如文件输出流;
  2.把对象通过writeObject的方式写入。

对象反序列化的步骤:
  1.创建对象输入流,在构造方法当中可以包含其他的输入节点流,如文件输入流;
  2.通过readObject()方法读取对象。

serialVersionUID: 序列化版本id
作用:从字面角度看,就是序列化版本号。凡是实现了Serializable接口的类,都会有一个默认的静态的序列化标识。

  • 类在不同的版本之间,可以解决序列化兼容问题,如果之前版本当中在文件中保存对象,那么版本升级后,如果序列化id一致,我们可以认为文件中的对象依然是此类的对象。
  • 如果类在不同的版本之间不希望兼容,但是还希望类的对象能够序列,那么就在不同版本中使用不同的序列化id。

transient修饰符: 当类中有属性不想被序列化,那么就使用这个修饰符修饰。

2.4 流的操作步骤

1) 字节输入流读取文件内容

操作步骤:

  • 1.创建流对象
  • 2.创建一个缓存字节的容器数组
  • 3.定义一个变量,保存实际读取的字节数
  • 4.循环读取数据
  • 5.操作保存数据的数组
  • 6.关闭流
public class FileInputStreamDemo01 {
    public static void main(String[] args) {
        File file = new File("C:\\Users\\Administrator\\Desktop\\2020-05-22\\2020-05-22.java");
//		1.创建流对象
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
//			2.创建一个缓存字节的容器数组
            byte[]buf = new byte[1024];
//			3.定义一个变量,保存实际读取的字节数
            int hasRead = 0;
//			4.循环读取数据
//          while (true) {
//              hasRead = fis.read(buf);
//              if (hasRead==-1) {
//                  break;
//              }
//				5.操作保存数据的数组
//              String msg = new String(buf, 0,hasRead);
//              System.out.print(msg);
//          }
//			第4、5步骤可以合并为以下代码:
            while ((hasRead = fis.read(buf))!=-1) {
                String str = new String(buf,0,hasRead);
                System.out.print(str);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
//			6.关闭流
            if (fis!=null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2) 字节输出流写入文件内容

操作步骤:

  • 1.选择流:创建流对象
  • 2.准备数据源,把数据源转换成字节数组类型
  • 3.通过流向文件当中写入数据
  • 4.刷新流
  • 5.关闭流
public class FileOutputStreamDemo01 {
    public static void main(String[] args) {
//      1.选择流:创建流对象
        FileOutputStream fos =null;
        try {
            fos = new FileOutputStream(new File("c:\\read.txt"),true);
//          2.准备数据源,把数据源转换成字节数组类型
            String msg = "\n野径云俱黑,江船火独明。\n晓看红湿处,花丛锦官城。";
            byte[] data = msg.getBytes();
//          3.通过流向文件当中写入数据
            fos.write(data, 0, data.length);
//          4.刷新流
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (fos!=null) {
//              5.关闭流
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3) 流关闭的顺序

依然是参考:JAVA I/O流 字符流和字节流、节点流和处理流(包装流、过滤流)、缓冲流

当然完全可以只关闭处理流,不用关闭节点流。处理流关闭的时候,会调用其处理的节点流的关闭方法
一般情况下是:

  • 先打开的后关闭,后打开的先关闭;
  • 另一种情况:看依赖关系,如果流a依赖流b,应该先关闭流a,再关闭流b
  • 例如处理流a依赖节点流b,应该先关闭处理流a,再关闭节点流b
  • 如果将节点流关闭以后再关闭处理流,会抛出IO异常;

2.5 File类

File类的由来:File类的出现弥补了IO流的不足,IO只能够操作数据,但是不能够对文件的信息做操作,操作文件必须使用File类。
功能: 可以将文件或者文件夹在程序当中分装成对象。
方便对于文件或者文件夹当中的属性信息进行操作。
File类通常通过构造函数作为参数传递到流的对象当中。

File类的常用方法介绍:

  1. 构造方法

File(String pathname):这个构造可以将已存在的或者不存在的文件或者文件夹封装成File的对象,pathname即文件的的路径。
File(File parent, String child):parent为child文件所在的路径。

	File file = new File (“c:\java”,”jre7”);
	//file的toString方法重写了,封装的地址是什么就打印什么
	//’/’和’\’都是目录分隔符,在其他系统当中目录分割符可能发生变化,这个写法不利于跨平台操作
	//最好使用File当中提供的字段separator进行分割。
  1. 创建文件相关函数

createNewFile():创建相关文件。并返回布尔值
createTemFile():在默认临时文件目录当中创建一个空文件,程序运行结束后就不存在了。
mkdirs():创建目录,如果你写的目录的父目录不存在。他会帮你创建好。

  1. 删除文件相关函数

delete():删除空目录或文件(ps只能是空目录)
deleteOnExit():在虚拟机终止时删除文件。

  1. 判断

exists(): 判断文件或者文件夹是否存在。
canExecute(): 判断文件是否可执行,和操作系统相关。
canRead(): 判断文件是否可读
canWrite(): 判断文件是否可写
equals(Object obj): 测试此抽象路径名与给定对象是否相等。
isAbsolute(): 测试此抽象路径名是否为绝对路径名。
isDirectory(): 判断file对象是否表示文件夹。
isFile(): 判断file对象是否表示文件
isHidden(): 判断file对象是否是隐藏文件

  1. 获取file对象属性信息的方法

getAbsoluteFile(): 返回此抽象路径名的绝对路径名形式。
getAbsolutePath(): 返回此抽象路径名的绝对路径名字符串。
getCanonicalFile(): 返回此抽象路径名的规范形式。
getCanonicalPath(): 返回此抽象路径名的规范路径名字符串。
getPath(): 将此抽象路径名转换为一个路径名字符串。
getName(): 返回由此抽象路径名表示的文件或目录的名称。
getParent(): 返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null。
getParentFile(): 返回此抽象路径名父目录的抽象路径名;如果此路径名没有指定父目录,则返回 null。
getTotalSpace(): 返回指定路径的全部空间的字节数 getFreeSpace(): 返回此抽象路径名指定的分区中未分配的字节数。
getUsableSpace(): 返回此抽象路径名指定的分区上可用于此虚拟机的字节数。
renameTo(File dest): 重新命名此抽象路径名表示的文件。

  1. 设置文件信息的方法

setExecutable(boolean executable): 设置文件可执行的方法
setLastModified(long time): 设置此抽象路径名指定的文件或目录的最后一次修改时间。
setReadable(boolean readable): 设置文件是否可读
setReadOnly(): 设置文件是否只读
setWritable(boolean writable): 设置文件是否可写

  1. 获取文件的常规信息的方法:

lastModified(): 获取文件最后一次被修改的时间。
length(): 返回由此抽象路径名表示的文件的长度。

  1. 操作文件夹的相关方法

list(): 把文件夹当中包含的目录和文件都存放到字符串数组当中。
listFiles(): 列举文件夹当中包含的目录和文件,存放到File数组当中。
listRoots(): 列出可用的文件系统根。

  1. 文件过滤器: 过滤不满足条件的文件对象
class MyFilter implements FilenameFilter{
    @Override
    public boolean accept(File dir, String name) {
        //判断是否为.java 结尾或者一个目录
        return name.endsWith(".java")||new File(name).isDirectory();
    }
}
public class FileNameFilterTest {
    public static void main(String[] args) {
        File file=new File("."); //获取当前项目工作目录下的所有文件
        String[] nameList=file.list(new MyFilter());
        for(String str : nameList){
            System.out.println(str);
        }
    }
}

三、BIO、NIO、AIO

参考文章:
Java面试常考的 BIO,NIO,AIO 总结
BIO、NIO、AIO 区别和应用场景

3.1 BIO

3.1.1 传统 BIO

3.1.2 伪异步 IO

3.2 NIO

3.2.1 NIO的特性/NIO与IO区别

1) Non-blocking IO (非阻塞IO)

2) Buffer (缓冲区)

3) Channel (通道)

4) Selectors (选择器)

3.2.2 NIO 读写数据方式

3.2.3 NIO 核心组件

3.3 AIO

3.3.1 NIO和AIO区别

你可能感兴趣的:(Java)