Java NIO 缓冲区

Java NIO 在JDK1.4的时候引入,主要解决传统IO的一些性能问题。NIO 主要内容包含 Buffer 、Channel、Selector等内容,本文主要讲解Buffer相关的内容。

Buffer的继承体系

Buffer的子类比较多,但是继承关系比较简单。8种基本类型,除了布尔类型,其余的类型都有对应的Buffer实现,名字也十分好记:基本数据类型首字母大写+Buffer。其中ByteBuffer最为常用,因为字节是操作系统及其I/O设备使用的基本数据类型,后面演示的时候也主要使用字节缓冲区。除此之外MappedByteBuffer也十分重要,主要和内存映射有该,后面会详细介绍。

重要属性

在Buffer抽象类中,主要有如下几个属性,这些属性十分重要。

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

属性介绍

属性名 描述
mark 位置标记。调用mark( )来设定mark = position。调用reset( )设定position = mark。
position 下一个要被读或写的元素的索引。位置会自动由相应的get( )和put( )函数更新。
limit 缓冲区的第一个不能被读或写的元素,或者说可供读写的最大位置。
capacity 缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。

重要方法

下面的方法是Buffer抽象类所定义的方法。但是它没有定义两个最重要的方法:put 和 get 。因为每一个Buffer子类它们所采用的参数类型,以及它们返回的数据类型都是唯一的,所以它们不能在顶层Buffer类中被抽象地声明。它们的定义必须被特定类型的子类所遵从。

int capacity()
int position()
Buffer position(int)
int limit()
Buffer limit(int)
Buffer mark()
Buffer reset()
Buffer clear()
Buffer flip()
Buffer rewind()
int remaining()
boolean hasRemaining()
boolean isReadOnly()
boolean hasArray()
Object array()
int arrayOffset()
boolean isDirect()

创建缓冲区

allocate、allocateDirect 和 wrap可以新建一个缓冲区,这几个方法含义各不相同。

  • allocate 用于创建普通的缓冲区,该缓冲区建立在JVM内存之上。
  • allocateDirect 用于创建直接缓冲区,该缓冲区建立在操作系统之中的内存,非JVM占用的那一部分。
  • wrap 使用自己的数组用做缓冲区的备份存储器,。这意味着通过调用put()函数造成的对缓冲区的改动会直接影响这个数组,而且对这个数组的任何改动也会对这个缓冲区对象可见。

下面我们使用 allocate 创建一个缓冲区。

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
    }

查看 allocate 方法源码:

    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }

再看 HeapByteBuffer 构造函数源码:

    HeapByteBuffer(int cap, int lim) {            // package-private

        super(-1, 0, lim, cap, new byte[cap], 0);
        /*
        hb = new byte[cap];
        offset = 0;
        */

    }

上面super其实调用ByteBuffer的构造函数:

    ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
                 byte[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }

在往上其实就是调用Buffer的构造函数:

    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }

这里其实就是给Buffer的四大属性赋值。注意下面的 10 是我们创建的时候传入的。

属性名
mark -1
position 0
limit 10
capacity 10

put方法

我们使用put方法给缓冲区存入数据:

   public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("Hello".getBytes());
    }

这里我们调用了put方法:

    public final ByteBuffer put(byte[] src) {
        return put(src, 0, src.length);
    }

可以看出 put 方法还有重载方法,对于给定的字符数组,可以指定从哪一个位置起,插入几个的功能。

    public ByteBuffer put(byte[] src, int offset, int length) {
        checkBounds(offset, length, src.length);
        if (length > remaining())
            throw new BufferOverflowException();
        int end = offset + length;
        for (int i = offset; i < end; i++)
            this.put(src[i]);
        return this;
    }

上面两个方法都是ByteBuffer的方法,ByteBuffer又是抽象类,this.put(src[i]); 这句其实是子类 HeapByteBuffer 所实现:

    public ByteBuffer put(byte x) {
        hb[ix(nextPutIndex())] = x;
        return this;
    }

其中nextPutIndex方法正是将position返回之后再自加1,这样的话将空闲的位置赋值并指向下一个空闲的位置。

    final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }
属性名
mark -1
position 5
limit 10
capacity 10

其中 0 - 4 这5哥索引位置存放 Hello 这5个字节。

特别注意:put 以及下面的get方法,如果指定位置进行存取时将不影响 Buffer 类的属性值。

flip方法

我们刚刚在缓冲区中写入了5个字节,上面也已经讲了 put 负责写,get 负责读数据,那么现在可不可以使用 get 方法进行读数据呢?答案是否定的。因为我们从 Buffer 的四个属性可知,position 表示读写的起始位置,目前处于索引下标为 5 的位置,继续写是可以的,但是要从这个位置开始读取数据,那么就是读取到空数据。

从Buffer的重要方法可以看出,有一个方法可以修改 position 的索引。

Buffer position(int)

