《Java编程思想第四版》笔记---18章(5)NIO 缓冲区

来源:互联网 发布:网络摄像机改ip软件 编辑:程序博客网 时间:2024/06/06 01:21

Buffer

缓冲区由:内容数据和4个索引组成。
缓冲区索引:

a.缓冲区标记(mark):使缓冲区能够记住一个位置并在之后将其返回。并非总需要定义标记,但在定义标记时,不能将其定义为负数,且不能大于其位置。

b.缓冲区位置(position):是缓冲区下一个要读取或写入元素的索引,位置不能为负,且不能大于其限制。

c.缓冲区限制(limit):是第一个不应该读取或写入的元素的索引,限制不能为负,且不能大于其容量。

d.缓冲区容量(capacity):是缓冲区所包含的元素数量,不能为负,且不能更改。

缓冲区索引遵循以下不变公式:
0 <= 标记 <= 位置 <= 限制 <= 容量
通过这些索引,缓冲区可以高效的访问和操作缓冲区中的内容数据,新创建的缓冲区总有一个0位置和一个未定义的标记,初始限制可以为0,也可以为其他值,取决于缓冲区类型和构建方式。

Buffer是一个抽象类,一些公共方法:

(1).capacity()方法:
返回此缓冲区的容量。
(2).clear()方法:
清除此缓冲区,将缓冲区位置设置为0,将缓冲区限制设置为容量,并丢弃标记。
(3).flip()方法:
反转此缓冲区,首先将限制设置为当前位置,然后将位置设置为0,如果已定义了标记,则丢弃该标记。
(4).limit()方法:
返回此缓冲区的限制。
(5).limit(int lim)方法:
设置此缓冲区的限制,如果位置大于新的限制,则将位置设置为新的限制,如果标记已定义且大于新限制,则丢弃该标记。
(6).mark()方法:
在此缓冲区的位置设置标记。
(7).position()方法:
返回此缓冲区的位置。
(8).position(int pos)方法:
设置此缓冲区的位置,如果标记已定义且大于新的位置,则丢弃该标记。
(9).remaining()方法:
返回当前位置与限制之间的元素个数,即limit-position。
(10).hasRemaining()方法:
判断在当前位置和限制之间是否有元素。

注意:这些API的设计中,例如, mark()返回的是一个Buffer,clear()返回的也是一个Buffer,这样设计,是为了级联操纵。

例如:

      buffer.mark().clear().position(0);  


当然如果太长了,也是不好看的。 
这里还有一个方法需要注意,isReadOnly()这说明,并非所有的缓冲区都是可以写的。所以如果这个方法返回true,那么,这个缓冲区就不能写了。 


***********************************************************************************************

缓冲区创建方法

Java提供了七个基本的缓冲区,分别由七个类来管理,它们都可以在java.nio包中找到。这七个类如下所示:


    ByteBuffer 
  ShortBuffer 
  IntBuffer 
  CharBuffer 
  FloatBuffer 
  DoubleBuffer 
  LongBuffer 

这七个类中的方法类似,只是它们的返回值或参数和相应的简单类型相对应,如ByteBuffer类的get方法返回了byte类型的数据,而put 方法需要一个byte类型的参数。在CharBuffer类中的get和put方法返回和传递的数据类型就是char。这七个类都没有public构造方 法,因此,它们不能通过new来创建相应的对象实例。


两种方式来创建相应的对象实例

第一种:通过静态方法allocate来创建缓冲区。

  这七类都有一个静态的allocate方法,通过这个方法可以创建有最大容量限制的缓冲区对象。allocate的定义如下:

  ByteBuffer类中的allocate方法:


 public static ByteBuffer allocate(int capacity)

  IntBuffer类中的allocate方法:


 public static IntBuffer allocate(int capacity)

  其他五个缓冲区类中的allocate 方法定义和上面的定义类似,只是返回值的类型是相应的缓冲区类。

  参数capacity,用来指定缓冲区容量的最大值。capacity的不能小于0,否则会抛出一个 IllegalArgumentException异常。使用allocate来创建缓冲区,并不是一下子就分配给缓冲区capacity大小的空间,而 是根据缓冲区中存储数据的情况来动态分配缓冲区的大小(实际上,在低层Java采用了数据结构中的堆来管理缓冲区的大小),因此,这个capacity可以是一个很大的值,如1024*1024(1M)。allocate的使用方法如下:


 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); 
 IntBuffer intBuffer = IntBuffer.allocate(1024);

  在使用allocate创建缓冲区时应用注意,capacity的含义随着缓冲区的不同而不同。如创建字节缓冲区时,capacity指的是字节数。而在创建整型(int)缓冲区时,capacity指的是int型值的数目,如果转换成字节数,capacity的值应该乘4。如上面代码中的 intBuffer缓冲区最大可容纳的字节数是1024*4 = 4096个。

