Java难点重构-NIO

简介: Java NIO 是从Java 1.4版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。

Java NIO 是从Java 1.4版本开始引入的一个新的 IO API,可以替代标准的 Java IO API。NIO与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向 缓冲区 的,基于 通道 的IO 操作,至于什么是缓冲区,什么是通道,接下来我将会用大白话一一说明。总之,NIO 就是以更高效的方式进行文件的读写操作


在学习本篇之前,首先你要对 IO 有一定的了解。当然不了解的话,也可以看得哈哈,我会说的很通俗易懂。


我们先看看 Java NIO 与 IO的主要区别:

image.png


上面的什么 面向缓冲区,又什么非阻塞IO,又是选择器的,这些到底都啥啊,拍桌子。。。


下面我会在本篇中对上面出现的概念及盲点进行解析。

IO与NIO的区别

首先我们看看他们的区别

为什么说IO是面向流,那流又是什么呢?



我们先看上面的图片。

我们在学IO的时候,肯定都听过这样的例子,IO流就相当于一条管道,它里面所有的操作都是单向的。如果要把文件中的数据拿到程序中,需要建立一条通道。想把程序中的数据存到文件中也需要建立一条通道。所以我们成io流是单向的。因为io管道里实际面对的是字节的流动,所以我们称io流为面向流。

那什么说NIO是面向缓冲区呢?



你可以这样想象,通道就相当于与一条道路,缓冲区相当于出租车,出租车上拉的是乘客,出租车可以上乘客也可以下乘客。回到NIO 上面,通道就是一条道路,他负责提供行驶的绝对条件,即就是有路啊,这样出租车才能基本出行,而缓冲区在这里是出租车,出租车里面坐的是人,出租车负责将乘客送到它要去的地方,当然,出租车不受限制,他可以在任意地方。所以简而言之,通道(Channel)负责传输,Buffer 负责存储。


在 NIO 里面,有两个特别重要的东西,那就是 通道(Channel)缓冲区(Buffer)


Java NIO系统的核心在于:通道 和缓冲区。通道表示 打开IO 设备(例如:文件,套接字)的链接。若需要使用 NIO 系统,需要获取用于链接 IO 的设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。


缓冲区(Buffer)

/*缓冲区(Buffer):在Java Nio中负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据
*
* 根据数据类型不同(boolean 除外),提供了相应类型的缓冲区
* ByteBuffer
* CharBuffer
* ...
*
* 上述缓冲区的管理方式几乎一致,都是通过allocate() 获取缓冲区
*
* 2/缓冲区存取数据的两个核心方法:
* put(): 存入数据到缓冲区中
* get():获取缓冲区中的数据
*
* 4.缓冲区中的4个核心属性:
* capacity:  容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变,(底层就是数组)
* limit:界限,表示缓冲区中可以操作数据的大小。(limit 后面的数据不能进行读写)
* position:位置,表示缓冲区中正在操作数据的位置。
*
* 5.直接缓冲区与非直接缓冲区
* 非直接缓冲区:通过 allocate()方法分配缓冲区,将缓冲区建立在 JVM的内存中
* 直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立物理内存中。
*
*  mark: 标记,表示记录当前 postion 的位置,可以通过 reset() 恢复到mark 位置
* position<=limit<=capacity
* */
public class Test {
    public static void main(String[] args) throws IOException {
        test2();
        test3();
    }
    private static void test1() {
        String str="Petterp";
        //1.分配一个指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        System.out.println("____________allocate_________");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //2.利用 put() 存入数据到缓冲区中
        buf.put(str.getBytes());
        System.out.println("____________put_________");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //3.切换读取数据模式
        buf.flip();
        System.out.println("____________flip_________");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //4.利用get() 读取缓冲区的数据
        byte[] dst=new byte[buf.limit()];
        buf.get(dst);
        System.out.println(new String(dst,0,dst.length));
        System.out.println("____________get_________");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //5.rewind()_可重复读数据
        buf.rewind();
        System.out.println("____________rewind_________");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
        //6.clear():  清空缓冲区.(缓冲区数据还在,但是处于"被遗忘"状态
        // 因为limit这些值全回到了初始状态,所以无法正确读取数据。)
        buf.clear();
        System.out.println("____________clear_________");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
    }
    private static void test2(){
        ByteBuffer buf =ByteBuffer.allocate(1024);
        String res="Petterp";
        buf.put(res.getBytes());
        buf.flip();
        //记录指针位置为0
        buf.mark();
        System.out.println("____________mark记录position位置_________");
        System.out.println(buf.position());
        byte[] bytes = new byte[buf.limit()];
        buf.get(bytes,0,3);
        System.out.println("打印get到的数据"+new String(bytes,0,bytes.length));
        System.out.println("____________get之后position_________");
        System.out.println(buf.position());
        //会到记录的指针位置
        buf.reset();
        System.out.println("____________reset之后position_________");
        System.out.println(buf.position());
        System.out.println("____________remaining判断可操作数据长度_________");
        //判断缓冲区是否还有剩余数据
        if (buf.hasRemaining()){
            //获取缓冲区中可以操作的数据长度
            System.out.println(buf.remaining());
        }
    }
    private  static void test3(){
        ByteBuffer buf=ByteBuffer.allocateDirect(1024);
        //判断是否是直接缓存区
        System.out.println(buf.isDirect());
    }
}