因此我们想把 position 的索引置为 0 不就可以了吗?稍等,还有一点,limit 是读写的最后一个位置,现在索引指向10,也就是前面 10个元素可以读取,但是这10个元素的后5个还没有写入数据呢,因此也要把limit 索引置为写入的最后一个位置 5。代码如下:

buffer.limit(buffer.position()).position(0);

这样完美解决了我们的问题,但是感觉比较繁琐,java NIO已经提供了一个方法 flip 实现了同样的功能。

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

这里 mark 为什么置为 -1 ,因为以前做的标记已经无效。

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("Hello".getBytes());
        buffer.flip();
    }

通过flip方法使得缓冲区已经从写模式转换为读模式。

属性名
mark -1
position 0
limit 5
capacity 10

get方法

get方法有几个重载方法,先使用下面的方法进行演示:

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("Hello".getBytes());
        buffer.flip();
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes);
        System.out.println(new String(bytes, 0, bytes.length));
    }

上面代码中,一次性读取5(buffer.limit())个元素,此时position的索引变为5。分析一下源码:

    public ByteBuffer get(byte[] dst) {
        return get(dst, 0, dst.length);
    }

上面的代码说明 get 的重载方法也可以指定接收数组的起始位置和长度:

    public ByteBuffer get(byte[] dst, int offset, int length) {
        checkBounds(offset, length, dst.length);
        if (length > remaining())
            throw new BufferUnderflowException();
        int end = offset + length;
        for (int i = offset; i < end; i++)
            dst[i] = get();
        return this;
    }

这和上面的 put 方法极为类似。

    public byte get() {
        return hb[ix(nextGetIndex())];
    }

这个 get 方法也是 HeapByteBuffer 中的,返回数组中的一个元素的值,其中nextGetIndex方法实现和 put 方法里面的一样:

    final int nextGetIndex() {                          // package-private
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }
属性名
mark -1
position 5
limit 5
capacity 10

rewind 方法

上面我们使用get方法读取了数据,那么我们想再重复读取一次该怎么办?此时也就是想把 position 置为0,limit保持不变。rewind 则派上用场了。

rewind()与flip()相似,但不影响 limit 属性。它只是将 position 设回0。可以使用rewind()后退,重读已经被翻转的缓冲区中的数据。

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("Hello".getBytes());
        buffer.flip();
        //第一次读取
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes);
        System.out.println(new String(bytes, 0, bytes.length));
        //使用rewind将 position 置为0
        buffer.rewind();
        //第二次读取
        byte[] desc = new byte[buffer.limit()];
        buffer.get(desc);
        System.out.println(new String(desc, 0, bytes.length));
    }

源码如下:

    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

下面是执行 rewind 方法后的 Buffer 类的属性值变化情况,注意此图还没有执行到第二次执行get的时候,仅表示rewind 方法执行完成那一刻的 属性 变化情况。

属性名
mark -1
position 0
limit 5
capacity 10

clear 方法

clear() 将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将limit设为容量的值,并把position 设回0,这使得缓冲区可以被重新填入。

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("Hello".getBytes());
        //清空缓冲区
        buffer.clear();
    }

源码如下:

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
属性名
mark -1
position 0
limit 10
capacity 10

这和刚刚创建好缓冲区属性的情况一致。

mark 和 reset 方法

上面 rewind 可以重复读数据,但是有一个限制,每次都是从头开始读取。现在有个需求,可以从不同的索引位置开始读取。

假如缓冲区有 HelloTom 8个字节,首先读取 Hello 5个字节,再把 Tom 这三个字节 读取 2次,显然 rewind 无法实现。使用 mark 和 reset 组合即可实现。

mark源码:

    public final Buffer mark() {
        mark = position;
        return this;
    }

reset源码:

    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

测试代码:

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        buffer.put("HelloTom".getBytes());
        buffer.flip();

        //读取 Hello
        byte[] dest1 = new byte[5];
        buffer.get(dest1);
        System.out.println(new String(dest1, 0, 5));

        //做好标记
        buffer.mark();

        //读取 Tom
        byte[] dest2 = new byte[3];
        buffer.get(dest2);
        System.out.println(new String(dest2, 0, 3));

        //回到标记位置
        buffer.reset();

        //读取 Tom
        byte[] dest3 = new byte[3];
        buffer.get(dest3);
        System.out.println(new String(dest3, 0, 3));
    }
  1. allocate

  1. put

  1. flip

  1. 第一次 get

  1. mark

  1. 第二次 get

  1. reset

  1. 第三次 get

其它方法

int capacity()  //获取缓冲区总容量
int position()  //获取缓冲区当前位置
int limit()  //获取缓冲区允许读写的最后一个位置
Buffer limit(int) //设置
int remaining() //从当前位置到limit还剩余的元素数目
boolean hasRemaining() //是否已经达到缓冲区的上界
boolean isReadOnly() //所有的缓冲区都是可读的,但并非所有都可写。每个具体的缓冲区类都通过执行isReadOnly()来标示其是否允许该缓存区的内容被修改。
boolean hasArray()
Object array()
int arrayOffset()
boolean isDirect()