第二种:通过静态方法wrap来创建缓冲区。

  使用allocate方法可以创建一个空的缓冲区。而wrap方法可以利用已经存在的数据来创建缓冲区。wrap方法可以将数组直接转换成相应类型的缓冲区。wrap方法有两种重载形式,它们的定义如下:

  ByteBuffer类中的wrap方法:


    public static ByteBuffer wrap(byte[] array) 
  public static ByteBuffer wrap(byte[] array, int offset, int length)

  IntBuffer类中的wrap方法:


    public static IntBuffer wrap(int[] array) 
  public static IntBuffer wrap(int[] array, int offset, int length)

  其他五个缓冲区类中的wrap 方法定义和上面的定义类似,只是返回值的类型是相应的缓冲区类。

  在wrap方法中的array参数是要转换的数组(如果是其他的缓冲区类,数组的类型就是相应的简单类型,如IntBuffer类中的wrap 方法的array就是int[]类型)。offset是要转换的子数组的偏移量,也就是子数组在array中的开始索引。length是要转换的子数组的长度。利用后两个参数可以将array数组中的一部分转换成缓冲区对象。它们的使用方法如下:


    byte[] myByte = new byte[] { 1, 2, 3 }; 
  int[] myInt = new int[] { 1, 2, 3, 4 }; 
  ByteBuffer byteBuffer = ByteBuffer.wrap(myByte); 
  IntBuffer intBuffer = IntBuffer.wrap(myInt, 1, 2);

  可以通过缓冲区类的capacity方法来得到缓冲区的大小。capacity方法的定义如下:


 public final int capacity()

  如果使用allocate方法来创建缓冲区,capacity方法的返回值就是capacity参数的值。而使用wrap方法来创建缓冲 区,capacity方法的返回值是array数组的长度,但要注意,使用wrap来转换array的字数组时,capacity的长度仍然是原数组的长度,如上面代码中的intBuffer缓冲区的capacity值是4,而不是2。

缓冲区转换成相应类型的数组  

除了可以将数组转换成缓冲区外,也可以通过缓冲区类的array方法将缓冲区转换成相应类型的数组。IntBuffer类的array方法的定义方法如下(其他缓冲区类的array的定义类似):


 public final int[] array()

下面的代码演示了如何使用array方法将缓冲区转换成相应类型的数组。


 int[] myInt = new int[] { 1, 2, 3, 4, 5, 6 }; 
  IntBuffer intBuffer = IntBuffer.wrap(myInt, 1, 3); 
  for (int v : intBuffer.array()) 
  System.out.print(v + " ");

  在执行上面代码后,我们发现输出的结果是1 2 3 4 5 6,而不是2 3 4。这说明在将子数组转换成缓冲区的过程中实际上是将整个数组转换成了缓冲区,这就是用wrap包装子数组后,capacity的值仍然是原数组长度的真正原因。


在以下两种缓冲区中不能使用array方法:

(1)只读的缓冲区

  如果使用只读缓冲区的array方法,将会抛出一个ReadOnlyBufferException异常。

(2)使用allocateDirect方法创建的缓冲区。

  如果调用这种缓冲区中的array方法,将会抛出一个UnsupportedOperationException异常。

  

可以通过缓冲区类的hasArray方法来判断这个缓冲区是否可以使用array方法,如果返回true,则说明这个缓冲区可以使用array方法,否则,使用array方法将会抛出上述的两种异常之一。

  注意: 使用array方法返回的数组并不是缓冲区数据的副本。被返回的数组实际上就是缓冲区中的数据,也就是说,array方法只返回了缓冲区数据的引用。当数组中的数据被修改后,缓冲区中的数据也会被修改,返之也是如此。

  

两个特殊的创建缓冲区方法

在上述的七个缓冲区类中,ByteBuffer类和CharBuffer类各自还有另外一种方法来创建缓冲区对象。