非直接缓冲区与缓冲区的区别

非直接缓冲区在,建立在JVM内存中,实际读写数据时,需要在 OS 和JVM之间进行数据拷贝。

网络异常,图片无法展示
|


为什么不直接让磁盘控制器把数据送到用户控件的缓冲区呢?

因为我们的硬件通常不能直接访问用户内存空间。如果有一个程序需要读写磁盘空间,出于系统安全考虑,磁盘中的文件无法直接传输到我们程序中,它必须经过系统的内核地址空间的缓存中,然后将内核地址空间数据复制到用户地址空间,这样数据才可以传输到我们的应用程序。

内存映射空间

直接缓冲区,缓冲区建立在受操作系统管理的物理内存中,OS和JVM直接通过这块物理内存进行交互,没有了中间的拷贝环节


但是直接缓冲区也有很多弊端:

  • 内存消耗大(分配与销毁不易控制)
  • 如果当Java 程序将数据写到物理内存中后,这个时候我们就无法管理这块内存,只能由系统进行控制。
//1.利用通道完成文件的复制(非直接缓冲区)
public static void test1(){
    try{
        long l = System.currentTimeMillis();
        fis = new FileInputStream("D:1.zip");
        fos = new FileOutputStream("D:2.zip");
        //1.获取通道
        inChannel = fis.getChannel();
        outChanel1 = fos.getChannel();
        //2.分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //3.将通道中的数据存入缓冲中
        while (inChannel.read(buf)!=-1){
            buf.flip();//切换读取数据模式
            //将缓冲区中的数据写入通道中
            outChanel1.write(buf);
            buf.clear();  //清空缓冲区
        }
        long l2 = System.currentTimeMillis();
        System.out.println("时间"+(l2-l));
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        if (outChanel1 != null) {
            try {
                outChanel1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (inChannel != null) {
            try {
                inChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
//使用直接缓冲区完成文件的复制(内存映射文件)
@RequiresApi(api = Build.VERSION_CODES.O)
public static  void test2(){
    try {
        long l = System.currentTimeMillis();
        //第一个参数是路径,第二个参数是模式
        FileChannel inchannel=FileChannel.open(Paths.get("D:demo.txt"),StandardOpenOption.READ);
        FileChannel outChannel=FileChannel.open(Paths.get("D:demo2.txt"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
        //内存映射文件
        MappedByteBuffer inMapBuf = inchannel.map(FileChannel.MapMode.READ_ONLY, 0, inchannel.size());
        MappedByteBuffer outMapBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inchannel.size());
        //直接对缓冲区进行数据的读写操作
        byte[] bytes = new byte[inMapBuf.limit()];
        inMapBuf.get(bytes);
        outMapBuf.put(bytes);
        inchannel.close();
        outChannel.close();
        long l2 = System.currentTimeMillis();
        System.out.println("时间"+(l2-l));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

通道(Channel)

通道(Channel) 由java.nio.channels 包定义的。Channel 表示 IO 源于目标打开的链接。Channel 类似于传统的流,只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer进行交互。

通道的主要实现类:
* Java.nio/channels.Channel 接口
*       FileChannel     本地文件传输
*       SocketChannel       网络传输
*       ServerSocketChannel
*       DatagramChannel
    获取通道
* -1. Java 镇对支持通道的类提供了 getChannel() 方法
*       本地IO
*       FiledInputStream/FileOutputStream
*       RandomAccessFile
*
*       网络IO
*       Socket
*       ServerSocket
*       DatagramSocket
* -2. 在 JDK 1.7中的 NIO.2 针对各个通道提供了静态方法 open()
*
* -3. 在 jdk 1.7中的 NIO.2 的 Files 工具类 newByteChannel()
//1.利用通道完成文件的复制(非直接缓冲区)
public static void test1(){
    try{
        long l = System.currentTimeMillis();
        fis = new FileInputStream("D:1.zip");
        fos = new FileOutputStream("D:2.zip");
        //1.获取通道
        inChannel = fis.getChannel();
        outChanel1 = fos.getChannel();
        //2.分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //3.将通道中的数据存入缓冲中
        while (inChannel.read(buf)!=-1){
            buf.flip();//切换读取数据模式
            //将缓冲区中的数据写入通道中
            outChanel1.write(buf);
            buf.clear();  //清空缓冲区
        }
        long l2 = System.currentTimeMillis();
        System.out.println("时间"+(l2-l));
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        if (outChanel1 != null) {
            try {
                outChanel1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (inChannel != null) {
            try {
                inChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

通道之间的数据传输

  • transferFrom()
  • transferTo()
  public static void test3(){
        try {
            FileChannel inchannel=FileChannel.open(Paths.get("D:demo.txt"),StandardOpenOption.READ);
            FileChannel outChannel=FileChannel.open(Paths.get("D:Demop.txt"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
            if (outChannel != null) {
//                inchannel.transferTo(0,inchannel.size(),outChannel);
                outChannel.transferFrom(inchannel,0,inchannel.size());
                inchannel.close();
                outChannel.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

分散(Scatter)与聚集(Gather)

[外链图片转存失败(img-FzYapv7x-1567945086087)(C:\Users\Pettepr\AppData\Roaming\Typora\typora-user-images\1554968002880.png)]

  • 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中
  • 聚集写入(Gathering Writes):将多个缓冲区的数据聚集到通道中
//分散和聚集
public static void test4() throws IOException {
RandomAccessFile rafi=new RandomAccessFile(“D:demo1.txt”,“rw”);
  //1.获取通道
  FileChannel channel=rafi.getChannel();
  //2.分配指定大小的缓冲区
  ByteBuffer buf1 =ByteBuffer.allocate(100);
  ByteBuffer buf2 = ByteBuffer.allocate(1024);
  //3.分散读取
  ByteBuffer[] bufs={buf1,buf2};
  channel.read(bufs);
  for (ByteBuffer byteBuffer:bufs){
      byteBuffer.flip();
  }
  System.out.println(new String(bufs[0].array(),0,bufs[0].limit()));
  System.out.println("----------");
  System.out.println(new String(bufs[1].array(),0,bufs[1].limit()));
  //4.聚集写入
  RandomAccessFile raf2=new RandomAccessFile("D:demo2.txt","rw");
  FileChannel channel2=raf2.getChannel();
  channel2.write(bufs);

字符集:Charset

计算机里的文件,数据,图片文件只是一种表面现象,所有文件在底层都是二进制文件,即全部都是字节码。

对于文本文件而言,之所以可以看到一个个的字符,这完全是因为系统将底层的二进制序列转换成字符的缘故。在这个过程中涉及两个概念:编码(Encode) 和解码 (Decode),通常而言,把明文的字符序列转换成计算机理解的二进制序列称为编码,把二进制序列转换成普通人能看懂的明文字符串称为解码。


Java 默认视同 Uniocde 字符集,但很多操作系统并不适用Unicode 字符集,那么当从系统中读取数据到 Java程序中时,就可能出现乱码等问题。

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


  • 编码:字符串 -> 字节数组
  • 解码:字节数组 -> 字符串
public static void test6() throws CharacterCodingException {
//字符集
Charset cs1 = Charset.forName(“GBK”);
    //查看Java支持的字符集格式
    private static void test5(){
        SortedMap<String, Charset> map = Charset.availableCharsets();
        Set<Map.Entry<String, Charset>> set = map.entrySet();
        for (Map.Entry<String,Charset> entry: set){
            System.out.println(entry.getKey()+"="+entry.getValue());
        }
    }
  //获取编码器
    CharsetEncoder ce = cs1.newEncoder();
    //获取解码器
    CharsetDecoder cd=cs1.newDecoder();
    CharBuffer cBuf = CharBuffer.allocate(1024);
    cBuf.put("我是Petterp");
    cBuf.flip();
    //编码
    System.out.println(cBuf.limit());
    ByteBuffer bBuf = ce.encode(cBuf);
    for (int i=0;i<11;i++){
        System.out.println(bBuf.get());
    }
    bBuf.flip();
    CharBuffer cBuf2 = cd.decode(bBuf);
    System.out.println(cBuf2.toString());
    System.out.println("__________");
    //获得解码器
    Charset cs2=Charset.forName("GBK");
    bBuf.flip();
    CharBuffer cBuf3 = cs2.decode(bBuf);
    System.out.println(cBuf3.toString());
}

二进制序列和字符之间如何对应呢?

为了解决二进制序列与字符之间的对应关系,这就需要字符集了。所谓字符集,就是为每个字符编个号码而已。任何人都可以制定自己独有的字符集明知要为每个字符编个号码即可。当然,如果每个人都制定自己独有的字符集,那程序就没法交流了。

目录
相关文章
|
1月前
|
设计模式 Java API
重构旧代码的秘诀:用设计模式 - 适配器模式(Adapter)给Java项目带来新生
【4月更文挑战第7天】适配器模式是解决接口不兼容问题的结构型设计模式,通过引入适配器类实现目标接口并持有不兼容类引用,实现旧代码与新接口的协作。适用于处理兼容性问题、整合遗留代码和集成第三方库。应用时,识别不兼容接口,创建适配器类转换方法调用,然后替换原有引用。注意保持适配器简单、使用组合和考虑扩展性。过度使用可能导致系统复杂和维护成本增加,应谨慎使用。
|
2月前
|
存储 Java 数据处理
|
2月前
|
Java API
java中IO与NIO有什么不同
java中IO与NIO有什么不同
|
9天前
|
Java
JAVA难点包括异常处理、多线程、泛型和反射,以及复杂的分布式系统知识
JAVA难点包括异常处理、多线程、泛型和反射,以及复杂的分布式系统知识。入坑JAVA因它的面向对象特性、平台无关性、强大的标准库和活跃的社区支持。
31 2
|
17天前
|
缓存 Java API
Java NIO和IO之间的区别
NIO(New IO),这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
16 1
|
22天前
|
监控 Java 开发者
深入理解 Java 网络编程和 NIO
【4月更文挑战第19天】Java网络编程基于Socket,但NIO(非阻塞I/O)提升了效率和性能。NIO特点是非阻塞模式、选择器机制和缓冲区,适合高并发场景。使用NIO涉及通道、选择器和事件处理,优点是高并发、资源利用率和可扩展性,但复杂度、错误处理和性能调优是挑战。开发者应根据需求选择是否使用NIO,并深入理解其原理。
|
24天前
|
存储 监控 Java
浅谈Java NIO
浅谈Java NIO
7 0
|
24天前
|
消息中间件 存储 Java
【Java NIO】那NIO为什么速度快?
是这样的,在NIO零拷贝出现之前,一个I/O操作会将同一份数据进行多次拷贝。可以看下图,一次I/O操作对数据进行了四次复制,同时来伴随两次内核态和用户态的上下文切换,众所周知上下文切换是很耗费性能的操作。
29 1
【Java NIO】那NIO为什么速度快?
|
26天前
|
设计模式 算法 Java
如何优雅地重构 Java 中的 if-else
【4月更文挑战第10天】
37 1
|
27天前
|
存储 监控 Java
Java输入输出:什么是NIO(New I/O)?
Java NIO是一种高效I/O库,特征包括非阻塞性操作、通道(如文件、网络连接)、缓冲区和选择器。选择器监控通道状态变化,通知应用程序数据可读写,避免轮询,提升性能。示例代码展示了一个使用NIO的服务器,监听连接、读取数据并处理客户端通信。
14 1