1. Channel简介:
1) 回顾Channel的特性:
i. NIO使用Channel(通道)来传送数据,即I/O流的角色;
ii. 但是Channel只能和Buffer直接交互,即输入输出的数据都必须要先经过Buffer才行,程序不能直接访问Channel中的数据;
iii. 输入的时候Channel相当于把物理节点中的数据映射到了Buffer中,在Buffer处理数据的效率比之前大大提高了;
2) Channel只是一个抽象基类,Java按照功能分别实现了多种Channel:
i. FileChannel:最常用的文件I/O通道;
ii. ServerSocketChannel、SocketChannel:TCP通信的通道;
iii. DatagramChannel:UDP通信通道;
iv. Pipe.SinkChannel、Pipe.SourceChannel:线程通信管道;
v. SelectableChannel:非阻塞式多路通道;
2. 创建通道:
1) 通道通道就是当前程序和某个物理节点之间建立的交换数据的通路而已,因此站在当前程序角度,通道必然是关联着某个物理节点;
2) 因此创建通道是利用流节点来创建(Java不提供Channel的构造器),直接使用节点流的getChannel方法获取和该流节点相关联的通道,一目了然,逻辑清晰:
i. 原型:XxxChannel getChannel();
ii. FileInputStream/FileOutputStream得到的就是FileChannel,其它的InputStream/OutputStream获得的就是相应类型的Channel;
3. 通过通道输入输出数据块:
1) 有了Channel以后就可以使用其read/write方法进行输入输出了,只不过这两个方法只能用来传送Buffer数据块而已;
!!接下来要介绍的read和write方法都是Channel的对象方法;
2) read:返回值都表示实际读取的多少个字节!可能为0,如果为-1表示已经达到结尾
i. int read(ByteBuffer dst); // 读取一块数据到dst
ii. long read(ByteBuffer[] dsts); // 连续读取dsts.length块数据,由于读取量可能很大,因此返回值是long型的
iii. long read(ByteBuffer[] dsts, int offset, int length); // 连续读取length块数据到offset起始的一系列块中
!!以上三个方法都会更新为指针(当前操作到Channel中数据的哪个字节);
iv. int read(ByteBuffer dst, long position); // 从Channel的position位置开始处读取一块数据到dst中,不改变Channel的位置指针!就是绝对定位读取
3) write:和read是相对应的,方法原型一模一样的!返回值也表示实际写入多少个字节
i. int write(ByteBuffer src); // 将块src输出到通道
ii. long write(ByteBuffer[] srcs); // 连续写入srcs.length块数据
iii. long write(ByteBuffer[] srcs, int offset, int length); // 连续写入length块数据,从srcs的offset开始写
!!以上三个同样会更新Channel的位置指针
iv. int read(ByteBuffer src, long position); // 从Channel的position位置开始写入一个数据块,不改变Channel的位置指针
!!!
可以看到Channel可以直接操作的只能是ByteBuffer,这是因为Java为了保证Channel-Buffer体系的高效,就只允许让Channel传送字节格式层级的数据,因此今后如果要用通道传输其它类型的块(char、long、double等)都需要转化成ByteBuffer才行;
!!!
4. 使用Charset传递CharBuffer:
1) Java程序默认使用Unicode编码,但是大多数操作系统的原生字符集都不是Unicode,有些使用GBK,有些则使用UTF-8,这就使得Java程序处理操作系统中的字符数据时经常会遇到乱码问题,这种问题不仅出现在和操作系统交流字符数据的情形,在其它很多应用中也会遇到字符编码问题,因此Java专门提供了一个工具类Charset对编码进行转换;
2) 首先可以使用Charset的availableCharsets静态方法查看JDK当前支持的所有字符集:
i. 原型:static SortedMap<String,Charset> Charset.availableCharsets();
ii. 使用示例:
SortedMap<String, Charset> map = Charset.availableCharsets();
for (String alias: map.keySet()) {
System.out.println(map.get(alias));
}
!!可以看到结果有N多种;
!!该方法的返回值是一个map,每一个Charset对象都有一个对应的字符集名称,这里只简单介绍以下最常见的4个:
a. GBK:简体中文字符集
b. BIG5:繁体中文字符集
c. UTF-8:8位UCS转换格式(中文编码也包含在其中)
d. ISO-8859-1:ISO拉丁字符表
3) 创建字符集对象:
i. 并不是用构造器创建的,Charset并没有对外公开其构造器,而是直接使用forName方法利用字符集名称来构造一个对象;
ii. 原型:static Charset Charset.forName(String charsetName); // 其实就是一个静态的工厂方法,用于声场你想要的字符集对象
iii. 例如:Charset cs = Charset.forName("GBK"); // 这样就构造了一个GBK字符集对象
4) 利用Charset进行ByteBuffer和CharBuffer之间的转换:
i. 由于计算机底层只能存储二进制字节码,因此将字符存储到节点时要对字符进行编码(字符转换成二进制字节码),而从节点取出字符显示时要进行解码(将二进制字节码转换成字符);
ii. 而字符集Charset则决定了该字符应该编码成怎样的二进制字节码,因此在两者之间转换时必须要确定使用的字符集;
iii. 由于Channel只能直接操作ByteBuffer,而处理字符时就需要在CharBuffer和ByteBuffer之间进行转换,而刚好Charset刚好提供了这样的功能;
!!转换的步骤:
a. 首先要获得Charset对象;
b. 调用Charset的newDecoder、newEncoder获得该字符集下的解码器/编码器:都是对象方法,不是静态方法
*i. CharsetDecoder newDecoder(); // 获得该字符集下的解码器
*ii. CharsetEncoder newEncoder(); // 获得该字符集下的编码器
c. 利用解码器的decode方法将ByteBuffer(字节)解码成CharBuffer(字符),用编码器的encode方法将CharBuffer(字符)编码成ByteBuffer(字节):
*i. CharBuffer CharsetDecoder.decode(ByteBuffer in); // 对象方法
*ii. ByteBuffer CharsetEncoder.encode(CharBuffer in); // 对象方法
5) 其实没有那么麻烦,Charset对象就直接包含encode和decode方法,无需要先获得编码器、解码器然后再进行编码和解码,可以直接用Charset编码和解码,并且Chaset的encode还能将String编码成ByteBuffer,比CharsetEncoder功能还多:也都是Charset的对象方法
i. CharBuffer Charset.decode(ByteBuffer bb);
ii. ByteBuffer Charset.encode(CharBuffer cb);
iii. ByteBuffer Charset.encode(String str);
!!那既然Charset已经包含所有的编解码功能了为什么还要多此一举地提供Encoder和Decoder呢?因为很多大型程序都是参照MVC模型构建的,在MVC中控制器、视图、逻辑层都是严格地分离开的,以此达到程序的健壮性和扩展性,在这种情况下就需要编解码器和编解码功能分开了!因此就提供了编解码器;
6) 使用Charset的defaultCharset方法获取当前操作系统默认的编码集:Charset Charset.defaultCharset(); // 这个很关键,也很常用,可以轻松编写平台无关代码
7) Charset实现了toString方法,因此可以直接打印,打印信息就是字符集的名称,在Windows中打印defaultCharset的结果就是GBK,也就是说Windows中文系统的默认字符集就是GBK;
!!!CharBuffer实现了toString方法,可以直接使用println打印其中的字符串!!
!!顺便提一下,String中也提供了一个byte[] getBytes(String charset)方法,可以将指定的字符串按照指定的编码集编码成二进制字节序列!
5. 示例:
1) 普通的编解码:
public class Test {
public static void main(String[] args) throws IOException {
Charset cn = Charset.forName("GBK");
CharBuffer cb = CharBuffer.allocate(8);
cb.put('你');
cb.put('们');
cb.put('好');
cb.flip(); // 在进行任何非清空的操作之前都必须要归位就绪!
ByteBuffer bb = cn.encode(cb); // 编码
for (int i = 0; i < bb.capacity(); i++) { // 打印每个字节
System.out.print(bb.get(i) + ' ' );
}
System.out.println("\n" + cn.decode(bb)); // 再对字节序列解码
}
}
2) 用一个Buffer循环多次读取文件:
public class Test {
public static void main(String[] args) throws IOException {
try (
FileInputStream fis = new FileInputStream("out.txt");
FileChannel fcin = fis.getChannel()
) {
ByteBuffer bb = ByteBuffer.allocate(16);
while (fcin.read(bb) != -1) {
bb.flip(); // 操作就绪
// 由于通道只能获取到字节码,因此必须解码才能正常显示字符
Charset cn = Charset.forName("UTF-16"); // Windows默认使用UTF-16编码集
System.out.print(cn.decode(bb));
bb.clear(); // 操作之后清空就绪
}
}
}
}