(1)ByteBuffer类

  还可以通过ByteBuffer类的allocateDirect方法来创建ByteBuffer对象。allocateDirect方法的定义如下:


 public static ByteBuffer allocateDirect(int capacity)

  前面说到,allocate和wrap创建的都是间接缓冲区。间接缓冲区是在操作系统层次创建一个临时缓冲区,将Jvm的缓存copy过去,再操作。直接缓冲区直接在操作系统而不是JVM进程中创建缓冲区,这就绕过了JVM栈,所以依赖于具体的操作系统。事实上,在操作系统中创建缓冲区,比起在JVM中,更加消耗资源。

      使用allocateDirect方法可以一次性分配capacity大小的连续字节空间。通过allocateDirect方法来创建具有连续空间的ByteBuffer对象虽然可以在一定程度上提高效率,但这种方式并不是平台独立的。也就是说,在一些操作系统平台上使用allocateDirect方法来创建ByteBuffer对象会使效率大幅度提高,而在另一些操作系统平台上,性能会表现得非常差。而且allocateDirect方法需要较长的时间来分配内存空间,在释放空间时也较慢。因此,在使用allocateDirect方法时应谨慎。

  通过isDirect方法可以判断缓冲区对象(其他的缓冲区类也有isDirect方法,因为,ByteBuffer对象可以转换成其他的缓冲区对象,这部分内容将在后面讲解)是用哪种方式创建的,如果isDirect方法返回true,则这个缓冲区对象是用allocateDirect方法创建的,否则,就是用其他方法创建的缓冲区对象。


(2)CharBuffer类

  我们可以发现,上述的七种缓冲区中并没有字符串缓冲区,而字符串在程序中却是最常用的一种数据类型。不过不要担心,虽然java.nio包中并 未提供字符串缓冲区,但却可以将字符串转换成字符缓冲区(就是CharBuffer对象)。在CharBuffer类中的wrap方法除了上述的两种重载 形式外,又多了两种重载形式,它们的定义如下:


    public static CharBuffer wrap(CharSequence csq) 
  public static CharBuffer wrap(CharSequence csq, int start, int end)

  其中csq参数表示要转换的字符串,但我们注意到csq的类型并不是String,而是CharSequence。CharSequence类 Java中四个可以表示字符串的类的父类,这四个类是String、StringBuffer、StringBuilder和CharBuffer(大家要注意,StringBuffer和本节讲的缓冲区类一点关系都没有,这个类在java.lang包中)。也就是说,CharBuffer类的wrap方法可以将这四个类的对象转换成CharBuffer对象。

  另外两个参数start和end分别是子字符串的开始索引和结束索引的下一个位置,如将字符串"1234"中的"23" 转换成CharBuffer对象的语句如下:


 CharBuffer cb = CharBuffer.wrap("1234", 1, 3);

  下面的代码演示了如何使用wrap方法将不同形式的字符串转换成CharBuffer对象。


    StringBuffer stringBuffer = new StringBuffer("通过StringBuffer创建CharBuffer对象"); 
  StringBuilder stringBuilder = new StringBuilder("通过StringBuilder创建CharBuffer对象"); 
  CharBuffer charBuffer1 = CharBuffer.wrap("通过String创建CharBuffer对象"); 
  CharBuffer charBuffer2 = CharBuffer.wrap(stringBuffer); 
  CharBuffer charBuffer3 = CharBuffer.wrap(stringBuilder); 
  CharBuffer charBuffer4 = CharBuffer.wrap(charBuffer1, 1, 3);

***********************************************************************************************

细讲ByteBuffer 字节缓冲区 

通道只和字节缓冲区打交道。其他的缓冲区和字节缓冲区都可以转换。 字节缓冲区可以存放原始字节,也可以存放java的8中基本类型数据,但是不能存放引用类型数据,String也是不能存放的。正是由于这种底层的存放类型,字节缓冲区可以更加高效和绝大部分操作系统的I/O进行映射。

内存模型

实际上,内存是按照字节为单位来存放数据的。现在最流行的便是8个位一个字节。我们熟悉的是int占用4个字节,这样,这四个字节如何排列,又成为一个问题。 这个问题已经是硬件问题了,因为不同的硬件有不同的表现。 对于intel处理器,一般都是小端(little endian) 
而对于摩托罗拉,Sun的处理器,一般都是大端(big endian) 还有网络字节顺序实际上也是大端的。 
那么这两者有什么区别呢? 
假设 一个int值 100
那么他在大端的存放 就类似 :   0x00  0x00  0x00  0x64 
那么他在小端的存放,就应该是: 0x64  0x00  0x00  0x00 

