问题:如果一个程序现在需要等待用户输入数据,则可以通过 system.in 来完成。但是这样一来,在使用时就会出现一个问题:
如果用户没有输入信息,则肯定会一直等待用户输入,大量的系统资源就会被败白白浪费。所以在 jdk 1.4 之后引入了 新IO 处理机制。---NIO .
IO的阻塞操作:
我们知道 在IO操作中从键盘解说数据是我们会使用readLine()方法,程序就要停止等待用户输入数据;在网络编程中我们知道在服务器端使用ServletSocket 类的accept() 方法时,服务器会一直处于等待操作,等待客户端连接。这两类操作都属于阻塞操作,因为都会让程序暂停。
新IO并没有在原来的IO基础上开发,而是采用了全新的类和接口,除了原有的功能之外还提供了一下新的特性:
多路选择的非封锁式I/O设施;
支持文件锁和内存映射;
支持正则表达式的模式匹配设施;
字符集编码器和译码器。
在新IO中使用 Buffer 和Channel 支持以上的操作。
一、缓冲区与Buffer
1、基本操作:
在基本IO操作中所有的操作都是直接一流的形式完成的;而在NIO中所有的操作都要使用缓冲区处理,且所有的读写操作都是通过缓冲区完成的。缓冲区(Buffer)是一个线性的、有序的数据集,只能容纳某种特定的事数据类型。
java.nio.Buffer 本身是一个抽象类;
Buffer有四个基本属性:
1、capacity 容量,buffer能够容纳的最大元素数目,在Buffer创建时设定并不能更改
2、limit buffer中有效位置数目
3、position 下一个读或者写的位置
4、mark 用于记忆的标志位,配合reset()使用,初始值未设定,调用mark后将当前position设为值
四者关系:0 <= mark <= position <= limit <= capacity
API:
package java.nio;
public abstract class Buffer {
public final int capacity( )
public final int position( )
public final Buffer position (int newPosition)
public final int limit( )
public final Buffer limit (int newLimit)
public final Buffer mark( )
public final Buffer reset( )
public final Buffer clear( )
public final Buffer flip( )
public final Buffer rewind( )
public final int remaining( )
public final boolean hasRemaining( )
public abstract boolean isReadOnly( );
}
支持链式调用,如:buffer.mark().position(5).reset( );
注意isReadOnly()方法决定了buffer是否可写。
下面以IntBuffer类的操作为实例:
代码:
package com.zsc.day02;
import java.nio.IntBuffer;
public class IntBufferDemo {
public static void main(String[] args) {
//声明int类型缓冲区 ,开辟缓冲区大小为10
IntBuffer buf=IntBuffer.allocate(10);
System.out.println("1.写入数据之前:position="+buf.position()+"," +
"limit="+buf.limit()+",capacity="+buf.capacity());
int temp[]={2,3,4,8,3,4};
buf.put(1); //写入数据
buf.put(temp);
System.out.println("2.写入数据之后:position="+buf.position()+"," +
"limit="+buf.limit()+",capacity="+buf.capacity());
buf.flip(); // 重置缓冲区 即position=0,limit=原本position
System.out.println("3.准备输出数据时:position="+buf.position()+"," +
"limit="+buf.limit()+",capacity="+buf.capacity());
//输出缓冲去内容
while(buf.hasRemaining()){
int x=buf.get();
System.out.print(x+" ");
}
}
}
运行结果
1.写入数据之前:position=0,limit=10,capacity=10
2.写入数据之后:position=7,limit=10,capacity=10
3.准备输出数据时:position=0,limit=7,capacity=10
1 2 3 4 8 3 4
如果只开辟了5个缓冲区大小,但是写入了7个数据时,则会发生以下错误:
Exception in thread "main" java.nio.BufferOverflowException
代码
package com.zsc.day02;
import java.nio.IntBuffer;
public class IntBufferDemo {
public static void main(String[] args) {
//声明int类型缓冲区 ,开辟缓冲区大小为5
IntBuffer buf=IntBuffer.allocate(5);
System.out.println("1.写入数据之前:position="+buf.position()+"," +
"limit="+buf.limit()+",capacity="+buf.capacity());
int temp[]={2,3,4,8,3,4};
buf.put(1); //写入数据
buf.put(temp);
System.out.println("2.写入数据之后:position="+buf.position()+"," +
"limit="+buf.limit()+",capacity="+buf.capacity());
buf.flip(); // 重置缓冲区 即position=0,limit=原本position
System.out.println("3.准备输出数据时:position="+buf.position()+"," +
"limit="+buf.limit()+",capacity="+buf.capacity());
//输出缓冲去内容
while(buf.hasRemaining()){
int x=buf.get();
System.out.print(x+" ");
}
}
}
运行结果
Exception in thread "main" java.nio.BufferOverflowException
at java.nio.HeapIntBuffer.put(HeapIntBuffer.java:183)
at java.nio.IntBuffer.put(IntBuffer.java:832)
at com.zsc.day02.IntBufferDemo.main(IntBufferDemo.java:11)
2、创建子缓冲区:
可以使用各个缓冲区类的slice()方法从一个缓冲区中创建一个新的子缓冲区,子缓冲区与原缓冲区中的部分数据可以共享。
代码:
package com.zsc.day02;
import java.nio.IntBuffer;
import java.nio.IntBuffer;
public class IntBufferDemo02 {
public static void main(String[] args) {
IntBuffer buf=IntBuffer.allocate(10); //声明int类型缓冲区 开辟10个大小缓冲区
IntBuffer sub=null; //定义缓冲区对象
for(int i=0;i<10;i++){
buf.put(2*i+1); //加入10个奇数
}
//创建子缓冲区
buf.position(2); //主缓冲区设置在第三个元素上
buf.limit(6); //主缓冲区limit 为6
sub=buf.slice(); //开辟子缓冲区
for(int i=0;i<sub.capacity();i++){
int temp=sub.get(i); //根据下表去的元素
sub.put(temp-1); //改变子缓冲区内容
}
buf.flip();//重设缓冲区
buf.limit(buf.capacity()); //设置limit
//输出缓冲区内容
while(buf.hasRemaining()){ //只要缓冲区中有数据则输出
int x=buf.get(); // 取出当前内容
System.out.print(x+" ");
}
}
}
运行结果:
1 3 4 6 8 10 13 15 17 19
3、创建只读缓冲区
如果现在要使用到缓冲区中的内容,但又不希望其内容被修改,则可以通过 asReadOnlyBuffer()方法创建一个只读缓冲区,但是创建完毕以后,此缓冲区不能变为可写状态。
代码:
package com.zsc.day02;
import java.nio.IntBuffer;
import java.nio.IntBuffer;
public class IntBufferDemo03 {
public static void main(String[] args) {
IntBuffer buf=IntBuffer.allocate(10); //声明int类型缓冲区 开辟10个大小缓冲区
IntBuffer readOnly=null;
for(int i=0;i<10;i++){
buf.put(2*i+1);
}
//创建子缓冲区
buf.position(2);
buf.limit(6);
readOnly=buf.asReadOnlyBuffer();
readOnly.flip();//重设缓冲区
readOnly.limit(buf.capacity());
//输出缓冲区内容
while(readOnly.hasRemaining()){
int x=readOnly.get();
System.out.print(x+" ");
}
System.out.println();
// readOnly.put(30); //错误不可写
}
}
运行结果:
1 3 5 7 9 11 13 15 17 19
4、直接创建缓冲区:
只缓冲区操作中,只有ByteBuffer 可以创建直接缓冲区,这样java虚拟机将尽最大努力直接对其执行本机的IO操作。
创建方法:public static ByteBuffer allocateDirect(int capacity)
代码:
package com.zsc.day02;
import java.nio.ByteBuffer;
public class ByteBufferDemo {
public static void main(String[] args) {
//声明ByteBuffer 对象
ByteBuffer buf = null ;
//直接开辟缓冲区
buf = ByteBuffer. allocateDirect(10);
byte temp[]={2,3,4,5,6,8,1}; //定义 byte数组
buf.put(temp); //向缓冲区中 写入一组数据
buf.flip(); //重设缓冲区
System.out.println("缓冲区中的内容:");
while(buf.hasRemaining()){
int x=buf.get();
System.out.print(x+" ");
}
}
}
运行结果:
缓冲区中的内容:
2 3 4 5 6 8 1
二、通道
通道(Channel)可以用来读取和写入数据,通道类似于之前的输入输出,但是程序不会直接操作通道,所有的内容都是先读到或写入到缓冲区中,在通过缓冲区中取得或写入的。
通道与传统的流操作不同,传统的流操作分为输入流或输出流,而通道本身是双向的,既可以完成输入也可以完成输出。
Channel 本身是一个接口。
1、FileChannel
FileChannel 是Channel 的子类,可以进行文件的读/写操作。
使用通道进行读写操作:
代码:
package com.zsc.day02;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest {
public static void main(String[] args) throws Exception {
File f1 = new File("d:"+File.separator+"Hello.java");
File f2 = new File("d:"+File.separator+"outHello.java");
FileInputStream fis = null ;//文件输入流
FileOutputStream fos = null ;//文件输出流
fis = new FileInputStream(f1);//实例化输入流
fos = new FileOutputStream(f2);//实例化输出流
FileChannel fin = null ;//声明输入的通道对象
FileChannel fon = null ;//声明输出的通道对象
fin = fis.getChannel();//得到输入的文件通道
fon = fos.getChannel();//得到输出的文件通道
ByteBuffer buf = ByteBuffer.allocate(1024);//开辟缓冲
int temp = 0 ;//声明变量接收的内容
while((temp=fin.read(buf))!=-1){//如果没读到底
buf.flip();//重设缓冲区
fon.write(buf);//输出缓冲区
buf.clear();//清空缓冲区
}
fin.close();//关闭输入流
fon.close();//关闭输出流
fis.close();//关闭输入通道
fos.close();//关闭输出通道
}
}
2、内存映射:
内存映射可以把文件映射到内存中,这样文件内的数据就可以用内存读/写指令来访问,而不是用IputStream 或OutputStream这样的I/O操作类,采用此种方式读取文件的速度是最快的。
java 中访问文件内容的4种方式。
RandomAccessFile ,随机读取数据,此种访问速度最慢;
FileInputStream,文件输入流,使用此种方式速度较慢;
缓冲读取(例如 BufferedReader)此种访问速度较快;
内存映射(MappedByteBuffer),使用此种方式读取速度最快。
代码:
package com.zsc.day02;
import java.io.File;
import java.io.FileInputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelTest {
public static void main(String[] args) throws Exception {
File file = new File("d:"+File.separator+"Hello.java");
FileInputStream fis = null ; //文件输入流
fis = new FileInputStream(file); //实例化输入流
FileChannel fin = null ; //声明输入的通道对象
fin = fis.getChannel(); //得到输入文件通道
MappedByteBuffer mbb = null ; //声明文件的内存映射
//将文件映射到内存中
mbb = fin.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
byte data[] = new byte[(int)file.length()]; //开辟字节数组,接收数据
int foot = 0 ; //定义下标
while(mbb.hasRemaining()){ //判断数否有数据
data[foot++] = mbb.get(); //取出数据
}
System.out.println(new String(data)); //显示输入的数据
fis.close(); //关闭输入流
fin.close(); //关闭输入通道
}
}
运行结果:
public calss Hello{
public static void mian(String args[]){
System.out.println("hello");
}
}
注意:尽管创建内存映射文件非常简单,但是如果使用MappedByteBuffer 写入数据就可能非常危险。因为仅仅是改变数组中的单个元素内容这样的操作简单,就有可能直接修改磁盘上的具体文件,因为修改数据与数据重新保存到磁盘是一样的。
三、文件锁:FileLock
在java 新io中提供了文件所得功能,这样当一个线程将文件锁定以后,其他线程是无法操作此文件的。要想进行文件的锁定,则要使用FileLock 类来完成,此类的对象需要依靠FileChannel 进行实例化。
文件锁定的两种方式:
共享锁:允许多个线程进行文件的读取操作;
独占所:只允许一个线程进行文件的读/写操作
文件锁定:
以下程序在运行时将文件进行独占锁定,这样其他线程在锁定的30庙内是无法对此文件进行读写操作的。
代码:
package com.zsc.day02;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
public class FileLockTest {
public static void main(String[] args) throws Exception {
File file = new File("d:"+File.separator+"Hello.java");
FileOutputStream fos = null ;
fos = new FileOutputStream(file,true);
FileChannel fon = null ;
fon = fos.getChannel();
FileLock fl = fon.tryLock();
if(fl != null){
System.out.println(file.getName()+"文件锁定30秒。");
Thread.sleep(30000);
fl.release();
System.out.println(file.getName()+"文件解除锁定。");
}
fos.close();
fon.close();
}
}
运行结果:
Hello.java文件锁定30秒。
Hello.java文件解除锁定。
四、字符集:Charset
在java语言中所有的信息都是以UNICODE 进行编码的,但是在计算机的世界里面并不是但存在一种编码,而是多个,而且如果对编码处理不当,就有可能引起乱码的产生。在java的新IO包中提供了Charset 类来负责处理编码的问题,该类还包含了创建编码器(CharsetEncoder)和创建解码器(CharsetDecoder)的操作。
1、获取Charset 类的全部编码:
代码:
package com.zsc.day02;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
public class GetAllCharset {
public static void main(String[] args) {
SortedMap<String,Charset> all = null ; //声明 SortedMap 集合
all = Charset.availableCharsets(); //获取全部编码
Iterator<Map.Entry<String,Charset>> iter = null ; //声明 Iterator 对象
iter = all.entrySet().iterator(); //实例化 Iterator
while(iter.hasNext()){ //迭代输出
Map.Entry<String, Charset> me = iter.next(); //取出每一个Map.Entry
System.out.println(me.getKey()+"---->"+me.getValue()); //输出信息
}
}
}
运行结果
Big5---->Big5
Big5-HKSCS---->Big5-HKSCS
EUC-JP---->EUC-JP
EUC-KR---->EUC-KR
GB18030---->GB18030
GB2312---->GB2312
GBK---->GBK
IBM-Thai---->IBM-Thai
IBM00858---->IBM00858
IBM01140---->IBM01140
IBM01141---->IBM01141
IBM01142---->IBM01142
...
2、 解码编码操作:
代码:
package com.zsc.day02;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
public class CharsetEnDeTest {
public static void main(String[] args) throws Exception {
Charset latin1 = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = latin1.newEncoder();
CharsetDecoder decoder = latin1.newDecoder();
//通过CharBuffer 类中的 wrap()方法,将一个字符串变为CharBuffer 类型
CharBuffer cb = CharBuffer.wrap("圣诞快乐");
ByteBuffer buf = encoder.encode(cb);
System.out.println(decoder.decode(buf));
}
}
运行结果:
Exception in thread "main" java.nio.charset.UnmappableCharacterException: Input length = 1
at java.nio.charset.CoderResult.throwException(CoderResult.java:278)
at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:798)
at com.zsc.day02.CharsetEnDeTest.main(CharsetEnDeTest.java:17)
注意:运行以后将以"ISO-8859-1"的编码方式显示中文,这样肯定是无法正常显示的,所以之后的解码操作都会造成乱码,一般CharsetEncoder 和CharsetDecoder 都经常使用在文件的读写上,已实现文件编码的转换功能。
五、Selector
在原来使用的IO和Socket 构造网络服务时,所有的网络服务将使用阻塞的方式进行客户端的连接,而如果使用新IO则可以构造一个非阻塞的网络服务。
下面使用Selector 创建一个非阻塞的服务器,此服务器向客户端返回当前的系统时间。
代码:
package com.zsc.day02;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
public class DateServer {
public static void main(String[] args) throws Exception { //所有异常抛出
int ports[] = {8000,8001,8002,8003,8004,8005,8006,8007,8008}; //定义一组连接端口
Selector selector = Selector.open(); //打开一个选择器
for(int i = 0 ;i<ports.length;i++){ //构造服务器的启动消息
ServerSocketChannel initSer = null ; //声明CharSocketChannel
initSer = ServerSocketChannel.open(); //打开服务器套接字通道
initSer.configureBlocking(false); //服务器配置为非阻塞
ServerSocket initSock = initSer.socket(); //检索与此通道关联的服务器套接字
InetSocketAddress address = null ; //表示监听地址
address = new InetSocketAddress(ports[i]); //实例化绑定地址
initSock.bind(address); //绑定地址
//注册选择器,相当于使用accept() 方法接收
initSer.register(selector, SelectionKey.OP_ACCEPT); //
System.out.println("服务器运行,在"+ports[i]+"端口监听。"); //
}
int keysAdd = 0 ; //
while((keysAdd=selector.select())>0){ //接收一组SelectionKey
Set<SelectionKey> selectedKeys = selector.selectedKeys(); //选择一组键,相应的通道以为IO准备就绪
//取出全部生成的key
Iterator<SelectionKey> iter = selectedKeys.iterator();
while(iter.hasNext()){ //迭代全部的key
SelectionKey key = (SelectionKey)iter.next(); //
if(key.isAcceptable()){ //取出每一个Selectionkey,判断客户端是否已经连接上
ServerSocketChannel server = (ServerSocketChannel)key.channel(); //取得 Channel
SocketChannel client = server.accept(); //接收新连接
client.configureBlocking(false); //设置非阻塞状态
ByteBuffer outBuf = ByteBuffer.allocateDirect(1024); //开辟缓冲去
outBuf.put(("当前时间为:"+new Date()).getBytes()); //向缓冲区设置内容
outBuf.flip(); //重置缓冲区
client.write(outBuf); //输出信息
client.close(); //关闭输出流
}
}
selectedKeys.clear(); //清除全部的key
}
}
}
运行结果:
以上程序运行后,程序将在8000,8001,8002,8003,8004,8005,8006,8007,8008 8 个端口进行监听进行服务器的监听,等待客户端连接,刻度连接后将返回系统的当前时间
在浏览器输入:http://127.0.0.1:8001
将返回:
当前时间为:Sat Dec 24 16:54:57 CST 2011