有些比较简单,有些暂时还没研究。以后用到再查阅资料。

原文地址:https://www.cnblogs.com/ye-feng-yu/p/12050597.html

时间: 2024-08-12 19:29:05

Java NIO 缓冲区的相关文章

java nio 缓冲区(一)

  本文来自于我的个人博客:java nio 缓冲区(一) 我们以Buffer类开始对java.nio包的浏览历程.这些类是java.nio的构造基础.这个系列中,我们将跟随<java NIO>书籍一起深入研究缓冲区,了解各种不同的类型,并学会怎样使用. 一个Buffer对象是固定数量的数据容器.其作用是一个存储器,或者分段运输区,在这里数据可被存储并在之后用于检索. Buffer类的家谱: 一,缓冲区基础 1.缓冲区的属性: 容量(capacity):缓冲区能够容纳的数据元素的最大数量,这一

Java NIO 缓冲区学习笔记

Buffer其实就是是一个容器对象,它包含一些要写入或者刚读出的数据.在NIO中加入Buffer对象,体现了新库与原I/O的一个重要区别.在面向流的I/O中,您将数据直接写入或者将数据直接读到Stream对象中. 在NIO库中,所有数据都是用缓冲区处理的.在读取数据时,它是直接读到缓冲区中的.在写入数据时,它是写入到缓冲区中的.任何时候访问NIO中的数据,您都是将它放到缓冲区中. 缓冲区实质上是一个数组.通常它是一个字节数组,但是也可以使用其他种类的数组.但是一个缓冲区不仅仅是一个数组.缓冲区提

java nio 缓冲区(二)

本文章来自于本人个人博客:java nio 缓冲区(二) 一,创建缓冲区 1.缓冲区的创建有两种方式,分别是ByteBuffer.allocate([int])或者ByteBuffer.wrap(byte[]),第一种方式是创建一个分配了int个字节的缓冲区,而第二种方式是在现有字节数组之上创建一个缓冲区,这个缓冲区的capacity就是数组的长度. 2.Buffer类的其它子类创建缓冲区也是一样的:CharBuffer.allocate(int)或者CharBuffer.wrap(byte[]

Java NIO -- 缓冲区(Buffer)的数据存取

缓冲区(Buffer): 一个用于特定基本数据类型的容器.由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类.Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的. Buffer 就像一个数组,可以保存多个相同类型的数据.根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:ByteBufferCharBuffer ShortBuffer IntBuffer LongBuffer Flo

Java-杂项-java.nio:java.nio

ylbtech-Java-杂项-java.nio:java.nio java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络. 1.返回顶部 1. 中文名:java非阻塞式IO 外文名:java nio 缓冲区:数据容器 特    性:Channel,Buffer,Selector 简    称:nio 目    的:提供非阻

Java NIO中的缓冲区Buffer(一)缓冲区基础

什么是缓冲区(Buffer) 定义 简单地说就是一块存储区域,哈哈哈,可能太简单了,或者可以换种说法,从代码的角度来讲(可以查看JDK中Buffer.ByteBuffer.DoubleBuffer等的源码),Buffer类内部其实就是一个基本数据类型的数组,以及对这个缓冲数组的各种操作: 常见的缓冲区如ByteBuffer.IntBuffer.DoubleBuffer...内部对应的数组依次是byte.int.double... 与通道的关系 在Java NIO中,缓冲区主要是跟通道(Chann

Java NIO (二) 缓冲区(Buffer)

缓冲区(Buffer):一个用于特定基本数据类型的容器,由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类. Java NIO 中的Buffer 主要用于和NIO中的通道(Channel)进行交互, 数据从通道(Channel)读入缓冲区(Buffer)或者从缓冲区(Buffer)写入通道(Channel).如下,我画的一个简图,Chanenl直接和数据源或者目的位置接触,Buffer作为中介这,从一个Channel中读取数据,然后将数据写入另一个Channel中. Bu

java NIO 直接与非直接缓冲区

ByteBuffer有两个创建缓冲区的方法:static ByteBuffer allocate(int capacity)static ByteBuffer allocateDirect(int capacity) 这两个方法都是创建缓冲区的方法,使用直接缓冲区的时候,JVM虚拟机会直接在此缓冲区上执行本机IO操作,也就是说,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会避免将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容). 直接字节缓冲区使用上边方法中的allocat

Java nio 笔记:系统IO、缓冲区、流IO、socket通道

一.Java IO 和 系统 IO 不匹配 在大多数情况下,Java 应用程序并非真的受着 I/O 的束缚.操作系统并非不能快速传送数据,让 Java 有事可做:相反,是 JVM 自身在 I/O 方面效率欠佳.操作系统与 Java 基于流的 I/O模型有些不匹配.操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取(DMA)的协助下完成的.而 JVM 的 I/O 操作类喜欢操作小块数据--单个字节.几行文本.结果,操作系统送来整缓冲区的数据,java.io 包的流数据类再花大量时间