所以我们的字节操作中有了顺序问题。 
  1. package java.nio;  
  2. public final class ByteOrder  
  3. {  
  4. public static final ByteOrder BIG_ENDIAN;  
  5. public static final ByteOrder LITTLE_ENDIAN;  
  6. public static ByteOrder nativeOrder();  
  7. public String toString();  
  8. }  


在JVM中,实际上我们是使用大端的。 
看看关于Order的操作。 

  1. public abstract class ByteBuffer extends Buffer  
  2. implements Comparable {  
  3. // This is a partial API listing   
  4. public final ByteOrder order();  
  5. public final ByteBuffer order(ByteOrder bo);  
  6. }  

字节缓冲区的常用操作

(1).读写单个字节的绝对和相对get和put方法:
a. 绝对方法:
get(int index):读取指定索引处的字节。
put(int index, byte b):将字节写入指定索引处。
b.相对方法:
get():读取此缓冲区当前位置的字节,然后该位置递增。
put(byte b):将字节写入此缓冲区的当前位置,然后该位置递增。
(2).相对批量get方法:
   ByteBuffer get(byte[] dst):将此缓冲区的字节传输到给定的目标数组中。
(3).相对批量put方法:
   ByteBuffer put(byte[] src):将给定的源byte数组的所有内容传输到此缓冲区中。
(4).读写其他基本类型值:
getChar(), putChar(char value), getChare(int index), putChar(int index, char value),getInt(), putInt(int value), getInt(int index), putInt(int index, int value)等等
注意:基本类型值的相对和绝对读写方法,根据java基本类型数据底层字节数进行缓冲区移动。

(5).缓冲区转换:
允许将字节缓冲区转换成视图缓冲区,字节缓冲区和视图缓冲区内容更改是相互可见的。这两种缓冲区的位置、限制和标记值都是独立的。创建方法如下:

字节缓冲区提供了很多这样的API 

  1. public abstract class ByteBuffer  
  2. extends Buffer implements Comparable {  
  3. // This is a partial API listing   
  4. public abstract CharBuffer asCharBuffer( );   
  5. public abstract ShortBuffer asShortBuffer( );   
  6. public abstract IntBuffer asIntBuffer( );   
  7. public abstract LongBuffer asLongBuffer( );   
  8. public abstract FloatBuffer asFloatBuffer( );  
  9. public abstract DoubleBuffer asDoubleBuffer( );  
  10. }  


前面提到过从ByteBuffer到IntBuffer的转换。其实这种转换,是一种解释包装。 
所以需要提供一个顺序,然后按照顺序和类型,转换成另外一种视图,而原始数据其实是不变的。 
例如,我们可以这样; 

  1. ByteBuffer byteBuffer =  ByteBuffer.allocate(7).order(ByteOrder.BIG_ENDIAN);  
  2. CharBuffer charBuffer = byteBuffer.asCharBuffer();  


注意,java中char是占两个字节的。我们的转换可以看下图: 



新的charBuffer视图其实还是使用的原来的缓冲区的数据,只是这时的元素变成了2个字节的char了。 


与具体类型的get和put方法系列相比,视图缓冲区优势如下:
a.视图缓冲区不是根据字节进行索引,而是根据其特定类型的值得大小进行索引。
b.视图缓冲区提供了相对批量get和put方法,这些方法可以在缓冲区和数组或相同类型的其他缓冲区直接传输连续序列的值。
c.视图缓冲区可能更高效,因为当且仅当其支持的字节缓冲区为直接缓冲区时它才是直接缓冲区。

例子如下:

  1. public class ViewBuffer {
  2. private static final int BSIZE = 1024;
  3. public static void main(String[] args){
  4. ByteBuffer bb = ByteBuffer.allocate(BSIZE);
  5. int i = 0;
  6. //检查字节缓冲区的内容是否为0
  7. while(i++ < bb.limit()){
  8. if(bb.get() != 0){
  9. System.out.println("nonzero");
  10. }
  11. System.out.println("i = " + i);
  12. //回到字节缓冲区开始处
  13. bb.rewind();
  14. }
  15. //创建字节缓冲区视图,作为char缓冲
  16. bb.asCharBuffer().put("Hello!");
  17. char c;
  18. while((c = bb.getChar)) != 0){
  19. System.out.println(c + " ");
  20. }
  21. bb.rewind();
  22. //创建字节缓冲区视图,作为short缓冲
  23. bb.asShortBuffer().put((Short)12390);
  24. System.out.println(bb.getShort());
  25. bb.rewind();
  26. //创建字节缓冲区视图,作为int缓冲
  27. bb.asIntBuffer().put(99471142);
  28. System.out.println(bb.getInt());
  29. bb.rewind();
  30. //创建字节缓冲区视图,作为long缓冲
  31. bb.asLongBuffer().put(99471142);
  32. System.out.println(bb.getLong());
  33. bb.rewind();
  34. //创建字节缓冲区视图,作为float缓冲
  35. bb.asFloatBuffer().put(99471142);
  36. System.out.println(bb.getFloat());
  37. bb.rewind();
  38. //创建字节缓冲区视图,作为double缓冲
  39. bb.asDoubleBuffer().put(99471142);
  40. System.out.println(bb.getDouble());
  41. }
  42. }

输出结果:
i = 1025
H e l l o !
12390
99471142
99471142
9.9471142E7
9.9471142E7
对于刚分配的字节缓冲区来说,其所有内容都是0,所有第一次i的输出值为1025.
向字节缓冲区中写入,或者从字节缓冲区中读取8中基本类型java数据最简便的方法是使用字节缓冲区的视图缓冲区。


(6).缓冲区其他操作:

a.ByteBuffer compact():压缩缓冲区,从缓冲区写入数据之后调用,以防写入不完整。

b.ByteBuffer duplicate():创建共享此缓冲区内容的新的字节缓冲区,新缓冲区中的内容为此缓冲区的内容,此缓冲区和新缓冲区内容更改是相互可见的。

c.ByteBuffer slice():创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。

*************************************************************************************************

无符号数存取 

对于java来说,没有无符号数处理(除了char)。
其基本原理是,使用比要存取的数据类型更大的数据类型来存放这个数,同时使用与运算强制设置符号位为0,剩余的位就可以作为数据而不是符号位了。 

  1. public class Unsigned {
  2. public static short getUnsignedByte (ByteBuffer bb) {
  3. return ((short)(bb.get() & 0xff));
  4. }
  5. public static void putUnsignedByte (ByteBuffer bb, int value) {
  6. bb.put ((byte)(value & 0xff));
  7. }
  8. ...
  9. }

*************************************************************************************************

内存映射缓冲区MappedByteBuffer  

      映射缓冲区通常是直接存取内存的,只能通过 FileChannel 类创建。映射缓冲区的用法和直接缓冲区类似,但是 MappedByteBuffer 对象具有许多文件存取独有的特征。映射缓冲区允许把比内存大的文件读入内存中创建和修改,使用映射缓冲区,可以像使用内存中数组一样在内存中访问整个文件,例子如下:

import java.nio.*;
import java.nio.channels.*;
import java.io.*;
pulbic class MemoryMappedFile {
    // 十六进制,128MB大小
    static int length = 0x8FFFFFF;
    public static void main(String[] args)throws Exception{  
        //通过文件通道将文件映射为内存中的自己缓冲区  
        MappedByteBuffer out = new RandomAccessFile("test.dat""rw")
                      .getChannel()
                      .map(FileChannel.MapMode.READ_WRITE, 0, length);  
        for(int i = 0; i < length; i++){  
            out.put((byte)"x");  
        }   
        System.out.println("Finished writing");  
        for(int i = length/2; i < length/2 + 6; i++){  
            System.out.println((char)out.get(i));  
        }  
    }

}  

MappedByteBuffer是直接字节缓冲区,其内容是文件的内存映射区域,映射的字节缓冲区和它所表示的文件映射关系在该缓冲区本身成为垃圾回收之前一直保持有效。

FileChannel的map(FileChannel.MapMode mode, long position, long size) thorws IOException方法可以将此通道的文件区域直接映射到内存中。

三个参数:

第一个参数是映射模式

(1).只读:视图修改得到的缓冲区将导致抛出ReadOnlyBufferException。

(2).读/写:对得到的缓冲区的更改将最终传播到文件,该更改对映射到同一文件的其他程序不一定是可见的。

(3).专用:对的到的缓冲区更改将不会被传播到文件,并且该更改对映射到同一文件的其他程序也是不可见的,相反,会创建缓冲区已修改部分的专用副本。

第二和第三个参数分别是文件区域映射的起始位置和大小

只有指定的部分才会被影响到内存的字节缓冲区中,而不再指定范围内的文件区域不会被映射到内存中,因此,对于大型文件来说,可以大大提高程序性能。

注意:映射关系一经创建,就不再依赖于创建它时所用的文件通道,特别是关闭该通道对映射关系的有效性没有任何影响。另外,从性能观点来讲,通常相对较大的文件映射到内存中才是值得的。




0 0
原创粉丝点击